mirror of
https://github.com/ironfox-oss/IronFox.git
synced 2026-01-30 00:42:34 -05:00
1532 lines
60 KiB
Diff
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
|
|
+}
|