Commit b9ce1488 authored by clairehurst's avatar clairehurst 🌱 Committed by Dan Ballard
Browse files

[android] Bug 45217: Implement YEC 2026 Takeover for Android Stable

parent b6746e50
Loading
Loading
Loading
Loading
Loading
+19 −1
Changes for mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt: 19 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -164,6 +164,9 @@ import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.tor.TorHomePage
import org.mozilla.fenix.tor.UrlQuickLoadViewModel

import org.mozilla.fenix.tor.TorCampaignViewModel
import java.util.Locale

@Suppress("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment(), UserInteractionHandler {
    private val args by navArgs<HomeFragmentArgs>()
@@ -179,6 +182,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
@@ -973,12 +977,26 @@ class HomeFragment : Fragment(), UserInteractionHandler {
        binding.torHomepageView.apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                FirefoxTheme {
                    val isToolbarAtTop = components.settings.toolbarPosition == ToolbarPosition.TOP
                        TorHomePage(
                    toolBarAtTop = settings().toolbarPosition == ToolbarPosition.TOP
                            toolBarAtTop = isToolbarAtTop,
                            shouldInitiallyShowPromo = torCampaignViewModel.shouldInitiallyShowPromo,
                            onClicked = {
                                val locale = Locale.getDefault().getLanguage()
                                var url = "https://www.torproject.org/donate/yec2026-tor-browser-android-${locale}"
                                (requireActivity() as HomeActivity).openToBrowserAndLoad(
                                    // from https://gitlab.torproject.org/tpo/applications/tor-browser/-/work_items/45217#note_3464207
                                    url,
                                    newTab = true,
                                    from = BrowserDirection.FromHome,
                                )
                            },
                        )
                }
            }
        }
    }

    private fun initComposeHomepage() {
        binding.sessionControlRecyclerView.isVisible = false
+283 −0
Changes for mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/CampaignCompose.kt: 283 added lines, 0 removed lines.
Original line number Diff line number Diff line
package org.mozilla.fenix.tor

import androidx.annotation.DrawableRes
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.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 = {},
    ) {
    BoxWithConstraints(
        contentAlignment = Alignment.Center,
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
    ) {
        val alternateLayout = this.maxWidth >= alternateLayoutThreshHold

        CampaignLayout(
            alternateLayout,
            maxWidth = this.maxWidth,
            shouldShowPromo,
            onDonateButtonClicked = onDonateButtonClicked,
        )
    }
}

@Composable
private fun CampaignLayout(
    alternateLayout: Boolean,
    maxWidth: Dp,
    shouldShowPromo: MutableState<Boolean>,
    onDonateButtonClicked: () -> Unit,
) {
    Column(
        modifier = Modifier
            .fillMaxWidth(getVariableWidth(maxWidth))
            .wrapContentHeight(),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        PurpleBox(
            alternateLayout,
            shouldShowPromo,
            onDonateButtonClicked = onDonateButtonClicked,
        )
        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,
) {
    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,
        )
    }
}

@Composable
private fun ExitIcon(shouldShowYec: MutableState<Boolean>) {
    IconButton(
        onClick = {
            shouldShowYec.value = false
        },
    ) {
        Icon(
            painter = painterResource(id = mozilla.components.ui.icons.R.drawable.mozac_ic_cross_20),
            tint = PhotonColors.White,
            contentDescription = stringResource(R.string.close_yec_button_description),
        )
    }
}


@Composable
private fun DynamicCampaignContent(
    alternateLayout: Boolean,
    onDonateButtonClicked: () -> Unit,
) {
    @Composable
    fun Icon(shouldShow: Boolean, @DrawableRes drawable: Int, contentDescription: String? = null) {
        if (shouldShow) {
            Image(
                painterResource(drawable),
                contentDescription = contentDescription,
                alignment = Alignment.Center,
            )
        }
    }
    Row(
        modifier = Modifier
            .padding(start = 16.dp, top = if (alternateLayout) 24.dp else 32.dp, end = 16.dp, bottom = 24.dp)
            .fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Column(
            modifier = Modifier.fillMaxWidth(),
            horizontalAlignment = if (alternateLayout) Alignment.Start else Alignment.CenterHorizontally,
        ) {
            Icon(shouldShow = !alternateLayout, R.drawable.bulb_illo_yec, contentDescription = stringResource(R.string.bulb_illo_yec_description))
            Spacer(Modifier.size (if (alternateLayout) 0.dp else 16.dp))
            Row(verticalAlignment = Alignment.CenterVertically) {
                Icon(shouldShow = alternateLayout, R.drawable.circled_lighning_yec)
                Spacer(Modifier.size(if (alternateLayout) 8.dp else 0.dp))
                TitleText()
            }
            Spacer(Modifier.size(8.dp))
            MainText()
            Spacer(Modifier.size(if (alternateLayout) 16.dp else 24.dp))
            Row(
                horizontalArrangement = if (alternateLayout) Arrangement.Start else Arrangement.Center,
                modifier = Modifier.fillMaxWidth(),
            ) {
                DonateButton(onDonateButtonClicked = onDonateButtonClicked, alternateLayout)
            }
        }
    }
}


