diff --git a/.circleci/config.yml b/.circleci/config.yml index 12bc5dc2..1cccb581 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -202,7 +202,7 @@ jobs: command: gcloud auth activate-service-account firebase-adminsdk-p9qvk@newspaper-84169.iam.gserviceaccount.com --key-file ${HOME}/client-secret.json - run: name: Run instrumented test on Firebase Test Lab - command: gcloud firebase test android run --type instrumentation --app mobile/build/outputs/apk/debug/mobile-debug.apk --test mobile/build/outputs/apk/androidTest/debug/mobile-debug-androidTest.apk --device model=Nexus5X,version=26,locale=en_US,orientation=portrait --environment-variables coverage=true,coverageFile=/sdcard/tmp/code-coverage/connected/coverage.ec --directories-to-pull=/sdcard/tmp --timeout 20m + command: gcloud firebase test android run --type instrumentation --app mobile/build/outputs/apk/debug/mobile-debug.apk --test mobile/build/outputs/apk/androidTest/debug/mobile-debug-androidTest.apk --device model=sailfish,version=26,locale=en_US,orientation=portrait --environment-variables coverage=true,coverageFile=/sdcard/tmp/code-coverage/connected/coverage.ec --directories-to-pull=/sdcard/tmp --timeout 20m - run: name: Create directory to store test results command: mkdir firebase @@ -228,7 +228,7 @@ jobs: - *attach_firebase_workspace - run: name: Move Firebase coverage report - command: mkdir -p mobile/build/outputs/code-coverage/connected && cp firebase/Nexus5X-26-en_US-portrait/artifacts/coverage.ec mobile/build/outputs/code-coverage/connected/coverage.ec + command: mkdir -p mobile/build/outputs/code-coverage/connected && cp firebase/sailfish-26-en_US-portrait/artifacts/coverage.ec mobile/build/outputs/code-coverage/connected/coverage.ec - *export_gservices_key - *decode_gservices_key - run: diff --git a/README.md b/README.md index 8735305b..e23f407b 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,9 @@ An aggregated news app containing news from 10+ local news publishers in Hong Ko * [Sky Post (晴報)](skypost.ulifestyle.com.hk) * [Hong Kong Economic Journal (信報)](http://www.hkej.com) * [RTHK (香港電台)](http://news.rthk.hk) +* [South China Morning Post (南華早報)](http://www.scmp.com/frontpage/hk) +* [The Standard (英文虎報)](http://www.thestandard.com.hk) +* [Wen Wei Po (文匯報)](http://news.wenweipo.com) ## Blog posts * [All you need to know about CircleCI 2.0 with Firebase Test Lab](https://medium.com/@ayltai/all-you-need-to-know-about-circleci-2-0-with-firebase-test-lab-2a66785ff3c2) @@ -59,7 +62,6 @@ This app is made with the support of open source projects: * [Calligraphy](https://github.com/InflationX/Calligraphy) * [AutoValue](https://github.com/google/auto/tree/master/value) * [Gson](https://github.com/google/gson) -* [GNU Trove](https://bitbucket.org/trove4j/trove) * [Espresso](https://google.github.io/android-testing-support-library) * [JUnit 4](https://github.com/junit-team/junit4) * [Mockito](https://github.com/mockito/mockito) diff --git a/build.gradle b/build.gradle index a07aec72..25434a77 100644 --- a/build.gradle +++ b/build.gradle @@ -2,15 +2,14 @@ buildscript { repositories { jcenter() google() - mavenCentral() maven { url 'https://maven.fabric.io/public' } } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' - classpath 'io.realm:realm-gradle-plugin:4.3.1' - classpath 'com.google.gms:google-services:3.1.2' - classpath 'com.google.firebase:firebase-plugins:1.1.4' + classpath 'io.realm:realm-gradle-plugin:4.3.4' + classpath 'com.google.gms:google-services:3.2.0' + classpath 'com.google.firebase:firebase-plugins:1.1.5' classpath 'io.fabric.tools:gradle:1.25.1' classpath 'org.jacoco:org.jacoco.core:0.7.9' } @@ -20,7 +19,6 @@ allprojects { repositories { jcenter() google() - mavenCentral() maven { url 'https://jitpack.io' } maven { url 'https://maven.fabric.io/public' } maven { url 'http://dl.bintray.com/piasy/maven' } @@ -28,6 +26,6 @@ allprojects { } } -task clean(type: Delete) { +task clean(type : Delete) { delete rootProject.buildDir } diff --git a/design/screenshot_cozy_framed.png b/design/screenshot_cozy_framed.png index fa7027be..01212d9b 100644 Binary files a/design/screenshot_cozy_framed.png and b/design/screenshot_cozy_framed.png differ diff --git a/design/screenshot_dark_framed.png b/design/screenshot_dark_framed.png index 4e6358ac..698dabd9 100644 Binary files a/design/screenshot_dark_framed.png and b/design/screenshot_dark_framed.png differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 569487de..1c1b1ae3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-bin.zip diff --git a/mobile/build.gradle b/mobile/build.gradle index d269c4f1..528233f1 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -15,13 +15,13 @@ String VERSION_HASH = 'git rev-parse --short HEAD'.execute().text.trim() android { compileSdkVersion 27 - buildToolsVersion '27.0.2' + buildToolsVersion '27.0.3' defaultConfig { applicationId 'com.github.ayltai.newspaper' minSdkVersion project.hasProperty('ciBuild') ? 16 : 21 targetSdkVersion 27 - versionCode 27 - versionName '3.9.' + VERSION_REVISION + '-' + VERSION_HASH + versionCode 28 + versionName '3.10.' + VERSION_REVISION + '-' + VERSION_HASH testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' testInstrumentationRunnerArguments disableAnalytics : 'true' @@ -59,15 +59,12 @@ android { } testOptions { + execution 'ANDROID_TEST_ORCHESTRATOR' + animationsDisabled true + unitTests { includeAndroidResources = true returnDefaultValues = true - - all { - jacoco { - includeNoLocationClasses true - } - } } } @@ -103,16 +100,15 @@ configurations { ext { multidexVersion = '1.0.2' supportLibraryVersion = '27.0.2' - architectureVersion = '1.0.0' - firebaseSdkVersion = '11.6.2' - daggerVersion = '2.13' - okhttpVersion = '3.9.1' + architectureVersion = '1.1.0' + firebaseSdkVersion = '11.8.0' + daggerVersion = '2.14.1' retrofitVersion = '2.3.0' - frescoVersion = '1.5.0' - bigImageViewerVersion = '1.4.5' - exoPlayerVersion = '2.6.0' + frescoVersion = '1.8.1' + bigImageViewerVersion = '1.4.6' + exoPlayerVersion = '2.7.0' leakCanaryVersion = '1.5.4' - robolectricVersion = '3.5.1' + robolectricVersion = '3.7.1' powerMockVersion = '2.0.0-beta.5' espressoVersion = '3.0.1' } @@ -131,9 +127,6 @@ dependencies { androidTestImplementation "com.android.support:multidex-instrumentation:$multidexVersion" // Android Architecture libraries - implementation ('android.arch.lifecycle:runtime:1.0.3') { - exclude group : 'com.android.support' - } implementation ("android.arch.lifecycle:extensions:$architectureVersion") { exclude group : 'com.android.support' } @@ -153,7 +146,7 @@ dependencies { implementation "com.google.firebase:firebase-perf:$firebaseSdkVersion" // Reactive programming - implementation 'io.reactivex.rxjava2:rxjava:2.1.7' + implementation 'io.reactivex.rxjava2:rxjava:2.1.10' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Dependency injection @@ -161,8 +154,7 @@ dependencies { annotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion" // Networking - implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" - implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" + implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion" implementation "com.squareup.retrofit2:converter-scalars:$retrofitVersion" @@ -192,29 +184,27 @@ dependencies { implementation 'com.nex3z:flow-layout:1.1.0' // Eye candies - implementation 'jp.wasabeef:recyclerview-animators:2.2.7' - implementation 'io.supercharge:shimmerlayout:1.0.2' - implementation 'com.github.castorflex.smoothprogressbar:library:1.1.0' + implementation 'jp.wasabeef:recyclerview-animators:2.3.0' + implementation 'io.supercharge:shimmerlayout:2.0.0' + implementation 'com.github.castorflex.smoothprogressbar:library:1.3.0' implementation 'pub.hanks:smallbang:1.2.2' implementation 'com.flaviofaria:kenburnsview:1.0.7' implementation 'com.gjiazhe:PanoramaImageView:1.0' implementation 'io.github.inflationx:calligraphy3:3.0.0' - implementation 'net.sf.trove4j:trove4j:3.0.3' - // Code generation tools compileOnly "com.jakewharton.auto.value:auto-value-annotations:1.5" - annotationProcessor "com.google.auto.value:auto-value:1.5.2" - annotationProcessor 'com.ryanharter.auto.value:auto-value-parcel:0.2.5' + annotationProcessor "com.google.auto.value:auto-value:1.5.3" + annotationProcessor 'com.ryanharter.auto.value:auto-value-parcel:0.2.6' implementation 'com.google.code.gson:gson:2.8.2' // Fabric - implementation ('com.crashlytics.sdk.android:crashlytics:2.8.0@aar') { + implementation ('com.crashlytics.sdk.android:crashlytics:2.9.0@aar') { transitive = true } // Instabug - implementation 'com.instabug.library:instabug:4.5.0' + implementation 'com.instabug.library:instabug:4.10.2' // Debugging implementation 'com.akaita.java:rxjava2-debug:1.2.2' @@ -223,14 +213,14 @@ dependencies { testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryVersion" // Unit testing - testImplementation 'org.mockito:mockito-core:2.13.0' + testImplementation 'org.mockito:mockito-core:2.15.3' testImplementation "org.powermock:powermock-module-junit4:$powerMockVersion" testImplementation "org.powermock:powermock-module-junit4-rule:$powerMockVersion" testImplementation "org.powermock:powermock-api-mockito2:$powerMockVersion" testImplementation "org.powermock:powermock-classloading-xstream:$powerMockVersion" testImplementation "org.robolectric:robolectric:$robolectricVersion" testImplementation "org.robolectric:shadows-multidex:$robolectricVersion" - testImplementation 'org.json:json:20171018' + testImplementation 'org.json:json:20180130' // Instrumented testing androidTestImplementation (name : 'cloudtestingscreenshotter_lib', ext : 'aar') @@ -243,9 +233,10 @@ dependencies { androidTestImplementation ("com.android.support.test.espresso:espresso-intents:$espressoVersion") { exclude group : 'com.android.support' } + androidTestUtil 'com.android.support.test:orchestrator:1.0.1' // Code coverage by Codacy - codacy 'com.github.codacy:codacy-coverage-reporter:2.0.1' + codacy 'com.github.codacy:codacy-coverage-reporter:2.0.2' } configurations.all { @@ -282,6 +273,10 @@ def coverageSourceDirs = [ 'src/debug/java' ] +tasks.withType(Test) { + jacoco.includeNoLocationClasses = true +} + task jacocoTestReport(type : JacocoReport, dependsOn : 'testDebugUnitTest') { group = 'Reporting' description = 'Generate JaCoCo coverage reports' diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro index 07b2efb7..d672783e 100644 --- a/mobile/proguard-rules.pro +++ b/mobile/proguard-rules.pro @@ -5,10 +5,8 @@ -keepnames class * implements android.os.Parcelable { public static final ** CREATOR; } - -## Retrolambda -dontwarn java.lang.invoke.* --dontwarn **$$Lambda$* +-dontwarn javax.** ## Facebook Fresco @@ -34,15 +32,11 @@ -dontwarn com.facebook.infer.** ## Realm --dontwarn javax.** -dontwarn io.realm.** ## Retrofit -dontwarn retrofit2.** -## BottomBar --dontwarn com.roughike.bottombar.** - ## SearchView -keep class android.support.v7.widget.SearchView { *; } @@ -53,3 +47,11 @@ @org.simpleframework.xml.* ; public (); } + +## BottomNavigationView +-keepclassmembers class android.support.design.internal.BottomNavigationMenuView { + boolean mShiftingMode; +} + +## Instabug +-dontwarn com.instabug.** diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/Constants.java b/mobile/src/main/java/com/github/ayltai/newspaper/Constants.java index 56cf2a88..2ce3ec24 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/Constants.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/Constants.java @@ -9,7 +9,7 @@ public final class Constants { public static final int INITIAL_RETRY_DELAY = 2; public static final int MAX_RETRIES = 5; public static final int CONNECTION_TIMEOUT = 5; - public static final int REFRESH_TIMEOUT = 10; + public static final int REFRESH_TIMEOUT = 7; public static final int HOUSEKEEP_TIME = 72 * 60 * 60 * 1000; public static final int REMOTE_CONFIG_CACHE_EXPIRATION = 30 * 60 * 1000; diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/MainActivity.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/MainActivity.java index 88c914cc..74def92a 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/MainActivity.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/MainActivity.java @@ -4,6 +4,7 @@ import android.arch.lifecycle.LifecycleObserver; import android.content.Context; +import android.os.Build; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.annotation.Nullable; @@ -14,6 +15,8 @@ import android.util.SparseIntArray; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.Window; +import android.view.WindowManager; import com.google.firebase.perf.FirebasePerformance; import com.google.firebase.perf.metrics.Trace; @@ -74,6 +77,12 @@ protected void onCreate(@Nullable final Bundle savedInstanceState) { this.setTheme(this.userConfig.getTheme() == Constants.THEME_LIGHT ? R.style.AppTheme_Light : R.style.AppTheme_Dark); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final Window window = this.getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.setStatusBarColor(ContextUtils.getColor(this, R.attr.primaryColorDark)); + } + if (!DevUtils.isRunningUnitTest()) this.initInstabug(); Single.create(emitter -> emitter.onSuccess(ComponentFactory.getInstance() diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/MainApplication.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/MainApplication.java index f21ca903..555ca568 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/MainApplication.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/MainApplication.java @@ -103,7 +103,7 @@ private void initFresco() { ImagePipelineConfig.getDefaultImageRequestConfig() .setProgressiveRenderingEnabled(true); - Fresco.initialize(this, OkHttpImagePipelineConfigFactory.newBuilder(this, DaggerHttpComponent.builder() + if (!DevUtils.isRunningUnitTest()) Fresco.initialize(this, OkHttpImagePipelineConfigFactory.newBuilder(this, DaggerHttpComponent.builder() .build() .httpClient()) .setDownsampleEnabled(true) diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/MainFlow.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/MainFlow.java index a20e43f8..506f0ace 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/MainFlow.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/MainFlow.java @@ -2,13 +2,14 @@ import java.lang.ref.SoftReference; +import android.animation.Animator; import android.app.Activity; +import android.graphics.Point; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.Pair; -import android.view.animation.Animation; +import android.view.View; -import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.analytics.ViewEvent; import com.github.ayltai.newspaper.app.data.model.Item; import com.github.ayltai.newspaper.app.view.DetailsPresenter; @@ -23,6 +24,8 @@ import flow.Direction; final class MainFlow extends RxFlow { + + MainFlow(@NonNull final Activity activity) { super(activity); } @@ -35,11 +38,10 @@ protected Object getDefaultKey() { @Nullable @Override - protected Animation getAnimation(@NonNull final Direction direction, @Nullable final Runnable onStart, @Nullable final Runnable onEnd) { - if (direction == Direction.FORWARD) return Animations.getAnimation(this.getContext(), R.anim.reveal_enter, android.R.integer.config_mediumAnimTime, onStart, onEnd); - if (direction == Direction.BACKWARD) return Animations.getAnimation(this.getContext(), R.anim.reveal_exit, android.R.integer.config_mediumAnimTime, onStart, onEnd); + protected Animator getAnimator(@NonNull final View view, @NonNull final Direction direction, @Nullable final Point location, @Nullable final Runnable onStart, @Nullable final Runnable onEnd) { + if (direction == Direction.FORWARD || direction == Direction.BACKWARD) return Animations.createDefaultAnimator(view, direction, location, onStart, onEnd); - return super.getAnimation(direction, onStart, onEnd); + return super.getAnimator(view, direction, location, onStart, onEnd); } @SuppressWarnings({ "unchecked", "CyclomaticComplexity" }) diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemListLoader.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemListLoader.java index cd94b077..52ca8998 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemListLoader.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemListLoader.java @@ -17,6 +17,7 @@ import com.akaita.java.rxjava2debug.RxJava2Debug; import com.github.ayltai.newspaper.Constants; import com.github.ayltai.newspaper.app.data.model.Category; +import com.github.ayltai.newspaper.app.data.model.Item; import com.github.ayltai.newspaper.app.data.model.NewsItem; import com.github.ayltai.newspaper.app.data.model.SourceFactory; import com.github.ayltai.newspaper.client.Client; @@ -24,6 +25,7 @@ import com.github.ayltai.newspaper.data.RealmLoader; import com.github.ayltai.newspaper.net.NetworkUtils; import com.github.ayltai.newspaper.util.DevUtils; +import com.github.ayltai.newspaper.util.Lists; import com.github.ayltai.newspaper.util.RxUtils; import com.github.ayltai.newspaper.util.StringUtils; @@ -33,7 +35,7 @@ import io.reactivex.schedulers.Schedulers; import io.realm.RealmObject; -public final class ItemListLoader extends RealmLoader> { +public final class ItemListLoader extends RealmLoader { //region Constants public static final int ID = ItemListLoader.class.hashCode(); @@ -88,20 +90,20 @@ public Flowable> build() { return Flowable.create(emitter -> this.activity .getSupportLoaderManager() - .restartLoader(ItemListLoader.ID + (categories == null ? 0 : categories.toString().hashCode()), this.args, new LoaderManager.LoaderCallbacks>() { + .restartLoader(ItemListLoader.ID + (categories == null ? 0 : categories.toString().hashCode()), this.args, new LoaderManager.LoaderCallbacks>() { @NonNull @Override - public Loader> onCreateLoader(final int id, final Bundle args) { + public Loader> onCreateLoader(final int id, final Bundle args) { return new ItemListLoader(ItemListLoader.Builder.this.activity, args); } @Override - public void onLoadFinished(final Loader> loader, final List items) { - emitter.onNext(items); + public void onLoadFinished(final Loader> loader, final List items) { + emitter.onNext(Lists.transform(items, item -> (NewsItem)item)); } @Override - public void onLoaderReset(final Loader> loader) { + public void onLoaderReset(final Loader> loader) { } }), BackpressureStrategy.LATEST); } @@ -113,15 +115,15 @@ private ItemListLoader(@NonNull final Context context, @Nullable final Bundle ar @NonNull @Override - protected Flowable> loadFromLocalSource(@NonNull final Context context, @Nullable final Bundle args) { - if (!this.isValid()) return Flowable.error(new IllegalStateException("Realm instance is null")); + protected Flowable> loadFromLocalSource(@NonNull final Context context, @Nullable final Bundle args) { + if (!this.isValid()) return Flowable.just(Collections.emptyList()); return Flowable.create(emitter -> ItemManager.create(this.getRealm()).getItems(ItemListLoader.getSources(args).toArray(StringUtils.EMPTY_ARRAY), ItemListLoader.getCategories(args).toArray(StringUtils.EMPTY_ARRAY)) .compose(RxUtils.applySingleSchedulers(this.getScheduler())) .map(items -> items.isEmpty() ? items : RealmObject.isManaged(items.get(0)) ? this.getRealm().copyFromRealm(items) : items) .map(items -> { Collections.sort(items); - return items; + return Lists.transform(items, item -> (Item)item); }) .subscribe(emitter::onNext), BackpressureStrategy.LATEST); } @@ -129,7 +131,7 @@ protected Flowable> loadFromLocalSource(@NonNull final Context co @SuppressWarnings("unchecked") @NonNull @Override - protected Flowable> loadFromRemoteSource(@NonNull final Context context, @Nullable final Bundle args) { + protected Flowable> loadFromRemoteSource(@NonNull final Context context, @Nullable final Bundle args) { if (NetworkUtils.isOnline(context)) { final List>> singles = this.createSingles(context, args); if (singles.isEmpty()) Flowable.just(Collections.emptyList()); @@ -140,32 +142,24 @@ protected Flowable> loadFromRemoteSource(@NonNull final Context c final List combinedList = new ArrayList<>(); for (final Object list : lists) combinedList.addAll((List)list); - Collections.sort(combinedList); - return combinedList; }) .map(items -> { - Collections.sort(items); - return items; - }) - .flatMap(items -> { if (this.isValid()) { - return Single.create(e -> ItemManager.create(this.getRealm()) + final List newsItems = ItemManager.create(this.getRealm()) .putItems(items) .compose(RxUtils.applySingleSchedulers(this.getScheduler())) - .subscribe( - e::onSuccess, - error -> { - if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), error.getMessage(), RxJava2Debug.getEnhancedStackTrace(error)); + .blockingGet(); + + Collections.sort(newsItems); - if (!e.isDisposed()) e.onError(error); - })); + return newsItems; } else { - return Single.just(items); + return items; } }) .subscribe( - emitter::onNext, + items -> emitter.onNext(Lists.transform(items, item -> (Item)item)), error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), error.getMessage(), RxJava2Debug.getEnhancedStackTrace(error)); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemManager.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemManager.java index 8acad9cc..433f2dd0 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemManager.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/ItemManager.java @@ -18,6 +18,7 @@ import com.github.ayltai.newspaper.util.RxUtils; import io.reactivex.Single; +import io.reactivex.SingleEmitter; import io.realm.Case; import io.realm.Realm; import io.realm.RealmObject; @@ -63,14 +64,7 @@ public Single> getItems(@Nullable final CharSequence searchText, .and() .in(NewsItem.FIELD_CATEGORY, categories); - if (!TextUtils.isEmpty(searchText)) query.and() - .beginGroup() - .contains(NewsItem.FIELD_TITLE, searchText.toString(), Case.INSENSITIVE) - .or() - .contains(NewsItem.FIELD_DESCRIPTION, searchText.toString(), Case.INSENSITIVE) - .endGroup(); - - if (!emitter.isDisposed()) emitter.onSuccess(this.getRealm().copyFromRealm(query.findAll())); + this.emit(emitter, query, searchText, sources, categories); }); } @@ -84,19 +78,9 @@ public Single> getHistoricalItems(@Nullable final CharSequence se return Single.create(emitter -> { final RealmQuery query = this.getRealm() .where(NewsItem.class) - .greaterThan(NewsItem.FIELD_LAST_ACCESSED_DATE, 0) - .in(NewsItem.FIELD_SOURCE, sources) - .and() - .in(NewsItem.FIELD_CATEGORY, categories); + .greaterThan(NewsItem.FIELD_LAST_ACCESSED_DATE, 0); - if (!TextUtils.isEmpty(searchText)) query.and() - .beginGroup() - .contains(NewsItem.FIELD_TITLE, searchText.toString(), Case.INSENSITIVE) - .or() - .contains(NewsItem.FIELD_DESCRIPTION, searchText.toString(), Case.INSENSITIVE) - .endGroup(); - - if (!emitter.isDisposed()) emitter.onSuccess(this.getRealm().copyFromRealm(query.findAllSorted(NewsItem.FIELD_LAST_ACCESSED_DATE, Sort.DESCENDING))); + this.emit(emitter, query, searchText, sources, categories); }); } @@ -110,19 +94,9 @@ public Single> getBookmarkedItems(@Nullable final CharSequence se return Single.create(emitter -> { final RealmQuery query = this.getRealm() .where(NewsItem.class) - .equalTo(NewsItem.FIELD_BOOKMARKED, true) - .in(NewsItem.FIELD_SOURCE, sources) - .and() - .in(NewsItem.FIELD_CATEGORY, categories); - - if (!TextUtils.isEmpty(searchText)) query.and() - .beginGroup() - .contains(NewsItem.FIELD_TITLE, searchText.toString(), Case.INSENSITIVE) - .or() - .contains(NewsItem.FIELD_DESCRIPTION, searchText.toString(), Case.INSENSITIVE) - .endGroup(); + .equalTo(NewsItem.FIELD_BOOKMARKED, true); - if (!emitter.isDisposed()) emitter.onSuccess(this.getRealm().copyFromRealm(query.findAllSorted(NewsItem.FIELD_LAST_ACCESSED_DATE, Sort.DESCENDING))); + this.emit(emitter, query, searchText, sources, categories); }); } @@ -230,4 +204,20 @@ private void clearObsoleteItems() { .findAll() .deleteAllFromRealm(); } + + private void emit(@NonNull final SingleEmitter> emitter, @NonNull final RealmQuery query, @Nullable final CharSequence searchText, @NonNull final String[] sources, @NonNull final String[] categories) { + query.and() + .in(NewsItem.FIELD_SOURCE, sources) + .and() + .in(NewsItem.FIELD_CATEGORY, categories); + + if (!TextUtils.isEmpty(searchText)) query.and() + .beginGroup() + .contains(NewsItem.FIELD_TITLE, searchText.toString(), Case.INSENSITIVE) + .or() + .contains(NewsItem.FIELD_DESCRIPTION, searchText.toString(), Case.INSENSITIVE) + .endGroup(); + + if (!emitter.isDisposed()) emitter.onSuccess(this.getRealm().copyFromRealm(query.sort(NewsItem.FIELD_LAST_ACCESSED_DATE, Sort.DESCENDING).findAll())); + } } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/FeaturedItem.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/FeaturedItem.java index abc12321..aff13ccd 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/FeaturedItem.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/FeaturedItem.java @@ -1,124 +1,129 @@ -package com.github.ayltai.newspaper.app.data.model; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Random; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.text.TextUtils; - -import io.realm.RealmList; - -public final class FeaturedItem implements Item { - private static final ThreadLocal RANDOM = new ThreadLocal() { - @Override - protected Random initialValue() { - return new Random(); - } - }; - - private final List items; - - private int index; - - private FeaturedItem(@NonNull final List items) { - this.items = items; - } - - //region Properties - - @Nullable - @Override - public String getTitle() { - return this.items.get(this.index).getTitle(); - } - - @Nullable - @Override - public String getDescription() { - return this.items.get(this.index).getDescription(); - } - - @Override - public boolean isFullDescription() { - return this.items.get(this.index).isFullDescription(); - } - - @NonNull - @Override - public String getLink() { - return this.items.get(this.index).getLink(); - } - - @Nullable - @Override - public Date getPublishDate() { - return this.items.get(this.index).getPublishDate(); - } - - @NonNull - @Override - public String getSource() { - return this.items.get(this.index).getSource(); - } - - @NonNull - @Override - public String getCategory() { - return this.items.get(this.index).getCategory(); - } - - @NonNull - @Override - public RealmList getImages() { - return this.items.get(this.index).getImages(); - } - - @Nullable - @Override - public Video getVideo() { - return this.items.get(this.index).getVideo(); - } - - @Override - public boolean isBookmarked() { - return this.items.get(this.index).isBookmarked(); - } - - //endregion - - @NonNull - public Item getItem() { - return this.items.get(this.index); - } - - public void next() { - this.index = this.index == this.items.size() - 1 ? 0 : this.index + 1; - } - - @Override - public int compareTo(@NonNull final Item item) { - return -1; - } - - @Nullable - public static Item create(@NonNull final List items) { - final List originalItems = new ArrayList<>(items); - final List featuredItems = new ArrayList<>(); - - while (!originalItems.isEmpty()) { - final Item item = originalItems.remove(FeaturedItem.RANDOM.get().nextInt(originalItems.size())); - if (FeaturedItem.canBeFeatured(item)) featuredItems.add(item); - } - - if (featuredItems.isEmpty()) return null; - - return new FeaturedItem(featuredItems); - } - - private static boolean canBeFeatured(@NonNull final Item item) { - return !TextUtils.isEmpty(item.getTitle()) && !item.getImages().isEmpty(); - } -} +package com.github.ayltai.newspaper.app.data.model; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import io.realm.RealmList; + +public final class FeaturedItem implements Item { + private static final ThreadLocal RANDOM = new ThreadLocal() { + @Override + protected Random initialValue() { + return new Random(); + } + }; + + private final List items; + + private int index; + + private FeaturedItem(@NonNull final List items) { + this.items = items; + } + + //region Properties + + @Nullable + @Override + public String getTitle() { + return this.items.get(this.index).getTitle(); + } + + @Nullable + @Override + public String getDescription() { + return this.items.get(this.index).getDescription(); + } + + @Override + public boolean isFullDescription() { + return this.items.get(this.index).isFullDescription(); + } + + @NonNull + @Override + public String getLink() { + return this.items.get(this.index).getLink(); + } + + @Nullable + @Override + public Date getPublishDate() { + return this.items.get(this.index).getPublishDate(); + } + + @NonNull + @Override + public String getSource() { + return this.items.get(this.index).getSource(); + } + + @NonNull + @Override + public String getCategory() { + return this.items.get(this.index).getCategory(); + } + + @NonNull + @Override + public RealmList getImages() { + return this.items.get(this.index).getImages(); + } + + @Nullable + @Override + public Video getVideo() { + return this.items.get(this.index).getVideo(); + } + + @Override + public boolean isBookmarked() { + return this.items.get(this.index).isBookmarked(); + } + + //endregion + + @NonNull + public Item getItem() { + return this.items.get(this.index); + } + + @NonNull + public Item getNextItem() { + return this.items.get(this.index == this.items.size() - 1 ? 0 : this.index + 1); + } + + public void next() { + this.index = this.index == this.items.size() - 1 ? 0 : this.index + 1; + } + + @Override + public int compareTo(@NonNull final Item item) { + return -1; + } + + @Nullable + public static Item create(@NonNull final List items) { + final List originalItems = new ArrayList<>(items); + final List featuredItems = new ArrayList<>(); + + while (!originalItems.isEmpty()) { + final Item item = originalItems.remove(FeaturedItem.RANDOM.get().nextInt(originalItems.size())); + if (FeaturedItem.canBeFeatured(item)) featuredItems.add(item); + } + + if (featuredItems.isEmpty()) return null; + + return new FeaturedItem(featuredItems); + } + + private static boolean canBeFeatured(@NonNull final Item item) { + return !TextUtils.isEmpty(item.getTitle()) && !item.getImages().isEmpty(); + } +} diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/NewsItem.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/NewsItem.java index 7c4a7ec4..7667e55d 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/NewsItem.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/NewsItem.java @@ -194,7 +194,12 @@ public final int compareTo(@NonNull final Item item) { if (this.publishDate != 0 && item.getPublishDate() != null) return (int)(item.getPublishDate().getTime() - this.publishDate); - return item.getTitle() == null ? 1 : this.title.compareTo(item.getTitle()); + if (this.title == null && item.getTitle() == null) return 0; + + if (this.title == null) return 1; + if (item.getTitle() == null) return -1; + + return this.title.compareTo(item.getTitle()); } @Override diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/SourceFactory.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/SourceFactory.java index 2b9b071c..ef23c726 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/SourceFactory.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/data/model/SourceFactory.java @@ -8,11 +8,11 @@ import android.content.Context; import android.support.annotation.NonNull; +import android.support.v4.util.ArrayMap; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.client.HeadlineClient; -import gnu.trove.map.hash.THashMap; import io.realm.RealmList; public final class SourceFactory { @@ -25,7 +25,7 @@ protected DateFormat initialValue() { private static SourceFactory instance; - private final Map sources = new THashMap<>(12); + private final Map sources = new ArrayMap<>(15); @NonNull public static SourceFactory getInstance(@NonNull final Context context) { @@ -51,7 +51,10 @@ private SourceFactory(@NonNull final Context context) { this.sources.put(sources[i++], SourceFactory.createHeadlineRealtimeSource(sources, categories)); this.sources.put(sources[i++], SourceFactory.createSkyPostSource(sources, categories)); this.sources.put(sources[i++], SourceFactory.createEconomicJournalSource(sources, categories)); - this.sources.put(sources[i], SourceFactory.createRadioTelevisionSource(sources, categories)); + this.sources.put(sources[i++], SourceFactory.createRadioTelevisionSource(sources, categories)); + this.sources.put(sources[i++], SourceFactory.createSouthChinaMorningPostSource(sources, categories)); + this.sources.put(sources[i++], SourceFactory.createTheStandardSource(sources, categories)); + this.sources.put(sources[i], SourceFactory.createWenWeiPoSource(sources, categories)); } @NonNull @@ -215,4 +218,41 @@ private static Source createRadioTelevisionSource(@NonNull final String[] source new Category("http://rthk.hk/rthk/news/rss/c_expressnews_cfinance.xml", categories[12]), new Category("http://rthk.hk/rthk/news/rss/c_expressnews_csport.xml", categories[15])), R.drawable.avatar_rthk); } + + @SuppressWarnings("checkstyle:magicnumber") + @NonNull + private static Source createSouthChinaMorningPostSource(@NonNull final String[] sources, @NonNull final String[] categories) { + return new Source(sources[12], new RealmList<>( + new Category("http://www.scmp.com/rss/2/feed", categories[9]), + new Category("http://www.scmp.com/rss/5/feed", categories[10]), + new Category("http://www.scmp.com/rss/4/feed", categories[11]), + new Category("http://www.scmp.com/rss/92/feed", categories[12]), + new Category("http://www.scmp.com/rss/96/feed", categories[13]), + new Category("http://www.scmp.com/rss/95/feed", categories[15]), + new Category("http://www.scmp.com/rss/94/feed", categories[16]) + + ), R.drawable.avatar_scmp); + } + + @SuppressWarnings("checkstyle:magicnumber") + @NonNull + private static Source createTheStandardSource(@NonNull final String[] sources, @NonNull final String[] categories) { + return new Source(sources[13], new RealmList<>( + new Category("http://www.thestandard.com.hk/ajax_sections_list.php?sid=4", categories[9]), + new Category("http://www.thestandard.com.hk/ajax_sections_list.php?sid=6", categories[10]), + new Category("http://www.thestandard.com.hk/ajax_sections_list.php?sid=3", categories[11]), + new Category("http://www.thestandard.com.hk/ajax_sections_list.php?sid=2", categories[12]), + new Category("http://www.thestandard.com.hk/ajax_sections_list.php?sid=8", categories[15]) + ), R.drawable.avatar_the_standard); + } + + @SuppressWarnings("checkstyle:magicnumber") + @NonNull + private static Source createWenWeiPoSource(@NonNull final String[] sources, @NonNull final String[] categories) { + return new Source(sources[14], new RealmList<>( + new Category("http://news.wenweipo.com/list_news.php?cat=000IN&instantCat=hk", categories[9]), + new Category("http://news.wenweipo.com/list_news.php?cat=000IN&instantCat=china", categories[10]), + new Category("http://news.wenweipo.com/list_news.php?cat=000IN&instantCat=world", categories[11]) + ), R.drawable.avatar_wen_wei_po); + } } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/FeaturedPresenter.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/FeaturedPresenter.java index 33dd5b6d..aaa02a1c 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/FeaturedPresenter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/FeaturedPresenter.java @@ -17,6 +17,7 @@ import com.github.ayltai.newspaper.Constants; import com.github.ayltai.newspaper.app.data.model.FeaturedItem; +import com.github.ayltai.newspaper.app.data.model.Item; import com.github.ayltai.newspaper.app.widget.FeaturedView; import com.github.ayltai.newspaper.media.BaseImageLoaderCallback; import com.github.ayltai.newspaper.media.DaggerImageComponent; @@ -35,11 +36,13 @@ public class FeaturedPresenter extends ItemPresenter implements Li @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) protected void onResume() { - this.disposable = Observable.interval(Constants.FEATURED_IMAGE_ROTATION, TimeUnit.SECONDS) + this.onPause(); + + this.disposable = Observable.interval(0, Constants.FEATURED_IMAGE_ROTATION, TimeUnit.SECONDS) .compose(RxUtils.applyObservableBackgroundToMainSchedulers()) .subscribe(time -> { if (this.getModel() instanceof FeaturedItem && this.getView() != null) { - ((FeaturedItem)this.getModel()).next(); + final Item item = ((FeaturedItem)this.getModel()).getNextItem(); final Integer requestId = this.requestId.incrementAndGet(); this.requestIds.add(requestId); @@ -48,7 +51,7 @@ protected void onResume() { .imageModule(new ImageModule(this.getView().getContext())) .build() .imageLoader() - .loadImage(requestId, Uri.parse(this.getModel().getImages().get(0).getUrl()), new BaseImageLoaderCallback() { + .loadImage(requestId, Uri.parse(item.getImages().get(0).getUrl()), new BaseImageLoaderCallback() { @Override public void onFinish() { FeaturedPresenter.this.requestIds.remove(requestId); @@ -57,13 +60,13 @@ public void onFinish() { @Override public void onSuccess(final File image) { if (FeaturedPresenter.this.getView() != null) { - FeaturedPresenter.this.getView().setImages(FeaturedPresenter.this.getModel().getImages()); - FeaturedPresenter.this.getView().setTitle(FeaturedPresenter.this.getModel().getTitle()); + FeaturedPresenter.this.getView().setImages(item.getImages()); + FeaturedPresenter.this.getView().setTitle(item.getTitle()); + + ((FeaturedItem)FeaturedPresenter.this.getModel()).next(); } } }); - - this.bindModel(this.getModel()); } }); } @@ -82,6 +85,8 @@ public void onViewAttached(@NonNull final FeaturedView view, final boolean isFir final Activity activity = Views.getActivity(view); if (activity instanceof AppCompatActivity) ((AppCompatActivity)activity).getLifecycle().addObserver(this); + + this.onResume(); } @Override diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemListAdapter.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemListAdapter.java index b14a0ffd..e3bba484 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemListAdapter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemListAdapter.java @@ -107,10 +107,10 @@ public FilterResults performFiltering(@Nullable final CharSequence searchText) { .compose(RxUtils.applySingleSchedulers(DataManager.SCHEDULER)) .flatMap(manager -> { if (this.isHistorical) return manager.getHistoricalItems(searchText, this.sources.toArray(StringUtils.EMPTY_ARRAY), this.categories.toArray(StringUtils.EMPTY_ARRAY)) - .compose(RxUtils.applySingleSchedulers(DataManager.SCHEDULER)); + .compose(RxUtils.applySingleSchedulers(DataManager.SCHEDULER)); if (this.isBookmarked) return manager.getBookmarkedItems(searchText, this.sources.toArray(StringUtils.EMPTY_ARRAY), this.categories.toArray(StringUtils.EMPTY_ARRAY)) - .compose(RxUtils.applySingleSchedulers(DataManager.SCHEDULER)); + .compose(RxUtils.applySingleSchedulers(DataManager.SCHEDULER)); return manager.getItems(searchText, this.sources.toArray(StringUtils.EMPTY_ARRAY), this.categories.toArray(StringUtils.EMPTY_ARRAY)) .compose(RxUtils.applySingleSchedulers(DataManager.SCHEDULER)); @@ -169,6 +169,11 @@ protected Iterable getItemAnimators(@NonNull final View view) { return Animations.isEnabled() ? Animations.createDefaultAnimators(view) : super.getItemAnimators(view); } + @Override + protected long getAnimationDuration() { + return 2 * this.context.getResources().getInteger(android.R.integer.config_mediumAnimTime); + } + @NonNull @Override public SimpleViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemPresenter.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemPresenter.java index 0e6ac217..4a516313 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemPresenter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/ItemPresenter.java @@ -4,6 +4,7 @@ import java.util.List; import android.app.Activity; +import android.graphics.Point; import android.support.annotation.CallSuper; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; @@ -23,6 +24,7 @@ import com.github.ayltai.newspaper.app.widget.DetailsView; import com.github.ayltai.newspaper.util.DevUtils; import com.github.ayltai.newspaper.util.Irrelevant; +import com.github.ayltai.newspaper.util.Optional; import com.github.ayltai.newspaper.view.Presenter; import com.github.ayltai.newspaper.view.binding.Binder; import com.github.ayltai.newspaper.view.binding.BindingPresenter; @@ -63,7 +65,7 @@ public interface View extends Presenter.View { void setIsRead(boolean isRead); @Nullable - Flowable clicks(); + Flowable> clicks(); @Nullable Flowable avatarClicks(); @@ -114,7 +116,7 @@ public void bindModel(final Item model) { } } - private void onClick() { + private void onClick(@NonNull final Optional location) { this.initAppConfig(); if (this.getView() != null) { @@ -131,7 +133,7 @@ private void onClick() { .logEvent(new ClickEvent() .setElementName(item instanceof FeaturedItem ? "Featured" : "Non-featured")); - if (!DevUtils.isRunningUnitTest()) Flow.get(this.getView().getContext()).set(DetailsView.Key.create(item instanceof NewsItem ? (NewsItem)item : (NewsItem)((FeaturedItem)item).getItem())); + if (!DevUtils.isRunningUnitTest()) Flow.get(this.getView().getContext()).set(DetailsView.Key.create(item instanceof NewsItem ? (NewsItem)item : (NewsItem)((FeaturedItem)item).getItem(), location.isPresent() ? location.get() : null)); } } @@ -216,8 +218,8 @@ private void onVideoClick() { public void onViewAttached(@NonNull final V view, final boolean isFirstTimeAttachment) { super.onViewAttached(view, isFirstTimeAttachment); - final Flowable clicks = view.clicks(); - if (clicks != null) this.manageDisposable(clicks.subscribe(irrelevant -> this.onClick())); + final Flowable> clicks = view.clicks(); + if (clicks != null) this.manageDisposable(clicks.subscribe(location -> this.onClick(location))); final Flowable avatarClicks = view.avatarClicks(); if (avatarClicks != null) this.manageDisposable(avatarClicks.subscribe(irrelevant -> this.onAvatarClick())); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/SourcesPresenter.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/SourcesPresenter.java index aacc6e39..a824bddf 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/view/SourcesPresenter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/view/SourcesPresenter.java @@ -18,7 +18,6 @@ import com.github.ayltai.newspaper.util.RxUtils; import com.github.ayltai.newspaper.view.OptionsPresenter; -import gnu.trove.set.hash.THashSet; import io.reactivex.Single; public class SourcesPresenter extends OptionsPresenter { @@ -36,7 +35,7 @@ protected Single> load() { return Single.create(emitter -> { final List sources = new ArrayList<>(ComponentFactory.getInstance().getConfigComponent(activity).userConfig().getDefaultSources()); - final Set displayNames = new THashSet<>(); + final Set displayNames = new ArraySet<>(); for (final String source : sources) displayNames.add(Source.toDisplayName(source)); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemListView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemListView.java index d9d13bd8..39479060 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemListView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemListView.java @@ -3,7 +3,6 @@ import android.content.Context; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; -import android.view.animation.AccelerateDecelerateInterpolator; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.app.data.model.Item; @@ -26,13 +25,9 @@ protected int getLayoutId() { @NonNull @Override protected UniversalAdapter createAdapter() { - final ItemListAdapter adapter = new ItemListAdapter.Builder(this.getContext()) + return new ItemListAdapter.Builder(this.getContext()) .addBinderFactory(new FeaturedBinderFactory()) .addBinderFactory(new CompactBinderFactory()) .build(); - - adapter.setAnimationInterpolator(new AccelerateDecelerateInterpolator()); - - return adapter; } } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemView.java index b8b868e4..2d95292e 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CompactItemView.java @@ -4,45 +4,53 @@ import java.util.List; import android.content.Context; +import android.graphics.Point; import android.net.Uri; import android.support.annotation.CallSuper; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.support.v4.view.GestureDetectorCompat; import android.text.Html; import android.text.TextUtils; +import android.view.GestureDetector; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; -import com.facebook.drawee.view.SimpleDraweeView; +import com.davemorrissey.labs.subscaleview.ImageSource; import com.github.ayltai.newspaper.Constants; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.app.data.model.Image; import com.github.ayltai.newspaper.util.DateUtils; +import com.github.ayltai.newspaper.util.DevUtils; import com.github.ayltai.newspaper.util.ImageUtils; -import com.github.ayltai.newspaper.util.Irrelevant; +import com.github.ayltai.newspaper.util.Optional; import com.github.piasy.biv.view.BigImageView; public final class CompactItemView extends ItemView { public static final int VIEW_TYPE = R.id.view_type_compact; + private final GestureDetectorCompat detector; + //region Components - private final BigImageView image; - private final TextView title; - private final TextView description; - private final SimpleDraweeView avatar; - private final TextView source; - private final TextView publishDate; + private final BigImageView image; + private final TextView title; + private final TextView description; + private final ImageView avatar; + private final TextView source; + private final TextView publishDate; //endregion public CompactItemView(@NonNull final Context context) { super(context); - final View view = LayoutInflater.from(context).inflate(R.layout.view_news_compact, this, true); + final View view = LayoutInflater.from(context).inflate(DevUtils.isRunningUnitTest() ? R.layout.view_news_compact_test : R.layout.view_news_compact, this, true); this.container = view.findViewById(R.id.container); this.image = view.findViewById(R.id.image); @@ -55,6 +63,18 @@ public CompactItemView(@NonNull final Context context) { this.image.getSSIV().setMaxScale(Constants.IMAGE_ZOOM_MAX); this.image.getSSIV().setPanEnabled(false); this.image.getSSIV().setZoomEnabled(false); + + this.detector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(@NonNull final MotionEvent event) { + final int[] location = new int[2]; + CompactItemView.this.image.getLocationOnScreen(location); + + CompactItemView.this.clicks.onNext(Optional.of(new Point((int)(location[0] + event.getX() + 0.5f), (int)(location[1] + event.getY() + 0.5f)))); + + return super.onSingleTapConfirmed(event); + } + }); } //region Properties @@ -98,10 +118,12 @@ public void setImages(@NonNull final List images) { if (images.isEmpty()) { this.image.setVisibility(View.GONE); } else { + this.image.getSSIV().setImage(ImageSource.resource(R.drawable.thumbnail_placeholder)); + ImageUtils.translateToFacesCenter(this.image); this.image.setVisibility(View.VISIBLE); - this.image.showImage(Uri.parse(images.get(0).getUrl())); + if (!DevUtils.isRunningUnitTest()) this.image.showImage(Uri.parse(images.get(0).getUrl())); } } @@ -160,7 +182,11 @@ public void setIsRead(final boolean isRead) { @CallSuper @Override public void onAttachedToWindow() { - this.image.setOnClickListener(view -> this.clicks.onNext(Irrelevant.INSTANCE)); + this.image.getSSIV().setOnTouchListener((view, event) -> { + this.detector.onTouchEvent(event); + + return true; + }); super.onAttachedToWindow(); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemListView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemListView.java index a60881a5..ac1871bb 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemListView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemListView.java @@ -3,7 +3,6 @@ import android.content.Context; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; -import android.view.animation.AccelerateDecelerateInterpolator; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.app.data.model.Item; @@ -26,13 +25,9 @@ protected int getLayoutId() { @NonNull @Override protected UniversalAdapter createAdapter() { - final ItemListAdapter adapter = new ItemListAdapter.Builder(this.getContext()) + return new ItemListAdapter.Builder(this.getContext()) .addBinderFactory(new FeaturedBinderFactory()) .addBinderFactory(new CozyBinderFactory()) .build(); - - adapter.setAnimationInterpolator(new AccelerateDecelerateInterpolator()); - - return adapter; } } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemView.java index 82aebcf2..0a456dd9 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/CozyItemView.java @@ -4,45 +4,53 @@ import java.util.List; import android.content.Context; +import android.graphics.Point; import android.net.Uri; import android.support.annotation.CallSuper; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.support.v4.view.GestureDetectorCompat; import android.text.Html; import android.text.TextUtils; +import android.view.GestureDetector; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; +import android.widget.ImageView; import android.widget.TextView; -import com.facebook.drawee.view.SimpleDraweeView; +import com.davemorrissey.labs.subscaleview.ImageSource; import com.github.ayltai.newspaper.Constants; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.app.data.model.Image; import com.github.ayltai.newspaper.util.DateUtils; +import com.github.ayltai.newspaper.util.DevUtils; import com.github.ayltai.newspaper.util.ImageUtils; -import com.github.ayltai.newspaper.util.Irrelevant; +import com.github.ayltai.newspaper.util.Optional; import com.github.piasy.biv.view.BigImageView; public final class CozyItemView extends ItemView { public static final int VIEW_TYPE = R.id.view_type_cozy; + private final GestureDetectorCompat detector; + //region Components - private final SimpleDraweeView avatar; - private final TextView source; - private final TextView publishDate; - private final BigImageView image; - private final TextView title; - private final TextView description; + private final ImageView avatar; + private final TextView source; + private final TextView publishDate; + private final BigImageView image; + private final TextView title; + private final TextView description; //endregion public CozyItemView(@NonNull final Context context) { super(context); - final View view = LayoutInflater.from(context).inflate(R.layout.view_news_cozy, this, true); + final View view = LayoutInflater.from(context).inflate(DevUtils.isRunningUnitTest() ? R.layout.view_news_cozy_test : R.layout.view_news_cozy, this, true); this.container = view.findViewById(R.id.container); this.avatar = view.findViewById(R.id.avatar); @@ -55,6 +63,18 @@ public CozyItemView(@NonNull final Context context) { this.image.getSSIV().setMaxScale(Constants.IMAGE_ZOOM_MAX); this.image.getSSIV().setPanEnabled(false); this.image.getSSIV().setZoomEnabled(false); + + this.detector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(@NonNull final MotionEvent event) { + final int[] location = new int[2]; + CozyItemView.this.image.getLocationOnScreen(location); + + CozyItemView.this.clicks.onNext(Optional.of(new Point((int)(location[0] + event.getX() + 0.5f), (int)(location[1] + event.getY() + 0.5f)))); + + return super.onSingleTapConfirmed(event); + } + }); } //region Properties @@ -99,10 +119,12 @@ public void setImages(@NonNull final List images) { if (images.isEmpty()) { this.image.setVisibility(View.GONE); } else { + this.image.getSSIV().setImage(ImageSource.resource(R.drawable.thumbnail_placeholder)); + ImageUtils.translateToFacesCenter(this.image); this.image.setVisibility(View.VISIBLE); - this.image.showImage(Uri.parse(images.get(0).getUrl())); + if (!DevUtils.isRunningUnitTest()) this.image.showImage(Uri.parse(images.get(0).getUrl())); } } @@ -158,7 +180,11 @@ public void setIsRead(final boolean isRead) { @CallSuper @Override public void onAttachedToWindow() { - this.image.setOnClickListener(view -> this.clicks.onNext(Irrelevant.INSTANCE)); + this.image.getSSIV().setOnTouchListener((view, event) -> { + this.detector.onTouchEvent(event); + + return true; + }); super.onAttachedToWindow(); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/DetailsView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/DetailsView.java index 1be1de77..41666138 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/DetailsView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/DetailsView.java @@ -9,6 +9,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.graphics.Point; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; @@ -22,6 +23,7 @@ import android.support.design.widget.Snackbar; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; +import android.support.v4.widget.NestedScrollView; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.text.Html; @@ -52,6 +54,7 @@ import com.github.ayltai.newspaper.util.DevUtils; import com.github.ayltai.newspaper.util.ImageUtils; import com.github.ayltai.newspaper.util.Irrelevant; +import com.github.ayltai.newspaper.util.Locatable; import com.github.ayltai.newspaper.util.RxUtils; import com.github.ayltai.newspaper.util.SimpleTextToSpeech; import com.github.ayltai.newspaper.util.SnackbarUtils; @@ -69,13 +72,17 @@ public final class DetailsView extends ItemView implements DetailsPresenter.View { @AutoValue - public abstract static class Key extends ClassKey implements Parcelable { + public abstract static class Key extends ClassKey implements Locatable, Parcelable { @NonNull public abstract NewsItem getItem(); + @Nullable + @Override + public abstract Point getLocation(); + @NonNull - public static DetailsView.Key create(@NonNull final NewsItem item) { - return new AutoValue_DetailsView_Key(item); + public static DetailsView.Key create(@NonNull final NewsItem item, @Nullable final Point location) { + return new AutoValue_DetailsView_Key(item, location); } } @@ -102,6 +109,7 @@ public static DetailsView.Key create(@NonNull final NewsItem item) { private final TextView toolbarTitle; private final View toolbarBackground; private final ViewGroup imageContainer; + private final NestedScrollView scrollView; private final View progress; private final SimpleDraweeView avatar; private final TextView source; @@ -134,6 +142,7 @@ public DetailsView(@NonNull final Context context) { this.collapsingToolbarLayout = view.findViewById(R.id.collapsingToolbarLayout); this.toolbar = view.findViewById(R.id.toolbar); this.imageContainer = view.findViewById(R.id.image_container); + this.scrollView = view.findViewById(R.id.scrollView); this.progress = view.findViewById(R.id.progress); this.avatar = view.findViewById(R.id.avatar); this.source = view.findViewById(R.id.source); @@ -261,8 +270,6 @@ public void setImages(@NonNull final List images) { ImageUtils.translateToFacesCenter(this.toolbarImage); } - this.appBarLayout.setExpanded(true, true); - if (TextUtils.isEmpty(images.get(0).getDescription())) { this.toolbarBackground.setVisibility(View.GONE); } else { @@ -272,6 +279,8 @@ public void setImages(@NonNull final List images) { this.imageContainer.addView(this.toolbarView); + this.appBarLayout.setExpanded(true, false); + if (images.size() > 1) { for (final Image image : images.subList(1, images.size() - 1)) { final View view = LayoutInflater.from(this.getContext()).inflate(R.layout.widget_image, this.imagesContainer, false); @@ -298,6 +307,8 @@ public void setVideo(@Nullable final Video video) { this.videoContainer.addView(this.videoView); } + + this.scrollView.scrollTo(0, 0); } //endregion @@ -495,6 +506,7 @@ private void subscribeImage(@NonNull final PanoramaImageView imageView, @NonNull if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), error.getMessage(), RxJava2Debug.getEnhancedStackTrace(error)); } ); + imageView.setOnClickListener(view -> this.imageClicks.onNext(image)); ((View)imageView.getParent()).setOnClickListener(view -> this.imageClicks.onNext(image)); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/FeaturedView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/FeaturedView.java index 9af813cc..21571c9a 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/FeaturedView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/FeaturedView.java @@ -3,12 +3,16 @@ import java.util.List; import android.content.Context; +import android.graphics.Point; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.view.GestureDetectorCompat; import android.text.TextUtils; import android.util.Log; +import android.view.GestureDetector; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.widget.TextView; @@ -19,12 +23,16 @@ import com.github.ayltai.newspaper.media.FrescoImageLoader; import com.github.ayltai.newspaper.util.Animations; import com.github.ayltai.newspaper.util.DevUtils; -import com.github.ayltai.newspaper.util.Irrelevant; +import com.github.ayltai.newspaper.util.Optional; import com.github.ayltai.newspaper.util.RxUtils; +import io.reactivex.disposables.Disposable; + public final class FeaturedView extends ItemView { public static final int VIEW_TYPE = R.id.view_type_featured; + private final GestureDetectorCompat detector; + //region Components private final KenBurnsView image; @@ -32,6 +40,8 @@ public final class FeaturedView extends ItemView { //endregion + private Disposable disposable; + public FeaturedView(@NonNull final Context context) { super(context); @@ -40,6 +50,18 @@ public FeaturedView(@NonNull final Context context) { this.container = view.findViewById(R.id.container); this.image = view.findViewById(R.id.featured_image); this.title = view.findViewById(R.id.title); + + this.detector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(@NonNull final MotionEvent event) { + final int[] location = new int[2]; + FeaturedView.this.image.getLocationOnScreen(location); + + FeaturedView.this.clicks.onNext(Optional.of(new Point((int)(location[0] + event.getX() + 0.5f), (int)(location[1] + event.getY() + 0.5f)))); + + return super.onSingleTapConfirmed(event); + } + }); } //region Properties @@ -54,7 +76,6 @@ public void setTitle(@Nullable final CharSequence title) { } } - @SuppressWarnings("IllegalCatch") @Override public void setImages(@NonNull final List images) { if (images.isEmpty()) { @@ -62,13 +83,19 @@ public void setImages(@NonNull final List images) { } else { if (DevUtils.isLoggable()) Log.d(this.getClass().getSimpleName(), "Featured image = " + images.get(0).getUrl()); - FrescoImageLoader.loadImage(images.get(0).getUrl()) + this.dispose(); + + this.disposable = FrescoImageLoader.loadImage(images.get(0).getUrl()) .compose(RxUtils.applyMaybeBackgroundToMainSchedulers()) .subscribe( bitmap -> { this.image.setImageBitmap(bitmap); - if (!Animations.isEnabled()) this.image.pause(); + if (Animations.isEnabled()) { + this.image.resume(); + } else { + this.image.pause(); + } }, error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), error.getMessage(), RxJava2Debug.getEnhancedStackTrace(error)); @@ -84,8 +111,31 @@ public void setImages(@NonNull final List images) { @CallSuper @Override public void onAttachedToWindow() { - this.image.setOnClickListener(view -> this.clicks.onNext(Irrelevant.INSTANCE)); + this.image.resume(); + + this.image.setOnTouchListener((view, event) -> { + this.detector.onTouchEvent(event); + + return true; + }); super.onAttachedToWindow(); } + + @CallSuper + @Override + public void onDetachedFromWindow() { + this.image.pause(); + + this.dispose(); + + super.onDetachedFromWindow(); + } + + private void dispose() { + if (this.disposable != null && !this.disposable.isDisposed()) { + this.disposable.dispose(); + this.disposable = null; + } + } } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemListView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemListView.java index f6805aa7..305f68e6 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemListView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemListView.java @@ -164,6 +164,8 @@ public void hideLoadingView() { final View view = this.findViewById(R.id.scrolling_background); if (view != null) view.setVisibility(View.VISIBLE); + + if (this.loadingView != null && Animations.isEnabled()) Animations.stopShimmerAnimation(this.loadingView); } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemView.java index 6286faa4..eb3cecf0 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/ItemView.java @@ -3,17 +3,24 @@ import java.util.Date; import java.util.List; +import android.annotation.SuppressLint; import android.content.Context; +import android.graphics.Point; +import android.os.Build; import android.support.annotation.CallSuper; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.view.GestureDetectorCompat; +import android.view.GestureDetector; +import android.view.MotionEvent; import android.view.View; import com.github.ayltai.newspaper.app.data.model.Image; import com.github.ayltai.newspaper.app.data.model.Video; import com.github.ayltai.newspaper.app.view.ItemPresenter; import com.github.ayltai.newspaper.util.Irrelevant; +import com.github.ayltai.newspaper.util.Optional; import com.github.ayltai.newspaper.widget.BaseView; import io.reactivex.Flowable; @@ -21,12 +28,26 @@ import io.reactivex.processors.PublishProcessor; public abstract class ItemView extends BaseView implements ItemPresenter.View { - protected final FlowableProcessor clicks = PublishProcessor.create(); + protected final FlowableProcessor> clicks = PublishProcessor.create(); + + private final GestureDetectorCompat detector; protected View container; protected ItemView(@NonNull final Context context) { super(context); + + this.detector = new GestureDetectorCompat(context, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(@NonNull final MotionEvent event) { + final int[] location = new int[2]; + ItemView.this.container.getLocationOnScreen(location); + + ItemView.this.clicks.onNext(Optional.of(new Point((int)(location[0] + event.getX() + 0.5f), (int)(location[1] + event.getY() + 0.5f)))); + + return super.onSingleTapConfirmed(event); + } + }); } //region Properties @@ -77,7 +98,7 @@ public void setIsRead(final boolean isRead) { @NonNull @Override - public Flowable clicks() { + public Flowable> clicks() { return this.clicks; } @@ -137,10 +158,23 @@ public Flowable videoClicks() { //endregion + @SuppressLint("NewApi") @CallSuper @Override public void onAttachedToWindow() { - if (this.container != null) this.container.setOnClickListener(view -> this.clicks.onNext(Irrelevant.INSTANCE)); + if (this.container != null) { + this.container.setOnTouchListener((view, event) -> { + this.detector.onTouchEvent(event); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) this.container + .getForeground() + .setHotspot(event.getX(), event.getY()); + + this.container.setPressed(event.getActionMasked() == MotionEvent.ACTION_DOWN || event.getActionMasked() == MotionEvent.ACTION_MOVE); + + return true; + }); + } super.onAttachedToWindow(); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/MainView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/MainView.java index 5a2981c1..145444cb 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/MainView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/MainView.java @@ -14,6 +14,7 @@ import android.support.design.internal.BottomNavigationMenuView; import android.support.design.widget.BottomNavigationView; import android.support.design.widget.FloatingActionButton; +import android.support.v4.util.ArrayMap; import android.support.v7.widget.SearchView; import android.support.v7.widget.Toolbar; import android.util.Log; @@ -37,7 +38,6 @@ import com.github.ayltai.newspaper.widget.BaseView; import flow.ClassKey; -import gnu.trove.map.hash.THashMap; import io.reactivex.Flowable; import io.reactivex.processors.FlowableProcessor; import io.reactivex.processors.PublishProcessor; @@ -62,7 +62,7 @@ static MainView.Key create() { //endregion - private final Map> cachedViews = new THashMap<>(); + private final Map> cachedViews = new ArrayMap<>(); //region Components @@ -164,7 +164,12 @@ public boolean onNavigationItemSelected(@NonNull final MenuItem item) { this.toolbar.getMenu().findItem(R.id.action_search).setVisible(true); if (!isCached) { - this.newsView = item.getItemId() == R.id.action_news ? new PagedNewsView(this.getContext()) : item.getItemId() == R.id.action_history ? new HistoricalNewsView(this.getContext()) : new BookmarkedNewsView(this.getContext()); + this.newsView = item.getItemId() == R.id.action_news + ? new PagedNewsView(this.getContext()) + : item.getItemId() == R.id.action_history + ? new HistoricalNewsView(this.getContext()) + : new BookmarkedNewsView(this.getContext()); + this.content.addView((View)this.newsView); this.cachedViews.put(item.getItemId(), new SoftReference<>((View)this.newsView)); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsAdapter.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsAdapter.java index dad78ad8..bb1f2e38 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsAdapter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsAdapter.java @@ -26,6 +26,7 @@ import com.github.ayltai.newspaper.app.ComponentFactory; import com.github.ayltai.newspaper.app.config.UserConfig; import com.github.ayltai.newspaper.app.data.model.Category; +import com.github.ayltai.newspaper.app.data.model.Item; import com.github.ayltai.newspaper.app.view.ItemListAdapter; import com.github.ayltai.newspaper.app.view.ItemListPresenter; import com.github.ayltai.newspaper.util.DevUtils; @@ -42,7 +43,7 @@ protected Filter.FilterResults performFiltering(@Nullable final CharSequence sea PagedNewsAdapter.this.searchText = searchText; for (int i = 0; i < PagedNewsAdapter.this.getCount(); i++) { - final VerticalListView listView = PagedNewsAdapter.this.getItem(i); + final VerticalListView listView = PagedNewsAdapter.this.getItem(i); if (listView instanceof ItemListView && listView.getAdapter() instanceof Filterable && ((Filterable)listView.getAdapter()).getFilter() instanceof ItemListAdapter.ItemListFilter) { ((ItemListView)listView).setSearchText(searchText); @@ -63,7 +64,7 @@ protected Filter.FilterResults performFiltering(@Nullable final CharSequence sea @Override protected void publishResults(@Nullable final CharSequence searchText, @Nullable final FilterResults filterResults) { for (int i = 0; i < PagedNewsAdapter.this.getCount(); i++) { - final VerticalListView listView = PagedNewsAdapter.this.getItem(i); + final VerticalListView listView = PagedNewsAdapter.this.getItem(i); if (listView != null && listView.getAdapter() instanceof Filterable && ((Filterable)listView.getAdapter()).getFilter() instanceof ItemListAdapter.ItemListFilter) { final FilterResults results = (FilterResults)PagedNewsAdapter.this.filterResults.get(i); @@ -130,12 +131,13 @@ public CharSequence getPageTitle(final int position) { return this.categories.get(position); } + @SuppressWarnings("unchecked") @Nullable - public VerticalListView getItem(final int position) { + public VerticalListView getItem(final int position) { final SoftReference view = this.views.get(position); if (view == null) return null; - return (VerticalListView)view.get(); + return (VerticalListView)view.get(); } public void setCurrentPosition(final int position) { @@ -145,10 +147,11 @@ public void setCurrentPosition(final int position) { @NonNull @Override public Object instantiateItem(@NonNull final ViewGroup container, final int position) { - final String category = this.categories.get(position); - final List categories = new ArrayList<>(Category.fromDisplayName(category)); - final ItemListPresenter presenter = new ItemListPresenter(categories); - final ItemListView view = this.userConfig == null || this.userConfig.getViewStyle() == Constants.VIEW_STYLE_COZY ? new CozyItemListView(container.getContext()) : new CompactItemListView(container.getContext()); + final String category = this.categories.get(position); + final List categories = new ArrayList<>(Category.fromDisplayName(category)); + final ItemListPresenter presenter = new ItemListPresenter(categories); + final VerticalListView listView = this.getItem(position); + final ItemListView view = listView == null ? this.userConfig == null || this.userConfig.getViewStyle() == Constants.VIEW_STYLE_COZY ? new CozyItemListView(container.getContext()) : new CompactItemListView(container.getContext()) : (ItemListView)listView; if (this.disposables == null) this.disposables = new CompositeDisposable(); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsView.java index 136d0cc5..3d67e06f 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/PagedNewsView.java @@ -22,6 +22,7 @@ import com.github.ayltai.newspaper.app.ComponentFactory; import com.github.ayltai.newspaper.app.MainActivity; import com.github.ayltai.newspaper.app.config.UserConfig; +import com.github.ayltai.newspaper.app.data.model.Item; import com.github.ayltai.newspaper.app.view.BaseNewsView; import com.github.ayltai.newspaper.widget.BaseView; import com.github.ayltai.newspaper.widget.VerticalListView; @@ -58,13 +59,13 @@ public void onAttachedToWindow() { @Override public void up() { - final VerticalListView view = this.adapter.getItem(this.viewPager.getCurrentItem()); + final VerticalListView view = this.adapter.getItem(this.viewPager.getCurrentItem()); if (view != null) view.up(); } @Override public void refresh() { - final VerticalListView view = this.adapter.getItem(this.viewPager.getCurrentItem()); + final VerticalListView view = this.adapter.getItem(this.viewPager.getCurrentItem()); if (view != null) view.refresh(); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/VideoView.java b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/VideoView.java index 62ced157..999256c8 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/VideoView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/app/widget/VideoView.java @@ -36,6 +36,7 @@ import com.github.ayltai.newspaper.app.config.UserConfig; import com.github.ayltai.newspaper.app.data.model.Video; import com.github.ayltai.newspaper.app.view.ItemPresenter; +import com.github.ayltai.newspaper.util.DevUtils; import com.github.ayltai.newspaper.util.DeviceUtils; import com.github.ayltai.newspaper.util.Irrelevant; import com.github.piasy.biv.view.BigImageView; @@ -112,7 +113,7 @@ public Flowable videoClicks() { //region Methods public void setUpThumbnail() { - if (!TextUtils.isEmpty(this.video.getThumbnailUrl())) this.thumbnail.showImage(Uri.parse(this.video.getThumbnailUrl())); + if (!DevUtils.isRunningUnitTest() && !TextUtils.isEmpty(this.video.getThumbnailUrl())) this.thumbnail.showImage(Uri.parse(this.video.getThumbnailUrl())); } public void setUpPlayer() { diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/AppleDailyClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/AppleDailyClient.java index b2408659..8d1be0b8 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/AppleDailyClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/AppleDailyClient.java @@ -94,12 +94,12 @@ public Single> getItems(@NonNull final String url) { } } - emitter.onSuccess(this.filter(items)); + if (!emitter.isDisposed()) emitter.onSuccess(this.filter(items)); }, error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + url, RxJava2Debug.getEnhancedStackTrace(error)); - emitter.onSuccess(Collections.emptyList()); + if (!emitter.isDisposed()) emitter.onSuccess(Collections.emptyList()); } )); } @@ -149,7 +149,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } ); }); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/ClientFactory.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/ClientFactory.java index dc13b844..140272c6 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/ClientFactory.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/ClientFactory.java @@ -5,6 +5,7 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.util.ArrayMap; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.app.data.model.SourceFactory; @@ -12,13 +13,12 @@ import com.github.ayltai.newspaper.net.DaggerHttpComponent; import com.github.ayltai.newspaper.net.HttpComponent; -import gnu.trove.map.hash.THashMap; import okhttp3.OkHttpClient; public final class ClientFactory { private static ClientFactory instance; - private final Map clients = new THashMap<>(12); + private final Map clients = new ArrayMap<>(15); @NonNull public static ClientFactory getInstance(@NonNull final Context context) { @@ -46,7 +46,10 @@ private ClientFactory(@NonNull final Context context) { this.clients.put(sources[i], new HeadlineRealtimeClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i++]))); this.clients.put(sources[i], new SkyPostClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i++]))); this.clients.put(sources[i], new HkejClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i++]))); - this.clients.put(sources[i], new RthkClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i]))); + this.clients.put(sources[i], new RthkClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i++]))); + this.clients.put(sources[i], new ScmpClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i++]))); + this.clients.put(sources[i], new TheStandardClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i++]))); + this.clients.put(sources[i], new WenWeiPoClient(client, apiService, SourceFactory.getInstance(context).getSource(sources[i]))); } @Nullable diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineClient.java index c881340c..91aa3f48 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineClient.java @@ -8,6 +8,7 @@ import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; +import android.support.v4.util.ArrayMap; import android.util.Log; import com.akaita.java.rxjava2debug.RxJava2Debug; @@ -23,7 +24,6 @@ import com.github.ayltai.newspaper.util.RxUtils; import com.github.ayltai.newspaper.util.StringUtils; -import gnu.trove.map.hash.THashMap; import io.reactivex.Single; import okhttp3.OkHttpClient; @@ -46,7 +46,7 @@ public final class HeadlineClient extends RssClient { //endregion - private static final Map KEYWORDS = new THashMap<>(8); + private static final Map KEYWORDS = new ArrayMap<>(8); static { HeadlineClient.KEYWORDS.put(HeadlineClient.CATEGORY_HONG_KONG, " (港聞) "); @@ -125,7 +125,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineRealtimeClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineRealtimeClient.java index 81312020..fce310c7 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineRealtimeClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/HeadlineRealtimeClient.java @@ -119,7 +119,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } ); }); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/HkejClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/HkejClient.java index c2e51698..5c672626 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/HkejClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/HkejClient.java @@ -65,7 +65,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/HketClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/HketClient.java index dfb429b2..e02babc5 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/HketClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/HketClient.java @@ -93,7 +93,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/MingPaoClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/MingPaoClient.java index ea5454cb..ed46c6e0 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/MingPaoClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/MingPaoClient.java @@ -56,6 +56,8 @@ final class MingPaoClient extends RssClient { @NonNull @Override public Single updateItem(@NonNull final NewsItem item) { + if (item.getLink().length() <= MingPaoClient.BASE_URI.length() || !item.getLink().contains(MingPaoClient.SLASH)) return Single.just(item); + final String[] tokens = item.getLink().substring(MingPaoClient.BASE_URI.length()).split(MingPaoClient.SLASH); final boolean isInstant = item.getLink().contains("/ins/"); @@ -93,7 +95,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/OrientalDailyClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/OrientalDailyClient.java index 9d9dce28..aa9c9221 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/OrientalDailyClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/OrientalDailyClient.java @@ -51,7 +51,7 @@ public Single updateItem(@NonNull final NewsItem item) { .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) .subscribe( html -> { - html = StringUtils.substringBetween(html, "
"); + html = StringUtils.substringBetween(html, "
"); final String[] imageContainers = StringUtils.substringsBetween(html, "
images = new ArrayList<>(); @@ -84,7 +84,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/RthkClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/RthkClient.java index b8dde88b..a44f8ae7 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/RthkClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/RthkClient.java @@ -70,7 +70,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/ScmpClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/ScmpClient.java new file mode 100644 index 00000000..950ecbb6 --- /dev/null +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/ScmpClient.java @@ -0,0 +1,89 @@ +package com.github.ayltai.newspaper.client; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.akaita.java.rxjava2debug.RxJava2Debug; +import com.github.ayltai.newspaper.Constants; +import com.github.ayltai.newspaper.app.data.model.Image; +import com.github.ayltai.newspaper.app.data.model.NewsItem; +import com.github.ayltai.newspaper.app.data.model.Source; +import com.github.ayltai.newspaper.net.ApiService; +import com.github.ayltai.newspaper.net.NetworkUtils; +import com.github.ayltai.newspaper.util.DevUtils; +import com.github.ayltai.newspaper.util.RxUtils; +import com.github.ayltai.newspaper.util.StringUtils; + +import io.reactivex.Single; +import okhttp3.OkHttpClient; + +final class ScmpClient extends RssClient { + private static final String CLOSE_DIV = "
"; + private static final String CLOSE_QUOTE = "\""; + + @Inject + ScmpClient(@NonNull final OkHttpClient client, @NonNull final ApiService apiService, @NonNull final Source source) { + super(client, apiService, source); + } + + @WorkerThread + @NonNull + @Override + public Single updateItem(@NonNull final NewsItem item) { + return Single.create(emitter -> this.apiService + .getHtml(item.getLink()) + .compose(RxUtils.applyObservableBackgroundSchedulers()) + .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) + .subscribe( + html -> { + final String[] contents = StringUtils.substringsBetween(StringUtils.substringBetween(html, "
", "

"); + final String imagesContainer = StringUtils.substringBetween(html, "
", ScmpClient.CLOSE_DIV); + final List images = new ArrayList<>(); + final StringBuilder builder = new StringBuilder(); + + if (imagesContainer != null) { + final String[] imageContainers = StringUtils.substringsBetween(imagesContainer, ""); + + for (final String imageContainer : imageContainers) { + final String imageUrl = StringUtils.substringBetween(imageContainer, "data-enlarge=\"", ScmpClient.CLOSE_QUOTE); + final String imageDescription = StringUtils.substringBetween(imageContainer, "data-caption=\"", ScmpClient.CLOSE_QUOTE); + + if (imageUrl != null) images.add(new Image(imageUrl, imageDescription)); + } + } + + for (final String content : contents) { + final String imageUrl = StringUtils.substringBetween(content, "data-original=\"", ScmpClient.CLOSE_QUOTE); + final String imageDescription = StringUtils.substringBetween(content, "
"); + } else { + images.add(new Image(imageUrl, imageDescription)); + } + } + + if (!images.isEmpty()) { + item.getImages().clear(); + item.getImages().addAll(images); + } + + item.setDescription(builder.toString()); + item.setIsFullDescription(true); + + if (!emitter.isDisposed()) emitter.onSuccess(item); + }, + error -> { + if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); + + if (!emitter.isDisposed()) emitter.onSuccess(item); + } + )); + } +} diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/SingPaoClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/SingPaoClient.java index f297ce5b..c22acddf 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/SingPaoClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/SingPaoClient.java @@ -34,6 +34,7 @@ final class SingPaoClient extends Client { private static final String BASE_URI = "https://www.singpao.com.hk/"; private static final String TAG = "'"; private static final String FONT = ""; + private static final String CLOSE = "

"; //endregion @@ -122,9 +123,10 @@ public Single updateItem(@NonNull final NewsItem item) { item.getImages().addAll(images); } - final String[] contents = StringUtils.substringsBetween(html, "

", "

"); - final StringBuilder builder = new StringBuilder(); + String[] contents = StringUtils.substringsBetween(html, "

", SingPaoClient.CLOSE); + if (contents.length == 0) contents = StringUtils.substringsBetween(html, "

", SingPaoClient.CLOSE); + final StringBuilder builder = new StringBuilder(); for (final String content : contents) builder.append(content).append("

"); item.setDescription(builder.toString()); @@ -135,7 +137,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } ); }); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoClient.java index aa440ef1..518156a1 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoClient.java @@ -137,7 +137,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), error); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } ); }); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoRealtimeClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoRealtimeClient.java index 48ae38dc..cb904112 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoRealtimeClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/SingTaoRealtimeClient.java @@ -132,7 +132,7 @@ public Single updateItem(@NonNull final NewsItem item) { error -> { if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } ); }); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/SkyPostClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/SkyPostClient.java index eed3f7eb..6d941a94 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/client/SkyPostClient.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/SkyPostClient.java @@ -43,8 +43,10 @@ final class SkyPostClient extends RssClient { @NonNull @Override public Single updateItem(@NonNull final NewsItem item) { + final String link = item.getLink().replaceAll("%", "%25"); + return Single.create(emitter -> this.apiService - .getHtml(item.getLink()) + .getHtml(link) .compose(RxUtils.applyObservableBackgroundSchedulers()) .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) .subscribe( @@ -90,9 +92,9 @@ public Single updateItem(@NonNull final NewsItem item) { if (!emitter.isDisposed()) emitter.onSuccess(item); }, error -> { - if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); + if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + link, RxJava2Debug.getEnhancedStackTrace(error)); - if (!emitter.isDisposed()) emitter.onError(error); + if (!emitter.isDisposed()) emitter.onSuccess(item); } )); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/TheStandardClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/TheStandardClient.java new file mode 100644 index 00000000..edd51cce --- /dev/null +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/TheStandardClient.java @@ -0,0 +1,170 @@ +package com.github.ayltai.newspaper.client; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.akaita.java.rxjava2debug.RxJava2Debug; +import com.github.ayltai.newspaper.Constants; +import com.github.ayltai.newspaper.app.data.model.Image; +import com.github.ayltai.newspaper.app.data.model.NewsItem; +import com.github.ayltai.newspaper.app.data.model.Source; +import com.github.ayltai.newspaper.net.ApiService; +import com.github.ayltai.newspaper.net.NetworkUtils; +import com.github.ayltai.newspaper.util.DevUtils; +import com.github.ayltai.newspaper.util.RxUtils; +import com.github.ayltai.newspaper.util.StringUtils; + +import io.reactivex.Single; +import okhttp3.OkHttpClient; + +final class TheStandardClient extends Client { + //region Constants + + private static final String BASE_URL = "http://www.thestandard.com.hk/"; + + private static final String CLOSE_QUOTE = "\""; + private static final String OPEN_HREF = " DATE_FORMAT_LONG = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("dd MMM yyyy h:mm a", Locale.ENGLISH); + } + }; + + private static final ThreadLocal DATE_FORMAT_SHORT = new ThreadLocal() { + @Override + protected DateFormat initialValue() { + return new SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH); + } + }; + + //endregion + + @Inject + TheStandardClient(@NonNull final OkHttpClient client, @NonNull final ApiService apiService, @NonNull final Source source) { + super(client, apiService, source); + } + + @WorkerThread + @NonNull + @Override + public Single> getItems(@NonNull final String url) { + final String[] tokens = url.split(Pattern.quote("?")); + final String sessionId = tokens[1].substring(tokens[1].indexOf("sid=") + 4); + + // TODO: Supports multi-page loading + return Single.create(emitter -> this.apiService + .postHtml(tokens[0], Integer.parseInt(sessionId), 1) + .compose(RxUtils.applyObservableBackgroundSchedulers()) + .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) + .subscribe( + html -> { + final String[] sections = StringUtils.substringsBetween(html, "

  • ", "
  • "); + final List items = new ArrayList<>(sections.length); + final String category = this.getCategoryName(url); + + for (final String section : sections) { + final NewsItem item = new NewsItem(); + final String link = StringUtils.substringBetween(section, TheStandardClient.OPEN_HREF, TheStandardClient.CLOSE_QUOTE); + + if (link != null) { + final String title = StringUtils.substringBetween(section, "

    ", "

    "); + final String trimmedTitle = StringUtils.substringBetween(title, "\">", "
    "); + item.setTitle(trimmedTitle == null ? title : trimmedTitle); + + item.setDescription(StringUtils.substringBetween(section, TheStandardClient.OPEN_PARAGRAPH, TheStandardClient.CLOSE_PARAGRAPH)); + item.setLink(TheStandardClient.BASE_URL + link); + item.setSource(this.source.getName()); + if (category != null) item.setCategory(category); + + final String image = StringUtils.substringBetween(section, "", ""); + try { + item.setPublishDate(TheStandardClient.DATE_FORMAT_LONG.get().parse(date)); + } catch (final ParseException e) { + try { + item.setPublishDate(TheStandardClient.DATE_FORMAT_SHORT.get().parse(date)); + } catch (final ParseException x) { + // Ignored + } + } + + items.add(item); + } + } + + if (!emitter.isDisposed()) emitter.onSuccess(this.filter(items)); + }, + error -> { + if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + url, RxJava2Debug.getEnhancedStackTrace(error)); + + if (!emitter.isDisposed()) emitter.onSuccess(Collections.emptyList()); + } + )); + } + + @WorkerThread + @NonNull + @Override + public Single updateItem(@NonNull final NewsItem item) { + return Single.create(emitter -> { + if (DevUtils.isLoggable()) Log.d(this.getClass().getSimpleName(), item.getLink()); + + this.apiService + .getHtml(item.getLink()) + .compose(RxUtils.applyObservableBackgroundSchedulers()) + .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) + .subscribe( + fullHtml -> { + final String html = StringUtils.substringBetween(fullHtml, "
    ", ""); + final String[] contents = StringUtils.substringsBetween(html, TheStandardClient.OPEN_PARAGRAPH, TheStandardClient.CLOSE_PARAGRAPH); + final StringBuilder builder = new StringBuilder(); + + for (final String content : contents) builder.append(content).append("

    "); + + final String[] imageContainers = StringUtils.substringsBetween(html, "
    ", "
    "); + final List images = new ArrayList<>(); + + for (final String imageContainer : imageContainers) { + final String imageUrl = StringUtils.substringBetween(imageContainer, TheStandardClient.OPEN_HREF, TheStandardClient.CLOSE_QUOTE); + final String imageDescription = StringUtils.substringBetween(imageContainer, "", ""); + + if (imageUrl != null) images.add(new Image(imageUrl, imageDescription)); + } + + if (!images.isEmpty()) { + item.getImages().clear(); + item.getImages().addAll(images); + } + + item.setDescription(builder.toString()); + item.setIsFullDescription(true); + + if (!emitter.isDisposed()) emitter.onSuccess(item); + }, + error -> { + if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); + + if (!emitter.isDisposed()) emitter.onSuccess(item); + } + ); + }); + } +} diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/client/WenWeiPoClient.java b/mobile/src/main/java/com/github/ayltai/newspaper/client/WenWeiPoClient.java new file mode 100644 index 00000000..27896fb6 --- /dev/null +++ b/mobile/src/main/java/com/github/ayltai/newspaper/client/WenWeiPoClient.java @@ -0,0 +1,146 @@ +package com.github.ayltai.newspaper.client; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; + +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.util.Log; + +import com.akaita.java.rxjava2debug.RxJava2Debug; +import com.github.ayltai.newspaper.Constants; +import com.github.ayltai.newspaper.app.data.model.Image; +import com.github.ayltai.newspaper.app.data.model.NewsItem; +import com.github.ayltai.newspaper.app.data.model.Source; +import com.github.ayltai.newspaper.net.ApiService; +import com.github.ayltai.newspaper.net.NetworkUtils; +import com.github.ayltai.newspaper.util.DevUtils; +import com.github.ayltai.newspaper.util.RxUtils; +import com.github.ayltai.newspaper.util.StringUtils; + +import io.reactivex.Single; +import okhttp3.OkHttpClient; + +final class WenWeiPoClient extends Client { + //region Constants + + private static final String CLOSE_QUOTE = "\""; + private static final String CLOSE_PARAGRAPH = "

    "; + private static final String LINE_BREAKS = "

    "; + + //endregion + + @Inject + WenWeiPoClient(@NonNull final OkHttpClient client, @NonNull final ApiService apiService, @NonNull final Source source) { + super(client, apiService, source); + } + + @WorkerThread + @NonNull + @Override + public Single> getItems(@NonNull final String url) { + return Single.create(emitter -> this.apiService + .getHtml(url) + .compose(RxUtils.applyObservableBackgroundSchedulers()) + .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) + .subscribe( + html -> { + final String[] sections = StringUtils.substringsBetween(html, "
    ", ""); + final List items = new ArrayList<>(sections.length); + final String category = this.getCategoryName(url); + final Calendar calendar = Calendar.getInstance(); + + for (final String section : sections) { + final NewsItem item = new NewsItem(); + final String link = StringUtils.substringBetween(section, "", "")); + item.setDescription(StringUtils.substringBetween(section, "

    ", WenWeiPoClient.CLOSE_PARAGRAPH)); + item.setLink(link); + item.setSource(this.source.getName()); + if (category != null) item.setCategory(category); + + final String image = StringUtils.substringBetween(section, "[ ", " ]

    "); + if (date != null) { + final String[] tokens = date.split("日 "); + final String[] times = tokens[1].split(":"); + + calendar.set(Calendar.DATE, Integer.parseInt(tokens[0])); + calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(times[0])); + calendar.set(Calendar.MINUTE, Integer.parseInt(times[1])); + + item.setPublishDate(calendar.getTime()); + } + + items.add(item); + } + } + + if (!emitter.isDisposed()) emitter.onSuccess(this.filter(items)); + }, + error -> { + if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + url, RxJava2Debug.getEnhancedStackTrace(error)); + + if (!emitter.isDisposed()) emitter.onSuccess(Collections.emptyList()); + } + )); + } + + @WorkerThread + @NonNull + @Override + public Single updateItem(@NonNull final NewsItem item) { + return Single.create(emitter -> { + if (DevUtils.isLoggable()) Log.d(this.getClass().getSimpleName(), item.getLink()); + + this.apiService + .getHtml(item.getLink()) + .compose(RxUtils.applyObservableBackgroundSchedulers()) + .retryWhen(RxUtils.exponentialBackoff(Constants.INITIAL_RETRY_DELAY, Constants.MAX_RETRIES, NetworkUtils::shouldRetry)) + .subscribe( + fullHtml -> { + final String html = StringUtils.substringBetween(fullHtml, "", "!-- Content end -->"); + final String[] imageContainers = StringUtils.substringsBetween(html, ""); + final List images = new ArrayList<>(); + + for (final String imageContainer : imageContainers) { + final String imageUrl = StringUtils.substringBetween(imageContainer, "src=\"", WenWeiPoClient.CLOSE_QUOTE); + final String imageDescription = StringUtils.substringBetween(imageContainer, "alt=\"", WenWeiPoClient.CLOSE_QUOTE); + + if (imageUrl != null) images.add(new Image(imageUrl, imageDescription)); + } + + if (!images.isEmpty()) { + item.getImages().clear(); + item.getImages().addAll(images); + } + + final String[] primaryContents = StringUtils.substringsBetween(html, "

    ", WenWeiPoClient.CLOSE_PARAGRAPH); + final String[] secondaryContents = StringUtils.substringsBetween(html, "

    ", WenWeiPoClient.CLOSE_PARAGRAPH); + final StringBuilder builder = new StringBuilder(); + + for (final String content : primaryContents) builder.append(content).append(WenWeiPoClient.LINE_BREAKS); + for (final String content : secondaryContents) builder.append(content).append(WenWeiPoClient.LINE_BREAKS); + + item.setDescription(builder.toString()); + item.setIsFullDescription(true); + + if (!emitter.isDisposed()) emitter.onSuccess(item); + }, + error -> { + if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), "Error URL = " + item.getLink(), RxJava2Debug.getEnhancedStackTrace(error)); + + if (!emitter.isDisposed()) emitter.onSuccess(item); + } + ); + }); + } +} diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/data/RealmLoader.java b/mobile/src/main/java/com/github/ayltai/newspaper/data/RealmLoader.java index 8823c1e3..b4493459 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/data/RealmLoader.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/data/RealmLoader.java @@ -1,16 +1,19 @@ package com.github.ayltai.newspaper.data; -import java.util.Collection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; import android.content.Context; import android.os.Bundle; import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.util.ArraySet; import android.util.Log; import com.akaita.java.rxjava2debug.RxJava2Debug; -import com.github.ayltai.newspaper.net.NetworkUtils; import com.github.ayltai.newspaper.util.DevUtils; import com.github.ayltai.newspaper.util.Irrelevant; import com.github.ayltai.newspaper.util.RxUtils; @@ -22,7 +25,7 @@ import io.reactivex.schedulers.Schedulers; import io.realm.Realm; -public abstract class RealmLoader extends RxLoader { +public abstract class RealmLoader> extends RxLoader { protected static final String KEY_REFRESH = "refresh"; private Realm realm; @@ -53,44 +56,32 @@ protected boolean isValid() { @NonNull @Override - protected Flowable load(@NonNull final Context context, @Nullable final Bundle args) { - final boolean isOnline = NetworkUtils.isOnline(context); - - if (isOnline && RealmLoader.isForceRefresh(args)) return this.loadFromRemoteSource(context, args); - - if (this.isValid()) { - return Flowable.create(emitter -> this.loadFromLocalSource(context, args) - .compose(RxUtils.applyFlowableSchedulers(this.getScheduler())) + protected Flowable> load(@NonNull final Context context, @Nullable final Bundle args) { + return Flowable.create(emitter -> { + this.loadFromLocalSource(context, args) + .zipWith(this.loadFromRemoteSource(context, args), (localItems, remoteItems) -> { + final Set results = new ArraySet<>(); + results.addAll(localItems); + results.addAll(remoteItems); + + return new ArrayList<>(results); + }) .subscribe( - data -> { - if (data instanceof Collection && !((Collection)data).isEmpty()) emitter.onNext(data); - - if (isOnline) { - this.loadFromRemoteSource(context, args) - .subscribe(items -> { - if (items instanceof Collection && !((Collection)items).isEmpty()) { - emitter.onNext(items); - } else { - emitter.onNext(data); - } - }); - } else { - emitter.onNext(data); - } - }, - error -> { - if (DevUtils.isLoggable()) Log.e(this.getClass().getSimpleName(), error.getMessage(), RxJava2Debug.getEnhancedStackTrace(error)); - }), BackpressureStrategy.LATEST); - } + results -> { + Collections.sort(results); - return this.loadFromRemoteSource(context, args); + emitter.onNext(results); + }, + emitter::onError + ); + }, BackpressureStrategy.LATEST); } @NonNull - protected abstract Flowable loadFromLocalSource(@NonNull Context context, @Nullable Bundle args); + protected abstract Flowable> loadFromLocalSource(@NonNull Context context, @Nullable Bundle args); @NonNull - protected abstract Flowable loadFromRemoteSource(@NonNull Context context, @Nullable Bundle args); + protected abstract Flowable> loadFromRemoteSource(@NonNull Context context, @Nullable Bundle args); @CallSuper @Override diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/data/RxLoader.java b/mobile/src/main/java/com/github/ayltai/newspaper/data/RxLoader.java index 330df6ac..53aaeb7a 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/data/RxLoader.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/data/RxLoader.java @@ -1,5 +1,7 @@ package com.github.ayltai.newspaper.data; +import java.util.List; + import android.content.Context; import android.os.Bundle; import android.support.annotation.CallSuper; @@ -15,7 +17,7 @@ import io.reactivex.Flowable; import io.reactivex.disposables.CompositeDisposable; -public abstract class RxLoader extends Loader { +public abstract class RxLoader extends Loader> { private final Bundle args; private CompositeDisposable disposables; @@ -74,7 +76,7 @@ protected void onReset() { } @NonNull - protected abstract Flowable load(@NonNull Context context, @Nullable Bundle args); + protected abstract Flowable> load(@NonNull Context context, @Nullable Bundle args); private void prepareDisposables() { if (this.disposables == null) this.disposables = new CompositeDisposable(); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/media/FileDataSubscriber.java b/mobile/src/main/java/com/github/ayltai/newspaper/media/FileDataSubscriber.java index 4a1adab5..f3d163c5 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/media/FileDataSubscriber.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/media/FileDataSubscriber.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.util.UUID; -import android.arch.lifecycle.LifecycleObserver; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; @@ -17,7 +16,7 @@ import com.facebook.datasource.DataSource; import com.github.ayltai.newspaper.util.IOUtils; -abstract class FileDataSubscriber extends BaseDataSubscriber> implements LifecycleObserver { +abstract class FileDataSubscriber extends BaseDataSubscriber> { private final Context context; private volatile boolean isFinished; diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/media/FrescoImageLoader.java b/mobile/src/main/java/com/github/ayltai/newspaper/media/FrescoImageLoader.java index 58fd4087..548f54cf 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/media/FrescoImageLoader.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/media/FrescoImageLoader.java @@ -20,6 +20,7 @@ import android.support.annotation.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.WorkerThread; +import android.support.v4.util.ArrayMap; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -51,7 +52,6 @@ import com.github.piasy.biv.loader.ImageLoader; import com.github.piasy.biv.view.BigImageView; -import gnu.trove.map.hash.THashMap; import io.reactivex.Maybe; import io.reactivex.Single; @@ -59,7 +59,7 @@ public final class FrescoImageLoader implements ImageLoader, Closeable, LifecycleObserver { private static final Handler HANDLER = new Handler(Looper.getMainLooper()); private static final List SOURCES = Collections.synchronizedList(new ArrayList<>()); - private static final Map CANCELLABLE_SOURCES = Collections.synchronizedMap(new THashMap<>()); + private static final Map CANCELLABLE_SOURCES = Collections.synchronizedMap(new ArrayMap<>()); protected static FrescoImageLoader instance; @@ -117,7 +117,7 @@ public static Maybe loadImage(@NonNull final String uri) { } @Override - public void loadImage(final int requestId, final Uri uri, final Callback callback) { + public void loadImage(final int requestId, final Uri uri, final ImageLoader.Callback callback) { final ImageRequest request = ImageRequest.fromUri(uri); final File file = FrescoImageLoader.getFileCache(request); @@ -125,7 +125,10 @@ public void loadImage(final int requestId, final Uri uri, final Callback callbac if (DevUtils.isLoggable()) Log.d(this.getClass().getSimpleName(), "File cache = " + file.getAbsolutePath()); if (file.exists()) { - if (callback != null) callback.onCacheHit(file); + if (callback != null) { + callback.onCacheHit(file); + callback.onSuccess(file); + } } else { if (callback != null) { callback.onStart(); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/net/ApiService.java b/mobile/src/main/java/com/github/ayltai/newspaper/net/ApiService.java index 08558c97..8f1e6478 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/net/ApiService.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/net/ApiService.java @@ -5,7 +5,10 @@ import com.github.ayltai.newspaper.rss.RssFeed; import io.reactivex.Observable; +import retrofit2.http.Field; +import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; +import retrofit2.http.POST; import retrofit2.http.Url; public interface ApiService { @@ -16,4 +19,9 @@ public interface ApiService { @NonNull @GET Observable getHtml(@Url String url); + + @NonNull + @FormUrlEncoded + @POST + Observable postHtml(@Url String url, @Field("sid") int sectionId, @Field("p") int page); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/net/HttpModule.java b/mobile/src/main/java/com/github/ayltai/newspaper/net/HttpModule.java index 0482ae00..8b8918c8 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/net/HttpModule.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/net/HttpModule.java @@ -6,12 +6,13 @@ import android.support.annotation.NonNull; -import com.github.ayltai.newspaper.util.DevUtils; +import com.github.ayltai.newspaper.BuildConfig; import dagger.Module; import dagger.Provides; import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; +import okhttp3.Response; +import okhttp3.ResponseBody; import retrofit2.Retrofit; import retrofit2.converter.scalars.ScalarsConverterFactory; import retrofit2.converter.simplexml.SimpleXmlConverterFactory; @@ -32,9 +33,26 @@ static OkHttpClient provideHttpClient() { final OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(HttpModule.TIMEOUT_CONNECT, TimeUnit.SECONDS) .readTimeout(HttpModule.TIMEOUT_READ, TimeUnit.SECONDS) - .writeTimeout(HttpModule.TIMEOUT_WRITE, TimeUnit.SECONDS); + .writeTimeout(HttpModule.TIMEOUT_WRITE, TimeUnit.SECONDS) + .addInterceptor(chain -> chain.proceed(chain.request() + .newBuilder() + .header("User-Agent", BuildConfig.APPLICATION_ID + " " + BuildConfig.VERSION_NAME) + .build())) + .addInterceptor(chain -> { + final Response response = chain.proceed(chain.request()); - if (DevUtils.isLoggable()) builder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.HEADERS)); + if (chain.request().url().host().contains("news.wenweipo.com")) { + final ResponseBody body = response.body(); + + if (body == null) return response; + + return response.newBuilder() + .body(ResponseBody.create(body.contentType(), new String(body.bytes(), "Big5"))) + .build(); + } + + return response; + }); return builder.build(); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/util/Animations.java b/mobile/src/main/java/com/github/ayltai/newspaper/util/Animations.java index 46bbc93a..0b157fd0 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/util/Animations.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/util/Animations.java @@ -6,9 +6,11 @@ import java.util.concurrent.TimeUnit; import android.animation.Animator; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Point; import android.os.Build; import android.provider.Settings; import android.support.annotation.AnimRes; @@ -16,6 +18,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.View; +import android.view.ViewAnimationUtils; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -23,10 +26,14 @@ import com.github.ayltai.newspaper.Constants; import com.github.ayltai.newspaper.R; +import flow.Direction; import io.reactivex.Flowable; import io.supercharge.shimmerlayout.ShimmerLayout; public final class Animations { + private static final String SCALE_Y = "scaleY"; + private static final String ALPHA = "alpha"; + public static final class Builder { //region Variables @@ -143,43 +150,72 @@ private Animations() { @NonNull public static Animation getAnimation(@NonNull final Context context, @AnimRes final int animationId, @IntegerRes final int durationId) { final Animation animation = AnimationUtils.loadAnimation(context, animationId); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - animation.setDuration((int)(context.getResources().getInteger(durationId) * Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1f))); - } else { - animation.setDuration(context.getResources().getInteger(durationId)); - } - + animation.setDuration(Animations.getAnimationDuration(context, durationId)); return animation; } @NonNull - public static Animation getAnimation(@NonNull final Context context, @AnimRes final int animationId, @IntegerRes final int durationId, @Nullable final Runnable onStart, @Nullable final Runnable onEnd) { - final Animation animation = Animations.getAnimation(context, animationId, durationId); + public static Animator createDefaultAnimator(@NonNull final View view, @NonNull final Direction direction, @Nullable final Point location, @Nullable final Runnable onStart, @Nullable final Runnable onEnd) { + final Animator animator; + final Point screenSize = DeviceUtils.getScreenSize(view.getContext()); + final int widthRadius = screenSize.x / 2; + final int heightRadius = screenSize.y / 2; + final int centerX = location == null ? widthRadius : location.x; + final int centerY = location == null ? heightRadius : location.y; + final int radiusX = centerX < widthRadius ? screenSize.x - centerX : centerX; + final int radiusY = centerX < heightRadius ? screenSize.y - centerX : centerX; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final float radius = (float)Math.hypot(radiusX, radiusY); + animator = ViewAnimationUtils.createCircularReveal(view, centerX, centerY, direction == Direction.FORWARD ? 0 : radius, direction == Direction.FORWARD ? radius : 0); + } else { + final AnimatorSet animators = new AnimatorSet(); + + if (direction == Direction.FORWARD) { + view.setScaleY(0); + view.setAlpha(0); - animation.setAnimationListener(new Animation.AnimationListener() { + animators.play(ObjectAnimator.ofFloat(view, Animations.SCALE_Y, 1)) + .with(ObjectAnimator.ofFloat(view, Animations.ALPHA, 1)); + } else { + animators.play(ObjectAnimator.ofFloat(view, Animations.SCALE_Y, 0)) + .with(ObjectAnimator.ofFloat(view, Animations.ALPHA, 0)); + } + + animator = animators; + } + + animator.setDuration(Animations.getAnimationDuration(view.getContext(), android.R.integer.config_mediumAnimTime)); + animator.setInterpolator(AnimationUtils.loadInterpolator(view.getContext(), direction == Direction.FORWARD ? android.R.interpolator.decelerate_cubic : android.R.interpolator.accelerate_quint)); + + animator.addListener(new Animator.AnimatorListener() { @Override - public void onAnimationStart(@NonNull final Animation animation) { + public void onAnimationStart(final Animator animator) { if (onStart != null) onStart.run(); } @Override - public void onAnimationEnd(@NonNull final Animation animation) { + public void onAnimationEnd(final Animator animator) { + if (onEnd != null) onEnd.run(); + } + + @Override + public void onAnimationCancel(final Animator animator) { if (onEnd != null) onEnd.run(); } @Override - public void onAnimationRepeat(@NonNull final Animation animation) { + public void onAnimationRepeat(final Animator animator) { } }); - return animation; + return animator; } @NonNull public static Iterable createDefaultAnimators(@NonNull final View view) { return Arrays.asList( - ObjectAnimator.ofFloat(view, "alpha", 0f, 1f), + ObjectAnimator.ofFloat(view, Animations.ALPHA, 0f, 1f), ObjectAnimator.ofFloat(view, "translationX", -view.getMeasuredWidth(), 0f) ); } @@ -202,7 +238,23 @@ public static void startShimmerAnimation(@NonNull final View view) { } } + public static void stopShimmerAnimation(@NonNull final View view) { + if (view instanceof ViewGroup) { + final ViewGroup parent = (ViewGroup)view; + + for (int i = 0; i < parent.getChildCount(); i++) Animations.stopShimmerAnimation(parent.getChildAt(i)); + + if (view instanceof ShimmerLayout) ((ShimmerLayout)view).stopShimmerAnimation(); + } + } + public static boolean isEnabled() { return !DevUtils.isRunningInstrumentedTest() && (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || ValueAnimator.areAnimatorsEnabled()); } + + private static int getAnimationDuration(@NonNull final Context context, @IntegerRes final int durationId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) return (int)(context.getResources().getInteger(durationId) * Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1f)); + + return context.getResources().getInteger(durationId); + } } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/util/Locatable.java b/mobile/src/main/java/com/github/ayltai/newspaper/util/Locatable.java new file mode 100644 index 00000000..0119b4f0 --- /dev/null +++ b/mobile/src/main/java/com/github/ayltai/newspaper/util/Locatable.java @@ -0,0 +1,9 @@ +package com.github.ayltai.newspaper.util; + +import android.graphics.Point; +import android.support.annotation.Nullable; + +public interface Locatable { + @Nullable + Point getLocation(); +} diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/util/Sets.java b/mobile/src/main/java/com/github/ayltai/newspaper/util/Sets.java index 884c33c4..eabe7d88 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/util/Sets.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/util/Sets.java @@ -4,8 +4,7 @@ import java.util.Set; import android.support.annotation.NonNull; - -import gnu.trove.set.hash.THashSet; +import android.support.v4.util.ArraySet; public final class Sets { private Sets() { @@ -13,7 +12,7 @@ private Sets() { @NonNull public static Set from(@NonNull final T[] items) { - final Set set = new THashSet<>(); + final Set set = new ArraySet<>(); Collections.addAll(set, items); return set; } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/util/SimpleTextToSpeech.java b/mobile/src/main/java/com/github/ayltai/newspaper/util/SimpleTextToSpeech.java index 22a65d60..357b9f69 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/util/SimpleTextToSpeech.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/util/SimpleTextToSpeech.java @@ -12,11 +12,11 @@ import android.speech.tts.UtteranceProgressListener; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.util.ArrayMap; import android.util.Log; import com.google.auto.value.AutoValue; -import gnu.trove.map.hash.THashMap; import io.reactivex.Single; import rx.functions.Action1; @@ -59,7 +59,7 @@ public final Single build(@NonNull final Activity activity) } } - private final Map availabilities = new THashMap<>(); + private final Map availabilities = new ArrayMap<>(); private final AtomicInteger utteranceId = new AtomicInteger(0); private Activity activity; diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/view/FloatingActionButtonBehavior.java b/mobile/src/main/java/com/github/ayltai/newspaper/view/FloatingActionButtonBehavior.java index c5a41d1c..ae86dd6d 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/view/FloatingActionButtonBehavior.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/view/FloatingActionButtonBehavior.java @@ -1,6 +1,7 @@ package com.github.ayltai.newspaper.view; import android.content.Context; +import android.support.annotation.NonNull; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CoordinatorLayout; import android.util.AttributeSet; @@ -15,7 +16,7 @@ public final class FloatingActionButtonBehavior extends CoordinatorLayout.Behavi private final float fabHeight; private final float bottomMargin; - public FloatingActionButtonBehavior(final Context context, final AttributeSet attrs) { + public FloatingActionButtonBehavior(@NonNull final Context context, final AttributeSet attrs) { super(context, attrs); this.toolbarHeight = ContextUtils.getDimensionPixelSize(context, R.attr.actionBarSize); @@ -24,12 +25,12 @@ public FloatingActionButtonBehavior(final Context context, final AttributeSet at } @Override - public boolean layoutDependsOn(final CoordinatorLayout parent, final View child, final View dependency) { + public boolean layoutDependsOn(@NonNull final CoordinatorLayout parent, @NonNull final View child, @NonNull final View dependency) { return dependency instanceof AppBarLayout; } @Override - public boolean onDependentViewChanged(final CoordinatorLayout parent, final View child, final View dependency) { + public boolean onDependentViewChanged(@NonNull final CoordinatorLayout parent, @NonNull final View child, @NonNull final View dependency) { if (this.layoutDependsOn(parent, child, dependency)) { child.setTranslationY((this.fabHeight + this.bottomMargin) * (dependency.getY() / -this.toolbarHeight)); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/view/RxFlow.java b/mobile/src/main/java/com/github/ayltai/newspaper/view/RxFlow.java index 972905a1..6f8ceba5 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/view/RxFlow.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/view/RxFlow.java @@ -4,21 +4,24 @@ import java.util.Collections; import java.util.Map; +import android.animation.Animator; import android.app.Activity; import android.content.Context; +import android.graphics.Point; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.util.ArrayMap; import android.support.v4.util.Pair; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.view.animation.Animation; import com.akaita.java.rxjava2debug.RxJava2Debug; import com.github.ayltai.newspaper.R; import com.github.ayltai.newspaper.util.Animations; import com.github.ayltai.newspaper.util.DevUtils; +import com.github.ayltai.newspaper.util.Locatable; import flow.Direction; import flow.Flow; @@ -26,13 +29,11 @@ import flow.KeyParceler; import flow.State; import flow.TraversalCallback; -import gnu.trove.map.TMap; -import gnu.trove.map.hash.THashMap; import io.reactivex.disposables.Disposable; public abstract class RxFlow { - private final Map, SoftReference>> cache = Collections.synchronizedMap(new THashMap<>()); - private final TMap disposables = new THashMap<>(); + private final Map, SoftReference>> cache = Collections.synchronizedMap(new ArrayMap<>()); + private final Map disposables = new ArrayMap<>(); private final Activity activity; @@ -54,7 +55,7 @@ protected Map, SoftReference(presenter), new SoftReference<>(view))); this.subscribe(presenter, view); - this.dispatch((View)view, incomingState, direction, callback); + this.dispatch((View)view, outgoingState, incomingState, direction, callback); } }).build()) .defaultKey(this.getDefaultKey()) @@ -104,10 +105,7 @@ public void onDestroy() { this.cache.clear(); synchronized (this.disposables) { - this.disposables.forEachValue(disposable -> { - disposable.dispose(); - return true; - }); + for (final Disposable disposable : this.disposables.values()) disposable.dispose(); this.disposables.clear(); } @@ -117,7 +115,7 @@ public void onDestroy() { protected abstract Pair onDispatch(@Nullable Object key); @SuppressWarnings("CyclomaticComplexity") - private void dispatch(@NonNull final View toView, @NonNull final State incomingState, @NonNull final Direction direction, @NonNull final TraversalCallback callback) { + private void dispatch(@NonNull final View toView, @Nullable final State outgoingState, @NonNull final State incomingState, @NonNull final Direction direction, @NonNull final TraversalCallback callback) { incomingState.restore(toView); final ViewGroup container = this.activity.findViewById(R.id.container); @@ -139,8 +137,12 @@ private void dispatch(@NonNull final View toView, @NonNull final State incomingS if (fromView != null) { if (Animations.isEnabled()) { - final Animation animation = this.getAnimation(Direction.FORWARD, null, () -> container.removeView(fromView)); - if (animation != null) toView.startAnimation(animation); + final Animator animator = this.getAnimator(toView, Direction.FORWARD, incomingState.getKey() instanceof Locatable ? ((Locatable)incomingState.getKey()).getLocation() : null, null, () -> container.removeView(fromView)); + if (animator == null) { + container.removeView(fromView); + } else { + animator.start(); + } } else { container.removeView(fromView); } @@ -150,8 +152,12 @@ private void dispatch(@NonNull final View toView, @NonNull final State incomingS if (fromView != null) { if (Animations.isEnabled()) { - final Animation animation = this.getAnimation(Direction.BACKWARD, null, () -> container.removeView(fromView)); - if (animation != null) fromView.startAnimation(animation); + final Animator animator = this.getAnimator(fromView, Direction.BACKWARD, outgoingState != null && outgoingState.getKey() instanceof Locatable ? ((Locatable)outgoingState.getKey()).getLocation() : null, null, () -> container.removeView(fromView)); + if (animator == null) { + container.removeView(fromView); + } else { + animator.start(); + } } else { container.removeView(fromView); } diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/view/SimpleUniversalAdapter.java b/mobile/src/main/java/com/github/ayltai/newspaper/view/SimpleUniversalAdapter.java index a72b77ae..b5bc6894 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/view/SimpleUniversalAdapter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/view/SimpleUniversalAdapter.java @@ -4,17 +4,17 @@ import java.util.Map; import android.support.annotation.NonNull; +import android.support.v4.util.ArrayMap; import android.view.View; import com.github.ayltai.newspaper.view.binding.Binder; import com.github.ayltai.newspaper.view.binding.FullBinderFactory; import com.github.ayltai.newspaper.widget.SimpleViewHolder; -import gnu.trove.map.hash.THashMap; import io.reactivex.disposables.Disposable; public abstract class SimpleUniversalAdapter> extends UniversalAdapter implements Disposable { - private final Map> bindings = new THashMap<>(); + private final Map> bindings = new ArrayMap<>(); protected SimpleUniversalAdapter(@NonNull final List> fullBinderFactories) { super(fullBinderFactories); diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/view/UniversalAdapter.java b/mobile/src/main/java/com/github/ayltai/newspaper/view/UniversalAdapter.java index d8e0b6ce..df784a5b 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/view/UniversalAdapter.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/view/UniversalAdapter.java @@ -28,18 +28,12 @@ public abstract class UniversalAdapter private final List> factories; private final List, Binder>> binders = new ArrayList<>(); - private int lastItemPosition; - private int animationDuration = UniversalAdapter.DEFAULT_ANIMATION_DURATION; - private Interpolator animationInterpolator = UniversalAdapter.DEFAULT_ANIMATION_INTERPOLATOR; + private int lastItemPosition; protected UniversalAdapter(@NonNull final List> factories) { this.factories = factories; } - public void setAnimationInterpolator(@Nullable final Interpolator animationInterpolator) { - this.animationInterpolator = animationInterpolator; - } - @Override public int getItemCount() { return this.binders.size(); @@ -60,6 +54,15 @@ protected Iterable getItemAnimators(@NonNull final View view) { return Collections.emptyList(); } + protected long getAnimationDuration() { + return UniversalAdapter.DEFAULT_ANIMATION_DURATION; + } + + @Nullable + protected Interpolator getAnimationInterpolator() { + return UniversalAdapter.DEFAULT_ANIMATION_INTERPOLATOR; + } + public void clear() { for (final Pair, Binder> binder : this.binders) { if (binder.second instanceof Disposable) { @@ -78,9 +81,12 @@ public void onBindViewHolder(final T holder, final int position) { final int adapterPosition = holder.getAdapterPosition(); if (adapterPosition > this.lastItemPosition) { + final Interpolator interpolator = this.getAnimationInterpolator(); + final long duration = this.getAnimationDuration(); + for (final Animator animator : this.getItemAnimators(holder.itemView)) { - animator.setInterpolator(this.animationInterpolator); - animator.setDuration(this.animationDuration).start(); + animator.setInterpolator(interpolator); + animator.setDuration(duration).start(); } this.lastItemPosition = adapterPosition; diff --git a/mobile/src/main/java/com/github/ayltai/newspaper/widget/VerticalListView.java b/mobile/src/main/java/com/github/ayltai/newspaper/widget/VerticalListView.java index d8acf806..785e5247 100644 --- a/mobile/src/main/java/com/github/ayltai/newspaper/widget/VerticalListView.java +++ b/mobile/src/main/java/com/github/ayltai/newspaper/widget/VerticalListView.java @@ -65,7 +65,7 @@ protected VerticalListView(@NonNull final Context context) { @Override public void bind(@NonNull final List models) { if (DevUtils.isLoggable()) { - for (final M model : models) Log.v(this.getClass().getSimpleName(), model.toString()); + for (final M model : models) Log.v(this.getClass().getSimpleName(), model == null ? null : model.toString()); } if (this.adapter.getItemCount() == 0 && models.isEmpty()) { diff --git a/mobile/src/main/res/anim/reveal_enter.xml b/mobile/src/main/res/anim/reveal_enter.xml index 18944d44..7be43157 100644 --- a/mobile/src/main/res/anim/reveal_enter.xml +++ b/mobile/src/main/res/anim/reveal_enter.xml @@ -3,11 +3,13 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:duration="@android:integer/config_mediumAnimTime" android:interpolator="@android:interpolator/decelerate_cubic"> - + diff --git a/mobile/src/main/res/anim/reveal_exit.xml b/mobile/src/main/res/anim/reveal_exit.xml index 9ab61c44..7d89c23b 100644 --- a/mobile/src/main/res/anim/reveal_exit.xml +++ b/mobile/src/main/res/anim/reveal_exit.xml @@ -2,12 +2,14 @@ - + android:interpolator="@android:interpolator/accelerate_quint"> + diff --git a/mobile/src/main/res/drawable-nodpi/avatar_scmp.png b/mobile/src/main/res/drawable-nodpi/avatar_scmp.png new file mode 100644 index 00000000..d33f8606 Binary files /dev/null and b/mobile/src/main/res/drawable-nodpi/avatar_scmp.png differ diff --git a/mobile/src/main/res/drawable-nodpi/avatar_the_standard.png b/mobile/src/main/res/drawable-nodpi/avatar_the_standard.png new file mode 100644 index 00000000..b4be1e7d Binary files /dev/null and b/mobile/src/main/res/drawable-nodpi/avatar_the_standard.png differ diff --git a/mobile/src/main/res/drawable-nodpi/avatar_wen_wei_po.png b/mobile/src/main/res/drawable-nodpi/avatar_wen_wei_po.png new file mode 100644 index 00000000..97106f94 Binary files /dev/null and b/mobile/src/main/res/drawable-nodpi/avatar_wen_wei_po.png differ diff --git a/mobile/src/main/res/layout/placeholder_item_compact.xml b/mobile/src/main/res/layout/placeholder_item_compact.xml index 63159a04..6a05785d 100644 --- a/mobile/src/main/res/layout/placeholder_item_compact.xml +++ b/mobile/src/main/res/layout/placeholder_item_compact.xml @@ -22,7 +22,7 @@ - + android:background="?attr/placeholderImage" /> - + android:background="?attr/placeholderImage" /> - + android:background="?attr/placeholderImage" /> - + android:background="?attr/placeholderImage" /> - - - - - - - + android:layout_height="wrap_content"> + + + + + + + + diff --git a/mobile/src/main/res/layout/placeholder_list_cozy.xml b/mobile/src/main/res/layout/placeholder_list_cozy.xml index a74b99dc..7b07d0ef 100644 --- a/mobile/src/main/res/layout/placeholder_list_cozy.xml +++ b/mobile/src/main/res/layout/placeholder_list_cozy.xml @@ -1,10 +1,14 @@ - - - - + android:layout_height="wrap_content"> + + + + + diff --git a/mobile/src/main/res/layout/view_details.xml b/mobile/src/main/res/layout/view_details.xml index abfef489..098485a5 100644 --- a/mobile/src/main/res/layout/view_details.xml +++ b/mobile/src/main/res/layout/view_details.xml @@ -5,9 +5,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/windowBackground" + android:background="?attr/cardBackground" android:fitsSystemWindows="true"> diff --git a/mobile/src/main/res/layout/view_main.xml b/mobile/src/main/res/layout/view_main.xml index 7dfb6ce1..ec007eb4 100644 --- a/mobile/src/main/res/layout/view_main.xml +++ b/mobile/src/main/res/layout/view_main.xml @@ -10,6 +10,7 @@ android:id="@+id/content" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingBottom="@dimen/bottombar_height" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + app:initScaleType="centerCrop" + app:tapToRetry="true" /> + + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/view_news_cozy.xml b/mobile/src/main/res/layout/view_news_cozy.xml index e2aadb6d..b51ea4f1 100644 --- a/mobile/src/main/res/layout/view_news_cozy.xml +++ b/mobile/src/main/res/layout/view_news_cozy.xml @@ -71,7 +71,8 @@ android:layout_below="@+id/avatar" android:layout_alignWithParentIfMissing="true" android:background="?attr/placeholderImage" - app:initScaleType="centerCrop" /> + app:initScaleType="centerCrop" + app:tapToRetry="true" /> + + + + + + + + + + + + diff --git a/mobile/src/main/res/layout/widget_video_player.xml b/mobile/src/main/res/layout/widget_video_player.xml index 957fcd70..fd977e53 100644 --- a/mobile/src/main/res/layout/widget_video_player.xml +++ b/mobile/src/main/res/layout/widget_video_player.xml @@ -5,4 +5,5 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" + android:keepScreenOn="true" app:resize_mode="fit" /> diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 4dcc15a5..c9243552 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -56,37 +56,40 @@ Sources Categories - 蘋果日報 - 東方日報 - 星島日報 - 星島即時 - 經濟日報 - 成報 - 明報 - 頭條日報 - 頭條即時 - 晴報 - 信報 - 香港電台 + 蘋果日報 + 東方日報 + 星島日報 + 星島即時 + 經濟日報 + 成報 + 明報 + 頭條日報 + 頭條即時 + 晴報 + 信報 + 香港電台 + 南華早報 + 英文虎報 + 文匯報 - 港聞 - 國際 - 兩岸 - 經濟 - 地產 - 娛樂 - 體育 - 副刊 - 教育 - 即時港聞 - 即時國際 - 即時兩岸 - 即時經濟 - 即時地產 - 即時娛樂 - 即時體育 - 即時副刊 + 港聞 + 國際 + 兩岸 + 經濟 + 地產 + 娛樂 + 體育 + 副刊 + 教育 + 即時港聞 + 即時國際 + 即時兩岸 + 即時經濟 + 即時地產 + 即時娛樂 + 即時體育 + 即時副刊 diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml index f610b9e4..608a4362 100644 --- a/mobile/src/main/res/values/styles.xml +++ b/mobile/src/main/res/values/styles.xml @@ -60,9 +60,9 @@ 2 @dimen/textHeading @color/textColorInverseLight - 1.5 - 1.5 - 1.5 + 2.5 + 2.5 + 2.5 @color/shadowEnd