Loading mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +12 −1 Original line number Diff line number Diff line Loading @@ -161,6 +161,7 @@ import org.mozilla.fenix.wallpapers.Wallpaper import org.mozilla.fenix.GleanMetrics.TabStrip as TabStripMetrics import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.tor.TorCampaignViewModel import org.mozilla.fenix.tor.TorHomePage import org.mozilla.fenix.tor.UrlQuickLoadViewModel Loading @@ -179,6 +180,7 @@ class HomeFragment : Fragment(), UserInteractionHandler { private val homeViewModel: HomeScreenViewModel by activityViewModels() private val urlQuickLoadViewModel: UrlQuickLoadViewModel by activityViewModels() private val torCampaignViewModel: TorCampaignViewModel by activityViewModels() private var _bottomToolbarContainerView: BottomToolbarContainerView? = null private val bottomToolbarContainerView: BottomToolbarContainerView Loading Loading @@ -974,7 +976,16 @@ class HomeFragment : Fragment(), UserInteractionHandler { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { TorHomePage( toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP shouldInitiallyShowPromo = torCampaignViewModel.shouldInitiallyShowPromo, toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP, toolPair = torCampaignViewModel.getToolPair(), onClicked = { (requireActivity() as HomeActivity).openToBrowserAndLoad( searchTermOrURL = "https://internetfreedom.torproject.org", newTab = true, from = BrowserDirection.FromHome, ) } ) } } Loading mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/SearchBar.kt +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ internal fun SearchBar( @Composable @PreviewLightDark private fun SearchBarPreview() { fun SearchBarPreview() { FirefoxTheme { Column( modifier = Modifier.background(color = FirefoxTheme.colors.layer1), Loading mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/CampaignCompose.kt 0 → 100644 +310 −0 Original line number Diff line number Diff line package org.mozilla.fenix.tor import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import mozilla.components.ui.colors.PhotonColors import org.mozilla.fenix.R private val alternateLayoutThreshHold = 500.dp @Composable @CampaignComposePreview fun CampaignBox( shouldShowPromo: MutableState<Boolean> = mutableStateOf(true), onDonateButtonClicked: () -> Unit = {}, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool> = Pair(TorCampaignViewModel.toolList[0], TorCampaignViewModel.toolList[1]), ) { BoxWithConstraints( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() .fillMaxHeight(), ) { val alternateLayout = this.maxWidth >= alternateLayoutThreshHold CampaignLayout( alternateLayout, maxWidth = this.maxWidth, shouldShowPromo, onDonateButtonClicked = onDonateButtonClicked, toolPair = toolPair, ) } } @Composable private fun CampaignLayout( alternateLayout: Boolean, maxWidth: Dp, shouldShowPromo: MutableState<Boolean>, onDonateButtonClicked: () -> Unit, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { Column( modifier = Modifier .padding(horizontal = 22.dp) .fillMaxWidth(getVariableWidth(maxWidth)) .wrapContentHeight(), horizontalAlignment = Alignment.CenterHorizontally, ) { PurpleBox( alternateLayout, shouldShowPromo, onDonateButtonClicked = onDonateButtonClicked, toolPair = toolPair, ) Spacer(Modifier.size(8.dp)) Text( text = stringResource(R.string.no_donation_required_yec), style = TextStyle( fontSize = 12.5.sp, lineHeight = 18.75.sp, fontFamily = FontFamily.SansSerif, fontWeight = FontWeight(400), color = PhotonColors.LightGrey05, textAlign = TextAlign.Center, ), ) } } private fun getVariableWidth(width: Dp): Float = (alternateLayoutThreshHold / width).coerceIn(0.80f, 1.0f) @Composable private fun PurpleBox( alternateLayout: Boolean, shouldShowPromo: MutableState<Boolean>, onDonateButtonClicked: () -> Unit, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { Box( modifier = Modifier.background( colorResource(mozilla.components.ui.colors.R.color.photonViolet90), shape = RoundedCornerShape(8.dp), ), ) { Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), horizontalArrangement = Arrangement.End, ) { ExitIcon(shouldShowPromo) } DynamicCampaignContent( alternateLayout, onDonateButtonClicked = onDonateButtonClicked, toolPair = toolPair ) } } @Composable private fun ExitIcon(shouldShowYec: MutableState<Boolean>) { IconButton( modifier = Modifier.padding(8.dp), onClick = { shouldShowYec.value = false }, ) { Icon( painter = painterResource(id = R.drawable.ic_close), tint = PhotonColors.White, contentDescription = stringResource(R.string.close_yec_button_description), ) } } @Composable private fun DynamicCampaignContent( alternateLayout: Boolean, onDonateButtonClicked: () -> Unit, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { @Composable fun Icon(shouldShow: Boolean) { if (shouldShow) { Image( painterResource(R.drawable.globe_chain_burst_yec), contentDescription = null, alignment = Alignment.Center, ) } } Row( modifier = Modifier .padding(start = 16.dp, top = 32.dp, end = 16.dp, bottom = 24.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = Modifier.fillMaxWidth(if (alternateLayout) 0.75f else 1.0f), horizontalAlignment = if (alternateLayout) Alignment.Start else Alignment.CenterHorizontally, ) { Icon(shouldShow = !alternateLayout) Spacer(Modifier.size(24.dp)) TitleText() Spacer(Modifier.size(16.dp)) MainText( toolPair = toolPair ) Spacer(Modifier.size(24.dp)) Row( horizontalArrangement = if (alternateLayout) Arrangement.Start else Arrangement.Center, modifier = Modifier.fillMaxWidth(), ) { DonateButton(onDonateButtonClicked = onDonateButtonClicked, alternateLayout) } } Icon(shouldShow = alternateLayout) } } @Composable private fun TitleText() { Text( text = stringResource(R.string.summer_2026_funding_heading), style = TextStyle( fontSize = 24.sp, lineHeight = 32.sp, fontWeight = FontWeight(400), color = PhotonColors.White, textAlign = TextAlign.Center, letterSpacing = 0.18.sp, ), ) } @Composable private fun MainText(toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>) { Column { Text( AnnotatedString.fromHtml( // Relevant documentation on HTML markup for android https://developer.android.com/guide/topics/resources/string-resource?utm_source=android-studio-app&utm_medium=app#StylingWithHTML stringResource( R.string.summer_2026_funding_intro, "<b>" + toolPair.first.name + "</b>", stringResource(toolPair.first.description), "<b>" + toolPair.second.name + "</b>", stringResource(toolPair.second.description), ), ), style = TextStyle( fontSize = 14.sp, lineHeight = 20.sp, color = PhotonColors.White, fontWeight = FontWeight(400), letterSpacing = 0.25.sp, ) ) Spacer(Modifier.size(8.dp)) Text( text = AnnotatedString.fromHtml( stringResource( R.string.summer_2026_funding_outro, "<b>" + stringResource(R.string.summer_2026_call_to_donate) + "</b>" ) ), modifier = Modifier.fillMaxWidth(), style = TextStyle( fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight(400), letterSpacing = 0.25.sp, color = PhotonColors.LightGrey05, ) ) } } @Composable private fun DonateButton(onDonateButtonClicked: () -> Unit, alternateLayout: Boolean) { Button( onClick = onDonateButtonClicked, colors = ButtonDefaults.buttonColors( colorResource(mozilla.components.ui.colors.R.color.photonViolet60), ), shape = RoundedCornerShape(4.dp), contentPadding = PaddingValues(horizontal = 16.dp), modifier = if (alternateLayout) Modifier.wrapContentWidth() else Modifier.fillMaxWidth() ) { Image( painterResource(R.drawable.heart), contentDescription = null, ) Spacer( Modifier.size(8.dp), ) Text( text = stringResource(R.string.donate_now_yec), textAlign = TextAlign.Center, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = PhotonColors.LightGrey05, ) } } @Preview( name = "Small Window", widthDp = 400, ) @Preview( name = "Medium Window", widthDp = 700, ) @Preview( name = "Large Window", widthDp = 1000, ) @Preview( name = "RTL Small Window", locale = "ar", widthDp = 400, ) @Preview( name = "RTL Large Window", locale = "ar", widthDp = 1000 ) annotation class CampaignComposePreview mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorCampaignViewModel.kt 0 → 100644 +67 −0 Original line number Diff line number Diff line package org.mozilla.fenix.tor import android.app.Application import android.util.Log import androidx.annotation.StringRes import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.application import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import java.text.SimpleDateFormat import java.util.Date class TorCampaignViewModel(application: Application) : AndroidViewModel(application) { val shouldInitiallyShowPromo: MutableState<Boolean> by lazy { mutableStateOf(shouldInitiallyShowPromo()) } fun shouldInitiallyShowPromo(): Boolean { val dateFormat = SimpleDateFormat("yyyy-MM-dd-hh-zzz") // From https://gitlab.torproject.org/tpo/applications/tor-browser/-/work_items/44747 val startDate = dateFormat.parse("2026-05-19-15-UTC") val endDate = dateFormat.parse("2026-06-19-00-UTC") val currentDate = Date() if (currentDate.before(startDate) || currentDate.after(endDate)) { return false } Log.d( "TorCampaignViewModel", "org.mozilla.fenix.BuildConfig.BUILD_TYPE = ${org.mozilla.fenix.BuildConfig.BUILD_TYPE}" ) return (org.mozilla.fenix.BuildConfig.BUILD_TYPE == "release") || (org.mozilla.fenix.BuildConfig.BUILD_TYPE == "debug") } companion object { val toolList: List<Tool> = listOf( Tool(name = "Onion Browser", description = R.string.summer_2026_funding_tool_onion_browser_description), Tool(name = "Quiet", description = R.string.summer_2026_funding_tool_quiet_description), Tool(name = "Ricochet Refresh", description = R.string.summer_2026_funding_tool_ricochet_refresh_description), Tool(name = "SecureDrop", description = R.string.summer_2026_funding_tool_securedrop_description), Tool(name = "OnionShare", description = R.string.summer_2026_funding_tool_onionshare_description), Tool(name = "Digital Security Helpdesk", description = R.string.summer_2026_funding_tool_digital_security_helpdesk_description), Tool(name = "Paskoocheh", description = R.string.summer_2026_funding_tool_paskoocheh_description), Tool(name = "Unredacted", description = R.string.summer_2026_funding_tool_unredacted_description), Tool(name = "Osservatorio Nessuno", description = R.string.summer_2026_funding_tool_osservatorio_nessuno_description), Tool(name = "Save", description = R.string.summer_2026_funding_tool_save_description), Tool(name = "OONI", description = R.string.summer_2026_funding_tool_ooni_description), ).shuffled() } private var toolIndex = 0 private fun incrementToolIndex() { toolIndex = (toolIndex + 2) % toolList.size } data class Tool( val name: String, @param:StringRes val description: Int, ) fun getToolPair() : Pair<Tool, Tool> { return Pair(toolList[toolIndex], toolList[(toolIndex + 1) % toolList.size]).also { incrementToolIndex() } } } mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorHomePage.kt +94 −24 Original line number Diff line number Diff line package org.mozilla.fenix.tor import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer Loading @@ -12,6 +13,10 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint Loading @@ -26,16 +31,33 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview import org.mozilla.fenix.R import org.mozilla.fenix.home.ui.SearchBarPreview @Composable @FlexibleWindowLightDarkPreview fun TorHomePage( toolBarAtTop: Boolean = true, shouldInitiallyShowPromo: MutableState<Boolean> = mutableStateOf(true), onClicked: () -> Unit = {}, toolBarAtTop: Boolean = false, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { // Will persist across a single session, but not multiple sessions. // Tapping the close button 'X' will hide the promo for the duration of the session val shouldShowPromo = rememberSaveable { shouldInitiallyShowPromo } // Will persist through screen rotations, but not navigations (e.g. Tap on settings -> come back will update) // Is expected to change with every new visit to about:tor val toolPair = remember { toolPair } Column( modifier = Modifier .fillMaxSize() Loading Loading @@ -82,6 +104,14 @@ fun TorHomePage( ) } Spacer(Modifier.weight(1f)) if (shouldShowPromo.value) { CampaignBox( shouldShowPromo, onDonateButtonClicked = onClicked, toolPair = toolPair ) Spacer(Modifier.weight(1f)) } else { Text( // Moved from the commit 5bb3cc6b93346dabd8d46677fae7f86a8f8a4fc2 // "[android] Modify UI/UX", and the file HomeFragment. Loading @@ -99,6 +129,8 @@ fun TorHomePage( ), modifier = Modifier.align(Alignment.CenterHorizontally), ) } if (!shouldShowPromo.value) { Spacer(Modifier.weight(1f)) Image( painter = painterResource( Loading @@ -107,5 +139,43 @@ fun TorHomePage( contentDescription = null, Modifier.fillMaxWidth(), ) } } Spacer(modifier = Modifier.size(17.dp)) } @Composable @Preview /** * Relevant documentation * https://developer.android.com/develop/ui/compose/tooling/previews#preview-viewmodel */ private fun TorHomePagePreview( @PreviewParameter( BooleanBooleanPreviewParameterProvider::class, ) booleanMatrix: Pair<Boolean, Boolean>, ) { val toolbarAtTop = booleanMatrix.second Box( contentAlignment = if (toolbarAtTop) Alignment.TopStart else Alignment.BottomStart, modifier = Modifier.fillMaxSize(), ) { SearchBarPreview() // unrestricted vertically so will follow contentAlignment TorHomePage( // restricted vertically so will not follow contentAlignment shouldInitiallyShowPromo = mutableStateOf(booleanMatrix.first), toolBarAtTop = toolbarAtTop, toolPair = Pair(TorCampaignViewModel.toolList[0], TorCampaignViewModel.toolList[1]), ) } } private class BooleanBooleanPreviewParameterProvider : PreviewParameterProvider<Pair<Boolean, Boolean>> { override val values: Sequence<Pair<Boolean, Boolean>> get() = sequenceOf( Pair(true, true), Pair(true, false), Pair(false, true), Pair(false, false), ) } Loading
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +12 −1 Original line number Diff line number Diff line Loading @@ -161,6 +161,7 @@ import org.mozilla.fenix.wallpapers.Wallpaper import org.mozilla.fenix.GleanMetrics.TabStrip as TabStripMetrics import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.tor.TorCampaignViewModel import org.mozilla.fenix.tor.TorHomePage import org.mozilla.fenix.tor.UrlQuickLoadViewModel Loading @@ -179,6 +180,7 @@ class HomeFragment : Fragment(), UserInteractionHandler { private val homeViewModel: HomeScreenViewModel by activityViewModels() private val urlQuickLoadViewModel: UrlQuickLoadViewModel by activityViewModels() private val torCampaignViewModel: TorCampaignViewModel by activityViewModels() private var _bottomToolbarContainerView: BottomToolbarContainerView? = null private val bottomToolbarContainerView: BottomToolbarContainerView Loading Loading @@ -974,7 +976,16 @@ class HomeFragment : Fragment(), UserInteractionHandler { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { TorHomePage( toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP shouldInitiallyShowPromo = torCampaignViewModel.shouldInitiallyShowPromo, toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP, toolPair = torCampaignViewModel.getToolPair(), onClicked = { (requireActivity() as HomeActivity).openToBrowserAndLoad( searchTermOrURL = "https://internetfreedom.torproject.org", newTab = true, from = BrowserDirection.FromHome, ) } ) } } Loading
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/SearchBar.kt +1 −1 Original line number Diff line number Diff line Loading @@ -34,7 +34,7 @@ internal fun SearchBar( @Composable @PreviewLightDark private fun SearchBarPreview() { fun SearchBarPreview() { FirefoxTheme { Column( modifier = Modifier.background(color = FirefoxTheme.colors.layer1), Loading
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/CampaignCompose.kt 0 → 100644 +310 −0 Original line number Diff line number Diff line package org.mozilla.fenix.tor import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import mozilla.components.ui.colors.PhotonColors import org.mozilla.fenix.R private val alternateLayoutThreshHold = 500.dp @Composable @CampaignComposePreview fun CampaignBox( shouldShowPromo: MutableState<Boolean> = mutableStateOf(true), onDonateButtonClicked: () -> Unit = {}, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool> = Pair(TorCampaignViewModel.toolList[0], TorCampaignViewModel.toolList[1]), ) { BoxWithConstraints( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() .fillMaxHeight(), ) { val alternateLayout = this.maxWidth >= alternateLayoutThreshHold CampaignLayout( alternateLayout, maxWidth = this.maxWidth, shouldShowPromo, onDonateButtonClicked = onDonateButtonClicked, toolPair = toolPair, ) } } @Composable private fun CampaignLayout( alternateLayout: Boolean, maxWidth: Dp, shouldShowPromo: MutableState<Boolean>, onDonateButtonClicked: () -> Unit, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { Column( modifier = Modifier .padding(horizontal = 22.dp) .fillMaxWidth(getVariableWidth(maxWidth)) .wrapContentHeight(), horizontalAlignment = Alignment.CenterHorizontally, ) { PurpleBox( alternateLayout, shouldShowPromo, onDonateButtonClicked = onDonateButtonClicked, toolPair = toolPair, ) Spacer(Modifier.size(8.dp)) Text( text = stringResource(R.string.no_donation_required_yec), style = TextStyle( fontSize = 12.5.sp, lineHeight = 18.75.sp, fontFamily = FontFamily.SansSerif, fontWeight = FontWeight(400), color = PhotonColors.LightGrey05, textAlign = TextAlign.Center, ), ) } } private fun getVariableWidth(width: Dp): Float = (alternateLayoutThreshHold / width).coerceIn(0.80f, 1.0f) @Composable private fun PurpleBox( alternateLayout: Boolean, shouldShowPromo: MutableState<Boolean>, onDonateButtonClicked: () -> Unit, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { Box( modifier = Modifier.background( colorResource(mozilla.components.ui.colors.R.color.photonViolet90), shape = RoundedCornerShape(8.dp), ), ) { Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight(), horizontalArrangement = Arrangement.End, ) { ExitIcon(shouldShowPromo) } DynamicCampaignContent( alternateLayout, onDonateButtonClicked = onDonateButtonClicked, toolPair = toolPair ) } } @Composable private fun ExitIcon(shouldShowYec: MutableState<Boolean>) { IconButton( modifier = Modifier.padding(8.dp), onClick = { shouldShowYec.value = false }, ) { Icon( painter = painterResource(id = R.drawable.ic_close), tint = PhotonColors.White, contentDescription = stringResource(R.string.close_yec_button_description), ) } } @Composable private fun DynamicCampaignContent( alternateLayout: Boolean, onDonateButtonClicked: () -> Unit, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { @Composable fun Icon(shouldShow: Boolean) { if (shouldShow) { Image( painterResource(R.drawable.globe_chain_burst_yec), contentDescription = null, alignment = Alignment.Center, ) } } Row( modifier = Modifier .padding(start = 16.dp, top = 32.dp, end = 16.dp, bottom = 24.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = Modifier.fillMaxWidth(if (alternateLayout) 0.75f else 1.0f), horizontalAlignment = if (alternateLayout) Alignment.Start else Alignment.CenterHorizontally, ) { Icon(shouldShow = !alternateLayout) Spacer(Modifier.size(24.dp)) TitleText() Spacer(Modifier.size(16.dp)) MainText( toolPair = toolPair ) Spacer(Modifier.size(24.dp)) Row( horizontalArrangement = if (alternateLayout) Arrangement.Start else Arrangement.Center, modifier = Modifier.fillMaxWidth(), ) { DonateButton(onDonateButtonClicked = onDonateButtonClicked, alternateLayout) } } Icon(shouldShow = alternateLayout) } } @Composable private fun TitleText() { Text( text = stringResource(R.string.summer_2026_funding_heading), style = TextStyle( fontSize = 24.sp, lineHeight = 32.sp, fontWeight = FontWeight(400), color = PhotonColors.White, textAlign = TextAlign.Center, letterSpacing = 0.18.sp, ), ) } @Composable private fun MainText(toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>) { Column { Text( AnnotatedString.fromHtml( // Relevant documentation on HTML markup for android https://developer.android.com/guide/topics/resources/string-resource?utm_source=android-studio-app&utm_medium=app#StylingWithHTML stringResource( R.string.summer_2026_funding_intro, "<b>" + toolPair.first.name + "</b>", stringResource(toolPair.first.description), "<b>" + toolPair.second.name + "</b>", stringResource(toolPair.second.description), ), ), style = TextStyle( fontSize = 14.sp, lineHeight = 20.sp, color = PhotonColors.White, fontWeight = FontWeight(400), letterSpacing = 0.25.sp, ) ) Spacer(Modifier.size(8.dp)) Text( text = AnnotatedString.fromHtml( stringResource( R.string.summer_2026_funding_outro, "<b>" + stringResource(R.string.summer_2026_call_to_donate) + "</b>" ) ), modifier = Modifier.fillMaxWidth(), style = TextStyle( fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight(400), letterSpacing = 0.25.sp, color = PhotonColors.LightGrey05, ) ) } } @Composable private fun DonateButton(onDonateButtonClicked: () -> Unit, alternateLayout: Boolean) { Button( onClick = onDonateButtonClicked, colors = ButtonDefaults.buttonColors( colorResource(mozilla.components.ui.colors.R.color.photonViolet60), ), shape = RoundedCornerShape(4.dp), contentPadding = PaddingValues(horizontal = 16.dp), modifier = if (alternateLayout) Modifier.wrapContentWidth() else Modifier.fillMaxWidth() ) { Image( painterResource(R.drawable.heart), contentDescription = null, ) Spacer( Modifier.size(8.dp), ) Text( text = stringResource(R.string.donate_now_yec), textAlign = TextAlign.Center, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, color = PhotonColors.LightGrey05, ) } } @Preview( name = "Small Window", widthDp = 400, ) @Preview( name = "Medium Window", widthDp = 700, ) @Preview( name = "Large Window", widthDp = 1000, ) @Preview( name = "RTL Small Window", locale = "ar", widthDp = 400, ) @Preview( name = "RTL Large Window", locale = "ar", widthDp = 1000 ) annotation class CampaignComposePreview
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorCampaignViewModel.kt 0 → 100644 +67 −0 Original line number Diff line number Diff line package org.mozilla.fenix.tor import android.app.Application import android.util.Log import androidx.annotation.StringRes import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.application import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import java.text.SimpleDateFormat import java.util.Date class TorCampaignViewModel(application: Application) : AndroidViewModel(application) { val shouldInitiallyShowPromo: MutableState<Boolean> by lazy { mutableStateOf(shouldInitiallyShowPromo()) } fun shouldInitiallyShowPromo(): Boolean { val dateFormat = SimpleDateFormat("yyyy-MM-dd-hh-zzz") // From https://gitlab.torproject.org/tpo/applications/tor-browser/-/work_items/44747 val startDate = dateFormat.parse("2026-05-19-15-UTC") val endDate = dateFormat.parse("2026-06-19-00-UTC") val currentDate = Date() if (currentDate.before(startDate) || currentDate.after(endDate)) { return false } Log.d( "TorCampaignViewModel", "org.mozilla.fenix.BuildConfig.BUILD_TYPE = ${org.mozilla.fenix.BuildConfig.BUILD_TYPE}" ) return (org.mozilla.fenix.BuildConfig.BUILD_TYPE == "release") || (org.mozilla.fenix.BuildConfig.BUILD_TYPE == "debug") } companion object { val toolList: List<Tool> = listOf( Tool(name = "Onion Browser", description = R.string.summer_2026_funding_tool_onion_browser_description), Tool(name = "Quiet", description = R.string.summer_2026_funding_tool_quiet_description), Tool(name = "Ricochet Refresh", description = R.string.summer_2026_funding_tool_ricochet_refresh_description), Tool(name = "SecureDrop", description = R.string.summer_2026_funding_tool_securedrop_description), Tool(name = "OnionShare", description = R.string.summer_2026_funding_tool_onionshare_description), Tool(name = "Digital Security Helpdesk", description = R.string.summer_2026_funding_tool_digital_security_helpdesk_description), Tool(name = "Paskoocheh", description = R.string.summer_2026_funding_tool_paskoocheh_description), Tool(name = "Unredacted", description = R.string.summer_2026_funding_tool_unredacted_description), Tool(name = "Osservatorio Nessuno", description = R.string.summer_2026_funding_tool_osservatorio_nessuno_description), Tool(name = "Save", description = R.string.summer_2026_funding_tool_save_description), Tool(name = "OONI", description = R.string.summer_2026_funding_tool_ooni_description), ).shuffled() } private var toolIndex = 0 private fun incrementToolIndex() { toolIndex = (toolIndex + 2) % toolList.size } data class Tool( val name: String, @param:StringRes val description: Int, ) fun getToolPair() : Pair<Tool, Tool> { return Pair(toolList[toolIndex], toolList[(toolIndex + 1) % toolList.size]).also { incrementToolIndex() } } }
mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorHomePage.kt +94 −24 Original line number Diff line number Diff line package org.mozilla.fenix.tor import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer Loading @@ -12,6 +13,10 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint Loading @@ -26,16 +31,33 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview import org.mozilla.fenix.R import org.mozilla.fenix.home.ui.SearchBarPreview @Composable @FlexibleWindowLightDarkPreview fun TorHomePage( toolBarAtTop: Boolean = true, shouldInitiallyShowPromo: MutableState<Boolean> = mutableStateOf(true), onClicked: () -> Unit = {}, toolBarAtTop: Boolean = false, toolPair: Pair<TorCampaignViewModel.Tool, TorCampaignViewModel.Tool>, ) { // Will persist across a single session, but not multiple sessions. // Tapping the close button 'X' will hide the promo for the duration of the session val shouldShowPromo = rememberSaveable { shouldInitiallyShowPromo } // Will persist through screen rotations, but not navigations (e.g. Tap on settings -> come back will update) // Is expected to change with every new visit to about:tor val toolPair = remember { toolPair } Column( modifier = Modifier .fillMaxSize() Loading Loading @@ -82,6 +104,14 @@ fun TorHomePage( ) } Spacer(Modifier.weight(1f)) if (shouldShowPromo.value) { CampaignBox( shouldShowPromo, onDonateButtonClicked = onClicked, toolPair = toolPair ) Spacer(Modifier.weight(1f)) } else { Text( // Moved from the commit 5bb3cc6b93346dabd8d46677fae7f86a8f8a4fc2 // "[android] Modify UI/UX", and the file HomeFragment. Loading @@ -99,6 +129,8 @@ fun TorHomePage( ), modifier = Modifier.align(Alignment.CenterHorizontally), ) } if (!shouldShowPromo.value) { Spacer(Modifier.weight(1f)) Image( painter = painterResource( Loading @@ -107,5 +139,43 @@ fun TorHomePage( contentDescription = null, Modifier.fillMaxWidth(), ) } } Spacer(modifier = Modifier.size(17.dp)) } @Composable @Preview /** * Relevant documentation * https://developer.android.com/develop/ui/compose/tooling/previews#preview-viewmodel */ private fun TorHomePagePreview( @PreviewParameter( BooleanBooleanPreviewParameterProvider::class, ) booleanMatrix: Pair<Boolean, Boolean>, ) { val toolbarAtTop = booleanMatrix.second Box( contentAlignment = if (toolbarAtTop) Alignment.TopStart else Alignment.BottomStart, modifier = Modifier.fillMaxSize(), ) { SearchBarPreview() // unrestricted vertically so will follow contentAlignment TorHomePage( // restricted vertically so will not follow contentAlignment shouldInitiallyShowPromo = mutableStateOf(booleanMatrix.first), toolBarAtTop = toolbarAtTop, toolPair = Pair(TorCampaignViewModel.toolList[0], TorCampaignViewModel.toolList[1]), ) } } private class BooleanBooleanPreviewParameterProvider : PreviewParameterProvider<Pair<Boolean, Boolean>> { override val values: Sequence<Pair<Boolean, Boolean>> get() = sequenceOf( Pair(true, true), Pair(true, false), Pair(false, true), Pair(false, false), ) }