From 2fcb0ce871804f23a8885f1d5fa3a803ee1b94a0 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Tue, 22 Oct 2019 19:02:28 +0000 Subject: [PATCH] Feature/show dialog with mnemonic --- .../anytype/di/common/ComponentManager.kt | 7 ++ .../anytype/di/feature/KeychainDI.kt | 46 ++++++++++++ .../anytype/di/main/MainComponent.kt | 2 + .../agileburo/anytype/navigation/Navigator.kt | 4 ++ .../anytype/ui/base/NavigationFragment.kt | 1 + .../ui/profile/KeychainPhraseDialog.kt | 65 ++++++++++++++++- .../res/layout/dialog_keychain_phrase.xml | 2 +- app/src/main/res/navigation/graph.xml | 70 ++++++++++-------- build.gradle | 2 +- dependencies.gradle | 13 +++- gradle/wrapper/gradle-wrapper.properties | 4 +- presentation/build.gradle | 8 +-- .../presentation/ExampleInstrumentedTest.java | 25 ------- .../keychain/KeychainPhraseViewModel.kt | 38 ++++++++++ .../presentation/navigation/AppNavigation.kt | 1 + presentation/src/test/java/MockDataFactory.kt | 62 ++++++++++++++++ .../anytype/presentation/ExampleUnitTest.java | 17 ----- .../keychain/KeychainPhraseViewModelTest.kt | 71 +++++++++++++++++++ .../org.mockito.plugins.MockMaker | 1 + 19 files changed, 352 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/com/agileburo/anytype/di/feature/KeychainDI.kt delete mode 100644 presentation/src/androidTest/java/com/agileburo/anytype/presentation/ExampleInstrumentedTest.java create mode 100644 presentation/src/main/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModel.kt create mode 100644 presentation/src/test/java/MockDataFactory.kt delete mode 100644 presentation/src/test/java/com/agileburo/anytype/presentation/ExampleUnitTest.java create mode 100644 presentation/src/test/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModelTest.kt create mode 100644 presentation/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt index 89dea11bf2..258ed0498f 100644 --- a/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/agileburo/anytype/di/common/ComponentManager.kt @@ -64,6 +64,13 @@ class ComponentManager(private val main: MainComponent) { .build() } + val keychainPhraseComponent = Component { + main + .keychainPhraseComponentBuilder() + .keychainPhraseModule(KeychainPhraseModule()) + .build() + } + class Component(private val builder: () -> T) { private var instance: T? = null diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/KeychainDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/KeychainDI.kt new file mode 100644 index 0000000000..d92bef5b25 --- /dev/null +++ b/app/src/main/java/com/agileburo/anytype/di/feature/KeychainDI.kt @@ -0,0 +1,46 @@ +package com.agileburo.anytype.di.feature + +import com.agileburo.anytype.core_utils.di.scope.PerScreen +import com.agileburo.anytype.domain.auth.interactor.GetMnemonic +import com.agileburo.anytype.domain.auth.repo.AuthRepository +import com.agileburo.anytype.presentation.keychain.KeychainPhraseViewModelFactory +import com.agileburo.anytype.ui.profile.KeychainPhraseDialog +import dagger.Module +import dagger.Provides +import dagger.Subcomponent + + +@Subcomponent( + modules = [KeychainPhraseModule::class] +) +@PerScreen +interface KeychainPhraseSubComponent { + + @Subcomponent.Builder + interface Builder { + fun keychainPhraseModule(module: KeychainPhraseModule): Builder + fun build(): KeychainPhraseSubComponent + } + + fun inject(fragment: KeychainPhraseDialog) +} + +@Module +class KeychainPhraseModule { + + @Provides + @PerScreen + fun provideKeychainPhraseViewModelFactory( + getMnemonic: GetMnemonic + ) = KeychainPhraseViewModelFactory( + getMnemonic = getMnemonic + ) + + @Provides + @PerScreen + fun provideGetMnemonicUseCase( + repository: AuthRepository + ): GetMnemonic = GetMnemonic( + repository = repository + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt index c6ed8aff03..3b84f73775 100644 --- a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt @@ -1,6 +1,7 @@ package com.agileburo.anytype.di.main import com.agileburo.anytype.di.feature.AuthSubComponent +import com.agileburo.anytype.di.feature.KeychainPhraseSubComponent import com.agileburo.anytype.di.feature.ProfileSubComponent import dagger.Component import javax.inject.Singleton @@ -15,4 +16,5 @@ import javax.inject.Singleton interface MainComponent { fun authComponentBuilder(): AuthSubComponent.Builder fun profileComponentBuilder(): ProfileSubComponent.Builder + fun keychainPhraseComponentBuilder(): KeychainPhraseSubComponent.Builder } \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt b/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt index b339b3971d..4f6a24e583 100644 --- a/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/agileburo/anytype/navigation/Navigator.kt @@ -46,6 +46,10 @@ class Navigator : AppNavigation { // TODO } + override fun openKeychainScreen() { + navController?.navigate(R.id.action_open_keychain) + } + override fun setupSelectedAccount(id: String) { navController?.navigate( R.id.action_setup_selected_account, diff --git a/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt index 3f0dae570d..c75f03fe73 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/base/NavigationFragment.kt @@ -32,6 +32,7 @@ abstract class NavigationFragment( is Command.ConfirmPinCodeScreen -> navigation.confirmPinCode(command.code) is Command.OpenProfile -> navigation.openProfile() is Command.OpenDocument -> navigation.openDocument(command.id) + is Command.OpenKeychainScreen -> navigation.openKeychainScreen() } } diff --git a/app/src/main/java/com/agileburo/anytype/ui/profile/KeychainPhraseDialog.kt b/app/src/main/java/com/agileburo/anytype/ui/profile/KeychainPhraseDialog.kt index e85cd8f8b7..b4618a60ef 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/profile/KeychainPhraseDialog.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/profile/KeychainPhraseDialog.kt @@ -4,13 +4,36 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import com.agileburo.anytype.R +import com.agileburo.anytype.core_utils.ui.ViewState +import com.agileburo.anytype.di.common.componentManager +import com.agileburo.anytype.presentation.keychain.KeychainPhraseViewModel +import com.agileburo.anytype.presentation.keychain.KeychainPhraseViewModelFactory import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.android.synthetic.main.dialog_keychain_phrase.* +import javax.inject.Inject -class KeychainPhraseDialog : BottomSheetDialogFragment() { +class KeychainPhraseDialog : BottomSheetDialogFragment(), Observer> { - companion object { - fun newInstance(): KeychainPhraseDialog = KeychainPhraseDialog() + private val vm by lazy { + ViewModelProviders + .of(this, factory) + .get(KeychainPhraseViewModel::class.java) + } + + @Inject + lateinit var factory: KeychainPhraseViewModelFactory + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injectDependencies() + } + + override fun onDestroy() { + super.onDestroy() + releaseDependencies() } override fun onCreateView( @@ -18,4 +41,40 @@ class KeychainPhraseDialog : BottomSheetDialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? = inflater.inflate(R.layout.dialog_keychain_phrase, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + init() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + vm.state.observe(this, this) + } + + override fun onChanged(state: ViewState) { + when (state) { + is ViewState.Success -> { + keychain.text = state.data + } + is ViewState.Error -> { + // TODO + } + is ViewState.Loading -> { + // TODO + } + } + } + + private fun init() { + doneButton.setOnClickListener { dismiss() } + } + + private fun injectDependencies() { + componentManager().keychainPhraseComponent.get().inject(this) + } + + private fun releaseDependencies() { + componentManager().keychainPhraseComponent.release() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_keychain_phrase.xml b/app/src/main/res/layout/dialog_keychain_phrase.xml index d403c13a45..5820b9c6c9 100644 --- a/app/src/main/res/layout/dialog_keychain_phrase.xml +++ b/app/src/main/res/layout/dialog_keychain_phrase.xml @@ -45,6 +45,7 @@ app:layout_constraintTop_toBottomOf="@+id/title" /> - + + app:destination="@id/createAccountScreen" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + app:popExitAnim="@anim/slide_out_right" /> + app:popExitAnim="@anim/slide_out_right" /> + app:popExitAnim="@anim/slide_out_right" /> + app:popExitAnim="@anim/slide_out_right" /> - + + app:argType="integer" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> + app:popExitAnim="@anim/slide_out_right" /> + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 258f514c65..8104a1a7b1 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:3.5.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.2' classpath 'io.fabric.tools:gradle:1.31.1' diff --git a/dependencies.gradle b/dependencies.gradle index 2d232da3fb..3ed0c8b564 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -19,7 +19,7 @@ ext { // Architecture Components lifecycle_version = '2.1.0' - navigation_version = '1.0.0-beta02' + navigation_version = '2.1.0' // Third party libraries glide_version = '4.9.0' @@ -39,6 +39,9 @@ ext { junit_version = '4.12' mockito_version = '1.4.0' kluent_version = '1.14' + coroutine_testing_version = '1.3.2' + live_data_testing_version = '1.1.0' + mockito_kotlin_version = '2.2.0' // Acceptance Testing runner_version = '1.1.0' @@ -68,8 +71,8 @@ ext { kotlin: "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version", coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version", androidxCore: "androidx.core:core-ktx:$androidx_core_version", - navigation: "android.arch.navigation:navigation-fragment-ktx:$navigation_version", - navigationUi: "android.arch.navigation:navigation-ui-ktx:$navigation_version", + navigation: "androidx.navigation:navigation-fragment-ktx:$navigation_version", + navigationUi: "androidx.navigation:navigation-ui-ktx:$navigation_version", viewModel: "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version", viewModelExtensions: "androidx.lifecycle:lifecycle-extensions:$lifecycle_version", lifecycleCompiler: "androidx.lifecycle:lifecycle-compiler:$lifecycle_version", @@ -106,7 +109,11 @@ ext { robolectric: "org.robolectric:robolectric:$robolectric_version", junit: "junit:junit:$junit_version", mockito: "com.nhaarman:mockito-kotlin:$mockito_version", + mockitoKotlin: "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin_version", kluent: "org.amshove.kluent:kluent:$kluent_version", + archCoreTesting: "androidx.arch.core:core-testing:$lifecycle_version", + liveDataTesting: "com.jraska.livedata:testing-ktx:$live_data_testing_version", + coroutineTesting: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version" ] acceptanceTesting = [ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 479aa8f795..e0fa411889 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Apr 19 19:39:37 MSK 2019 +#Tue Oct 22 17:14:40 MSK 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/presentation/build.gradle b/presentation/build.gradle index 11bb89df22..6d0bd28e6d 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -34,8 +34,8 @@ dependencies { testImplementation unitTestDependencies.junit testImplementation unitTestDependencies.kotlinTest - //testImplementation unitTestDependencies.mockito - - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" - testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2' + testImplementation unitTestDependencies.mockitoKotlin + testImplementation unitTestDependencies.coroutineTesting + testImplementation unitTestDependencies.liveDataTesting + testImplementation unitTestDependencies.archCoreTesting } diff --git a/presentation/src/androidTest/java/com/agileburo/anytype/presentation/ExampleInstrumentedTest.java b/presentation/src/androidTest/java/com/agileburo/anytype/presentation/ExampleInstrumentedTest.java deleted file mode 100644 index b7156e90eb..0000000000 --- a/presentation/src/androidTest/java/com/agileburo/anytype/presentation/ExampleInstrumentedTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.agileburo.anytype.presentation; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.agileburo.anytype.presentation.test", appContext.getPackageName()); - } -} diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModel.kt new file mode 100644 index 0000000000..073541b8b6 --- /dev/null +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModel.kt @@ -0,0 +1,38 @@ +package com.agileburo.anytype.presentation.keychain + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.agileburo.anytype.core_utils.ui.ViewState +import com.agileburo.anytype.core_utils.ui.ViewStateViewModel +import com.agileburo.anytype.domain.auth.interactor.GetMnemonic +import timber.log.Timber + +class KeychainPhraseViewModel( + private val getMnemonic: GetMnemonic +) : ViewStateViewModel>() { + + init { + proceedWithGettingMnemonic() + } + + private fun proceedWithGettingMnemonic() { + getMnemonic.invoke(viewModelScope, Unit) { result -> + result.either( + fnL = { e -> Timber.e(e, "Error while getting mnemonic") }, + fnR = { stateData.postValue(ViewState.Success(it)) } + ) + } + } +} + +class KeychainPhraseViewModelFactory( + private val getMnemonic: GetMnemonic +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return KeychainPhraseViewModel( + getMnemonic = getMnemonic + ) as T + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt index aca2ebb8d3..266de6f4ea 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/navigation/AppNavigation.kt @@ -13,6 +13,7 @@ interface AppNavigation { fun workspace() fun openProfile() fun openDocument(id: String) + fun openKeychainScreen() sealed class Command { object OpenStartLoginScreen : Command() diff --git a/presentation/src/test/java/MockDataFactory.kt b/presentation/src/test/java/MockDataFactory.kt new file mode 100644 index 0000000000..b4b004ee9c --- /dev/null +++ b/presentation/src/test/java/MockDataFactory.kt @@ -0,0 +1,62 @@ +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object MockDataFactory { + + fun randomUuid(): String { + return UUID.randomUUID().toString() + } + + fun randomString(): String { + return randomUuid() + } + + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomInt(max: Int): Int { + return ThreadLocalRandom.current().nextInt(0, max) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomFloat(): Float { + return randomInt().toFloat() + } + + fun randomDouble(): Double { + return randomInt().toDouble() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + + fun makeIntList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomInt()) + } + return items + } + + fun makeStringList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + + fun makeDoubleList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomDouble()) + } + return items + } +} \ No newline at end of file diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/ExampleUnitTest.java b/presentation/src/test/java/com/agileburo/anytype/presentation/ExampleUnitTest.java deleted file mode 100644 index 14c3a0a880..0000000000 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.agileburo.anytype.presentation; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModelTest.kt new file mode 100644 index 0000000000..d82aca5783 --- /dev/null +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/keychain/KeychainPhraseViewModelTest.kt @@ -0,0 +1,71 @@ +package com.agileburo.anytype.presentation.keychain + +import MockDataFactory +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.agileburo.anytype.core_utils.ui.ViewState +import com.agileburo.anytype.domain.auth.interactor.GetMnemonic +import com.agileburo.anytype.domain.base.Either +import com.jraska.livedata.test +import com.nhaarman.mockitokotlin2.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + + +class KeychainPhraseViewModelTest { + + @get:Rule + val rule = InstantTaskExecutorRule() + + @Mock + lateinit var getMnemonic: GetMnemonic + + lateinit var vm: KeychainPhraseViewModel + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun `should proceed with getting mnemonic when vm is created`() { + vm = buildViewModel() + verify(getMnemonic, times(1)).invoke(any(), any(), any()) + } + + @Test + fun `should emit mnemonic when it is received`() { + + val mnemonic = MockDataFactory.randomString() + + getMnemonic.stub { + on { invoke(any(), any(), any()) } doAnswer { answer -> + answer.getArgument<(Either) -> Unit>(2)(Either.Right(mnemonic)) + } + } + + vm = buildViewModel() + + vm.state.test().assertValue(ViewState.Success(mnemonic)) + } + + @Test + fun `should emit nothing when error occurs`() { + + val exception = Exception() + + getMnemonic.stub { + on { invoke(any(), any(), any()) } doAnswer { answer -> + answer.getArgument<(Either) -> Unit>(2)(Either.Left(exception)) + } + } + + vm = buildViewModel() + + vm.state.test().assertNoValue() + } + + private fun buildViewModel() = KeychainPhraseViewModel(getMnemonic = getMnemonic) +} \ No newline at end of file diff --git a/presentation/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/presentation/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000000..ca6ee9cea8 --- /dev/null +++ b/presentation/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file