Dan Ballard pushed to branch tor-browser-128.8.0esr-14.0-1 at The Tor Project / Applications / Tor Browser
Commits: b27b9530 by Dan Ballard at 2025-03-24T15:29:54-07:00 Bug 43505 [android]: Add 2025 UX Survey Campaign
- - - - -
6 changed files:
- mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt - + mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/CampaignStrings.kt - mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt - + mobile/android/fenix/app/src/main/res/drawable/campaign_hand.xml - mobile/android/fenix/app/src/main/res/layout/fragment_home.xml - mobile/android/fenix/app/src/main/res/values/preference_keys.xml
Changes:
===================================== mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt ===================================== @@ -12,33 +12,65 @@ import android.content.res.Configuration import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.activity.result.ActivityResultLauncher import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +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.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.getColor -import androidx.core.view.children -import androidx.core.view.doOnLayout import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -108,6 +140,7 @@ import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.components import org.mozilla.fenix.components.menu.MenuAccessPoint import org.mozilla.fenix.components.toolbar.IncompleteRedesignToolbarFeature import org.mozilla.fenix.components.toolbar.ToolbarPosition @@ -151,7 +184,6 @@ import org.mozilla.fenix.messaging.DefaultMessageController import org.mozilla.fenix.messaging.FenixMessageSurfaceId import org.mozilla.fenix.messaging.MessagingFeature import org.mozilla.fenix.microsurvey.ui.MicrosurveyRequestPrompt -import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController import org.mozilla.fenix.search.toolbar.SearchSelectorMenu @@ -160,12 +192,16 @@ import org.mozilla.fenix.tabstray.TabsTrayAccessPoint import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.tor.TorBootstrapFragmentDirections import org.mozilla.fenix.tor.TorBootstrapStatus +import org.mozilla.fenix.tor.CampaignStrings import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.wallpapers.Wallpaper import java.lang.ref.WeakReference import org.mozilla.fenix.GleanMetrics.TabStrip as TabStripMetrics
+import java.text.SimpleDateFormat +import java.util.Date + @Suppress("TooManyFunctions", "LargeClass") class HomeFragment : Fragment(), UserInteractionHandler { private val args by navArgs<HomeFragmentArgs>() @@ -513,6 +549,8 @@ class HomeFragment : Fragment(), UserInteractionHandler {
activity.themeManager.applyStatusBarTheme(activity)
+ tryShowUX2025Survey() + // FxNimbus.features.homescreen.recordExposure()
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL! @@ -524,6 +562,31 @@ class HomeFragment : Fragment(), UserInteractionHandler { return binding.root }
+ private fun tryShowUX2025Survey() { + val allowedLocales = arrayListOf("en", "es", "ru", "fr", "pt") + val locale = CampaignStrings.getLocale() + + val dateFormat = SimpleDateFormat("yyyy-MM-dd-hh-zzz") + val startDate = dateFormat.parse("2025-04-14-12-UTC") + + val endDate = dateFormat.parse("2025-04-28-00-UTC") + val currentDate = Date() + + if (currentDate.before(startDate) || currentDate.after(endDate)) { + return // comment out to test + } + + if (allowedLocales.contains(locale) && !requireContext().settings().hideCampaign) { + binding.onionPatternImage.visibility = View.GONE + binding.campaignBox.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + CampaignBox() + } + } + } + } + private fun reinitializeNavBar() { initializeNavBar(activity = requireActivity() as HomeActivity) } @@ -1402,4 +1465,213 @@ class HomeFragment : Fragment(), UserInteractionHandler { requireActivity().finish() return true } + + @Composable + fun CampaignBox() { + BoxWithConstraints( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + val alternateLayout = this.maxWidth >= 500.dp + + CampaignLayout( + alternateLayout, + maxWidth = this.maxWidth, + modifier = Modifier + .padding(top = if (alternateLayout) 65.dp else 55.dp, bottom = 56.dp), + ) + } + } + + @Composable + private fun CampaignLayout( + alternateLayout: Boolean, + maxWidth: Dp, + modifier: Modifier + ) { + Column( + modifier = modifier + .padding(horizontal = 22.dp) + .verticalScroll(rememberScrollState()) + .fillMaxWidth(getVariableWidth(maxWidth)), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PurpleBox(alternateLayout) + } + } + + private fun getVariableWidth(width: Dp): Float = (500.dp / width).coerceIn(0.75f, 1.0f) + + @Composable + private fun PurpleBox( + alternateLayout: Boolean, + ) { + Box( + modifier = Modifier.background(PhotonColors.Violet90, shape = RoundedCornerShape(8.dp)) + .padding(16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + Emoji() + Spacer(Modifier.weight(1f)) + ExitIcon() + } + DynamicCampaignContent(alternateLayout) + } + } + + @Composable + private fun Emoji() { + val alpha38Violet40 = Color(PhotonColors.Violet40.red, PhotonColors.Violet40.green, PhotonColors.Violet40.blue, 0.38f) + Image( + painter = painterResource(id = R.drawable.campaign_hand), + contentDescription = null, + modifier = Modifier + .size(64.dp) + .padding(16.dp) + .drawBehind { + drawCircle( + color = alpha38Violet40, + radius = this.size.maxDimension + ) + } + ) + } + + @Composable + private fun ExitIcon() { + IconButton( + onClick = { + binding.campaignBox.visibility = View.GONE + binding.onionPatternImage.visibility = View.VISIBLE + context?.components?.settings?.hideCampaign = true + }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + tint = Color( + getColor( + requireContext(), + R.color.photonWhite, + ), + ), + contentDescription = CampaignStrings.get(CampaignStrings.CloseKey), + modifier = Modifier + .size(48.dp) + .padding(8.dp) + ) + } + } + + + @Composable + private fun DynamicCampaignContent( + alternateLayout: Boolean + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column( + modifier = Modifier.fillMaxWidth() + .padding( top = 88.dp), + horizontalAlignment = Alignment.Start, + ) { + TitleText() + MainText() + + if (alternateLayout) { + Row(modifier = Modifier.fillMaxWidth()) { + Button1(alternateLayout) + Button2() + } + } else { + Button1(alternateLayout) + Button2() + } + + } + } + } + + @Composable + private fun TitleText() { + + Text(text = CampaignStrings.get(CampaignStrings.HeaderKey), + color = PhotonColors.LightGrey05, + textAlign = TextAlign.Left, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 34.sp, + modifier = Modifier.padding(bottom = 16.dp) + ) + } + + @Composable + private fun MainText() { + + Text(text = CampaignStrings.get(CampaignStrings.BodyKey), + modifier = Modifier + .fillMaxWidth() + .padding( + start = 0.dp, + end = 0.dp, + bottom = 18.dp, + ), + color = PhotonColors.LightGrey05, + fontSize = 18.sp, + textAlign = TextAlign.Left, + ) + } + + @Composable + private fun Button1(alternateLayout: Boolean) { + Button( + onClick = { + var locale = CampaignStrings.getLocale() + if (locale == "pt") { + locale = "pt-BR" + } + (activity as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = "https://survey.torproject.org/index.php/923269?lang=$%7Blocale%7D", + newTab = true, + from = BrowserDirection.FromHome, + ) + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = PhotonColors.Violet60), + shape = RoundedCornerShape(4.dp), + modifier = Modifier.padding(0.dp) + .fillMaxWidth(fraction = if (alternateLayout) 0.5f else 1f), + + ) { + Text(text = CampaignStrings.get(CampaignStrings.CTAKey), + color = PhotonColors.LightGrey05, + textAlign = TextAlign.Center, + fontSize = 18.sp, + modifier = Modifier.padding(8.dp)) + } + } + + @Composable + private fun Button2() { + Button( + onClick = { + binding.campaignBox.visibility = View.GONE + binding.onionPatternImage.visibility = View.VISIBLE + context?.components?.settings?.hideCampaign = true + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = PhotonColors.Violet90), + shape = RoundedCornerShape(4.dp), + modifier = Modifier.padding(0.dp) + .fillMaxWidth() + ) { + Text(text = CampaignStrings.get(CampaignStrings.DismissKey), + color = PhotonColors.Violet20, + textAlign = TextAlign.Center, + fontSize = 18.sp, + modifier = Modifier.padding(8.dp)) + } + } }
===================================== mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/CampaignStrings.kt ===================================== @@ -0,0 +1,64 @@ +package org.mozilla.fenix.tor + +import java.util.Locale + +object CampaignStrings { + + val HeaderKey = "key_header" + val BodyKey = "key_body" + val CTAKey = "key_cta" + val DismissKey = "key_dismiss" + val CloseKey = "key_close" + + private val translations: HashMap<String, HashMap<String, String>> = hashMapOf( + "en" to hashMapOf( + HeaderKey to "We’d love your feedback", + BodyKey to "Help us improve Tor Browser by completing this 10-minute survey.", + CTAKey to "Launch the survey", + DismissKey to "Dismiss", + CloseKey to "Close", + ), + "es" to hashMapOf( + HeaderKey to "Danos tu opinión", + BodyKey to "Ayúdanos a mejorar el Navegador Tor completando esta encuesta de 10 minutos.", + CTAKey to "Iniciar la encuesta", + DismissKey to "Descartar", + CloseKey to "Cerrar", + ), + "ru" to hashMapOf( + HeaderKey to "Мы будем рады вашим отзывам", + BodyKey to "Помогите нам улучшить браузер Tor, пройдя 10-минутный опрос.", + CTAKey to "Начать опрос", + DismissKey to "Отклонить", + CloseKey to "Закрыть", + ), + "fr" to hashMapOf( + HeaderKey to "Nous serions ravis d’avoir votre avis !", + BodyKey to "Aidez-nous à améliorer le navigateur Tor en répondant à cette enquête de 10 minutes.", + CTAKey to "Lancer l'enquête", + DismissKey to "Ignorer", + CloseKey to "Fermer", + ), + "pt" to hashMapOf( + HeaderKey to "Adoraríamos ouvir sua opinião", + BodyKey to "Ajude-nos a melhorar o Navegador Tor respondendo a esta pesquisa de 10 minutos.", + CTAKey to "Iniciar a pesquisa", + DismissKey to "Dispensar", + CloseKey to "Fechar" + ), + ) + + fun getLocale(): String { + // TODO: do we care about spoofEnglish setting? + return Locale.getDefault().getLanguage(); + } + + + fun get(key: String): String { + val localeStrings = translations.get(getLocale()) + if (localeStrings == null) { + return "" + } + return localeStrings.get(key) ?: "" + } +}
===================================== mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/Settings.kt ===================================== @@ -43,15 +43,7 @@ import org.mozilla.fenix.components.settings.lazyFeatureFlagPreference import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey -import org.mozilla.fenix.nimbus.CookieBannersSection import org.mozilla.fenix.nimbus.FxNimbus -import org.mozilla.fenix.nimbus.HomeScreenSection -import org.mozilla.fenix.nimbus.Mr2022Section -import org.mozilla.fenix.nimbus.QueryParameterStrippingSection -import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING -import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING_ALLOW_LIST -import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING_PMB -import org.mozilla.fenix.nimbus.QueryParameterStrippingSection.QUERY_PARAMETER_STRIPPING_STRIP_LIST import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu @@ -2111,4 +2103,9 @@ class Settings(private val appContext: Context) : PreferencesHolder { appContext.getPreferenceKey(R.string.pref_key_use_html_connection_ui), default = false, ) + + var hideCampaign by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_hide_campaign_2025_ux_survey), + default = false, + ) }
===================================== mobile/android/fenix/app/src/main/res/drawable/campaign_hand.xml ===================================== @@ -0,0 +1,21 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" + android:viewportWidth="36" + android:viewportHeight="36" + android:width="36dp" + android:height="36dp"> + <path + android:pathData="M4.861 9.147c0.94 -0.657 2.357 -0.531 3.201 0.166l-0.968 -1.407c-0.779 -1.111 -0.5 -2.313 0.612 -3.093 1.112 -0.777 4.263 1.312 4.263 1.312 -0.786 -1.122 -0.639 -2.544 0.483 -3.331 1.122 -0.784 2.67 -0.513 3.456 0.611l10.42 14.72L25 31l-11.083 -4.042L4.25 12.625c-0.793 -1.129 -0.519 -2.686 0.611 -3.478z" + android:fillColor="#EF9645" /> + <path + android:pathData="M2.695 17.336s-1.132 -1.65 0.519 -2.781c1.649 -1.131 2.78 0.518 2.78 0.518l5.251 7.658c0.181 -0.302 0.379 -0.6 0.6 -0.894L4.557 11.21s-1.131 -1.649 0.519 -2.78c1.649 -1.131 2.78 0.518 2.78 0.518l6.855 9.997c0.255 -0.208 0.516 -0.417 0.785 -0.622L7.549 6.732s-1.131 -1.649 0.519 -2.78c1.649 -1.131 2.78 0.518 2.78 0.518l7.947 11.589c0.292 -0.179 0.581 -0.334 0.871 -0.498L12.238 4.729s-1.131 -1.649 0.518 -2.78c1.649 -1.131 2.78 0.518 2.78 0.518l7.854 11.454 1.194 1.742c-4.948 3.394 -5.419 9.779 -2.592 13.902 0.565 0.825 1.39 0.26 1.39 0.26 -3.393 -4.949 -2.357 -10.51 2.592 -13.903L24.515 8.62s-0.545 -1.924 1.378 -2.47c1.924 -0.545 2.47 1.379 2.47 1.379l1.685 5.004c0.668 1.984 1.379 3.961 2.32 5.831 2.657 5.28 1.07 11.842 -3.94 15.279 -5.465 3.747 -12.936 2.354 -16.684 -3.11L2.695 17.336z" + android:fillColor="#FFDC5D" /> + <path + android:pathData="M12 32.042C8 32.042 3.958 28 3.958 24c0 -0.553 -0.405 -1 -0.958 -1s-1.042 0.447 -1.042 1C1.958 30 6 34.042 12 34.042c0.553 0 1 -0.489 1 -1.042s-0.447 -0.958 -1 -0.958z" + android:fillColor="#5DADEC" /> + <path + android:pathData="M7 34c-3 0 -5 -2 -5 -5 0 -0.553 -0.447 -1 -1 -1s-1 0.447 -1 1c0 4 3 7 7 7 0.553 0 1 -0.447 1 -1s-0.447 -1 -1 -1zM24 2c-0.552 0 -1 0.448 -1 1s0.448 1 1 1c4 0 8 3.589 8 8 0 0.552 0.448 1 1 1s1 -0.448 1 -1c0 -5.514 -4 -10 -10 -10z" + android:fillColor="#5DADEC" /> + <path + android:pathData="M29 0.042c-0.552 0 -1 0.406 -1 0.958s0.448 1.042 1 1.042c3 0 4.958 2.225 4.958 4.958 0 0.552 0.489 1 1.042 1s0.958 -0.448 0.958 -1C35.958 3.163 33 0.042 29 0.042z" + android:fillColor="#5DADEC" /> +</vector> \ No newline at end of file
===================================== mobile/android/fenix/app/src/main/res/layout/fragment_home.xml ===================================== @@ -134,6 +134,12 @@
</com.google.android.material.appbar.AppBarLayout>
+ <androidx.compose.ui.platform.ComposeView + android:id="@+id/campaignBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center"/> + <androidx.recyclerview.widget.RecyclerView android:id="@+id/sessionControlRecyclerView" android:layout_width="match_parent"
===================================== mobile/android/fenix/app/src/main/res/values/preference_keys.xml ===================================== @@ -431,4 +431,5 @@ <string name="pref_key_tor_network_settings_bridges_enabled">pref_key_tor_network_settings_bridges_enabled</string>
<string name="pref_key_spoof_english" translatable="false">pref_key_spoof_english</string> + <string name="pref_key_hide_campaign_2025_ux_survey" translatable="false">pref_key_hide_campaign_2025_ux_survey_test</string> </resources>
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/b27b9530...
tor-commits@lists.torproject.org