tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 3e9c784a79539625b8a75758875b63a13ea4dd0f
parent 177e948254df00f7a6d7608c45abfb82d31c922b
Author: mcarare <48995920+mcarare@users.noreply.github.com>
Date:   Thu,  2 Oct 2025 18:20:11 +0000

Bug 1992046 - Replace MainScope with a custom CoroutineScope in BrowserToolbarIntegration. r=android-reviewers,avirvara

All `flowScoped` calls now create a new `CoroutineScope` using the injected `coroutineDispatcher` and a `SupervisorJob()`. This ensures that each flow collector has its own isolated scope, preventing failures in one from affecting others.

Differential Revision: https://phabricator.services.mozilla.com/D267240

Diffstat:
Mmobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt | 335++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mmobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt | 3+--
2 files changed, 178 insertions(+), 160 deletions(-)

diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegration.kt @@ -19,8 +19,10 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.children +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -79,7 +81,7 @@ class BrowserToolbarIntegration( private val customTabId: String? = null, isOnboardingTab: Boolean = false, renderStyle: ToolbarFeature.RenderStyle = ToolbarFeature.RenderStyle.ColoredUrl, - private val coroutineScope: CoroutineScope = MainScope(), + private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.Main, ) : LifecycleAwareFeature { val backgroundColor = customTabId ?.let { store.state.findCustomTab(it)?.config } @@ -251,16 +253,17 @@ class BrowserToolbarIntegration( } private fun setBrowserActionButtons() { - tabsCounterScope = store.flowScoped(coroutineScope = coroutineScope) { flow -> - flow.distinctUntilChangedBy { state -> state.tabs.size > 1 } - .collect { state -> - if (state.tabs.size > 1) { - toolbar.addBrowserAction(tabsAction) - } else { - toolbar.removeBrowserAction(tabsAction) + tabsCounterScope = + store.flowScoped(coroutineScope = CoroutineScope(coroutineDispatcher + SupervisorJob())) { flow -> + flow.distinctUntilChangedBy { state -> state.tabs.size > 1 } + .collect { state -> + if (state.tabs.size > 1) { + toolbar.addBrowserAction(tabsAction) + } else { + toolbar.removeBrowserAction(tabsAction) + } } - } - } + } } override fun start() { @@ -287,50 +290,55 @@ class BrowserToolbarIntegration( @VisibleForTesting internal fun observeEraseCfr() { - eraseTabsCfrScope = fragment.components?.appStore?.flowScoped(coroutineScope = coroutineScope) { flow -> - flow.mapNotNull { state -> state.showEraseTabsCfr } - .distinctUntilChanged() - .collect { showEraseCfr -> - if (showEraseCfr) { - val eraseActionView = - toolbar.findViewById<LinearLayout>(toolbarR.id.mozac_browser_toolbar_navigation_actions) - .children - .last() - CFRPopup( - anchor = eraseActionView, - properties = CFRPopupProperties( - popupWidth = 256.dp, - popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR, - popupBodyColors = listOf( - ContextCompat.getColor( - fragment.requireContext(), - R.color.cfr_pop_up_shape_end_color, + eraseTabsCfrScope = + fragment.components?.appStore?.flowScoped( + coroutineScope = CoroutineScope( + coroutineDispatcher + SupervisorJob(), + ), + ) { flow -> + flow.mapNotNull { state -> state.showEraseTabsCfr } + .distinctUntilChanged() + .collect { showEraseCfr -> + if (showEraseCfr) { + val eraseActionView = + toolbar.findViewById<LinearLayout>(toolbarR.id.mozac_browser_toolbar_navigation_actions) + .children + .last() + CFRPopup( + anchor = eraseActionView, + properties = CFRPopupProperties( + popupWidth = 256.dp, + popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_end_color, + ), + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_start_color, + ), ), - ContextCompat.getColor( + dismissButtonColor = ContextCompat.getColor( fragment.requireContext(), - R.color.cfr_pop_up_shape_start_color, + cardViewR.color.cardview_light_background, ), + popupVerticalOffset = 0.dp, ), - dismissButtonColor = ContextCompat.getColor( - fragment.requireContext(), - cardViewR.color.cardview_light_background, - ), - popupVerticalOffset = 0.dp, - ), - onDismiss = { onDismissEraseTabsCfr() }, - text = { - Text( - style = focusTypography.cfrTextStyle, - text = fragment.getString(R.string.cfr_for_toolbar_delete_icon2), - color = colorResource(R.color.cfr_text_color), - ) - }, - ).apply { - show() + onDismiss = { onDismissEraseTabsCfr() }, + text = { + Text( + style = focusTypography.cfrTextStyle, + text = fragment.getString(R.string.cfr_for_toolbar_delete_icon2), + color = colorResource(R.color.cfr_text_color), + ) + }, + ).apply { + show() + } } } - } - } + } } private fun onDismissEraseTabsCfr() { @@ -340,114 +348,122 @@ class BrowserToolbarIntegration( @VisibleForTesting internal fun observeCookieBannerCfr() { cookieBannerCfrScope = - fragment.components?.appStore?.flowScoped(coroutineScope = coroutineScope) { flow -> - flow.mapNotNull { state -> state.showCookieBannerCfr } - .distinctUntilChanged() - .collect { showCookieBannerCfr -> - if (showCookieBannerCfr) { - CFRPopup( - anchor = toolbar.findViewById<AppCompatEditText>( - toolbarR.id.mozac_browser_toolbar_background, - ), - properties = CFRPopupProperties( - popupWidth = 256.dp, - popupAlignment = CFRPopup.PopupAlignment.BODY_TO_ANCHOR_START, - popupBodyColors = listOf( - ContextCompat.getColor( - fragment.requireContext(), - R.color.cfr_pop_up_shape_end_color, + fragment.components?.appStore?.flowScoped( + coroutineScope = CoroutineScope( + coroutineDispatcher + SupervisorJob(), + ), + ) { flow -> + flow.mapNotNull { state -> state.showCookieBannerCfr } + .distinctUntilChanged() + .collect { showCookieBannerCfr -> + if (showCookieBannerCfr) { + CFRPopup( + anchor = toolbar.findViewById<AppCompatEditText>( + toolbarR.id.mozac_browser_toolbar_background, + ), + properties = CFRPopupProperties( + popupWidth = 256.dp, + popupAlignment = CFRPopup.PopupAlignment.BODY_TO_ANCHOR_START, + popupBodyColors = listOf( + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_end_color, + ), + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_start_color, + ), ), - ContextCompat.getColor( + dismissButtonColor = ContextCompat.getColor( fragment.requireContext(), - R.color.cfr_pop_up_shape_start_color, + cardViewR.color.cardview_light_background, ), + popupVerticalOffset = 0.dp, + indicatorArrowStartOffset = 10.dp, ), - dismissButtonColor = ContextCompat.getColor( - fragment.requireContext(), - cardViewR.color.cardview_light_background, - ), - popupVerticalOffset = 0.dp, - indicatorArrowStartOffset = 10.dp, - ), - onDismiss = { onDismissCookieBannerCfr() }, - text = { - val textCookieBannerCfr = stringResource( - id = R.string.cfr_cookie_banner, - LocalContext.current.getString(R.string.onboarding_short_app_name), - LocalContext.current.getString(R.string.cfr_cookie_banner_link), - ) - ClickableSubstringLink( - text = textCookieBannerCfr, - style = focusTypography.cfrCookieBannerTextStyle, - linkTextDecoration = TextDecoration.Underline, - clickableStartIndex = textCookieBannerCfr.indexOf( - LocalContext.current.getString( - R.string.cfr_cookie_banner_link, + onDismiss = { onDismissCookieBannerCfr() }, + text = { + val textCookieBannerCfr = stringResource( + id = R.string.cfr_cookie_banner, + LocalContext.current.getString(R.string.onboarding_short_app_name), + LocalContext.current.getString(R.string.cfr_cookie_banner_link), + ) + ClickableSubstringLink( + text = textCookieBannerCfr, + style = focusTypography.cfrCookieBannerTextStyle, + linkTextDecoration = TextDecoration.Underline, + clickableStartIndex = textCookieBannerCfr.indexOf( + LocalContext.current.getString( + R.string.cfr_cookie_banner_link, + ), ), - ), - clickableEndIndex = textCookieBannerCfr.length, - onClick = { - fragment.requireComponents.appStore.dispatch( - AppAction.OpenSettings(Screen.Settings.Page.CookieBanner), - ) - onDismissCookieBannerCfr() - }, - ) - }, - ).apply { - show() - stopObserverCookieBannerCfrChanges() + clickableEndIndex = textCookieBannerCfr.length, + onClick = { + fragment.requireComponents.appStore.dispatch( + AppAction.OpenSettings(Screen.Settings.Page.CookieBanner), + ) + onDismissCookieBannerCfr() + }, + ) + }, + ).apply { + show() + stopObserverCookieBannerCfrChanges() + } } } - } - } + } } @VisibleForTesting internal fun observeTrackingProtectionCfr() { trackingProtectionCfrScope = - fragment.components?.appStore?.flowScoped(coroutineScope = coroutineScope) { flow -> - flow.mapNotNull { state -> state.showTrackingProtectionCfrForTab } - .distinctUntilChanged() - .collect { showTrackingProtectionCfrForTab -> - if (showTrackingProtectionCfrForTab[store.state.selectedTabId] == true) { - CFRPopup( - anchor = toolbar.findViewById( - toolbarR.id.mozac_browser_toolbar_tracking_protection_indicator, - ), - properties = CFRPopupProperties( - popupWidth = 256.dp, - popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR, - popupBodyColors = listOf( - ContextCompat.getColor( - fragment.requireContext(), - R.color.cfr_pop_up_shape_end_color, + fragment.components?.appStore?.flowScoped( + coroutineScope = CoroutineScope( + coroutineDispatcher + SupervisorJob(), + ), + ) { flow -> + flow.mapNotNull { state -> state.showTrackingProtectionCfrForTab } + .distinctUntilChanged() + .collect { showTrackingProtectionCfrForTab -> + if (showTrackingProtectionCfrForTab[store.state.selectedTabId] == true) { + CFRPopup( + anchor = toolbar.findViewById( + toolbarR.id.mozac_browser_toolbar_tracking_protection_indicator, + ), + properties = CFRPopupProperties( + popupWidth = 256.dp, + popupAlignment = CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR, + popupBodyColors = listOf( + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_end_color, + ), + ContextCompat.getColor( + fragment.requireContext(), + R.color.cfr_pop_up_shape_start_color, + ), ), - ContextCompat.getColor( + dismissButtonColor = ContextCompat.getColor( fragment.requireContext(), - R.color.cfr_pop_up_shape_start_color, + cardViewR.color.cardview_light_background, ), + popupVerticalOffset = 0.dp, ), - dismissButtonColor = ContextCompat.getColor( - fragment.requireContext(), - cardViewR.color.cardview_light_background, - ), - popupVerticalOffset = 0.dp, - ), - onDismiss = { onDismissTrackingProtectionCfr() }, - text = { - Text( - style = focusTypography.cfrTextStyle, - text = fragment.getString(R.string.cfr_for_toolbar_shield_icon2), - color = colorResource(R.color.cfr_text_color), - ) - }, - ).apply { - show() + onDismiss = { onDismissTrackingProtectionCfr() }, + text = { + Text( + style = focusTypography.cfrTextStyle, + text = fragment.getString(R.string.cfr_for_toolbar_shield_icon2), + color = colorResource(R.color.cfr_text_color), + ) + }, + ).apply { + show() + } } } - } - } + } } private fun onDismissCookieBannerCfr() { @@ -476,27 +492,30 @@ class BrowserToolbarIntegration( @VisibleForTesting internal fun observerSecurityIndicatorChanges() { - securityIndicatorScope = store.flowScoped(coroutineScope = coroutineScope) { flow -> - flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabId) } - .distinctUntilChangedBy { tab -> tab.content.securityInfo } - .collect { - val secure = it.content.securityInfo.secure - val url = it.content.url.trim() - when { - secure && Indicators.SECURITY in toolbar.display.indicators -> { - addTrackingProtectionIndicator() - } - - secure || Indicators.SECURITY in toolbar.display.indicators || url.startsWith("about:") -> { - // do nothing - } - - else -> { - addSecurityIndicator() + securityIndicatorScope = + store.flowScoped(coroutineScope = CoroutineScope(coroutineDispatcher + SupervisorJob())) { flow -> + flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabId) } + .distinctUntilChangedBy { tab -> tab.content.securityInfo } + .collect { + val secure = it.content.securityInfo.secure + val url = it.content.url.trim() + when { + secure && Indicators.SECURITY in toolbar.display.indicators -> { + addTrackingProtectionIndicator() + } + + secure || Indicators.SECURITY in toolbar.display.indicators || url.startsWith( + "about:", + ) -> { + // do nothing + } + + else -> { + addSecurityIndicator() + } } } - } - } + } } override fun stop() { diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt @@ -6,7 +6,6 @@ package org.mozilla.focus.browser.integration import android.view.View import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.state.BrowserState @@ -82,7 +81,7 @@ class BrowserToolbarIntegrationTest { onUrlLongClicked = { false }, eraseActionListener = {}, tabCounterListener = {}, - coroutineScope = TestScope(testDispatcher), + coroutineDispatcher = testDispatcher, ), ) }