Skip to content
Snippets Groups Projects
Commit 1ad70af5 authored by Dan Ballard's avatar Dan Ballard Committed by Pier Angelo Vendrame
Browse files

Bug 43505 [android]: Add 2025 UX Survey Campaign

parent 8bfcf069
Branches
Tags
1 merge request!1462BB/TB 43584: Rebased stable again onto 128.9.0esr build2
Pipeline #263588 passed
......@@ -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=${locale}",
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))
}
}
}
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) ?: ""
}
}
......@@ -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,
)
}
<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
......@@ -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"
......
......@@ -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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment