Bug 1959107 - Fix intermittent coroutines memory leak related to top sites on home r=android-reviewers,pollymce

# Conflicts:
#	mobile/android/fenix/app/src/main/java/org/mozilla/fenix/FenixApplication.kt

Differential Revision: https://phabricator.services.mozilla.com/D246359
This commit is contained in:
Segun Famisa
2025-04-28 07:48:34 +00:00
parent df918085d3
commit 99d5d3c43f
25 changed files with 435 additions and 159 deletions

View File

@@ -17,4 +17,9 @@ interface TopSitesProvider {
* @return a list of top sites from the provider.
*/
suspend fun getTopSites(allowCache: Boolean = true): List<TopSite>
/**
* Refreshes the cache with the latest top sites response if the cache is expired.
*/
suspend fun refreshTopSitesIfCacheExpired()
}

View File

@@ -97,7 +97,7 @@ class MarsTopSitesProvider(
* Refreshes the cache with the latest tiles response from [endPointURL] if the cache
* is expired.
*/
suspend fun refreshTopSitesIfCacheExpired() {
override suspend fun refreshTopSitesIfCacheExpired() {
if (!isCacheExpired()) return
getTopSites(allowCache = false)

View File

@@ -92,7 +92,7 @@ class ContileTopSitesProvider(
* Refreshes the cache with the latest top sites response from [endPointURL]
* if the cache is expired.
*/
suspend fun refreshTopSitesIfCacheExpired() {
override suspend fun refreshTopSitesIfCacheExpired() {
if (!isCacheExpired(shouldUseServerMaxAge = false)) return
getTopSites(allowCache = false)

View File

@@ -12,7 +12,6 @@ import androidx.test.espresso.intent.rule.IntentsRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.AppAndSystemHelper.deleteDownloadedFileOnStorage
@@ -293,7 +292,6 @@ class DownloadTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/1632384
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun warningWhenClosingPrivateTabsWhileDownloadingTest() {
homeScreen {
}.togglePrivateBrowsingMode()

View File

@@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.TestAsset
import org.mozilla.fenix.helpers.TestAssetHelper.getGPCTestAsset
@@ -58,7 +57,6 @@ class GlobalPrivacyControlTest : TestSetup() {
}
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2429364
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
@Test
fun testGPCinPrivateBrowsing() {
homeScreen { }.togglePrivateBrowsingMode()

View File

@@ -14,7 +14,6 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.IntentReceiverActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AppAndSystemHelper.assertExternalAppOpens
@@ -122,7 +121,6 @@ class MainMenuTestCompose : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2860843
@SmokeTest
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun verifyTheNewPrivateTabButtonTest() {
val testPage = getGenericAsset(mockWebServer, 1)

View File

@@ -158,7 +158,6 @@ class NavigationToolbarTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2256552
@SmokeTest
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun goToHomeScreenInPrivateModeTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)

View File

@@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import androidx.core.net.toUri
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper.assertYoutubeAppOpens
@@ -93,7 +92,6 @@ class SettingsAdvancedTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2121052
// Assumes Youtube is installed and enabled
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun privateBrowsingAskBeforeOpeningOpenLinkInAppTest() {
val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
@@ -183,7 +181,6 @@ class SettingsAdvancedTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2121051
// Assumes Youtube is installed and enabled
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun privateBrowsingAskBeforeOpeningLinkInAppCancelTest() {
TestHelper.appContext.settings().shouldShowCookieBannersCFR = false
val externalLinksPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)

View File

@@ -10,7 +10,6 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
@@ -143,7 +142,6 @@ class SettingsGeneralTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/516078
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun verifyFollowDeviceLanguageTest() {
val frenchLocale = LocaleListCompat.forLanguageTags("fr")

View File

@@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AppAndSystemHelper.openAppFromExternalLink
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
@@ -178,7 +177,6 @@ class SettingsHomepageTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/1569843
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun verifyOpeningScreenAfterLaunchingExternalLinkTest() {
val genericPage = getGenericAsset(mockWebServer, 1)

View File

@@ -508,7 +508,6 @@ class SettingsSearchTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/464420
// Tests the "Don't allow" option from private mode search suggestions onboarding dialog
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun doNotAllowSearchSuggestionsInPrivateBrowsingTest() {
homeScreen {
togglePrivateBrowsingModeOnOff()
@@ -523,7 +522,6 @@ class SettingsSearchTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/1957063
// Tests the "Allow" option from private mode search suggestions onboarding dialog
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun allowSearchSuggestionsInPrivateBrowsingTest() {
homeScreen {
togglePrivateBrowsingModeOnOff()

View File

@@ -6,7 +6,6 @@ package org.mozilla.fenix.ui
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.Constants.defaultTopSitesList
import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredShortcutTitle
@@ -62,7 +61,6 @@ class SponsoredShortcutsTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/1729334
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun openSponsoredShortcutInPrivateTabTest() {
homeScreen {
sponsoredShortcutTitle = getSponsoredShortcutTitle(2)

View File

@@ -141,7 +141,6 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/903591
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun closingPrivateTabsTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@@ -182,7 +181,6 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/903592
@SmokeTest
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun verifyCloseAllPrivateTabsNotificationTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@@ -336,7 +334,12 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2343663
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
@SkipLeaks(
reasons = [
"https://bugzilla.mozilla.org/show_bug.cgi?id=1962065",
"https://bugzilla.mozilla.org/show_bug.cgi?id=1962070",
],
)
fun tabsCounterShortcutMenuNewPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@@ -353,7 +356,7 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2343662
@Test
@SkipLeaks
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1962065"])
fun tabsCounterShortcutMenuNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@@ -370,7 +373,7 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/927315
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1962065"])
fun privateTabsCounterShortcutMenuCloseTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@@ -402,7 +405,7 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2344199
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1962065"])
fun privateTabsCounterShortcutMenuNewPrivateTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@@ -422,7 +425,7 @@ class TabbedBrowsingTest : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2344198
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1962065"])
fun privateTabsCounterShortcutMenuNewTabTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

View File

@@ -8,7 +8,6 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SkipLeaks
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.DataGenerationHelper.generateRandomString
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@@ -188,7 +187,6 @@ class TopSitesTestCompose : TestSetup() {
// TestRail link: https://mozilla.testrail.io/index.php?/cases/view/2323641
@Test
@SkipLeaks(reasons = ["https://bugzilla.mozilla.org/show_bug.cgi?id=1959107"])
fun removeTopSiteFromMainMenuTest() {
val defaultWebPage = getGenericAsset(mockWebServer, 1)

View File

@@ -96,6 +96,8 @@ import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.ext.isCustomEngine
import org.mozilla.fenix.ext.isKnownSearchDomain
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.TOP_SITES_PROVIDER_LIMIT
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.lifecycle.StoreLifecycleObserver
import org.mozilla.fenix.lifecycle.VisibilityLifecycleObserver
import org.mozilla.fenix.nimbus.FxNimbus
@@ -111,8 +113,6 @@ import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_LIMIT
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.isLargeScreenSize
import org.mozilla.fenix.wallpapers.Wallpaper
import java.util.UUID

View File

@@ -69,7 +69,6 @@ import mozilla.components.service.pocket.PocketStoriesService
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.UserInteractionOnBackPressedCallback
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import mozilla.components.support.ktx.android.content.call
import mozilla.components.support.ktx.android.content.email
@@ -123,6 +122,7 @@ import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.extension.WebExtensionPromptFeature
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.TopSitesRefresher
import org.mozilla.fenix.home.intent.AssistIntentProcessor
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor
@@ -495,6 +495,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
extensionsProcessDisabledBackgroundController,
serviceWorkerSupport,
crashReporterBinding,
TopSitesRefresher(
settings = settings(),
topSitesProvider = if (settings().marsAPIEnabled) {
components.core.marsTopSitesProvider
} else {
components.core.contileTopSitesProvider
},
),
)
if (!isCustomTabIntent(intent)) {
@@ -644,7 +652,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
@CallSuper
@Suppress("TooGenericExceptionCaught")
override fun onResume() {
super.onResume()
@@ -655,18 +662,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
)
lifecycleScope.launch(IO) {
try {
if (settings().showContileFeature) {
if (settings().marsAPIEnabled) {
components.core.marsTopSitesProvider.refreshTopSitesIfCacheExpired()
} else {
components.core.contileTopSitesProvider.refreshTopSitesIfCacheExpired()
}
}
} catch (e: Exception) {
Logger.error("Failed to refresh contile top sites", e)
}
if (settings().checkIfFenixIsDefaultBrowserOnAppResume()) {
if (components.appStore.state.wasNativeDefaultBrowserPromptShown) {
Metrics.defaultBrowserChangedViaNativeSystemPrompt.record(NoExtras())

View File

@@ -29,7 +29,6 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat.getColor
import androidx.core.graphics.drawable.toDrawable
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -65,17 +64,13 @@ import mozilla.components.compose.base.Divider
import mozilla.components.compose.cfr.CFRPopup
import mozilla.components.compose.cfr.CFRPopupLayout
import mozilla.components.compose.cfr.CFRPopupProperties
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.feature.accounts.push.SendTabUseCases
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
import mozilla.components.feature.top.sites.TopSitesProviderConfig
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.observeAsState
@@ -113,7 +108,6 @@ import org.mozilla.fenix.compose.snackbar.Snackbar
import org.mozilla.fenix.compose.snackbar.SnackbarState
import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.isToolbarAtBottom
import org.mozilla.fenix.ext.nav
@@ -147,6 +141,10 @@ import org.mozilla.fenix.home.toolbar.HomeToolbarView
import org.mozilla.fenix.home.toolbar.SearchSelectorBinding
import org.mozilla.fenix.home.toolbar.SearchSelectorMenuBinding
import org.mozilla.fenix.home.topsites.DefaultTopSitesView
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.AMAZON_SEARCH_ENGINE_NAME
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.AMAZON_SPONSORED_TITLE
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.EBAY_SPONSORED_TITLE
import org.mozilla.fenix.home.topsites.getTopSitesConfig
import org.mozilla.fenix.home.ui.Homepage
import org.mozilla.fenix.messaging.DefaultMessageController
import org.mozilla.fenix.messaging.FenixMessageSurfaceId
@@ -166,8 +164,6 @@ import org.mozilla.fenix.snackbar.SnackbarBinding
import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_LIMIT
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wallpapers.Wallpaper
import java.lang.ref.WeakReference
@@ -384,7 +380,10 @@ class HomeFragment : Fragment() {
settings = components.settings,
),
storage = components.core.topSitesStorage,
config = ::getTopSitesConfig,
config = getTopSitesConfig(
settings = requireContext().settings(),
store = store,
),
),
owner = viewLifecycleOwner,
view = binding.root,
@@ -1063,37 +1062,6 @@ class HomeFragment : Fragment() {
private fun shouldShowMicrosurveyPrompt(context: Context) =
context.components.settings.shouldShowMicrosurveyPrompt
/**
* Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
* not frequently visited sites should be displayed.
*/
@VisibleForTesting
internal fun getTopSitesConfig(): TopSitesConfig {
val settings = requireContext().settings()
return TopSitesConfig(
totalSites = settings.topSitesMaxLimit,
frecencyConfig = if (FxNimbus.features.homepageHideFrecentTopSites.value().enabled) {
null
} else {
TopSitesFrecencyConfig(
frecencyTresholdOption = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES,
) { !it.url.toUri().containsQueryParameters(settings.frecencyFilterQuery) }
},
providerConfig = TopSitesProviderConfig(
showProviderTopSites = settings.showContileFeature,
limit = TOP_SITES_PROVIDER_LIMIT,
maxThreshold = TOP_SITES_PROVIDER_MAX_THRESHOLD,
providerFilter = { topSite ->
when (store.state.search.selectedOrDefaultSearchEngine?.name) {
AMAZON_SEARCH_ENGINE_NAME -> topSite.title != AMAZON_SPONSORED_TITLE
EBAY_SPONSORED_TITLE -> topSite.title != EBAY_SPONSORED_TITLE
else -> true
}
},
),
)
}
@VisibleForTesting
internal fun showUndoSnackbarForTopSite(topSite: TopSite) {
lifecycleScope.allowUndo(
@@ -1658,11 +1626,6 @@ class HomeFragment : Fragment() {
// Delay for scrolling to the collection header
private const val ANIM_SCROLL_DELAY = 100L
// Sponsored top sites titles and search engine names used for filtering
const val AMAZON_SPONSORED_TITLE = "Amazon"
const val AMAZON_SEARCH_ENGINE_NAME = "Amazon.com"
const val EBAY_SPONSORED_TITLE = "eBay"
// Elevation for undo toasts
internal const val TOAST_ELEVATION = 80f

View File

@@ -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 http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.feature.top.sites.TopSitesProvider
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.utils.Settings
/**
* Class to refresh the top sites using the [TopSitesProvider]
*
* @param settings Fenix [Settings]
* @param topSitesProvider [TopSitesProvider] to refresh top sites
* @param dispatcher [CoroutineDispatcher] to use launch the refresh job.
* Default value is [Dispatchers.IO]. It is helpful to improve testability
*/
class TopSitesRefresher(
private val settings: Settings,
private val topSitesProvider: TopSitesProvider,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : DefaultLifecycleObserver {
private val logger = Logger("TopSitesRefresher")
private val scope = CoroutineScope(dispatcher)
override fun onResume(owner: LifecycleOwner) {
scope.launch(dispatcher) {
runCatching {
if (settings.showContileFeature) {
topSitesProvider.refreshTopSitesIfCacheExpired()
}
}.onFailure { exception ->
logger.error("Failed to refresh contile top sites", exception)
}
}
}
override fun onPause(owner: LifecycleOwner) {
scope.cancel()
}
}

View File

@@ -0,0 +1,42 @@
/* 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.home.topsites
/**
* Constants used for the [mozilla.components.feature.top.sites.TopSitesConfig]
*/
internal object TopSitesConfigConstants {
/**
* Only fetch top sites from the [mozilla.components.feature.top.sites.TopSitesProvider]
* when the number of default and pinned sites are below this maximum threshold.
*/
internal const val TOP_SITES_PROVIDER_MAX_THRESHOLD = 8
/**
* Number of top sites to take from the [mozilla.components.feature.top.sites.TopSitesProvider].
*/
internal const val TOP_SITES_PROVIDER_LIMIT = 2
/**
* The maximum number of top sites to display.
*/
internal const val TOP_SITES_MAX_COUNT = 16
/**
* Sponsored top sites titles for Amazon used for filtering
*/
const val AMAZON_SPONSORED_TITLE = "Amazon"
/**
* Sponsored top sites search engine for Amazon used for filtering
*/
const val AMAZON_SEARCH_ENGINE_NAME = "Amazon.com"
/**
* Sponsored top sites titles for eBay used for filtering
*/
const val EBAY_SPONSORED_TITLE = "eBay"
}

View File

@@ -0,0 +1,60 @@
/* 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.home.topsites
import androidx.core.net.toUri
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.feature.top.sites.TopSitesConfig
import mozilla.components.feature.top.sites.TopSitesFeature
import mozilla.components.feature.top.sites.TopSitesFrecencyConfig
import mozilla.components.feature.top.sites.TopSitesProviderConfig
import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.AMAZON_SEARCH_ENGINE_NAME
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.AMAZON_SPONSORED_TITLE
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.EBAY_SPONSORED_TITLE
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.TOP_SITES_PROVIDER_LIMIT
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
/**
* Top level function that creates [TopSitesConfig] for Fenix based on information from the [BrowserStore]
* and [Settings].
*
* This is meant to be used with the [TopSitesFeature] and it exists instead of the lambda which
* holds an implicit reference to the [HomeFragment].
*/
internal fun getTopSitesConfig(
settings: Settings,
store: BrowserStore,
): () -> TopSitesConfig {
return {
TopSitesConfig(
totalSites = settings.topSitesMaxLimit,
frecencyConfig = if (FxNimbus.features.homepageHideFrecentTopSites.value().enabled) {
null
} else {
TopSitesFrecencyConfig(
frecencyTresholdOption = FrecencyThresholdOption.SKIP_ONE_TIME_PAGES,
) { !it.url.toUri().containsQueryParameters(settings.frecencyFilterQuery) }
},
providerConfig = TopSitesProviderConfig(
showProviderTopSites = settings.showContileFeature,
limit = TOP_SITES_PROVIDER_LIMIT,
maxThreshold = TOP_SITES_PROVIDER_MAX_THRESHOLD,
providerFilter = { topSite ->
when (store.state.search.selectedOrDefaultSearchEngine?.name) {
AMAZON_SEARCH_ENGINE_NAME -> topSite.title != AMAZON_SPONSORED_TITLE
EBAY_SPONSORED_TITLE -> topSite.title != EBAY_SPONSORED_TITLE
else -> true
}
},
),
)
}
}

View File

@@ -21,7 +21,6 @@ import mozilla.components.concept.engine.EngineSession.CookieBannerHandlingMode
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
import mozilla.components.feature.top.sites.TopSitesProvider
import mozilla.components.support.ktx.android.content.PreferencesHolder
import mozilla.components.support.ktx.android.content.booleanPreference
import mozilla.components.support.ktx.android.content.floatPreference
@@ -45,6 +44,7 @@ import org.mozilla.fenix.components.toolbar.navbar.shouldAddNavigationBar
import org.mozilla.fenix.debugsettings.addresses.SharedPrefsAddressesDebugLocalesRepository
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.TOP_SITES_MAX_COUNT
import org.mozilla.fenix.nimbus.CookieBannersSection
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.HomeScreenSection
@@ -98,20 +98,6 @@ class Settings(private val appContext: Context) : PreferencesHolder {
@VisibleForTesting
internal var SEARCH_GROUP_MINIMUM_SITES: Int = 2
// The maximum number of top sites to display.
const val TOP_SITES_MAX_COUNT = 16
/**
* Only fetch top sites from the [TopSitesProvider] when the number of default and
* pinned sites are below this maximum threshold.
*/
const val TOP_SITES_PROVIDER_MAX_THRESHOLD = 8
/**
* Number of top sites to take from the [TopSitesProvider].
*/
const val TOP_SITES_PROVIDER_LIMIT = 2
/**
* Minimum number of days between Set as default Browser prompt displays in home page.
*/

View File

@@ -0,0 +1,65 @@
/* 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.lifecycle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Lifecycle.State
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
/**
* [LifecycleOwner] to be used for testing
*
* @param initialState Initial state. Defaults to [State.CREATED]
*/
class TestLifecycleOwner(initialState: State = State.CREATED) : LifecycleOwner {
private val registry = LifecycleRegistry.createUnsafe(this)
init {
registry.currentState = initialState
}
override val lifecycle: Lifecycle
get() = registry
/**
* Registers a [LifecycleObserver] for this [LifecycleOwner]
*/
fun registerObserver(observer: LifecycleObserver) {
registry.addObserver(observer)
}
/**
* Simulates the `onCreate()` event
*/
fun onCreate() = registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
/**
* Simulates the on `onStart()` event
*/
fun onStart() = registry.handleLifecycleEvent(Lifecycle.Event.ON_START)
/**
* Simulates the on `onResume()` event
*/
fun onResume() = registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
/**
* Simulates the on `onPause()` event
*/
fun onPause() = registry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
/**
* Simulates the on `onStop()` event
*/
fun onStop() = registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
/**
* Simulates the on `onDestroy()` event
*/
fun onDestroy() = registry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}

View File

@@ -8,12 +8,6 @@ import android.content.Context
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SearchState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.top.sites.TopSite
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
@@ -25,8 +19,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.Core
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragment.Companion.AMAZON_SPONSORED_TITLE
import org.mozilla.fenix.home.HomeFragment.Companion.EBAY_SPONSORED_TITLE
import org.mozilla.fenix.utils.Settings
class HomeFragmentTest {
@@ -57,52 +49,6 @@ class HomeFragmentTest {
}
@Test
fun `WHEN getTopSitesConfig is called THEN it returns TopSitesConfig with non-null frecencyConfig`() {
every { settings.topSitesMaxLimit } returns 10
val topSitesConfig = homeFragment.getTopSitesConfig()
assertNotNull(topSitesConfig.frecencyConfig)
}
@Test
fun `GIVEN a topSitesMaxLimit WHEN getTopSitesConfig is called THEN it returns TopSitesConfig with totalSites = topSitesMaxLimit`() {
val topSitesMaxLimit = 10
every { settings.topSitesMaxLimit } returns topSitesMaxLimit
val topSitesConfig = homeFragment.getTopSitesConfig()
assertEquals(topSitesMaxLimit, topSitesConfig.totalSites)
}
@Test
fun `GIVEN the selected search engine is set to eBay WHEN getTopSitesConfig is called THEN providerFilter filters the eBay provided top sites`() {
val searchEngine: SearchEngine = mockk()
val browserStore = BrowserStore(
initialState = BrowserState(
search = SearchState(
regionSearchEngines = listOf(searchEngine),
),
),
)
every { core.store } returns browserStore
every { searchEngine.name } returns EBAY_SPONSORED_TITLE
val eBayTopSite = TopSite.Provided(1L, EBAY_SPONSORED_TITLE, "eBay.com", "", "", "", 0L)
val amazonTopSite = TopSite.Provided(2L, AMAZON_SPONSORED_TITLE, "Amazon.com", "", "", "", 0L)
val firefoxTopSite = TopSite.Provided(3L, "Firefox", "mozilla.org", "", "", "", 0L)
val providedTopSites = listOf(eBayTopSite, amazonTopSite, firefoxTopSite)
val topSitesConfig = homeFragment.getTopSitesConfig()
val filteredProvidedSites = providedTopSites.filter {
topSitesConfig.providerConfig?.providerFilter?.invoke(it) ?: true
}
assertTrue(filteredProvidedSites.containsAll(listOf(amazonTopSite, firefoxTopSite)))
assertFalse(filteredProvidedSites.contains(eBayTopSite))
}
fun `GIVEN the user is in normal mode WHEN checking if should enable wallpaper THEN return true`() {
val activity: HomeActivity = mockk {
every { themeManager.currentTheme.isPrivate } returns false

View File

@@ -0,0 +1,76 @@
/* 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.home
import io.mockk.every
import io.mockk.mockk
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSitesProvider
import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.test.rule.runTestOnMain
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.lifecycle.TestLifecycleOwner
import org.mozilla.fenix.utils.Settings
/**
* Class to test the [TopSitesRefresher]
*/
class TopSitesRefresherTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val lifecycleOwner = TestLifecycleOwner()
private val topSitesProvider = FakeTopSitesProvider()
private val settings: Settings = mockk(relaxed = true)
@Before
fun setUp() {
lifecycleOwner.registerObserver(
observer = TopSitesRefresher(
settings = settings,
topSitesProvider = topSitesProvider,
dispatcher = coroutinesTestRule.testDispatcher,
),
)
}
@Test
fun `WHEN lifecycle resumes AND we want to show contile feature THEN top sites are refreshed`() =
runTestOnMain {
every { settings.showContileFeature } returns true
lifecycleOwner.onResume()
assertTrue(topSitesProvider.cacheRefreshed)
}
@Test
fun `WHEN lifecycle resumes AND we DO NOT want to show contile feature THEN top sites are NOT refreshed`() =
runTestOnMain {
every { settings.showContileFeature } returns false
lifecycleOwner.onResume()
assertFalse(topSitesProvider.cacheRefreshed)
}
private class FakeTopSitesProvider : TopSitesProvider {
var expectedTopSites: List<TopSite> = emptyList()
var cacheRefreshed: Boolean = false
override suspend fun getTopSites(allowCache: Boolean): List<TopSite> = expectedTopSites
override suspend fun refreshTopSitesIfCacheExpired() {
cacheRefreshed = true
}
}
}

View File

@@ -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 org.mozilla.fenix.home.topsites
import io.mockk.every
import io.mockk.mockk
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SearchState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSitesConfig
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.AMAZON_SPONSORED_TITLE
import org.mozilla.fenix.home.topsites.TopSitesConfigConstants.EBAY_SPONSORED_TITLE
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.HomepageHideFrecentTopSites
import org.mozilla.fenix.utils.Settings
/**
* Tests the top-level function [getTopSitesConfig] we use to create [TopSitesConfig]
*/
class TopSitesConfigCreatorTest {
private lateinit var settings: Settings
private val browserStore = BrowserStore(
initialState = BrowserState(search = SearchState(regionSearchEngines = listOf())),
)
@Before
fun setUp() {
settings = mockk(relaxed = true)
every { settings.topSitesMaxLimit } returns 10
}
@Test
fun `WHEN hide top sites flag is not enabled THEN it returns TopSitesConfig with non-null frequencyConfig`() {
FxNimbus.features.homepageHideFrecentTopSites.withCachedValue(
HomepageHideFrecentTopSites(
enabled = false,
),
)
val topSitesConfig = getTopSitesConfig(settings = settings, store = browserStore).invoke()
assertNotNull(topSitesConfig.frecencyConfig)
}
@Test
fun `WHEN hide top sites flag is enabled THEN it returns TopSitesConfig with null frequencyConfig`() {
FxNimbus.features.homepageHideFrecentTopSites.withCachedValue(
HomepageHideFrecentTopSites(enabled = true),
)
val topSitesConfig = getTopSitesConfig(settings = settings, store = browserStore).invoke()
assertNull(topSitesConfig.frecencyConfig)
}
@Test
fun `GIVEN a topSitesMaxLimit THEN it returns TopSitesConfig with totalSites = topSitesMaxLimit`() {
val topSitesMaxLimit = 15
every { settings.topSitesMaxLimit } returns topSitesMaxLimit
val topSitesConfig = getTopSitesConfig(settings = settings, store = browserStore).invoke()
assertEquals(topSitesMaxLimit, topSitesConfig.totalSites)
}
@Test
fun `GIVEN the selected search engine is set to eBay THEN providerFilter filters the eBay provided top sites`() {
val searchEngine: SearchEngine = mockk()
val browserStore = BrowserStore(
initialState = BrowserState(
search = SearchState(
regionSearchEngines = listOf(searchEngine),
),
),
)
every { searchEngine.name } returns EBAY_SPONSORED_TITLE
val eBayTopSite = TopSite.Provided(1L, EBAY_SPONSORED_TITLE, "eBay.com", "", "", "", 0L)
val amazonTopSite =
TopSite.Provided(2L, AMAZON_SPONSORED_TITLE, "Amazon.com", "", "", "", 0L)
val firefoxTopSite = TopSite.Provided(3L, "Firefox", "mozilla.org", "", "", "", 0L)
val providedTopSites = listOf(eBayTopSite, amazonTopSite, firefoxTopSite)
val topSitesConfig = getTopSitesConfig(settings = settings, store = browserStore).invoke()
val filteredProvidedSites = providedTopSites.filter {
topSitesConfig.providerConfig?.providerFilter?.invoke(it) != false
}
assertTrue(filteredProvidedSites.containsAll(listOf(amazonTopSite, firefoxTopSite)))
assertFalse(filteredProvidedSites.contains(eBayTopSite))
}
}