diff --git a/.experimenter.yaml b/.experimenter.yaml index 7b7ec1f3902..b6f673e38d4 100644 --- a/.experimenter.yaml +++ b/.experimenter.yaml @@ -10,11 +10,6 @@ onboarding: is-enabled: type: boolean description: "If `true`, the app will show the new onboarding screen" -tabs: - description: Nimbus feature name intended to control the multiple tabs feature in the app. - hasExposure: true - exposureDescription: "" - variables: - is_multi_tab: + is-promote-search-widget-dialog-enabled: type: boolean - description: "Nimbus variable of [FEATURE_TABS] allowing outside control of whether the multiple tabs feature should be enabled or not." + description: "If `true`, the app will show the new dialog for promote search widget" diff --git a/app/metrics.yaml b/app/metrics.yaml index 36b2c9008b0..7116c2922b2 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -1997,3 +1997,53 @@ search_widget: metadata: tags: - Search + promote_dialog_shown: + type: event + description: | + Promote search widget dialog is shown to the user. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7506 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/ + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 119 + metadata: + tags: + - Search + add_to_home_screen_button: + type: event + description: | + The user has pressed on add search widget + to home screen button from promote dialog. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7506 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/ + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 119 + metadata: + tags: + - Search + widget_was_added: + type: event + description: | + The user has added successfully the search widget from + promote search widget dialog. + bugs: + - https://github.com/mozilla-mobile/focus-android/issues/7506 + data_reviews: + - https://github.com/mozilla-mobile/focus-android/pull/ + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: 119 + metadata: + tags: + - Search diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt index e0f2db69580..d6f46c62d92 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/EnhancedTrackingProtectionSettingsTest.kt @@ -46,6 +46,7 @@ class EnhancedTrackingProtectionSettingsTest { dispatcher = MockWebServerHelper.AndroidAssetDispatcher() start() } + featureSettingsHelper.setSearchWidgetDialogEnabled(false) } @After diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt index 0492a3bafc1..931beb2cdfe 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/EraseBrowsingDataTest.kt @@ -52,6 +52,7 @@ class EraseBrowsingDataTest { start() } featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) } @After @@ -60,6 +61,7 @@ class EraseBrowsingDataTest { featureSettingsHelper.resetAllFeatureFlags() } + @Ignore("https://github.com/mozilla-mobile/focus-android/issues/7695") @SmokeTest @Test fun trashButtonTest() { diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt index 1daa8d4f7df..4843da7d242 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/SafeBrowsingTest.kt @@ -35,6 +35,7 @@ class SafeBrowsingTest { @Before fun setUp() { featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) webServer = MockWebServer().apply { dispatcher = MockWebServerHelper.AndroidAssetDispatcher() start() diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt index 1d6434c3662..b58756521ba 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/SearchTest.kt @@ -29,6 +29,7 @@ class SearchTest { @Before fun setUp() { featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) } @After diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt index f9b8cc83276..d0cedb10cf1 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/SettingsAdvancedTest.kt @@ -36,6 +36,7 @@ class SettingsAdvancedTest { start() } featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) } @After diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt index 81c9d55b049..e8b30d09544 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/ShortcutsTest.kt @@ -30,6 +30,7 @@ class ShortcutsTest { @Before fun setUp() { featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) webServer = MockWebServer().apply { dispatcher = MockWebServerHelper.AndroidAssetDispatcher() start() diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt index ab75730c4d8..79345cc3766 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/SitePermissionsTest.kt @@ -52,6 +52,7 @@ class SitePermissionsTest { @Before fun setUp() { featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) webServer = MockWebServer().apply { dispatcher = MockWebServerHelper.AndroidAssetDispatcher() start() diff --git a/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt b/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt index e1aba47ee67..373fea858ab 100644 --- a/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt @@ -44,6 +44,7 @@ class WebControlsTest { start() } featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) } @After diff --git a/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt b/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt index 513181a3e21..be7da05b919 100644 --- a/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt +++ b/app/src/androidTest/java/org/mozilla/focus/helpers/FeatureSettingsHelper.kt @@ -19,6 +19,14 @@ class FeatureSettingsHelper { settings.shouldShowCfrForTrackingProtection = enabled } + fun setSearchWidgetDialogEnabled(enabled: Boolean) { + if (enabled) { + settings.addClearBrowsingSessions(4) + } else { + settings.addClearBrowsingSessions(10) + } + } + // Important: // Use this after each test if you have modified these feature settings // to make sure the app goes back to the default state diff --git a/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt b/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt index 36897ea51bc..839f90f9242 100644 --- a/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt +++ b/app/src/androidTest/java/org/mozilla/focus/privacy/LocalSessionStorageTest.kt @@ -42,6 +42,7 @@ class LocalSessionStorageTest { start() } featureSettingsHelper.setCfrForTrackingProtectionEnabled(false) + featureSettingsHelper.setSearchWidgetDialogEnabled(false) } @After diff --git a/app/src/main/java/org/mozilla/focus/fragment/UrlInputFragment.kt b/app/src/main/java/org/mozilla/focus/fragment/UrlInputFragment.kt index 1cfd968ea5a..fefa19ca324 100644 --- a/app/src/main/java/org/mozilla/focus/fragment/UrlInputFragment.kt +++ b/app/src/main/java/org/mozilla/focus/fragment/UrlInputFragment.kt @@ -196,6 +196,14 @@ class UrlInputFragment : binding.browserToolbar.editMode() isInitialized = true } + + if ( + requireComponents.settings.searchWidgetInstalled && + requireComponents.appStore.state.showSearchWidgetSnackbar + ) { + ViewUtils.showBrandedSnackbar(view, R.string.promote_search_widget_snackbar_message, 0) + requireComponents.appStore.dispatch(AppAction.ShowSearchWidgetSnackBar(false)) + } } override fun onPause() { @@ -240,7 +248,6 @@ class UrlInputFragment : TopSitesOverlay() } } - return binding.root } diff --git a/app/src/main/java/org/mozilla/focus/navigation/MainActivityNavigation.kt b/app/src/main/java/org/mozilla/focus/navigation/MainActivityNavigation.kt index 1965a4cbfff..9354dfb7dc5 100644 --- a/app/src/main/java/org/mozilla/focus/navigation/MainActivityNavigation.kt +++ b/app/src/main/java/org/mozilla/focus/navigation/MainActivityNavigation.kt @@ -5,6 +5,7 @@ package org.mozilla.focus.navigation import android.os.Build +import org.mozilla.experiments.nimbus.internal.FeatureHolder import org.mozilla.focus.R import org.mozilla.focus.activity.MainActivity import org.mozilla.focus.autocomplete.AutocompleteAddFragment @@ -14,6 +15,7 @@ import org.mozilla.focus.autocomplete.AutocompleteSettingsFragment import org.mozilla.focus.biometrics.BiometricAuthenticationFragment import org.mozilla.focus.exceptions.ExceptionsListFragment import org.mozilla.focus.exceptions.ExceptionsRemoveFragment +import org.mozilla.focus.ext.components import org.mozilla.focus.fragment.BrowserFragment import org.mozilla.focus.fragment.FirstrunFragment import org.mozilla.focus.fragment.UrlInputFragment @@ -24,6 +26,8 @@ import org.mozilla.focus.fragment.onboarding.OnboardingStep import org.mozilla.focus.fragment.onboarding.OnboardingStorage import org.mozilla.focus.locale.screen.LanguageFragment import org.mozilla.focus.nimbus.FocusNimbus +import org.mozilla.focus.nimbus.Onboarding +import org.mozilla.focus.searchwidget.SearchWidgetUtils import org.mozilla.focus.settings.GeneralSettingsFragment import org.mozilla.focus.settings.InstalledSearchEnginesSettingsFragment import org.mozilla.focus.settings.ManualAddSearchEngineSettingsFragment @@ -59,12 +63,19 @@ class MainActivityNavigation( val isShowingBrowser = browserFragment != null val crashReporterIsVisible = browserFragment?.crashReporterIsVisible() ?: false + val onboardingFeature = FocusNimbus.features.onboarding + val onboardingConfig = onboardingFeature.value(activity) + if (isShowingBrowser && !crashReporterIsVisible) { - ViewUtils.showBrandedSnackbar( - activity.findViewById(android.R.id.content), - R.string.feedback_erase2, - activity.resources.getInteger(R.integer.erase_snackbar_delay), - ) + if (onboardingConfig.isPromoteSearchWidgetDialogEnabled) { + showPromoteSearchWidgetDialog(onboardingFeature) + } else { + ViewUtils.showBrandedSnackbar( + activity.findViewById(android.R.id.content), + R.string.feedback_erase2, + activity.resources.getInteger(R.integer.erase_snackbar_delay), + ) + } } // We add the url input fragment to the layout if it doesn't exist yet. @@ -88,10 +99,33 @@ class MainActivityNavigation( // Ideally we'd make it possible to pause observers while the app is in the background: // https://github.com/mozilla-mobile/android-components/issues/876 transaction - .replace(R.id.container, UrlInputFragment.createWithoutSession(), UrlInputFragment.FRAGMENT_TAG) + .replace( + R.id.container, + UrlInputFragment.createWithoutSession(), + UrlInputFragment.FRAGMENT_TAG, + ) .commitAllowingStateLoss() } + /** + * Display the widget promo at first data clearing action and if it wasn't added after 5th Focus session. + */ + @Suppress("MagicNumber") + private fun showPromoteSearchWidgetDialog(onboardingFeature: FeatureHolder) { + if (!activity.components.settings.searchWidgetInstalled) { + val clearBrowsingSessions = activity.components.settings.getClearBrowsingSessions() + activity.components.settings.addClearBrowsingSessions(1) + onboardingFeature.recordExposure() + + if ( + clearBrowsingSessions == 0 || + clearBrowsingSessions == 4 + ) { + SearchWidgetUtils.showPromoteSearchWidgetDialog(activity) + } + } + } + /** * Show browser for tab with the given [tabId]. */ diff --git a/app/src/main/java/org/mozilla/focus/searchwidget/PromoteSearchWidgetDialogCompose.kt b/app/src/main/java/org/mozilla/focus/searchwidget/PromoteSearchWidgetDialogCompose.kt new file mode 100644 index 00000000000..3a3c7af0ff1 --- /dev/null +++ b/app/src/main/java/org/mozilla/focus/searchwidget/PromoteSearchWidgetDialogCompose.kt @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.searchwidget + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.constraintlayout.compose.ConstraintLayout +import mozilla.components.ui.colors.PhotonColors +import org.mozilla.focus.R +import org.mozilla.focus.ui.theme.FocusTheme +import org.mozilla.focus.ui.theme.focusColors +import org.mozilla.focus.ui.theme.focusTypography + +@Composable +@Preview +private fun PromoteSearchWidgetDialogComposePreview() { + FocusTheme { + PromoteSearchWidgetDialogCompose({}, {}) + } +} + +@Suppress("LongMethod") +@Composable +fun PromoteSearchWidgetDialogCompose( + onAddSearchWidgetButtonClick: () -> Unit, + onDismiss: () -> Unit, +) { + val openDialog = remember { mutableStateOf(true) } + if (openDialog.value) { + Dialog( + onDismissRequest = { + onDismiss() + }, + DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = false), + ) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(20.dp)), + ) { + ConstraintLayout( + modifier = Modifier + .wrapContentSize() + .background( + colorResource(id = R.color.promote_search_widget_dialog_background), + ), + ) { + val (closeButton, content) = createRefs() + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 8.dp, end = 16.dp) + .constrainAs(closeButton) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.End, + ) { + CloseButton(openDialog, onDismiss) + } + Column( + modifier = Modifier.constrainAs(content) { + top.linkTo(closeButton.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource( + id = R.string.promote_search_widget_dialog_title, + ), + modifier = Modifier + .padding(16.dp), + color = focusColors.dialogTextColor, + textAlign = TextAlign.Center, + style = focusTypography.dialogTitle, + ) + Text( + text = stringResource( + id = R.string.promote_search_widget_dialog_subtitle, + LocalContext.current.getString(R.string.onboarding_short_app_name), + ), + modifier = Modifier + .padding(top = 16.dp, start = 16.dp, end = 16.dp), + color = focusColors.dialogTextColor, + textAlign = TextAlign.Center, + style = focusTypography.dialogContent, + ) + Image( + painter = painterResource(R.drawable.focus_search_widget_promote_dialog), + contentDescription = LocalContext.current.getString( + R.string.promote_search_widget_dialog_picture_content_description, + ), + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp) + .background( + colorResource(id = R.color.promote_search_widget_dialog_background), + ), + ) + ComponentAddWidgetButton({ onAddSearchWidgetButtonClick() }, { onDismiss() }, openDialog) + } + } + } + } + } +} + +@Composable +private fun ComponentAddWidgetButton( + onAddSearchWidgetButtonClick: () -> Unit, + onDismiss: () -> Unit, + openState: MutableState, +) { + Button( + onClick = { + openState.value = false + onAddSearchWidgetButtonClick() + onDismiss() + }, + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.textButtonColors( + backgroundColor = colorResource(R.color.promote_search_widget_dialog_add_widget_button_background), + ), + ) { + Text( + text = AnnotatedString( + LocalContext.current.resources.getString( + R.string.promote_search_widget_button_text, + ), + ), + color = PhotonColors.White, + ) + } +} + +@Composable +private fun CloseButton( + openState: MutableState, + onDismiss: () -> Unit, +) { + IconButton( + onClick = { + onDismiss() + openState.value = false + }, + modifier = Modifier + .background( + colorResource(id = R.color.promote_search_widget_dialog_close_button_background), + shape = CircleShape, + ) + .size(48.dp) + .padding(10.dp), + ) { + Icon( + painter = painterResource(R.drawable.mozac_ic_close), + contentDescription = stringResource(id = R.string.promote_search_widget_dialog_content_description), + tint = focusColors.closeIcon, + ) + } +} diff --git a/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt b/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt index 5618b0efa2e..3f9894bac3f 100644 --- a/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt +++ b/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetProvider.kt @@ -15,11 +15,19 @@ import mozilla.components.support.utils.PendingIntentUtils import org.mozilla.focus.R import org.mozilla.focus.activity.IntentReceiverActivity import org.mozilla.focus.ext.components +import org.mozilla.focus.session.VisibilityLifeCycleCallback +import org.mozilla.focus.state.AppAction class SearchWidgetProvider : AppSearchWidgetProvider() { override fun onEnabled(context: Context) { context.components.settings.addSearchWidgetInstalled(1) + + // The snackBar that informs the user that search widget was added successfully + // should appear only if the app is in foreground + if (!VisibilityLifeCycleCallback.isInBackground(context)) { + context.components.appStore.dispatch(AppAction.ShowSearchWidgetSnackBar(true)) + } } override fun onDeleted(context: Context, appWidgetIds: IntArray) { diff --git a/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetUtils.kt b/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetUtils.kt new file mode 100644 index 00000000000..32d62bc3100 --- /dev/null +++ b/app/src/main/java/org/mozilla/focus/searchwidget/SearchWidgetUtils.kt @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.focus.searchwidget + +import android.app.Activity +import android.app.Dialog +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Intent +import android.os.Build +import android.os.Bundle +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.ViewTreeLifecycleOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import mozilla.components.support.utils.PendingIntentUtils +import org.mozilla.focus.activity.MainActivity +import org.mozilla.focus.ui.theme.FocusTheme + +object SearchWidgetUtils { + + private fun addSearchWidgetToHomeScreen(activity: Activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val appWidgetManager = AppWidgetManager.getInstance(activity) + val searchWidgetProvider = ComponentName(activity, SearchWidgetProvider::class.java) + if (appWidgetManager!!.isRequestPinAppWidgetSupported) { + val pinnedWidgetCallbackIntent = Intent(activity, SearchWidgetProvider::class.java) + val successCallback = PendingIntent.getBroadcast( + activity, + 0, + pinnedWidgetCallbackIntent, + PendingIntentUtils.defaultFlags or + PendingIntent.FLAG_UPDATE_CURRENT, + ) + appWidgetManager.requestPinAppWidget(searchWidgetProvider, Bundle(), successCallback) + } + } + } + + /** + * Shows promote search widget dialog + */ + fun showPromoteSearchWidgetDialog(activity: MainActivity) { + val promoteSearchWidgetDialog = Dialog(activity) + promoteSearchWidgetDialog.apply { + setContentView( + ComposeView(activity).apply { + ViewTreeLifecycleOwner.set(this, activity) + this.setViewTreeSavedStateRegistryOwner(activity) + setContent { + FocusTheme { + PromoteSearchWidgetDialogCompose( + onAddSearchWidgetButtonClick = { + addSearchWidgetToHomeScreen(activity) + }, + onDismiss = { + promoteSearchWidgetDialog.dismiss() + }, + ) + } + } + }, + ) + }.show() + } +} diff --git a/app/src/main/java/org/mozilla/focus/state/AppAction.kt b/app/src/main/java/org/mozilla/focus/state/AppAction.kt index 7be763242ce..8c10edf6467 100644 --- a/app/src/main/java/org/mozilla/focus/state/AppAction.kt +++ b/app/src/main/java/org/mozilla/focus/state/AppAction.kt @@ -105,4 +105,9 @@ sealed class AppAction : Action { * State of show Tracking Protection CFR has changed */ data class ShowTrackingProtectionCfrChange(val value: Map) : AppAction() + + /** + * State of Snackbar for promote search widget has changed + */ + data class ShowSearchWidgetSnackBar(val value: Boolean) : AppAction() } diff --git a/app/src/main/java/org/mozilla/focus/state/AppReducer.kt b/app/src/main/java/org/mozilla/focus/state/AppReducer.kt index e9a4d737399..6f27b018a25 100644 --- a/app/src/main/java/org/mozilla/focus/state/AppReducer.kt +++ b/app/src/main/java/org/mozilla/focus/state/AppReducer.kt @@ -39,6 +39,7 @@ object AppReducer : Reducer { is AppAction.OpenSitePermissionOptionsScreen -> openSitePermissionOptionsScreen(state, action) is AppAction.ShowHomeScreen -> showHomeScreen(state) is AppAction.ShowOnboardingSecondScreen -> showOnBoardingSecondScreen(state) + is AppAction.ShowSearchWidgetSnackBar -> showSearchWidgetSnackBarChanged(state, action) } } } @@ -186,6 +187,13 @@ private fun showEraseTabsCfrChanged(state: AppState, action: AppAction.ShowErase return state.copy(showEraseTabsCfr = action.value) } +/** + * The state of search widget snackBar changed + */ +private fun showSearchWidgetSnackBarChanged(state: AppState, action: AppAction.ShowSearchWidgetSnackBar): AppState { + return state.copy(showSearchWidgetSnackbar = action.value) +} + /** * The state of tracking protection CFR changed */ diff --git a/app/src/main/java/org/mozilla/focus/state/AppState.kt b/app/src/main/java/org/mozilla/focus/state/AppState.kt index 95d9929bab7..df8dc26df37 100644 --- a/app/src/main/java/org/mozilla/focus/state/AppState.kt +++ b/app/src/main/java/org/mozilla/focus/state/AppState.kt @@ -18,6 +18,7 @@ import java.util.UUID * whether they have been updated or not * @property secretSettingsEnabled A flag which reflects the state of debug secret settings * @property showEraseTabsCfr A flag which reflects the state erase tabs CFR + * @property showSearchWidgetSnackbar A flag which reflects the state of search widget snackbar */ data class AppState( val screen: Screen, @@ -25,6 +26,7 @@ data class AppState( val sitePermissionOptionChange: Boolean = false, val secretSettingsEnabled: Boolean = false, val showEraseTabsCfr: Boolean = false, + val showSearchWidgetSnackbar: Boolean = false, val showTrackingProtectionCfrForTab: Map = emptyMap(), ) : State diff --git a/app/src/main/java/org/mozilla/focus/ui/theme/FocusColors.kt b/app/src/main/java/org/mozilla/focus/ui/theme/FocusColors.kt index 3ecdc84cc1b..1ee24060161 100644 --- a/app/src/main/java/org/mozilla/focus/ui/theme/FocusColors.kt +++ b/app/src/main/java/org/mozilla/focus/ui/theme/FocusColors.kt @@ -31,6 +31,7 @@ data class FocusColors( val settingsTextColor: Color, val settingsTextSummaryColor: Color, val closeIcon: Color, + val dialogTextColor: Color, ) { val primary: Color get() = material.primary val primaryVariant: Color get() = material.primaryVariant diff --git a/app/src/main/java/org/mozilla/focus/ui/theme/FocusTheme.kt b/app/src/main/java/org/mozilla/focus/ui/theme/FocusTheme.kt index 85c47556471..bbc7609c16e 100644 --- a/app/src/main/java/org/mozilla/focus/ui/theme/FocusTheme.kt +++ b/app/src/main/java/org/mozilla/focus/ui/theme/FocusTheme.kt @@ -81,6 +81,7 @@ private fun darkColorPalette(): FocusColors = FocusColors( settingsTextColor = PhotonColors.White, settingsTextSummaryColor = PhotonColors.LightGrey50, closeIcon = PhotonColors.LightGrey70, + dialogTextColor = PhotonColors.White, ) private fun lightColorPalette(): FocusColors = FocusColors( @@ -103,6 +104,7 @@ private fun lightColorPalette(): FocusColors = FocusColors( onboardingSemiBoldText = PhotonColors.DarkGrey90, onboardingNormalText = PhotonColors.DarkGrey05, closeIcon = PhotonColors.LightGrey90, + dialogTextColor = PhotonColors.Black, ) /** diff --git a/app/src/main/java/org/mozilla/focus/ui/theme/FocusTypography.kt b/app/src/main/java/org/mozilla/focus/ui/theme/FocusTypography.kt index b6de757e084..055d8f4627a 100644 --- a/app/src/main/java/org/mozilla/focus/ui/theme/FocusTypography.kt +++ b/app/src/main/java/org/mozilla/focus/ui/theme/FocusTypography.kt @@ -32,6 +32,7 @@ data class FocusTypography( val links: TextStyle, val dialogTitle: TextStyle, val dialogInput: TextStyle, + val dialogContent: TextStyle, val onboardingTitle: TextStyle, val onboardingSubtitle: TextStyle, val onboardingDescription: TextStyle, @@ -79,6 +80,11 @@ val focusTypography: FocusTypography fontSize = 20.sp, color = focusColors.onPrimary, ), + dialogContent = TextStyle( + fontFamily = metropolis, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + ), onboardingTitle = TextStyle( fontFamily = metropolis, fontWeight = FontWeight.SemiBold, diff --git a/app/src/main/java/org/mozilla/focus/utils/Settings.kt b/app/src/main/java/org/mozilla/focus/utils/Settings.kt index ca47c42a8c2..b4c060516bb 100644 --- a/app/src/main/java/org/mozilla/focus/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/focus/utils/Settings.kt @@ -386,6 +386,23 @@ class Settings( 0, ) + /** + * This is used for promote search widget dialog to appear only at the first data clearing and + * at the 5th one. + */ + fun addClearBrowsingSessions(count: Int) { + val key = getPreferenceKey(R.string.pref_key_clear_browsing_sessions) + val newValue = preferences.getInt(key, 0) + count + preferences.edit() + .putInt(key, newValue) + .apply() + } + + fun getClearBrowsingSessions() = preferences.getInt( + getPreferenceKey(R.string.pref_key_clear_browsing_sessions), + 0, + ) + fun getHttpsOnlyMode(): Engine.HttpsOnlyMode { return if (preferences.getBoolean(getPreferenceKey(R.string.pref_key_https_only), true)) { Engine.HttpsOnlyMode.ENABLED diff --git a/app/src/main/res/drawable-hdpi/focus_search_widget_promote_dialog.png b/app/src/main/res/drawable-hdpi/focus_search_widget_promote_dialog.png new file mode 100644 index 00000000000..e785788998b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/focus_search_widget_promote_dialog.png differ diff --git a/app/src/main/res/drawable-night-hdpi/focus_search_widget_promote_dialog.png b/app/src/main/res/drawable-night-hdpi/focus_search_widget_promote_dialog.png new file mode 100644 index 00000000000..60dceda0b10 Binary files /dev/null and b/app/src/main/res/drawable-night-hdpi/focus_search_widget_promote_dialog.png differ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 9c62a692028..bb95f57a28b 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -135,4 +135,9 @@ #3C286A #21163E #1D1133 + + + #2B2A33 + #3B3C3F + #7542E5 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 1ef84afce26..8dc71f8581b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -142,4 +142,8 @@ #D9D3F5 #D6C9EB + + #FFFFFF + #F2F2F7 + #312A64 diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index aa17d9a31ea..63e85d9463a 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -70,6 +70,7 @@ security_category pref_key_search_widget_installed + pref_key_clear_browsing_sessions pref_screen_exceptions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e54ea1fe97..0fb3142aafc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -470,7 +470,8 @@ Welcome to %1$s! - !-- Content description (not visible, for screen readers etc.): This is the description for the close button from the new onboarding flow screen one and two --> + !-- Content description (not visible, for screen readers etc.): This is the description for the + close button from the new onboarding flow screen one and two --> Close @@ -508,22 +509,22 @@ - %s is a privacy-first browser with extra-strong tracking and cookie protection built in. + %s is a privacy-first browser with extra-strong tracking and cookie protection built in. Browse like it never happened - One tap erases your browsing history and cookies — and stops ads from following you around. + One tap erases your browsing history and cookies — and stops ads from following you around. - Privacy that fits any situation + Privacy that fits any situation - If your browsing needs change, it’s easy to scale your privacy settings up or down. + If your browsing needs change, it’s easy to scale your privacy settings up or down. - Start browsing + Start browsing Power up your privacy @@ -992,4 +993,28 @@ The new line here must be kept as the second half of the string is clickable for Get rid of your personal data, browsing history and more from this session by tapping the trash button. Give it a try! Tap here to trash it all — history, cookies, everything — and start fresh on a new tab. + + + !-- Content description (not visible, for screen readers etc.): This is the description for the + close button from promote search widget dialog. --> + Close + + !-- Content description (not visible, for screen readers etc.): This is the description for + picture of search widget from promote search widget dialog. --> + Search widget + + !-- This is the title of promote search widget dialog. --> + Browsing history cleared! 🎉 + + !-- This is the subtitle of promote search widget dialog. %1$s will get replaced with the name + of the app (e.g. "Focus") --> + We’ll leave you to your private browsing, but get a quicker start next time with the %1$s widget on your Home screen. + + !-- This is te text from add search widget to home screen button .The button is located on + promote search widget dialog. --> + Add widget to home screen + + !-- This is te text of the snackbar that appears after the search widget was added successfully + to the home screen. --> + Widget added to home screen diff --git a/nimbus.fml.yaml b/nimbus.fml.yaml index bd471feb912..d0f0c3d97a5 100644 --- a/nimbus.fml.yaml +++ b/nimbus.fml.yaml @@ -20,6 +20,17 @@ features: description: If `true`, the app will show the cfrs type: Boolean default: false + is-promote-search-widget-dialog-enabled: + description: If `true`, the app will show the new dialog for promote search widget + type: Boolean + default: false + defaults: + - channel: debug + value: { + "is-enabled": true, + "is-cfr-enabled": true, + "is-promote-search-widget-dialog-enabled": true, + } types: objects: { } enums: { }