[fenix] Bug 1808874 - Add notification pre permission prompt

pull/600/head
rahulsainani 1 year ago committed by mergify[bot]
parent 863e8df3c9
commit af78546293

@ -67,6 +67,11 @@ object FeatureFlags {
*/
const val saveToPDF = true
/**
* Enables the notification pre permission prompt.
*/
val notificationPrePermissionPromptEnabled = Config.channel.isNightlyOrDebug
/**
* Enables storage maintenance feature.
*

@ -80,7 +80,6 @@ import org.mozilla.fenix.ext.isKnownSearchDomain
import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline
@ -369,16 +368,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}
// For Android 13 or above, prompt the user for notification permission at the start.
// Regardless if the user accepts or denies the permission prompt, the prompt will occur only once.
fun queueNotificationPermissionRequest() {
if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queue.runIfReadyOrQueue {
ensureMarketingChannelExists(this)
}
}
}
initQueue()
// We init these items in the visual completeness queue to avoid them initing in the critical
@ -388,7 +377,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
queueReviewPrompt()
queueRestoreLocale()
queueStorageMaintenance()
queueNotificationPermissionRequest()
}
private fun startMetricsIfEnabled() {

@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
@ -85,6 +86,7 @@ import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
@ -107,6 +109,7 @@ import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance
@ -320,6 +323,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
showNotificationPermissionPromptIfRequired()
components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
lifecycleScope.launch(IO) {
// If we're authenticated, kick-off a sync and a device state refresh.
@ -337,6 +342,25 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
/**
* On Android 13 or above, prompt the user for notification permission at the start.
* Show the pre permission dialog to the user once if the notification are not enabled.
*/
private fun showNotificationPermissionPromptIfRequired() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
!NotificationManagerCompat.from(applicationContext).areNotificationsEnabledSafe()
) {
if (settings().notificationPrePermissionPromptEnabled) {
if (!settings().isNotificationPrePermissionShown && settings().numberOfAppLaunches <= 1) {
navHost.navController.navigate(NavGraphDirections.actionGlobalHomeNotificationPermissionDialog())
}
} else {
// This will trigger the notification permission system dialog as app targets sdk 32.
ensureMarketingChannelExists(applicationContext)
}
}
}
private fun checkAndExitPiP() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) {
// Exit PiP mode

@ -0,0 +1,64 @@
/* 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.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.DialogFragment
import com.google.accompanist.insets.ProvideWindowInsets
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.view.NotificationPermissionDialogScreen
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Dialog displaying notification pre-permission prompt.
*/
class HomeNotificationPermissionDialogFragment : DialogFragment() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.HomeOnboardingDialogStyle)
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
override fun onDestroy() {
super.onDestroy()
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
ProvideWindowInsets {
FirefoxTheme {
NotificationPermissionDialogScreen(
onDismiss = ::onDismiss,
grantNotificationPermission = {
ensureMarketingChannelExists(context.applicationContext)
onDismiss()
},
)
}
}
}
}
private fun onDismiss() {
dismiss()
context?.settings()?.isNotificationPrePermissionShown = true
}
}

