tor-browser

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

commit 5268af90d48886646439fb7f46fe2374e4ac2561
parent a07e6bed44d2163b5fbd9c2fe21a45e0efc90dbc
Author: mcarare <48995920+mcarare@users.noreply.github.com>
Date:   Mon, 10 Nov 2025 12:00:53 +0000

Bug 1998659 - Migrate AboutFragment to be fully composable r=android-reviewers,giorga

This patch refactors the `AboutFragment` to be a fully composable screen, removing the dependency on fragment lifecycle methods like `onCreate` and `onResume`.
Key changes include:
 - Moving logic for initializing content, version info, and click handlers into the main `@Composable` `Content` function using `remember`.
 - Simplifying the `SecretSettingsUnlocker` to accept a callback lambda, decoupling it from `AppAction` dispatching and making it more reusable.
 - Changing the `openLearnMore` function to return `Unit` instead of `Job`, as it is a fire-and-forget action.

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

Diffstat:
Mmobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/AboutFragment.kt | 216+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mmobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/SecretSettingsUnlocker.kt | 9+++++----
Mmobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/preferences/LearnMoreLink.kt | 3+--
3 files changed, 113 insertions(+), 115 deletions(-)

diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/AboutFragment.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/AboutFragment.kt @@ -4,7 +4,7 @@ package org.mozilla.focus.fragment.about -import android.os.Bundle +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -16,13 +16,14 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import androidx.core.content.pm.PackageInfoCompat -import kotlinx.coroutines.Job import mozilla.components.browser.state.state.SessionState import mozilla.components.support.utils.ext.getPackageInfoCompat import org.mozilla.focus.BuildConfig @@ -43,34 +44,20 @@ import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig */ class AboutFragment : BaseComposeFragment() { - private lateinit var secretSettingsUnlocker: SecretSettingsUnlocker - private lateinit var appName: String - private lateinit var aboutHeader: String - private lateinit var content: String - private lateinit var aboutContent: String - - private val openLearnMore = { - val tabId = requireContext().components.tabsUseCases.addTab( - url = manifestoURL, - source = SessionState.Source.Internal.Menu, - selectTab = true, - private = true, - ) - requireContext().components.appStore.dispatch(AppAction.OpenTab(tabId)) - } - override val titleRes: Int = R.string.menu_about - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - secretSettingsUnlocker = SecretSettingsUnlocker(requireContext()) - appName = requireContext().resources.getString(R.string.app_name) - aboutContent = - requireContext().getString(R.string.about_content, appName, "") + @Composable + override fun Content() { + val context = LocalContext.current - aboutHeader = getAboutHeader() + val aboutHeader = remember { + getAboutHeader(context) + } + + val content = remember { + val appName = context.getString(R.string.app_name) + val aboutContent = context.getString(R.string.about_content, appName, "") - content = aboutContent .replace("<li>", "\u2022 \u0009 ") .replace("</li>", "\n") @@ -80,106 +67,117 @@ class AboutFragment : BaseComposeFragment() { .replace("</p>", "") .replaceAfter("<br/>", "") .replace("<br/>", "") - } + } - override fun onResume() { - super.onResume() - secretSettingsUnlocker.resetCounter() - } + val openLearnMore = remember { + { + val tabId = context.components.tabsUseCases.addTab( + url = manifestoURL, + source = SessionState.Source.Internal.Menu, + selectTab = true, + private = true, + ) + context.components.appStore.dispatch(AppAction.OpenTab(tabId)) + Unit + } + } + + val secretSettingsUnlocker = remember { + SecretSettingsUnlocker( + context, + ) { + context.components.appStore.dispatch( + AppAction.SecretSettingsStateChange( + true, + ), + ) + } + } - @Composable - override fun Content() { AboutPageContent( aboutVersion = aboutHeader, content = content, - secretSettingsUnlocker = secretSettingsUnlocker, + onLogoClick = secretSettingsUnlocker::increment, openLearnMore = openLearnMore, ) } +} - private fun getAboutHeader(): String { - val gecko = " \uD83E\uDD8E " - val engineIndicator = - gecko + GeckoViewBuildConfig.MOZ_APP_VERSION + "-" + GeckoViewBuildConfig.MOZ_APP_BUILDID - val servicesAbbreviation = getString(R.string.services_abbreviation) - val servicesIndicator = mozilla.components.Build.APPLICATION_SERVICES_VERSION - val packageInfo = - requireContext().packageManager.getPackageInfoCompat(requireContext().packageName, 0) - val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString() - val vcsHash = if (BuildConfig.VCS_HASH.isNotBlank()) ", ${BuildConfig.VCS_HASH}" else "" - - @Suppress("ImplicitDefaultLocale") // We want LTR in all cases as the version is not translatable. - return String.format( - "%s (Build #%s)%s\n%s: %s", - packageInfo.versionName, - versionCode + engineIndicator, - vcsHash, - servicesAbbreviation, - servicesIndicator, - ) - } - - @Composable - private fun AboutPageContent( - aboutVersion: String, - content: String, - secretSettingsUnlocker: SecretSettingsUnlocker, - openLearnMore: () -> Job, - ) { - FocusTheme { - Column( - modifier = Modifier - .padding(8.dp) - .fillMaxSize() - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - LogoIcon(secretSettingsUnlocker) - VersionInfo(aboutVersion) - AboutContent(content) - LearnMoreLink(openLearnMore) - } - } - } - - @Composable - private fun LogoIcon(secretSettingsUnlocker: SecretSettingsUnlocker) { - Image( - painter = painterResource(R.drawable.wordmark2), - contentDescription = null, +@Composable +private fun AboutPageContent( + aboutVersion: String, + content: String, + onLogoClick: () -> Unit, + openLearnMore: () -> Unit, +) { + FocusTheme { + Column( modifier = Modifier - .padding(4.dp) - .clickable { - secretSettingsUnlocker.increment() - }, - ) - } - - @Composable - private fun VersionInfo(aboutVersion: String) { - SelectionContainer { - Text( - text = aboutVersion, - color = focusColors.aboutPageText, - style = focusTypography.bodyLarge.copy( - // Use LTR in all cases since the version is not translatable. - textDirection = TextDirection.Ltr, - ), - modifier = Modifier - .padding(10.dp), - ) + .padding(8.dp) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + LogoIcon(onLogoClick) + VersionInfo(aboutVersion) + AboutContent(content) + LearnMoreLink(openLearnMore) } } +} - @Composable - private fun AboutContent(content: String) { +@Composable +private fun LogoIcon(onLogoClick: () -> Unit) { + Image( + painter = painterResource(R.drawable.wordmark2), + contentDescription = null, + modifier = Modifier + .padding(4.dp) + .clickable(onClick = onLogoClick), + ) +} + +@Composable +private fun VersionInfo(aboutVersion: String) { + SelectionContainer { Text( - text = content, + text = aboutVersion, color = focusColors.aboutPageText, - style = focusTypography.bodyLarge, + style = focusTypography.bodyLarge.copy( + // Use LTR in all cases since the version is not translatable. + textDirection = TextDirection.Ltr, + ), modifier = Modifier .padding(10.dp), ) } } + +@Composable +private fun AboutContent(content: String) { + Text( + text = content, + color = focusColors.aboutPageText, + style = focusTypography.bodyLarge, + modifier = Modifier + .padding(10.dp), + ) +} + +private fun getAboutHeader(context: Context): String { + val gecko = " \uD83E\uDD8E " + val engineIndicator = + gecko + GeckoViewBuildConfig.MOZ_APP_VERSION + "-" + GeckoViewBuildConfig.MOZ_APP_BUILDID + val servicesAbbreviation = context.getString(R.string.services_abbreviation) + val servicesIndicator = mozilla.components.Build.APPLICATION_SERVICES_VERSION + val packageInfo = + context.packageManager.getPackageInfoCompat(context.packageName, 0) + val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString() + val vcsHash = if (BuildConfig.VCS_HASH.isNotBlank()) ", ${BuildConfig.VCS_HASH}" else "" + + return """ + ${packageInfo.versionName} (Build #${versionCode}$engineIndicator)$vcsHash + $servicesAbbreviation: $servicesIndicator + """.trimIndent() +} diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/SecretSettingsUnlocker.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/fragment/about/SecretSettingsUnlocker.kt @@ -7,13 +7,14 @@ package org.mozilla.focus.fragment.about import android.content.Context import android.widget.Toast import org.mozilla.focus.R -import org.mozilla.focus.ext.components -import org.mozilla.focus.state.AppAction /** * Triggers the "secret" debug menu when logoView is tapped 5 times. */ -class SecretSettingsUnlocker(private val context: Context) { +class SecretSettingsUnlocker( + private val context: Context, + private val onLogoClicked: () -> Unit, +) { private var secretSettingsClicks = 0 private var lastDebugMenuToast: Toast? = null @@ -47,7 +48,7 @@ class SecretSettingsUnlocker(private val context: Context) { R.string.about_debug_menu_toast_done, Toast.LENGTH_LONG, ).show() - context.components.appStore.dispatch(AppAction.SecretSettingsStateChange(true)) + onLogoClicked() } } } diff --git a/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/preferences/LearnMoreLink.kt b/mobile/android/focus-android/app/src/main/java/org/mozilla/focus/ui/preferences/LearnMoreLink.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.Alignment.Companion.Start import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import kotlinx.coroutines.Job import org.mozilla.focus.R import org.mozilla.focus.ui.theme.focusColors import org.mozilla.focus.ui.theme.focusTypography @@ -27,7 +26,7 @@ import org.mozilla.focus.ui.theme.focusTypography */ @Composable fun ColumnScope.LearnMoreLink( - openLearnMore: () -> Job, + openLearnMore: () -> Unit, modifier: Modifier = Modifier, ) { Text(