From 60f969d023540015ac61a6370ad1a92395bfad72 Mon Sep 17 00:00:00 2001 From: Zeyad-37 Date: Tue, 16 Jan 2018 00:28:44 +0100 Subject: [PATCH] Started Espresso Tests, fixed some unit tests, fix gradle wrapper and build files, added animation files, added cipher util. Updated rxredux and gadapter --- build.gradle | 7 +- gradle/wrapper/gradle-wrapper.jar | Bin 54208 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 6 +- sampleApp/build.gradle | 67 ++--- .../RecyclerViewItemCountAssertion.java | 40 +++ .../screens/splash/UserListActivityTest.java | 19 ++ .../user/detail/UserDetailFragmentTest.java | 47 ++++ .../user/detail/UserDetailFragmentTest2.java | 37 +++ .../user/list/UserListActivityTest.java | 43 +++ .../usecases/app/GenericApplication.java | 37 ++- .../app/components/ScrollEventCalculator.java | 26 ++ .../usecases/app/screens/BaseActivity.java | 14 +- .../usecases/app/screens/BaseFragment.java | 11 +- .../app/screens/user/ViewModelFactory.java | 25 ++ .../screens/user/detail/GetReposEvent.java | 5 +- .../user/detail/UserDetailFragment.java | 64 ++--- .../screens/user/detail/UserDetailState.java | 40 ++- .../app/screens/user/detail/UserDetailVM.java | 20 +- .../screens/user/list/UserDiffCallBack.java | 51 ++++ .../screens/user/list/UserListActivity.java | 252 ++++++------------ .../app/screens/user/list/UserListState.java | 63 ++++- .../app/screens/user/list/UserListVM.java | 56 +++- .../user/list/events/DeleteUsersEvent.java | 9 +- .../list/events/GetPaginatedUsersEvent.java | 9 +- .../user/list/events/SearchUsersEvent.java | 9 +- .../zeyad/usecases/app/utils/CipherUtil.java | 149 +++++++++++ .../grid_layout_animation_from_bottom.xml | 8 + .../res/anim/grid_layout_animation_scale.xml | 7 + .../grid_layout_animation_scale_random.xml | 6 + .../res/anim/item_animation_fall_down.xml | 22 ++ .../res/anim/item_animation_from_bottom.xml | 14 + .../res/anim/item_animation_from_right.xml | 14 + .../main/res/anim/item_animation_scale.xml | 17 ++ .../res/anim/layout_animation_fall_down.xml | 5 + .../res/anim/layout_animation_from_bottom.xml | 5 + .../res/anim/layout_animation_from_right.xml | 5 + sampleApp/src/main/res/values/integers.xml | 5 + .../screens/user_detail/UserDetailVMTest.java | 7 +- .../screens/user_list/UserListVMTest.java | 16 +- usecases/build.gradle | 30 +-- .../zeyad/usecases/api/DataServiceTest.java | 8 +- .../usecases/services/jobs/FileIOTest.java | 28 +- 43 files changed, 933 insertions(+), 373 deletions(-) create mode 100644 sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/RecyclerViewItemCountAssertion.java create mode 100644 sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/splash/UserListActivityTest.java create mode 100644 sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest.java create mode 100644 sampleApp/src/androidTest/java/com/zeyad/usecases/app/screens/user/detail/UserDetailFragmentTest2.java create mode 100644 sampleApp/src/main/java/com/zeyad/usecases/app/components/ScrollEventCalculator.java create mode 100644 sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/ViewModelFactory.java create mode 100644 sampleApp/src/main/java/com/zeyad/usecases/app/screens/user/list/UserDiffCallBack.java create mode 100644 sampleApp/src/main/java/com/zeyad/usecases/app/utils/CipherUtil.java create mode 100644 sampleApp/src/main/res/anim/grid_layout_animation_from_bottom.xml create mode 100644 sampleApp/src/main/res/anim/grid_layout_animation_scale.xml create mode 100644 sampleApp/src/main/res/anim/grid_layout_animation_scale_random.xml create mode 100644 sampleApp/src/main/res/anim/item_animation_fall_down.xml create mode 100644 sampleApp/src/main/res/anim/item_animation_from_bottom.xml create mode 100644 sampleApp/src/main/res/anim/item_animation_from_right.xml create mode 100644 sampleApp/src/main/res/anim/item_animation_scale.xml create mode 100644 sampleApp/src/main/res/anim/layout_animation_fall_down.xml create mode 100644 sampleApp/src/main/res/anim/layout_animation_from_bottom.xml create mode 100644 sampleApp/src/main/res/anim/layout_animation_from_right.xml create mode 100644 sampleApp/src/main/res/values/integers.xml 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 5c27ee3f1477ce0f65315b6616605e0090c6a69f..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 100644 GIT binary patch delta 19176 zcmZ6yV{m3&w>2Ew?6_mwwr$(Cadpzk6(=3rwylnB+qRv&_j&H~)mP8?v1`rRHP^0M zRb!8-vHq5TH&%cnDanFEV1R(Yz<_{&2!q5Sk^f@|gK)(i$;9z$V5gtp+_~JkfP(xV zx6+>w|8bw7{C9n3{Lk9pK+OU9A1k4hVdpL(FvcSWu!X=@u3r5y z{&N>8=QoGFZncMeBZ+ejqAmiv=%4C)ob7te?`iRLT;nJRl6bo!fyRx8{r=D9z5UL4ODV`5f(yi zVZh^C*&pM4cV;SH4@jv65Rk`0EoM7JD!n6C7nwL2gybnT{qpPx;woxaqpvCQVns19 zyj75-x(>Mr4Lq2fY=ERe7^lzkr0QZ+cLlpQAE}LFT-d%x-hdBzvPhu8x?=fd%rDsD z*=7P2i=Xtv|Q=c`3^TFdX&KOq&?uixhEl zg8vOx(gsDXO~u<93tikV3LpC;;@dM7Ax*Hn_}5|^I%{EQI4H92)>UrBAhGD&Mr8cJ z0ywbB&p#+XiKv+-q=6Vn$r|>Znw}jK2SaabUtKQajD|Xn{bY--Xa$EV7Y@6iiQ#7*7;x)!8K}A8I^6 z@*xY4w>JpSvkIN8d#{iAVp{c3Kt6FiW(+m8ugSTs#EjIG-mW=$B8+Ltq!Uw*sJII0 zmD4G$Qq}+sQZFdF!YoeV1M0s~^XC?I3Jw_w1cU+~1cVqQL3sc(fe#M`z+h@;?CP4O z4R3_8?9Q*awuuuhL8Kv7Yjk7s7wZid?>9Yy1R5AbJc@_!j^nf4`b}QYdhUKJpi##jqon!q z$p6a!x!ZTT>$>~#xaaj0Fb%ds&G18Nr#;ZPQ>}l?((GP)wUaJG^Wco}U;fEJykI9bat@r9jdf}`4>IX1z{WcxAy>O!om434h)%QXG-V?c`Y` z*Ei2iFVLM1J4CnY1gtJcIR^@c39N?mH`6LFX`|J8w@$f-)nn+w2HHe-Kwbn0>Y|vZ>u_Ni33K3Fim)xjX9YAXMA6gbj(q+#rR%-O}J> zd)S#tB!k23phjCShrHeLKvjCHJP6oP?*57aU`(duvQfxu!IhEz!Qw-E{Wd{0`Pv8U zvA}c~ITmri1o?>whlrcvClut_n?B{GZj7LD5sODtV}TIBe)}a)6x@u{iLJhP zRZ@Co<|;FQ80EwheWBBs>U4!PtxbBJ*IK@KL3@LmmfhG}ntMtS>SL8mzRQ|DsVu*v z++usudHaOfA>AeG^gwQ?VO%+P!eEmo{6Wkh>hEM#n?L^gH6an+#C5t`2Qs;8^O|x4 z8f+OHb7N+rlJttT;OW70wc(U(D6(F9{qZ458Xi1=1)5;{37=NnoW-ErZkf1#(Q>YN znb-8#SXxriqPT=yqJ)S7~^ zqGthEB{*E#zfDNDDk9TqotB|28S82)$amE!r`t4>!yI<2W!ueVy=38*)3Nq_t_HP~ z*?nJHf@>|qsETH7&N00M+5?t~4mlEMyr0}6=Gt9t3pwq@^>P
    ;{uxCFdo17>QTVNzIrR=a?@u;vR$fngPL2ck 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 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; }