@ -0,0 +1,222 @@
/* 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.view
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
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.Column
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.insets.navigationBarsPadding
import com.google.accompanist.insets.statusBarsPadding
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.compose.button.SecondaryButton
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Model containing data for the [NotificationPermissionPage].
*
* @param image [DrawableRes] displayed on the page.
* @param title [StringRes] of the permission headline text.
* @param description [StringRes] of the permission body text.
* @param primaryButtonText [StringRes] of the primary button text.
* @param secondaryButtonText [StringRes] of the secondary button text.
* @param onRecordImpressionEvent Callback for recording impression event.
*/
private data class NotificationPermissionPageState(
@DrawableRes val image: Int,
@StringRes val title: Int,
@StringRes val description: Int,
@StringRes val primaryButtonText: Int,
@StringRes val secondaryButtonText: Int? = null,
val onRecordImpressionEvent: () -> Unit,
)
/**
* A screen for displaying notification pre permission prompt.
*
* @param onDismiss Invoked when the user clicks on the close or the negative button.
* @param grantNotificationPermission Invoked when the user clicks on the positive button.
*/
@Composable
fun NotificationPermissionDialogScreen(
onDismiss: () -> Unit,
grantNotificationPermission: () -> Unit,
) {
NotificationPermissionContent(
notificationPermissionPageState = NotificationPageState,
onDismiss = onDismiss,
onPrimaryButtonClick = grantNotificationPermission,
onSecondaryButtonClick = onDismiss,
)
}
@Composable
private fun NotificationPermissionContent(
notificationPermissionPageState: NotificationPermissionPageState,
onDismiss: () -> Unit,
onPrimaryButtonClick: () -> Unit,
onSecondaryButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(Modifier.fillMaxSize()) {
val boxWithConstraintsScope = this
Column(
modifier = modifier
.background(FirefoxTheme.colors.layer1)
.fillMaxSize()
.padding(bottom = 32.dp)
.statusBarsPadding()
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
IconButton(
onClick = onDismiss,
modifier = Modifier.align(Alignment.End),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(R.string.content_description_close_button),
tint = FirefoxTheme.colors.iconPrimary,
)
}
NotificationPermissionPage(
pageState = notificationPermissionPageState,
onPrimaryButtonClick = onPrimaryButtonClick,
onSecondaryButtonClick = onSecondaryButtonClick,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
imageModifier = Modifier
.height(boxWithConstraintsScope.maxHeight.times(IMAGE_HEIGHT_RATIO)),
)
}
}
}
/**
* A page for displaying Notification Permission Content.
*
* @param pageState The page content that's displayed.
* @param onPrimaryButtonClick Invoked when the user clicks the primary button.
* @param onSecondaryButtonClick Invoked when the user clicks the secondary button.
* @param modifier The modifier to be applied to the Composable.
*/
@Composable
private fun NotificationPermissionPage(
pageState: NotificationPermissionPageState,
onPrimaryButtonClick: () -> Unit,
onSecondaryButtonClick: () -> Unit,
modifier: Modifier = Modifier,
imageModifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Image(
painter = painterResource(id = pageState.image),
contentDescription = null,
modifier = imageModifier,
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(
id = pageState.title,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
color = FirefoxTheme.colors.textPrimary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.headline5,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(
id = pageState.description,
formatArgs = arrayOf(stringResource(R.string.app_name)),
),
color = FirefoxTheme.colors.textSecondary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.body2,
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(top = 16.dp),
) {
PrimaryButton(
text = stringResource(id = pageState.primaryButtonText),
onClick = onPrimaryButtonClick,
)
if (pageState.secondaryButtonText != null) {
Spacer(modifier = Modifier.height(8.dp))
SecondaryButton(
text = stringResource(id = pageState.secondaryButtonText),
onClick = onSecondaryButtonClick,
)
}
}
}
LaunchedEffect(pageState) {
pageState.onRecordImpressionEvent()
}
}
private val NotificationPageState = NotificationPermissionPageState(
image = R.drawable.ic_notification_permission,
title = R.string.onboarding_home_enable_notifications_title,
description = R.string.onboarding_home_enable_notifications_description,
primaryButtonText = R.string.onboarding_home_enable_notifications_positive_button,
secondaryButtonText = R.string.onboarding_home_enable_notifications_negative_button,
onRecordImpressionEvent = {},
)
private const val IMAGE_HEIGHT_RATIO = 0.4f
@Preview
@Composable
private fun NotificationPermissionScreenPreview() {
FirefoxTheme {
NotificationPermissionDialogScreen(
grantNotificationPermission = {},
onDismiss = { },
)
}
}

