diff --git a/build.gradle b/build.gradle index 87f16da..b605ec5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,8 @@ project.ext { archComp = '1.0.0' rxjava = '2.1.7' rxAndroid = '2.0.1' - genericRecyclerViewAdapter = '1.6.2' - rxredux = '2.0.0' + genericRecyclerViewAdapter = '1.8.0' + rxredux = '2.1.1' glide = '4.0.0' lottie = '2.2.0' retrofit = '2.3.0' @@ -16,7 +16,8 @@ project.ext { leakCanary = '1.5.4' androidSupportTest = '1.0.1' espressoCore = '3.0.1' - powerMock = '1.7.3' + powerMock = '1.6.6' +// powerMock = '1.7.3' robolectric = '3.3.2' // robolectric = '3.5.1' okhttpIdelingResource = '1.0.0' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c27ee3..7a3265e 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 27b8ec9..f16d266 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Mon Dec 11 16:14:29 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip diff --git a/gradlew b/gradlew index 4453cce..cccdd3d 100755 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/sampleApp/build.gradle b/sampleApp/build.gradle index b16eaa5..c17a566 100644 --- a/sampleApp/build.gradle +++ b/sampleApp/build.gradle @@ -1,29 +1,15 @@ -buildscript { - repositories { - mavenCentral() - } - - dependencies { - classpath 'me.tatarka:gradle-retrolambda:3.7.0' - } -} - -repositories { - mavenCentral() -} - apply plugin: 'com.android.application' -apply plugin: 'me.tatarka.retrolambda' +apply plugin: 'kotlin-android' apply plugin: 'realm-android' android { - compileSdkVersion 25 - buildToolsVersion '25.0.3' + compileSdkVersion 26 + buildToolsVersion '26.0.2' defaultConfig { applicationId "com.zeyad.usecase.accesslayer" minSdkVersion 21 - targetSdkVersion 25 + targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "com.zeyad.usecases.app.UseCasesTestRunner" @@ -107,17 +93,6 @@ android { } } -ext { - supportLibrary = '25.4.0' - butterKnife = '8.6.0' - rxbinding = '2.0.0' - rxLifeCycle = '2.0.1' - leakCanary = '1.5.1' - androidSupportTest = '0.5' - espressoCore = '2.2.2' - archComp = '1.0.0-alpha5' -} - dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':usecases') @@ -132,12 +107,12 @@ dependencies { compile 'com.android.support.constraint:constraint-layout:1.0.2' // Bootstrap - compile 'com.github.Zeyad-37:GenericRecyclerViewAdapter:1.3.0' - compile 'com.github.Zeyad-37:RxRedux:1.5.2' + compile "com.github.Zeyad-37:GenericRecyclerViewAdapter:$genericRecyclerViewAdapter" + compile "com.github.Zeyad-37:RxRedux:$rxredux" // compile "android.arch.lifecycle:runtime:$archComp" - compile "android.arch.lifecycle:extensions:$archComp" - compile "android.arch.lifecycle:reactivestreams:$archComp" +// compile "android.arch.lifecycle:extensions:$archComp" +// compile "android.arch.lifecycle:reactivestreams:$archComp" // Network compile 'com.github.bumptech.glide:glide:3.7.0' // Rx @@ -146,17 +121,19 @@ dependencies { compile "com.jakewharton.rxbinding2:rxbinding-appcompat-v7:$rxbinding" compile "com.jakewharton.rxbinding2:rxbinding-design:$rxbinding" compile "com.jakewharton.rxbinding2:rxbinding-recyclerview-v7:$rxbinding" - compile "com.trello.rxlifecycle2:rxlifecycle:$rxLifeCycle" - compile "com.trello.rxlifecycle2:rxlifecycle-components:$rxLifeCycle" +// compile "com.trello.rxlifecycle2:rxlifecycle:$rxLifeCycle" +// compile "com.trello.rxlifecycle2:rxlifecycle-components:$rxLifeCycle" // Injection compile "com.jakewharton:butterknife:$butterKnife" annotationProcessor "com.jakewharton:butterknife-compiler:$butterKnife" // Utilities - compile 'com.airbnb.android:lottie:2.1.0' - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' - testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' - annotationProcessor "org.parceler:parceler:1.1.9" + compile "nl.littlerobots.rxlint:rxlint:$rxlint" + compile 'com.airbnb.android:lottie:2.2.0' + debugCompile "com.squareup.leakcanary:leakcanary-android:$leakCanary" + releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanary" + testCompile "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanary" + compile 'org.parceler:parceler-api:1.1.9' + annotationProcessor 'org.parceler:parceler:1.1.9' // compile("io.flowup:android-sdk:0.2.4") { // exclude group: 'com.google.android.gms' // } @@ -168,17 +145,19 @@ dependencies { androidTestCompile "com.android.support.test.espresso:espresso-core:$espressoCore" androidTestCompile 'junit:junit:4.12' androidTestCompile 'org.hamcrest:hamcrest-library:1.3' - androidTestCompile 'org.mockito:mockito-core:1.10.19' + androidTestCompile "org.mockito:mockito-core:$mockito" androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile('com.jakewharton.espresso:okhttp3-idling-resource:1.0.0') { exclude group: 'com.android.support' } - androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.0' - androidTestCompile 'com.github.andrzejchm.RESTMock:android:0.1.4' + androidTestCompile "com.squareup.okhttp3:mockwebserver:$okhttpVersion" + androidTestCompile "com.github.andrzejchm.RESTMock:android:$restMock" testCompile 'junit:junit:4.12' - testCompile 'org.mockito:mockito-core:1.10.19' + testCompile "org.mockito:mockito-core:$mockito" + debugCompile 'com.21buttons:fragment-test-rule:1.0.0' + compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" } //apply from: "$project.rootDir/tools/script-git-version.gradle" diff --git a/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/RecyclerViewItemCountAssertion.java b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/RecyclerViewItemCountAssertion.java new file mode 100644 index 0000000..9469853 --- /dev/null +++ b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/RecyclerViewItemCountAssertion.java @@ -0,0 +1,40 @@ +package com.zeyad.usecases.app.screens; + +import android.support.test.espresso.NoMatchingViewException; +import android.support.test.espresso.ViewAssertion; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import org.hamcrest.Matcher; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * @author by ZIaDo on 7/31/17. + */ +public class RecyclerViewItemCountAssertion implements ViewAssertion { + private final Matcher matcher; + + private RecyclerViewItemCountAssertion(Matcher matcher) { + this.matcher = matcher; + } + + public static RecyclerViewItemCountAssertion withItemCount(int expectedCount) { + return withItemCount(is(expectedCount)); + } + + public static RecyclerViewItemCountAssertion withItemCount(Matcher matcher) { + return new RecyclerViewItemCountAssertion(matcher); + } + + @Override + public void check(View view, NoMatchingViewException noViewFoundException) { + if (noViewFoundException != null) { + throw noViewFoundException; + } + RecyclerView recyclerView = (RecyclerView) view; + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + assertThat(adapter.getItemCount(), matcher); + } +} diff --git a/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/splash/UserListActivityTest.java b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/splash/UserListActivityTest.java new file mode 100644 index 0000000..f18756b --- /dev/null +++ b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/splash/UserListActivityTest.java @@ -0,0 +1,19 @@ +package com.zeyad.usecases.app.screens.splash; + +import android.support.test.rule.ActivityTestRule; +import android.test.suitebuilder.annotation.LargeTest; + +import org.junit.Rule; +import org.junit.Test; + +@LargeTest +//@RunWith(AndroidJUnit4.class) +public class UserListActivityTest { + + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(SplashActivity.class); + + @Test + public void userListActivityTest() { + } +} diff --git a/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest.java b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest.java new file mode 100644 index 0000000..cd1807e --- /dev/null +++ b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest.java @@ -0,0 +1,47 @@ +package com.zeyad.usecases.app.screens.user.detail; + +import android.content.Intent; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; + +import com.zeyad.usecases.app.screens.user.list.User; + +import org.junit.Rule; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author by ZIaDo on 8/1/17. + */ +public class UserDetailFragmentTest { + private User user; + private List repositories; + @Rule + public ActivityTestRule mActivityTestRule = new ActivityTestRule( + UserDetailActivity.class) { + @Override + protected Intent getActivityIntent() { + return UserDetailActivity.getCallingIntent( + InstrumentationRegistry.getInstrumentation().getTargetContext(), UserDetailState.builder() + .setIsTwoPane(false).setRepos(mockRepos()).setUser(mockUser().getLogin()).build()); + } + }; + + private User mockUser() { + user = new User(); + user.setAvatarUrl("https://avatars2.githubusercontent.com/u/5938141?v=3"); + user.setId(5938141); + user.setLogin("Zeyad-37"); + return user; + } + + private List mockRepos() { + repositories = new ArrayList<>(); + Repository repository = new Repository(); + repository.setId(1); + repository.setName("Repo"); + repository.setOwner(user); + return repositories; + } +} diff --git a/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest2.java b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest2.java new file mode 100644 index 0000000..a273504 --- /dev/null +++ b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest2.java @@ -0,0 +1,37 @@ +package com.zeyad.usecases.app.screens.user.detail; + +import com.android21buttons.fragmenttestrule.FragmentTestRule; +import com.zeyad.usecases.app.screens.user.list.User; + +import org.junit.Rule; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author by ZIaDo on 8/1/17. + */ +public class UserDetailFragmentTest2 { + @Rule + public FragmentTestRule fragmentTestRule = + FragmentTestRule.create(UserDetailFragment.class); + private User user; + private List repositories; + + private User mockUser() { + user = new User(); + user.setAvatarUrl("https://avatars2.githubusercontent.com/u/5938141?v=3"); + user.setId(5938141); + user.setLogin("Zeyad-37"); + return user; + } + + private List mockRepos() { + repositories = new ArrayList<>(); + Repository repository = new Repository(); + repository.setId(1); + repository.setName("Repo"); + repository.setOwner(user); + return repositories; + } +} diff --git a/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/list/UserListActivityTest.java b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/list/UserListActivityTest.java index 12aca2b..761599b 100644 --- a/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/list/UserListActivityTest.java +++ b/sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/list/UserListActivityTest.java @@ -1,12 +1,23 @@ package com.zeyad.usecases.app.screens.user.list; +import android.support.test.espresso.ViewInteraction; +import android.support.test.filters.LargeTest; import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; import com.zeyad.usecases.app.OkHttpIdlingResourceRule; +import com.zeyad.usecases.app.R; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -15,12 +26,20 @@ import io.appflate.restmock.RequestsVerifier; import okhttp3.mockwebserver.MockResponse; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static com.zeyad.usecases.app.screens.RecyclerViewItemCountAssertion.withItemCount; import static io.appflate.restmock.utils.RequestMatchers.pathEndsWith; import static io.appflate.restmock.utils.RequestMatchers.pathStartsWith; +import static org.hamcrest.Matchers.allOf; /** * @author by ZIaDo on 6/15/17. */ +@LargeTest +@RunWith(AndroidJUnit4.class) public class UserListActivityTest { private static final String USER_LIST_BODY = "{ \"login\" : \"octocat\", \"followers\" : 1500 }"; private final String urlPart = "users?since=0"; @@ -34,11 +53,35 @@ public class UserListActivityTest { // @Rule // public MockWebServerRule mockWebServerRule = new MockWebServerRule(); + private static Matcher childAtPosition(final Matcher parentMatcher, final int position) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + parentMatcher.describeTo(description.appendText("Child at position " + position + " in parent ")); + } + + @Override + public boolean matchesSafely(View view) { + ViewParent parent = view.getParent(); + return parent instanceof ViewGroup && parentMatcher.matches(parent) + && view.equals(((ViewGroup) parent).getChildAt(position)); + } + }; + } + @Before public void before() { RESTMockServer.reset(); } + @Test + public void userListActivityTest() { + ViewInteraction recyclerView = onView(allOf(withId(R.id.user_list), + childAtPosition(childAtPosition(withId(R.id.frameLayout), 0), 0), isDisplayed())); + recyclerView.check(matches(isDisplayed())); + onView(withId(R.id.user_list)).check(withItemCount(30)); + } + @Test public void followers() throws IOException, InterruptedException { RESTMockServer.whenGET(pathEndsWith(urlPart)) diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/GenericApplication.java b/sampleApp/src/main/java/com/zeyad/usecases/app/GenericApplication.java index c432f03..5b5a9b6 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/GenericApplication.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/GenericApplication.java @@ -8,12 +8,14 @@ import android.content.pm.PackageManager; import android.content.pm.Signature; import android.os.StrictMode; +import android.provider.Settings; import android.support.annotation.NonNull; import android.util.Base64; import android.util.Log; import com.rollbar.android.Rollbar; import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; import com.zeyad.rxredux.core.eventbus.RxEventBusFactory; import com.zeyad.usecases.api.DataServiceConfig; import com.zeyad.usecases.api.DataServiceFactory; @@ -28,6 +30,7 @@ import javax.net.ssl.X509TrustManager; import io.reactivex.Completable; +import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; import io.realm.RealmConfiguration; @@ -45,6 +48,9 @@ */ public class GenericApplication extends Application { private static final int TIME_OUT = 15; + private Disposable disposable; + private RefWatcher refwatcher; + @TargetApi(value = 24) private static boolean checkAppSignature(Context context) { try { @@ -106,9 +112,9 @@ public void onCreate() { if (LeakCanary.isInAnalyzerProcess(this)) { return; } - // initializeStrictMode(); - LeakCanary.install(this); - Completable.fromAction(() -> { + initializeStrictMode(); + refwatcher = LeakCanary.install(this); + disposable = Completable.fromAction(() -> { if (!checkAppTampering(this)) { throw new IllegalAccessException("App might be tampered with!"); } @@ -163,13 +169,10 @@ String getApiBaseUrl() { return API_BASE_URL; } - private void initializeStrictMode() { - if (BuildConfig.DEBUG) { - StrictMode.setThreadPolicy( - new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); - StrictMode.setVmPolicy( - new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); - } + @Override + public void onTerminate() { + disposable.dispose(); + super.onTerminate(); } private void initializeRealm() { @@ -194,7 +197,21 @@ X509TrustManager getX509TrustManager() { return null; } + private void initializeStrictMode() { + if (BuildConfig.DEBUG + || "true".equals(Settings.System.getString(getContentResolver(), "firebase.test.lab"))) { + StrictMode.setThreadPolicy( + new StrictMode.ThreadPolicy.Builder().detectAll().penaltyDeath().penaltyLog().build()); + StrictMode + .setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().penaltyDeath().build()); + } + } + SSLSocketFactory getSSlSocketFactory() { return null; } + + public RefWatcher getRefwatcher() { + return refwatcher; + } } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/components/ScrollEventCalculator.java b/sampleApp/src/main/java/com/zeyad/usecases/app/components/ScrollEventCalculator.java new file mode 100644 index 0000000..514bbaf --- /dev/null +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/components/ScrollEventCalculator.java @@ -0,0 +1,26 @@ +package com.zeyad.usecases.app.components; + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; + +import com.jakewharton.rxbinding2.support.v7.widget.RecyclerViewScrollEvent; + +public class ScrollEventCalculator { + + private ScrollEventCalculator() { + } + + /** + * Determine if the scroll event at the end of the recycler view. + * + * @return true if at end of linear list recycler view, false otherwise. + */ + public static boolean isAtScrollEnd(RecyclerViewScrollEvent recyclerViewScrollEvent) { + RecyclerView.LayoutManager layoutManager = recyclerViewScrollEvent.view().getLayoutManager(); + if (layoutManager instanceof LinearLayoutManager) { + LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager; + return linearLayoutManager.getItemCount() <= (linearLayoutManager.findLastVisibleItemPosition() + 2); + } + return false; + } +} diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseActivity.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseActivity.java index 03103c0..86cf80f 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseActivity.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseActivity.java @@ -1,5 +1,6 @@ package com.zeyad.usecases.app.screens; +import android.os.Parcelable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.util.Pair; @@ -9,12 +10,11 @@ import com.zeyad.rxredux.core.redux.BaseViewModel; import com.zeyad.usecases.app.components.snackbar.SnackBarFactory; -import java.util.List; - /** * @author by ZIaDo on 7/21/17. */ -public abstract class BaseActivity> extends com.zeyad.rxredux.core.redux.BaseActivity { +public abstract class BaseActivity> extends + com.zeyad.rxredux.core.redux.prelollipop.BaseActivity { /** * Adds a {@link Fragment} to this activity's layout. @@ -23,7 +23,7 @@ public abstract class BaseActivity> extends com.z * @param fragment The fragment to be added. */ public void addFragment(int containerViewId, Fragment fragment, String currentFragTag, - List> sharedElements) { + Pair... sharedElements) { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); if (sharedElements != null) { for (Pair pair : sharedElements) { @@ -67,18 +67,18 @@ public void showSnackBarMessage(View view, String message, int duration) { } public void showSnackBarWithAction(String typeSnackBar, View view, - String message, String actionText, View.OnClickListener onClickListener) { + String message, String actionText, View.OnClickListener onClickListener) { if (view != null) { SnackBarFactory.getSnackBarWithAction( typeSnackBar, view, message, actionText, onClickListener) - .show(); + .show(); } else { throw new IllegalArgumentException("View is null"); } } public void showSnackBarWithAction(String typeSnackBar, View view, - String message, int actionText, View.OnClickListener onClickListener) { + String message, int actionText, View.OnClickListener onClickListener) { showSnackBarWithAction(typeSnackBar, view, message, getString(actionText), onClickListener); } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseFragment.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseFragment.java index 89a8231..8c713d2 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseFragment.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/BaseFragment.java @@ -1,16 +1,19 @@ package com.zeyad.usecases.app.screens; +import android.os.Parcelable; import android.view.View; import android.widget.Toast; import com.zeyad.rxredux.core.redux.BaseViewModel; +import com.zeyad.usecases.app.GenericApplication; import com.zeyad.usecases.app.components.snackbar.SnackBarFactory; /** * @author by ZIaDo on 7/21/17. */ -public abstract class BaseFragment> extends com.zeyad.rxredux.core.redux.BaseFragment { +public abstract class BaseFragment> + extends com.zeyad.rxredux.core.redux.prelollipop.BaseFragment { public void showToastMessage(String message) { showToastMessage(message, Toast.LENGTH_LONG); @@ -62,4 +65,10 @@ public void showErrorSnackBar(String message, View view, int duration) { throw new IllegalArgumentException("View is null"); } } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ((GenericApplication) getContext().getApplicationContext()).getRefwatcher().watch(this); + } } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/ViewModelFactory.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/ViewModelFactory.java new file mode 100644 index 0000000..0f94a02 --- /dev/null +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/ViewModelFactory.java @@ -0,0 +1,25 @@ +package com.zeyad.usecases.app.screens.user; + +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; +import android.support.annotation.NonNull; + +import com.zeyad.usecases.app.screens.user.detail.UserDetailVM; +import com.zeyad.usecases.app.screens.user.list.UserListVM; + +/** + * @author ZIaDo on 12/13/17. + */ +public class ViewModelFactory extends ViewModelProvider.NewInstanceFactory { + + @NonNull + @Override + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(UserListVM.class)) { + return (T) new UserListVM(); + } else if (modelClass.isAssignableFrom(UserDetailVM.class)) { + return (T) new UserDetailVM(); + } + throw new IllegalArgumentException("Unknown ViewModel class: " + modelClass.getSimpleName()); + } +} diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/GetReposEvent.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/GetReposEvent.java index 9cba2e0..5879fa5 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/GetReposEvent.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/GetReposEvent.java @@ -5,14 +5,15 @@ /** * @author by ZIaDo on 4/22/17. */ -class GetReposEvent implements BaseEvent { +class GetReposEvent implements BaseEvent { private final String login; GetReposEvent(String login) { this.login = login; } - String getLogin() { + @Override + public String getPayLoad() { return login; } } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragment.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragment.java index 92f567c..8e3df07 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragment.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragment.java @@ -1,26 +1,5 @@ package com.zeyad.usecases.app.screens.user.detail; -import static com.zeyad.rxredux.core.redux.BaseActivity.UI_MODEL; - -import java.util.ArrayList; -import java.util.List; - -import org.parceler.Parcels; - -import com.bumptech.glide.Glide; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; -import com.bumptech.glide.request.RequestListener; -import com.bumptech.glide.request.target.Target; -import com.zeyad.gadapter.GenericRecyclerViewAdapter; -import com.zeyad.gadapter.ItemInfo; -import com.zeyad.rxredux.core.redux.ErrorMessageFactory; -import com.zeyad.usecases.api.DataServiceFactory; -import com.zeyad.usecases.app.R; -import com.zeyad.usecases.app.screens.BaseFragment; -import com.zeyad.usecases.app.screens.user.list.User; -import com.zeyad.usecases.app.screens.user.list.UserListActivity; -import com.zeyad.usecases.app.utils.Utils; - import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.graphics.Bitmap; @@ -43,10 +22,32 @@ import android.view.ViewGroup; import android.widget.LinearLayout; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.zeyad.gadapter.GenericRecyclerViewAdapter; +import com.zeyad.gadapter.ItemInfo; +import com.zeyad.rxredux.core.redux.BaseEvent; +import com.zeyad.rxredux.core.redux.ErrorMessageFactory; +import com.zeyad.usecases.api.DataServiceFactory; +import com.zeyad.usecases.app.R; +import com.zeyad.usecases.app.screens.BaseFragment; +import com.zeyad.usecases.app.screens.user.list.User; +import com.zeyad.usecases.app.screens.user.list.UserListActivity; +import com.zeyad.usecases.app.utils.Utils; + +import org.parceler.Parcels; + +import java.util.ArrayList; +import java.util.List; + import butterknife.BindView; import butterknife.ButterKnife; import io.reactivex.Observable; +import static com.zeyad.rxredux.core.redux.BaseActivity.UI_MODEL; + /** * A fragment representing a single Repository detail screen. This fragment is either contained in a * {@link UserListActivity} in two-pane mode (on tablets) or a {@link UserDetailActivity} on @@ -101,12 +102,12 @@ public void initialize() { viewState = Parcels.unwrap(arguments.getParcelable(UI_MODEL)); } viewModel = ViewModelProviders.of(this).get(UserDetailVM.class); - viewModel.init((newResult, event, currentStateBundle) -> UserDetailState.builder() - .setRepos((List) newResult) - .setUser(currentStateBundle.getUserLogin()) - .setIsTwoPane(currentStateBundle.isTwoPane()) - .build(), viewState, DataServiceFactory.getInstance()); - events = Observable.just(new GetReposEvent(viewState.getUserLogin())); + viewModel.init(DataServiceFactory.getInstance()); + } + + @Override + public Observable events() { + return Observable.just(new GetReposEvent(viewState.getUserLogin())); } @Override @@ -124,7 +125,7 @@ private void setupRecyclerView() { getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE), new ArrayList<>()) { @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new RepositoryViewHolder(mLayoutInflater.inflate(viewType, parent, false)); + return new RepositoryViewHolder(getLayoutInflater().inflate(viewType, parent, false)); } }; recyclerViewRepositories.setAdapter(repositoriesAdapter); @@ -136,9 +137,12 @@ public void renderSuccessState(UserDetailState userDetailState) { User user = viewState.getOwner(); List repoModels = viewState.getRepos(); if (Utils.isNotEmpty(repoModels)) { - repositoriesAdapter.setDataList(Observable.fromIterable(repoModels) + repositoriesAdapter.animateTo(Observable.fromIterable(repoModels) .map(repository -> new ItemInfo(repository, R.layout.repo_item_layout)) .toList(repoModels.size()).blockingGet()); +// repositoriesAdapter.setDataList(Observable.fromIterable(repoModels) +// .map(repository -> new ItemInfo(repository, R.layout.repo_item_layout)) +// .toList(repoModels.size()).blockingGet()); } if (user != null) { RequestListener requestListener = new RequestListener() { @@ -165,7 +169,7 @@ public boolean onResourceReady(GlideDrawable resource, String model, Target CREATOR = new Creator() { + @Override + public UserDetailState createFromParcel(Parcel in) { + return new UserDetailState(in); + } + + @Override + public UserDetailState[] newArray(int size) { + return new UserDetailState[size]; + } + }; boolean isTwoPane; String userLogin; @Transient @@ -30,10 +42,26 @@ private UserDetailState(Builder builder) { repos = builder.repos; } + protected UserDetailState(Parcel in) { + isTwoPane = in.readByte() != 0; + userLogin = in.readString(); + } + public static Builder builder() { return new Builder(); } + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte((byte) (isTwoPane ? 1 : 0)); + dest.writeString(userLogin); + } + + @Override + public int describeContents() { + return 0; + } + boolean isTwoPane() { return isTwoPane; } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailVM.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailVM.java index dfdf708..700a42b 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailVM.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/detail/UserDetailVM.java @@ -2,7 +2,7 @@ import com.zeyad.rxredux.core.redux.BaseEvent; import com.zeyad.rxredux.core.redux.BaseViewModel; -import com.zeyad.rxredux.core.redux.SuccessStateAccumulator; +import com.zeyad.rxredux.core.redux.StateReducer; import com.zeyad.usecases.api.IDataService; import com.zeyad.usecases.app.utils.Utils; import com.zeyad.usecases.requests.GetRequest; @@ -22,18 +22,24 @@ public class UserDetailVM extends BaseViewModel { private IDataService dataUseCase; @Override - public void init(SuccessStateAccumulator successStateAccumulator, - UserDetailState initialState, Object... otherDependencies) { - setSuccessStateAccumulator(successStateAccumulator); - setInitialState(initialState); + public void init(Object... otherDependencies) { if (dataUseCase == null) { dataUseCase = (IDataService) otherDependencies[0]; } } @Override - public Function> mapEventsToExecutables() { - return event -> getRepositories(((GetReposEvent) event).getLogin()); + public StateReducer stateReducer() { + return (newResult, event, currentStateBundle) -> UserDetailState.builder() + .setRepos((List) newResult) + .setUser(currentStateBundle.getUserLogin()) + .setIsTwoPane(currentStateBundle.isTwoPane()) + .build(); + } + + @Override + protected Function> mapEventsToActions() { + return event -> getRepositories(((GetReposEvent) event).getPayLoad()); } public Flowable> getRepositories(String userLogin) { diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserDiffCallBack.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserDiffCallBack.java new file mode 100644 index 0000000..c7c3977 --- /dev/null +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserDiffCallBack.java @@ -0,0 +1,51 @@ +package com.zeyad.usecases.app.screens.user.list; + +import android.support.annotation.Nullable; +import android.support.v7.util.DiffUtil; + +import com.zeyad.gadapter.ItemInfo; + +import java.util.List; + +/** + * @author ZIaDo on 12/13/17. + */ + +public class UserDiffCallBack extends DiffUtil.Callback { + + List oldUsers; + List newUsers; + + public UserDiffCallBack(List newUsers, List oldUsers) { + this.newUsers = newUsers; + this.oldUsers = oldUsers; + } + + @Override + public int getOldListSize() { + return oldUsers.size(); + } + + @Override + public int getNewListSize() { + return newUsers.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return oldUsers.get(oldItemPosition).getId() == newUsers.get(newItemPosition).getId(); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + return oldUsers.get(oldItemPosition).getData().equals(newUsers.get(newItemPosition) + .getData()); + } + + @Nullable + @Override + public Object getChangePayload(int oldItemPosition, int newItemPosition) { + //you can return particular field for changed item. + return super.getChangePayload(oldItemPosition, newItemPosition); + } +} diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListActivity.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListActivity.java index b715f16..19dc309 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListActivity.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListActivity.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; +import android.support.v7.util.DiffUtil; import android.support.v7.view.ActionMode; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -26,21 +27,17 @@ import com.jakewharton.rxbinding2.support.v7.widget.RxRecyclerView; import com.jakewharton.rxbinding2.support.v7.widget.RxSearchView; -import com.jakewharton.rxbinding2.view.RxMenuItem; import com.zeyad.gadapter.GenericRecyclerViewAdapter; import com.zeyad.gadapter.ItemInfo; import com.zeyad.gadapter.OnStartDragListener; import com.zeyad.gadapter.SimpleItemTouchHelperCallback; -import com.zeyad.gadapter.fastscroll.FastScroller; -import com.zeyad.gadapter.stickyheaders.StickyLayoutManager; -import com.zeyad.gadapter.stickyheaders.exposed.StickyHeaderListener; import com.zeyad.rxredux.core.redux.BaseEvent; import com.zeyad.rxredux.core.redux.ErrorMessageFactory; -import com.zeyad.rxredux.core.redux.SuccessStateAccumulator; -import com.zeyad.rxredux.core.redux.UISubscriber; import com.zeyad.usecases.api.DataServiceFactory; import com.zeyad.usecases.app.R; +import com.zeyad.usecases.app.components.ScrollEventCalculator; import com.zeyad.usecases.app.screens.BaseActivity; +import com.zeyad.usecases.app.screens.user.ViewModelFactory; import com.zeyad.usecases.app.screens.user.detail.UserDetailActivity; import com.zeyad.usecases.app.screens.user.detail.UserDetailFragment; import com.zeyad.usecases.app.screens.user.detail.UserDetailState; @@ -53,19 +50,17 @@ import com.zeyad.usecases.app.utils.Utils; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; import butterknife.BindView; import butterknife.ButterKnife; -import io.reactivex.BackpressureStrategy; import io.reactivex.Observable; import io.reactivex.Single; +import io.reactivex.subjects.PublishSubject; -import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING; import static com.zeyad.gadapter.ItemInfo.SECTION_HEADER; +import static java.util.Collections.singletonList; /** * An activity representing a list of Repos. This activity has different presentations for handset @@ -73,8 +68,9 @@ * lead to a {@link UserDetailActivity} representing item details. On tablets, the activity presents * the list of items and item details side-by-side using two vertical panes. */ -public class UserListActivity extends BaseActivity implements OnStartDragListener, ActionMode.Callback { - public static final int PAGE_SIZE = 6; +public class UserListActivity extends BaseActivity implements + OnStartDragListener, ActionMode.Callback { + public static final String FIRED = "fired!"; @BindView(R.id.imageView_avatar) public ImageView imageViewAvatar; @@ -88,8 +84,8 @@ public class UserListActivity extends BaseActivity im @BindView(R.id.user_list) RecyclerView userRecycler; - @BindView(R.id.fastscroll) - FastScroller fastScroller; +// @BindView(R.id.fastscroll) +// FastScroller fastScroller; private ItemTouchHelper itemTouchHelper; private GenericRecyclerViewAdapter usersAdapter; @@ -97,6 +93,9 @@ public class UserListActivity extends BaseActivity im private String currentFragTag; private boolean twoPane; + private PublishSubject postOnResumeEvents = PublishSubject.create(); + private Observable eventObservable; + public static Intent getCallingIntent(Context context) { return new Intent(context, UserListActivity.class); } @@ -109,35 +108,17 @@ public ErrorMessageFactory errorMessageFactory() { @Override public void initialize() { - viewModel = ViewModelProviders.of(this).get(UserListVM.class); - viewModel.init(getUserListStateSuccessStateAccumulator(), viewState, DataServiceFactory.getInstance()); + eventObservable = Observable.empty(); + viewModel = ViewModelProviders.of(this, new ViewModelFactory()).get(UserListVM.class); + viewModel.init(DataServiceFactory.getInstance()); if (viewState == null) { - events = Single. just(new GetPaginatedUsersEvent(0)) - .doOnSuccess(event -> Log.d("GetPaginatedUsersEvent", "fired!")) - .toObservable(); + eventObservable = Single.just(new GetPaginatedUsersEvent(0)) + .doOnSuccess(event -> Log.d("GetPaginatedUsersEvent", FIRED)).toObservable(); } - rxEventBus.toFlowable() - .compose(bindToLifecycle()) - .subscribe(stream -> events.mergeWith((Observable) stream) - .toFlowable(BackpressureStrategy.BUFFER) - .compose(uiModelsTransformer) - .compose(bindToLifecycle()) - .subscribe(new UISubscriber<>(this, errorMessageFactory()))); } -// @Override -// protected void onResume() { -// super.onResume(); -// viewModel.getUser() - // .compose(bindToLifecycle()) - // .doOnCancel(() -> Log.d("Test", "Cancelled")) - // .subscribe(user -> Log.d("Test", user.toString()), - // throwable -> { - // }); -// } - @Override - public void setupUI() { + public void setupUI(boolean isNew) { setContentView(R.layout.activity_user_list); ButterKnife.bind(this); setSupportActionBar(toolbar); @@ -146,53 +127,36 @@ public void setupUI() { twoPane = findViewById(R.id.user_detail_container) != null; } - @NonNull - private SuccessStateAccumulator getUserListStateSuccessStateAccumulator() { - return (newResult, event, currentStateBundle) -> { - List resultList = (List) newResult; - List users = currentStateBundle == null ? new ArrayList<>() : - currentStateBundle.getUsers(); - List searchList = new ArrayList<>(); - switch (event) { - case "GetPaginatedUsersEvent": - users.addAll(resultList); - break; - case "SearchUsersEvent": - searchList.clear(); - searchList.addAll(resultList); - break; - case "DeleteUsersEvent": - users = Observable.fromIterable(users) - .filter(user -> !resultList.contains((long) user.getId())) - .distinct() - .toList() - .blockingGet(); - break; - default: - break; - } - int lastId = users.get(users.size() - 1).getId(); - users = new ArrayList<>(new HashSet<>(users)); - Collections.sort(users, (user1, user2) -> - String.valueOf(user1.getId()).compareTo(String.valueOf(user2.getId()))); - return UserListState.builder().users(users).searchList(searchList).lastId(lastId).build(); - }; + @Override + public Observable events() { + return Observable.merge(eventObservable, initialEvent()).mergeWith(postOnResumeEvents()); + } + + private Observable postOnResumeEvents() { + return postOnResumeEvents; + } + + private Observable initialEvent() { +// if (viewState == null) { + return Observable.just(new GetPaginatedUsersEvent(0)) + .doOnNext(event -> Log.d("GetPaginatedUsersEvent", FIRED)); +// } +// return Observable.just(viewState); } @Override public void renderSuccessState(UserListState state) { - viewState = state; - List users = viewState.getUsers(); - List searchList = viewState.getSearchList(); + List users = state.getUsers(); + List searchList = state.getSearchList(); if (Utils.isNotEmpty(searchList)) { - usersAdapter.setDataList(Observable.fromIterable(searchList) - .map(user -> new ItemInfo(user, R.layout.user_item_layout).setId(user.getId())) - .toList(users.size()).blockingGet()); +// usersAdapter.animateTo(searchList); + usersAdapter.setDataList(searchList, + DiffUtil.calculateDiff(new UserDiffCallBack(searchList, + usersAdapter.getAdapterData()))); } else if (Utils.isNotEmpty(users)) { - usersAdapter.setDataList(Observable.fromIterable(users) - .map(user -> new ItemInfo(user, R.layout.user_item_layout).setId(user.getId())) - .toList(users.size()).blockingGet()); - // usersAdapter.addSectionHeader(0, "1st Section"); +// usersAdapter.animateTo(users); + usersAdapter.setDataList(users, + DiffUtil.calculateDiff(new UserDiffCallBack(users, usersAdapter.getDataList()))); } } @@ -214,28 +178,26 @@ private void setupRecyclerView() { public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case SECTION_HEADER: - return new SectionHeaderViewHolder(mLayoutInflater.inflate(R.layout.section_header_layout, - parent, false)); + return new SectionHeaderViewHolder(getLayoutInflater() + .inflate(R.layout.section_header_layout, parent, false)); case R.layout.empty_view: - return new EmptyViewHolder(mLayoutInflater.inflate(R.layout.empty_view, - parent, false)); + return new EmptyViewHolder(getLayoutInflater() + .inflate(R.layout.empty_view, parent, false)); case R.layout.user_item_layout: - return new UserViewHolder(mLayoutInflater.inflate(R.layout.user_item_layout, - parent, false)); + return new UserViewHolder(getLayoutInflater() + .inflate(R.layout.user_item_layout, parent, false)); default: - return null; + throw new IllegalArgumentException("Could not find view of type " + viewType); } } }; - // usersAdapter.setSectionTitleProvider(i -> "Section " + (i + 1)); usersAdapter.setAreItemsClickable(true); usersAdapter.setOnItemClickListener((position, itemInfo, holder) -> { if (actionMode != null) { toggleSelection(position); } else if (itemInfo.getData() instanceof User) { - User userModel = (User) itemInfo.getData(); - UserDetailState userDetailState = UserDetailState.builder() - .setUser(userModel) + User userModel = itemInfo.getData(); + UserDetailState userDetailState = UserDetailState.builder().setUser(userModel.getLogin()) .setIsTwoPane(twoPane) .build(); Pair pair = null; @@ -248,21 +210,19 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { secondPair = Pair.create(textViewTitle, textViewTitle.getTransitionName()); } if (twoPane) { - List> pairs = new ArrayList<>(); - pairs.add(pair); - pairs.add(secondPair); if (Utils.isNotEmpty(currentFragTag)) { removeFragment(currentFragTag); } UserDetailFragment orderDetailFragment = UserDetailFragment.newInstance(userDetailState); currentFragTag = orderDetailFragment.getClass().getSimpleName() + userModel.getId(); - addFragment(R.id.user_detail_container, orderDetailFragment, currentFragTag, pairs); + addFragment(R.id.user_detail_container, orderDetailFragment, currentFragTag, + pair, secondPair); } else { if (Utils.hasLollipop()) { - ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, - pair, secondPair); - navigator.navigateTo(this, UserDetailActivity.getCallingIntent(this, - userDetailState), options); + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, pair, + secondPair); + navigator.navigateTo(this, UserDetailActivity.getCallingIntent(this, userDetailState), + options); } else { navigator.navigateTo(this, UserDetailActivity.getCallingIntent(this, userDetailState)); } @@ -276,56 +236,21 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { } return true; }); - usersAdapter.setOnItemSwipeListener(itemInfo -> { - events = events.mergeWith(Observable.defer(() -> - Observable.just(new DeleteUsersEvent(Collections.singletonList(((User) itemInfo.getData()).getLogin()))) - .doOnEach(notification -> Log.d("DeleteEvent", "fired!")))); - rxEventBus.send(events); - }); - LinearLayoutManager layoutManager = new LinearLayoutManager(this); - StickyLayoutManager stickyLayoutManager = new TopSnappedStickyLayoutManager(this, usersAdapter); - stickyLayoutManager.setStickyHeaderListener(new StickyHeaderListener() { - @Override - public void headerAttached(View headerView, int adapterPosition) { - Log.d("Listener", "Attached with position: " + adapterPosition); - } - - @Override - public void headerDetached(View headerView, int adapterPosition) { - Log.d("Listener", "Detached with position: " + adapterPosition); - } - }); - userRecycler.setLayoutManager(stickyLayoutManager); - // userRecycler.setLayoutManager(layoutManager); + eventObservable = eventObservable.mergeWith(usersAdapter.getItemSwipeObservable() + .map(itemInfo -> + new DeleteUsersEvent(singletonList(((User) itemInfo.getData()).getLogin()))) + .doOnEach(notification -> Log.d("DeleteEvent", FIRED))); + userRecycler.setLayoutManager(new LinearLayoutManager(this)); userRecycler.setAdapter(usersAdapter); usersAdapter.setAllowSelection(true); - fastScroller.setRecyclerView(userRecycler); - // fastScroller.setViewProvider(new DefaultScrollerViewProvider()); - // fastScroller.setBubbleColor(0xffff0000); - // fastScroller.setHandleColor(0xffff0000); - // fastScroller.setBubbleTextAppearance(R.style.StyledScrollerTextAppearance); - events = events.mergeWith(Observable.defer(() -> - RxRecyclerView.scrollStateChanges(userRecycler) - .map(integer -> { - if (integer == SCROLL_STATE_SETTLING) { - int totalItemCount = layoutManager.getItemCount(); - int firstVisibleItemPosition = layoutManager - .findFirstVisibleItemPosition(); - return (layoutManager.getChildCount() + firstVisibleItemPosition) >= - totalItemCount && - firstVisibleItemPosition >= 0 && totalItemCount >= - PAGE_SIZE - ? - new GetPaginatedUsersEvent(viewState.getLastId()) : - new GetPaginatedUsersEvent(-1); - } else { - return new GetPaginatedUsersEvent(-1); - } - }) - .filter(usersNextPageEvent -> usersNextPageEvent.getLastId() != -1) - .throttleLast(200, TimeUnit.MILLISECONDS) - .debounce(300, TimeUnit.MILLISECONDS) - .doOnNext(searchUsersEvent -> Log.d("NextPageEvent", "fired!")))); +// fastScroller.setRecyclerView(userRecycler); + eventObservable = eventObservable.mergeWith(RxRecyclerView.scrollEvents(userRecycler) + .map(recyclerViewScrollEvent -> new GetPaginatedUsersEvent(ScrollEventCalculator + .isAtScrollEnd(recyclerViewScrollEvent) ? viewState.getLastId() : -1)) + .filter(usersNextPageEvent -> usersNextPageEvent.getPayLoad() != -1) + .throttleLast(200, TimeUnit.MILLISECONDS) + .debounce(300, TimeUnit.MILLISECONDS) + .doOnNext(searchUsersEvent -> Log.d("NextPageEvent", FIRED))); itemTouchHelper = new ItemTouchHelper(new SimpleItemTouchHelperCallback(usersAdapter)); itemTouchHelper.attachToRecyclerView(userRecycler); } @@ -337,18 +262,15 @@ public boolean onCreateOptionsMenu(Menu menu) { SearchView searchView = (SearchView) menu.findItem(R.id.menu_search).getActionView(); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); searchView.setOnCloseListener(() -> { - events = events.mergeWith(Single. just(new GetPaginatedUsersEvent(viewState.getLastId())) - .doOnSuccess(event -> Log.d("CloseSearchViewEvent", "fired!")) - .toObservable()); - rxEventBus.send(events); + postOnResumeEvents.onNext(new GetPaginatedUsersEvent(viewState == null ? -1 : viewState.getLastId())); return false; }); - events = events.mergeWith(RxSearchView.queryTextChanges(searchView) - .filter(charSequence -> !charSequence.toString().isEmpty()) - .map(query -> new SearchUsersEvent(query.toString())) - .throttleLast(100, TimeUnit.MILLISECONDS) - .debounce(200, TimeUnit.MILLISECONDS) - .doOnNext(searchUsersEvent -> Log.d("SearchEvent", "eventFired"))); + eventObservable = eventObservable.mergeWith(RxSearchView.queryTextChanges(searchView) + .filter(charSequence -> !charSequence.toString().isEmpty()) + .map(query -> new SearchUsersEvent(query.toString())) + .throttleLast(100, TimeUnit.MILLISECONDS) + .debounce(200, TimeUnit.MILLISECONDS) + .doOnEach(searchUsersEvent -> Log.d("SearchEvent", FIRED))); return super.onCreateOptionsMenu(menu); } @@ -374,16 +296,12 @@ private void toggleSelection(int position) { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.getMenuInflater().inflate(R.menu.selected_list_menu, menu); - events = events.mergeWith(Observable.defer(() -> - RxMenuItem.clicks(menu.findItem(R.id.delete_item)) - .map(click -> new DeleteUsersEvent(Observable.fromIterable(usersAdapter.getSelectedItems()) - .map(itemInfo -> ((User) itemInfo.getData()).getLogin()) - .toList().blockingGet())) - .doOnEach(notification -> { - actionMode.finish(); - Log.d("DeleteEvent", "fired!"); - }))); - rxEventBus.send(events); + menu.findItem(R.id.delete_item).setOnMenuItemClickListener(menuItem -> { + postOnResumeEvents.onNext(new DeleteUsersEvent(Observable.fromIterable(usersAdapter.getSelectedItems()) + .map(itemInfo -> itemInfo.getData().getLogin()).toList() + .blockingGet())); + return true; + }); return true; } @@ -404,7 +322,7 @@ public void onDestroyActionMode(ActionMode mode) { try { usersAdapter.clearSelection(); } catch (Exception e) { - e.printStackTrace(); + Log.e("onDestroyActionMode", e.getMessage(), e); } actionMode = null; toolbar.setVisibility(View.VISIBLE); diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListState.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListState.java index 02626e3..6932c31 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListState.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListState.java @@ -1,20 +1,37 @@ package com.zeyad.usecases.app.screens.user.list; -import org.parceler.Parcel; +import android.os.Parcel; +import android.os.Parcelable; + +import com.zeyad.gadapter.ItemInfo; +import com.zeyad.usecases.app.R; + import org.parceler.Transient; import java.util.ArrayList; import java.util.List; +import io.reactivex.Observable; + /** * @author by ZIaDo on 1/28/17. */ -@Parcel -public class UserListState { +public class UserListState implements Parcelable { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public UserListState createFromParcel(Parcel source) { + return new UserListState(source); + } + + @Override + public UserListState[] newArray(int size) { + return new UserListState[size]; + } + }; @Transient - List users; + List users; @Transient - List searchList; + List searchList; long lastId; UserListState() { @@ -27,15 +44,23 @@ private UserListState(Builder builder) { lastId = builder.lastId; } + protected UserListState(Parcel in) { + this.users = new ArrayList<>(); + // in.readList(this.users, User.class.getClassLoader()); + this.searchList = new ArrayList<>(); + // in.readList(this.searchList, User.class.getClassLoader()); + this.lastId = in.readLong(); + } + static Builder builder() { return new Builder(); } - List getUsers() { + List getUsers() { return users; } - List getSearchList() { + List getSearchList() { return searchList; } @@ -58,21 +83,37 @@ public boolean equals(Object o) { return lastId == that.lastId; } + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // dest.writeList(this.users); + // dest.writeList(this.searchList); + dest.writeLong(this.lastId); + } + static class Builder { - List users; - List searchList; + List users; + List searchList; long lastId; Builder() { } Builder users(List value) { - users = value; + users = Observable.fromIterable(value) + .map(user -> new ItemInfo(user, R.layout.user_item_layout).setId(user.getId())) + .toList(value.size()).blockingGet(); return this; } Builder searchList(List value) { - searchList = value; + searchList = Observable.fromIterable(value) + .map(user -> new ItemInfo(user, R.layout.user_item_layout).setId(user.getId())) + .toList().blockingGet(); return this; } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListVM.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListVM.java index cbc6c25..22cf081 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListVM.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserListVM.java @@ -1,8 +1,9 @@ package com.zeyad.usecases.app.screens.user.list; +import com.zeyad.gadapter.ItemInfo; import com.zeyad.rxredux.core.redux.BaseEvent; import com.zeyad.rxredux.core.redux.BaseViewModel; -import com.zeyad.rxredux.core.redux.SuccessStateAccumulator; +import com.zeyad.rxredux.core.redux.StateReducer; import com.zeyad.usecases.api.IDataService; import com.zeyad.usecases.app.screens.user.list.events.DeleteUsersEvent; import com.zeyad.usecases.app.screens.user.list.events.GetPaginatedUsersEvent; @@ -16,6 +17,7 @@ import java.util.List; import io.reactivex.Flowable; +import io.reactivex.Observable; import io.reactivex.functions.BiFunction; import io.reactivex.functions.Function; @@ -30,27 +32,59 @@ public class UserListVM extends BaseViewModel { private IDataService dataUseCase; @Override - public void init(SuccessStateAccumulator successStateAccumulator, - UserListState initialState, Object... otherDependencies) { + public void init(Object... otherDependencies) { if (dataUseCase == null) { dataUseCase = (IDataService) otherDependencies[0]; } - setSuccessStateAccumulator(successStateAccumulator); - setInitialState(initialState); } @Override - public Function> mapEventsToExecutables() { + public StateReducer stateReducer() { + return (newResult, event, currentStateBundle) -> { + List resultList = (List) newResult; + List users; + if (currentStateBundle == null || currentStateBundle.getUsers() == null) + users = new ArrayList<>(); + else users = Observable.fromIterable(currentStateBundle.getUsers()) + .map(ItemInfo::getData).toList().blockingGet(); + List searchList = new ArrayList<>(); + switch (event) { + case "GetPaginatedUsersEvent": + users.addAll(resultList); + break; + case "SearchUsersEvent": + searchList.addAll(resultList); + break; + case "DeleteUsersEvent": + users = Observable.fromIterable(users) + .filter(user -> !resultList.contains((long) user.getId())) + .distinct() + .toList() + .blockingGet(); + break; + default: + break; + } + int lastId = users.get(users.size() - 1).getId(); + users = new ArrayList<>(new HashSet<>(users)); + Collections.sort(users, (user1, user2) -> + String.valueOf(user1.getId()).compareTo(String.valueOf(user2.getId()))); + return UserListState.builder().users(users).searchList(searchList).lastId(lastId).build(); + }; + } + + @Override + protected Function> mapEventsToActions() { return event -> { - Flowable executable = Flowable.empty(); + Flowable action = Flowable.empty(); if (event instanceof GetPaginatedUsersEvent) { - executable = getUsers(((GetPaginatedUsersEvent) event).getLastId()); + action = getUsers(((GetPaginatedUsersEvent) event).getPayLoad()); } else if (event instanceof DeleteUsersEvent) { - executable = deleteCollection(((DeleteUsersEvent) event).getSelectedItemsIds()); + action = deleteCollection(((DeleteUsersEvent) event).getPayLoad()); } else if (event instanceof SearchUsersEvent) { - executable = search(((SearchUsersEvent) event).getQuery()); + action = search(((SearchUsersEvent) event).getPayLoad()); } - return executable; + return action; }; } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/DeleteUsersEvent.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/DeleteUsersEvent.java index 0078822..439f29f 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/DeleteUsersEvent.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/DeleteUsersEvent.java @@ -4,8 +4,10 @@ import java.util.List; -/** @author by ZIaDo on 3/27/17. */ -public final class DeleteUsersEvent implements BaseEvent { +/** + * @author by ZIaDo on 3/27/17. + */ +public final class DeleteUsersEvent implements BaseEvent> { private final List selectedItemsIds; @@ -13,7 +15,8 @@ public DeleteUsersEvent(List selectedItemsIds) { this.selectedItemsIds = selectedItemsIds; } - public List getSelectedItemsIds() { + @Override + public List getPayLoad() { return selectedItemsIds; } } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/GetPaginatedUsersEvent.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/GetPaginatedUsersEvent.java index a22eb20..ac87e50 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/GetPaginatedUsersEvent.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/GetPaginatedUsersEvent.java @@ -2,8 +2,10 @@ import com.zeyad.rxredux.core.redux.BaseEvent; -/** @author by ZIaDo on 4/19/17. */ -public class GetPaginatedUsersEvent implements BaseEvent { +/** + * @author by ZIaDo on 4/19/17. + */ +public class GetPaginatedUsersEvent implements BaseEvent { private final long lastId; @@ -11,7 +13,8 @@ public GetPaginatedUsersEvent(long lastId) { this.lastId = lastId; } - public long getLastId() { + @Override + public Long getPayLoad() { return lastId; } } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/SearchUsersEvent.java b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/SearchUsersEvent.java index 5df55c4..a951d68 100644 --- a/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/SearchUsersEvent.java +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/events/SearchUsersEvent.java @@ -2,8 +2,10 @@ import com.zeyad.rxredux.core.redux.BaseEvent; -/** @author by ZIaDo on 4/20/17. */ -public class SearchUsersEvent implements BaseEvent { +/** + * @author by ZIaDo on 4/20/17. + */ +public class SearchUsersEvent implements BaseEvent { private final String query; @@ -11,7 +13,8 @@ public SearchUsersEvent(String s) { query = s; } - public String getQuery() { + @Override + public String getPayLoad() { return query; } } diff --git a/sampleApp/src/main/java/com/zeyad/usecases/app/utils/CipherUtil.java b/sampleApp/src/main/java/com/zeyad/usecases/app/utils/CipherUtil.java new file mode 100644 index 0000000..7bc4908 --- /dev/null +++ b/sampleApp/src/main/java/com/zeyad/usecases/app/utils/CipherUtil.java @@ -0,0 +1,149 @@ +package com.zeyad.usecases.app.utils; + +import android.content.Context; +import android.icu.util.Calendar; +import android.os.Build; +import android.preference.PreferenceManager; +import android.security.KeyPairGeneratorSpec; +import android.support.annotation.RequiresApi; +import android.util.Base64; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.util.Locale; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.security.auth.x500.X500Principal; + +import io.realm.RealmConfiguration; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * @author by ZIaDo on 8/15/17. + */ +public class CipherUtil { + + @RequiresApi(api = Build.VERSION_CODES.N) + public static KeyStore.Entry key(Context context) + throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, + NoSuchProviderException, InvalidAlgorithmParameterException, UnrecoverableEntryException { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + boolean containAlias = keyStore.containsAlias("com.zeyad.usecases.app"); + if (!containAlias) { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); + Calendar start = Calendar.getInstance(Locale.ENGLISH); + Calendar end = Calendar.getInstance(Locale.ENGLISH); + end.add(Calendar.YEAR, 99); + KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context).setAlias("com.zeyad.usecases.app") + .setSubject(new X500Principal(X500Principal.CANONICAL)).setSerialNumber(BigInteger.ONE) + .setStartDate(start.getTime()).setEndDate(end.getTime()).build(); + kpg.initialize(spec); + kpg.generateKeyPair(); + } + return keyStore.getEntry("com.zeyad.usecases.app", null); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static String decrypt(String cipherText) throws KeyStoreException, CertificateException, + NoSuchAlgorithmException, IOException, UnrecoverableEntryException, IllegalBlockSizeException, + InvalidKeyException, BadPaddingException, NoSuchPaddingException { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + KeyStore.Entry entry = keyStore.getEntry("com.zeyad.usecases.app", null); + if (entry instanceof KeyStore.PrivateKeyEntry) { + return new String(cipherUsingKey(null, ((KeyStore.PrivateKeyEntry) entry).getPrivateKey(), false, + Base64.decode(cipherText.getBytes(UTF_8), Base64.DEFAULT))); + } + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static byte[] decryptToByteArray(String cipherText) throws KeyStoreException, CertificateException, + NoSuchAlgorithmException, IOException, UnrecoverableEntryException, IllegalBlockSizeException, + InvalidKeyException, BadPaddingException, NoSuchPaddingException { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + KeyStore.Entry entry = keyStore.getEntry("com.zeyad.usecases.app", null); + if (entry instanceof KeyStore.PrivateKeyEntry) { + return cipherUsingKey(null, ((KeyStore.PrivateKeyEntry) entry).getPrivateKey(), false, + Base64.decode(cipherText.getBytes(UTF_8), Base64.DEFAULT)); + } + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static String encrypt(Context context, String plainText) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableEntryException, + NoSuchProviderException, InvalidAlgorithmParameterException, IOException, IllegalBlockSizeException, + InvalidKeyException, BadPaddingException, NoSuchPaddingException { + KeyStore.Entry entry = key(context); + if (entry instanceof KeyStore.PrivateKeyEntry) { + return new String( + Base64.encode(cipherUsingKey(((KeyStore.PrivateKeyEntry) entry).getCertificate().getPublicKey(), + null, true, plainText.getBytes(UTF_8)), Base64.DEFAULT)); + } + return null; + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static String encrypt(Context context, byte[] plainText) + throws CertificateException, NoSuchAlgorithmException, KeyStoreException, UnrecoverableEntryException, + NoSuchProviderException, InvalidAlgorithmParameterException, IOException, IllegalBlockSizeException, + InvalidKeyException, BadPaddingException, NoSuchPaddingException { + KeyStore.Entry entry = key(context); + if (entry instanceof KeyStore.PrivateKeyEntry) { + return new String( + Base64.encode(cipherUsingKey(((KeyStore.PrivateKeyEntry) entry).getCertificate().getPublicKey(), + null, true, plainText), Base64.DEFAULT)); + } + return null; + } + + public static byte[] cipherUsingKey(PublicKey publicKey, PrivateKey privateKey, boolean encrypt, byte[] bytes) + throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, + IllegalBlockSizeException { + Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + inCipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.ENCRYPT_MODE, encrypt ? publicKey : privateKey); + return inCipher.doFinal(bytes); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public static String createSecureRealmKey(Context context) + throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, + UnrecoverableEntryException, InvalidAlgorithmParameterException, IllegalBlockSizeException, + NoSuchProviderException, BadPaddingException, NoSuchPaddingException, KeyStoreException { + byte[] realmkey = new byte[RealmConfiguration.KEY_LENGTH]; + new SecureRandom().nextBytes(realmkey); + return encrypt(context, realmkey); + } + + // disable in manifest allowBackUp = true for encryption + @RequiresApi(api = Build.VERSION_CODES.N) + public static byte[] getRealmKey(Context context) + throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeyException, + UnrecoverableEntryException, InvalidAlgorithmParameterException, IllegalBlockSizeException, + BadPaddingException, NoSuchPaddingException, KeyStoreException, NoSuchProviderException { + String loadedKey = PreferenceManager.getDefaultSharedPreferences(context).getString("realmKey", ""); + if (loadedKey.isEmpty()) { + loadedKey = createSecureRealmKey(context); + } + return decryptToByteArray(loadedKey); + } +} diff --git a/sampleApp/src/main/res/anim/grid_layout_animation_from_bottom.xml b/sampleApp/src/main/res/anim/grid_layout_animation_from_bottom.xml new file mode 100644 index 0000000..d766c14 --- /dev/null +++ b/sampleApp/src/main/res/anim/grid_layout_animation_from_bottom.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/sampleApp/src/main/res/anim/grid_layout_animation_scale.xml b/sampleApp/src/main/res/anim/grid_layout_animation_scale.xml new file mode 100644 index 0000000..b21c116 --- /dev/null +++ b/sampleApp/src/main/res/anim/grid_layout_animation_scale.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/sampleApp/src/main/res/anim/grid_layout_animation_scale_random.xml b/sampleApp/src/main/res/anim/grid_layout_animation_scale_random.xml new file mode 100644 index 0000000..0c76322 --- /dev/null +++ b/sampleApp/src/main/res/anim/grid_layout_animation_scale_random.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/sampleApp/src/main/res/anim/item_animation_fall_down.xml b/sampleApp/src/main/res/anim/item_animation_fall_down.xml new file mode 100644 index 0000000..f486805 --- /dev/null +++ b/sampleApp/src/main/res/anim/item_animation_fall_down.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/sampleApp/src/main/res/anim/item_animation_from_bottom.xml b/sampleApp/src/main/res/anim/item_animation_from_bottom.xml new file mode 100644 index 0000000..a9dfde9 --- /dev/null +++ b/sampleApp/src/main/res/anim/item_animation_from_bottom.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/sampleApp/src/main/res/anim/item_animation_from_right.xml b/sampleApp/src/main/res/anim/item_animation_from_right.xml new file mode 100644 index 0000000..3753009 --- /dev/null +++ b/sampleApp/src/main/res/anim/item_animation_from_right.xml @@ -0,0 +1,14 @@ + + + + + + + diff --git a/sampleApp/src/main/res/anim/item_animation_scale.xml b/sampleApp/src/main/res/anim/item_animation_scale.xml new file mode 100644 index 0000000..e638072 --- /dev/null +++ b/sampleApp/src/main/res/anim/item_animation_scale.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/sampleApp/src/main/res/anim/layout_animation_fall_down.xml b/sampleApp/src/main/res/anim/layout_animation_fall_down.xml new file mode 100644 index 0000000..6736b9f --- /dev/null +++ b/sampleApp/src/main/res/anim/layout_animation_fall_down.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sampleApp/src/main/res/anim/layout_animation_from_bottom.xml b/sampleApp/src/main/res/anim/layout_animation_from_bottom.xml new file mode 100644 index 0000000..f04b269 --- /dev/null +++ b/sampleApp/src/main/res/anim/layout_animation_from_bottom.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sampleApp/src/main/res/anim/layout_animation_from_right.xml b/sampleApp/src/main/res/anim/layout_animation_from_right.xml new file mode 100644 index 0000000..9ea2fa9 --- /dev/null +++ b/sampleApp/src/main/res/anim/layout_animation_from_right.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sampleApp/src/main/res/values/integers.xml b/sampleApp/src/main/res/values/integers.xml new file mode 100644 index 0000000..3616732 --- /dev/null +++ b/sampleApp/src/main/res/values/integers.xml @@ -0,0 +1,5 @@ + + + 300 + 400 + \ No newline at end of file diff --git a/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_detail/UserDetailVMTest.java b/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_detail/UserDetailVMTest.java index 111253d..041669b 100644 --- a/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_detail/UserDetailVMTest.java +++ b/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_detail/UserDetailVMTest.java @@ -1,9 +1,7 @@ package com.zeyad.usecases.app.screens.screens.user_detail; -import com.zeyad.rxredux.core.redux.SuccessStateAccumulator; import com.zeyad.usecases.api.IDataService; import com.zeyad.usecases.app.screens.user.detail.Repository; -import com.zeyad.usecases.app.screens.user.detail.UserDetailState; import com.zeyad.usecases.app.screens.user.detail.UserDetailVM; import com.zeyad.usecases.db.RealmQueryProvider; @@ -33,11 +31,10 @@ public class UserDetailVMTest { public void setUp() throws Exception { mockDataUseCase = mock(IDataService.class); userDetailVM = new UserDetailVM(); - userDetailVM.init( - mock(SuccessStateAccumulator.class), mock(UserDetailState.class), mockDataUseCase); + userDetailVM.init(mockDataUseCase); repository = new Repository(); - repository.setFullName("testUser"); + repository.setName("testUser"); repository.setId(1); } diff --git a/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_list/UserListVMTest.java b/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_list/UserListVMTest.java index e2a6d86..58833f9 100644 --- a/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_list/UserListVMTest.java +++ b/sampleApp/src/test/java/com/zeyad/usecases/app/screens/screens/user_list/UserListVMTest.java @@ -1,6 +1,5 @@ package com.zeyad.usecases.app.screens.screens.user_list; -import com.zeyad.rxredux.core.redux.SuccessStateAccumulator; import com.zeyad.usecases.api.IDataService; import com.zeyad.usecases.app.screens.user.list.User; import com.zeyad.usecases.app.screens.user.list.UserListVM; @@ -34,7 +33,7 @@ public class UserListVMTest { public void setUp() throws Exception { mockDataUseCase = mock(IDataService.class); userListVM = new UserListVM(); - userListVM.init(mock(SuccessStateAccumulator.class), null, mockDataUseCase); + userListVM.init(mockDataUseCase); } @Test @@ -64,10 +63,9 @@ public void deleteCollection() throws Exception { List ids = new ArrayList<>(); ids.add("1"); ids.add("2"); - Flowable> observableUserRealm = Flowable.just(ids); when(mockDataUseCase.deleteCollectionByIds(any(PostRequest.class))) - .thenReturn(Flowable.just(true)); + .thenReturn(Flowable.just(ids)); TestSubscriber> subscriber = new TestSubscriber<>(); userListVM.deleteCollection(ids).subscribe(subscriber); @@ -87,12 +85,10 @@ public void search() throws Exception { user.setId(1); userList = new ArrayList<>(); userList.add(user); - Flowable> listObservable = Flowable.just(userList); - Flowable userObservable = Flowable.just(user); - when(mockDataUseCase.getObject(any(GetRequest.class))).thenReturn(userObservable); + when(mockDataUseCase.getObject(any(GetRequest.class))).thenReturn(Flowable.just(user)); when(mockDataUseCase.queryDisk(any(RealmQueryProvider.class))) - .thenReturn(listObservable); + .thenReturn(Flowable.just(userList)); TestSubscriber> subscriber = new TestSubscriber<>(); userListVM.search("Zoz").subscribe(subscriber); @@ -102,6 +98,8 @@ public void search() throws Exception { subscriber.assertComplete(); subscriber.assertNoErrors(); - // subscriber.assertValue(userList); + List expectedResult = new ArrayList<>(); + expectedResult.add(user); + subscriber.assertValue(expectedResult); } } diff --git a/usecases/build.gradle b/usecases/build.gradle index 0e674ed..d447401 100644 --- a/usecases/build.gradle +++ b/usecases/build.gradle @@ -1,32 +1,6 @@ apply plugin: 'com.android.library' -//apply plugin: 'me.tatarka.retrolambda' -//apply plugin: 'android-apt' // remove -apply plugin: 'maven' apply plugin: 'realm-android' - -apply plugin: 'com.github.dcendents.android-maven' - -//apply plugin: "net.ltgt.errorprone" - -version = "1.0.1" -group = "com.github.zeyad-37" - -//buildscript { -// repositories { -// maven { -// url "https://plugins.gradle.org/m2/" -// } -// } -// -// dependencies { -//// classpath 'me.tatarka:gradle-retrolambda:3.7.0' -//// classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.0.13" -// } -//} -// -//repositories { -// mavenCentral() -//} +apply plugin: "net.ltgt.errorprone" android { compileSdkVersion 26 @@ -112,7 +86,7 @@ dependencies { transitive = true } compile 'com.rollbar:rollbar-android:0.2.1' - compile 'nl.littlerobots.rxlint:rxlint:1.6' + compile "nl.littlerobots.rxlint:rxlint:$rxlint" // Testing testCompile 'junit:junit:4.12' testCompile "com.android.support:support-annotations:$supportLibrary" diff --git a/usecases/src/test/java/com/zeyad/usecases/api/DataServiceTest.java b/usecases/src/test/java/com/zeyad/usecases/api/DataServiceTest.java index a688a4a..80ffd65 100644 --- a/usecases/src/test/java/com/zeyad/usecases/api/DataServiceTest.java +++ b/usecases/src/test/java/com/zeyad/usecases/api/DataServiceTest.java @@ -423,8 +423,8 @@ public void uploadFile() throws Exception { .cloud(Object.class) .dynamicUploadFile( anyString(), - (HashMap) anyMap(), - (HashMap) anyMap(), + (HashMap) anyMap(), + (HashMap) anyMap(), anyBoolean(), anyBoolean(), anyBoolean(), @@ -437,8 +437,8 @@ public void uploadFile() throws Exception { verify(dataStoreFactory.cloud(Object.class), times(1)) .dynamicUploadFile( anyString(), - (HashMap) anyMap(), - (HashMap) anyMap(), + (HashMap) anyMap(), + (HashMap) anyMap(), anyBoolean(), anyBoolean(), anyBoolean(), diff --git a/usecases/src/test/java/com/zeyad/usecases/services/jobs/FileIOTest.java b/usecases/src/test/java/com/zeyad/usecases/services/jobs/FileIOTest.java index 018bc9b..8451aed 100644 --- a/usecases/src/test/java/com/zeyad/usecases/services/jobs/FileIOTest.java +++ b/usecases/src/test/java/com/zeyad/usecases/services/jobs/FileIOTest.java @@ -74,8 +74,8 @@ public void testReQueue() throws JSONException { FileIORequest fileIOReq = mockFileIoReq(true, true, getValidFile()); fileIO = createFileIO(fileIOReq, true); Mockito.doNothing() - .when(utils) - .queueFileIOCore(any(), anyBoolean(), any(FileIORequest.class), anyInt()); + .when(utils) + .queueFileIOCore(any(), anyBoolean(), any(FileIORequest.class), anyInt()); fileIO.queueIOFile(); verify(utils, times(1)).queueFileIOCore(any(), anyBoolean(), any(FileIORequest.class), anyInt()); } @@ -110,21 +110,21 @@ private CloudStore createCloudDataStore() { final CloudStore cloudStore = mock(CloudStore.class); Mockito.when( cloudStore.dynamicDownloadFile( - Mockito.anyString(), - any(), - anyBoolean(), - anyBoolean(), - anyBoolean())) + Mockito.anyString(), + any(), + anyBoolean(), + anyBoolean(), + anyBoolean())) .thenReturn(Flowable.empty()); Mockito.when( cloudStore.dynamicUploadFile( - Mockito.anyString(), - (HashMap) anyMap(), - (HashMap) anyMap(), - anyBoolean(), - anyBoolean(), - anyBoolean(), - any())) + Mockito.anyString(), + (HashMap) anyMap(), + (HashMap) anyMap(), + anyBoolean(), + anyBoolean(), + anyBoolean(), + any())) .thenReturn(Flowable.empty()); return cloudStore; }