16900 make navgraph inflation asynchronous (#18889)

* For #16900: implement async navgraph inflation

For #16900: removed nav graph from xml

For #16900: inflate navGraph programatically

For #16900: Made NavGraph inflation asynchronous

For #16900: Changed to block with runBlocking

For #16900: Refactored blocking call into a function

For 16900: NavGraph inflation is now async

We now attach the nav graph (or check if its attached) on every nav call ( an extension function for NavController).
This is done by checking the value of the job stored in PerfNavController.map which keeps track of the job with the NavController as a Key.
If the job hasn't been completed, it will block the main thread until the job is done. The job itself is responsible for attaching the navgraph
to the navcontroller (and the inflation of the latter too)

For 16900: rebased upstream master

For 16900: Rebase on master

For #16900: Fixed Async Navgraph navigation per review comments.

1)The Asynchronous method is now found in NavGraphProvider.kt. It creates a job on the IO dispatcher
2)The Job is tracked through a WeakHashMap from Controller --> NavGraph
3)The Coroutine scope doesn't use MainScope() anymore
4)The Coroutine is cancelled if the Activity is destroyed
5)The tests mockk the blockForNavGraphInflation method through the FenixReoboelectricTestApplication instead of calling the mock every setup()

For #16900: inflateNavGraphAsync now takes navController

For #16900: Pass lifecycleScope to NavGraphProvider

For #16900: removed unused mock

For #16900: Added linter rules for navigate calls

We need linting rules to make sure no one calls the NavController.navigate() methods

For #16900: Added TestRule to help abstract the mocks in the code

For 16900: Fix linting problems

For #16900: Cleaned duplicated code in tests

For #16900: cleaned up NavGraphTestRule for finished test

For #16900: had to revert an accidentally edited file

For #16900: rebased master

* For #16900: Review nits for async navgraph

This is composed of squash commits, the original messages can be found below:

-> DisableNavGraphProviderAssertionRule + kdoc.

Use test rule in RobolectricApplication.

Fix failing CrashReporterControllerTest

Fix blame by -> navigate in tests.

This commit was generated by the following commands only:
```
find app/src/test -type f -exec sed -i '' "/import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph/d" {} \;
find app/src/test -type f -exec sed -i "" "s/navigateBlockingForAsyncNavGraph/navigate/g" {} \;
git checkout app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
```

Fix various blame

This is expected to be squashed into the first commit so, if so, it'd
fix the blame.

Move test rule to helpers pkg.

add missing license header

Add import change I missed

fix unused imports

Replace robolectricTestrunner with test rule.

Improve navGraphProvider docs

Remove unnecessary rule as defined by robolectric.

add clarifying comment to robolectric

remove unnecessary space

* For #16900: nit fixes for MozillaNavigateCheck and lint fixes

3 squash commits:
 *Changed violation message and fixed the lint rule for MozillaNavigateCheck
 *Added suppression to NavController.kt
 *Fixed detekt violations

* For 16900: Fixed failing tests

Co-authored-by: Michael Comella <michael.l.comella@gmail.com>
upstream-sync
MarcLeclair 3 years ago committed by GitHub
parent 973c891c5e
commit 990bfa7e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -21,7 +21,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
// BEFORE INCREASING THESE VALUES, PLEASE CONSULT WITH THE PERF TEAM.
private const val EXPECTED_SUPPRESSION_COUNT = 11
private const val EXPECTED_RUNBLOCKING_COUNT = 2
private const val EXPECTED_RUNBLOCKING_COUNT = 3
private const val EXPECTED_COMPONENT_INIT_COUNT = 42
private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12
private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4

@ -15,6 +15,7 @@ import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import java.lang.ref.WeakReference
class AppRequestInterceptor(
@ -97,7 +98,7 @@ class AppRequestInterceptor(
// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate(
navController?.get()?.navigateBlockingForAsyncNavGraph(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
)

@ -33,12 +33,12 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers.IO
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.search.SearchEngine
@ -79,6 +79,7 @@ import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExcepti
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
@ -94,6 +95,7 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.NavGraphProvider
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers
@ -131,7 +133,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// components requires context to access.
protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos()
private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager
@ -193,7 +194,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.publicSuffixList.prefetch()
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
setContentView(R.layout.activity_home)
setContentView(R.layout.activity_home).run {
// Do not call anything between setContentView and inflateNavGraphAsync.
// It needs to start its job as early as possible.
NavGraphProvider.inflateNavGraphAsync(navHost.navController, lifecycleScope)
}
// Must be after we set the content view
if (isVisuallyComplete) {
@ -934,7 +939,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
webExtensionId = webExtensionState.id,
webExtensionTitle = webExtensionState.name
)
navHost.navController.navigate(action)
navHost.navController.navigateBlockingForAsyncNavGraph(action)
}
/**

@ -8,6 +8,7 @@ import androidx.navigation.NavController
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.navigateSafe
/**
@ -55,6 +56,6 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
unsupportedAddons.toTypedArray()
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
}

@ -25,6 +25,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -215,7 +216,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections
.actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon)
}
Navigation.findNavController(this).navigate(directions)
Navigation.findNavController(this).navigateBlockingForAsyncNavGraph(directions)
}
}
}
@ -226,7 +227,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon
)
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
}
@ -236,7 +237,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
addon
)
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
}

@ -128,6 +128,7 @@ import java.lang.ref.WeakReference
import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior
import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import mozilla.components.support.ktx.android.view.enterToImmersiveMode
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.ext.measureNoInline
@ -557,7 +558,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
showPage = true,
sessionId = getCurrentTab()?.id
)
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
},
onNeedToRequestPermissions = { permissions ->
@ -568,7 +569,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserAnimator.captureEngineViewAndDrawStatically {
val directions =
NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
}
),
@ -979,7 +980,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
override fun onBackLongPressed(): Boolean {
findNavController().navigate(
findNavController().navigateBlockingForAsyncNavGraph(
NavGraphDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)

@ -33,6 +33,7 @@ import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.requireComponents
@ -264,7 +265,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
)
.setText(view.context.getString(messageStringRes))
.setAction(requireContext().getString(R.string.create_collection_view)) {
findNavController().navigate(
findNavController().navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false)
)
}

@ -24,6 +24,7 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel
@ -118,7 +119,7 @@ class DefaultBrowserToolbarController(
// When closing the last tab we must show the undo snackbar in the home fragment
if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) {
homeViewModel.sessionToDelete = it.id
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome()
)
} else {
@ -132,7 +133,7 @@ class DefaultBrowserToolbarController(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB)
)
activity.browsingModeManager.mode = BrowsingMode.Normal
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}
@ -143,7 +144,7 @@ class DefaultBrowserToolbarController(
)
)
activity.browsingModeManager.mode = BrowsingMode.Private
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}

@ -39,6 +39,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
@ -162,7 +163,7 @@ class DefaultBrowserToolbarMenuController(
}
is ToolbarMenu.Item.Back -> {
if (item.viewHistory) {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)
@ -175,7 +176,7 @@ class DefaultBrowserToolbarMenuController(
}
is ToolbarMenu.Item.Forward -> {
if (item.viewHistory) {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)
@ -212,7 +213,7 @@ class DefaultBrowserToolbarMenuController(
),
showPage = true
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
@ -341,7 +342,7 @@ class DefaultBrowserToolbarMenuController(
)
}
is ToolbarMenu.Item.NewTab -> {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}

@ -2,6 +2,9 @@
* 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/. */
// We suppress the calls to `navigate` since we invoke the Android `NavController.navigate` through
// this file. Detekt checks for the `navigate()` function calls, which should be ignored in this file.
@file:Suppress("MozillaNavigateCheck")
package org.mozilla.fenix.ext
import androidx.annotation.IdRes
@ -10,12 +13,15 @@ import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import io.sentry.Sentry
import org.mozilla.fenix.components.isSentryEnabled
import org.mozilla.fenix.perf.NavGraphProvider
/**
* Navigate from the fragment with [id] using the given [directions].
* If the id doesn't match the current destination, an error is recorded.
*/
fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: NavOptions? = null) {
NavGraphProvider.blockForNavGraphInflation(this)
if (id == null || this.currentDestination?.id == id) {
this.navigate(directions, navOptions)
} else {
@ -23,6 +29,21 @@ fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: Na
}
}
fun NavController.navigateBlockingForAsyncNavGraph(resId: Int) {
NavGraphProvider.blockForNavGraphInflation(this)
this.navigate(resId)
}
fun NavController.navigateBlockingForAsyncNavGraph(directions: NavDirections) {
NavGraphProvider.blockForNavGraphInflation(this)
this.navigate(directions)
}
fun NavController.navigateBlockingForAsyncNavGraph(directions: NavDirections, navOptions: NavOptions?) {
NavGraphProvider.blockForNavGraphInflation(this)
this.navigate(directions, navOptions)
}
fun NavController.alreadyOnDestination(@IdRes destId: Int?): Boolean {
return destId?.let { currentDestination?.id == it || popBackStack(it, false) } ?: false
}
@ -38,6 +59,6 @@ fun NavController.navigateSafe(
directions: NavDirections
) {
if (currentDestination?.id == resId) {
this.navigate(directions)
this.navigateBlockingForAsyncNavGraph(directions)
}
}

@ -102,6 +102,7 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
@ -533,7 +534,7 @@ class HomeFragment : Fragment() {
requireContext().getString(R.string.snackbar_deleted_undo),
{
requireComponents.useCases.tabsUseCases.undo.invoke()
findNavController().navigate(
findNavController().navigateBlockingForAsyncNavGraph(
HomeFragmentDirections.actionGlobalBrowser(null)
)
},
@ -624,7 +625,8 @@ class HomeFragment : Fragment() {
}
private fun navToSavedLogins() {
findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
findNavController().navigateBlockingForAsyncNavGraph(
HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
}
private fun dispatchModeChanges(mode: Mode) {

@ -8,6 +8,7 @@ import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.lib.crash.Crash
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
/**
* When the app crashes, the user has the option to report it.
@ -26,6 +27,6 @@ class CrashReporterIntentProcessor : HomeIntentProcessor {
private fun openToCrashReporter(intent: Intent, navController: NavController) {
val directions = NavGraphDirections.actionGlobalCrashReporter(intent)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
}

@ -21,6 +21,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.SearchWidgetCreator
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor.DeepLinkVerifier
import org.mozilla.fenix.settings.SupportUtils
@ -76,7 +77,7 @@ class DeepLinkIntentProcessor(
}
if (!navController.alreadyOnDestination(globalDirections.destinationId)) {
navController.navigate(globalDirections.navDirections)
navController.navigateBlockingForAsyncNavGraph(globalDirections.navDirections)
}
}

@ -11,6 +11,7 @@ import kotlinx.android.synthetic.main.onboarding_manual_signin.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragmentDirections
class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -22,7 +23,7 @@ class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(vie
it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn)
val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
}

@ -88,7 +88,7 @@ class DefaultBookmarkController(
}
override fun handleBookmarkEdit(node: BookmarkNode) {
navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
navigateToGivenDirection(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
}
override fun handleBookmarkSelected(node: BookmarkNode) {
@ -118,7 +118,7 @@ class DefaultBookmarkController(
}
override fun handleBookmarkSharing(item: BookmarkNode) {
navigate(
navigateToGivenDirection(
BookmarkFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)
@ -182,7 +182,7 @@ class DefaultBookmarkController(
}
}
private fun navigate(directions: NavDirections) {
private fun navigateToGivenDirection(directions: NavDirections) {
invokePendingDeletion.invoke()
navController.nav(R.id.bookmarkFragment, directions)
}

@ -202,7 +202,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
true
}
R.id.add_bookmark_folder -> {
navigate(
navigateToBookmarkFragment(
BookmarkFragmentDirections
.actionBookmarkFragmentToBookmarkAddFolderFragment()
)
@ -226,7 +226,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
val shareTabs = bookmarkStore.state.mode.selectedItems.map {
ShareData(url = it.url, title = it.title)
}
navigate(
navigateToBookmarkFragment(
BookmarkFragmentDirections.actionGlobalShareFragment(
data = shareTabs.toTypedArray()
)
@ -243,10 +243,10 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
private fun showTabTray() {
invokePendingDeletion()
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
navigateToBookmarkFragment(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
}
private fun navigate(directions: NavDirections) {
private fun navigateToBookmarkFragment(directions: NavDirections) {
invokePendingDeletion()
findNavController().nav(
R.id.bookmarkFragment,

@ -15,6 +15,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.selection.SelectionInteractor
@ -119,7 +120,7 @@ class BookmarkView(
adapter = bookmarkAdapter
}
view.bookmark_folders_sign_in.setOnClickListener {
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
navController.navigateBlockingForAsyncNavGraph(NavGraphDirections.actionGlobalTurnOnSync())
}
view.swipe_refresh.setOnRefreshListener {
interactor.onRequestSync()

@ -17,6 +17,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
@Suppress("TooManyFunctions")
interface HistoryController {
@ -94,7 +95,7 @@ class DefaultHistoryController(
}
override fun handleShare(item: HistoryItem) {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
HistoryFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)
@ -110,7 +111,7 @@ class DefaultHistoryController(
}
override fun handleEnterRecentlyClosed() {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
HistoryFragmentDirections.actionGlobalRecentlyClosed(),
NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build()
)

@ -304,10 +304,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
val directions = HistoryFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navigate(directions)
navigateToHistoryFragment(directions)
}
private fun navigate(directions: NavDirections) {
private fun navigateToHistoryFragment(directions: NavDirections) {
invokePendingDeletion()
findNavController().nav(
R.id.historyFragment,

@ -19,6 +19,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
interface RecentlyClosedController {
fun handleOpen(item: RecoverableTab, mode: BrowsingMode? = null)
@ -48,7 +49,7 @@ class DefaultRecentlyClosedController(
}
override fun handleNavigateToHistory() {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment(),
NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build()
)
@ -64,7 +65,7 @@ class DefaultRecentlyClosedController(
}
override fun handleShare(item: RecoverableTab) {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
)

@ -7,6 +7,7 @@ package org.mozilla.fenix.nimbus
import androidx.navigation.NavController
import mozilla.components.service.nimbus.ui.NimbusExperimentsAdapterDelegate
import org.mozilla.experiments.nimbus.EnrolledExperiment
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
/**
* View used for managing Nimbus experiments.
@ -21,6 +22,6 @@ class NimbusExperimentsView(
experiment.userFacingName
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
}

@ -0,0 +1,59 @@
/* 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 org.mozilla.fenix.perf
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavController
import java.util.WeakHashMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.mozilla.fenix.R
/**
* This class asynchronously loads the navigation graph XML. This is a performance optimization:
* large nav graphs take ~29ms to inflate on the Moto G5 (#16900). This also seemingly prevents the
* HomeFragment layout XML from being unnecessarily inflated when it isn't used, improving perf by
* ~148ms on the Moto G5 for VIEW start up (#18245) though it was unintentional and we may wish to
* implement more intentional for that.
*
* This class is defined as an Object, rather than as a class instance in our Components, because
* it needs to be called by the [NavController] extension function which can't easily access Components.
*
* To use this class properly, [inflateNavGraphAsync] must be called first before
* [blockForNavGraphInflation] using the same [NavController] instance.
*/
object NavGraphProvider {
// We want to store member state on the NavController. However, there is no way to do this.
// Instead, we store state as part of a map: NavController instance -> State. In order to
// garbage collect our data when the NavController is no longer relevant, we use a WeakHashMap.
private val map = WeakHashMap<NavController, Job>()
fun inflateNavGraphAsync(navController: NavController, lifecycleScope: LifecycleCoroutineScope) {
val inflationJob = lifecycleScope.launch(Dispatchers.IO) {
val inflater = navController.navInflater
navController.graph = inflater.inflate(R.navigation.nav_graph)
}
map[navController] = inflationJob
}
/**
* The job should block the main thread if it isn't completed so that the NavGraph can be loaded
* before any navigation is done.
*
* [inflateNavGraphAsync] must be called before this method.
*
* @throws IllegalStateException if [inflateNavGraphAsync] wasn't called first for this [NavController]
*/
fun blockForNavGraphInflation(navController: NavController) {
val inflationJob = map[navController] ?: throw IllegalStateException("Expected " +
"`NavGraphProvider.inflateNavGraphAsync` to be called before this method with the same " +
"`NavController` instance. If this occurred in a test, you probably need to add the " +
"DisableNavGraphProviderAssertionRule.")
runBlockingIncrement { inflationJob.join() }
}
}

@ -45,6 +45,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.navigateToNotificationsSettings
import org.mozilla.fenix.ext.requireComponents
@ -480,7 +481,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private fun navigateFromSettings(directions: NavDirections) {
view?.findNavController()?.let { navController ->
if (navController.currentDestination?.id == R.id.settingsFragment) {
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
}
}

@ -16,6 +16,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -32,7 +33,7 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
private val exceptionsClickListener = Preference.OnPreferenceClickListener {
val directions =
TrackingProtectionFragmentDirections.actionTrackingProtectionFragmentToExceptionsFragment()
requireView().findNavController().navigate(directions)
requireView().findNavController().navigateBlockingForAsyncNavGraph(directions)
true
}
private lateinit var customCookies: CheckBoxPreference

@ -22,6 +22,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.settings.SupportUtils
@ -178,7 +179,7 @@ class AboutFragment : Fragment(), AboutPageListener {
private fun openLibrariesPage() {
val navController = findNavController()
navController.navigate(R.id.action_aboutFragment_to_aboutLibrariesFragment)
navController.navigateBlockingForAsyncNavGraph(R.id.action_aboutFragment_to_aboutLibrariesFragment)
}
override fun onAboutItemClicked(item: AboutItem) {

@ -26,6 +26,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
@ -59,7 +60,7 @@ class TurnOnSyncFragment : Fragment(), AccountObserver {
private fun navigateToPairFragment() {
val directions = TurnOnSyncFragmentDirections.actionTurnOnSyncFragmentToPairFragment()
requireView().findNavController().navigate(directions)
requireView().findNavController().navigateBlockingForAsyncNavGraph(directions)
requireComponents.analytics.metrics.track(Event.SyncAuthScanPairing)
}

@ -11,6 +11,7 @@ import androidx.preference.PreferenceFragmentCompat
import mozilla.components.service.fxa.SyncEngine
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.SyncPreferenceView
@ -39,17 +40,17 @@ class CreditCardsSettingFragment : PreferenceFragmentCompat() {
onSignInToSyncClicked = {
val directions =
CreditCardsSettingFragmentDirections.actionCreditCardsSettingFragmentToTurnOnSyncFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
},
onSyncStatusClicked = {
val directions =
CreditCardsSettingFragmentDirections.actionGlobalAccountSettingsFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
},
onReconnectClicked = {
val directions =
CreditCardsSettingFragmentDirections.actionGlobalAccountProblemFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
)
}
@ -61,13 +62,13 @@ class CreditCardsSettingFragment : PreferenceFragmentCompat() {
val directions =
CreditCardsSettingFragmentDirections
.actionCreditCardsSettingFragmentToCreditCardEditorFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
getPreferenceKey(R.string.pref_key_credit_cards_manage_saved_cards) -> {
val directions =
CreditCardsSettingFragmentDirections
.actionCreditCardsSettingFragmentToCreditCardsManagementFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
}

@ -5,6 +5,7 @@
package org.mozilla.fenix.settings.creditcards.controller
import androidx.navigation.NavController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.settings.creditcards.CreditCardsManagementFragmentDirections
/**
@ -27,7 +28,7 @@ class DefaultCreditCardsManagementController(
) : CreditCardsManagementController {
override fun handleCreditCardClicked() {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
CreditCardsManagementFragmentDirections
.actionCreditCardsManagementFragmentToCreditCardEditorFragment()
)

@ -28,6 +28,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
@ -199,7 +200,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
// If the user deletes all open tabs we need to make sure we remove
// the BrowserFragment from the backstack.
popBackStack(R.id.homeFragment, false)
navigate(DeleteBrowsingDataFragmentDirections.actionGlobalSettingsFragment())
navigateBlockingForAsyncNavGraph(DeleteBrowsingDataFragmentDirections.actionGlobalSettingsFragment())
}
}
}

@ -8,6 +8,7 @@ import androidx.navigation.NavController
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.logins.LoginsAction
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
@ -40,7 +41,7 @@ class LoginsListController(
fun handleItemClicked(item: SavedLogin) {
loginsFragmentStore.dispatch(LoginsAction.LoginSelected(item))
metrics.track(Event.OpenOneLogin)
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid)
)
}

@ -20,6 +20,7 @@ import mozilla.components.service.sync.logins.LoginsStorageException
import mozilla.components.service.sync.logins.NoSuchRecordException
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.settings.logins.LoginsAction
import org.mozilla.fenix.settings.logins.LoginsFragmentStore
import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections
@ -83,7 +84,7 @@ open class SavedLoginsStorageController(
EditLoginFragmentDirections.actionEditLoginFragmentToLoginDetailFragment(
loginId
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
}
saveLoginJob?.invokeOnCompletion {

@ -33,6 +33,7 @@ import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.redirectToReAuth
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -199,7 +200,7 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
LoginDetailFragmentDirections.actionLoginDetailFragmentToEditLoginFragment(
login!!
)
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
private fun displayDeleteLoginDialog() {

@ -27,6 +27,7 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.secure
@ -130,17 +131,17 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() {
onSignInToSyncClicked = {
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
},
onSyncStatusClicked = {
val directions =
SavedLoginsAuthFragmentDirections.actionGlobalAccountSettingsFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
},
onReconnectClicked = {
val directions =
SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
)
@ -213,19 +214,19 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() {
context?.components?.analytics?.metrics?.track(Event.OpenLogins)
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginsListFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
private fun navigateToSaveLoginSettingFragment() {
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToSavedLoginsSettingFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
private fun navigateToLoginExceptionFragment() {
val directions =
SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginExceptionsFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
companion object {

@ -18,6 +18,7 @@ import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase
import mozilla.components.support.base.feature.OnNeedToRequestPermissions
import org.mozilla.fenix.components.PermissionStorage
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled
import org.mozilla.fenix.settings.toggle
@ -199,6 +200,6 @@ class DefaultQuickSettingsController(
private fun navigateToManagePhoneFeature(phoneFeature: PhoneFeature) {
val directions = QuickSettingsSheetDialogFragmentDirections
.actionGlobalSitePermissionsManagePhoneFeature(phoneFeature)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
}

@ -38,6 +38,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.utils.allowUndo
class RadioSearchEngineListPreference @JvmOverloads constructor(
@ -146,7 +147,7 @@ class RadioSearchEngineListPreference @JvmOverloads constructor(
val directions = SearchEngineFragmentDirections
.actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.id)
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
private fun deleteSearchEngine(

@ -13,6 +13,7 @@ import androidx.preference.SwitchPreference
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.settings.SharedPreferenceUpdater
@ -99,7 +100,7 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
getPreferenceKey(R.string.pref_key_add_search_engine) -> {
val directions = SearchEngineFragmentDirections
.actionSearchEngineFragmentToAddSearchEngineFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
}

@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext
import mozilla.components.feature.sitepermissions.SitePermissions
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
@ -161,6 +162,6 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() {
phoneFeature = phoneFeature,
sitePermissions = sitePermissions
)
requireView().findNavController().navigate(directions)
requireView().findNavController().navigateBlockingForAsyncNavGraph(directions)
}
}

@ -12,6 +12,7 @@ import androidx.preference.PreferenceFragmentCompat
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
@ -42,7 +43,7 @@ class SitePermissionsFragment : PreferenceFragmentCompat() {
exceptionsCategory.onPreferenceClickListener = OnPreferenceClickListener {
val directions = SitePermissionsFragmentDirections.actionSitePermissionsToExceptions()
Navigation.findNavController(requireView()).navigate(directions)
Navigation.findNavController(requireView()).navigateBlockingForAsyncNavGraph(directions)
true
}
}
@ -76,6 +77,6 @@ class SitePermissionsFragment : PreferenceFragmentCompat() {
requireComponents.analytics.metrics.track(Event.AutoPlaySettingVisited)
}
Navigation.findNavController(requireView()).navigate(directions)
Navigation.findNavController(requireView()).navigateBlockingForAsyncNavGraph(directions)
}
}

@ -31,6 +31,7 @@ import mozilla.components.support.ktx.kotlin.isExtensionUrl
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.share.listadapters.AppShareOption
@ -121,7 +122,7 @@ class DefaultShareController(
override fun handleAddNewDevice() {
val directions = ShareFragmentDirections.actionShareFragmentToAddNewDeviceFragment()
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
override fun handleShareToDevice(device: Device) {

@ -19,6 +19,7 @@ import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem
/**
@ -63,7 +64,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item
errorItem.navController?.let { navController ->
itemView.sync_tabs_error_cta_button.visibility = VISIBLE
itemView.sync_tabs_error_cta_button.setOnClickListener {
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
navController.navigateBlockingForAsyncNavGraph(NavGraphDirections.actionGlobalTurnOnSync())
}
}
}

@ -21,6 +21,7 @@ import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabstray.ext.getTabSessionState
@ -91,11 +92,13 @@ class DefaultNavigationInteractor(
}
override fun onTabSettingsClicked() {
navController.navigate(TabsTrayFragmentDirections.actionGlobalTabSettingsFragment())
navController.navigateBlockingForAsyncNavGraph(
TabsTrayFragmentDirections.actionGlobalTabSettingsFragment())
}
override fun onOpenRecentlyClosedClicked() {
navController.navigate(TabsTrayFragmentDirections.actionGlobalRecentlyClosed())
navController.navigateBlockingForAsyncNavGraph(
TabsTrayFragmentDirections.actionGlobalRecentlyClosed())
metrics.track(Event.RecentlyClosedTabsOpened)
}
@ -106,7 +109,7 @@ class DefaultNavigationInteractor(
val directions = TabsTrayFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
override fun onShareTabsOfTypeClicked(private: Boolean) {
@ -117,7 +120,7 @@ class DefaultNavigationInteractor(
val directions = TabsTrayFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
@OptIn(ExperimentalCoroutinesApi::class)

@ -14,6 +14,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
interface TabsTrayController {
@ -43,7 +44,8 @@ class DefaultTabsTrayController(
override fun onNewTabTapped(isPrivate: Boolean) {
val startTime = profiler?.getProfilerTime()
browsingModeManager.mode = BrowsingMode.fromBoolean(isPrivate)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
navController.navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
navigationInteractor.onTabTrayDismissed()
profiler?.addMarker(
"DefaultTabTrayController.onNewTabTapped",

@ -33,6 +33,7 @@ import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
@ -241,7 +242,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}
if (!navController.popBackStack(R.id.browserFragment, false)) {
navController.navigate(R.id.browserFragment)
navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}
}
@ -301,7 +302,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
homeViewModel.sessionToDelete = sessionId
val directions = NavGraphDirections.actionGlobalHome()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
dismissAllowingStateLoss()
}
}

@ -26,6 +26,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragment
/**
@ -104,7 +105,8 @@ class DefaultTabTrayController(
override fun handleNewTabTapped(private: Boolean) {
val startTime = profiler?.getProfilerTime()
browsingModeManager.mode = BrowsingMode.fromBoolean(private)
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
navController.navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true))
dismissTabTray()
profiler?.addMarker(
"DefaultTabTrayController.onNewTabTapped",
@ -113,7 +115,8 @@ class DefaultTabTrayController(
}
override fun handleTabSettingsClicked() {
navController.navigate(TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment())
navController.navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment())
}
override fun handleTabTrayDismissed() {
@ -148,7 +151,7 @@ class DefaultTabTrayController(
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
override fun handleShareSelectedTabsClicked(selectedTabs: Set<Tab>) {
@ -158,7 +161,7 @@ class DefaultTabTrayController(
val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment(
data = data.toTypedArray()
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
override fun handleBookmarkSelectedTabs(selectedTabs: Set<Tab>) {
@ -236,13 +239,13 @@ class DefaultTabTrayController(
override fun handleRecentlyClosedClicked() {
val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
metrics.track(Event.RecentlyClosedTabsOpened)
}
override fun handleGoToTabsSettingClicked() {
val directions = TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
metrics.track(Event.TabsTrayCfrTapped)
}
}

@ -57,6 +57,7 @@ import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getDefaultCollectionNumber
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -331,7 +332,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
homeViewModel.sessionToDelete = sessionId
val directions = NavGraphDirections.actionGlobalHome()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
dismissAllowingStateLoss()
}
@ -344,7 +345,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
dismissAllowingStateLoss()
if (findNavController().currentDestination?.id == R.id.browserFragment) return
if (!findNavController().popBackStack(R.id.browserFragment, false)) {
findNavController().navigate(R.id.browserFragment)
findNavController().navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}
}
@ -379,7 +380,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
.setText(requireContext().getString(messageStringRes))
.setAction(requireContext().getString(R.string.create_collection_view)) {
dismissAllowingStateLoss()
findNavController().navigate(
findNavController().navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalHome(
focusOnAddressBar = false,
focusOnCollection = collectionToSelect ?: -1L
@ -403,7 +404,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler
.setText(requireContext().getString(R.string.snackbar_message_bookmarks_saved))
.setAction(requireContext().getString(R.string.snackbar_message_bookmarks_view)) {
dismissAllowingStateLoss()
findNavController().navigate(
findNavController().navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
)
}

@ -17,11 +17,11 @@
android:layout_width="match_parent"
android:layout_height="56dp" />
<!--The navGraph is set dynamically in NavGraphProvider -->
<fragment
android:id="@+id/container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
app:defaultNavHost="true"/>
</LinearLayout>

@ -12,14 +12,20 @@ import io.mockk.verify
import mozilla.components.lib.crash.Crash
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.utils.Settings
class CrashReporterControllerTest {
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
private lateinit var components: Components
private lateinit var crash: Crash
private lateinit var sessionId: String

@ -18,11 +18,16 @@ import io.mockk.mockkStatic
import io.mockk.verify
import io.sentry.Sentry
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.components.isSentryEnabled
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
class NavControllerTest {
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
private val currentDestId = 4
@MockK(relaxUnitFun = true) private lateinit var navController: NavController
@MockK private lateinit var navDirections: NavDirections

@ -0,0 +1,43 @@
/* 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 org.mozilla.fenix.helpers
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.mozilla.fenix.perf.NavGraphProvider
/**
* Disables the call order assertions defined by the [NavGraphProvider] for use in testing.
* This is necessary because unit tests generally don't follow the application lifecycle and thus
* call the methods out of order, causing an assertion to be thrown unexpectedly. You may need to
* apply this rule if you see the following exception in your test:
*
* Unfortunately, JUnit 4 discourages setting test state globally so we apply this to each test that
* has the failure rather than disabling it globally.
*/
class DisableNavGraphProviderAssertionRule : TestWatcher() {
// public for code reuse.
fun setUp() {
mockkObject(NavGraphProvider)
every { NavGraphProvider.blockForNavGraphInflation(any()) } returns Unit
}
// public for code reuse.
fun tearDown() { //
unmockkObject(NavGraphProvider)
}
override fun starting(description: Description?) {
setUp()
}
override fun finished(description: Description?) {
tearDown()
}
}

@ -7,13 +7,19 @@ package org.mozilla.fenix.helpers
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.R
import org.mozilla.fenix.components.TestComponents
import org.robolectric.TestLifecycleApplication
import java.lang.reflect.Method
/**
* An override of our application for use in Robolectric-based unit tests. We're forced to override
* because our standard application fails to initialize in Robolectric with exceptions like:
* "Crash handler service must run in a separate process".
*/
class FenixRobolectricTestApplication : FenixApplication() {
class FenixRobolectricTestApplication : FenixApplication(), TestLifecycleApplication {
// Though JUnit 4 discourages global rules, we can apply global rules in robolectric so we do
// to prevent confusion from devs.
private val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
override fun onCreate() {
super.onCreate()
@ -37,4 +43,19 @@ class FenixRobolectricTestApplication : FenixApplication() {
// https://github.com/mozilla-mobile/fenix/pull/15646#issuecomment-709411141
setTheme(R.style.NormalTheme)
}
// Before test runs before the test class is initialized
override fun beforeTest(method: Method?) {}
// Prepare test runs once the test class and all its member variables (mock and
// non mocks) are initialized. This method runs after application.onCreate
override fun prepareTest(test: Any?) {
// We call this in prepareTest rather than beforeTest because member vars
// are initialized so it feels more correct to call it here.
disableNavGraphProviderAssertionRule.setUp()
}
override fun afterTest(method: Method?) {
disableNavGraphProviderAssertionRule.tearDown()
}
}

@ -39,6 +39,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage
@ -113,6 +114,9 @@ class DefaultSessionControlControllerTest {
private lateinit var store: BrowserStore
private lateinit var controller: DefaultSessionControlController
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setup() {
store = BrowserStore(

@ -30,9 +30,11 @@ import mozilla.components.concept.storage.BookmarkNodeType
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.Services
@ -87,6 +89,9 @@ class BookmarkControllerTest {
BookmarkNodeType.FOLDER, BookmarkRoot.Root.id, null, 0, BookmarkRoot.Root.name, null, null
)
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setup() {
every { homeActivity.components.services } returns services

@ -26,13 +26,16 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.search.SearchDialogFragmentDirections.Companion.actionGlobalAddonsManagementFragment
import org.mozilla.fenix.search.SearchDialogFragmentDirections.Companion.actionGlobalSearchEngineFragment
import org.mozilla.fenix.settings.SupportUtils
@ -55,13 +58,14 @@ class SearchDialogControllerTest {
private lateinit var controller: SearchDialogController
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setUp() {
MockKAnnotations.init(this)
mockkObject(MetricsUtils)
val browserStore = BrowserStore()
every { store.state.tabId } returns "test-tab-id"
every { store.state.searchEngineSource.searchEngine } returns searchEngine
every { sessionManager.select(any()) } just Runs

@ -13,11 +13,16 @@ import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.R
class AccountSettingsInteractorTest {
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Test
fun onSyncNow() {
var ranSyncNow = false

@ -9,8 +9,10 @@ import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.settings.creditcards.controller.DefaultCreditCardsManagementController
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
class DefaultCreditCardsManagementControllerTest {
@ -18,6 +20,9 @@ class DefaultCreditCardsManagementControllerTest {
private lateinit var controller: DefaultCreditCardsManagementController
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setup() {
controller = spyk(

@ -20,10 +20,12 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.tabstray.Tab
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.collections.CollectionsDialog
import org.mozilla.fenix.collections.show
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
@ -42,6 +44,9 @@ class NavigationInteractorTest {
private val context: Context = mockk(relaxed = true)
private val collectionStorage: TabCollectionStorage = mockk(relaxed = true)
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setup() {
store = BrowserStore(initialState = BrowserState(tabs = listOf(testTab)))

@ -31,8 +31,10 @@ import mozilla.components.feature.tabs.TabsUseCases
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
@ -68,6 +70,9 @@ class DefaultTabTrayControllerTest {
private val tab1 = createTab(url = "http://firefox.com", id = "5678")
private val tab2 = createTab(url = "http://mozilla.org", id = "1234")
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setUp() {
mockkStatic("org.mozilla.fenix.ext.SessionManagerKt")

@ -122,6 +122,9 @@ mozilla-detekt-rules:
excludes: "**/*Test.kt, **/*Spec.kt, **/test/**, **/androidTest/**"
MozillaUseLazyMonitored:
active: true
MozillaNavigateCheck:
active: true
excludes: "**/*Test.kt, **/*Spec.kt, **/test/**, **/androidTest/**"
empty-blocks:
active: true

@ -8,6 +8,7 @@ import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider
import org.mozilla.fenix.detektrules.perf.MozillaBannedPropertyAccess
import org.mozilla.fenix.detektrules.perf.MozillaNavigateCheck
import org.mozilla.fenix.detektrules.perf.MozillaStrictModeSuppression
import org.mozilla.fenix.detektrules.perf.MozillaRunBlockingCheck
import org.mozilla.fenix.detektrules.perf.MozillaUseLazyMonitored
@ -22,7 +23,8 @@ class CustomRulesetProvider : RuleSetProvider {
MozillaStrictModeSuppression(config),
MozillaCorrectUnitTestRunner(config),
MozillaRunBlockingCheck(config),
MozillaUseLazyMonitored(config)
MozillaUseLazyMonitored(config),
MozillaNavigateCheck(config)
)
)
}

@ -0,0 +1,50 @@
/* 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 https://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.detektrules.perf
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtCallExpression
private const val VIOLATION_MSG = "If you named a method `navigate`, please rename it to something a bit" +
"more specific such as `navigateTo...`. However, if you are trying to invoke the Android NavController.navigate" +
"please use `org.mozilla.fenix.ext.NavController.navigateBlockingForAsyncNavGraph`" +
"instead. Because using `navigate` directly in the code can lead to the NavGraph not being loaded" +
"since it relies on a blocking call done in `navigateBlockingForAsyncNavGraph`."
/**
* A check to prevent the use of `navController.navigate`. Since the NavGraph is loaded dynamically
* and asynchronously, there is a check in place to make sure the graph is loaded by wrapping the
* `navigate` function. However, using it directly might lead to code that needs the NavGraph being
* called before it being inflated.
*/
class MozillaNavigateCheck(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
"MozillaNavigateCheck",
Severity.Performance,
"Prevents us from calling `navController.navigate` instead of the functions that" +
"wrap it",
Debt.TEN_MINS
)
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
val calledMethod = expression.calleeExpression?.firstChild?.node?.chars
//We check for the navigate method and we have to ignore our extension function file, since
//we call navigate there
if (calledMethod == "navigate" ) {
report(CodeSmell(issue, Entity.from(expression), VIOLATION_MSG))
}
}
}
Loading…
Cancel
Save