@ -1471,6 +1471,23 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = setOf(),
)
/**
* Indicates if notification pre permission prompt feature is enabled.
*/
var notificationPrePermissionPromptEnabled by lazyFeatureFlagPreference(
key = appContext.getPreferenceKey(R.string.pref_key_notification_pre_permission_prompt_enabled),
default = { FxNimbus.features.prePermissionNotificationPrompt.value(appContext).enabled },
featureFlag = FeatureFlags.notificationPrePermissionPromptEnabled,
)
/**
* Indicates if notification permission prompt has been shown to the user.
*/
var isNotificationPrePermissionShown by booleanPreference(
key = appContext.getPreferenceKey(R.string.pref_key_is_notification_pre_permission_prompt_shown),
default = false,
)
/**
* Get the current mode for how https-only is enabled.
*/

File diff suppressed because one or more lines are too long

@ -33,6 +33,10 @@
android:id="@+id/action_global_home_onboarding_dialog"
app:destination="@id/homeOnboardingDialogFragment" />
<action
android:id="@+id/action_global_home_notification_permission_dialog"
app:destination="@id/homeNotificationPermissionDialogFragment" />
<action
android:id="@+id/action_global_wallpaper_onboarding_dialog"
app:destination="@id/wallpaperOnboardingDialogFragment"
@ -196,6 +200,10 @@
android:id="@+id/homeOnboardingDialogFragment"
android:name="org.mozilla.fenix.onboarding.HomeOnboardingDialogFragment" />
<dialog
android:id="@+id/homeNotificationPermissionDialogFragment"
android:name="org.mozilla.fenix.onboarding.HomeNotificationPermissionDialogFragment" />
<dialog
android:id="@+id/wallpaperOnboardingDialogFragment"
android:name="org.mozilla.fenix.onboarding.WallpaperOnboardingDialogFragment" />

@ -321,4 +321,8 @@
<string name="pref_key_growth_first_week_series_sent" translatable="false">pref_key_growth_first_week_series_sent</string>
<string name="pref_key_growth_first_week_days_of_use" translatable="false">pref_key_growth_first_week_days_of_use</string>
<string name="pref_key_growth_ad_click_sent" translatable="false">pref_key_growth_ad_click_sent</string>
<!-- Notification Pre Permission Prompt -->
<string name="pref_key_notification_pre_permission_prompt_enabled">pref_key_notification_pre_permission_prompt_enabled</string>
<string name="pref_key_is_notification_pre_permission_prompt_shown">pref_key_is_notification_pre_permission_prompt_shown</string>
</resources>

@ -277,14 +277,14 @@
<!-- Notification pre-permission dialog -->
<!-- Enable notification pre permission dialog title
The first parameter is the name of the app defined in app_name (for example: Fenix) -->
<string name="onboarding_home_enable_notifications_title" tools:ignore="UnusedResources">Notifications help you do more with %s</string>
<string name="onboarding_home_enable_notifications_title">Notifications help you do more with %s</string>
<!-- Enable notification pre permission dialog description with rationale
The first parameter is the name of the app defined in app_name (for example: Fenix) -->
<string name="onboarding_home_enable_notifications_description" tools:ignore="UnusedResources">Sync your tabs between devices, manage downloads, get tips about making the most of %ss privacy protection, and more.</string>
<string name="onboarding_home_enable_notifications_description">Sync your tabs between devices, manage downloads, get tips about making the most of %ss privacy protection, and more.</string>
<!-- Text for the button to request notification permission on the device -->
<string name="onboarding_home_enable_notifications_positive_button" tools:ignore="UnusedResources">Continue</string>
<string name="onboarding_home_enable_notifications_positive_button">Continue</string>
<!-- Text for the button to not request notification permission on the device and dismiss the dialog -->
<string name="onboarding_home_enable_notifications_negative_button" tools:ignore="UnusedResources">Not now</string>
<string name="onboarding_home_enable_notifications_negative_button">Not now</string>
<!-- Search Widget -->
<!-- Content description for searching with a widget. The first parameter is the name of the application.-->

Loading…
Cancel
Save