@Composable
private fun TitleText() {
    Text(
        text = stringResource(R.string.yec26_tor_powered_by_you),
        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() {
    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.yec26_call_to_action,
                ),
            ), style = TextStyle(
                fontSize = 14.sp,
                lineHeight = 20.sp,
                color = PhotonColors.White,
                fontWeight = FontWeight(400),
                letterSpacing = 0.25.sp,
            )
        )
    }
}

@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.fillMaxWidth() else Modifier.fillMaxWidth()
    ) {
        Text(
            text = stringResource(R.string.donate_now_yec),
            textAlign = TextAlign.Center,
            fontSize = 16.sp,
            fontWeight = FontWeight.SemiBold,
            color = PhotonColors.LightGrey05,
        )
        Spacer(
            Modifier.size(8.dp),
        )
        Image(
            painterResource(R.drawable.heart),
            contentDescription = null,
        )
    }
}



@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
+37 −0
Changes for mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorCampaignViewModel.kt: 37 added lines, 0 removed lines.
Original line number Diff line number Diff line
package org.mozilla.fenix.tor

import android.annotation.SuppressLint
import android.app.Application
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import java.text.SimpleDateFormat
import java.util.Date

class TorCampaignViewModel(application: Application) : AndroidViewModel(application) {

    val shouldInitiallyShowPromo: MutableState<Boolean> by lazy {
        mutableStateOf(shouldInitiallyShowPromo())
    }

    @SuppressLint("SimpleDateFormat")
    fun shouldInitiallyShowPromo(): Boolean {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd-hh-zzz")
        // From https://gitlab.torproject.org/tpo/applications/tor-browser/-/work_items/45217#note_3462659
        val startDate = dateFormat.parse("2026-10-13-08-UTC")
        // From https://gitlab.torproject.org/tpo/applications/tor-browser/-/work_items/45217#note_3463954
        val endDate =   dateFormat.parse("2027-01-05-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")
    }

}
+61 −1
Changes for mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorHomePage.kt: 61 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -31,11 +31,28 @@ import androidx.compose.ui.unit.sp
import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview
import org.mozilla.fenix.R

import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider

@Composable
@FlexibleWindowLightDarkPreview
fun TorHomePage(
    toolBarAtTop: Boolean = true,
    shouldInitiallyShowPromo: MutableState<Boolean> = mutableStateOf(true),
    onClicked: () -> Unit = {},
) {
    // 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
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
@@ -64,7 +81,7 @@ fun TorHomePage(
    ) {
        Spacer(modifier = Modifier.size(17.dp))
        Row(
            modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically,
            modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), verticalAlignment = Alignment.CenterVertically,
        ) {
            Image(
                painter = painterResource(R.drawable.tor_browser_app_icon),
@@ -82,6 +99,13 @@ fun TorHomePage(
            )
        }
        Spacer(Modifier.weight(1f))
        if (shouldShowPromo.value) {
            CampaignBox(
                shouldShowPromo,
                onDonateButtonClicked = onClicked,
            )
            Spacer(Modifier.weight(1f))
        } else {
            Text(
                // Moved from the commit 5bb3cc6b93346dabd8d46677fae7f86a8f8a4fc2
                // "[android] Modify UI/UX", and the file HomeFragment.
@@ -99,6 +123,8 @@ fun TorHomePage(
                ),
                modifier = Modifier.align(Alignment.CenterHorizontally),
            )
        }
        if (!shouldShowPromo.value) {
            Spacer(Modifier.weight(1f))
            Image(
                painter = painterResource(
@@ -107,5 +133,39 @@ fun TorHomePage(
                contentDescription = null, Modifier.fillMaxWidth(),
            )
        }
    }
    Spacer(modifier = Modifier.size(17.dp))
}

@SuppressLint("UnrememberedMutableState")
@Composable
@Preview
/**
 * Relevant documentation
 * https://developer.android.com/develop/ui/compose/tooling/previews#preview-viewmodel
 */
private fun TorHomePagePreview(
    @PreviewParameter(
        BooleanPreviewParameterProvider::class,
    ) shouldShowPromo: Boolean,
) {
    Box(
        contentAlignment = Alignment.TopStart,
        modifier = Modifier.fillMaxSize(),
    ) {
        TorHomePage(
            // restricted vertically so will not follow contentAlignment
            shouldInitiallyShowPromo = mutableStateOf( shouldShowPromo),
            toolBarAtTop = true,
            onClicked = {},
        )
    }
}

private class BooleanPreviewParameterProvider :
    PreviewParameterProvider<Boolean> {
    override val values: Sequence<Boolean>
        get() = sequenceOf(
            true, false
        )
}
+107 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading