diff --git a/app/metrics.yaml b/app/metrics.yaml
index d68c093a70..8aeb63e4a3 100644
--- a/app/metrics.yaml
+++ b/app/metrics.yaml
@@ -1318,6 +1318,105 @@ onboarding:
metadata:
tags:
- Onboarding
+ add_search_widget_card:
+ type: event
+ description: |
+ User viewed juno onboarding add search widget card.
+ extra_keys:
+ element_type:
+ type: string
+ description: |
+ Type of element that was viewed.
+ action:
+ type: string
+ description: |
+ Type of action taken by the user.
+ sequence_position:
+ type: string
+ description: |
+ Position of the onboarding card in the onboarding flow.
+ sequence_id:
+ type: string
+ description: |
+ Identifier for the sequence.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1848960
+ data_reviews:
+ - https://github.com/mozilla-mobile/firefox-android/pull/3310
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - android-probes@mozilla.com
+ expires: 126
+ metadata:
+ tags:
+ - Onboarding
+ add_search_widget:
+ type: event
+ description: |
+ User tapped on Add Firefox Widget in juno onboarding.
+ extra_keys:
+ element_type:
+ type: string
+ description: |
+ Type of element that was viewed.
+ action:
+ type: string
+ description: |
+ Type of action taken by the user.
+ sequence_position:
+ type: string
+ description: |
+ Position of the onboarding card in the onboarding flow.
+ sequence_id:
+ type: string
+ description: |
+ Identifier for the sequence.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1848960
+ data_reviews:
+ - https://github.com/mozilla-mobile/firefox-android/pull/3310
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - android-probes@mozilla.com
+ expires: 126
+ metadata:
+ tags:
+ - Onboarding
+ skip_add_search_widget:
+ type: event
+ description: |
+ User tapped on skip add search widget button in juno onboarding.
+ extra_keys:
+ element_type:
+ type: string
+ description: |
+ Type of element that was viewed.
+ action:
+ type: string
+ description: |
+ Type of action taken by the user.
+ sequence_position:
+ type: string
+ description: |
+ Position of the onboarding card in the onboarding flow.
+ sequence_id:
+ type: string
+ description: |
+ Identifier for the sequence.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1848960
+ data_reviews:
+ - https://github.com/mozilla-mobile/firefox-android/pull/3310
+ data_sensitivity:
+ - interaction
+ notification_emails:
+ - android-probes@mozilla.com
+ expires: 126
+ metadata:
+ tags:
+ - Onboarding
privacy_policy:
type: event
description: |
diff --git a/app/onboarding.fml.yaml b/app/onboarding.fml.yaml
index 1f004cb100..1922b43ce3 100644
--- a/app/onboarding.fml.yaml
+++ b/app/onboarding.fml.yaml
@@ -23,6 +23,15 @@ features:
primary-button-label: juno_onboarding_default_browser_positive_button
secondary-button-label: juno_onboarding_default_browser_negative_button
+ add-search-widget:
+ card-type: add-search-widget
+ title: juno_onboarding_add_search_widget_title
+ body: juno_onboarding_add_search_widget_description
+ image-res: ic_onboarding_search_widget
+ ordering: 15
+ primary-button-label: juno_onboarding_add_search_widget_positive_button
+ secondary-button-label: juno_onboarding_add_search_widget_negative_button
+
sync-sign-in:
card-type: sync-sign-in
title: juno_onboarding_sign_in_title
@@ -108,3 +117,5 @@ enums:
description: Allows user to sync with a Firefox account.
notification-permission:
description: Allows user to enable notification permission.
+ add-search-widget:
+ description: Allows user to add search widget to homescreen.
diff --git a/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt b/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt
index 4016c16ef2..d74edfcd4b 100644
--- a/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt
+++ b/app/src/androidTest/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt
@@ -20,15 +20,27 @@ class JunoOnboardingMapperTest {
HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
@Test
- fun showNotificationTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages() {
+ fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData)
- assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true))
+ assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, false))
}
@Test
- fun showNotificationFalse_pagesToDisplay_returnsSortedListOfConvertedPagesWithoutNotificationPage() {
+ fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData)
- assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false))
+ assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, false))
+ }
+
+ @Test
+ fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() {
+ val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData)
+ assertEquals(expected, unsortedAllKnownCardData.toPageUiData(false, true))
+ }
+
+ @Test
+ fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() {
+ val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData, notificationPageUiData)
+ assertEquals(expected, unsortedAllKnownCardData.toPageUiData(true, true))
}
}
@@ -41,6 +53,15 @@ private val defaultBrowserPageUiData = OnboardingPageUiData(
primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text",
)
+private val addSearchWidgetPageUiData = OnboardingPageUiData(
+ type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
+ imageRes = R.drawable.ic_onboarding_search_widget,
+ title = "add search widget title",
+ description = "add search widget body with link text",
+ linkText = "link text",
+ primaryButtonLabel = "add search widget primary button text",
+ secondaryButtonLabel = "add search widget secondary button text",
+)
private val syncPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
@@ -68,6 +89,16 @@ private val defaultBrowserCardData = OnboardingCardData(
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10,
)
+private val addSearchWidgetCardData = OnboardingCardData(
+ cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
+ imageRes = R.drawable.ic_onboarding_search_widget,
+ title = StringHolder(null, "add search widget title"),
+ body = StringHolder(null, "add search widget body with link text"),
+ linkText = StringHolder(null, "link text"),
+ primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
+ secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
+ ordering = 15,
+)
private val syncCardData = OnboardingCardData(
cardType = OnboardingCardType.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
@@ -91,4 +122,5 @@ private val unsortedAllKnownCardData = listOf(
syncCardData,
notificationCardData,
defaultBrowserCardData,
+ addSearchWidgetCardData,
)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 302b5ed108..a2c21352fa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -338,6 +338,13 @@
android:resource="@xml/search_widget_info" />
+
+
+
+
+
+
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt
index 640d829559..5f96d60053 100644
--- a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt
@@ -5,7 +5,10 @@
package org.mozilla.fenix.onboarding
import android.annotation.SuppressLint
+import android.appwidget.AppWidgetManager
+import android.content.ComponentName
import android.content.Context
+import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
@@ -18,6 +21,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.fragment.findNavController
import mozilla.components.support.base.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.R
@@ -34,14 +38,21 @@ import org.mozilla.fenix.onboarding.view.telemetrySequenceId
import org.mozilla.fenix.onboarding.view.toPageUiData
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.theme.FirefoxTheme
+import org.mozilla.gecko.search.SearchWidgetProvider
/**
* Fragment displaying the juno onboarding flow.
*/
class JunoOnboardingFragment : Fragment() {
- private val pagesToDisplay by lazy { pagesToDisplay(shouldShowNotificationPage(requireContext())) }
+ private val pagesToDisplay by lazy {
+ pagesToDisplay(
+ canShowNotificationPage(requireContext()),
+ canShowAddWidgetCard(),
+ )
+ }
private val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() }
+ private val pinAppWidgetReceiver = WidgetPinnedReceiver()
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -49,6 +60,9 @@ class JunoOnboardingFragment : Fragment() {
if (isNotATablet()) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
+ val filter = IntentFilter(WidgetPinnedReceiver.ACTION)
+ LocalBroadcastManager.getInstance(requireContext())
+ .registerReceiver(pinAppWidgetReceiver, filter)
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -74,6 +88,7 @@ class JunoOnboardingFragment : Fragment() {
if (isNotATablet()) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
+ LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(pinAppWidgetReceiver)
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -141,6 +156,15 @@ class JunoOnboardingFragment : Fragment() {
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.NOTIFICATION_PERMISSION),
)
},
+ onAddFirefoxWidgetClick = {
+ showAddSearchWidgetDialog()
+ },
+ onSkipFirefoxWidgetClick = {
+ telemetryRecorder.onSkipAddWidgetClick(
+ pagesToDisplay.telemetrySequenceId(),
+ pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.ADD_SEARCH_WIDGET),
+ )
+ },
onFinish = {
onFinish(
sequenceId = pagesToDisplay.telemetrySequenceId(),
@@ -157,6 +181,19 @@ class JunoOnboardingFragment : Fragment() {
)
}
+ private fun showAddSearchWidgetDialog() {
+ // Requesting to pin app widget is only available for Android 8.0 and above
+ if (canShowAddWidgetCard()) {
+ val appWidgetManager = AppWidgetManager.getInstance(activity)
+ val searchWidgetProvider =
+ ComponentName(requireActivity(), SearchWidgetProvider::class.java)
+ if (appWidgetManager.isRequestPinAppWidgetSupported) {
+ val successCallback = WidgetPinnedReceiver.getPendingIntent(requireContext())
+ appWidgetManager.requestPinAppWidget(searchWidgetProvider, null, successCallback)
+ }
+ }
+ }
+
private fun onFinish(sequenceId: String, sequencePosition: String) {
requireComponents.fenixOnboarding.finish()
findNavController().nav(
@@ -169,12 +206,20 @@ class JunoOnboardingFragment : Fragment() {
)
}
- private fun shouldShowNotificationPage(context: Context) =
+ private fun canShowNotificationPage(context: Context) =
!NotificationManagerCompat.from(context.applicationContext)
.areNotificationsEnabledSafe() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+ private fun canShowAddWidgetCard() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+
private fun isNotATablet() = !resources.getBoolean(R.bool.tablet)
- private fun pagesToDisplay(showNotificationPage: Boolean): List =
- FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(showNotificationPage)
+ private fun pagesToDisplay(
+ showNotificationPage: Boolean,
+ showAddWidgetPage: Boolean,
+ ): List =
+ FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(
+ showNotificationPage,
+ showAddWidgetPage,
+ )
}
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingTelemetryRecorder.kt b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingTelemetryRecorder.kt
index c1b048e4fd..c820a9df85 100644
--- a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingTelemetryRecorder.kt
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingTelemetryRecorder.kt
@@ -49,6 +49,17 @@ class JunoOnboardingTelemetryRecorder {
)
}
+ OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> {
+ Onboarding.addSearchWidgetCard.record(
+ Onboarding.AddSearchWidgetCardExtra(
+ action = ACTION_IMPRESSION,
+ elementType = ET_ONBOARDING_CARD,
+ sequenceId = sequenceId,
+ sequencePosition = sequencePosition,
+ ),
+ )
+ }
+
OnboardingPageUiData.Type.SYNC_SIGN_IN -> {
Onboarding.signInCard.record(
Onboarding.SignInCardExtra(
@@ -121,6 +132,22 @@ class JunoOnboardingTelemetryRecorder {
)
}
+ /**
+ * Records add search widget click event.
+ * @param sequenceId The identifier of the onboarding sequence shown to the user.
+ * @param sequencePosition The sequence position of the page for which the impression occurred.
+ */
+ fun onAddSearchWidgetClick(sequenceId: String, sequencePosition: String) {
+ Onboarding.addSearchWidget.record(
+ Onboarding.AddSearchWidgetExtra(
+ action = ACTION_CLICK,
+ elementType = ET_PRIMARY_BUTTON,
+ sequenceId = sequenceId,
+ sequencePosition = sequencePosition,
+ ),
+ )
+ }
+
/**
* Records skip set to default click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
@@ -153,6 +180,22 @@ class JunoOnboardingTelemetryRecorder {
)
}
+ /**
+ * Records skip add widget click event.
+ * @param sequenceId The identifier of the onboarding sequence shown to the user.
+ * @param sequencePosition The sequence position of the page for which the impression occurred.
+ */
+ fun onSkipAddWidgetClick(sequenceId: String, sequencePosition: String) {
+ Onboarding.skipAddSearchWidget.record(
+ Onboarding.SkipAddSearchWidgetExtra(
+ action = ACTION_CLICK,
+ elementType = ET_SECONDARY_BUTTON,
+ sequenceId = sequenceId,
+ sequencePosition = sequencePosition,
+ ),
+ )
+ }
+
/**
* Records skip notification permission click event.
* @param sequenceId The identifier of the onboarding sequence shown to the user.
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/WidgetPinnedReceiver.kt b/app/src/main/java/org/mozilla/fenix/onboarding/WidgetPinnedReceiver.kt
new file mode 100644
index 0000000000..a036f16684
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/WidgetPinnedReceiver.kt
@@ -0,0 +1,83 @@
+/* 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.fenix.onboarding
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import mozilla.components.support.utils.PendingIntentUtils
+import org.mozilla.fenix.onboarding.view.JunoOnboardingScreen
+
+/**
+ * Receiver required to catch callback from Launcher when prompted
+ * to add search widget from the Juno Onboarding.
+ */
+class WidgetPinnedReceiver : BroadcastReceiver() {
+
+ companion object {
+ const val ACTION = "org.mozilla.fenix.onboarding.WidgetPinnedReceiver.PIN_SEARCH_WIDGET_SUCCESS"
+
+ /**
+ * Prepare success callback for when requesting to pin Search Widget.
+ */
+ fun getPendingIntent(context: Context): PendingIntent {
+ val callbackIntent = Intent(context, WidgetPinnedReceiver::class.java)
+ val bundle = Bundle()
+ bundle.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 1)
+ callbackIntent.putExtras(bundle)
+ return PendingIntent.getBroadcast(
+ context,
+ 0,
+ callbackIntent,
+ PendingIntentUtils.defaultFlags or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+ }
+ }
+
+ /**
+ * Object containing boolean that updates behavior of Add Search Widget
+ * card from [JunoOnboardingScreen].
+ * - True if widget added successfully and app resumed from launcher add widget dialog.
+ * - False if dialog opened but widget was not added.
+ */
+ object WidgetPinnedState {
+ private val _isPinned = MutableStateFlow(false)
+ val isPinned: StateFlow = _isPinned
+
+ /**
+ * Update state when resumed to add search widget card
+ * and the widget was added successfully.
+ */
+ fun widgetPinned() {
+ _isPinned.value = true
+ }
+ }
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (context == null || intent == null) {
+ return
+ } else if (intent.action == ACTION) {
+ // Returned to fragment, go to next page and update button behavior.
+ WidgetPinnedState.widgetPinned()
+ }
+
+ val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
+
+ if (widgetId == -1) {
+ // No widget id received.
+ return
+ } else {
+ // Callback from system, widget pinned successfully, update compose now.
+ val updateIntent = Intent(ACTION)
+ LocalBroadcastManager.getInstance(context).sendBroadcast(updateIntent)
+ }
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt
index 5b3e4578b0..b57d489d96 100644
--- a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapper.kt
@@ -12,12 +12,21 @@ import org.mozilla.fenix.settings.SupportUtils
/**
* Returns a list of all the required Nimbus 'cards' that have been converted to [OnboardingPageUiData].
*/
-internal fun Collection.toPageUiData(showNotificationPage: Boolean): List =
+internal fun Collection.toPageUiData(
+ showNotificationPage: Boolean,
+ canShowAddWidgetPage: Boolean,
+): List =
filter {
- if (it.cardType == OnboardingCardType.NOTIFICATION_PERMISSION) {
- showNotificationPage
- } else {
- true
+ when (it.cardType) {
+ OnboardingCardType.NOTIFICATION_PERMISSION -> {
+ showNotificationPage
+ }
+ OnboardingCardType.ADD_SEARCH_WIDGET -> {
+ canShowAddWidgetPage
+ }
+ else -> {
+ true
+ }
}
}.sortedBy { it.ordering }
.map { it.toPageUiData() }
@@ -36,6 +45,7 @@ private fun OnboardingCardType.toPageUiDataType() = when (this) {
OnboardingCardType.DEFAULT_BROWSER -> OnboardingPageUiData.Type.DEFAULT_BROWSER
OnboardingCardType.SYNC_SIGN_IN -> OnboardingPageUiData.Type.SYNC_SIGN_IN
OnboardingCardType.NOTIFICATION_PERMISSION -> OnboardingPageUiData.Type.NOTIFICATION_PERMISSION
+ OnboardingCardType.ADD_SEARCH_WIDGET -> OnboardingPageUiData.Type.ADD_SEARCH_WIDGET
}
/**
@@ -52,6 +62,8 @@ internal fun mapToOnboardingPageState(
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onNotificationPermissionSkipClick: () -> Unit,
+ onAddFirefoxWidgetClick: () -> Unit,
+ onAddFirefoxWidgetSkipClick: () -> Unit,
): OnboardingPageState = when (onboardingPageUiData.type) {
OnboardingPageUiData.Type.DEFAULT_BROWSER -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
@@ -60,6 +72,13 @@ internal fun mapToOnboardingPageState(
onUrlClick = onPrivacyPolicyClick,
)
+ OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> createOnboardingPageState(
+ onboardingPageUiData = onboardingPageUiData,
+ onPositiveButtonClick = onAddFirefoxWidgetClick,
+ onNegativeButtonClick = onAddFirefoxWidgetSkipClick,
+ onUrlClick = onPrivacyPolicyClick,
+ )
+
OnboardingPageUiData.Type.SYNC_SIGN_IN -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onSignInButtonClick,
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingScreen.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingScreen.kt
index 3f97e6fb14..5eea74fdc6 100644
--- a/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingScreen.kt
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/JunoOnboardingScreen.kt
@@ -19,6 +19,8 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
@@ -30,12 +32,15 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.PagerIndicator
import org.mozilla.fenix.compose.annotation.LightDarkPreview
+import org.mozilla.fenix.onboarding.JunoOnboardingTelemetryRecorder
+import org.mozilla.fenix.onboarding.WidgetPinnedReceiver.WidgetPinnedState
import org.mozilla.fenix.theme.FirefoxTheme
/**
@@ -50,11 +55,13 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onNotificationPermissionButtonClick Invoked when positive button on notification page is
* clicked.
* @param onSkipNotificationClick Invoked when negative button on notification page is clicked.
+ * @param onAddFirefoxWidgetClick Invoked when positive button on add search widget page is clicked.
+ * @param onSkipFirefoxWidgetClick Invoked when negative button on add search widget page is clicked.
* @param onFinish Invoked when the onboarding is completed.
* @param onImpression Invoked when a page in the pager is displayed.
*/
@Composable
-@Suppress("LongParameterList")
+@Suppress("LongParameterList", "LongMethod")
fun JunoOnboardingScreen(
pagesToDisplay: List,
onMakeFirefoxDefaultClick: () -> Unit,
@@ -64,6 +71,8 @@ fun JunoOnboardingScreen(
onSkipSignInClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onSkipNotificationClick: () -> Unit,
+ onAddFirefoxWidgetClick: () -> Unit,
+ onSkipFirefoxWidgetClick: () -> Unit,
onFinish: (pageType: OnboardingPageUiData) -> Unit,
onImpression: (pageType: OnboardingPageUiData) -> Unit,
) {
@@ -71,6 +80,9 @@ fun JunoOnboardingScreen(
val pagerState = rememberPagerState()
val isSignedIn: State = components.backgroundServices.syncStore
.observeAsComposableState { it.account != null }
+ val telemetryRecorder by lazy { JunoOnboardingTelemetryRecorder() }
+ val widgetPinnedFlow: StateFlow = WidgetPinnedState.isPinned
+ val isWidgetPinnedState by widgetPinnedFlow.collectAsState()
BackHandler(enabled = pagerState.currentPage > 0) {
coroutineScope.launch {
@@ -100,6 +112,16 @@ fun JunoOnboardingScreen(
}
}
+ LaunchedEffect(isWidgetPinnedState) {
+ if (isWidgetPinnedState) {
+ scrollToNextPageOrDismiss()
+ telemetryRecorder.onAddSearchWidgetClick(
+ pagesToDisplay.telemetrySequenceId(),
+ pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.ADD_SEARCH_WIDGET),
+ )
+ }
+ }
+
JunoOnboardingContent(
pagesToDisplay = pagesToDisplay,
pagerState = pagerState,
@@ -130,6 +152,17 @@ fun JunoOnboardingScreen(
scrollToNextPageOrDismiss()
onSkipNotificationClick()
},
+ onAddFirefoxWidgetClick = {
+ if (isWidgetPinnedState) {
+ scrollToNextPageOrDismiss()
+ } else {
+ onAddFirefoxWidgetClick()
+ }
+ },
+ onSkipFirefoxWidgetClick = {
+ scrollToNextPageOrDismiss()
+ onSkipFirefoxWidgetClick()
+ },
)
}
@@ -145,6 +178,8 @@ private fun JunoOnboardingContent(
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
onNotificationPermissionSkipClick: () -> Unit,
+ onAddFirefoxWidgetClick: () -> Unit,
+ onSkipFirefoxWidgetClick: () -> Unit,
) {
val nestedScrollConnection = remember { DisableForwardSwipeNestedScrollConnection(pagerState) }
@@ -172,6 +207,8 @@ private fun JunoOnboardingContent(
onSignInSkipClick = onSignInSkipClick,
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
onNotificationPermissionSkipClick = onNotificationPermissionSkipClick,
+ onAddFirefoxWidgetClick = onAddFirefoxWidgetClick,
+ onAddFirefoxWidgetSkipClick = onSkipFirefoxWidgetClick,
)
OnboardingPage(pageState = onboardingPageState)
}
@@ -224,6 +261,8 @@ private fun JunoOnboardingScreenPreview() {
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
+ onAddFirefoxWidgetClick = {},
+ onSkipFirefoxWidgetClick = {},
)
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt
index 9c0b357fcb..10dc40e033 100644
--- a/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/view/OnboardingPageUiData.kt
@@ -32,6 +32,9 @@ data class OnboardingPageUiData(
SYNC_SIGN_IN(
telemetryId = "sync",
),
+ ADD_SEARCH_WIDGET(
+ telemetryId = "search_widget",
+ ),
NOTIFICATION_PERMISSION(
telemetryId = "notification",
),
diff --git a/app/src/main/res/drawable/ic_onboarding_search_widget.xml b/app/src/main/res/drawable/ic_onboarding_search_widget.xml
new file mode 100644
index 0000000000..75dc5ee5eb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_search_widget.xml
@@ -0,0 +1,293 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7441cbfc7b..12d94fe152 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -330,6 +330,17 @@
Turn on notifications
Not now
+
+ Try the Firefox search widget
+
+ With Firefox on your home screen, you’ll have easy access to the privacy-first browser that blocks cross-site trackers.
+
+ Add Firefox widget
+
+ Not now
diff --git a/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt b/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt
index ff2278636b..a48b4edd39 100644
--- a/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/onboarding/view/JunoOnboardingMapperTest.kt
@@ -45,6 +45,8 @@ class JunoOnboardingMapperTest {
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
+ onAddFirefoxWidgetClick = {},
+ onAddFirefoxWidgetSkipClick = {},
)
assertEquals(expected, actual)
@@ -78,6 +80,8 @@ class JunoOnboardingMapperTest {
onSignInSkipClick = unitLambda,
onNotificationPermissionButtonClick = {},
onNotificationPermissionSkipClick = {},
+ onAddFirefoxWidgetClick = {},
+ onAddFirefoxWidgetSkipClick = {},
)
assertEquals(expected, actual)
@@ -111,6 +115,48 @@ class JunoOnboardingMapperTest {
onSignInSkipClick = {},
onNotificationPermissionButtonClick = unitLambda,
onNotificationPermissionSkipClick = unitLambda,
+ onAddFirefoxWidgetClick = {},
+ onAddFirefoxWidgetSkipClick = {},
+ )
+
+ assertEquals(expected, actual)
+ }
+
+ @Test
+ fun `GIVEN an add search widget page WHEN mapToOnboardingPageState is called THEN creates the expected OnboardingPageState`() {
+ val expected = OnboardingPageState(
+ imageRes = R.drawable.ic_onboarding_search_widget,
+ title = "add search widget title",
+ description = "add search widget body with link text",
+ linkTextState = LinkTextState(
+ text = "link text",
+ url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
+ onClick = stringLambda,
+ ),
+ primaryButton = Action("add search widget primary button text", unitLambda),
+ secondaryButton = Action("add search widget secondary button text", unitLambda),
+ )
+
+ val onboardingPageUiData = OnboardingPageUiData(
+ type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
+ imageRes = R.drawable.ic_onboarding_search_widget,
+ title = "add search widget title",
+ description = "add search widget body with link text",
+ linkText = "link text",
+ primaryButtonLabel = "add search widget primary button text",
+ secondaryButtonLabel = "add search widget secondary button text",
+ )
+ val actual = mapToOnboardingPageState(
+ onboardingPageUiData = onboardingPageUiData,
+ onMakeFirefoxDefaultClick = {},
+ onMakeFirefoxDefaultSkipClick = {},
+ onPrivacyPolicyClick = stringLambda,
+ onSignInButtonClick = {},
+ onSignInSkipClick = {},
+ onNotificationPermissionButtonClick = {},
+ onNotificationPermissionSkipClick = {},
+ onAddFirefoxWidgetClick = unitLambda,
+ onAddFirefoxWidgetSkipClick = unitLambda,
)
assertEquals(expected, actual)