1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Merge branch 'feature/show-dialog-with-mnemonic' into 'develop'

Feature/show dialog with mnemonic

See merge request ra3orbladez/anytype.io.mobile!86
This commit is contained in:
Evgenii Kozlov 2019-10-22 19:02:29 +00:00
commit 8aade7b250
19 changed files with 352 additions and 87 deletions

View file

@ -64,6 +64,13 @@ class ComponentManager(private val main: MainComponent) {
.build()
}
val keychainPhraseComponent = Component {
main
.keychainPhraseComponentBuilder()
.keychainPhraseModule(KeychainPhraseModule())
.build()
}
class Component<T>(private val builder: () -> T) {
private var instance: T? = null

View file

@ -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
)
}

View file

@ -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
}

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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<ViewState<String>> {
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<String>) {
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()
}
}

View file

@ -45,6 +45,7 @@
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView
android:textIsSelectable="true"
android:id="@+id/keychain"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -57,7 +58,6 @@
android:paddingTop="12dp"
android:paddingEnd="20dp"
android:paddingBottom="12dp"
android:text="@string/keychain_mock"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -11,19 +11,19 @@
android:label="StartLoginFragment"
tools:layout="@layout/fragment_start_login">
<action
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_open_sign_in"
app:destination="@id/keychainLoginScreen" />
<action
app:destination="@id/keychainLoginScreen"
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" />
<action
android:id="@+id/action_open_sign_up"
app:destination="@id/createAccountScreen" />
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" />
</fragment>
<fragment
@ -32,12 +32,12 @@
android:label="StartLoginFragment"
tools:layout="@layout/fragment_create_account">
<action
android:id="@+id/action_setup_new_account"
app:destination="@id/setupNewAccountScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_setup_new_account"
app:destination="@id/setupNewAccountScreen" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
@ -46,12 +46,12 @@
android:label="SetupAccount"
tools:layout="@layout/fragment_setup_new_account">
<action
android:id="@+id/action_open_congratulation_screen"
app:destination="@id/congratulationScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_open_congratulation_screen"
app:destination="@id/congratulationScreen" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
@ -60,12 +60,12 @@
android:label="KeychainLogin"
tools:layout="@layout/fragment_keychain_login">
<action
android:id="@+id/action_select_account"
app:destination="@id/selectAccountScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_select_account"
app:destination="@id/selectAccountScreen" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
@ -74,12 +74,12 @@
android:label="ChooseProfileScreen"
tools:layout="@layout/fragment_select_account">
<action
android:id="@+id/action_setup_selected_account"
app:destination="@id/setupSelectedAccountScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_setup_selected_account"
app:destination="@id/setupSelectedAccountScreen" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
@ -88,20 +88,20 @@
android:label="SetupAccount"
tools:layout="@layout/fragment_setup_selected_account">
<action
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_open_congratulation_screen"
app:destination="@id/congratulationScreen" />
<argument
app:destination="@id/congratulationScreen"
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" />
<argument
android:name="selected_account_id"
android:defaultValue="0"
app:argType="integer" />
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" />
</fragment>
<fragment
@ -132,12 +132,12 @@
android:label="DesktopFragment"
tools:layout="@layout/fragment_desktop">
<action
android:id="@+id/action_open_profile"
app:destination="@id/profileScreen"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
android:id="@+id/action_open_profile"
app:destination="@id/profileScreen" />
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
@ -150,6 +150,14 @@
app:destination="@id/startLoginScreen"
app:enterAnim="@anim/fade_in"
app:exitAnim="@anim/fade_out" />
<action
android:id="@+id/action_open_keychain"
app:destination="@id/keychainDialog" />
</fragment>
<dialog
android:id="@+id/keychainDialog"
android:name="com.agileburo.anytype.ui.profile.KeychainPhraseDialog"
tools:layout="@layout/dialog_keychain_phrase" />
</navigation>

View file

@ -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'

View file

@ -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 = [

View file

@ -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

View file

@ -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
}

View file

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View file

@ -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<ViewState<String>>() {
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 <T : ViewModel?> create(modelClass: Class<T>): T {
return KeychainPhraseViewModel(
getMnemonic = getMnemonic
) as T
}
}

View file

@ -13,6 +13,7 @@ interface AppNavigation {
fun workspace()
fun openProfile()
fun openDocument(id: String)
fun openKeychainScreen()
sealed class Command {
object OpenStartLoginScreen : Command()

View file

@ -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<Int> {
val items = mutableListOf<Int>()
repeat(count) {
items.add(randomInt())
}
return items
}
fun makeStringList(count: Int): List<String> {
val items = mutableListOf<String>()
repeat(count) {
items.add(randomUuid())
}
return items
}
fun makeDoubleList(count: Int): List<Double> {
val items = mutableListOf<Double>()
repeat(count) {
items.add(randomDouble())
}
return items
}
}

View file

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View file

@ -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<Throwable, String>) -> 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<Throwable, String>) -> Unit>(2)(Either.Left(exception))
}
}
vm = buildViewModel()
vm.state.test().assertNoValue()
}
private fun buildViewModel() = KeychainPhraseViewModel(getMnemonic = getMnemonic)
}

View file

@ -0,0 +1 @@
mock-maker-inline