tor-browser

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

commit 3515b1681d19e1903ffefdf6ba05ef52d84d9058
parent f5d96ad1050da7cb8f96d443fbc20c7bdf5b7d9e
Author: fmasalha <fmasalha@mozilla.com>
Date:   Thu, 30 Oct 2025 16:47:14 +0000

Bug 1991714 - Added a flag check for allowScreenCaptureInSecureScreens around secure screens and dialogs r=android-reviewers,matt-tighe

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

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt | 2+-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt | 3+--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/autofill/AutofillTools.kt | 25+++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt | 10+++++++---
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/RequireAuthorization.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SecureTabsTrayBinding.kt | 5+++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBinding.kt | 5+++--
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt | 7+++++++
Mmobile/android/fenix/app/src/main/res/values/preference_keys.xml | 1+
Mmobile/android/fenix/app/src/main/res/values/static_strings.xml | 3+++
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/FragmentTest.kt | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/SecureTabsTrayBindingTest.kt | 22+++++++++++++++++++++-
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBindingTest.kt | 20+++++++++++++++++++-
13 files changed, 147 insertions(+), 13 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -1309,7 +1309,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } private fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) { - if (mode == BrowsingMode.Private && !settings().allowScreenshotsInPrivateMode) { + if (mode == BrowsingMode.Private && !settings().shouldSecureModeBeOverridden) { window.addFlags(FLAG_SECURE) } else { window.clearFlags(FLAG_SECURE) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -702,13 +702,12 @@ abstract class BaseBrowserFragment : view = binding.root, ) - val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode secureWindowFeature.set( feature = SecureWindowFeature( window = requireActivity().window, store = store, customTabId = customTabSessionId, - isSecure = { !allowScreenshotsInPrivateMode && it.content.private }, + isSecure = { !context.settings().shouldSecureModeBeOverridden && it.content.private }, clearFlagOnStop = false, ), owner = this, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/autofill/AutofillTools.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/debugsettings/autofill/AutofillTools.kt @@ -9,21 +9,45 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview import org.mozilla.fenix.R +import org.mozilla.fenix.components.components +import org.mozilla.fenix.compose.list.SwitchListItem import org.mozilla.fenix.compose.list.TextListItem import org.mozilla.fenix.debugsettings.store.DebugDrawerAction import org.mozilla.fenix.debugsettings.store.DebugDrawerStore import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.utils.Settings @Composable internal fun AutofillTools( debugDrawerStore: DebugDrawerStore, modifier: Modifier = Modifier, + settings: Settings = components.settings, ) { + var isChecked by remember { mutableStateOf(settings.allowScreenCaptureInSecureScreens) } + Column(modifier = modifier) { + SwitchListItem( + label = stringResource(R.string.autofill_debug_enable_screen_capture), + checked = isChecked, + showSwitchAfter = true, + onClick = { + val newCheckedState = !isChecked + isChecked = newCheckedState + settings.allowScreenCaptureInSecureScreens = newCheckedState + }, + ) + + HorizontalDivider() + TextListItem( label = stringResource(id = R.string.debug_drawer_logins_title), onClick = { @@ -59,6 +83,7 @@ internal fun AutofillToolsPreview() { FirefoxTheme { AutofillTools( debugDrawerStore = DebugDrawerStore(), + settings = Settings(LocalContext.current.applicationContext), modifier = Modifier.background(MaterialTheme.colorScheme.surface), ) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt @@ -183,11 +183,15 @@ fun Fragment.breadcrumb( /** * Sets the [WindowManager.LayoutParams.FLAG_SECURE] flag for the current activity window. + * + * When user preference allowScreenCaptureInSecureScreens is true, this function is a no-op */ fun Fragment.secure() { - this.activity?.window?.addFlags( - WindowManager.LayoutParams.FLAG_SECURE, - ) + if (context?.settings()?.allowScreenCaptureInSecureScreens != true) { + this.activity?.window?.addFlags( + WindowManager.LayoutParams.FLAG_SECURE, + ) + } } /** diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/RequireAuthorization.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/logins/ui/RequireAuthorization.kt @@ -17,6 +17,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner import mozilla.components.lib.state.ext.observeAsState +import org.mozilla.fenix.ext.settings @Composable internal fun RequireAuthorization( @@ -68,7 +69,11 @@ private fun ObserveLifecycle(store: LoginsStore) { override fun onResume(owner: LifecycleOwner) { super.onResume(owner) - activityContext.window?.lock() + if (activityContext.settings().allowScreenCaptureInSecureScreens) { + activityContext.window?.unlock() + } else { + activityContext.window?.lock() + } store.dispatch(LifecycleAction.OnResume) } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SecureTabsTrayBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SecureTabsTrayBinding.kt @@ -32,8 +32,9 @@ class SecureTabsTrayBinding( ) } .collect { state -> - if (state.selectedPage == Page.PrivateTabs && - !settings.allowScreenshotsInPrivateMode + if ( + state.selectedPage == Page.PrivateTabs && + !settings.shouldSecureModeBeOverridden ) { fragment.secure() dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBinding.kt @@ -34,8 +34,9 @@ class SecureTabManagerBinding( ) } .collect { state -> - if (state.selectedPage == Page.PrivateTabs && - !settings.allowScreenshotsInPrivateMode + if ( + state.selectedPage == Page.PrivateTabs && + !settings.shouldSecureModeBeOverridden ) { fragment.secure() } else if (!settings.lastKnownMode.isPrivate) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -463,11 +463,18 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false, ) + val shouldSecureModeBeOverridden + get() = allowScreenshotsInPrivateMode || allowScreenCaptureInSecureScreens var allowScreenshotsInPrivateMode by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_allow_screenshots_in_private_mode), default = false, ) + var allowScreenCaptureInSecureScreens by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_dev_debug_allow_capture_of_secure_screens), + default = false, + ) + val appIconSelection by lazyFeatureFlagPreference( key = appContext.getPreferenceKey(R.string.pref_key_app_icon_selection_enabled), featureFlag = FeatureFlags.APP_ICON_SELECTION, diff --git a/mobile/android/fenix/app/src/main/res/values/preference_keys.xml b/mobile/android/fenix/app/src/main/res/values/preference_keys.xml @@ -281,6 +281,7 @@ <string name="pref_key_open_links_in_apps_ask" translatable="false">pref_key_open_links_in_apps_ask</string> <string name="pref_key_open_links_in_apps_never" translatable="false">pref_key_open_links_in_apps_never</string> <string name="pref_key_allow_screenshots_in_private_mode" translatable="false">pref_key_allow_screenshots_in_private_mode</string> + <string name="pref_key_dev_debug_allow_capture_of_secure_screens" translatable="false">pref_key_dev_debug_allow_capture_of_secure_screens</string> <!-- This preference controls behaviour that corresponds to the private browsing mode lock feature being enabled. --> <string name="pref_key_private_browsing_locked_enabled" translatable="false">pref_key_private_browsing_locked_enabled</string> <string name="pref_key_private_browsing_locked" translatable="false">pref_key_private_browsing_locked</string> diff --git a/mobile/android/fenix/app/src/main/res/values/static_strings.xml b/mobile/android/fenix/app/src/main/res/values/static_strings.xml @@ -252,6 +252,9 @@ <string name="crash_debug_crash_app_warning">This will crash the app to start crash reporting flow. Please also ensure that the crash report option is set to "Ask before sending" in the data collections settings if you want the dialog to pop up.</string> <!-- Timer text for deferral period %s is the timer string --> <string name="crash_debug_deferral_timer">Current deferral period: %s</string> + + <!-- Button text for enabling screen capture on private screens --> + <string name="autofill_debug_enable_screen_capture">Enable screen capture on secure screens</string> <!-- Button text to show startup crash screen --> <string name="crash_debug_show_startup_crash_screen">Show startup crash screen</string> <!-- Subtitle warning text for crash button --> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/FragmentTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ext/FragmentTest.kt @@ -6,7 +6,9 @@ package org.mozilla.fenix.ext import android.content.Context import android.content.res.Configuration +import android.view.WindowManager import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavDirections @@ -20,6 +22,8 @@ import io.mockk.spyk import io.mockk.verify import junit.framework.TestCase.assertEquals import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -27,6 +31,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.navigation.NavControllerProvider import org.mozilla.fenix.utils.Settings +import org.robolectric.Robolectric import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @@ -347,4 +352,49 @@ class FragmentTest { assertEquals(0, bottomToolbarHeight) } + + @Test + fun `WHEN allowScreenCaptureInSecureScreens is true and a fragment is secured THEN FLAG_SECURE flag is added to the LayoutParams`() { + every { settings.allowScreenCaptureInSecureScreens } returns false + + val testFragment = TestFragment(mockContext) + + startFragment(testFragment) + + testFragment.secure() + + val flags = testFragment.requireActivity().window.attributes.flags + assertTrue(flags and WindowManager.LayoutParams.FLAG_SECURE != 0) + } + + @Test + fun `WHEN allowScreenCaptureInSecureScreens is false and a fragment is secured THEN no window flags are added to the LayoutParams`() { + every { settings.allowScreenCaptureInSecureScreens } returns true + + val testFragment = TestFragment(mockContext) + + startFragment(testFragment) + + testFragment.secure() + + val flags = testFragment.requireActivity().window.attributes.flags + assertFalse(flags and WindowManager.LayoutParams.FLAG_SECURE != 0) + } + + class TestFragment(private val mockContext: Context) : Fragment() { + override fun getContext(): Context? { + return mockContext + } + } + + private fun startFragment(fragment: Fragment) { + val activity = Robolectric.buildActivity(FragmentActivity::class.java) + .create() + .start() + .resume() + .get() + activity.supportFragmentManager.beginTransaction() + .add(fragment, null) + .commitNow() + } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/SecureTabsTrayBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/SecureTabsTrayBindingTest.kt @@ -59,7 +59,7 @@ class SecureTabsTrayBindingTest { } @Test - fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() { + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() { val tabsTrayStore = TabsTrayStore(TabsTrayState()) val secureTabsTrayBinding = SecureTabsTrayBinding( store = tabsTrayStore, @@ -78,6 +78,26 @@ class SecureTabsTrayBindingTest { } @Test + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode false and shouldSecureModeBeOverridden true THEN set fragment to un-secure`() { + val tabsTrayStore = TabsTrayStore(TabsTrayState()) + val secureTabsTrayBinding = SecureTabsTrayBinding( + store = tabsTrayStore, + settings = settings, + fragment = fragment, + dialog = dialog, + ) + every { settings.allowScreenshotsInPrivateMode } returns false + every { settings.allowScreenCaptureInSecureScreens } returns false + + secureTabsTrayBinding.start() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) + tabsTrayStore.waitUntilIdle() + + verify { fragment.removeSecure() } + verify { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } + } + + @Test fun `GIVEN not in private mode WHEN tab selected page switches to normal tabs from private THEN set fragment to un-secure`() { every { settings.lastKnownMode.isPrivate } returns false val tabsTrayStore = TabsTrayStore(TabsTrayState()) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBindingTest.kt @@ -54,7 +54,7 @@ class SecureTabManagerBindingTest { } @Test - fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() { + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() { val tabsTrayStore = TabsTrayStore(TabsTrayState()) val secureTabManagerBinding = SecureTabManagerBinding( store = tabsTrayStore, @@ -71,6 +71,24 @@ class SecureTabManagerBindingTest { } @Test + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode false and shouldSecureModeBeOverridden true THEN set fragment to un-secure`() { + val tabsTrayStore = TabsTrayStore(TabsTrayState()) + val secureTabManagerBinding = SecureTabManagerBinding( + store = tabsTrayStore, + settings = settings, + fragment = fragment, + ) + every { settings.allowScreenshotsInPrivateMode } returns false + every { settings.shouldSecureModeBeOverridden } returns true + + secureTabManagerBinding.start() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) + tabsTrayStore.waitUntilIdle() + + verify { fragment.removeSecure() } + } + + @Test fun `GIVEN not in private mode WHEN tab selected page switches to normal tabs from private THEN set fragment to un-secure`() { every { settings.lastKnownMode.isPrivate } returns false val tabsTrayStore = TabsTrayStore(TabsTrayState())