diff --git a/.travis.yml b/.travis.yml index b6dfa996d..a04d5775a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ android: - tools # The BuildTools version used by your project - - build-tools-23.0.1 + - build-tools-23.0.2 # The SDK version used to compile your project - - android-22 + - android-23 # Additional components # - extra-google-google_play_services @@ -22,5 +22,5 @@ android: # if you need to run emulator(s) during your tests # - sys-img-armeabi-v7a-android-19 # - sys-img-x86-android-17 - +sudo: false script: ./gradlew build check diff --git a/app/app.iml b/app/app.iml deleted file mode 100644 index deda272d3..000000000 --- a/app/app.iml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index bfd294fd6..3a39f403e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,19 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 22 - buildToolsVersion '23.0.1' + compileSdkVersion 23 + buildToolsVersion '23.0.2' defaultConfig { applicationId "fr.gaulupeau.apps.InThePoche" minSdkVersion 8 - targetSdkVersion 22 - versionCode 11 - versionName "1.8" + targetSdkVersion 23 + versionCode 17 + versionName "1.9" + } + + lintOptions { + abortOnError false } buildTypes { @@ -27,16 +31,13 @@ android { } } } - - lintOptions { - abortOnError false - } } dependencies { - compile 'com.android.support:appcompat-v7:22.+' - compile 'com.android.support:recyclerview-v7:22.+' - compile 'com.android.support:design:22.+' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' + compile 'com.android.support:recyclerview-v7:23.1.1' + compile 'com.android.support:design:23.1.1' compile 'de.greenrobot:greendao:2.0.0' compile 'com.facebook.stetho:stetho:1.2.0' compile 'com.squareup.okhttp:okhttp:2.5.0' diff --git a/app/src-gen/fr/gaulupeau/apps/Poche/entity/Article.java b/app/src-gen/fr/gaulupeau/apps/Poche/entity/Article.java index 4c6c65bc2..acf4611c0 100644 --- a/app/src-gen/fr/gaulupeau/apps/Poche/entity/Article.java +++ b/app/src-gen/fr/gaulupeau/apps/Poche/entity/Article.java @@ -12,9 +12,11 @@ public class Article { private String author; private String title; private String url; + private Boolean favorite; private Boolean archive; private Boolean sync; private java.util.Date updateDate; + private Double articleProgress; public Article() { } @@ -23,16 +25,18 @@ public Article(Long id) { this.id = id; } - public Article(Long id, Integer articleId, String content, String author, String title, String url, Boolean archive, Boolean sync, java.util.Date updateDate) { + public Article(Long id, Integer articleId, String content, String author, String title, String url, Boolean favorite, Boolean archive, Boolean sync, java.util.Date updateDate, Double articleProgress) { this.id = id; this.articleId = articleId; this.content = content; this.author = author; this.title = title; this.url = url; + this.favorite = favorite; this.archive = archive; this.sync = sync; this.updateDate = updateDate; + this.articleProgress = articleProgress; } public Long getId() { @@ -83,6 +87,14 @@ public void setUrl(String url) { this.url = url; } + public Boolean getFavorite() { + return favorite; + } + + public void setFavorite(Boolean favorite) { + this.favorite = favorite; + } + public Boolean getArchive() { return archive; } @@ -107,17 +119,12 @@ public void setUpdateDate(java.util.Date updateDate) { this.updateDate = updateDate; } - @Override - public String toString() { - return "Article{" + - "id=" + id + - ", articleId=" + articleId + - ", author='" + author + '\'' + - ", title='" + title + '\'' + - ", url='" + url + '\'' + - ", archive=" + archive + - ", sync=" + sync + - ", updateDate=" + updateDate + - '}'; + public Double getArticleProgress() { + return articleProgress; } + + public void setArticleProgress(Double articleProgress) { + this.articleProgress = articleProgress; + } + } diff --git a/app/src-gen/fr/gaulupeau/apps/Poche/entity/ArticleDao.java b/app/src-gen/fr/gaulupeau/apps/Poche/entity/ArticleDao.java index f03effd14..2e43ab111 100644 --- a/app/src-gen/fr/gaulupeau/apps/Poche/entity/ArticleDao.java +++ b/app/src-gen/fr/gaulupeau/apps/Poche/entity/ArticleDao.java @@ -29,9 +29,11 @@ public static class Properties { public final static Property Author = new Property(3, String.class, "author", false, "author"); public final static Property Title = new Property(4, String.class, "title", false, "title"); public final static Property Url = new Property(5, String.class, "url", false, "url"); - public final static Property Archive = new Property(6, Boolean.class, "archive", false, "archive"); - public final static Property Sync = new Property(7, Boolean.class, "sync", false, "sync"); - public final static Property UpdateDate = new Property(8, java.util.Date.class, "updateDate", false, "update_date"); + public final static Property Favorite = new Property(6, Boolean.class, "favorite", false, "favorite"); + public final static Property Archive = new Property(7, Boolean.class, "archive", false, "archive"); + public final static Property Sync = new Property(8, Boolean.class, "sync", false, "sync"); + public final static Property UpdateDate = new Property(9, java.util.Date.class, "updateDate", false, "update_date"); + public final static Property ArticleProgress = new Property(10, Double.class, "articleProgress", false, "article_progress"); }; @@ -53,9 +55,11 @@ public static void createTable(SQLiteDatabase db, boolean ifNotExists) { "\"author\" TEXT," + // 3: author "\"title\" TEXT," + // 4: title "\"url\" TEXT," + // 5: url - "\"archive\" INTEGER," + // 6: archive - "\"sync\" INTEGER," + // 7: sync - "\"update_date\" INTEGER);"); // 8: updateDate + "\"favorite\" INTEGER," + // 6: favorite + "\"archive\" INTEGER," + // 7: archive + "\"sync\" INTEGER," + // 8: sync + "\"update_date\" INTEGER," + // 9: updateDate + "\"article_progress\" REAL);"); // 10: articleProgress } /** Drops the underlying database table. */ @@ -99,19 +103,29 @@ protected void bindValues(SQLiteStatement stmt, Article entity) { stmt.bindString(6, url); } + Boolean favorite = entity.getFavorite(); + if (favorite != null) { + stmt.bindLong(7, favorite ? 1L: 0L); + } + Boolean archive = entity.getArchive(); if (archive != null) { - stmt.bindLong(7, archive ? 1L: 0L); + stmt.bindLong(8, archive ? 1L: 0L); } Boolean sync = entity.getSync(); if (sync != null) { - stmt.bindLong(8, sync ? 1L: 0L); + stmt.bindLong(9, sync ? 1L: 0L); } java.util.Date updateDate = entity.getUpdateDate(); if (updateDate != null) { - stmt.bindLong(9, updateDate.getTime()); + stmt.bindLong(10, updateDate.getTime()); + } + + Double articleProgress = entity.getArticleProgress(); + if (articleProgress != null) { + stmt.bindDouble(11, articleProgress); } } @@ -131,9 +145,11 @@ public Article readEntity(Cursor cursor, int offset) { cursor.isNull(offset + 3) ? null : cursor.getString(offset + 3), // author cursor.isNull(offset + 4) ? null : cursor.getString(offset + 4), // title cursor.isNull(offset + 5) ? null : cursor.getString(offset + 5), // url - cursor.isNull(offset + 6) ? null : cursor.getShort(offset + 6) != 0, // archive - cursor.isNull(offset + 7) ? null : cursor.getShort(offset + 7) != 0, // sync - cursor.isNull(offset + 8) ? null : new java.util.Date(cursor.getLong(offset + 8)) // updateDate + cursor.isNull(offset + 6) ? null : cursor.getShort(offset + 6) != 0, // favorite + cursor.isNull(offset + 7) ? null : cursor.getShort(offset + 7) != 0, // archive + cursor.isNull(offset + 8) ? null : cursor.getShort(offset + 8) != 0, // sync + cursor.isNull(offset + 9) ? null : new java.util.Date(cursor.getLong(offset + 9)), // updateDate + cursor.isNull(offset + 10) ? null : cursor.getDouble(offset + 10) // articleProgress ); return entity; } @@ -147,9 +163,11 @@ public void readEntity(Cursor cursor, Article entity, int offset) { entity.setAuthor(cursor.isNull(offset + 3) ? null : cursor.getString(offset + 3)); entity.setTitle(cursor.isNull(offset + 4) ? null : cursor.getString(offset + 4)); entity.setUrl(cursor.isNull(offset + 5) ? null : cursor.getString(offset + 5)); - entity.setArchive(cursor.isNull(offset + 6) ? null : cursor.getShort(offset + 6) != 0); - entity.setSync(cursor.isNull(offset + 7) ? null : cursor.getShort(offset + 7) != 0); - entity.setUpdateDate(cursor.isNull(offset + 8) ? null : new java.util.Date(cursor.getLong(offset + 8))); + entity.setFavorite(cursor.isNull(offset + 6) ? null : cursor.getShort(offset + 6) != 0); + entity.setArchive(cursor.isNull(offset + 7) ? null : cursor.getShort(offset + 7) != 0); + entity.setSync(cursor.isNull(offset + 8) ? null : cursor.getShort(offset + 8) != 0); + entity.setUpdateDate(cursor.isNull(offset + 9) ? null : new java.util.Date(cursor.getLong(offset + 9))); + entity.setArticleProgress(cursor.isNull(offset + 10) ? null : cursor.getDouble(offset + 10)); } /** @inheritdoc */ diff --git a/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoMaster.java b/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoMaster.java index fc89b200d..e81a26e79 100644 --- a/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoMaster.java +++ b/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoMaster.java @@ -9,22 +9,25 @@ import de.greenrobot.dao.identityscope.IdentityScopeType; import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.entity.OfflineURLDao; // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. /** - * Master of DAO (schema version 1): knows all DAOs. + * Master of DAO (schema version 2): knows all DAOs. */ public class DaoMaster extends AbstractDaoMaster { - public static final int SCHEMA_VERSION = 1; + public static final int SCHEMA_VERSION = 2; /** Creates underlying database table using DAOs. */ public static void createAllTables(SQLiteDatabase db, boolean ifNotExists) { ArticleDao.createTable(db, ifNotExists); + OfflineURLDao.createTable(db, ifNotExists); } /** Drops underlying database table using DAOs. */ public static void dropAllTables(SQLiteDatabase db, boolean ifExists) { ArticleDao.dropTable(db, ifExists); + OfflineURLDao.dropTable(db, ifExists); } public static abstract class OpenHelper extends SQLiteOpenHelper { @@ -57,6 +60,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { public DaoMaster(SQLiteDatabase db) { super(db, SCHEMA_VERSION); registerDaoClass(ArticleDao.class); + registerDaoClass(OfflineURLDao.class); } public DaoSession newSession() { diff --git a/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoSession.java b/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoSession.java index c05020677..7f7a3926f 100644 --- a/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoSession.java +++ b/app/src-gen/fr/gaulupeau/apps/Poche/entity/DaoSession.java @@ -10,8 +10,10 @@ import de.greenrobot.dao.internal.DaoConfig; import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.OfflineURL; import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.entity.OfflineURLDao; // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. @@ -23,8 +25,10 @@ public class DaoSession extends AbstractDaoSession { private final DaoConfig articleDaoConfig; + private final DaoConfig offlineURLDaoConfig; private final ArticleDao articleDao; + private final OfflineURLDao offlineURLDao; public DaoSession(SQLiteDatabase db, IdentityScopeType type, Map>, DaoConfig> daoConfigMap) { @@ -33,17 +37,27 @@ public DaoSession(SQLiteDatabase db, IdentityScopeType type, Map { + + public static final String TABLENAME = "OFFLINE_URL"; + + /** + * Properties of entity OfflineURL.
+ * Can be used for QueryBuilder and for referencing column names. + */ + public static class Properties { + public final static Property Id = new Property(0, Long.class, "id", true, "_id"); + public final static Property Url = new Property(1, String.class, "url", false, "url"); + }; + + + public OfflineURLDao(DaoConfig config) { + super(config); + } + + public OfflineURLDao(DaoConfig config, DaoSession daoSession) { + super(config, daoSession); + } + + /** Creates the underlying database table. */ + public static void createTable(SQLiteDatabase db, boolean ifNotExists) { + String constraint = ifNotExists? "IF NOT EXISTS ": ""; + db.execSQL("CREATE TABLE " + constraint + "\"OFFLINE_URL\" (" + // + "\"_id\" INTEGER PRIMARY KEY ," + // 0: id + "\"url\" TEXT UNIQUE );"); // 1: url + } + + /** Drops the underlying database table. */ + public static void dropTable(SQLiteDatabase db, boolean ifExists) { + String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"OFFLINE_URL\""; + db.execSQL(sql); + } + + /** @inheritdoc */ + @Override + protected void bindValues(SQLiteStatement stmt, OfflineURL entity) { + stmt.clearBindings(); + + Long id = entity.getId(); + if (id != null) { + stmt.bindLong(1, id); + } + + String url = entity.getUrl(); + if (url != null) { + stmt.bindString(2, url); + } + } + + /** @inheritdoc */ + @Override + public Long readKey(Cursor cursor, int offset) { + return cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0); + } + + /** @inheritdoc */ + @Override + public OfflineURL readEntity(Cursor cursor, int offset) { + OfflineURL entity = new OfflineURL( // + cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id + cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1) // url + ); + return entity; + } + + /** @inheritdoc */ + @Override + public void readEntity(Cursor cursor, OfflineURL entity, int offset) { + entity.setId(cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0)); + entity.setUrl(cursor.isNull(offset + 1) ? null : cursor.getString(offset + 1)); + } + + /** @inheritdoc */ + @Override + protected Long updateKeyAfterInsert(OfflineURL entity, long rowId) { + entity.setId(rowId); + return rowId; + } + + /** @inheritdoc */ + @Override + public Long getKey(OfflineURL entity) { + if(entity != null) { + return entity.getId(); + } else { + return null; + } + } + + /** @inheritdoc */ + @Override + protected boolean isEntityUpdateable() { + return true; + } + +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 50be7a2d1..c0c68d429 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -10,25 +11,16 @@ android:allowBackup="true" android:icon="@drawable/icon" android:label="@string/app_name" - android:theme="@style/app_theme"> - + android:theme="@style/LightTheme"> + - - - - - - - - - - + @@ -46,6 +38,10 @@ + diff --git a/app/src/main/assets/dark.css b/app/src/main/assets/dark.css new file mode 100755 index 000000000..2790017f5 --- /dev/null +++ b/app/src/main/assets/dark.css @@ -0,0 +1,261 @@ +/* ========================================================================== + Sommaire + + 1 = Style Guide + 2 = Layout + 3 = Pictos + 4 = Messages + 5 = Article + 6 = Media queries + + 11.09.2015: Caliandroid: nightview.css is just a variation of the main.css with eye-friendly black background & white foreground for the dark theme + + ========================================================================== */ + +html { + min-height: 100%; +} + +body { + background: #333; +} + +.high-contrast { + background: #000; +} + +.serif-font { + font-family: serif; +} + +/* ========================================================================== + 1 = Style Guide + ========================================================================== */ + +::selection { + color: #FFF; + background: #000; +} + +h1, h2, h3, h4 { + font-family: 'PT Sans', sans-serif; + text-transform: uppercase; + color: #FFF; +} + +p, li { + color: #FFF; +} + +.high-contrast p, +.high-contrast li { + color: #FFF; + background: #000; +} + +a { + color: #FFF; + font-weight: bold; +} + +a:hover, a:focus { + text-decoration: none; +} + +h2:after { + content: ""; + height: 4px; + width: 70px; + background: #000; + display: block; +} + +.links { + padding: 0; + margin: 0; +} + .links li { + list-style: none; + margin: 0; + padding: 0; + } + + +#links { + position: fixed; + top: 0; + width: 10em; + left: 0; + text-align: right; + background: #000; + padding-top: 9.5em; + height: 100%; + box-shadow:inset -4px 0 20px rgba(0,0,0,0.6); + z-index: 10; +} + +#main { + margin-left: 13em; + position: relative; + z-index: 10; + padding-right: 5%; + padding-bottom: 1em; + color: #FFF; +} + + #links a { + display: block; + padding: 0.5em 2em 0.5em 1em; + color: #FFF; + position: relative; + text-transform: uppercase; + text-decoration: none; + font-weight: normal; + font-family: 'PT Sans', sans-serif; + -webkit-transition: all 0.5s ease; + -moz-transition: all 0.5s ease; + -ms-transition: all 0.5s ease; + -o-transition: all 0.5s ease; + transition: all 0.5s ease; + } + + #links a:hover, #links a:focus { + background: #000; + color: #FFF; + } + + #links .current:after { + content: ""; + width: 0; + height: 0; + position: absolute; + border-style: solid; + border-width: 10px; + border-color: transparent #EEE transparent transparent; + right: 0; + top: 50%; + margin-top: -10px; + } + + #links li:last-child { + position: fixed; + bottom: 1em; + width: 10em; + } + + #links li:last-child a:before { + font-size: 1.2em; + position: relative; + top: 2px; + } + + + +/* ========================================================================== + 2 = Layout + ========================================================================== */ + +#content { + margin-top: 1em; + min-height: 30em; +} + +footer { + text-align: right; + position: relative; + bottom: 0; + right: 5em; + color: #fff; + font-size: 0.8em; + font-style: italic; + z-index: 20; +} + +footer a { + color: #fff; + font-weight: normal; +} + +/* ========================================================================== + 5 = Article + ========================================================================== */ + +header.mbm { + text-align: left; +} + +#article { + width: 70%; +/* margin-bottom: 3em; */ + text-align: justify; + word-wrap: break-word; +} + +#article .tags { + margin-bottom: 1em; +} + +#article i { + font-style: normal; +} + +blockquote { + border:1px solid #999; + background: #000; + padding: 1em; + margin: 0; +} + +#article h2, #article h3, #article h4 { + text-transform: none; +} + +#article h2:after { + content: none; +} + +/* ========================================================================== + 6 = Media Queries + ========================================================================== */ + + +@media screen { + body > header { + background: #000; + position: fixed; + top: 0; + width: 100%; + height: 3em; + z-index: 11; + } + #links li:last-child { + position: static; + width: auto; + } + #links li:last-child a:before { + content: none; + } + #links { + display: none; + width: 100%; + height: auto; + padding-top: 3em; + } + footer { + position: static; + margin-right: 3em; + } + #main { + margin-left: 1.5em; + padding-right: 1.5em; + position: static; + } + + #article { + width: 100%; + } + + #article h1 { + font-size: 1.2em; + } +} diff --git a/app/src/main/assets/ic_action_web_site.png b/app/src/main/assets/ic_action_web_site.png new file mode 100644 index 000000000..7f722201d Binary files /dev/null and b/app/src/main/assets/ic_action_web_site.png differ diff --git a/app/src/main/assets/main.css b/app/src/main/assets/main.css index 40f485eb7..cc7a3421a 100755 --- a/app/src/main/assets/main.css +++ b/app/src/main/assets/main.css @@ -18,6 +18,15 @@ body { background: #EEE; } +.high-contrast { + background: #FFF; + font-weight: 600; +} + +.serif-font { + font-family: serif; +} + /* ========================================================================== 1 = Style Guide ========================================================================== */ @@ -36,6 +45,12 @@ p, li { color: #666; } +.high-contrast p, +.high-contrast li { + color: #000; + background: #FFF; +} + a { color: #000; font-weight: bold; @@ -239,5 +254,15 @@ blockquote { #article h1 { font-size: 1.2em; + margin-bottom:0px; } + + .mbm .domain { + margin-top:0px; + margin-left:0px; + } + + #domainimg { + vertical-align:top; + } } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/App.java b/app/src/main/java/fr/gaulupeau/apps/Poche/App.java index f6aa1ee3e..bfd6bb154 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/App.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/App.java @@ -7,12 +7,16 @@ import fr.gaulupeau.apps.InThePoche.BuildConfig; import fr.gaulupeau.apps.Poche.data.DbConnection; import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; /** * @author Victor Häggqvist * @since 10/19/15 */ public class App extends Application { + + private static App instance; + private Settings settings; @Override @@ -23,9 +27,18 @@ public void onCreate() { DbConnection.setContext(this); settings = new Settings(this); + + WallabagConnection.init(this); + + instance = this; } public Settings getSettings() { return settings; } + + public static App getInstance() { + return instance; + } + } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/FeedUpdater.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/FeedUpdater.java deleted file mode 100644 index 14405cdb6..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/data/FeedUpdater.java +++ /dev/null @@ -1,287 +0,0 @@ -package fr.gaulupeau.apps.Poche.data; - -import android.os.AsyncTask; -import android.util.Log; - -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLDecoder; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import de.greenrobot.dao.DaoException; -import fr.gaulupeau.apps.Poche.entity.Article; -import fr.gaulupeau.apps.Poche.entity.ArticleDao; -import fr.gaulupeau.apps.Poche.entity.DaoSession; -import fr.gaulupeau.apps.Poche.util.arrays; - -public class FeedUpdater extends AsyncTask { - - private String wallabagUrl; - private String apiUserId; - private String apiToken; - private FeedUpdaterInterface callback; - private String errorMessage; - - public FeedUpdater(String wallabagUrl, String apiUserId, String apiToken, FeedUpdaterInterface callback) { - this.wallabagUrl = wallabagUrl; - this.apiUserId = apiUserId; - this.apiToken = apiToken; - this.callback = callback; - } - - @Override - protected Void doInBackground(Void... params) { - parseRSS(); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - super.onPostExecute(aVoid); - if (callback == null) - return; - - if (errorMessage == null) { - callback.feedUpdatedFinishedSuccessfully(); - } else { - callback.feedUpdaterFinishedWithError(errorMessage); - } - } - - public void parseRSS() { - URL url; - // Set the url (you will need to change this to your RSS URL - try { - url = new URL(wallabagUrl + "/?feed&type=home&user_id=" + apiUserId + "&token=" + apiToken); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - - OkHttpClient client = WallabagConnection.getClient(); - - String requestUrl = wallabagUrl + "/?feed&type=home&user_id=" + apiUserId + "&token=" + apiToken; - - Request request = new Request.Builder() - .url(requestUrl) - .build(); - - Response response = null; - try { - response = client.newCall(request).execute(); - } catch (IOException e) { - e.printStackTrace(); - } - - - Document document = null; - try { - document = createDocument(response); - } catch (IOException e) { - e.printStackTrace(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } - - parseFeed(document); - } - - private void parseFeed(Document document) { - DaoSession session = DbConnection.getSession(); - ArticleDao articleDao = session.getArticleDao(); - - // This is the root node of each section you want to parse - NodeList itemLst = document.getElementsByTagName("item"); - - // This sets up some arrays to hold the data parsed - arrays.PodcastTitle = new String[itemLst.getLength()]; - arrays.PodcastURL = new String[itemLst.getLength()]; - arrays.PodcastContent = new String[itemLst.getLength()]; - arrays.PodcastMedia = new String[itemLst.getLength()]; - arrays.PodcastDate = new String[itemLst.getLength()]; - arrays.PodcastId = new String[itemLst.getLength()]; - - // Loop through the XML passing the data to the arrays - for (int i = 0; i < itemLst.getLength(); i++) { - - Node item = itemLst.item(i); - if (item.getNodeType() == Node.ELEMENT_NODE) { - Element ielem = (Element) item; - - // This section gets the elements from the XML - // that we want to use you will need to add - // and remove elements that you want / don't want - NodeList title = ielem.getElementsByTagName("title"); - NodeList link = ielem.getElementsByTagName("link"); - NodeList date = ielem.getElementsByTagName("pubDate"); - NodeList content = ielem - .getElementsByTagName("description"); - NodeList source = ielem.getElementsByTagName("source"); - - //NodeList media = ielem - // .getElementsByTagName("media:content"); - - // This is an attribute of an element so I create - // a string to make it easier to use - //String mediaurl = media.item(0).getAttributes() - // .getNamedItem("url").getNodeValue(); - - // This section adds an entry to the arrays with the - // data retrieved from above. I have surrounded each - // with try/catch just incase the element does not - // exist - try { - arrays.PodcastTitle[i] = cleanString(title.item(0).getChildNodes().item(0).getNodeValue()); - } catch (NullPointerException e) { - e.printStackTrace(); - arrays.PodcastTitle[i] = "Echec"; - } - try { - arrays.PodcastDate[i] = date.item(0).getChildNodes().item(0).getNodeValue(); - } catch (NullPointerException e) { - e.printStackTrace(); - arrays.PodcastDate[i] = null; - } - try { - arrays.PodcastURL[i] = link.item(0).getChildNodes() - .item(0).getNodeValue(); - } catch (NullPointerException e) { - e.printStackTrace(); - arrays.PodcastURL[i] = "Echec"; - } - try { - arrays.PodcastContent[i] = content.item(0) - .getChildNodes().item(0).getNodeValue(); - } catch (NullPointerException e) { - e.printStackTrace(); - arrays.PodcastContent[i] = "Echec"; - } - - NamedNodeMap sourceAttrs = source.item(0).getAttributes(); - Node item1 = sourceAttrs.item(0); - String sourceUrl = item1.getNodeValue(); - Map urlQueryParams = getUrlQueryParams(sourceUrl); - String id = urlQueryParams.get("id"); - arrays.PodcastId[i] = id; - - - fr.gaulupeau.apps.Poche.entity.Article article = new Article(null); - article.setTitle(arrays.PodcastTitle[i]); - article.setContent(arrays.PodcastContent[i]); - article.setUrl(arrays.PodcastURL[i]); - article.setArticleId(Integer.parseInt(arrays.PodcastId[i])); - DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z"); - Date date1 = null; - try { - date1 = dateFormat.parse(arrays.PodcastDate[i]); - article.setUpdateDate(date1); - } catch (ParseException e) { - e.printStackTrace(); - } - - article.setArchive(false); - article.setSync(false); - - try { - Article existing = articleDao.queryBuilder() - .where(ArticleDao.Properties.ArticleId.eq(article.getArticleId())) - .build().uniqueOrThrow(); - if (existing == null) { - articleDao.insert(article); - } - } catch (DaoException e) { - articleDao.insert(article); - } -// Log.d("foo", "insert " + article.getArticleId()); - } - } - Log.d("foo", "articles "+ articleDao.count()); - } - - private Document createDocument(Response response) throws IOException, ParserConfigurationException, SAXException { - InputStream inputStream = response.body().byteStream(); - - // Retreive the XML from the URL - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder(); - return documentBuilder.parse(inputStream); - } - - public String cleanString(String s) { - s = s.replace("é", "é"); - s = s.replace("è", "è"); - s = s.replace("ê", "ê"); - s = s.replace("ë", "ë"); - s = s.replace("à", "à"); - s = s.replace("ä", "ä"); - s = s.replace("â", "â"); - s = s.replace("ù", "ù"); - s = s.replace("û", "û"); - s = s.replace("ü", "ü"); - s = s.replace("ô", "ô"); - s = s.replace("ö", "ö"); - s = s.replace("î", "î"); - s = s.replace("ï", "ï"); - s = s.replace("ç", "ç"); - s = s.replace("&", "&"); - - // Replace multiple whitespaces with single space - s = s.replaceAll("\\s+", " "); - s = s.trim(); - - return s; - } - - /** - * Split up URL params a la http://stackoverflow.com/a/13592567/1592572 - */ - private Map getUrlQueryParams(String surl) { - URL url = null; - try { - url = (new URI(surl)).toURL(); - } catch (MalformedURLException e) { - e.printStackTrace(); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - Map query_pairs = new LinkedHashMap(); - String query = url.getQuery(); - String[] pairs = query.split("&"); - for (String pair : pairs) { - int idx = pair.indexOf("="); - try { - query_pairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - } - return query_pairs; - } - -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/FeedUpdaterInterface.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/FeedUpdaterInterface.java deleted file mode 100644 index 06408f3a7..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/data/FeedUpdaterInterface.java +++ /dev/null @@ -1,10 +0,0 @@ -package fr.gaulupeau.apps.Poche.data; - -/** - * Created by kevinmeyer on 13/12/14. - */ - -public interface FeedUpdaterInterface { - void feedUpdaterFinishedWithError(String errorMessage); - void feedUpdatedFinishedSuccessfully(); -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListAdapter.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListAdapter.java index 6fd044e6d..54da7d1be 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListAdapter.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListAdapter.java @@ -4,13 +4,17 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; +import java.net.URL; import java.util.List; import fr.gaulupeau.apps.InThePoche.R; import fr.gaulupeau.apps.Poche.entity.Article; +import static fr.gaulupeau.apps.Poche.data.ListTypes.*; + /** * @author Victor Häggqvist * @since 10/19/15 @@ -19,12 +23,19 @@ public class ListAdapter extends RecyclerView.Adapter { private List
articles; private OnItemClickListener listener; + private int listType = -1; public ListAdapter(List
articles, OnItemClickListener listener) { this.articles = articles; this.listener = listener; } + public ListAdapter(List
articles, OnItemClickListener listener, int listType) { + this.articles = articles; + this.listener = listener; + this.listType = listType; + } + @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false); @@ -45,18 +56,48 @@ public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickL OnItemClickListener listener; TextView title; TextView url; + ImageView favourite; + ImageView read; public ViewHolder(View itemView, OnItemClickListener listener) { super(itemView); this.listener = listener; title = (TextView) itemView.findViewById(R.id.title); url = (TextView) itemView.findViewById(R.id.url); + favourite = (ImageView) itemView.findViewById(R.id.favourite); + read = (ImageView) itemView.findViewById(R.id.read); itemView.setOnClickListener(this); } public void bind(Article article) { + String urlText = article.getUrl(); + try { + URL url = new URL(urlText); + urlText = url.getHost(); + } catch (Exception ignored) {} + title.setText(article.getTitle()); - url.setText(article.getUrl()); + url.setText(urlText); + + boolean showFavourite = false; + boolean showRead = false; + switch(listType) { + case LIST_TYPE_UNREAD: + case LIST_TYPE_ARCHIVED: + showFavourite = article.getFavorite(); + break; + + case LIST_TYPE_FAVORITES: + showRead = article.getArchive(); + break; + + default: // we don't actually use it right now + showFavourite = article.getFavorite(); + showRead = article.getArchive(); + break; + } + favourite.setVisibility(showFavourite ? View.VISIBLE : View.GONE); + read.setVisibility(showRead ? View.VISIBLE : View.GONE); } @Override diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListTypes.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListTypes.java new file mode 100644 index 000000000..723a3f5c6 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/data/ListTypes.java @@ -0,0 +1,9 @@ +package fr.gaulupeau.apps.Poche.data; + +public interface ListTypes { + + int LIST_TYPE_UNREAD = 0; + int LIST_TYPE_FAVORITES = 1; + int LIST_TYPE_ARCHIVED = 2; + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/Settings.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/Settings.java index 36f54568f..fc58121ed 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/data/Settings.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/data/Settings.java @@ -3,8 +3,6 @@ import android.content.Context; import android.content.SharedPreferences; -import fr.gaulupeau.apps.InThePoche.BuildConfig; - /** * @author Victor Häggqvist * @since 10/20/15 @@ -16,9 +14,15 @@ public class Settings { public static final String URL = "pocheUrl"; public static final String USER_ID = "APIUsername"; public static final String TOKEN = "APIToken"; + public static final String ALL_CERTS = "all_certs"; + public static final String FONT_SIZE = "font_size"; + public static final String SERIF_FONT = "serif_font"; + public static final String LIST_LIMIT = "list_limit"; public static final String USERNAME = "username"; public static final String PASSWORD = "password"; - public static final String VERSION_CODE = "version_code"; + public static final String HTTP_AUTH_USERNAME = "http_auth_username"; + public static final String HTTP_AUTH_PASSWORD = "http_auth_password"; + public static final String THEME = "theme"; private SharedPreferences pref; @@ -30,6 +34,14 @@ public void setString(String key, String value) { pref.edit().putString(key, value).commit(); } + public void setInt(String key, int value) { + pref.edit().putInt(key, value).commit(); + } + + public void setBoolean(String key, boolean value) { + pref.edit().putBoolean(key, value).commit(); + } + public String getUrl() { return pref.getString(URL, null); } @@ -38,15 +50,16 @@ public String getKey(String key) { return pref.getString(key, null); } - public void setAppVersion(int versionCode) { - pref.edit().putInt(VERSION_CODE, versionCode).commit(); + public String getString(String key, String defValue) { + return pref.getString(key, defValue); } - public int getPrevAppVersion() { - return pref.getInt(VERSION_CODE, BuildConfig.VERSION_CODE); + public int getInt(String key, int defValue) { + return pref.getInt(key, defValue); } - public boolean hasUpdateChecher() { - return pref.getInt("update_checker", -1) != -1; + public boolean getBoolean(String key, boolean defValue) { + return pref.getBoolean(key, defValue); } + } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagConnection.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagConnection.java deleted file mode 100644 index 2f627fa30..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagConnection.java +++ /dev/null @@ -1,114 +0,0 @@ -package fr.gaulupeau.apps.Poche.data; - -import android.util.Log; - -import com.facebook.stetho.okhttp.StethoInterceptor; -import com.squareup.okhttp.Interceptor; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; - -import java.io.IOException; -import java.net.CookieManager; -import java.net.CookiePolicy; -import java.security.cert.CertificateException; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import fr.gaulupeau.apps.InThePoche.BuildConfig; - -/** - * @author Victor Häggqvist - * @since 10/20/15 - */ -public class WallabagConnection { - - public static OkHttpClient getClient() { - if (Holder.client != null) - return Holder.client; - - OkHttpClient client = new OkHttpClient(); - - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) { - CookieManager cookieManager = new CookieManager(); - cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); - client.setCookieHandler(cookieManager); - } - - try { - final TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return null; - } - } - }; - - // Install the all-trusting trust manager - final SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - // Create an ssl socket factory with our all-trusting manager - final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - - client.setSslSocketFactory(sslSocketFactory); - client.setHostnameVerifier(new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }); - } catch (Exception e) {} - - if (BuildConfig.DEBUG) { - client.interceptors().add(new LoggingInterceptor()); - client.networkInterceptors().add(new StethoInterceptor()); - } - - Holder.client = client; - return client; - } - - - private static class Holder { - private static OkHttpClient client = getClient(); - } - - /** - * OkHttp Logging interceptor - * http://stackoverflow.com/a/30625572/1592572 - */ - static class LoggingInterceptor implements Interceptor { - @Override public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); - - long t1 = System.nanoTime(); - Log.d("OkHttp", String.format("Sending request %s on %s%n%s", - request.url(), chain.connection(), request.headers())); - - Response response = chain.proceed(request); - - long t2 = System.nanoTime(); - Log.d("OkHttp", String.format("Received response for %s in %.1fms, status %d%n%s", - response.request().url(), (t2 - t1) / 1e6d, response.code(), response.headers())); - - - - return response; - } - } -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagService.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagService.java deleted file mode 100644 index ee5d359dc..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagService.java +++ /dev/null @@ -1,85 +0,0 @@ -package fr.gaulupeau.apps.Poche.data; - -import android.util.Log; - -import com.squareup.okhttp.FormEncodingBuilder; -import com.squareup.okhttp.HttpUrl; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.RequestBody; -import com.squareup.okhttp.Response; - -import java.io.IOException; -import java.net.URL; - -/** - * @author Victor Häggqvist - * @since 10/20/15 - */ -public class WallabagService { - - private String endpoint; - private final String username; - private final String password; - private OkHttpClient client; - - public WallabagService(String endpoint, String username, String password) { - this.endpoint = endpoint; - this.username = username; - this.password = password; - client = WallabagConnection.getClient(); - } - - public void addLink(String link) throws IOException { - doLogin(); - - HttpUrl url = HttpUrl.parse(endpoint) - .newBuilder() - .setQueryParameter("plainurl", link) - .build(); - - Request request = new Request.Builder() - .url(url) - .build(); - - client.newCall(request).execute(); - } - - private boolean doLogin() throws IOException { - String url = endpoint+"/?login"; - - RequestBody formBody = new FormEncodingBuilder() - .add("login", username) - .add("password", password) - .build(); - - Request request = new Request.Builder() - .url(url) - .post(formBody) - .build(); - - Response response = client.newCall(request).execute(); - - return response.code() == 200; - } - - public boolean toogleArchive(int articleId) throws IOException { - doLogin(); - - HttpUrl url = HttpUrl.parse(endpoint) - .newBuilder() - .setQueryParameter("action", "toggle_archive") - .setQueryParameter("id", Integer.toString(articleId)) - .build(); - - Request request = new Request.Builder() - .url(url) - .build(); - - Response response = client.newCall(request).execute(); - - Log.d("foo", String.valueOf(response.code())); - return response.code() == 200; - - } -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagSettings.java b/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagSettings.java new file mode 100644 index 000000000..dd0373432 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/data/WallabagSettings.java @@ -0,0 +1,43 @@ +package fr.gaulupeau.apps.Poche.data; + +import static fr.gaulupeau.apps.Poche.data.Settings.URL; +import static fr.gaulupeau.apps.Poche.data.Settings.USER_ID; +import static fr.gaulupeau.apps.Poche.data.Settings.TOKEN; + +/** + * Created by kevinmeyer on 19/01/15. + */ +public class WallabagSettings { + public String wallabagURL; + public String userID; + public String userToken; + + private Settings settings; + + public WallabagSettings(Settings settings) { + this.settings = settings; + } + + public static WallabagSettings settingsFromDisk(Settings settings) { + WallabagSettings wallabagSettings = new WallabagSettings(settings); + wallabagSettings.load(); + return wallabagSettings; + } + + public boolean isValid() { + //TODO Should also check for valid URL and valid userID + return !(wallabagURL.equals("http://")) && !(userID.equals("")) && !(userToken.equals("")); + } + + public void load() { + wallabagURL = settings.getString(URL, "http://"); + userID = settings.getString(USER_ID, ""); + userToken = settings.getString(TOKEN, ""); + } + + public void save() { + settings.setString(URL, wallabagURL); + settings.setString(USER_ID, userID); + settings.setString(TOKEN, userToken); + } +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/WallabagConnection.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/WallabagConnection.java new file mode 100644 index 000000000..91566aade --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/WallabagConnection.java @@ -0,0 +1,162 @@ +package fr.gaulupeau.apps.Poche.network; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import com.facebook.stetho.okhttp.StethoInterceptor; +import com.squareup.okhttp.Credentials; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.security.cert.CertificateException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import fr.gaulupeau.apps.InThePoche.BuildConfig; +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.Settings; + +/** + * @author Victor Häggqvist + * @since 10/20/15 + */ +public class WallabagConnection { + + private static String basicAuthCredentials; + + public static void init(App app) { + Settings settings = app.getSettings(); + + setBasicAuthCredentials( + settings.getString(Settings.HTTP_AUTH_USERNAME, null), + settings.getString(Settings.HTTP_AUTH_PASSWORD, null) + ); + } + + public static void setBasicAuthCredentials(String username, String password) { + if((username == null || username.length() == 0) + && (password == null || password.length() == 0)) { + basicAuthCredentials = null; + } else { + basicAuthCredentials = Credentials.basic(username, password); + } + } + + public static Request.Builder getRequestBuilder() { + Request.Builder b = new Request.Builder(); + + // we use this method instead of OkHttpClient.setAuthenticator() + // to save time on 401 responses + if(basicAuthCredentials != null) b.header("Authorization", basicAuthCredentials); + + return b; + } + + public static boolean isNetworkOnline() { + ConnectivityManager cm = (ConnectivityManager) App.getInstance() + .getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + return networkInfo != null && networkInfo.isConnectedOrConnecting(); + } + + public static OkHttpClient getClient() { + if (Holder.client != null) + return Holder.client; + + OkHttpClient client = createClient(); + + Holder.client = client; + return client; + } + + public static OkHttpClient createClient() { + OkHttpClient client = new OkHttpClient(); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) { + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + client.setCookieHandler(cookieManager); + } + + if(App.getInstance().getSettings().getBoolean(Settings.ALL_CERTS, false)) { + try { + final TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) + throws CertificateException {} + + @Override + public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) + throws CertificateException {} + + @Override + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }; + + // Install the all-trusting trust manager + final SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + // Create an ssl socket factory with our all-trusting manager + final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + client.setSslSocketFactory(sslSocketFactory); + client.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } catch (Exception ignored) {} + } + + if (BuildConfig.DEBUG) { + client.interceptors().add(new LoggingInterceptor()); + client.networkInterceptors().add(new StethoInterceptor()); + } + + return client; + } + + private static class Holder { + private static OkHttpClient client = getClient(); + } + + /** + * OkHttp Logging interceptor + * http://stackoverflow.com/a/30625572/1592572 + */ + static class LoggingInterceptor implements Interceptor { + @Override public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + long t1 = System.nanoTime(); + Log.d("OkHttp", String.format("Sending request %s on %s%n%s", + request.url(), chain.connection(), request.headers())); + + Response response = chain.proceed(request); + + long t2 = System.nanoTime(); + Log.d("OkHttp", String.format("Received response for %s in %.1fms, status %d%n%s", + response.request().url(), (t2 - t1) / 1e6d, response.code(), response.headers())); + + return response; + } + } +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/WallabagService.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/WallabagService.java new file mode 100644 index 000000000..84df63f43 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/WallabagService.java @@ -0,0 +1,284 @@ +package fr.gaulupeau.apps.Poche.network; + +import android.util.Log; + +import com.squareup.okhttp.FormEncodingBuilder; +import com.squareup.okhttp.HttpUrl; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.App; + +import static fr.gaulupeau.apps.Poche.network.WallabagConnection.getRequestBuilder; + +/** + * @author Victor Häggqvist + * @since 10/20/15 + */ +public class WallabagService { + + public static class FeedsCredentials { + public String userID; + public String token; + } + + private static final String TAG = WallabagService.class.getSimpleName(); + + private String endpoint; + private final String username; + private final String password; + private OkHttpClient client; + + public WallabagService(String endpoint, String username, String password) { + this(endpoint, username, password, WallabagConnection.getClient()); + } + + public WallabagService(String endpoint, String username, String password, OkHttpClient client) { + this.endpoint = endpoint; + this.username = username; + this.password = password; + this.client = client; + } + + public FeedsCredentials getCredentials() throws IOException { + Request configRequest = getConfigRequest(); + + String response = executeRequestForResult(configRequest); + if(response == null) return null; + + Pattern pattern = Pattern.compile( + "\"\\?feed&type=home&user_id=(\\d+)&token=([a-zA-Z0-9]+)\"", + Pattern.DOTALL + ); + + Matcher matcher = pattern.matcher(response); + if(!matcher.find()) { + Request generateTokenRequest = getGenerateTokenRequest(); + executeRequest(generateTokenRequest); + + response = executeRequestForResult(configRequest); + if(response == null) return null; + + matcher = pattern.matcher(response); + if(!matcher.find()) return null; + } + + FeedsCredentials credentials = new FeedsCredentials(); + credentials.userID = matcher.group(1); + credentials.token = matcher.group(2); + + return credentials; + } + + public boolean addLink(String link) throws IOException { + HttpUrl url = HttpUrl.parse(endpoint) + .newBuilder() + .setQueryParameter("plainurl", link) + .build(); + + Request request = getRequestBuilder() + .url(url) + .build(); + + return executeRequest(request); + } + + public boolean toggleArchive(int articleId) throws IOException { + HttpUrl url = HttpUrl.parse(endpoint) + .newBuilder() + .setQueryParameter("action", "toggle_archive") + .setQueryParameter("id", Integer.toString(articleId)) + .build(); + + Request request = getRequestBuilder() + .url(url) + .build(); + + return executeRequest(request); + } + + public boolean toggleFavorite(int articleId) throws IOException { + HttpUrl url = HttpUrl.parse(endpoint) + .newBuilder() + .setQueryParameter("action", "toggle_fav") + .setQueryParameter("id", Integer.toString(articleId)) + .build(); + + Request request = getRequestBuilder() + .url(url) + .build(); + + return executeRequest(request); + } + + public boolean deleteArticle(int articleId) throws IOException { + HttpUrl url = HttpUrl.parse(endpoint) + .newBuilder() + .setQueryParameter("action", "delete") + .setQueryParameter("id", Integer.toString(articleId)) + .build(); + + Request request = getRequestBuilder() + .url(url) + .build(); + + return executeRequest(request); + } + + public int testConnection() throws IOException { + // TODO: detect redirects + // TODO: check response codes prior to getting body + + String url = endpoint + "/?view=about"; + Request testRequest = getRequestBuilder().url(url).build(); + + Response response = exec(testRequest); + String body = response.body().string(); + if(!isLoginPage(body)) { + return 1; // it's not even wallabag login page: probably something wrong with the URL + } + + Request loginRequest = getLoginRequest(); + + response = exec(loginRequest); + body = response.body().string(); + + if(isLoginPage(body)) { +// if(body.contains("div class='messages error'")) + return 2; // still login page: probably wrong username or password + } + + response = exec(testRequest); + body = response.body().string(); + + if(isLoginPage(body)) { + return 3; // login page AGAIN: weird, probably authorization problems (maybe cookies expire) + } + + if(!body.contains("href=\"./?logout\"")) { + return 4; // unexpected content: expected to find "log out" button + } + + return 0; + } + + private Request getLoginRequest() { + String url = endpoint + "/?login"; + + RequestBody formBody = new FormEncodingBuilder() + .add("login", username) + .add("password", password) +// .add("longlastingsession", "on") + .build(); + + return getRequestBuilder() + .url(url) + .post(formBody) + .build(); + } + + private Request getConfigRequest() { + HttpUrl url = HttpUrl.parse(endpoint) + .newBuilder() + .setQueryParameter("view", "config") + .build(); + + return getRequestBuilder() + .url(url) + .build(); + } + + private Request getGenerateTokenRequest() { + HttpUrl url = HttpUrl.parse(endpoint) + .newBuilder() + .setQueryParameter("feed", null) + .setQueryParameter("action", "generate") + .build(); + + Log.d(TAG, "getGenerateTokenRequest() url: " + url.toString()); + + return getRequestBuilder() + .url(url) + .build(); + } + + private boolean executeRequest(Request request) throws IOException { + return executeRequest(request, true, true); + } + + private boolean executeRequest(Request request, boolean checkResponse, boolean autoRelogin) throws IOException { + return executeRequestForResult(request, checkResponse, autoRelogin) != null; + } + + private String executeRequestForResult(Request request) throws IOException { + return executeRequestForResult(request, true, true); + } + + private String executeRequestForResult(Request request, boolean checkResponse, boolean autoRelogin) + throws IOException { + Log.d(TAG, "executeRequest() start; autoRelogin: " + autoRelogin); + + Response response = exec(request); + Log.d(TAG, "executeRequest() got response"); + + if(checkResponse) checkResponse(response); + String body = response.body().string(); + if(!isLoginPage(body)) return body; + Log.d(TAG, "executeRequest() response is login page"); + if(!autoRelogin) return null; + + Log.d(TAG, "executeRequest() trying to re-login"); + Response loginResponse = exec(getLoginRequest()); + if(checkResponse) checkResponse(response); + if(isLoginPage(loginResponse.body().string())) { + throw new IOException(App.getInstance() + .getString(R.string.wrongUsernameOrPassword_errorMessage)); + } + + Log.d(TAG, "executeRequest() re-login response is OK; re-executing request"); + response = exec(request); + + if(checkResponse) checkResponse(response); + body = response.body().string(); + return !isLoginPage(body) ? body : null; + } + + private Response exec(Request request) throws IOException { + return client.newCall(request).execute(); + } + + private boolean checkResponse(Response response) throws IOException { + return checkResponse(response, true); + } + + private boolean checkResponse(Response response, boolean throwException) throws IOException { + if(!response.isSuccessful()) { + Log.w(TAG, "checkResponse() response is not OK; response code: " + response.code() + + ", response message: " + response.message()); + if(throwException) + throw new IOException(String.format( + App.getInstance().getString(R.string.unsuccessfulRequest_errorMessage), + response.code(), response.message() + )); + + return false; + } + + return true; + } + + private boolean isLoginPage(String body) throws IOException { + if(body == null || body.length() == 0) return false; + +// "" + return body.contains("
"); // any way to improve? + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/AddLinkTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/AddLinkTask.java new file mode 100644 index 000000000..ed4eb0039 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/AddLinkTask.java @@ -0,0 +1,115 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import java.io.IOException; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; +import fr.gaulupeau.apps.Poche.network.WallabagService; +import fr.gaulupeau.apps.Poche.entity.OfflineURL; +import fr.gaulupeau.apps.Poche.entity.OfflineURLDao; +import fr.gaulupeau.apps.Poche.ui.DialogHelperActivity; + +public class AddLinkTask extends AsyncTask { + + private final String url; + private String errorMessage; + private Context context; + private ProgressBar progressBar; + private ProgressDialog progressDialog; + + private boolean isOffline; + private boolean savedOffline; + + public AddLinkTask(String url, Context context) { + this(url, context, null, null); + } + + public AddLinkTask(String url, Context context, ProgressBar progressBar, + ProgressDialog progressDialog) { + this.url = url; + this.context = context; + this.progressBar = progressBar; + this.progressDialog = progressDialog; + } + + @Override + protected void onPreExecute() { + if(progressBar != null) progressBar.setVisibility(View.VISIBLE); + } + + @Override + protected Boolean doInBackground(Void... params) { + boolean result = false; + + if(WallabagConnection.isNetworkOnline()) { + Settings settings = App.getInstance().getSettings(); + WallabagService service = new WallabagService( + settings.getUrl(), + settings.getKey(Settings.USERNAME), + settings.getKey(Settings.PASSWORD)); + + try { + if(service.addLink(url)) { + result = true; + } else if(context != null) { + errorMessage = context.getString(R.string.addLink_errorMessage); + } + } catch (IOException e) { + errorMessage = e.getMessage(); + e.printStackTrace(); + } + + if(result) return true; + } else { + isOffline = true; + } + + OfflineURLDao urlDao = DbConnection.getSession().getOfflineURLDao(); + OfflineURL offlineURL = new OfflineURL(); + offlineURL.setUrl(url); + urlDao.insert(offlineURL); + + savedOffline = true; + + return false; + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { + if(context != null) { + Toast.makeText(context, R.string.addLink_success_text, Toast.LENGTH_SHORT).show(); + } + } else { + if(context != null) { + if(!isOffline) { + showDialog(context, R.string.d_addLink_failedOnline_title, errorMessage, R.string.ok); + } else if(!savedOffline) { + showDialog(context, R.string.d_addLink_failed_title, errorMessage, R.string.ok); + } + + if(savedOffline) { + Toast.makeText(context, R.string.addLink_savedOffline, Toast.LENGTH_SHORT).show(); + } + } + } + + if(progressBar != null) progressBar.setVisibility(View.GONE); + if(progressDialog != null) progressDialog.dismiss(); + } + + private void showDialog(Context c, int titleId, String message, int buttonId) { + DialogHelperActivity.showAlertDialog(c, c.getString(titleId), message, c.getString(buttonId)); + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/DeleteArticleTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/DeleteArticleTask.java new file mode 100644 index 000000000..2c060d961 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/DeleteArticleTask.java @@ -0,0 +1,56 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.content.Context; +import android.widget.Toast; + +import java.io.IOException; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.ui.DialogHelperActivity; + +public class DeleteArticleTask extends GenericArticleTask { + + public DeleteArticleTask(Context context, int articleId, ArticleDao articleDao, Article article) { + super(context, articleId, articleDao, article); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + articleDao.delete(article); + } + + @Override + protected Boolean doInBackgroundSimple(Void... params) throws IOException { + if(isOffline) return false; + + if(service.deleteArticle(articleId)) return true; + + if(context != null) errorMessage = context.getString(R.string.deleteArticle_errorMessage); + return false; + } + + @Override + protected void onPostExecute(Boolean success) { + super.onPostExecute(success); + + if(success || isOffline) { + if(context != null) { + Toast.makeText(context, R.string.deleteArticle_deleted, Toast.LENGTH_SHORT).show(); + + if(isOffline) { + Toast.makeText(context, R.string.deleteArticle_noInternetConnection, + Toast.LENGTH_SHORT).show(); + } + } + } else { + if(context != null) { + DialogHelperActivity.showConnectionFailureDialog(context, errorMessage); + } + } + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/GenericArticleTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/GenericArticleTask.java new file mode 100644 index 000000000..622be07e8 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/GenericArticleTask.java @@ -0,0 +1,90 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.IOException; + +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; +import fr.gaulupeau.apps.Poche.network.WallabagService; +import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.entity.DaoSession; + +public abstract class GenericArticleTask extends AsyncTask { + + protected static String TAG = ToggleFavoriteTask.class.getSimpleName(); + + protected Context context; + protected int articleId; + protected DaoSession daoSession; + protected ArticleDao articleDao; + protected Article article; + protected WallabagService service; + protected String errorMessage; + protected boolean isOffline; + + public GenericArticleTask(Context context, int articleId, DaoSession daoSession) { + this.context = context; + this.articleId = articleId; + this.daoSession = daoSession; + } + + public GenericArticleTask(Context context, int articleId, ArticleDao articleDao, Article article) { + this.context = context; + this.articleId = articleId; + this.articleDao = articleDao; + this.article = article; + } + + @Override + protected void onPreExecute() { + preparePre(); + } + + @Override + protected Boolean doInBackground(Void... params) { + prepareBG(); + + try { + return doInBackgroundSimple(params); + } catch (IOException e) { + Log.w(TAG, "IOException", e); + errorMessage = e.getMessage(); + return false; + } + } + + protected Boolean doInBackgroundSimple(Void... params) throws IOException { + return false; + } + + protected void preparePre() { + if(articleDao == null) { + if(daoSession == null) daoSession = DbConnection.getSession(); + articleDao = daoSession.getArticleDao(); + } + if(article == null) { + article = articleDao.queryBuilder() + .where(ArticleDao.Properties.Id.eq(articleId)) + .build().unique(); + } + } + + protected void prepareBG() { + if(WallabagConnection.isNetworkOnline()) { + Settings settings = App.getInstance().getSettings(); + service = new WallabagService( + settings.getUrl(), + settings.getKey(Settings.USERNAME), + settings.getKey(Settings.PASSWORD)); + } else { + isOffline = true; + } + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/GetCredentialsTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/GetCredentialsTask.java new file mode 100644 index 000000000..6b5feaf86 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/GetCredentialsTask.java @@ -0,0 +1,65 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.widget.EditText; +import android.widget.Toast; + +import java.io.IOException; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.network.WallabagService; + +public class GetCredentialsTask extends AsyncTask { + + private Context context; + private final String endpoint; + private final String username; + private final String password; + private EditText userId; + private EditText token; + private ProgressDialog progressDialog; + private WallabagService.FeedsCredentials credentials; + + public GetCredentialsTask(Context context, String endpoint, String username, String password, + EditText userId, EditText token, ProgressDialog progressDialog) { + this.context = context; + this.endpoint = endpoint; + this.username = username; + this.password = password; + this.userId = userId; + this.token = token; + this.progressDialog = progressDialog; + } + + @Override + protected Boolean doInBackground(Void... params) { + WallabagService service = new WallabagService(endpoint, username, password); + try { + credentials = service.getCredentials(); + + return credentials != null; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + @Override + protected void onPostExecute(Boolean success) { + if(progressDialog != null) progressDialog.dismiss(); + + if (success) { + userId.setText(credentials.userID); + token.setText(credentials.token); + + if(context != null) + Toast.makeText(context, R.string.getCredentials_success, Toast.LENGTH_SHORT).show(); + } else { + if(context != null) + Toast.makeText(context, R.string.getCredentials_fail, Toast.LENGTH_SHORT).show(); + } + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/TestConnectionTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/TestConnectionTask.java new file mode 100644 index 000000000..fb6432cf1 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/TestConnectionTask.java @@ -0,0 +1,95 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.support.v7.app.AlertDialog; +import android.util.Log; + +import java.io.IOException; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; +import fr.gaulupeau.apps.Poche.network.WallabagService; + +public class TestConnectionTask extends AsyncTask { + + private static final String TAG = TestConnectionTask.class.getSimpleName(); + + private Context context; + private final String endpoint; + private final String username; + private final String password; + private ProgressDialog progressDialog; + private String errorMessage; + + public TestConnectionTask(Context context, String endpoint, String username, String password, + ProgressDialog progressDialog) { + this.context = context; + this.endpoint = endpoint; + this.username = username; + this.password = password; + this.progressDialog = progressDialog; + } + + @Override + protected Integer doInBackground(Void... params) { + WallabagService service = new WallabagService(endpoint, username, password, + WallabagConnection.createClient()); + try { + int result = service.testConnection(); + + Log.d(TAG, "Connection test result code: " + result); + + return result; + } catch (IOException e) { + Log.d(TAG, "Connection test: IOException", e); + errorMessage = e.getMessage(); + return null; + } + } + + @Override + protected void onPostExecute(Integer result) { + if(progressDialog != null) progressDialog.dismiss(); + + if(context == null) return; + + if(result != null && result == 0) { + new AlertDialog.Builder(context) + .setTitle(R.string.d_testConnection_success_title) + .setMessage(R.string.d_connectionTest_success_text) + .setPositiveButton(R.string.ok, null) + .show(); + } else { + if(result != null) { + switch(result) { + case 1: + errorMessage = context.getString(R.string.testConnection_errorMessage1, + result); + break; + + case 2: + errorMessage = context.getString(R.string.testConnection_errorMessage2, + result); + break; + + case 3: + errorMessage = context.getString(R.string.testConnection_errorMessage3, + result); + break; + + default: + errorMessage = context.getString(R.string.testConnection_errorMessage_unknown, + result); + } + } + new AlertDialog.Builder(context) + .setTitle(R.string.d_testConnection_fail_title) + .setMessage(errorMessage) + .setPositiveButton(R.string.ok, null) + .show(); + } + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/ToggleArchiveTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/ToggleArchiveTask.java new file mode 100644 index 000000000..40420766c --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/ToggleArchiveTask.java @@ -0,0 +1,63 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.content.Context; +import android.widget.Toast; + +import java.io.IOException; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.ui.DialogHelperActivity; + +public class ToggleArchiveTask extends GenericArticleTask { + + public ToggleArchiveTask(Context context, int articleId, ArticleDao articleDao, Article article) { + super(context, articleId, articleDao, article); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + article.setArchive(!article.getArchive()); + articleDao.update(article); + } + + @Override + protected Boolean doInBackgroundSimple(Void... params) throws IOException { + if(isOffline) return false; + + if(service.toggleArchive(articleId)) return true; + + if(context != null) errorMessage = context.getString(R.string.toggleArchive_errorMessage); + return false; + } + + @Override + protected void onPostExecute(Boolean success) { + super.onPostExecute(success); + + article.setSync(success); // ? + articleDao.update(article); + + if(success || isOffline) { + if(context != null) { + Toast.makeText(context, article.getArchive() + ? R.string.moved_to_archive_message + : R.string.marked_as_unread_message, + Toast.LENGTH_SHORT).show(); + + if(isOffline) { + Toast.makeText(context, R.string.toggleArchive_noInternetConnection, + Toast.LENGTH_SHORT).show(); + } + } + } else { + if(context != null) { + DialogHelperActivity.showConnectionFailureDialog(context, errorMessage); + } + } + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/ToggleFavoriteTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/ToggleFavoriteTask.java new file mode 100644 index 000000000..1dfdbaf34 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/ToggleFavoriteTask.java @@ -0,0 +1,63 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.content.Context; +import android.widget.Toast; + +import java.io.IOException; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.ui.DialogHelperActivity; + +public class ToggleFavoriteTask extends GenericArticleTask { + + public ToggleFavoriteTask(Context context, int articleId, ArticleDao articleDao, Article article) { + super(context, articleId, articleDao, article); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + + article.setFavorite(!article.getFavorite()); + articleDao.update(article); + } + + @Override + protected Boolean doInBackgroundSimple(Void... params) throws IOException { + if(isOffline) return false; + + if(service.toggleFavorite(articleId)) return true; + + if(context != null) errorMessage = context.getString(R.string.toggleFavorite_errorMessage); + return false; + } + + @Override + protected void onPostExecute(Boolean success) { + super.onPostExecute(success); + + article.setSync(success); // ? + articleDao.update(article); + + if(success || isOffline) { + if(context != null) { + Toast.makeText(context, article.getFavorite() + ? R.string.added_to_favorites_message + : R.string.removed_from_favorites_message, + Toast.LENGTH_SHORT).show(); + + if(isOffline) { + Toast.makeText(context, R.string.toggleFavorite_noInternetConnection, + Toast.LENGTH_SHORT).show(); + } + } + } else { + if(context != null) { + DialogHelperActivity.showConnectionFailureDialog(context, errorMessage); + } + } + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/UpdateFeedTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/UpdateFeedTask.java new file mode 100644 index 000000000..ed55261fa --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/UpdateFeedTask.java @@ -0,0 +1,439 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.database.sqlite.SQLiteDatabase; +import android.os.AsyncTask; +import android.util.Log; +import android.util.Xml; + +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Locale; + +import de.greenrobot.dao.query.WhereCondition; +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; +import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.ArticleDao; + +public class UpdateFeedTask extends AsyncTask { + + public enum UpdateType { Full, Fast } + + public enum FeedType { + Main("home"), Favorite("fav"), Archive("archive"); + + String urlPart; + + FeedType(String urlPart) { + this.urlPart = urlPart; + } + } + + private String baseURL; + private String apiUserId; + private String apiToken; + private CallbackInterface callback; + private FeedType feedType; + private UpdateType updateType; + + private String errorMessage; + + public UpdateFeedTask(String baseURL, String apiUserId, String apiToken, + CallbackInterface callback, + FeedType feedType, UpdateType updateType) { + this.baseURL = baseURL; + this.apiUserId = apiUserId; + this.apiToken = apiToken; + this.callback = callback; + this.feedType = feedType; + this.updateType = updateType; + } + + @Override + protected Void doInBackground(Void... params) { + if(feedType == null && updateType == null) { + updateAllFeeds(); + } else { + if(feedType == null) { + throw new IllegalArgumentException("If updateType is set, feedType must be set too"); + } + if(updateType == null) { + updateType = UpdateType.Full; + } + + update(feedType, updateType); + } + + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + if (callback == null) + return; + + if (errorMessage == null) { + callback.feedUpdateFinishedSuccessfully(); + } else { + callback.feedUpdateFinishedWithError(errorMessage); + } + } + + private void updateAllFeeds() { + ArticleDao articleDao = DbConnection.getSession().getArticleDao(); + SQLiteDatabase db = articleDao.getDatabase(); + + db.beginTransaction(); + try { + articleDao.deleteAll(); + + if(!updateByFeed(articleDao, FeedType.Main, UpdateType.Full, 0)) { + return; + } + + if(!updateByFeed(articleDao, FeedType.Archive, UpdateType.Full, 0)) { + return; + } + + if(!updateByFeed(articleDao, FeedType.Favorite, UpdateType.Fast, 0)) { + return; + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private void update(FeedType feedType, UpdateType updateType) { + ArticleDao articleDao = DbConnection.getSession().getArticleDao(); + SQLiteDatabase db = articleDao.getDatabase(); + + db.beginTransaction(); + try { + + Integer latestID = null; + if(feedType == FeedType.Main || feedType == FeedType.Archive) { + WhereCondition cond = feedType == FeedType.Main + ? ArticleDao.Properties.Archive.notEq(true) + : ArticleDao.Properties.Archive.eq(true); + List
l = articleDao.queryBuilder().where(cond) + .orderDesc(ArticleDao.Properties.ArticleId).limit(1).list(); + + if(!l.isEmpty()) { + latestID = l.get(0).getArticleId(); + } + } + + if(!updateByFeed(articleDao, feedType, updateType, latestID)) { + return; + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + private boolean updateByFeed(ArticleDao articleDao, FeedType feedType, UpdateType updateType, + Integer id) { + InputStream is = null; + try { + // TODO: rewrite? + try { + is = getInputStream(getFeedUrl(feedType)); + } catch (IOException e) { + Log.e("FeedUpdater.updateByF", "IOException on " + feedType, e); + errorMessage = App.getInstance().getString(R.string.feedUpdater_IOException); + return false; + } catch (RuntimeException e) { + Log.e("FeedUpdater.updateByF", "RuntimeException on " + feedType, e); + errorMessage = e.getMessage(); + return false; + } + + try { + processFeed(articleDao, is, feedType, updateType, id); + } catch (IOException e) { + Log.e("FeedUpdater.updateByF", "IOException on " + feedType, e); + errorMessage = App.getInstance() + .getString(R.string.feedUpdater_IOExceptionOnProcessingFeed); + return false; + } catch (XmlPullParserException e) { + Log.e("FeedUpdater.updateByF", "XmlPullParserException on " + feedType, e); + errorMessage = App.getInstance().getString(R.string.feedUpdater_feedProcessingError); + return false; + } + + return true; + } finally { + if(is != null) { + try { + is.close(); + } catch (IOException ignored) {} + } + } + } + + private String getFeedUrl(FeedType feedType) { + return baseURL + "/?feed" + + "&type=" + feedType.urlPart + + "&user_id=" + apiUserId + + "&token=" + apiToken; + } + + private InputStream getInputStream(String urlStr) throws IOException { + Request request = WallabagConnection.getRequestBuilder().url(urlStr).build(); + + Response response = WallabagConnection.getClient().newCall(request).execute(); + + if(response.isSuccessful()) { + return response.body().byteStream(); + } else { + // TODO: fix + throw new RuntimeException(String.format( + App.getInstance().getString(R.string.unsuccessfulRequest_errorMessage), + response.code(), response.message() + )); + } + } + + private void processFeed(ArticleDao articleDao, InputStream is, + FeedType feedType, UpdateType updateType, Integer latestID) + throws XmlPullParserException, IOException { + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(is, null); + + parser.nextTag(); + parser.require(XmlPullParser.START_TAG, null, "rss"); + + goToElement(parser, "channel", true); + parser.next(); + + DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); + + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + if ("item".equals(parser.getName())) { + if(feedType == FeedType.Main || feedType == FeedType.Archive + || (feedType == FeedType.Favorite && updateType == UpdateType.Full)) { + // Main: Full, Fast + // Archive: Full, Fast + // Favorite: Full + + Item item = parseItem(parser); + Integer id = getIDFromURL(item.sourceUrl); + + if(updateType == UpdateType.Fast && latestID != null && id != null + && latestID >= id) break; + + Article article = articleDao.queryBuilder() + .where(ArticleDao.Properties.ArticleId.eq(id)).build().unique(); + + boolean existing = true; + if(article == null) { + article = new Article(null); + existing = false; + } + + article.setTitle(item.title); + article.setContent(item.description); + article.setUrl(item.link); + article.setArticleId(id); + try { + article.setUpdateDate(dateFormat.parse(item.pubDate)); + } catch (ParseException e) { + e.printStackTrace(); + } + if(existing) { + if(feedType == FeedType.Archive) { + article.setArchive(true); + } else if(feedType == FeedType.Favorite) { + article.setFavorite(true); + } + } else { + article.setArchive(feedType == FeedType.Archive); + article.setFavorite(feedType == FeedType.Favorite); + } + + articleDao.insertOrReplace(article); + } else if(feedType == FeedType.Favorite) { + // Favorite: Fast (ONLY applicable if Main and Archive feeds are up to date) + // probably a bit faster then "Favorite: Full" + + Integer id = parseItemID(parser); + if(id == null) continue; + + Article article = articleDao.queryBuilder() + .where(ArticleDao.Properties.ArticleId.eq(id)) + .build().unique(); + + if(article.getFavorite() != null && article.getFavorite()) continue; + + article.setFavorite(true); + + articleDao.update(article); + } + } else { + skipElement(parser); + } + } + } + + private static void goToElement(XmlPullParser parser, String elementName, boolean hierarchically) + throws XmlPullParserException, IOException { + do { + if(parser.getEventType() == XmlPullParser.START_TAG) { + if(elementName.equals(parser.getName())) return; + else if(!hierarchically) skipElement(parser); + } + } while(parser.next() != XmlPullParser.END_DOCUMENT); + } + + private static void skipElement(XmlPullParser parser) + throws XmlPullParserException, IOException { + if(parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException(); + } + + int depth = 1; + while(depth != 0) { + switch (parser.next()) { + case XmlPullParser.END_TAG: + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; + } + } + } + + private static Item parseItem(XmlPullParser parser) throws XmlPullParserException, IOException { + Item item = new Item(); + + while(parser.next() != XmlPullParser.END_TAG) { + if(parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + switch (parser.getName()) { + case "title": + item.title = cleanString(parser.nextText()); + break; + case "source": + String sourceUrl = parser.getAttributeValue(null, "url"); + parser.nextText(); // ignore "empty" element + item.sourceUrl = sourceUrl; + break; + case "link": + item.link = parser.nextText(); + break; + case "pubDate": + item.pubDate = parser.nextText(); + break; + case "description": + item.description = parser.nextText(); + break; + + default: + skipElement(parser); + break; + } + } + + return item; + } + + private static Integer parseItemID(XmlPullParser parser) throws XmlPullParserException, IOException { + while(parser.next() != XmlPullParser.END_TAG) { + if(parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + + switch (parser.getName()) { + case "source": + return getIDFromURL(parser.getAttributeValue(null, "url")); + + default: + skipElement(parser); + break; + } + } + + return null; + } + + private static Integer getIDFromURL(String url) { + if(url != null) { + String marker = "id="; + int index = url.indexOf(marker); + if(index >= 0) { + String idStr = url.substring(index + marker.length()); + try { + return Integer.parseInt(idStr); + } catch (NumberFormatException ignored) {} + } + } + + return null; + } + + private static String cleanString(String s) { + if(s == null || s.length() == 0) return s; + + s = s.replace("é", "é"); + s = s.replace("è", "è"); + s = s.replace("ê", "ê"); + s = s.replace("ë", "ë"); + s = s.replace("à", "à"); + s = s.replace("ä", "ä"); + s = s.replace("â", "â"); + s = s.replace("ù", "ù"); + s = s.replace("û", "û"); + s = s.replace("ü", "ü"); + s = s.replace("ô", "ô"); + s = s.replace("ö", "ö"); + s = s.replace("î", "î"); + s = s.replace("ï", "ï"); + s = s.replace("ç", "ç"); + + s = s.trim(); + + // Replace multiple whitespaces with single space + s = s.replaceAll("\\s+", " "); + + return s; + } + + private static class Item { + String title; + String sourceUrl; + String link; + String pubDate; + String description; + } + + public interface CallbackInterface { + void feedUpdateFinishedWithError(String errorMessage); + void feedUpdateFinishedSuccessfully(); + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/UploadOfflineURLsTask.java b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/UploadOfflineURLsTask.java new file mode 100644 index 000000000..ea5ac348d --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/network/tasks/UploadOfflineURLsTask.java @@ -0,0 +1,132 @@ +package fr.gaulupeau.apps.Poche.network.tasks; + +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.widget.Toast; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.network.WallabagService; +import fr.gaulupeau.apps.Poche.entity.OfflineURL; +import fr.gaulupeau.apps.Poche.entity.OfflineURLDao; +import fr.gaulupeau.apps.Poche.ui.DialogHelperActivity; + +public class UploadOfflineURLsTask extends AsyncTask { + + private String errorMessage; + private Context context; + private ProgressDialog progressDialog; + + private int totalUploaded, totalCount; + + public UploadOfflineURLsTask(Context context, ProgressDialog progressDialog) { + this.context = context; + this.progressDialog = progressDialog; + } + + @Override + protected Boolean doInBackground(Void... params) { + Settings settings = App.getInstance().getSettings(); + WallabagService service = new WallabagService( + settings.getUrl(), + settings.getKey(Settings.USERNAME), + settings.getKey(Settings.PASSWORD)); + + OfflineURLDao offlineURLDao = DbConnection.getSession().getOfflineURLDao(); + List urls = offlineURLDao.queryBuilder() + .orderAsc(OfflineURLDao.Properties.Id).build().list(); + + if(urls.isEmpty()) { + return true; + } + + List uploaded = new ArrayList<>(urls.size()); + + int counter = 0; + int size = urls.size(); + + publishProgress(counter, size); + + boolean result = false; + try { + // add multithreading? + + for(OfflineURL url: urls) { + if(isCancelled()) break; + if(!service.addLink(url.getUrl())) { + if(context != null) { + errorMessage = context.getString(R.string.couldntUploadURL_errorMessage); + } + break; + } + + uploaded.add(url); + + publishProgress(++counter, size); + } + + result = true; + } catch (IOException e) { + errorMessage = e.getMessage(); + e.printStackTrace(); + } + + if(!uploaded.isEmpty()) { + for(OfflineURL url: uploaded) { + offlineURLDao.delete(url); + } + } + + totalUploaded = counter; + totalCount = size; + + return result; + } + + @Override + protected void onProgressUpdate(Integer... progress) { + if(progressDialog != null) { + int current = progress[0]; + if(current == 0) { + int max = progress[1]; + if(progressDialog.getMax() != max) progressDialog.setMax(max); + } + + progressDialog.setProgress(current); + } + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { + if(context != null) { + if(totalCount == 0) { + Toast.makeText(context, R.string.uploadURLs_nothingToUpload, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(context, R.string.uploadURLs_finished, Toast.LENGTH_SHORT).show(); + } + } + } else { + if(context != null) { + DialogHelperActivity.showAlertDialog(context, + context.getString(R.string.d_uploadURLs_title), errorMessage, + context.getString(R.string.ok)); + + Toast.makeText(context, String.format( + context.getString(R.string.uploadURLs_result_text), + totalUploaded, totalCount), + Toast.LENGTH_SHORT).show(); + } + } + + if(progressDialog != null) progressDialog.dismiss(); + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/AddActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/AddActivity.java index 823efc1ce..e3719e570 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/AddActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/AddActivity.java @@ -1,30 +1,20 @@ package fr.gaulupeau.apps.Poche.ui; -import android.content.DialogInterface; -import android.os.AsyncTask; -import android.support.v7.app.AlertDialog; -import android.support.v7.app.AppCompatActivity; import android.os.Bundle; -import android.util.Log; import android.view.View; import android.widget.EditText; import android.widget.ProgressBar; -import android.widget.Toast; - -import java.io.IOException; import fr.gaulupeau.apps.InThePoche.R; -import fr.gaulupeau.apps.Poche.App; -import fr.gaulupeau.apps.Poche.data.Settings; -import fr.gaulupeau.apps.Poche.data.WallabagService; +import fr.gaulupeau.apps.Poche.network.tasks.AddLinkTask; -public class AddActivity extends AppCompatActivity { +public class AddActivity extends BaseActionBarActivity { ProgressBar progressBar; - private Settings settings; @Override protected void onCreate(Bundle savedInstanceState) { + Themes.applyTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_add); @@ -33,75 +23,15 @@ protected void onCreate(Bundle savedInstanceState) { progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setIndeterminate(true); - settings = ((App) getApplication()).getSettings(); - + // TODO: lock button while operation is running + // TODO: cancel operation if activity is hiding findViewById(R.id.add).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - - String endpoint = settings.getUrl(); - String username = settings.getKey(Settings.USERNAME); - String password = settings.getKey(Settings.PASSWORD); - new AddTask(endpoint, username, password, pageUrl.getText().toString()).execute(); - + new AddLinkTask(pageUrl.getText().toString(), getApplicationContext(), + progressBar, null).execute(); } }); - - } - private class AddTask extends AsyncTask { - - private final String endpoint; - private final String username; - private final String password; - private final String url; - private String errorMessage; - - public AddTask(String endpoint, String username,String password, String url) { - - this.endpoint = endpoint; - this.username = username; - this.password = password; - this.url = url; - } - - @Override - protected void onPreExecute() { - progressBar.setVisibility(View.VISIBLE); - } - - @Override - protected Boolean doInBackground(Void... params) { - WallabagService service = new WallabagService(endpoint, username, password); - try { - service.addLink(url); - return true; - } catch (IOException e) { - errorMessage = e.getMessage(); - e.printStackTrace(); - return false; - } - } - - @Override - protected void onPostExecute(Boolean success) { - if (success) { - Toast.makeText(AddActivity.this, "Added", Toast.LENGTH_SHORT).show(); - } else { - new AlertDialog.Builder(AddActivity.this) - .setTitle("Fail") - .setMessage(errorMessage) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }); - } - - progressBar.setVisibility(View.GONE); - AddActivity.this.finish(); - } - } } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ArticlesListActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ArticlesListActivity.java new file mode 100644 index 000000000..285866de2 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ArticlesListActivity.java @@ -0,0 +1,411 @@ +package fr.gaulupeau.apps.Poche.ui; + +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import java.util.Map; +import java.util.WeakHashMap; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.data.WallabagSettings; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; +import fr.gaulupeau.apps.Poche.network.tasks.UpdateFeedTask; +import fr.gaulupeau.apps.Poche.network.tasks.UploadOfflineURLsTask; + +import static fr.gaulupeau.apps.Poche.data.ListTypes.*; + +public class ArticlesListActivity extends AppCompatActivity + implements UpdateFeedTask.CallbackInterface, + ArticlesListFragment.OnFragmentInteractionListener { + + private UpdateFeedTask feedUpdater; + + private Settings settings; + + private ArticlesListPagerAdapter adapter; + private ViewPager viewPager; + + private boolean firstTimeShown = true; + private boolean showEmptyDbDialogOnResume; + private boolean dbIsEmpty; + + private boolean fullUpdateRunning; + private int refreshingFragment = -1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Themes.applyTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_articles_list); + + settings = new Settings(this); + + adapter = new ArticlesListPagerAdapter(getSupportFragmentManager()); + + viewPager = (ViewPager) findViewById(R.id.articles_list_pager); + viewPager.setAdapter(adapter); + + TabLayout tabLayout = (TabLayout) findViewById(R.id.articles_list_tab_layout); + + tabLayout.setTabsFromPagerAdapter(adapter); + tabLayout.setOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(viewPager)); + viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout) { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + + fragmentOnShow(); + } + }); + + viewPager.setCurrentItem(1); + + dbIsEmpty = DbConnection.getSession().getArticleDao().queryBuilder().limit(1).count() == 0; + } + + @Override + protected void onStart() { + super.onStart(); + + if(dbIsEmpty) { + showEmptyDbDialogOnResume = true; + } + } + + @Override + protected void onResume() { + super.onResume(); + + updateLists(); + + if(firstTimeShown) { + firstTimeShown = false; + + WallabagSettings wallabagSettings = WallabagSettings.settingsFromDisk(settings); + if (!wallabagSettings.isValid()) { + AlertDialog.Builder messageBox = new AlertDialog.Builder(ArticlesListActivity.this); + messageBox.setTitle(R.string.firstRun_d_welcome); + messageBox.setMessage(R.string.firstRun_d_configure); + messageBox.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + startActivity(new Intent(getBaseContext(), SettingsActivity.class)); + } + }); + messageBox.setCancelable(false); + messageBox.create().show(); + } + } + + if(showEmptyDbDialogOnResume) { + showEmptyDbDialogOnResume = false; + + WallabagSettings wallabagSettings = WallabagSettings.settingsFromDisk(settings); + if(wallabagSettings.isValid()) { + AlertDialog.Builder messageBox = new AlertDialog.Builder(ArticlesListActivity.this); + messageBox.setTitle(R.string.d_emptyDB_title); + messageBox.setMessage(R.string.d_emptyDB_text); + messageBox.setPositiveButton(R.string.d_emptyDB_answer_updateAll, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + fullUpdate(); + } + }); + messageBox.setNegativeButton(R.string.negative_answer, null); + messageBox.create().show(); + } + } + } + + @Override + protected void onPause() { + super.onPause(); + if (feedUpdater != null) { + feedUpdater.cancel(true); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.option_list, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menuFullUpdate: + fullUpdate(); + return true; + case R.id.menuSettings: + startActivity(new Intent(getBaseContext(), SettingsActivity.class)); + return true; + case R.id.menuBagPage: + startActivity(new Intent(getBaseContext(), AddActivity.class)); + return true; + case R.id.menuOpenRandomArticle: + openRandomArticle(); + return true; + case R.id.menuUploadOfflineURLs: + uploadOfflineURLs(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public void feedUpdateFinishedSuccessfully() { + updateFinished(); + Toast.makeText(this, R.string.txtSyncDone, Toast.LENGTH_SHORT).show(); + } + + @Override + public void feedUpdateFinishedWithError(String errorMessage) { + updateFinished(); + new AlertDialog.Builder(this) + .setMessage(getString(R.string.error_feed) + errorMessage) + .setTitle(R.string.error) + .setPositiveButton(R.string.ok, null) + .setCancelable(false) + .create().show(); + } + + private void updateFinished() { + dbIsEmpty = false; + showEmptyDbDialogOnResume = false; + + if(fullUpdateRunning) { + fullUpdateRunning = false; + updateLists(); + setRefreshingUI(false); + } + if(refreshingFragment != -1) { + if(viewPager.getCurrentItem() == refreshingFragment) { + updateList(refreshingFragment); + setRefreshingUI(false, refreshingFragment); + } + refreshingFragment = -1; + } + } + + @Override + public boolean isFullUpdateRunning() { + return fullUpdateRunning; + } + + @Override + public boolean isCurrentFeedUpdating() { + return refreshingFragment != -1 && refreshingFragment == viewPager.getCurrentItem(); + } + + @Override + public void updateFeed() { + int position = viewPager.getCurrentItem(); + UpdateFeedTask.FeedType feedType = ArticlesListPagerAdapter.getFeedType(position); + UpdateFeedTask.UpdateType updateType = feedType == UpdateFeedTask.FeedType.Main + ? UpdateFeedTask.UpdateType.Fast : UpdateFeedTask.UpdateType.Full; + if(updateFeed(true, feedType, updateType)) { + refreshingFragment = position; + setRefreshingUI(true); + } else { + setRefreshingUI(false); + } + } + + private void fullUpdate() { + if(updateFeed(true, null, null)) { + fullUpdateRunning = true; + setRefreshingUI(true); + } + } + + private boolean updateFeed(boolean showErrors, + UpdateFeedTask.FeedType feedType, + UpdateFeedTask.UpdateType updateType) { + boolean result = false; + + WallabagSettings wallabagSettings = WallabagSettings.settingsFromDisk(settings); + if(fullUpdateRunning || refreshingFragment != -1) { + Toast.makeText(this, R.string.updateFeed_previousUpdateNotFinished, Toast.LENGTH_SHORT) + .show(); + } else if(!wallabagSettings.isValid()) { + if(showErrors) { + Toast.makeText(this, getString(R.string.txtConfigNotSet), Toast.LENGTH_SHORT).show(); + } + } else if(WallabagConnection.isNetworkOnline()) { + feedUpdater = new UpdateFeedTask(wallabagSettings.wallabagURL, + wallabagSettings.userID, wallabagSettings.userToken, this, feedType, updateType); + feedUpdater.execute(); + result = true; + } else { + if(showErrors) { + Toast.makeText(this, getString(R.string.txtNetOffline), Toast.LENGTH_SHORT).show(); + } + } + + return result; + } + + private void updateLists() { + if(adapter != null) { + for(int i = 0; i < ArticlesListPagerAdapter.PAGES.length; i++) { + ArticlesListFragment f = adapter.getCachedFragment(i); + if(f != null) { + f.updateList(); + } + } + } + } + + private void updateList(int position) { + ArticlesListFragment f = getFragment(position); + if(f != null) { + f.updateList(); + } + } + + private void setRefreshingUI(boolean refreshing) { + ArticlesListFragment f = getCurrentFragment(); + if(f != null) { + f.setRefreshingUI(refreshing); + } + } + + private void setRefreshingUI(boolean refreshing, int position) { + ArticlesListFragment f = getFragment(position); + if(f != null) { + f.setRefreshingUI(refreshing); + } + } + + private void openRandomArticle() { + ArticlesListFragment f = getCurrentFragment(); + if(f != null) { + f.openRandomArticle(); + } + } + + private void fragmentOnShow() { + ArticlesListFragment f = getCurrentFragment(); + if(f != null) { + f.onShow(); + } + } + + private ArticlesListFragment getCurrentFragment() { + return adapter == null || viewPager == null ? null + : adapter.getCachedFragment(viewPager.getCurrentItem()); + } + + private ArticlesListFragment getFragment(int position) { + return adapter != null ? adapter.getCachedFragment(position) : null; + } + + private void uploadOfflineURLs() { + ProgressDialog progressDialog = new ProgressDialog(this); + progressDialog.setMessage(getString(R.string.d_uploadingOfflineURLs)); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setCancelable(true); + + final UploadOfflineURLsTask uploadOfflineURLsTask + = new UploadOfflineURLsTask(getApplicationContext(), progressDialog); + + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + uploadOfflineURLsTask.cancel(false); + } + }); + + progressDialog.show(); + uploadOfflineURLsTask.execute(); + } + + public static class ArticlesListPagerAdapter extends FragmentPagerAdapter { + + private static final String TAG = "ArtListPagerAdapter"; + + private static int[] PAGES = { + LIST_TYPE_FAVORITES, + LIST_TYPE_UNREAD, + LIST_TYPE_ARCHIVED + }; + + private Map fragments = new WeakHashMap<>(3); + + public ArticlesListPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + Log.d(TAG, "getItem " + position); + return getFragment(position); + } + + @Override + public int getCount() { + return PAGES.length; + } + + @Override + public CharSequence getPageTitle(int position) { + switch(PAGES[position]) { + case LIST_TYPE_FAVORITES: + return App.getInstance().getString(R.string.feedName_favorites); + case LIST_TYPE_ARCHIVED: + return App.getInstance().getString(R.string.feedName_archived); + default: + return App.getInstance().getString(R.string.feedName_unread); + } + } + + public static UpdateFeedTask.FeedType getFeedType(int position) { + switch(ArticlesListPagerAdapter.PAGES[position]) { + case LIST_TYPE_FAVORITES: + return UpdateFeedTask.FeedType.Favorite; + case LIST_TYPE_ARCHIVED: + return UpdateFeedTask.FeedType.Archive; + default: + return UpdateFeedTask.FeedType.Main; + } + } + + public ArticlesListFragment getCachedFragment(int position) { + return fragments.get(position); + } + + private ArticlesListFragment getFragment(int position) { + Log.d(TAG, "getFragment " + position); + ArticlesListFragment f = fragments.get(position); + if(f == null) { + Log.d(TAG, "creating new instance"); + f = ArticlesListFragment.newInstance(PAGES[position]); + fragments.put(position, f); + } + + return f; + } + + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ArticlesListFragment.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ArticlesListFragment.java new file mode 100644 index 000000000..aae79c619 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ArticlesListFragment.java @@ -0,0 +1,260 @@ +package fr.gaulupeau.apps.Poche.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import de.greenrobot.dao.query.LazyList; +import de.greenrobot.dao.query.QueryBuilder; +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.data.ListAdapter; +import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.entity.Article; +import fr.gaulupeau.apps.Poche.entity.ArticleDao; +import fr.gaulupeau.apps.Poche.entity.DaoSession; + +import static fr.gaulupeau.apps.Poche.data.ListTypes.*; + +public class ArticlesListFragment extends Fragment implements ListAdapter.OnItemClickListener { + + // TODO: remove logging + private static final String TAG = ArticlesListFragment.class.getSimpleName(); + + private static final String LIST_TYPE_PARAM = "list_type"; + + private int listType; + + private OnFragmentInteractionListener host; + + private SwipeRefreshLayout refreshLayout; + + private Settings settings; + private List
mArticles; + private ArticleDao mArticleDao; + private ListAdapter mAdapter; + + private boolean firstShown = true; + + public static ArticlesListFragment newInstance(int listType) { + ArticlesListFragment fragment = new ArticlesListFragment(); + + Bundle args = new Bundle(); + args.putInt(LIST_TYPE_PARAM, listType); + fragment.setArguments(args); + + return fragment; + } + + public ArticlesListFragment() {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + listType = getArguments().getInt(LIST_TYPE_PARAM, LIST_TYPE_UNREAD); + } + + settings = new Settings(getActivity()); + + DaoSession daoSession = DbConnection.getSession(); + mArticleDao = daoSession.getArticleDao(); + + mArticles = new ArrayList<>(); + + mAdapter = new ListAdapter(mArticles, this, listType); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.list, container, false); + + RecyclerView readList = (RecyclerView) view.findViewById(R.id.article_list); + + LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); + readList.setLayoutManager(layoutManager); + + readList.setAdapter(mAdapter); + + refreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_container); + refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + if (host != null) { + host.updateFeed(); + } + } + }); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + Log.d(TAG, "Fragment " + listType + " onResume()"); + + if(firstShown) { + firstShown = false; + + updateList(); + } + + onShow(); + } + + public void onPause() { + super.onPause(); + + Log.d(TAG, "Fragment " + listType + " onPause()"); + + if(refreshLayout != null) { + // http://stackoverflow.com/a/27073879 + refreshLayout.setRefreshing(false); +// refreshLayout.destroyDrawingCache(); + refreshLayout.clearAnimation(); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + Log.d(TAG, "Fragment " + listType + " onAttach()"); + + if(context instanceof OnFragmentInteractionListener) { + host = (OnFragmentInteractionListener) context; + } + } + + @Override + public void onDetach() { + super.onDetach(); + + Log.d(TAG, "Fragment " + listType + " onDetach()"); + + host = null; + } + + @Override + public void onItemClick(int position) { + Article article = mArticles.get(position); + openArticle(article.getId()); + } + + public void onShow() { + Log.d(TAG, "Fragment " + listType + " onShow()"); + + checkRefresh(); + } + + public void updateList() { + List
articles = getArticles(); + + mArticles.clear(); + mArticles.addAll(articles); + mAdapter.notifyDataSetChanged(); + } + + public void openRandomArticle() { + LazyList
articles = getArticlesQueryBuilder(false).listLazyUncached(); + + if(!articles.isEmpty()) { + long id = articles.get(new Random().nextInt(articles.size())).getId(); + articles.close(); + + openArticle(id); + } else { + Toast.makeText(getActivity(), R.string.no_articles, Toast.LENGTH_SHORT).show(); + } + } + + public void setRefreshingUI(boolean refreshing) { + if(refreshLayout != null) { + refreshLayout.setRefreshing(refreshing); + } + } + + private void checkRefresh() { + if(host != null) { + setRefreshingUI(host.isFullUpdateRunning() || host.isCurrentFeedUpdating()); + } + } + + private List
getArticles() { + return getArticlesQueryBuilder(true).list(); + } + + private QueryBuilder
getArticlesQueryBuilder(boolean honorLimit) { + QueryBuilder
qb = mArticleDao.queryBuilder(); + + switch(listType) { + case LIST_TYPE_ARCHIVED: + qb.where(ArticleDao.Properties.Archive.eq(true)); + break; + + case LIST_TYPE_FAVORITES: + qb.where(ArticleDao.Properties.Favorite.eq(true)); + break; + + default: + qb.where(ArticleDao.Properties.Archive.eq(false)); + break; + } + + qb.orderDesc(ArticleDao.Properties.ArticleId); + + if(honorLimit) { + int limit = settings.getInt(Settings.LIST_LIMIT, -1); + if(limit > 0) qb.limit(limit); + } + + return qb; + } + + private void openArticle(long id) { + Activity activity = getActivity(); + if(activity != null) { + Intent intent = new Intent(activity, ReadArticleActivity.class); + intent.putExtra(ReadArticleActivity.EXTRA_ID, id); + + switch(listType) { + case LIST_TYPE_FAVORITES: + intent.putExtra(ReadArticleActivity.EXTRA_LIST_FAVORITES, true); + break; + case LIST_TYPE_ARCHIVED: + intent.putExtra(ReadArticleActivity.EXTRA_LIST_ARCHIVED, true); + break; + default: + intent.putExtra(ReadArticleActivity.EXTRA_LIST_ARCHIVED, false); + break; + } + + startActivity(intent); + } + } + + public interface OnFragmentInteractionListener { + void updateFeed(); + boolean isFullUpdateRunning(); + boolean isCurrentFeedUpdating(); + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BagItProxyActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BagItProxyActivity.java index 49ac14191..630c3fcf5 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BagItProxyActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BagItProxyActivity.java @@ -3,113 +3,66 @@ import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.util.Patterns; -import android.widget.Toast; -import java.io.IOException; import java.util.regex.Matcher; -import fr.gaulupeau.apps.Poche.App; -import fr.gaulupeau.apps.Poche.data.Settings; -import fr.gaulupeau.apps.Poche.data.WallabagService; +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.network.tasks.AddLinkTask; public class BagItProxyActivity extends AppCompatActivity { private static final String TAG = BagItProxyActivity.class.getSimpleName(); - private ProgressDialog mProgressDialog; @Override protected void onCreate(Bundle savedInstanceState) { + Themes.applyProxyTheme(this); super.onCreate(savedInstanceState); Intent intent = getIntent(); Bundle extras = intent.getExtras(); - final String extraText = extras.getString("android.intent.extra.TEXT"); + final String extraText = extras.getString(Intent.EXTRA_TEXT); final String pageUrl; // Parsing string for urls. - Matcher matcher = Patterns.WEB_URL.matcher(extraText); - if (matcher.find()) { + Matcher matcher; + if (extraText != null && extraText.length() > 0 + && (matcher = Patterns.WEB_URL.matcher(extraText)).find()) { pageUrl = matcher.group(); } else { new AlertDialog.Builder(this) - .setTitle("Fail") - .setMessage("Couldn't find a URL in share string:\n" + extraText) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { + .setTitle(R.string.d_bag_fail_title) + .setMessage(getString(R.string.d_bag_fail_text) + extraText) + .setPositiveButton(R.string.ok, null) + .setOnDismissListener(new DialogInterface.OnDismissListener() { @Override - public void onClick(DialogInterface dialog, int which) { - // nop + public void onDismiss(DialogInterface dialog) { + finish(); } - }).create(); + }) + .show(); return; } - Settings settings = ((App) getApplication()).getSettings(); + Log.d(TAG, "Bagging " + pageUrl); - Log.d(TAG, "Baging " + pageUrl); - - mProgressDialog = new ProgressDialog(this); - mProgressDialog.setMessage("Baging page"); - mProgressDialog.setCanceledOnTouchOutside(false); - mProgressDialog.show(); - - new AddTask(pageUrl, settings).execute(); - } - - private class AddTask extends AsyncTask { - - private final String url; - private final String endpoint; - private final String username; - private final String password; - private String errorMessage; - - public AddTask(String url, Settings settings) { - - this.url = url; - endpoint = settings.getUrl(); - username = settings.getKey(Settings.USERNAME); - password = settings.getKey(Settings.PASSWORD); - } - - @Override - protected Boolean doInBackground(Void... params) { - WallabagService service = new WallabagService(endpoint, username, password); - try { - service.addLink(url); - return true; - } catch (IOException e) { - errorMessage = e.getMessage(); - e.printStackTrace(); - return false; + ProgressDialog progressDialog = new ProgressDialog(this); + progressDialog.setMessage(getString(R.string.d_addingToWallabag_text)); + progressDialog.setCanceledOnTouchOutside(false); + progressDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + finish(); } - } + }); + progressDialog.show(); - @Override - protected void onPostExecute(Boolean success) { - if (success) { - Toast.makeText(BagItProxyActivity.this, "Added", Toast.LENGTH_SHORT).show(); - } else { - new AlertDialog.Builder(BagItProxyActivity.this) - .setTitle("Fail") - .setMessage(errorMessage) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - } - }); - } - - Log.d(TAG, "Baging done"); - mProgressDialog.dismiss(); - BagItProxyActivity.this.finish(); - } + new AddLinkTask(pageUrl, this, null, progressDialog).execute(); } + } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BaseActionBarActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BaseActionBarActivity.java index 86915d778..06d5a5cfb 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BaseActionBarActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/BaseActionBarActivity.java @@ -9,29 +9,40 @@ public class BaseActionBarActivity extends AppCompatActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addBackButtonToActionBar(); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addBackButtonToActionBar(); + } - @TargetApi(11) - protected void addBackButtonToActionBar() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - try { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } catch (Exception e) { - // - } - } - } + @TargetApi(11) + protected void addBackButtonToActionBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + try { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } catch (Exception e) { + // + } + } + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - this.finish(); - return true; - } - return super.onOptionsItemSelected(item); - } + @TargetApi(11) + protected void hideBackButtonToActionBar() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + try { + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } catch (Exception e) { + // + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + this.finish(); + return true; + } + return super.onOptionsItemSelected(item); + } } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ConnectionFailAlert.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ConnectionFailAlert.java deleted file mode 100644 index e0b29ceed..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ConnectionFailAlert.java +++ /dev/null @@ -1,24 +0,0 @@ -package fr.gaulupeau.apps.Poche.ui; - -import android.content.Context; -import android.content.DialogInterface; -import android.support.v7.app.AlertDialog; - -/** - * @author Victor Häggqvist - * @since 10/20/15 - */ -public class ConnectionFailAlert { - - public static AlertDialog getDialog(Context context, String message) { - return new AlertDialog.Builder(context) - .setTitle("Connection Failure") - .setMessage(message) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // nop - } - }).create(); - } -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/DialogHelperActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/DialogHelperActivity.java new file mode 100644 index 000000000..6f3f62015 --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/DialogHelperActivity.java @@ -0,0 +1,63 @@ +package fr.gaulupeau.apps.Poche.ui; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; + +import fr.gaulupeau.apps.InThePoche.R; + +public class DialogHelperActivity extends AppCompatActivity { + + private static final String EXTRA_TITLE = "title"; + private static final String EXTRA_MESSAGE = "message"; + private static final String EXTRA_BUTTON_POSITIVE = "button_positive"; + + public static void showConnectionFailureDialog(Context context, String message) { + showAlertDialog(context, context.getString(R.string.d_connectionFail_title), + message, context.getString(R.string.ok)); + } + + public static void showAlertDialog(Context context, String title, String message, + String positiveButtonText) { + Intent intent = new Intent(context, DialogHelperActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if(title != null) intent.putExtra(EXTRA_TITLE, title); + if(message != null) intent.putExtra(EXTRA_MESSAGE, message); + if(positiveButtonText != null) intent.putExtra(EXTRA_BUTTON_POSITIVE, positiveButtonText); + + context.startActivity(intent); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Themes.applyProxyTheme(this); + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + + String title = extras.getString(EXTRA_TITLE); + String message = extras.getString(EXTRA_MESSAGE); + String positiveButton = extras.getString(EXTRA_BUTTON_POSITIVE); + + AlertDialog.Builder b = new AlertDialog.Builder(this); + + if(title != null) b.setTitle(title); + if(message != null) b.setMessage(message); + if(positiveButton != null) b.setPositiveButton(positiveButton, null); + + b.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + finish(); + } + }); + + b.show(); + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ListArticlesActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ListArticlesActivity.java deleted file mode 100644 index 91d68931f..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ListArticlesActivity.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.gaulupeau.apps.Poche.ui; - -import android.content.Intent; -import android.os.Bundle; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; - -import java.util.ArrayList; -import java.util.List; - -import de.greenrobot.dao.query.LazyList; -import fr.gaulupeau.apps.InThePoche.R; -import fr.gaulupeau.apps.Poche.data.DbConnection; -import fr.gaulupeau.apps.Poche.data.ListAdapter; -import fr.gaulupeau.apps.Poche.entity.Article; -import fr.gaulupeau.apps.Poche.entity.ArticleDao; -import fr.gaulupeau.apps.Poche.entity.DaoSession; - -public class ListArticlesActivity extends BaseActionBarActivity implements ListAdapter.OnItemClickListener { - - private RecyclerView readList; - private boolean showAll = false; - - private DaoSession mSession; - private List
mArticles; - private ArticleDao mArticleDao; - private ListAdapter mAdapter; - - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.list); - - readList = (RecyclerView) findViewById(R.id.article_list); - - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - readList.setLayoutManager(layoutManager); - - - mSession = DbConnection.getSession(); - mArticleDao = mSession.getArticleDao(); - - mArticles = new ArrayList<>(); - - mAdapter = new ListAdapter(mArticles, this); - readList.setAdapter(mAdapter); - } - - @Override - protected void onResume() { - super.onResume(); - updateList(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.option_list, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.findItem(R.id.menuShowAll).setTitle(getString(showAll ? R.string.menuShowUnread : R.string.menuShowAll)); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menuShowAll: - showAll = !showAll; - updateList(); - return true; - case R.id.menuWipeDb: - mSession.getArticleDao().deleteAll(); - updateList(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void updateList() { - LazyList
articles = mArticleDao.queryBuilder() - .where(ArticleDao.Properties.Archive.notEq(true)) - .orderDesc(ArticleDao.Properties.UpdateDate) - .limit(50) - .listLazy(); - mArticles.clear(); - mArticles.addAll(articles); - mAdapter.notifyDataSetChanged(); - } - - @Override - public void onItemClick(int position) { - Article article = mArticles.get(position); - Intent intent = new Intent(this, ReadArticleActivity.class); - intent.putExtra(ReadArticleActivity.EXTRA_ID, article.getId()); - startActivity(intent); - } - -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/PocheActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/PocheActivity.java deleted file mode 100644 index a5f091410..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/PocheActivity.java +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Android to Poche - * A simple app to make the full save bookmark to Poche - * web page available via the Share menu on Android tablets - * @author GAULUPEAU Jonathan - * August 2013 - */ - -package fr.gaulupeau.apps.Poche.ui; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.Toast; - -import de.greenrobot.dao.query.LazyList; -import fr.gaulupeau.apps.InThePoche.BuildConfig; -import fr.gaulupeau.apps.InThePoche.R; -import fr.gaulupeau.apps.Poche.App; -import fr.gaulupeau.apps.Poche.data.FeedUpdater; -import fr.gaulupeau.apps.Poche.data.FeedUpdaterInterface; -import fr.gaulupeau.apps.Poche.data.DbConnection; -import fr.gaulupeau.apps.Poche.data.Settings; -import fr.gaulupeau.apps.Poche.entity.ArticleDao; -import fr.gaulupeau.apps.Poche.entity.DaoSession; - - -/** - * Main activity class - */ -@TargetApi(Build.VERSION_CODES.FROYO) -public class PocheActivity extends Activity implements FeedUpdaterInterface { - - private static final String TAG = PocheActivity.class.getSimpleName(); - Button btnGetPost; - Button btnSync; - Button btnSettings; - String action; - - private FeedUpdater feedUpdater; - private Settings settings; - - private String mUrl; - private String mUserId; - private String mToken; - - - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - action = intent.getAction(); - - settings = ((App) getApplication()).getSettings(); - - getSettings(); - - setContentView(R.layout.main); - checkAndHandleAfterUpdate(); - - btnSync = (Button) findViewById(R.id.btnSync); - btnSync.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - updateFeed(); - } - }); - - btnGetPost = (Button) findViewById(R.id.btnArticles); - btnGetPost.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - startActivity(new Intent(getBaseContext(), ListArticlesActivity.class)); - } - }); - - btnSettings = (Button) findViewById(R.id.btnSettings); - btnSettings.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - startActivity(new Intent(getBaseContext(), SettingsActivity.class)); - } - }); - - findViewById(R.id.add).setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - startActivity(new Intent(PocheActivity.this, AddActivity.class)); - } - }); - } - - private void updateFeed() { - if (mUrl == null) { - Toast.makeText(PocheActivity.this, R.string.txtConfigNotSet, Toast.LENGTH_SHORT).show(); - return; - } - - // Run update task - findViewById(R.id.progressBar1).setVisibility(View.VISIBLE); - feedUpdater = new FeedUpdater(mUrl, mUserId, mToken, this); - feedUpdater.execute(); - } - - private void checkAndHandleAfterUpdate() { - if (settings.hasUpdateChecher() && settings.getPrevAppVersion() < BuildConfig.VERSION_CODE) { - new AlertDialog.Builder(this) - .setTitle("App update") - .setMessage("This a breaking update.\n\nMake sure you fill in your Username and Password in settings, otherwise things will be broken.") - .setPositiveButton("OK", null) - .setCancelable(false) - .create().show(); - } else if (settings.getPrevAppVersion() < BuildConfig.VERSION_CODE) { - Log.d(TAG, "Do upgrade stuff if needed"); - } - - settings.setAppVersion(BuildConfig.VERSION_CODE); - } - - private void getSettings() { - mUrl = settings.getKey(Settings.URL); - mUserId = settings.getKey(Settings.USER_ID); - mToken = settings.getKey(Settings.TOKEN); - } - - @Override - protected void onResume() { - super.onResume(); - getSettings(); - updateUnread(); - } - - @Override - protected void onPause() { - super.onPause(); - if (feedUpdater != null) { - feedUpdater.cancel(true); - } - } - - private void updateUnread() { - DaoSession session = DbConnection.getSession(); - ArticleDao articleDao = session.getArticleDao(); - LazyList articles = articleDao.queryBuilder().where(ArticleDao.Properties.Archive.eq(false)).build().listLazy(); - btnGetPost.setText(String.format(getString(R.string.btnGetPost), articles.size())); - } - - @Override - public void feedUpdatedFinishedSuccessfully() { - Toast.makeText(PocheActivity.this, R.string.txtSyncDone, Toast.LENGTH_SHORT).show(); - updateUnread(); - findViewById(R.id.progressBar1).setVisibility(View.GONE); - } - - @Override - public void feedUpdaterFinishedWithError(String errorMessage) { - new AlertDialog.Builder(this) - .setMessage(getString(R.string.error_feed) + errorMessage) - .setTitle(getString(R.string.error)) - .setPositiveButton("OK", null) - .setCancelable(false) - .create().show(); - - updateUnread(); - findViewById(R.id.progressBar1).setVisibility(View.GONE); - } -} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java index d64ab1818..277bc97d9 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/ReadArticleActivity.java @@ -1,110 +1,493 @@ package fr.gaulupeau.apps.Poche.ui; +import android.annotation.TargetApi; +import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.util.Log; +import android.view.GestureDetector; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; +import android.webkit.ConsoleMessage; +import android.webkit.HttpAuthHandler; +import android.webkit.WebChromeClient; import android.webkit.WebView; +import android.webkit.WebViewClient; import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; import android.widget.Toast; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import de.greenrobot.dao.query.QueryBuilder; import fr.gaulupeau.apps.InThePoche.R; import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.network.tasks.AddLinkTask; import fr.gaulupeau.apps.Poche.data.DbConnection; +import fr.gaulupeau.apps.Poche.network.tasks.DeleteArticleTask; import fr.gaulupeau.apps.Poche.data.Settings; -import fr.gaulupeau.apps.Poche.data.WallabagService; +import fr.gaulupeau.apps.Poche.network.tasks.ToggleArchiveTask; +import fr.gaulupeau.apps.Poche.network.tasks.ToggleFavoriteTask; import fr.gaulupeau.apps.Poche.entity.Article; import fr.gaulupeau.apps.Poche.entity.ArticleDao; import fr.gaulupeau.apps.Poche.entity.DaoSession; public class ReadArticleActivity extends BaseActionBarActivity { - public static final String EXTRA_ID = "ReadArticleActivity.id"; + public static final String EXTRA_ID = "ReadArticleActivity.id"; + public static final String EXTRA_LIST_ARCHIVED = "ReadArticleActivity.archived"; + public static final String EXTRA_LIST_FAVORITES = "ReadArticleActivity.favorites"; - WebView webViewContent; + private static final String TAG = ReadArticleActivity.class.getSimpleName(); + + private ScrollView scrollView; + private WebView webViewContent; + private TextView loadingPlaceholder; + private LinearLayout bottomTools; + private View hrBar; private Article mArticle; private ArticleDao mArticleDao; + private Boolean contextFavorites; + private Boolean contextArchived; + + private String titleText; + private String originalUrlText; + private String domainText; + private Double positionToRestore; + private int webViewHeightBeforeUpdate; + private Runnable positionRestorationRunnable; + + private Long previousArticleID; + private Long nextArticleID; + + private boolean loadingFinished; + + private Settings settings; + + private int fontSize = 100; + public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.article); + Themes.applyTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.article); - Intent intent = getIntent(); + Intent intent = getIntent(); long articleId = intent.getLongExtra(EXTRA_ID, -1); + if(intent.hasExtra(EXTRA_LIST_FAVORITES)) { + contextFavorites = intent.getBooleanExtra(EXTRA_LIST_FAVORITES, false); + } + if(intent.hasExtra(EXTRA_LIST_ARCHIVED)) { + contextArchived = intent.getBooleanExtra(EXTRA_LIST_ARCHIVED, false); + } DaoSession session = DbConnection.getSession(); mArticleDao = session.getArticleDao(); - mArticle = mArticleDao.queryBuilder().where(ArticleDao.Properties.Id.eq(articleId)).build().unique(); + mArticle = mArticleDao.queryBuilder() + .where(ArticleDao.Properties.Id.eq(articleId)).build().unique(); - String titleText = mArticle.getTitle(); - String originalUrlText = mArticle.getUrl(); - String originalUrlDesc = originalUrlText; - String htmlContent = mArticle.getContent(); + titleText = mArticle.getTitle(); + originalUrlText = mArticle.getUrl(); + String htmlContent = mArticle.getContent(); + positionToRestore = mArticle.getArticleProgress(); setTitle(titleText); - try { - URL originalUrl = new URL(originalUrlText); - originalUrlDesc = originalUrl.getHost(); - } catch (Exception e) { - // - } - - String htmlHeader = "\n" + - "\t\n" + - "\t\t\n" + - "\t\t\n" + - "\t\t\n" + - "\t\t\n" + - "\t\n" + - "\t\t
\n" + - "\t\t\t\n" + - "\t\t\t\t
\n" + - "\t\t\t\t\t
\n" + - "\t\t\t\t\t\t
\n" + - "\t\t\t\t\t\t\t

" + titleText + "

\n" + - "\t\t\t\t\t\t\t

Open Original: " + originalUrlDesc + "

\n" + - "\t\t\t\t\t\t
\n" + - "\t\t\t\t\t\t
"; - - String htmlFooter = "
\n" + - "\t\t\t\t\t
\n" + - "\t\t\t\t
\n" + - "\t\t\t\n" + - "\t\t
\n" + - ""; - - - webViewContent = (WebView) findViewById(R.id.webViewContent); - webViewContent.loadDataWithBaseURL("file:///android_asset/", htmlHeader + htmlContent + htmlFooter, "text/html", "utf-8", null); - - Button btnMarkRead = (Button) findViewById(R.id.btnMarkRead); - btnMarkRead.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - markAsReadAndClose(); - } - }); - } + settings = App.getInstance().getSettings(); + + String cssName; + boolean highContrast = false; + switch(Themes.getCurrentTheme()) { + case LightContrast: + highContrast = true; + case Light: + default: + cssName = "main"; + break; + + case DarkContrast: + highContrast = true; + case Dark: + cssName = "dark"; + break; + } + + fontSize = settings.getInt(Settings.FONT_SIZE, fontSize); + boolean serifFont = settings.getBoolean(Settings.SERIF_FONT, false); + + if(fontSize < 5) fontSize = 100; // TODO: remove: temp hack for compatibility + + List additionalClasses = new ArrayList<>(1); + if(highContrast) additionalClasses.add("high-contrast"); + if(serifFont) additionalClasses.add("serif-font"); + + String classAttr; + if(!additionalClasses.isEmpty()) { + StringBuilder sb = new StringBuilder(); + + sb.append(" class=\""); + for(String cl: additionalClasses) { + sb.append(cl).append(' '); + } + sb.append('"'); + + classAttr = sb.toString(); + } else { + classAttr = ""; + } + + try { + URL url = new URL(originalUrlText); + domainText = url.getHost(); + } catch (Exception ignored) {} + + String htmlBase; + try { + htmlBase = readRawString(R.raw.webview_htmlbase); + } catch(Exception ignored) { + // TODO: show error message + finish(); + return; + } + + String htmlPage = String.format(htmlBase, cssName, classAttr, titleText, + originalUrlText, domainText, htmlContent); + + final String httpAuthHost = settings.getUrl(); + final String httpAuthUsername = settings.getString(Settings.HTTP_AUTH_USERNAME, null); + final String httpAuthPassword = settings.getString(Settings.HTTP_AUTH_PASSWORD, null); + + scrollView = (ScrollView) findViewById(R.id.scroll); + webViewContent = (WebView) findViewById(R.id.webViewContent); + webViewContent.getSettings().setJavaScriptEnabled(true); // TODO: make optional? + webViewContent.setWebChromeClient(new WebChromeClient() { + @Override + public boolean onConsoleMessage(ConsoleMessage cm) { + Log.d("WebView.onCM", String.format("%s @ %d: %s", cm.message(), + cm.lineNumber(), cm.sourceId())); + return true; + } + }); + webViewContent.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(WebView view, String url) { + // dirty. Looks like there is no good solution + view.postDelayed(new Runnable() { + @Override + public void run() { + if (webViewContent.getHeight() == 0) { + webViewContent.postDelayed(this, 10); + } else { + loadingFinished(); + } + } + }, 10); + + super.onPageFinished(view, url); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + if (!url.equals(originalUrlText)) { + return openUrl(url); + } else { // If we try to open current URL, do not propose to save it, directly open browser + Intent launchBrowserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(launchBrowserIntent); + return true; + } + } + + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, + String host, String realm) { + Log.d(TAG, "onReceivedHttpAuthRequest() host: " + host + ", realm: " + realm); + if(httpAuthHost != null && httpAuthHost.contains(host)) { // TODO: check + Log.d(TAG, "onReceivedHttpAuthRequest() host match"); + handler.proceed(httpAuthUsername, httpAuthPassword); + } else { + Log.d(TAG, "onReceivedHttpAuthRequest() host mismatch"); + super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + } + }); + + webViewContent.loadDataWithBaseURL("file:///android_asset/", htmlPage, + "text/html", "utf-8", null); + + // TODO: remove logging after calibrated + GestureDetector.SimpleOnGestureListener gestureListener + = new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + // note: e1 - previous event, e2 - current event + // velocity* - velocity in pixels per second + + if(e1 == null || e2 == null) return false; + if(e1.getPointerCount() > 1 || e2.getPointerCount() > 1) return false; + +// if(Math.abs(e1.getY() - e2.getY()) > 150) { +// Log.d("FLING", "not a horizontal fling (distance)"); +// return false; // not a horizontal move (distance) +// } + + if(Math.abs(velocityX) < 80) { + Log.d("FLING", "too slow"); + return false; // too slow + } + + if(Math.abs(velocityX / velocityY) < 3) { + Log.d("FLING", "not a horizontal fling"); + return false; // not a horizontal fling + } + + float diff = e1.getX() - e2.getX(); + + if(Math.abs(diff) < 80) { // configurable + Log.d("FLING", "too small distance"); + return false; // too small distance + } + + if(diff > 0) { // right-to-left: next + Log.d("FLING", "right-to-left: next"); + openNextArticle(); + } else { // left-to-right: prev + Log.d("FLING", "left-to-right: prev"); + openPreviousArticle(); + } + return true; + } + }; + + final GestureDetector gestureDetector = new GestureDetector(this, gestureListener); + + webViewContent.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + + loadingPlaceholder = (TextView) findViewById(R.id.tv_loading_article); + bottomTools = (LinearLayout) findViewById(R.id.bottomTools); + hrBar = findViewById(R.id.view1); + + previousArticleID = getAdjacentArticle(true); + nextArticleID = getAdjacentArticle(false); + + Button btnMarkRead = (Button) findViewById(R.id.btnMarkRead); + if(mArticle.getArchive()) { + btnMarkRead.setText(R.string.btnMarkUnread); + } + btnMarkRead.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + markAsReadAndClose(); + } + }); + + ImageButton btnGoPrevious; + ImageButton btnGoNext; + btnGoPrevious = (ImageButton) findViewById(R.id.btnGoPrevious); + if(previousArticleID == null) { + btnGoPrevious.setVisibility(View.GONE); + } + btnGoNext = (ImageButton) findViewById(R.id.btnGoNext); + if(nextArticleID == null) { + btnGoNext.setVisibility(View.GONE); + } + btnGoPrevious.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + openPreviousArticle(); + } + }); + btnGoNext.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + openNextArticle(); + } + }); + } + + private void loadingFinished() { + loadingFinished = true; + + loadingPlaceholder.setVisibility(View.GONE); + bottomTools.setVisibility(View.VISIBLE); + hrBar.setVisibility(View.VISIBLE); + + if(applyDisplaySettings()) { + restorePositionAfterUpdate(); + } else { + restoreReadingPosition(); + } + } + + private boolean applyDisplaySettings() { + prepareToRestorePosition(false); + + boolean changed = false; + + if(fontSize != 100) { + changed = true; + setFontSize(webViewContent, fontSize); + } + + return changed; + } + + private boolean openUrl(final String url) { + if(url == null) return true; + + // TODO: fancy dialog + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View v = getLayoutInflater().inflate(R.layout.dialog_title_url, null); + TextView tv = (TextView) v.findViewById(R.id.tv_dialog_title_url); + tv.setText(url); + builder.setCustomTitle(v); + + builder.setItems( + new CharSequence[]{ + getString(R.string.d_urlAction_openInBrowser), + getString(R.string.d_urlAction_addToWallabag) + }, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case 0: + Intent launchBrowserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(launchBrowserIntent); + break; + case 1: + new AddLinkTask(url, getApplicationContext()).execute(); + break; + } + } + }); + builder.show(); + + return true; + } private void markAsReadAndClose() { - new ToggleArchiveTask(mArticle.getArticleId()).execute(); + new ToggleArchiveTask(getApplicationContext(), + mArticle.getArticleId(), mArticleDao, mArticle).execute(); + finish(); } + private boolean toggleFavorite() { + new ToggleFavoriteTask(getApplicationContext(), + mArticle.getArticleId(), mArticleDao, mArticle).execute(); + + return true; + } + + private boolean shareArticle() { + Intent send = new Intent(Intent.ACTION_SEND); + send.setType("text/plain"); + send.putExtra(Intent.EXTRA_SUBJECT, titleText); + send.putExtra(Intent.EXTRA_TEXT, originalUrlText + getString(R.string.share_text_extra)); + startActivity(Intent.createChooser(send, getString(R.string.share_article_title))); + return true; + } + + private boolean deleteArticle() { + AlertDialog.Builder b = new AlertDialog.Builder(this); + b.setTitle(R.string.d_deleteArticle_title); + b.setMessage(R.string.d_deleteArticle_message); + b.setPositiveButton(R.string.positive_answer, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new DeleteArticleTask(getApplicationContext(), + mArticle.getArticleId(), mArticleDao, mArticle).execute(); + + finish(); + } + }); + b.setNegativeButton(R.string.negative_answer, null); + b.create().show(); + + return true; + } + + private boolean openOriginal() { + Intent launchBrowserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(originalUrlText)); + startActivity(launchBrowserIntent); + + return true; + } + + private void openArticle(Long id) { + Intent intent = new Intent(this, ReadArticleActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.putExtra(ReadArticleActivity.EXTRA_ID, id); + if(contextFavorites != null) intent.putExtra(EXTRA_LIST_FAVORITES, contextFavorites); + if(contextArchived != null) intent.putExtra(EXTRA_LIST_ARCHIVED, contextArchived); + startActivity(intent); + } + + private boolean openPreviousArticle() { + if(previousArticleID != null) { + openArticle(previousArticleID); + return true; + } + + Toast.makeText(this, R.string.noPreviousArticle, Toast.LENGTH_SHORT).show(); + return false; + } + + private boolean openNextArticle() { + if(nextArticleID != null) { + openArticle(nextArticleID); + return true; + } + + Toast.makeText(this, R.string.noNextArticle, Toast.LENGTH_SHORT).show(); + return false; + } + @Override public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.option_article, menu); + + boolean unread = mArticle.getArchive() != null && !mArticle.getArchive(); + + MenuItem markReadItem = menu.findItem(R.id.menuArticleMarkAsRead); + markReadItem.setTitle(unread ? R.string.btnMarkRead : R.string.btnMarkUnread); + + boolean favorite = mArticle.getFavorite() != null && mArticle.getFavorite(); + + MenuItem toggleFavoriteItem = menu.findItem(R.id.menuArticleToggleFavorite); + toggleFavoriteItem.setTitle( + favorite ? R.string.remove_from_favorites : R.string.add_to_favorites); + // TODO: replace star icon + toggleFavoriteItem.setIcon(getIcon(favorite + ? R.drawable.abc_btn_rating_star_on_mtrl_alpha + : R.drawable.abc_btn_rating_star_off_mtrl_alpha, null) + ); + return true; } @@ -114,52 +497,201 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.menuArticleMarkAsRead: markAsReadAndClose(); return true; + case R.id.menuArticleToggleFavorite: + toggleFavorite(); + invalidateOptionsMenu(); + return true; + case R.id.menuShare: + return shareArticle(); + case R.id.menuDelete: + return deleteArticle(); + case R.id.menuOpenOriginal: + return openOriginal(); + case R.id.menuIncreaseFontSize: + changeFontSize(true); + return true; + case R.id.menuDecreaseFontSize: + changeFontSize(false); + return true; default: return super.onOptionsItemSelected(item); } } - private class ToggleArchiveTask extends AsyncTask { - private int articleId; - private WallabagService service; - private String errorMessage; + @Override + public void onStop() { + if(loadingFinished && mArticle != null) { + mArticle.setArticleProgress(getReadingPosition()); + mArticleDao.update(mArticle); + } + + super.onStop(); + } + + private double getReadingPosition() { + String t = "ReadArticle.getPos"; + + int yOffset = scrollView.getScrollY(); + int viewHeight = scrollView.getHeight(); + int totalHeight = scrollView.getChildAt(0).getHeight(); + // id/btnMarkRead height; not necessary; insignificantly increases accuracy +// int appendixHeight = ((LinearLayout)view.getChildAt(0)).getChildAt(1).getHeight(); + Log.d(t, "yOffset: " + yOffset + ", viewHeight: " + viewHeight + ", totalHeight: " + totalHeight); + +// totalHeight -= appendixHeight; + totalHeight -= viewHeight; + + double progress = totalHeight >= 0 ? yOffset * 1. / totalHeight : 0; + Log.d(t, "progress: " + progress); + + return progress; + } + + private void restoreReadingPosition() { + String t = "ReadArticle.restorePos"; + + Log.d(t, "positionToRestore: " + positionToRestore); + if(positionToRestore != null) { + int viewHeight = scrollView.getHeight(); +// int appendixHeight = ((LinearLayout)view.getChildAt(0)).getChildAt(1).getHeight(); + int totalHeight = scrollView.getChildAt(0).getHeight(); + Log.d(t, "viewHeight: " + viewHeight + ", totalHeight: " + totalHeight); + +// totalHeight -= appendixHeight; + totalHeight -= viewHeight; + + int yOffset = totalHeight > 0 ? ((int)Math.round(positionToRestore * totalHeight)) : 0; + Log.d(t, "yOffset: " + yOffset); - public ToggleArchiveTask(int articleId) { - this.articleId = articleId; + scrollView.scrollTo(scrollView.getScrollX(), yOffset); } + } - @Override - protected void onPreExecute() { - mArticle.setArchive(!mArticle.getArchive()); - mArticleDao.update(mArticle); - Settings settings = ((App) getApplication()).getSettings(); - service = new WallabagService( - settings.getUrl(), - settings.getKey(Settings.USERNAME), - settings.getKey(Settings.PASSWORD)); - } - - @Override - protected Boolean doInBackground(Void... params) { - try { - return service.toogleArchive(articleId); - } catch (IOException e) { - errorMessage = e.getMessage(); - e.printStackTrace(); - return false; + private Long getAdjacentArticle(boolean previous) { + QueryBuilder
qb = mArticleDao.queryBuilder(); + + if(previous) qb.where(ArticleDao.Properties.ArticleId.gt(mArticle.getArticleId())); + else qb.where(ArticleDao.Properties.ArticleId.lt(mArticle.getArticleId())); + + if(contextFavorites != null) qb.where(ArticleDao.Properties.Favorite.eq(contextFavorites)); + if(contextArchived != null) qb.where(ArticleDao.Properties.Archive.eq(contextArchived)); + + if(previous) qb.orderAsc(ArticleDao.Properties.ArticleId); + else qb.orderDesc(ArticleDao.Properties.ArticleId); + + List
l = qb.limit(1).list(); + if(!l.isEmpty()) { + return l.get(0).getId(); + } + + return null; + } + + private String readRawString(int id) throws IOException { + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(getResources().openRawResource(id))); + + StringBuilder sb = new StringBuilder(); + String s; + while((s = reader.readLine()) != null) { + sb.append(s).append('\n'); + } + + return sb.toString(); + } finally { + if(reader != null) { + reader.close(); } } + } + + private void prepareToRestorePosition(boolean savePosition) { + if(savePosition) positionToRestore = getReadingPosition(); + + webViewHeightBeforeUpdate = webViewContent.getHeight(); + } - @Override - protected void onPostExecute(Boolean success) { - if (success) { - mArticle.setSync(true); - mArticleDao.update(mArticle); - } else { - ConnectionFailAlert.getDialog(ReadArticleActivity.this, errorMessage).show(); + private void restorePositionAfterUpdate() { + cancelPositionRestoration(); + + webViewContent.postDelayed(positionRestorationRunnable = new Runnable() { + int counter; + + @Override + public void run() { + if(webViewContent.getHeight() == webViewHeightBeforeUpdate) { + if(++counter > 1000) { + Log.d(TAG, "restorePositionAfterUpdate() giving up"); + return; + } + + Log.v(TAG, "restorePositionAfterUpdate() scheduling another postDelay"); + webViewContent.postDelayed(this, 10); + } else { + Log.d(TAG, "restorePositionAfterUpdate() restoring position"); + restoreReadingPosition(); + } } - Toast.makeText(ReadArticleActivity.this, "Archived", Toast.LENGTH_SHORT).show(); + }, 10); + } + + private void cancelPositionRestoration() { + if(positionRestorationRunnable != null) { + Log.d(TAG, "cancelPositionRestoration() trying to cancel previous task"); + webViewContent.removeCallbacks(positionRestorationRunnable); + positionRestorationRunnable = null; } } + private void changeFontSize(boolean increase) { + prepareToRestorePosition(true); + + int step = 5; + fontSize += step * (increase ? 1 : -1); + if(!increase && fontSize < 5) fontSize = 5; + + setFontSize(webViewContent, fontSize); + + settings.setInt(Settings.FONT_SIZE, fontSize); + + restorePositionAfterUpdate(); + } + + private void setFontSize(WebView view, int size) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + setFontSizeNew(view, size); + } else { + setFontSizeOld(view, size); + } + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void setFontSizeNew(WebView view, int size) { + view.getSettings().setTextZoom(size); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + private void setFontSizeOld(WebView view, int size) { + view.getSettings().setDefaultFontSize(size); + } + + private Drawable getIcon(int id, Resources.Theme theme) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return getIconNew(id, theme); + } + + return getIconOld(id); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + private Drawable getIconOld(int id) { + return getResources().getDrawable(id); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private Drawable getIconNew(int id, Resources.Theme theme) { + return getResources().getDrawable(id, theme); + } + } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/SettingsActivity.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/SettingsActivity.java index 40de8e80d..d44609634 100644 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/SettingsActivity.java +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/SettingsActivity.java @@ -1,65 +1,289 @@ package fr.gaulupeau.apps.Poche.ui; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.AppCompatSpinner; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; import fr.gaulupeau.apps.InThePoche.BuildConfig; import fr.gaulupeau.apps.InThePoche.R; import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.DbConnection; import fr.gaulupeau.apps.Poche.data.Settings; +import fr.gaulupeau.apps.Poche.data.WallabagSettings; +import fr.gaulupeau.apps.Poche.network.WallabagConnection; +import fr.gaulupeau.apps.Poche.network.tasks.GetCredentialsTask; +import fr.gaulupeau.apps.Poche.network.tasks.TestConnectionTask; public class SettingsActivity extends BaseActionBarActivity { - Button btnDone; - EditText editPocheUrl; - EditText editAPIUsername; - EditText editAPIToken; - TextView textViewVersion; - EditText username; - EditText password; + Button btnTestConnection; + Button btnGetCredentials; + Button btnDone; + EditText editPocheUrl; + EditText editAPIUsername; + EditText editAPIToken; + CheckBox allCerts; + AppCompatSpinner themeChooser; + EditText fontSizeET; + CheckBox serifFont; + EditText listLimit; + TextView textViewVersion; + EditText username; + EditText password; + EditText httpAuthUsername; + EditText httpAuthPassword; + ProgressDialog progressDialog; private Settings settings; + private WallabagSettings wallabagSettings; + + private TestConnectionTask testConnectionTask; + private GetCredentialsTask getCredentialsTask; @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.settings); - - settings = ((App) getApplication()).getSettings(); - - String pocheUrl = settings.getKey(Settings.URL); - String apiUsername = settings.getKey(Settings.USER_ID); - String apiToken = settings.getKey(Settings.TOKEN); - - editPocheUrl = (EditText) findViewById(R.id.pocheUrl); - editPocheUrl.setText(pocheUrl == null ? "http://" : pocheUrl); - editAPIUsername = (EditText) findViewById(R.id.APIUsername); - editAPIUsername.setText(apiUsername); - editAPIToken = (EditText) findViewById(R.id.APIToken); - editAPIToken.setText(apiToken); - - username = (EditText) findViewById(R.id.username); - username.setText(settings.getKey(Settings.USERNAME)); - password = (EditText) findViewById(R.id.password); - password.setText(settings.getKey(Settings.PASSWORD)); - - btnDone = (Button) findViewById(R.id.btnDone); - btnDone.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - settings.setString(Settings.URL, editPocheUrl.getText().toString()); - settings.setString(Settings.USER_ID, editAPIUsername.getText().toString()); - settings.setString(Settings.TOKEN, editAPIToken.getText().toString()); + protected void onCreate(Bundle savedInstanceState) { + Themes.applyTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.settings); + + settings = App.getInstance().getSettings(); + wallabagSettings = WallabagSettings.settingsFromDisk(settings); + + if (!wallabagSettings.isValid()) { + hideBackButtonToActionBar(); + } + + editPocheUrl = (EditText) findViewById(R.id.pocheUrl); + editAPIUsername = (EditText) findViewById(R.id.APIUsername); + editAPIToken = (EditText) findViewById(R.id.APIToken); + allCerts = (CheckBox) findViewById(R.id.accept_all_certs_cb); + themeChooser = (AppCompatSpinner) findViewById(R.id.themeChooser); + fontSizeET = (EditText) findViewById(R.id.fontSizeET); + serifFont = (CheckBox) findViewById(R.id.ui_font_serif); + listLimit = (EditText) findViewById(R.id.list_limit_number); + + editPocheUrl.setText(wallabagSettings.wallabagURL); + editPocheUrl.setSelection(editPocheUrl.getText().length()); + editAPIUsername.setText(wallabagSettings.userID); + editAPIToken.setText(wallabagSettings.userToken); + allCerts.setChecked(settings.getBoolean(Settings.ALL_CERTS, false)); + + Themes.Theme[] themes = Themes.Theme.values(); + String[] themeOptions = new String[themes.length]; + Themes.Theme currentThemeName = Themes.getCurrentTheme(); + int currentThemeIndex = 0; + for(int i = 0; i < themes.length; i++) { + if(themes[i] == currentThemeName) currentThemeIndex = i; + themeOptions[i] = getString(themes[i].getNameId()); + } + themeChooser.setAdapter(new ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, themeOptions)); + themeChooser.setSelection(currentThemeIndex); + + fontSizeET.setText(String.valueOf(settings.getInt(Settings.FONT_SIZE, 100))); + + serifFont.setChecked(settings.getBoolean(Settings.SERIF_FONT, false)); + listLimit.setText(String.valueOf(settings.getInt(Settings.LIST_LIMIT, 50))); + + username = (EditText) findViewById(R.id.username); + username.setText(settings.getKey(Settings.USERNAME)); + password = (EditText) findViewById(R.id.password); + password.setText(settings.getKey(Settings.PASSWORD)); + + httpAuthUsername = (EditText) findViewById(R.id.http_auth_username); + httpAuthUsername.setText(settings.getKey(Settings.HTTP_AUTH_USERNAME)); + httpAuthPassword = (EditText) findViewById(R.id.http_auth_password); + httpAuthPassword.setText(settings.getKey(Settings.HTTP_AUTH_PASSWORD)); + + TextWatcher textWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { } + + @Override + public void afterTextChanged(Editable s) { + updateButton(); + } + }; + + editPocheUrl.addTextChangedListener(textWatcher); + editAPIUsername.addTextChangedListener(textWatcher); + editAPIToken.addTextChangedListener(textWatcher); + + progressDialog = new ProgressDialog(this); + progressDialog.setCanceledOnTouchOutside(true); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + cancelTasks(); + } + }); + + btnTestConnection = (Button) findViewById(R.id.btnTestConnection); + btnTestConnection.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + cancelTask(testConnectionTask); + + progressDialog.setMessage(getString(R.string.settings_testingConnection)); + progressDialog.show(); + + applyHttpAuth(); + + testConnectionTask = new TestConnectionTask( + SettingsActivity.this, editPocheUrl.getText().toString(), + username.getText().toString(), password.getText().toString(), + progressDialog); + + testConnectionTask.execute(); + } + }); + + btnGetCredentials = (Button) findViewById(R.id.btnGetFeedsCredentials); + btnGetCredentials.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + cancelTask(getCredentialsTask); + + progressDialog.setMessage(getString(R.string.settings_gettingCredentials)); + progressDialog.show(); + + applyHttpAuth(); + + getCredentialsTask = new GetCredentialsTask( + SettingsActivity.this, editPocheUrl.getText().toString(), + username.getText().toString(), password.getText().toString(), + editAPIUsername, editAPIToken, progressDialog); + + getCredentialsTask.execute(); + } + }); + + btnDone = (Button) findViewById(R.id.btnDone); + btnDone.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + wallabagSettings.wallabagURL = editPocheUrl.getText().toString(); + wallabagSettings.userID = editAPIUsername.getText().toString(); + wallabagSettings.userToken = editAPIToken.getText().toString(); + + settings.setBoolean(Settings.ALL_CERTS, allCerts.isChecked()); + Themes.Theme selectedTheme = Themes.Theme.values()[themeChooser.getSelectedItemPosition()]; + settings.setString(Settings.THEME, selectedTheme.toString()); + try { + settings.setInt(Settings.FONT_SIZE, Integer.parseInt(fontSizeET.getText().toString())); + } catch(NumberFormatException ignored) {} + settings.setBoolean(Settings.SERIF_FONT, serifFont.isChecked()); + try { + settings.setInt(Settings.LIST_LIMIT, Integer.parseInt(listLimit.getText().toString())); + } catch (NumberFormatException ignored) {} + settings.setString(Settings.USERNAME, username.getText().toString()); settings.setString(Settings.PASSWORD, password.getText().toString()); - finish(); - } - }); - textViewVersion = (TextView) findViewById(R.id.version); - textViewVersion.setText(BuildConfig.VERSION_NAME); + applyHttpAuth(); + + settings.setString(Settings.HTTP_AUTH_USERNAME, httpAuthUsername.getText().toString()); + settings.setString(Settings.HTTP_AUTH_PASSWORD, httpAuthPassword.getText().toString()); + + if (wallabagSettings.isValid()) { + wallabagSettings.save(); + finish(); + + if(selectedTheme != Themes.getCurrentTheme()) { + Themes.init(); + + Intent intent = new Intent(SettingsActivity.this, ArticlesListActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + } + } + }); + + textViewVersion = (TextView) findViewById(R.id.version); + textViewVersion.setText(BuildConfig.VERSION_NAME); + + this.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + + updateButton(); + } + + @Override + protected void onStop() { + super.onStop(); + + cancelTasks(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.option_settings, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menuWipeDb: { + AlertDialog.Builder b = new AlertDialog.Builder(this); + b.setTitle(R.string.wipe_db_dialog_title); + b.setMessage(R.string.wipe_db_dialog_message); + b.setPositiveButton(R.string.positive_answer, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + DbConnection.getSession().getArticleDao().deleteAll(); + } + }); + b.setNegativeButton(R.string.negative_answer, null); + b.create().show(); + return true; + } + } + + return super.onOptionsItemSelected(item); + } + + private void cancelTasks() { + cancelTask(testConnectionTask); + cancelTask(getCredentialsTask); + } + + private void cancelTask(AsyncTask task) { + if(task != null) { + task.cancel(true); + } + } + + private void applyHttpAuth() { + String username = httpAuthUsername.getText().toString(); + String password = httpAuthPassword.getText().toString(); + + WallabagConnection.setBasicAuthCredentials(username, password); + } + + // TODO: remove? + private void updateButton() { + wallabagSettings.wallabagURL = editPocheUrl.getText().toString(); + wallabagSettings.userID = editAPIUsername.getText().toString(); + wallabagSettings.userToken = editAPIToken.getText().toString(); - } + btnDone.setEnabled(wallabagSettings.isValid()); + } } diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/ui/Themes.java b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/Themes.java new file mode 100644 index 000000000..13e60b0cc --- /dev/null +++ b/app/src/main/java/fr/gaulupeau/apps/Poche/ui/Themes.java @@ -0,0 +1,95 @@ +package fr.gaulupeau.apps.Poche.ui; + +import android.app.Activity; + +import fr.gaulupeau.apps.InThePoche.R; +import fr.gaulupeau.apps.Poche.App; +import fr.gaulupeau.apps.Poche.data.Settings; + +public class Themes { + + private static Theme theme; + + static { + init(); + } + + public static void init() { + String themeName = App.getInstance().getSettings().getString(Settings.THEME, null); + + Theme theme = null; + if(themeName != null) { + try { + theme = Theme.valueOf(themeName); + } catch(IllegalArgumentException ignored) {} + } + + if(theme == null) { + theme = android.os.Build.MODEL.equals("NOOK") ? Theme.LightContrast : Theme.Light; + } + + Themes.theme = theme; + } + + public static Theme getCurrentTheme() { + return theme; + } + + public static void applyTheme(Activity activity) { + activity.setTheme(theme.getResId()); + } + + public static void applyProxyTheme(Activity activity) { + activity.setTheme(theme.getProxyResId()); + } + + public enum Theme { + Light( + R.string.themeName_light, + R.style.LightTheme, + R.style.ProxyTheme + ), + + LightContrast( + R.string.themeName_light_contrast, + R.style.LightThemeContrast, + R.style.ProxyTheme + ), + + Dark( + R.string.themeName_dark, + R.style.DarkTheme, + R.style.ProxyThemeDark + ), + + DarkContrast( + R.string.themeName_dark_contrast, + R.style.DarkThemeContrast, + R.style.ProxyThemeDark + ); + + private int nameId; + private int resId; + private int proxyResId; + + Theme(int nameId, int resId, int dialogResId) { + this.nameId = nameId; + this.resId = resId; + this.proxyResId = dialogResId; + } + + public int getNameId() { + return nameId; + } + + public int getResId() { + return resId; + } + + public int getProxyResId() { + return proxyResId; + } + + } + +} diff --git a/app/src/main/java/fr/gaulupeau/apps/Poche/util/arrays.java b/app/src/main/java/fr/gaulupeau/apps/Poche/util/arrays.java deleted file mode 100644 index a329652eb..000000000 --- a/app/src/main/java/fr/gaulupeau/apps/Poche/util/arrays.java +++ /dev/null @@ -1,10 +0,0 @@ -package fr.gaulupeau.apps.Poche.util; - -public class arrays { - public static String[] PodcastTitle; - public static String[] PodcastURL; - public static String[] PodcastContent; - public static String[] PodcastMedia; - public static String[] PodcastDate; - public static String[] PodcastId; -} diff --git a/app/src/main/res/drawable-hdpi/ic_action_next_item.png b/app/src/main/res/drawable-hdpi/ic_action_next_item.png new file mode 100644 index 000000000..fc998a511 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_next_item.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_previous_item.png b/app/src/main/res/drawable-hdpi/ic_action_previous_item.png new file mode 100644 index 000000000..10fcec8a6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_previous_item.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_refresh.png b/app/src/main/res/drawable-hdpi/ic_action_refresh.png new file mode 100644 index 000000000..dae27903e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_refresh.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_refresh.png b/app/src/main/res/drawable-mdpi/ic_action_refresh.png new file mode 100644 index 000000000..94ab6f4c5 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_refresh.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_refresh.png b/app/src/main/res/drawable-xhdpi/ic_action_refresh.png new file mode 100644 index 000000000..ab4ab9da6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_refresh.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_new.png b/app/src/main/res/drawable-xxhdpi/ic_action_new.png new file mode 100644 index 000000000..c42c2bfb5 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_new.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png b/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png new file mode 100644 index 000000000..44ee117ee Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_refresh.png differ diff --git a/app/src/main/res/drawable/ic_done_black_24dp.xml b/app/src/main/res/drawable/ic_done_black_24dp.xml new file mode 100644 index 000000000..83ee7bb4e --- /dev/null +++ b/app/src/main/res/drawable/ic_done_black_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_star_black_24dp.xml b/app/src/main/res/drawable/ic_star_black_24dp.xml new file mode 100644 index 000000000..b03b76533 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_black_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/activity_articles_list.xml b/app/src/main/res/layout/activity_articles_list.xml new file mode 100644 index 000000000..e7cef395a --- /dev/null +++ b/app/src/main/res/layout/activity_articles_list.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/app/src/main/res/layout/article.xml b/app/src/main/res/layout/article.xml index e79109bb9..ef2b14fc2 100644 --- a/app/src/main/res/layout/article.xml +++ b/app/src/main/res/layout/article.xml @@ -31,14 +31,45 @@ android:layout_height="1dip" android:layout_marginBottom="5sp" android:layout_marginTop="5sp" - android:background="#000000" /> + android:background="#000000" + android:visibility="gone" /> -