From 2061029d36ccae11a6f1fa7f26734be6f0e8e771 Mon Sep 17 00:00:00 2001 From: Elise Richards Date: Wed, 3 Apr 2019 15:38:46 -0700 Subject: [PATCH] 508: Merge route presenters (#539) * Creating separate parent class for route presenters * Adding super route presenter to app route presenter and autofill route presenter. Separating generic methods into parent. * Moving more generic functions into the parent class * Merging graph_main and graph_autofill. Setting up for separate route presenter test * Testing route presenter methods * Dialog fragment tests * Rebasing with #502 * Making routepresenter abstract, moving some funs around per review suggestions. (WIP) * Re-separating the nav graphs * Fixing tests and lint errors * Remove commented code * Upgrade dependencies --- app/build.gradle | 6 +- ...senterTest.kt => AppRoutePresenterTest.kt} | 2 +- .../lockbox/presenter/AppRoutePresenter.kt | 171 ++++++++++++++ .../presenter/AutofillRoutePresenter.kt | 96 ++------ .../lockbox/presenter/RoutePresenter.kt | 188 +++------------- .../java/mozilla/lockbox/view/RootActivity.kt | 4 +- .../presenter/AppRoutePresenterTest.kt | 105 +++++++++ .../presenter/AutofillRoutePresenterTest.kt | 21 +- .../lockbox/presenter/RoutePresenterTest.kt | 209 ++++++++++++++++++ build.gradle | 2 +- 10 files changed, 550 insertions(+), 254 deletions(-) rename app/src/androidTest/java/mozilla/lockbox/{RoutePresenterTest.kt => AppRoutePresenterTest.kt} (98%) create mode 100644 app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt create mode 100644 app/src/test/java/mozilla/lockbox/presenter/AppRoutePresenterTest.kt create mode 100644 app/src/test/java/mozilla/lockbox/presenter/RoutePresenterTest.kt diff --git a/app/build.gradle b/app/build.gradle index 7c5baddd1..c5ba1e046 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,7 +82,7 @@ configurations.all { config -> dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0-alpha03' + implementation 'androidx.appcompat:appcompat:1.1.0-alpha04' implementation 'androidx.cardview:cardview:1.0.0' implementation 'com.google.android.material:material:1.1.0-alpha05' implementation "androidx.recyclerview:recyclerview:$recyclerview_version" @@ -113,8 +113,8 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2' releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2' testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2' - implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha03' - implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0-alpha03' + implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha04' + implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0-alpha04' implementation "android.arch.navigation:navigation-fragment:$navigation_version" implementation "android.arch.navigation:navigation-ui-ktx:$navigation_version" implementation 'com.adjust.sdk:adjust-android:4.17.0' diff --git a/app/src/androidTest/java/mozilla/lockbox/RoutePresenterTest.kt b/app/src/androidTest/java/mozilla/lockbox/AppRoutePresenterTest.kt similarity index 98% rename from app/src/androidTest/java/mozilla/lockbox/RoutePresenterTest.kt rename to app/src/androidTest/java/mozilla/lockbox/AppRoutePresenterTest.kt index 6a953801f..0f9162444 100644 --- a/app/src/androidTest/java/mozilla/lockbox/RoutePresenterTest.kt +++ b/app/src/androidTest/java/mozilla/lockbox/AppRoutePresenterTest.kt @@ -24,7 +24,7 @@ import org.junit.runner.RunWith */ @ExperimentalCoroutinesApi @RunWith(AndroidJUnit4::class) -open class RoutePresenterTest { +open class AppRoutePresenterTest { private val navigator = Navigator() @Rule @JvmField diff --git a/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt b/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt new file mode 100644 index 000000000..43c241380 --- /dev/null +++ b/app/src/main/java/mozilla/lockbox/presenter/AppRoutePresenter.kt @@ -0,0 +1,171 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +@file:Suppress("DEPRECATION") + +package mozilla.lockbox.presenter + +import android.os.Bundle +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.Navigation +import io.reactivex.android.schedulers.AndroidSchedulers.mainThread +import io.reactivex.rxkotlin.addTo +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.lockbox.R +import mozilla.lockbox.action.AppWebPageAction +import mozilla.lockbox.action.DialogAction +import mozilla.lockbox.action.RouteAction +import mozilla.lockbox.action.Setting +import mozilla.lockbox.action.SettingAction +import mozilla.lockbox.extensions.view.AlertDialogHelper +import mozilla.lockbox.flux.Dispatcher +import mozilla.lockbox.store.RouteStore +import mozilla.lockbox.store.SettingStore +import mozilla.lockbox.view.AppWebPageFragmentArgs +import mozilla.lockbox.view.FingerprintAuthDialogFragment +import mozilla.lockbox.view.ItemDetailFragmentArgs + +@ExperimentalCoroutinesApi +class AppRoutePresenter( + private val activity: AppCompatActivity, + private val dispatcher: Dispatcher = Dispatcher.shared, + private val routeStore: RouteStore = RouteStore.shared, + private val settingStore: SettingStore = SettingStore.shared +) : RoutePresenter(activity, dispatcher, routeStore) { + + override fun onViewReady() { + navController = Navigation.findNavController(activity, R.id.fragment_nav_host) + } + + override fun onPause() { + super.onPause() + activity.removeOnBackPressedCallback(backListener) + compositeDisposable.clear() + } + + override fun onResume() { + super.onResume() + activity.addOnBackPressedCallback(backListener) + routeStore.routes + .observeOn(mainThread()) + .subscribe(this::route) + .addTo(compositeDisposable) + } + + fun bundle(action: AppWebPageAction): Bundle { + return AppWebPageFragmentArgs.Builder() + .setUrl(action.url!!) + .setTitle(action.title!!) + .build() + .toBundle() + } + + fun bundle(action: RouteAction.ItemDetail): Bundle { + return ItemDetailFragmentArgs.Builder() + .setItemId(action.id) + .build() + .toBundle() + } + + override fun route(action: RouteAction) { + activity.setTheme(R.style.AppTheme) + when (action) { + is RouteAction.Welcome -> navigateToFragment(R.id.fragment_welcome) + is RouteAction.Login -> navigateToFragment(R.id.fragment_fxa_login) + is RouteAction.Onboarding.FingerprintAuth -> + navigateToFragment(R.id.fragment_fingerprint_onboarding) + is RouteAction.Onboarding.Autofill -> navigateToFragment(R.id.fragment_autofill_onboarding) + is RouteAction.Onboarding.Confirmation -> navigateToFragment(R.id.fragment_onboarding_confirmation) + is RouteAction.ItemList -> navigateToFragment(R.id.fragment_item_list) + is RouteAction.SettingList -> navigateToFragment(R.id.fragment_setting) + is RouteAction.AccountSetting -> navigateToFragment(R.id.fragment_account_setting) + is RouteAction.LockScreen -> navigateToFragment(R.id.fragment_locked) + is RouteAction.Filter -> navigateToFragment(R.id.fragment_filter_backdrop) + is RouteAction.ItemDetail -> navigateToFragment(R.id.fragment_item_detail, bundle(action)) + is RouteAction.OpenWebsite -> openWebsite(action.url) + is RouteAction.SystemSetting -> openSetting(action) + is RouteAction.UnlockFallbackDialog -> showUnlockFallback(action) + is RouteAction.AutoLockSetting -> showAutoLockSelections() + is RouteAction.DialogFragment.FingerprintDialog -> + showDialogFragment(FingerprintAuthDialogFragment(), action) + is DialogAction -> showDialog(action) + is AppWebPageAction -> navigateToFragment(R.id.fragment_webview, bundle(action)) + } + } + + override fun findTransitionId(@IdRes src: Int, @IdRes dest: Int): Int? { + // This maps two nodes in the graph_main.xml to the edge between them. + // If a RouteAction is called from a place the graph doesn't know about then + // the app will log.error. + return when (src to dest) { + R.id.fragment_null to R.id.fragment_item_list -> R.id.action_init_to_unlocked + R.id.fragment_null to R.id.fragment_locked -> R.id.action_init_to_locked + R.id.fragment_null to R.id.fragment_welcome -> R.id.action_init_to_unprepared + + R.id.fragment_welcome to R.id.fragment_fxa_login -> R.id.action_welcome_to_fxaLogin + + R.id.fragment_fxa_login to R.id.fragment_item_list -> R.id.action_fxaLogin_to_itemList + R.id.fragment_fxa_login to R.id.fragment_fingerprint_onboarding -> + R.id.action_fxaLogin_to_fingerprint_onboarding + R.id.fragment_fxa_login to R.id.fragment_onboarding_confirmation -> + R.id.action_fxaLogin_to_onboarding_confirmation + + R.id.fragment_fingerprint_onboarding to R.id.fragment_onboarding_confirmation -> + R.id.action_fingerprint_onboarding_to_confirmation + R.id.fragment_fingerprint_onboarding to R.id.fragment_autofill_onboarding -> + R.id.action_onboarding_fingerprint_to_autofill + + R.id.fragment_autofill_onboarding to R.id.fragment_item_list -> R.id.action_to_itemList + R.id.fragment_autofill_onboarding to R.id.fragment_onboarding_confirmation -> R.id.action_autofill_onboarding_to_confirmation + + R.id.fragment_onboarding_confirmation to R.id.fragment_item_list -> R.id.action_to_itemList + R.id.fragment_onboarding_confirmation to R.id.fragment_webview -> R.id.action_to_webview + + R.id.fragment_locked to R.id.fragment_item_list -> R.id.action_locked_to_itemList + R.id.fragment_locked to R.id.fragment_welcome -> R.id.action_locked_to_welcome + + R.id.fragment_item_list to R.id.fragment_item_detail -> R.id.action_itemList_to_itemDetail + R.id.fragment_item_list to R.id.fragment_setting -> R.id.action_itemList_to_setting + R.id.fragment_item_list to R.id.fragment_account_setting -> R.id.action_itemList_to_accountSetting + R.id.fragment_item_list to R.id.fragment_locked -> R.id.action_itemList_to_locked + R.id.fragment_item_list to R.id.fragment_filter_backdrop -> R.id.action_itemList_to_filter + R.id.fragment_item_list to R.id.fragment_webview -> R.id.action_to_webview + + R.id.fragment_item_detail to R.id.fragment_webview -> R.id.action_to_webview + + R.id.fragment_setting to R.id.fragment_webview -> R.id.action_to_webview + + R.id.fragment_account_setting to R.id.fragment_welcome -> R.id.action_to_welcome + + R.id.fragment_filter_backdrop to R.id.fragment_item_detail -> R.id.action_filter_to_itemDetail + + else -> null + } + } + + private fun showAutoLockSelections() { + val autoLockValues = Setting.AutoLockTime.values() + val items = autoLockValues.map { it.stringValue }.toTypedArray() + + settingStore.autoLockTime.take(1) + .map { autoLockValues.indexOf(it) } + .flatMap { + AlertDialogHelper.showRadioAlertDialog( + activity, + R.string.auto_lock, + items, + it, + negativeButtonTitle = R.string.cancel + ) + } + .flatMapIterable { + listOf(RouteAction.InternalBack, SettingAction.AutoLockTime(autoLockValues[it])) + } + .subscribe(dispatcher::dispatch) + .addTo(compositeDisposable) + } +} \ No newline at end of file diff --git a/app/src/main/java/mozilla/lockbox/presenter/AutofillRoutePresenter.kt b/app/src/main/java/mozilla/lockbox/presenter/AutofillRoutePresenter.kt index 6a8114c96..e801cad9a 100644 --- a/app/src/main/java/mozilla/lockbox/presenter/AutofillRoutePresenter.kt +++ b/app/src/main/java/mozilla/lockbox/presenter/AutofillRoutePresenter.kt @@ -3,15 +3,11 @@ package mozilla.lockbox.presenter import android.app.Activity import android.content.Intent import android.os.Build -import android.os.Bundle import android.service.autofill.FillResponse import android.view.autofill.AutofillManager import androidx.annotation.IdRes import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import androidx.navigation.NavController import androidx.navigation.Navigation import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers @@ -24,7 +20,6 @@ import mozilla.lockbox.action.RouteAction import mozilla.lockbox.autofill.FillResponseBuilder import mozilla.lockbox.autofill.IntentBuilder import mozilla.lockbox.flux.Dispatcher -import mozilla.lockbox.flux.Presenter import mozilla.lockbox.log import mozilla.lockbox.store.AutofillStore import mozilla.lockbox.store.DataStore @@ -36,7 +31,7 @@ import mozilla.lockbox.view.FingerprintAuthDialogFragment @ExperimentalCoroutinesApi @RequiresApi(Build.VERSION_CODES.O) -class AutofillRoutePresenter( +open class AutofillRoutePresenter( private val activity: AppCompatActivity, private val responseBuilder: FillResponseBuilder, private val dispatcher: Dispatcher = Dispatcher.shared, @@ -44,23 +39,11 @@ class AutofillRoutePresenter( private val autofillStore: AutofillStore = AutofillStore.shared, private val dataStore: DataStore = DataStore.shared, private val pslSupport: PublicSuffixSupport = PublicSuffixSupport.shared -) : Presenter() { - private lateinit var navController: NavController - - private val navHostFragmentManager: FragmentManager - get() { - val fragmentManager = activity.supportFragmentManager - val navHost = fragmentManager.fragments.last() - return navHost.childFragmentManager - } - - private val currentFragment: Fragment - get() { - return navHostFragmentManager.fragments.last() - } +) : RoutePresenter(activity, dispatcher, routeStore) { override fun onViewReady() { navController = Navigation.findNavController(activity, R.id.autofill_fragment_nav_host) + routeStore.routes .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) @@ -94,7 +77,7 @@ class AutofillRoutePresenter( .addTo(compositeDisposable) } - private fun route(action: RouteAction) { + override fun route(action: RouteAction) { when (action) { is RouteAction.LockScreen -> { dismissDialogIfPresent() @@ -111,51 +94,8 @@ class AutofillRoutePresenter( } } - private fun navigateToFragment(@IdRes destinationId: Int, args: Bundle? = null) { - val src = navController.currentDestination ?: return - val srcId = src.id - if (srcId == destinationId && args == null) { - // No point in navigating if nothing has changed. - return - } - - val transition = findTransitionId(srcId, destinationId) ?: destinationId - - val navOptions = if (transition == destinationId) { - // Without being able to detect if we're in developer mode, - // it is too dangerous to RuntimeException. - val from = activity.resources.getResourceName(srcId) - val to = activity.resources.getResourceName(destinationId) - val graphName = activity.resources.getResourceName(navController.graph.id) - log.error( - "Cannot route from $from to $to. " + - "This is a developer bug, fixable by adding an action to $graphName.xml and/or ${javaClass.simpleName}" - ) - null - } else { - // Get the transition action out of the graph, before we manually clear the back - // stack, because it causes IllegalArgumentExceptions. - src.getAction(transition)?.navOptions?.let { navOptions -> - if (navOptions.shouldLaunchSingleTop()) { - while (navController.popBackStack()) { - // NOP - } - routeStore.clearBackStack() - } - navOptions - } - } - - try { - navController.navigate(destinationId, args, navOptions) - } catch (e: IllegalArgumentException) { - log.error("This appears to be a bug in navController", e) - navController.navigate(destinationId, args) - } - } - - private fun findTransitionId(@IdRes src: Int, @IdRes destination: Int): Int? { - return when (src to destination) { + override fun findTransitionId(@IdRes src: Int, @IdRes dest: Int): Int? { + return when (Pair(src, dest)) { R.id.fragment_null to R.id.fragment_filter_backdrop -> R.id.action_to_filter R.id.fragment_null to R.id.fragment_locked -> R.id.action_to_locked R.id.fragment_locked to R.id.fragment_filter_backdrop -> R.id.action_locked_to_filter @@ -164,10 +104,10 @@ class AutofillRoutePresenter( } } - private fun showDialogFragment(dialogFragment: DialogFragment, destination: RouteAction.DialogFragment) { + override fun showDialogFragment(dialogFragment: DialogFragment, destination: RouteAction.DialogFragment) { + val fragmentManager = activity.supportFragmentManager try { - dialogFragment.setTargetFragment(currentFragment, 0) - dialogFragment.show(navHostFragmentManager, dialogFragment.javaClass.name) + dialogFragment.show(fragmentManager, dialogFragment.javaClass.name) dialogFragment.setupDialog(destination.dialogTitle, destination.dialogSubtitle) } catch (e: IllegalStateException) { log.error("Could not show dialog", e) @@ -180,7 +120,7 @@ class AutofillRoutePresenter( private fun finishAutofill(action: AutofillAction) { when (action) { - is AutofillAction.Cancel -> setFillResponseAndFinish() + is AutofillAction.Cancel -> cancelAndFinish() is AutofillAction.Complete -> finishResponse(listOf(action.login)) is AutofillAction.CompleteMultiple -> finishResponse(action.logins) } @@ -188,15 +128,17 @@ class AutofillRoutePresenter( private fun finishResponse(passwords: List) { val response = responseBuilder.buildFilteredFillResponse(activity, passwords) - setFillResponseAndFinish(response) + response?.let { setFillResponseAndFinish(it) } ?: cancelAndFinish() } - private fun setFillResponseAndFinish(fillResponse: FillResponse? = null) { - if (fillResponse == null) { - activity.setResult(Activity.RESULT_CANCELED) - } else { - activity.setResult(Activity.RESULT_OK, Intent().putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse)) - } + private fun cancelAndFinish() { + activity.setResult(Activity.RESULT_CANCELED) + activity.finish() + } + + private fun setFillResponseAndFinish(fillResponse: FillResponse) { + val results = Intent().putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) + activity.setResult(Activity.RESULT_OK, results) activity.finish() } } \ No newline at end of file diff --git a/app/src/main/java/mozilla/lockbox/presenter/RoutePresenter.kt b/app/src/main/java/mozilla/lockbox/presenter/RoutePresenter.kt index eb86a6ed9..9455cd1b4 100644 --- a/app/src/main/java/mozilla/lockbox/presenter/RoutePresenter.kt +++ b/app/src/main/java/mozilla/lockbox/presenter/RoutePresenter.kt @@ -4,6 +4,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@file:Suppress("DEPRECATION") + package mozilla.lockbox.presenter import android.app.KeyguardManager @@ -17,55 +19,42 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.navigation.NavController -import androidx.navigation.Navigation -import io.reactivex.android.schedulers.AndroidSchedulers.mainThread import io.reactivex.rxkotlin.addTo import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.lockbox.R -import mozilla.lockbox.action.AppWebPageAction import mozilla.lockbox.action.DialogAction import mozilla.lockbox.action.RouteAction -import mozilla.lockbox.action.Setting -import mozilla.lockbox.action.SettingAction import mozilla.lockbox.extensions.view.AlertDialogHelper import mozilla.lockbox.extensions.view.AlertState import mozilla.lockbox.flux.Dispatcher import mozilla.lockbox.flux.Presenter import mozilla.lockbox.log import mozilla.lockbox.store.RouteStore -import mozilla.lockbox.store.SettingStore -import mozilla.lockbox.view.AppWebPageFragmentArgs import mozilla.lockbox.view.DialogFragment -import mozilla.lockbox.view.FingerprintAuthDialogFragment -import mozilla.lockbox.view.ItemDetailFragmentArgs @ExperimentalCoroutinesApi -class RoutePresenter( +abstract class RoutePresenter( private val activity: AppCompatActivity, - private val dispatcher: Dispatcher = Dispatcher.shared, - private val routeStore: RouteStore = RouteStore.shared, - private val settingStore: SettingStore = SettingStore.shared + private val dispatcher: Dispatcher, + private val routeStore: RouteStore ) : Presenter() { - private lateinit var navController: NavController - private val backListener = OnBackPressedCallback { - dispatcher.dispatch(RouteAction.InternalBack) - false - } + lateinit var navController: NavController - private val navHostFragmentManager: FragmentManager + open val navHostFragmentManager: FragmentManager get() { val fragmentManager = activity.supportFragmentManager val navHost = fragmentManager.fragments.last() return navHost.childFragmentManager } - private val currentFragment: Fragment + open val currentFragment: Fragment get() { return navHostFragmentManager.fragments.last() } - override fun onViewReady() { - navController = Navigation.findNavController(activity, R.id.fragment_nav_host) + val backListener = OnBackPressedCallback { + dispatcher.dispatch(RouteAction.InternalBack) + false } override fun onPause() { @@ -77,54 +66,13 @@ class RoutePresenter( override fun onResume() { super.onResume() activity.addOnBackPressedCallback(backListener) - routeStore.routes - .observeOn(mainThread()) - .subscribe(this::route) - .addTo(compositeDisposable) } - private fun route(action: RouteAction) { - activity.setTheme(R.style.AppTheme) - when (action) { - is RouteAction.Welcome -> navigateToFragment(R.id.fragment_welcome) - is RouteAction.Login -> navigateToFragment(R.id.fragment_fxa_login) - is RouteAction.Onboarding.FingerprintAuth -> - navigateToFragment(R.id.fragment_fingerprint_onboarding) - is RouteAction.Onboarding.Autofill -> navigateToFragment(R.id.fragment_autofill_onboarding) - is RouteAction.Onboarding.Confirmation -> navigateToFragment(R.id.fragment_onboarding_confirmation) - is RouteAction.ItemList -> navigateToFragment(R.id.fragment_item_list) - is RouteAction.SettingList -> navigateToFragment(R.id.fragment_setting) - is RouteAction.AccountSetting -> navigateToFragment(R.id.fragment_account_setting) - is RouteAction.LockScreen -> navigateToFragment(R.id.fragment_locked) - is RouteAction.Filter -> navigateToFragment(R.id.fragment_filter) - is RouteAction.ItemDetail -> navigateToFragment(R.id.fragment_item_detail, bundle(action)) - is RouteAction.OpenWebsite -> openWebsite(action.url) - is RouteAction.SystemSetting -> openSetting(action) - is RouteAction.UnlockFallbackDialog -> showUnlockFallback(action) - is RouteAction.AutoLockSetting -> showAutoLockSelections() - is RouteAction.DialogFragment.FingerprintDialog -> - showDialogFragment(FingerprintAuthDialogFragment(), action) - is DialogAction -> showDialog(action) - is AppWebPageAction -> navigateToFragment(R.id.fragment_webview, bundle(action)) - } - } - - private fun bundle(action: AppWebPageAction): Bundle { - return AppWebPageFragmentArgs.Builder() - .setUrl(action.url!!) - .setTitle(action.title!!) - .build() - .toBundle() - } + protected abstract fun route(action: RouteAction) - private fun bundle(action: RouteAction.ItemDetail): Bundle { - return ItemDetailFragmentArgs.Builder() - .setItemId(action.id) - .build() - .toBundle() - } + protected abstract fun findTransitionId(@IdRes src: Int, @IdRes dest: Int): Int? - private fun showDialog(destination: DialogAction) { + fun showDialog(destination: DialogAction) { AlertDialogHelper.showAlertDialog(activity, destination.viewModel) .map { alertState -> when (alertState) { @@ -141,29 +89,7 @@ class RoutePresenter( .addTo(compositeDisposable) } - private fun showAutoLockSelections() { - val autoLockValues = Setting.AutoLockTime.values() - val items = autoLockValues.map { it.stringValue }.toTypedArray() - - settingStore.autoLockTime.take(1) - .map { autoLockValues.indexOf(it) } - .flatMap { - AlertDialogHelper.showRadioAlertDialog( - activity, - R.string.auto_lock, - items, - it, - negativeButtonTitle = R.string.cancel - ) - } - .flatMapIterable { - listOf(RouteAction.InternalBack, SettingAction.AutoLockTime(autoLockValues[it])) - } - .subscribe(dispatcher::dispatch) - .addTo(compositeDisposable) - } - - private fun showDialogFragment(dialogFragment: DialogFragment, destination: RouteAction.DialogFragment) { + open fun showDialogFragment(dialogFragment: DialogFragment, destination: RouteAction.DialogFragment) { try { dialogFragment.setTargetFragment(currentFragment, 0) dialogFragment.show(navHostFragmentManager, dialogFragment.javaClass.name) @@ -173,16 +99,18 @@ class RoutePresenter( } } - private fun showUnlockFallback(action: RouteAction.UnlockFallbackDialog) { - val manager = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - val intent = manager.createConfirmDeviceCredentialIntent( - activity.getString(R.string.unlock_fallback_title), - activity.getString(R.string.confirm_pattern) - ) - currentFragment.startActivityForResult(intent, action.requestCode) + fun openWebsite(url: String) { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + activity.startActivity(browserIntent, null) + } + + fun openSetting(settingAction: RouteAction.SystemSetting) { + val settingIntent = Intent(settingAction.setting.intentAction) + settingIntent.data = settingAction.setting.data + activity.startActivity(settingIntent, null) } - private fun navigateToFragment(@IdRes destinationId: Int, args: Bundle? = null) { + fun navigateToFragment(@IdRes destinationId: Int, args: Bundle? = null) { val src = navController.currentDestination ?: return val srcId = src.id if (srcId == destinationId && args == null) { @@ -225,64 +153,12 @@ class RoutePresenter( } } - private fun findTransitionId(@IdRes from: Int, @IdRes to: Int): Int? { - // This maps two nodes in the graph_main.xml to the edge between them. - // If a RouteAction is called from a place the graph doesn't know about then - // the app will log.error. - return when (from to to) { - R.id.fragment_null to R.id.fragment_item_list -> R.id.action_init_to_unlocked - R.id.fragment_null to R.id.fragment_locked -> R.id.action_init_to_locked - R.id.fragment_null to R.id.fragment_welcome -> R.id.action_init_to_unprepared - - R.id.fragment_welcome to R.id.fragment_fxa_login -> R.id.action_welcome_to_fxaLogin - - R.id.fragment_fxa_login to R.id.fragment_item_list -> R.id.action_fxaLogin_to_itemList - R.id.fragment_fxa_login to R.id.fragment_fingerprint_onboarding -> - R.id.action_fxaLogin_to_fingerprint_onboarding - R.id.fragment_fxa_login to R.id.fragment_onboarding_confirmation -> - R.id.action_fxaLogin_to_onboarding_confirmation - - R.id.fragment_fingerprint_onboarding to R.id.fragment_onboarding_confirmation -> - R.id.action_fingerprint_onboarding_to_confirmation - R.id.fragment_fingerprint_onboarding to R.id.fragment_autofill_onboarding -> - R.id.action_onboarding_fingerprint_to_autofill - - R.id.fragment_autofill_onboarding to R.id.fragment_item_list -> R.id.action_to_itemList - R.id.fragment_autofill_onboarding to R.id.fragment_onboarding_confirmation -> R.id.action_autofill_onboarding_to_confirmation - - R.id.fragment_onboarding_confirmation to R.id.fragment_item_list -> R.id.action_to_itemList - R.id.fragment_onboarding_confirmation to R.id.fragment_webview -> R.id.action_to_webview - - R.id.fragment_locked to R.id.fragment_item_list -> R.id.action_locked_to_itemList - R.id.fragment_locked to R.id.fragment_welcome -> R.id.action_locked_to_welcome - - R.id.fragment_item_list to R.id.fragment_item_detail -> R.id.action_itemList_to_itemDetail - R.id.fragment_item_list to R.id.fragment_setting -> R.id.action_itemList_to_setting - R.id.fragment_item_list to R.id.fragment_account_setting -> R.id.action_itemList_to_accountSetting - R.id.fragment_item_list to R.id.fragment_locked -> R.id.action_itemList_to_locked - R.id.fragment_item_list to R.id.fragment_filter -> R.id.action_itemList_to_filter - R.id.fragment_item_list to R.id.fragment_webview -> R.id.action_to_webview - - R.id.fragment_item_detail to R.id.fragment_webview -> R.id.action_to_webview - - R.id.fragment_setting to R.id.fragment_webview -> R.id.action_to_webview - - R.id.fragment_account_setting to R.id.fragment_welcome -> R.id.action_to_welcome - - R.id.fragment_filter to R.id.fragment_item_detail -> R.id.action_filter_to_itemDetail - - else -> null - } - } - - private fun openWebsite(url: String) { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - activity.startActivity(browserIntent, null) - } - - private fun openSetting(settingAction: RouteAction.SystemSetting) { - val settingIntent = Intent(settingAction.setting.intentAction) - settingIntent.data = settingAction.setting.data - activity.startActivity(settingIntent, null) + fun showUnlockFallback(action: RouteAction.UnlockFallbackDialog) { + val manager = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val intent = manager.createConfirmDeviceCredentialIntent( + activity.getString(R.string.unlock_fallback_title), + activity.getString(R.string.confirm_pattern) + ) + currentFragment.startActivityForResult(intent, action.requestCode) } } \ No newline at end of file diff --git a/app/src/main/java/mozilla/lockbox/view/RootActivity.kt b/app/src/main/java/mozilla/lockbox/view/RootActivity.kt index 7bec7e985..7b2079b59 100644 --- a/app/src/main/java/mozilla/lockbox/view/RootActivity.kt +++ b/app/src/main/java/mozilla/lockbox/view/RootActivity.kt @@ -11,12 +11,12 @@ import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.lockbox.R -import mozilla.lockbox.presenter.RoutePresenter +import mozilla.lockbox.presenter.AppRoutePresenter import mozilla.lockbox.support.isDebug @ExperimentalCoroutinesApi class RootActivity : AppCompatActivity() { - private var presenter: RoutePresenter = RoutePresenter(this) + private var presenter: AppRoutePresenter = AppRoutePresenter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/test/java/mozilla/lockbox/presenter/AppRoutePresenterTest.kt b/app/src/test/java/mozilla/lockbox/presenter/AppRoutePresenterTest.kt new file mode 100644 index 000000000..fc708da9c --- /dev/null +++ b/app/src/test/java/mozilla/lockbox/presenter/AppRoutePresenterTest.kt @@ -0,0 +1,105 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package mozilla.lockbox.presenter + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.Navigation +import io.reactivex.Observable +import io.reactivex.observers.TestObserver +import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.lockbox.R +import mozilla.lockbox.action.AppWebPageAction +import mozilla.lockbox.action.RouteAction +import mozilla.lockbox.flux.Action +import mozilla.lockbox.flux.Dispatcher +import mozilla.lockbox.store.RouteStore +import mozilla.lockbox.store.SettingStore +import mozilla.lockbox.view.FingerprintAuthDialogFragment +import org.hamcrest.CoreMatchers +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner + +@ExperimentalCoroutinesApi +@RunWith(PowerMockRunner::class) +@PrepareForTest( + Navigation::class, + FingerprintAuthDialogFragment::class, + Intent::class +) +class AppRoutePresenterTest { + + @Mock + val activity: AppCompatActivity = Mockito.mock(AppCompatActivity::class.java) + + @Mock + val context: Context = Mockito.mock(Context::class.java) + + @Mock + val navController: NavController = Mockito.mock(NavController::class.java) + + @Mock + val navDestination: NavDestination = Mockito.mock(NavDestination::class.java) + + @Mock + private val settingStore = Mockito.mock(SettingStore::class.java) + + class FakeRouteStore : RouteStore() { + val routeStub = PublishSubject.create() + override val routes: Observable + get() = routeStub + } + + private val dispatcher = Dispatcher() + private val dispatcherObserver = TestObserver.create() + private val routeStore = FakeRouteStore() + + lateinit var subject: AppRoutePresenter + + @Before + fun setUp() { + dispatcher.register.subscribe(dispatcherObserver) + PowerMockito.`when`(navDestination.id).thenReturn(R.id.fragment_null) + PowerMockito.`when`(navController.currentDestination).thenReturn(navDestination) + + PowerMockito.mockStatic(Navigation::class.java) + + subject = AppRoutePresenter( + activity, + dispatcher, + routeStore, + settingStore + ) + subject.navController = navController + } + + @Test + fun `web view bundle is created`() { + val action = AppWebPageAction.FaqList + val result = subject.bundle(action) + Assert.assertThat(result, CoreMatchers.instanceOf(Bundle::class.java)) + } + + @Test + fun `item detail bundle is created`() { + val action = RouteAction.ItemDetail("id") + val result = subject.bundle(action) + Assert.assertThat(result, CoreMatchers.instanceOf(Bundle::class.java)) + } +} \ No newline at end of file diff --git a/app/src/test/java/mozilla/lockbox/presenter/AutofillRoutePresenterTest.kt b/app/src/test/java/mozilla/lockbox/presenter/AutofillRoutePresenterTest.kt index a68c08814..728d99e45 100644 --- a/app/src/test/java/mozilla/lockbox/presenter/AutofillRoutePresenterTest.kt +++ b/app/src/test/java/mozilla/lockbox/presenter/AutofillRoutePresenterTest.kt @@ -12,7 +12,6 @@ import android.content.Intent import android.service.autofill.FillResponse import android.view.autofill.AutofillManager import androidx.appcompat.app.AppCompatActivity -import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.navigation.NavController @@ -47,10 +46,9 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito +import org.mockito.Mockito.any import org.mockito.Mockito.anyString import org.mockito.Mockito.mock import org.mockito.Mockito.verify @@ -61,8 +59,6 @@ import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import org.powermock.api.mockito.PowerMockito.`when` as whenCalled -fun any(): T = Mockito.any() - @ExperimentalCoroutinesApi @RunWith(PowerMockRunner::class) @PrepareForTest( @@ -80,7 +76,8 @@ class AutofillRoutePresenterTest { val navDestination: NavDestination = mock(NavDestination::class.java) @Mock - val fingerprintAuthDialogFragment: FingerprintAuthDialogFragment = PowerMockito.mock(FingerprintAuthDialogFragment::class.java) + val fingerprintAuthDialogFragment: FingerprintAuthDialogFragment = + PowerMockito.mock(FingerprintAuthDialogFragment::class.java) @Mock val autofillFilterFragment: AutofillFilterFragment = PowerMockito.mock(AutofillFilterFragment::class.java) @@ -92,7 +89,7 @@ class AutofillRoutePresenterTest { val childFragmentManager: FragmentManager = mock(FragmentManager::class.java) @Mock - val currentFragment: DialogFragment = mock(DialogFragment::class.java) + val currentFragment: Fragment = mock(Fragment::class.java) @Mock val navHost: Fragment = mock(Fragment::class.java) @@ -171,7 +168,6 @@ class AutofillRoutePresenterTest { private val autofillStore = FakeAutofillStore() private val pslSupport = FakePslSupport() private val dispatcherObserver = TestObserver.create() - lateinit var subject: AutofillRoutePresenter @Before @@ -196,7 +192,7 @@ class AutofillRoutePresenterTest { IntentBuilder.setSearchRequired(intent, true) whenCalled(activity.intent).thenReturn(callingIntent) whenCalled(activity.supportFragmentManager).thenReturn(fragmentManager) - whenCalled(navDestination.id).thenReturn(R.id.fragment_filter_backdrop) + whenCalled(navDestination.id).thenReturn(R.id.fragment_null) whenCalled(navController.currentDestination).thenReturn(navDestination) PowerMockito.mockStatic(Navigation::class.java) whenCalled(Navigation.findNavController(activity, R.id.autofill_fragment_nav_host)).thenReturn(navController) @@ -216,15 +212,12 @@ class AutofillRoutePresenterTest { @Test fun `locked routes`() { routeStore.routeStub.onNext(RouteAction.LockScreen) - verify(navController).navigate(R.id.fragment_locked, null, null) } @Test fun `item list routes navigate to filter backdrop`() { - whenCalled(navDestination.id).thenReturn(R.id.fragment_locked) routeStore.routeStub.onNext(RouteAction.ItemList) - verify(navController).navigate(R.id.fragment_filter_backdrop, null, null) } @@ -232,7 +225,7 @@ class AutofillRoutePresenterTest { fun `autofill search dialog route route to autofill filter fragment`() { routeStore.routeStub.onNext(RouteAction.DialogFragment.AutofillSearchDialog) - verify(autofillFilterFragment).show(eq(childFragmentManager), anyString()) + verify(autofillFilterFragment).show(eq(fragmentManager), anyString()) verify(autofillFilterFragment).setupDialog(R.string.autofill, null) } @@ -241,7 +234,7 @@ class AutofillRoutePresenterTest { val title = R.string.fingerprint_dialog_title routeStore.routeStub.onNext(RouteAction.DialogFragment.FingerprintDialog(title)) - verify(fingerprintAuthDialogFragment).show(eq(childFragmentManager), anyString()) + verify(fingerprintAuthDialogFragment).show(eq(fragmentManager), anyString()) verify(fingerprintAuthDialogFragment).setupDialog(title, null) } diff --git a/app/src/test/java/mozilla/lockbox/presenter/RoutePresenterTest.kt b/app/src/test/java/mozilla/lockbox/presenter/RoutePresenterTest.kt new file mode 100644 index 000000000..642b89bf1 --- /dev/null +++ b/app/src/test/java/mozilla/lockbox/presenter/RoutePresenterTest.kt @@ -0,0 +1,209 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package mozilla.lockbox.presenter + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.Navigation +import io.reactivex.Observable +import io.reactivex.Scheduler +import io.reactivex.android.plugins.RxAndroidPlugins +import io.reactivex.disposables.Disposable +import io.reactivex.internal.schedulers.ExecutorScheduler +import io.reactivex.observers.TestObserver +import io.reactivex.subjects.PublishSubject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.lockbox.R +import mozilla.lockbox.action.RouteAction +import mozilla.lockbox.action.SettingIntent +import mozilla.lockbox.autofill.IntentBuilder +import mozilla.lockbox.extensions.view.AlertDialogHelper +import mozilla.lockbox.flux.Action +import mozilla.lockbox.flux.Dispatcher +import mozilla.lockbox.store.RouteStore +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit +import org.powermock.api.mockito.PowerMockito.`when` as whenCalled + +@ExperimentalCoroutinesApi +@RunWith(PowerMockRunner::class) +@PrepareForTest( + RoutePresenter::class, + Navigation::class, + Intent::class, + AlertDialogHelper::class +) +class RoutePresenterTest { + + @Mock + val activity: AppCompatActivity = Mockito.mock(AppCompatActivity::class.java) + + @Mock + val context: Context = Mockito.mock(Context::class.java) + + @Mock + val navController: NavController = Mockito.mock(NavController::class.java) + + @Mock + val fragmentManager: FragmentManager = Mockito.mock(FragmentManager::class.java) + + @Mock + val currentFragment: Fragment = Mockito.mock(Fragment::class.java) + + @Mock + val navDestination: NavDestination = Mockito.mock(NavDestination::class.java) + + @Mock + val dialogFragment = Mockito.mock(mozilla.lockbox.view.DialogFragment::class.java) + + @Mock + val intent: Intent = PowerMockito.mock(Intent::class.java) + + @Mock + val settingIntent = Mockito.mock(SettingIntent.Security::class.java) + + @Mock + val settingAction = Mockito.mock(RouteAction.SystemSetting::class.java) + + @Mock + val action = Mockito.mock(Action::class.java) + + @Mock + val dialogHelper = Mockito.mock(AlertDialogHelper::class.java) + + class FakeRouteStore : RouteStore() { + val routeStub = PublishSubject.create() + override val routes: Observable + get() = routeStub + } + + private val immediate = object : Scheduler() { + override fun scheduleDirect( + run: Runnable, + delay: Long, + unit: TimeUnit + ): Disposable { + return super.scheduleDirect(run, 0, unit) + } + + override fun createWorker(): Scheduler.Worker { + return ExecutorScheduler.ExecutorWorker( + Executor { it.run() }) + } + } + + private val dispatcher = Dispatcher() + private val dispatcherObserver = TestObserver.create() + private val routeStore = FakeRouteStore() + val callingIntent = Intent() + + lateinit var subject: RoutePresenter + + class RoutePresenterStub( + activity: AppCompatActivity, + dispatcher: Dispatcher, + routeStore: RouteStore, + private val navHostFragmentManagerStub: FragmentManager, + private val currentFragmentStub: Fragment + ) : RoutePresenter(activity, dispatcher, routeStore) { + + override fun route(action: RouteAction) {} + + override fun findTransitionId(src: Int, dest: Int): Int? { return null } + + override val navHostFragmentManager: FragmentManager + get() = navHostFragmentManagerStub + + override val currentFragment: Fragment + get() = currentFragmentStub + } + + @Before + fun setUp() { + dispatcher.register.subscribe(dispatcherObserver) + + // needed to make `.observeOn(AndroidSchedulers.mainThread())` work + // this is unnecessary in other tests because we are using the + // robolectric testrunner there. + RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate } + + whenCalled(activity.supportFragmentManager).thenReturn(fragmentManager) + whenCalled(navDestination.id).thenReturn(R.id.fragment_null) + whenCalled(navController.currentDestination).thenReturn(navDestination) + + PowerMockito.mockStatic(Navigation::class.java) + PowerMockito.mockStatic(AlertDialogHelper::class.java) + PowerMockito.whenNew(Intent::class.java).withNoArguments() + .thenReturn(intent) + PowerMockito.whenNew(AlertDialogHelper::class.java).withNoArguments() + .thenReturn(dialogHelper) + + IntentBuilder.setSearchRequired(intent, true) + whenCalled(activity.intent).thenReturn(callingIntent) + whenCalled(settingAction.setting).thenReturn(settingIntent) + + val securityIntent = "android.provider.Settings.ACTION_SECURITY_SETTINGS" + whenCalled(settingIntent.intentAction).thenReturn(securityIntent) + whenCalled(Intent(ArgumentMatchers.any(String::class.java))).thenReturn(callingIntent) + + subject = RoutePresenterStub( + activity, + dispatcher, + routeStore, + fragmentManager, + currentFragment + ) + subject.navController = navController + } + + @Test + fun `security settings intent starts the activity`() { + val settingAction = settingAction + subject.openSetting(settingAction) + verify(activity).startActivity(eq(ArgumentMatchers.any()), null) + } + + @Test + fun `website url will create browser intent and start activity`() { + val url = "com.meow" + subject.openWebsite(url) + verify(activity).startActivity(eq(ArgumentMatchers.any()), null) + } + + @Test + fun `dialog fragment is set up`() { + val destination = RouteAction.DialogFragment.FingerprintDialog( + R.string.fingerprint_dialog_title, + R.string.enable_fingerprint_dialog_subtitle + ) + subject.showDialogFragment(dialogFragment, destination) + verify(dialogFragment).setupDialog(destination.dialogTitle, destination.dialogSubtitle) + } + + @Test + fun `autofill dialog fragment is set up`() { + val destination = RouteAction.DialogFragment.AutofillSearchDialog + subject.showDialogFragment(dialogFragment, destination) + verify(dialogFragment).setupDialog(destination.dialogTitle, null) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index d211c981e..31aad57ca 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { ext.coroutine_version = '1.0.1' ext.androidxTest_version = '1.1.0' ext.picasso_version = '2.71828' - ext.recyclerview_version = '1.1.0-alpha03' + ext.recyclerview_version = '1.1.0-alpha04' repositories { google()