Files
IronFox/patches/ironfox-onboarding.patch
celenity 806373ac2a v141.0
Signed-off-by: celenity <celenity@celenity.dev>
2025-07-22 18:50:57 -04:00

1532 lines
60 KiB
Diff

diff --git a/mobile/android/fenix/app/onboarding.fml.yaml b/mobile/android/fenix/app/onboarding.fml.yaml
index eb51cfe835..b233fd9291 100644
--- a/mobile/android/fenix/app/onboarding.fml.yaml
+++ b/mobile/android/fenix/app/onboarding.fml.yaml
@@ -120,6 +120,43 @@ features:
body-line-one-text: onboarding_marketing_learn_more
body-line-one-link-text: onboarding_marketing_learn_more
body-line-two-text: onboarding_marketing_opt_in_checkbox
+
+ if-preference-doh:
+ card-type: if-preference-doh
+ enabled: true
+ title: onboarding_if_preferences_title
+ body: onboarding_if_preference_doh_caption
+ image-res: ic_launcher_foreground
+ ordering: 10000
+ primary-button-label: onboarding_save_and_continue_button
+
+ if-preferences:
+ card-type: if-preferences
+ enabled: true
+ title: onboarding_if_preferences_title
+ body: onboarding_if_preferences_description
+ image-res: ic_launcher_foreground
+ ordering: 10005
+ primary-button-label: onboarding_save_and_start_button
+ extra-data:
+ if-preferences-data:
+ - switch-type: js-jit
+ switch-label: preference_jit_enabled
+ switch-caption: preference_jit_enabled_caption
+ switch-description: preference_jit_enabled_description
+ - switch-type: safe-browsing
+ switch-label: preference_safe_browsing_enabled
+ switch-caption: preference_safe_browsing_enabled_caption
+ switch-description: preference_safe_browsing_enabled_description
+ - switch-type: spoof-english
+ switch-label: tor_spoof_english_title
+ switch-caption: tor_spoof_english_caption
+ switch-description: tor_spoof_english_description
+ - switch-type: install-ublock
+ switch-label: onboarding_install_ublock_title
+ switch-caption: onboarding_install_ublock_caption
+ switch-description: onboarding_install_ublock_description
+
defaults:
- channel: developer
value:
@@ -318,6 +355,12 @@ objects:
An optional marketing data for the onboarding card.
default: null
+ if-preferences-data:
+ type: List<IfPreferencesData>
+ description: >
+ A list of preferences for the IronFox preferences onboarding card.
+ default: [ ]
+
CustomizationToolbarData:
description: An object to describe the placement of the toolbar.
fields:
@@ -416,6 +459,27 @@ objects:
description: The text for line two of the body.
default: ""
+ IfPreferencesData:
+ description: An object to describe the IronFox preferences onboarding card.
+ fields:
+ switch-label:
+ type: Text
+ description: The text for the preference switch.
+ default: ""
+ switch-caption:
+ type: Text
+ description: The text for the brief caption of the switch.
+ default: ""
+ switch-description:
+ type: Text
+ description: The text for the long description of the switch.
+ default: ""
+ switch-type:
+ type: IfOnboardingPreferenceType
+ description: The type of preference.
+ # This should never be defaulted
+ default: default
+
enums:
OnboardingCardType:
@@ -437,6 +501,11 @@ enums:
description: Page to display the terms of services.
marketing-data:
description: Allows user to opt out of marketing data collection.
+ if-preference-doh:
+ description: Allows user to configure DNS-over-HTTPS.
+ if-preferences:
+ description: Allows user to configure IronFox-specific preferences.
+
ToolbarType:
description: An enum to describe a toolbar placement option.
@@ -455,3 +524,17 @@ enums:
description: Sets the theme to light mode.
theme-dark:
description: Sets the theme to dark mode.
+
+ IfOnboardingPreferenceType:
+ description: An enum to describe an IronFox preference option.
+ variants:
+ default:
+ description: Default value for preference type. NEVER use this.
+ js-jit:
+ description: Whether to enable JavaScript JIT.
+ safe-browsing:
+ description: Whether to enable safe browsing.
+ spoof-english:
+ description: Whether to request English versions of web pages for enhanced privacy.
+ install-ublock:
+ description: Whether to install uBlock Origin.
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingTelemetryRecorder.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingTelemetryRecorder.kt
index effc671c02..ac7cbafb7d 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingTelemetryRecorder.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingTelemetryRecorder.kt
@@ -132,6 +132,8 @@ class OnboardingTelemetryRecorder {
),
)
}
+ OnboardingPageUiData.Type.IF_PREFERENCE_DOH -> {}
+ OnboardingPageUiData.Type.IF_PREFERENCES -> {}
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/IronFoxPreferenceDohOnboardingPage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/IronFoxPreferenceDohOnboardingPage.kt
new file mode 100644
index 0000000000..3a509acade
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/IronFoxPreferenceDohOnboardingPage.kt
@@ -0,0 +1,501 @@
+package org.mozilla.fenix.onboarding.view
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.BoxWithConstraintsScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ExposedDropdownMenuBox
+import androidx.compose.material.ExposedDropdownMenuDefaults
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.RadioButton
+import androidx.compose.material.RadioButtonDefaults
+import androidx.compose.material.Text
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import mozilla.components.compose.base.button.PrimaryButton
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.settings.doh.CustomProviderErrorState
+import org.mozilla.fenix.settings.doh.DefaultDohSettingsProvider
+import org.mozilla.fenix.settings.doh.DohSettingsState
+import org.mozilla.fenix.settings.doh.DohUrlValidator
+import org.mozilla.fenix.settings.doh.ProtectionLevel
+import org.mozilla.fenix.settings.doh.Provider
+import org.mozilla.fenix.settings.doh.UrlValidationException
+import org.mozilla.fenix.settings.doh.root.AlertDialogAddCustomProvider
+import org.mozilla.fenix.theme.FirefoxTheme
+
+/**
+ * The default ratio of the image height to the parent height.
+ */
+private const val IMAGE_HEIGHT_RATIO_DEFAULT = 0.2f
+
+/**
+ * The ratio of the image height to the parent height for medium sized devices.
+ */
+private const val IMAGE_HEIGHT_RATIO_MEDIUM = 0.1f
+
+/**
+ * The ratio of the image height to the parent height for small devices.
+ */
+private const val IMAGE_HEIGHT_RATIO_SMALL = 0.05f
+
+private sealed interface IfPreferenceDohContentState {
+ data object ModeSelection : IfPreferenceDohContentState
+ data object ProviderSelection : IfPreferenceDohContentState
+}
+
+@Composable
+fun IronFoxPreferenceDohOnboardingPage(
+ pageState: OnboardingPageState,
+) {
+ BoxWithConstraints {
+ val boxWithConstraintsScope = this
+ Column(
+ modifier = Modifier
+ .background(FirefoxTheme.colors.layer1)
+ .padding(horizontal = 16.dp, vertical = 24.dp)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ with(pageState) {
+ Spacer(Modifier)
+
+ val size = remember { mainImageHeight(boxWithConstraintsScope) }
+
+ Image(
+ painter = painterResource(id = imageRes),
+ contentDescription = "",
+ modifier = Modifier.size(size),
+ )
+
+ Spacer(Modifier.height(16.dp))
+
+ Column(
+ modifier = Modifier.padding(vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = title,
+ color = FirefoxTheme.colors.textPrimary,
+ textAlign = TextAlign.Center,
+ style = FirefoxTheme.typography.headline5,
+ )
+
+ Text(
+ text = description,
+ color = FirefoxTheme.colors.textPrimary,
+ textAlign = TextAlign.Center,
+ style = FirefoxTheme.typography.body2,
+ )
+ }
+
+ Spacer(Modifier.height(32.dp))
+
+ var contentState by remember {
+ mutableStateOf<IfPreferenceDohContentState>(
+ IfPreferenceDohContentState.ModeSelection,
+ )
+ }
+
+ val updateContentState = remember {
+ { newState: IfPreferenceDohContentState ->
+ contentState = newState
+ }
+ }
+
+ BackHandler(contentState != IfPreferenceDohContentState.ModeSelection) {
+ when (contentState) {
+ IfPreferenceDohContentState.ModeSelection -> {}
+ IfPreferenceDohContentState.ProviderSelection -> {
+ contentState = IfPreferenceDohContentState.ModeSelection
+ }
+ }
+ }
+
+ val context = LocalContext.current
+ val dohSettingsProvider = remember(context) {
+ DefaultDohSettingsProvider(
+ engine = context.components.core.engine,
+ settings = context.settings(),
+ )
+ }
+
+ var dohSettingsState by remember(dohSettingsProvider) {
+ mutableStateOf(
+ DohSettingsState(
+ allProtectionLevels = listOf(
+ ProtectionLevel.Default,
+ ProtectionLevel.Increased,
+ ProtectionLevel.Max,
+ ),
+ selectedProtectionLevel = dohSettingsProvider.getSelectedProtectionLevel(),
+ providers = dohSettingsProvider.getDefaultProviders(),
+ selectedProvider = dohSettingsProvider.getSelectedProvider(),
+ ),
+ )
+ }
+
+ val updateDohSettingsState = remember {
+ { newState: DohSettingsState ->
+ dohSettingsState = newState
+ }
+ }
+
+ val captionText = when (contentState) {
+ IfPreferenceDohContentState.ModeSelection -> stringResource(
+ when (dohSettingsState.selectedProtectionLevel) {
+ ProtectionLevel.Default -> R.string.preference_doh_default_protection_summary
+ ProtectionLevel.Increased -> R.string.preference_doh_increased_protection_summary
+ ProtectionLevel.Max -> R.string.preference_doh_max_protection_summary
+ ProtectionLevel.Off -> R.string.onboarding_if_preference_doh_off_summary
+ },
+ stringResource(R.string.app_name),
+ )
+
+ IfPreferenceDohContentState.ProviderSelection ->
+ when (val provider = dohSettingsState.selectedProvider) {
+ is Provider.BuiltIn -> provider.url
+ is Provider.Custom -> provider.url
+ null -> ""
+ }
+ }
+
+ AnimatedContent(
+ targetState = contentState,
+ contentAlignment = Alignment.Center,
+ ) { currentState ->
+ when (currentState) {
+ IfPreferenceDohContentState.ModeSelection -> {
+ IronFoxPreferenceDoHModeSelection(
+ state = dohSettingsState,
+ onUpdateState = updateDohSettingsState,
+ )
+ }
+
+ IfPreferenceDohContentState.ProviderSelection ->
+ IronFoxPreferenceDoHProviderSelection(
+ state = dohSettingsState,
+ dohSettingsProvider = dohSettingsProvider,
+ onUpdateState = updateDohSettingsState,
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = captionText,
+ style = FirefoxTheme.typography.caption,
+ color = FirefoxTheme.colors.textSecondary,
+ textAlign = TextAlign.Center,
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ PrimaryButton(
+ text = primaryButton.text,
+ modifier = Modifier
+ .width(width = FirefoxTheme.layout.size.maxWidth.small)
+ .semantics {
+ testTag = title + "onboarding_card.positive_button"
+ },
+ onClick = {
+ applyDohSettings(
+ contentState = contentState,
+ onContentStateChange = updateContentState,
+ state = dohSettingsState,
+ dohSettingsProvider = dohSettingsProvider,
+ )
+ },
+ )
+ }
+ }
+ }
+}
+
+private fun OnboardingPageState.applyDohSettings(
+ contentState: IfPreferenceDohContentState,
+ onContentStateChange: (IfPreferenceDohContentState) -> Unit,
+ state: DohSettingsState,
+ dohSettingsProvider: DefaultDohSettingsProvider,
+) {
+ when (contentState) {
+ IfPreferenceDohContentState.ModeSelection -> {
+ if (state.selectedProtectionLevel is ProtectionLevel.Increased
+ || state.selectedProtectionLevel is ProtectionLevel.Max
+ ) {
+ onContentStateChange(IfPreferenceDohContentState.ProviderSelection)
+ } else {
+ dohSettingsProvider.setProtectionLevel(
+ state.selectedProtectionLevel,
+ state.selectedProvider,
+ )
+ primaryButton.onClick()
+ }
+ }
+
+ IfPreferenceDohContentState.ProviderSelection -> {
+ // apply settings
+ dohSettingsProvider.setProtectionLevel(
+ state.selectedProtectionLevel,
+ state.selectedProvider,
+ )
+
+ // set custom provider, if any
+ (state.selectedProvider as? Provider.Custom?)?.also { provider ->
+ dohSettingsProvider.setCustomProvider(provider.url)
+ }
+
+ // then proceed to the next page
+ primaryButton.onClick()
+ }
+ }
+}
+
+@Suppress("UnusedReceiverParameter")
+@Composable
+private fun ColumnScope.IronFoxPreferenceDoHModeSelection(
+ state: DohSettingsState,
+ modifier: Modifier = Modifier,
+ onUpdateState: (DohSettingsState) -> Unit,
+) {
+ val onSelect = remember {
+ { newLevel: ProtectionLevel ->
+ onUpdateState(state.copy(selectedProtectionLevel = newLevel))
+ }
+ }
+
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Column(
+ modifier = Modifier.width(IntrinsicSize.Max),
+ ) {
+ state.allProtectionLevels.forEach { level ->
+ val selected = remember { level == state.selectedProtectionLevel }
+ ModeSelectionRadioButton(
+ selected = selected,
+ onSelect = onSelect,
+ level = level,
+ state = state,
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ModeSelectionRadioButton(
+ selected: Boolean,
+ onSelect: (ProtectionLevel) -> Unit,
+ level: ProtectionLevel,
+ state: DohSettingsState,
+) {
+ Row(
+ modifier = Modifier
+ .height(48.dp)
+ .selectable(
+ selected = selected,
+ enabled = true,
+ onClick = { onSelect(level) },
+ )
+ .padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ RadioButton(
+ selected = state.selectedProtectionLevel == level,
+ onClick = null,
+ colors = RadioButtonDefaults.colors(selectedColor = FirefoxTheme.colors.actionPrimary),
+ )
+
+ Text(
+ text = stringResource(
+ when (level) {
+ ProtectionLevel.Default -> R.string.preference_doh_default_protection
+ ProtectionLevel.Increased -> R.string.preference_doh_increased_protection
+ ProtectionLevel.Max -> R.string.preference_doh_max_protection
+ ProtectionLevel.Off -> R.string.onboarding_if_preference_doh_off_summary
+ },
+ ),
+ style = FirefoxTheme.typography.body1,
+ color = FirefoxTheme.colors.textPrimary,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ }
+}
+
+@Suppress("UnusedReceiverParameter")
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun ColumnScope.IronFoxPreferenceDoHProviderSelection(
+ state: DohSettingsState,
+ dohSettingsProvider: DefaultDohSettingsProvider,
+ modifier: Modifier = Modifier,
+ onUpdateState: (DohSettingsState) -> Unit,
+) {
+ var expanded by remember {
+ mutableStateOf(false)
+ }
+
+ val setProvider = remember {
+ { newProvider: Provider, showCustomProviderDialog: Boolean ->
+ onUpdateState(
+ state.copy(
+ selectedProvider = newProvider,
+ isCustomProviderDialogOn = showCustomProviderDialog,
+ ),
+ )
+ }
+ }
+
+ val providerName: (@Composable (Provider) -> String) = remember {
+ { provider ->
+ when (provider) {
+ is Provider.BuiltIn -> provider.name
+ is Provider.Custom -> stringResource(R.string.preference_doh_provider_custom)
+ }
+ }
+ }
+
+ Column(
+ modifier = modifier,
+ ) {
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = { expanded = it },
+ ) {
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = providerName(state.selectedProvider!!),
+ onValueChange = {},
+ readOnly = true,
+ label = {
+ Text(
+ text = stringResource(R.string.preference_doh_choose_provider),
+ modifier = Modifier.padding(top = 14.dp)
+ )
+ },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ colors = TextFieldDefaults.outlinedTextFieldColors(
+ backgroundColor = MaterialTheme.colors.surface,
+ textColor = MaterialTheme.colors.onSurface,
+ ),
+ )
+
+ ExposedDropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ ) {
+ state.providers.forEach { provider ->
+ DropdownMenuItem(
+ onClick = {
+ setProvider(provider, provider is Provider.Custom)
+ expanded = false
+ },
+ ) {
+ Text(
+ text = providerName(provider),
+ style = FirefoxTheme.typography.body1,
+ color = MaterialTheme.colors.onSurface,
+ )
+ }
+ }
+ }
+ }
+
+ if (state.selectedProvider is Provider.Custom && state.isCustomProviderDialogOn) {
+ AlertDialogAddCustomProvider(
+ customProviderErrorState = state.customProviderErrorState,
+ onCustomCancelClicked = {
+
+ // user cancelled the dialog, reset provider to default
+ onUpdateState(
+ state.copy(
+ selectedProvider = dohSettingsProvider.getDefaultProviders().first(),
+ ),
+ )
+ },
+ onCustomAddClicked = { input ->
+ try {
+ val validUrl = DohUrlValidator.validate(input)
+ onUpdateState(
+ state.copy(
+ customProviderErrorState = CustomProviderErrorState.Valid,
+ selectedProvider = Provider.Custom(validUrl),
+ isCustomProviderDialogOn = false,
+ ),
+ )
+ } catch (e: UrlValidationException.NonHttpsUrlException) {
+ onUpdateState(
+ state.copy(
+ customProviderErrorState = CustomProviderErrorState.NonHttps,
+ ),
+ )
+ } catch (e: UrlValidationException.InvalidUrlException) {
+ onUpdateState(
+ state.copy(
+ customProviderErrorState = CustomProviderErrorState.Invalid,
+ ),
+ )
+ }
+ },
+ )
+ }
+ }
+}
+
+private fun mainImageHeight(boxWithConstraintsScope: BoxWithConstraintsScope): Dp {
+ val imageHeightRatio: Float = when {
+ boxWithConstraintsScope.maxHeight <= ONBOARDING_SMALL_DEVICE -> IMAGE_HEIGHT_RATIO_SMALL
+ boxWithConstraintsScope.maxHeight <= ONBOARDING_MEDIUM_DEVICE -> IMAGE_HEIGHT_RATIO_MEDIUM
+ else -> IMAGE_HEIGHT_RATIO_DEFAULT
+ }
+ return boxWithConstraintsScope.maxHeight.times(imageHeightRatio)
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/IronFoxPreferencesPage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/IronFoxPreferencesPage.kt
new file mode 100644
index 0000000000..977b45f5c0
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/IronFoxPreferencesPage.kt
@@ -0,0 +1,458 @@
+package org.mozilla.fenix.onboarding.view
+
+import android.content.Context
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.BoxWithConstraintsScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.LinearProgressIndicator
+import androidx.compose.material.Switch
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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.res.stringResource
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTag
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import mozilla.components.compose.base.button.PrimaryButton
+import mozilla.components.support.base.log.logger.Logger
+import org.mozilla.fenix.R
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.onboarding.view.IfPreferencesContentState.Configuration
+import org.mozilla.fenix.onboarding.view.IfPreferencesContentState.Error
+import org.mozilla.fenix.onboarding.view.IfPreferencesContentState.Progress
+import org.mozilla.fenix.theme.FirefoxTheme
+import org.mozilla.fenix.utils.IronFoxAddons
+import org.mozilla.fenix.utils.IronFoxPreferences
+
+/**
+ * The default ratio of the image height to the parent height.
+ */
+private const val IMAGE_HEIGHT_RATIO_DEFAULT = 0.2f
+
+/**
+ * The ratio of the image height to the parent height for medium sized devices.
+ */
+private const val IMAGE_HEIGHT_RATIO_MEDIUM = 0.15f
+
+/**
+ * The ratio of the image height to the parent height for small devices.
+ */
+private const val IMAGE_HEIGHT_RATIO_SMALL = 0.1f
+
+private sealed interface IfPreferencesContentState {
+
+ /**
+ * Show IronFox preference switches.
+ */
+ data class Configuration(
+ val optionIndex: Int,
+ ) : IfPreferencesContentState
+
+ /**
+ * Show a progress bar with a message.
+ */
+ data class Progress(val message: String) : IfPreferencesContentState
+
+ /**
+ * Show an error message.
+ */
+ data class Error(val message: String) : IfPreferencesContentState
+}
+
+private data class IfPreferencesSwitchStates(
+ val jitEnabled: Boolean = false,
+ val safeBrowsingEnabled: Boolean = true,
+ val spoofEnglish: Boolean = true,
+ val installUBlock: Boolean = true,
+)
+
+private val logger = Logger("IronFoxOnboardingPreferences")
+
+@Composable
+fun IronFoxPreferencesOnboardingPage(
+ pageState: OnboardingPageState,
+) {
+ BoxWithConstraints {
+ val boxWithConstraintsScope = this
+ Column(
+ modifier = Modifier
+ .background(FirefoxTheme.colors.layer1)
+ .padding(horizontal = 16.dp, vertical = 24.dp)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+
+ verticalArrangement = Arrangement.SpaceBetween,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ with(pageState) {
+ Spacer(Modifier)
+
+ Image(
+ painter = painterResource(id = imageRes),
+ contentDescription = "",
+ modifier = Modifier.height(mainImageHeight(boxWithConstraintsScope)),
+ )
+
+ Spacer(Modifier.height(16.dp))
+
+ val preferenceOptions = remember { ifPreferenceOptions!! }
+ var currentPreferenceIndex by remember { mutableIntStateOf(0) }
+
+ var contentState by remember {
+ mutableStateOf<IfPreferencesContentState>(
+ Configuration(currentPreferenceIndex),
+ )
+ }
+
+ var switchStates by remember {
+ mutableStateOf(IfPreferencesSwitchStates())
+ }
+
+ val context = LocalContext.current
+ val onContentStateChange = remember {
+ { newState: IfPreferencesContentState ->
+ contentState = newState
+ }
+ }
+
+ val applyPreference: suspend (IfPreferenceOption) -> Unit =
+ remember(context, switchStates, primaryButton, onContentStateChange) {
+ { option ->
+ applyPreference(
+ context,
+ option,
+ switchStates,
+ onContentStateChange,
+ )
+ }
+ }
+
+ val applyAction: () -> Unit = remember(
+ applyPreference,
+ preferenceOptions,
+ currentPreferenceIndex,
+ onContentStateChange,
+ primaryButton,
+ ) {
+ {
+ CoroutineScope(Dispatchers.Default).launch {
+ applyPreference(preferenceOptions[currentPreferenceIndex])
+ if (currentPreferenceIndex < preferenceOptions.lastIndex) {
+ onContentStateChange(Configuration(++currentPreferenceIndex))
+ } else {
+ withContext(Dispatchers.Main) {
+ primaryButton.onClick()
+ }
+ }
+ }
+ }
+ }
+
+ BackHandler(currentPreferenceIndex > 0) {
+ onContentStateChange(Configuration(--currentPreferenceIndex))
+ }
+
+ (contentState as? Configuration?)?.also { configuration ->
+ Text(
+ text = title,
+ color = FirefoxTheme.colors.textPrimary,
+ textAlign = TextAlign.Center,
+ style = FirefoxTheme.typography.headline5,
+ )
+
+ Spacer(Modifier.height(8.dp))
+
+ Text(
+ text = preferenceOptions[configuration.optionIndex].caption,
+ color = FirefoxTheme.colors.textPrimary,
+ textAlign = TextAlign.Center,
+ style = FirefoxTheme.typography.body2,
+ )
+ }
+
+ Spacer(Modifier.height(32.dp))
+
+ AnimatedContent(
+ targetState = contentState,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ ) { currentState ->
+ when (currentState) {
+ is Configuration -> IronFoxPreferenceConfiguration(
+ option = preferenceOptions[currentState.optionIndex],
+ modifier = Modifier.fillMaxSize(),
+ state = switchStates,
+ onUpdateSwitchStates = { newSwitchStates ->
+ switchStates = newSwitchStates
+ },
+ )
+
+ is Progress -> IronFoxPreferencesProgress(
+ state = currentState,
+ )
+
+ is Error -> IronFoxPreferencesError(
+ state = currentState,
+ onRetry = applyAction,
+ )
+ }
+ }
+
+ if (contentState is Configuration) {
+ Spacer(modifier = Modifier.height(16.dp))
+
+ PrimaryButton(
+ text = if (preferenceOptions.lastIndex == currentPreferenceIndex) {
+ primaryButton.text
+ } else {
+ stringResource(R.string.onboarding_save_and_continue_button)
+ },
+ modifier = Modifier
+ .width(width = FirefoxTheme.layout.size.maxWidth.small)
+ .semantics {
+ testTag = title + "onboarding_card.positive_button"
+ },
+ onClick = applyAction,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun IronFoxPreferenceConfiguration(
+ option: IfPreferenceOption,
+ state: IfPreferencesSwitchStates,
+ onUpdateSwitchStates: (IfPreferencesSwitchStates) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(modifier = modifier) {
+ IronFoxPreferenceSwitchItem(
+ option = option,
+ isChecked = when (option.preferenceType) {
+ IfPreferenceType.JS_JIT -> state.jitEnabled
+ IfPreferenceType.INSTALL_UBLOCK -> state.installUBlock
+ IfPreferenceType.SAFE_BROWSING -> state.safeBrowsingEnabled
+ IfPreferenceType.SPOOF_ENGLISH -> state.spoofEnglish
+ IfPreferenceType.DEFAULT -> throw UnsupportedOperationException()
+ },
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ onPreferenceChange = { type ->
+ onUpdateSwitchStates(
+ when (type) {
+ IfPreferenceType.JS_JIT -> state.copy(jitEnabled = !state.jitEnabled)
+ IfPreferenceType.INSTALL_UBLOCK -> state.copy(installUBlock = !state.installUBlock)
+ IfPreferenceType.SAFE_BROWSING -> state.copy(safeBrowsingEnabled = !state.safeBrowsingEnabled)
+ IfPreferenceType.SPOOF_ENGLISH -> state.copy(spoofEnglish = !state.spoofEnglish)
+ IfPreferenceType.DEFAULT -> throw UnsupportedOperationException()
+ },
+ )
+ },
+ )
+
+ Spacer(modifier = Modifier.height(32.dp))
+
+ Text(
+ modifier = Modifier
+ .padding(16.dp)
+ .fillMaxWidth(),
+ text = option.description,
+ style = FirefoxTheme.typography.body2,
+ color = FirefoxTheme.colors.textSecondary,
+ )
+ }
+}
+
+private suspend fun applyPreference(
+ context: Context,
+ option: IfPreferenceOption,
+ state: IfPreferencesSwitchStates,
+ onContentStateChange: (IfPreferencesContentState) -> Unit,
+) {
+ when (option.preferenceType) {
+ IfPreferenceType.DEFAULT -> {}
+ IfPreferenceType.JS_JIT -> {
+ IronFoxPreferences.setJavascriptJitEnabled(
+ context,
+ state.jitEnabled,
+ )
+ }
+
+ IfPreferenceType.SAFE_BROWSING -> {
+ IronFoxPreferences.setSafeBrowsingEnabled(
+ context,
+ state.safeBrowsingEnabled,
+ )
+ }
+
+ IfPreferenceType.SPOOF_ENGLISH -> {
+ IronFoxPreferences.setLocaleSpoofingEnabled(
+ context,
+ state.spoofEnglish,
+ )
+ }
+
+ IfPreferenceType.INSTALL_UBLOCK -> {
+ installUBlockOrigin(context, onContentStateChange)
+ }
+ }
+}
+
+private suspend fun installUBlockOrigin(
+ context: Context,
+ onContentStateChange: (IfPreferencesContentState) -> Unit,
+) {
+ onContentStateChange(
+ Progress(
+ context.getString(R.string.onboarding_state_installing_ublock),
+ ),
+ )
+
+ val components = context.components
+ val result = IronFoxAddons.installAddon(components, IronFoxAddons.UBLOCK_ORIGIN)
+ if (result.isFailure) {
+ logger.error("Failed to install uBlock Origin", result.exceptionOrNull())
+ onContentStateChange(
+ Error(
+ context.getString(
+ R.string.onboarding_state_installing_ublock_error,
+ result.exceptionOrNull()?.message ?: "Unknown error",
+ ),
+ ),
+ )
+ }
+}
+
+@Composable
+private fun IronFoxPreferenceSwitchItem(
+ option: IfPreferenceOption,
+ isChecked: Boolean,
+ modifier: Modifier = Modifier,
+ onPreferenceChange: (IfPreferenceType) -> Unit,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(
+ onClick = {
+ onPreferenceChange(option.preferenceType)
+ },
+ )
+ .padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = option.label,
+ style = FirefoxTheme.typography.body1,
+ color = FirefoxTheme.colors.textPrimary,
+ )
+
+ Switch(
+ checked = isChecked,
+ onCheckedChange = { onPreferenceChange(option.preferenceType) },
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+}
+
+@Composable
+private fun IronFoxPreferencesProgress(
+ state: Progress,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier,
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp),
+ ) {
+ Text(
+ text = state.message,
+ style = FirefoxTheme.typography.body1,
+ color = FirefoxTheme.colors.textPrimary,
+ )
+
+ LinearProgressIndicator(
+ modifier = Modifier
+ .padding(horizontal = 32.dp)
+ .fillMaxWidth(),
+ )
+ }
+ }
+}
+
+@Composable
+private fun IronFoxPreferencesError(
+ state: Error,
+ modifier: Modifier = Modifier,
+ onRetry: () -> Unit,
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Text(
+ text = state.message,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ )
+
+ PrimaryButton(
+ text = stringResource(R.string.onboarding_action_retry),
+ modifier = Modifier
+ .width(width = FirefoxTheme.layout.size.maxWidth.small),
+ onClick = onRetry,
+ )
+ }
+}
+
+private fun mainImageHeight(boxWithConstraintsScope: BoxWithConstraintsScope): Dp {
+ val imageHeightRatio: Float = when {
+ boxWithConstraintsScope.maxHeight <= ONBOARDING_SMALL_DEVICE -> IMAGE_HEIGHT_RATIO_SMALL
+ boxWithConstraintsScope.maxHeight <= ONBOARDING_MEDIUM_DEVICE -> IMAGE_HEIGHT_RATIO_MEDIUM
+ else -> IMAGE_HEIGHT_RATIO_DEFAULT
+ }
+ return boxWithConstraintsScope.maxHeight.times(imageHeightRatio)
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingMapper.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingMapper.kt
index 795fcd7b7e..b6fd72c0dd 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingMapper.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingMapper.kt
@@ -6,12 +6,15 @@ package org.mozilla.fenix.onboarding.view
import org.mozilla.fenix.nimbus.CustomizationThemeData
import org.mozilla.fenix.nimbus.CustomizationToolbarData
+import org.mozilla.fenix.nimbus.IfOnboardingPreferenceType
+import org.mozilla.fenix.nimbus.IfPreferencesData
import org.mozilla.fenix.nimbus.MarketingData
import org.mozilla.fenix.nimbus.OnboardingCardData
import org.mozilla.fenix.nimbus.OnboardingCardType
import org.mozilla.fenix.nimbus.TermsOfServiceData
import org.mozilla.fenix.nimbus.ThemeType
import org.mozilla.fenix.nimbus.ToolbarType
+import java.lang.UnsupportedOperationException
/**
* Returns a list of all the required Nimbus 'cards' that have been converted to [OnboardingPageUiData].
@@ -114,6 +117,7 @@ private fun OnboardingCardData.toPageUiData(privacyCaption: Caption?) = Onboardi
?.toOnboardingThemeOptions(),
termsOfService = extraData?.termOfServiceData?.toOnboardingTermsOfService(),
marketingData = extraData?.marketingData?.toOnboardingMarketingData(),
+ ifPreferencesData = extraData?.ifPreferencesData?.toOnboardingIfPreferenceOptions()
)
private fun OnboardingCardType.toPageUiDataType() = when (this) {
@@ -125,6 +129,8 @@ private fun OnboardingCardType.toPageUiDataType() = when (this) {
OnboardingCardType.THEME_SELECTION -> OnboardingPageUiData.Type.THEME_SELECTION
OnboardingCardType.TERMS_OF_SERVICE -> OnboardingPageUiData.Type.TERMS_OF_SERVICE
OnboardingCardType.MARKETING_DATA -> OnboardingPageUiData.Type.MARKETING_DATA
+ OnboardingCardType.IF_PREFERENCE_DOH -> OnboardingPageUiData.Type.IF_PREFERENCE_DOH
+ OnboardingCardType.IF_PREFERENCES -> OnboardingPageUiData.Type.IF_PREFERENCES
}
private fun List<CustomizationToolbarData>.toOnboardingToolbarOptions() = map { it.toOnboardingCustomizeToolbar() }
@@ -177,6 +183,25 @@ private fun ThemeType.toThemeOptionType() = when (this) {
ThemeType.THEME_SYSTEM -> ThemeOptionType.THEME_SYSTEM
}
+private fun List<IfPreferencesData>.toOnboardingIfPreferenceOptions() = map { it.toOnboardingThemeOption() }
+
+private fun IfPreferencesData.toOnboardingThemeOption() = with(this) {
+ IfPreferenceOption(
+ label = switchLabel,
+ caption = switchCaption,
+ description = switchDescription,
+ preferenceType = switchType.toIfPreferenceType(),
+ )
+}
+
+private fun IfOnboardingPreferenceType.toIfPreferenceType() = when (this) {
+ IfOnboardingPreferenceType.JS_JIT -> IfPreferenceType.JS_JIT
+ IfOnboardingPreferenceType.SAFE_BROWSING -> IfPreferenceType.SAFE_BROWSING
+ IfOnboardingPreferenceType.SPOOF_ENGLISH -> IfPreferenceType.SPOOF_ENGLISH
+ IfOnboardingPreferenceType.INSTALL_UBLOCK -> IfPreferenceType.INSTALL_UBLOCK
+ IfOnboardingPreferenceType.DEFAULT -> throw UnsupportedOperationException("Unsupported IfOnboardingPreferenceType: default")
+}
+
/**
* Mapper to convert [OnboardingPageUiData] to [OnboardingPageState] that is a param for
* [OnboardingPage] composable.
@@ -195,6 +220,8 @@ internal fun mapToOnboardingPageState(
onCustomizeToolbarButtonClick: () -> Unit,
onCustomizeThemeClick: () -> Unit,
onTermsOfServiceButtonClick: () -> Unit,
+ onIfPreferenceDohButtonClick: () -> Unit,
+ onIfPreferencesButtonClick: () -> Unit,
onMarketingDataContinueClick: () -> Unit = {},
): OnboardingPageState = when (onboardingPageUiData.type) {
OnboardingPageUiData.Type.DEFAULT_BROWSER -> createOnboardingPageState(
@@ -244,6 +271,18 @@ internal fun mapToOnboardingPageState(
onPositiveButtonClick = onMarketingDataContinueClick,
onNegativeButtonClick = {}, // No negative button option for marketing data.
)
+
+ OnboardingPageUiData.Type.IF_PREFERENCE_DOH -> createOnboardingPageState(
+ onboardingPageUiData = onboardingPageUiData,
+ onPositiveButtonClick = onIfPreferenceDohButtonClick,
+ onNegativeButtonClick = {}, // No negative button option for IronFox preferences.
+ )
+
+ OnboardingPageUiData.Type.IF_PREFERENCES -> createOnboardingPageState(
+ onboardingPageUiData = onboardingPageUiData,
+ onPositiveButtonClick = onIfPreferencesButtonClick,
+ onNegativeButtonClick = {}, // No negative button option for IronFox preferences.
+ )
}
private fun createOnboardingPageState(
@@ -263,4 +302,5 @@ private fun createOnboardingPageState(
toolbarOptions = onboardingPageUiData.toolbarOptions,
termsOfService = onboardingPageUiData.termsOfService,
marketingData = onboardingPageUiData.marketingData,
+ ifPreferenceOptions = onboardingPageUiData.ifPreferencesData
)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt
index bed8e9959a..d3e50c8636 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageState.kt
@@ -6,6 +6,7 @@ package org.mozilla.fenix.onboarding.view
import androidx.annotation.DrawableRes
import org.mozilla.fenix.compose.LinkTextState
+import org.mozilla.fenix.nimbus.IfPreferencesData
/**
* Model containing data for [OnboardingPage].
@@ -33,6 +34,7 @@ data class OnboardingPageState(
val termsOfService: OnboardingTermsOfService? = null,
val toolbarOptions: List<ToolbarOption>? = null,
val marketingData: OnboardingMarketingData? = null,
+ val ifPreferenceOptions: List<IfPreferenceOption>? = null,
val onRecordImpressionEvent: () -> Unit = {},
)
@@ -107,6 +109,21 @@ enum class ThemeOptionType(val id: String) {
THEME_SYSTEM("theme_system"),
}
+data class IfPreferenceOption(
+ val label: String,
+ val caption: String,
+ val description: String,
+ val preferenceType: IfPreferenceType,
+)
+
+enum class IfPreferenceType(val id: String) {
+ DEFAULT("default"),
+ JS_JIT("js_jit"),
+ SAFE_BROWSING("safe_browsing"),
+ SPOOF_ENGLISH("spoof_english"),
+ INSTALL_UBLOCK("install_ublock"),
+}
+
/**
* Model containing data for the terms of service page during onboarding.
*/
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt
index 29ef99bc73..d30ae89e90 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt
@@ -22,6 +22,7 @@ data class OnboardingPageUiData(
val themeOptions: List<ThemeOption>? = null,
val termsOfService: OnboardingTermsOfService? = null,
val marketingData: OnboardingMarketingData? = null,
+ val ifPreferencesData: List<IfPreferenceOption>? = null
) {
/**
* Model for different types of Onboarding Pages.
@@ -55,6 +56,12 @@ data class OnboardingPageUiData(
MARKETING_DATA(
telemetryId = "marketing_data",
),
+ IF_PREFERENCE_DOH(
+ telemetryId = "if_preference_doh",
+ ),
+ IF_PREFERENCES(
+ telemetryId = "if_preferences",
+ ),
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingScreen.kt
index 732a80cb03..baea33e988 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingScreen.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingScreen.kt
@@ -246,6 +246,12 @@ fun OnboardingScreen(
onMarketingDataContinueClick(allowMarketingDataCollection)
scrollToNextPageOrDismiss()
},
+ onIfPreferenceDohButtonClick = {
+ scrollToNextPageOrDismiss()
+ },
+ onIfPreferencesButtonClick = {
+ scrollToNextPageOrDismiss()
+ },
onboardingStore = onboardingStore,
)
}
@@ -293,6 +299,8 @@ private fun OnboardingContent(
onMarketingOptInToggle: (optIn: Boolean) -> Unit,
onMarketingDataLearnMoreClick: () -> Unit,
onMarketingDataContinueClick: (allowMarketingDataCollection: Boolean) -> Unit,
+ onIfPreferenceDohButtonClick: () -> Unit,
+ onIfPreferencesButtonClick: () -> Unit,
) {
val nestedScrollConnection = remember { DisableForwardSwipeNestedScrollConnection(pagerState) }
@@ -322,6 +330,8 @@ private fun OnboardingContent(
onCustomizeToolbarButtonClick = onCustomizeToolbarButtonClick,
onCustomizeThemeClick = onCustomizeThemeButtonClick,
onTermsOfServiceButtonClick = onAgreeAndConfirmTermsOfService,
+ onIfPreferenceDohButtonClick = onIfPreferenceDohButtonClick,
+ onIfPreferencesButtonClick = onIfPreferencesButtonClick,
)
OnboardingPageForType(
type = pageUiState.type,
@@ -412,6 +422,14 @@ private fun OnboardingPageForType(
state,
termsOfServiceEventHandler,
)
+
+ OnboardingPageUiData.Type.IF_PREFERENCE_DOH -> IronFoxPreferenceDohOnboardingPage(
+ pageState = state,
+ )
+
+ OnboardingPageUiData.Type.IF_PREFERENCES -> IronFoxPreferencesOnboardingPage(
+ pageState = state,
+ )
}
}
@@ -461,6 +479,8 @@ private fun OnboardingScreenPreview() {
onMarketingDataLearnMoreClick = {},
onMarketingOptInToggle = {},
onMarketingDataContinueClick = {},
+ onIfPreferenceDohButtonClick = {},
+ onIfPreferencesButtonClick = {},
)
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/doh/root/DohSettingsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/doh/root/DohSettingsScreen.kt
index f290b21561..bbe9d9d8f7 100644
--- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/doh/root/DohSettingsScreen.kt
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/doh/root/DohSettingsScreen.kt
@@ -441,7 +441,7 @@ private fun buildProviderMenuItems(
}
@Composable
-private fun AlertDialogAddCustomProvider(
+internal fun AlertDialogAddCustomProvider(
customProviderErrorState: CustomProviderErrorState,
onCustomCancelClicked: () -> Unit,
onCustomAddClicked: (String) -> Unit,
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/IronFoxAddons.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/IronFoxAddons.kt
new file mode 100644
index 0000000000..b35658273e
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/IronFoxAddons.kt
@@ -0,0 +1,54 @@
+package org.mozilla.fenix.utils
+
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import mozilla.components.concept.engine.webextension.InstallationMethod
+import mozilla.components.feature.addons.Addon
+import mozilla.components.support.base.log.logger.Logger
+import org.mozilla.fenix.components.Components
+
+object IronFoxAddons {
+ private val logger = Logger("IronFoxAddons")
+
+ val UBLOCK_ORIGIN = Addon(
+ id = "uBlock0@raymondhill.net",
+ downloadUrl = "https://addons.mozilla.org/firefox/downloads/latest/uBlock0@raymondhill.net/latest.xpi"
+ )
+
+ fun isUBlockOrigin(addon: Addon) = addon.id == UBLOCK_ORIGIN.id
+
+ suspend fun installAddon(
+ components: Components,
+ addon: Addon,
+ ): Result<Addon> = withContext(Dispatchers.IO) {
+ runCatching {
+ val addonManager = components.addonManager
+ val addons = addonManager.getAddons(waitForPendingActions = false)
+ if (addons.none { it.id == addon.id && it.isInstalled() }) {
+ logger.warn("Installing addon: '${addon.id}'")
+ val deferred = withContext(Dispatchers.Main) {
+ val deferred = CompletableDeferred<Addon>()
+ addonManager.installAddon(
+ url = addon.downloadUrl,
+ installationMethod = InstallationMethod.MANAGER,
+ onSuccess = { result ->
+ logger.info("Addon '${addon.id}' installed.")
+ deferred.complete(result)
+ },
+ onError = { err ->
+ logger.error("Failed to install addon with id '${addon.id}'", err)
+ deferred.completeExceptionally(err)
+ }
+ )
+
+ deferred
+ }
+
+ deferred.await()
+ }
+
+ addon
+ }
+ }
+}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/IronFoxPreferences.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/IronFoxPreferences.kt
new file mode 100644
index 0000000000..5eff50dba9
--- /dev/null
+++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/utils/IronFoxPreferences.kt
@@ -0,0 +1,126 @@
+package org.mozilla.fenix.utils
+
+import android.content.Context
+import mozilla.components.concept.engine.EngineSession
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.settings
+
+object IronFoxPreferences {
+
+ /**
+ * Set whether to enable JavaScript Just-in-time compilation preferences.
+ *
+ * @param context The application context.
+ * @param isEnabled Whether to enable JavaScript Just-in-time compilation.
+ */
+ fun setJavascriptJitEnabled(
+ context: Context,
+ isEnabled: Boolean,
+ ) {
+ val settings = context.settings()
+ val components = context.components
+
+ settings.javascriptJitEnabled = isEnabled
+
+ components.core.engine.settings.javascriptJitBaselineEnabled = isEnabled
+ components.core.engine.settings.javascriptJitBaselineWasmEnabled = isEnabled
+ components.core.engine.settings.javascriptJitHintsEnabled = isEnabled
+ components.core.engine.settings.javascriptJitIonEnabled = isEnabled
+ components.core.engine.settings.javascriptJitMainProcessEnabled = isEnabled
+ components.core.engine.settings.javascriptJitNativeRegexpEnabled = isEnabled
+ components.core.engine.settings.javascriptJitTrustedPrincipalsEnabled = isEnabled
+ }
+
+ /**
+ * Check if JavaScript Just-in-time compilation is enabled.
+ */
+ fun isJavascriptJitEnabled(
+ context: Context
+ ): Boolean = context.components.core.engine.settings.run {
+ javascriptJitBaselineEnabled &&
+ javascriptJitBaselineWasmEnabled &&
+ javascriptJitHintsEnabled &&
+ javascriptJitIonEnabled &&
+ javascriptJitMainProcessEnabled &&
+ javascriptJitNativeRegexpEnabled &&
+ javascriptJitTrustedPrincipalsEnabled
+ }
+
+ /**
+ * Set whether to enable WebAssembly preferences.
+ *
+ * @param context The application context.
+ * @param isEnabled Whether to enable WebAssembly.
+ */
+ fun setWebAssemblyEnabled(
+ context: Context,
+ isEnabled: Boolean,
+ ) {
+ val settings = context.settings()
+ val components = context.components
+
+ settings.wasmEnabled = isEnabled
+ components.core.engine.settings.wasmEnabled = isEnabled
+ }
+
+ /**
+ * Check if WebAssembly is enabled.
+ */
+ fun isWebAssemblyEnabled(
+ context: Context
+ ): Boolean = context.components.core.engine.settings.wasmEnabled
+
+ /**
+ * Set whether to enable Safe Browsing preferences.
+ *
+ * @param context The application context.
+ * @param isEnabled Whether to enable Safe Browsing.
+ */
+ fun setSafeBrowsingEnabled(
+ context: Context,
+ isEnabled: Boolean,
+ ) {
+ val engineSettings = context.components.core.engine.settings
+ val settings = context.settings()
+ val components = context.components
+
+ settings.safeBrowsingEnabled = isEnabled
+
+ if (settings.safeBrowsingEnabled) {
+ engineSettings.safeBrowsingPolicy = arrayOf(EngineSession.SafeBrowsingPolicy.RECOMMENDED)
+ } else {
+ engineSettings.safeBrowsingPolicy = arrayOf(EngineSession.SafeBrowsingPolicy.NONE)
+ }
+ context.components.useCases.sessionUseCases.reload()
+ }
+
+ /**
+ * Check if Safe Browsing is enabled.
+ */
+ fun isSafeBrowsingEnabled(
+ context: Context
+ ): Boolean = context.settings().safeBrowsingEnabled
+
+ /**
+ * Set whether to enable the locale spoofing preference.
+ *
+ * @param context The application context.
+ * @param isEnabled Whether to enable locale spoofing.
+ */
+ fun setLocaleSpoofingEnabled(
+ context: Context,
+ isEnabled: Boolean,
+ ) {
+ val settings = context.settings()
+ val components = context.components
+ settings.spoofEnglish = isEnabled
+ components.core.engine.settings.spoofEnglish = isEnabled
+ }
+
+ /**
+ * Check if locale spoofing is enabled.
+ */
+ fun isLocaleSpoofingEnabled(
+ context: Context,
+ ): Boolean = context.components.core.engine.settings.spoofEnglish
+}