Close #27949: Add engagement notification for inactive users

fork
Roger Yang 2 years ago committed by mergify[bot]
parent 9b920a472c
commit 11efaff96c

@ -71,6 +71,14 @@ nimbus-validation:
settings-title: settings-title:
type: string type: string
description: The title of displayed in the Settings screen and app menu. description: The title of displayed in the Settings screen and app menu.
re-engagement-notification:
description: A feature that shows the re-enagement notification if the user is inactive.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the re-engagement notification is shown to the inactive user."
search-term-groups: search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from. description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true hasExposure: true

@ -206,6 +206,32 @@ events:
- https://github.com/mozilla-mobile/fenix/issues/27779 - https://github.com/mozilla-mobile/fenix/issues/27779
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27780 - https://github.com/mozilla-mobile/fenix/pull/27780
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
re_engagement_notif_tapped:
type: event
description: |
User tapped on the re-engagement notification
bugs:
- https://github.com/mozilla-mobile/fenix/issues/27949
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27978
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
re_engagement_notif_shown:
type: event
description: |
Re-engagement notification was shown to the user
bugs:
- https://github.com/mozilla-mobile/fenix/issues/27949
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27978
data_sensitivity: data_sensitivity:
- technical - technical
notification_emails: notification_emails:

@ -103,6 +103,7 @@ import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDir
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.Performance
@ -377,6 +378,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.appStore.dispatch(AppAction.ResumedMetricsAction) components.appStore.dispatch(AppAction.ResumedMetricsAction)
DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext) DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
} }
} }

@ -261,6 +261,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
subscribeToTabCollections() subscribeToTabCollections()
updateLastBrowseActivity()
} }
override fun onStop() { override fun onStop() {

@ -6,12 +6,17 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker.Companion.isDefaultBrowserNotificationIntent import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker.Companion.isDefaultBrowserNotificationIntent
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker.Companion.isReEngagementNotificationIntent
/** /**
* When the default browser notification is tapped we need to launch [openSetDefaultBrowserOption] * When the default browser notification is tapped we need to launch [openSetDefaultBrowserOption]
@ -24,12 +29,26 @@ class DefaultBrowserIntentProcessor(
) : HomeIntentProcessor { ) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (isDefaultBrowserNotificationIntent(intent)) { return when {
activity.openSetDefaultBrowserOption() isDefaultBrowserNotificationIntent(intent) -> {
Events.defaultBrowserNotifTapped.record(NoExtras()) Events.defaultBrowserNotifTapped.record(NoExtras())
true
} else { activity.openSetDefaultBrowserOption()
false true
}
isReEngagementNotificationIntent(intent) -> {
Events.reEngagementNotifTapped.record(NoExtras())
activity.browsingModeManager.mode = BrowsingMode.Private
activity.openToBrowserAndLoad(
ReEngagementNotificationWorker.NOTIFICATION_TARGET_URL,
newTab = true,
from = BrowserDirection.FromGlobal,
flags = EngineSession.LoadUrlFlags.external(),
)
true
}
else -> false
} }
} }
} }

@ -5,12 +5,9 @@
package org.mozilla.fenix.onboarding package org.mozilla.fenix.onboarding
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -22,7 +19,6 @@ import androidx.work.WorkerParameters
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.ids.SharedIdsHelper import mozilla.components.support.base.ids.SharedIdsHelper
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -36,9 +32,15 @@ class DefaultBrowserNotificationWorker(
) : Worker(context, workerParameters) { ) : Worker(context, workerParameters) {
override fun doWork(): Result { override fun doWork(): Result {
val channelId = ensureChannelExists() val channelId = ensureMarketingChannelExists(applicationContext)
NotificationManagerCompat.from(applicationContext) NotificationManagerCompat.from(applicationContext)
.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(channelId)) .notify(
NOTIFICATION_TAG,
DEFAULT_BROWSER_NOTIFICATION_ID,
buildNotification(channelId),
)
Events.defaultBrowserNotifShown.record(NoExtras()) Events.defaultBrowserNotifShown.record(NoExtras())
// default browser notification should only happen once // default browser notification should only happen once
@ -81,45 +83,7 @@ class DefaultBrowserNotificationWorker(
} }
} }
/**
* Make sure a notification channel for default browser notification exists.
*
* Returns the channel id to be used for notifications.
*/
private fun ensureChannelExists(): String {
var channelEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
applicationContext.getString(R.string.notification_marketing_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
)
notificationManager.createNotificationChannel(channel)
val existingChannel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
channelEnabled =
existingChannel != null && existingChannel.importance != NotificationManager.IMPORTANCE_NONE
}
@Suppress("TooGenericExceptionCaught")
val notificationsEnabled = try {
NotificationManagerCompat.from(applicationContext).areNotificationsEnabled()
} catch (e: Exception) {
false
}
marketingNotificationAllowed.set(notificationsEnabled && channelEnabled)
return NOTIFICATION_CHANNEL_ID
}
companion object { companion object {
private const val NOTIFICATION_CHANNEL_ID = "org.mozilla.fenix.default.browser.channel"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.default.browser" private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.default.browser"
private const val INTENT_DEFAULT_BROWSER_NOTIFICATION = "org.mozilla.fenix.default.browser.intent" private const val INTENT_DEFAULT_BROWSER_NOTIFICATION = "org.mozilla.fenix.default.browser.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.default.browser.tag" private const val NOTIFICATION_TAG = "org.mozilla.fenix.default.browser.tag"

@ -0,0 +1,60 @@
/* 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.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.R
// Channel ID was not updated when it was renamed to marketing. Thus, we'll have to continue
// to use this ID as the marketing channel ID
private const val MARKETING_CHANNEL_ID = "org.mozilla.fenix.default.browser.channel"
// For notification that uses the marketing notification channel, IDs should be unique.
const val DEFAULT_BROWSER_NOTIFICATION_ID = 1
const val RE_ENGAGEMENT_NOTIFICATION_ID = 2
/**
* Make sure the marketing notification channel exists.
*
* Returns the channel id to be used for notifications.
*/
fun ensureMarketingChannelExists(context: Context): String {
var channelEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
var channel =
notificationManager.getNotificationChannel(MARKETING_CHANNEL_ID)
if (channel == null) {
channel = NotificationChannel(
MARKETING_CHANNEL_ID,
context.getString(R.string.notification_marketing_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
)
notificationManager.createNotificationChannel(channel)
}
channelEnabled = channel.importance != NotificationManager.IMPORTANCE_NONE
}
@Suppress("TooGenericExceptionCaught")
val notificationsEnabled = try {
NotificationManagerCompat.from(context).areNotificationsEnabled()
} catch (e: Exception) {
false
}
marketingNotificationAllowed.set(notificationsEnabled && channelEnabled)
return MARKETING_CHANNEL_ID
}

@ -0,0 +1,139 @@
/* 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.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import mozilla.components.support.base.ids.SharedIdsHelper
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.Settings
import java.util.concurrent.TimeUnit
/**
* Worker that builds and schedules the re-engagement notification
*/
class ReEngagementNotificationWorker(
context: Context,
workerParameters: WorkerParameters,
) : Worker(context, workerParameters) {
override fun doWork(): Result {
val settings = applicationContext.settings()
if (isActiveUser(settings) || !settings.shouldShowReEngagementNotification()) {
return Result.success()
}
val channelId = ensureMarketingChannelExists(applicationContext)
NotificationManagerCompat.from(applicationContext)
.notify(
NOTIFICATION_TAG,
RE_ENGAGEMENT_NOTIFICATION_ID,
buildNotification(channelId),
)
// re-engagement notification should only be shown once
settings.reEngagementNotificationShown = true
Events.reEngagementNotifShown.record(NoExtras())
return Result.success()
}
private fun buildNotification(channelId: String): Notification {
val intent = Intent(applicationContext, HomeActivity::class.java)
intent.putExtra(INTENT_RE_ENGAGEMENT_NOTIFICATION, true)
val pendingIntent = PendingIntent.getActivity(
applicationContext,
SharedIdsHelper.getNextIdForTag(applicationContext, NOTIFICATION_PENDING_INTENT_TAG),
intent,
IntentUtils.defaultIntentPendingFlags,
)
with(applicationContext) {
val appName = getString(R.string.app_name)
return NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_status_logo)
.setContentTitle(
applicationContext.getString(R.string.notification_re_engagement_title),
)
.setContentText(
applicationContext.getString(R.string.notification_re_engagement_text, appName),
)
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
.setColor(ContextCompat.getColor(this, R.color.primary_text_light_theme))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setShowWhen(false)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
}
}
companion object {
const val NOTIFICATION_TARGET_URL = "https://www.mozilla.org/firefox/privacy/"
private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.re-engagement"
private const val INTENT_RE_ENGAGEMENT_NOTIFICATION = "org.mozilla.fenix.re-engagement.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.re-engagement.tag"
private const val NOTIFICATION_WORK_NAME = "org.mozilla.fenix.re-engagement.work"
private const val NOTIFICATION_DELAY = Settings.TWO_DAYS_MS
// We are trying to reach the users that are inactive after the initial 24 hours
private const val INACTIVE_USER_THRESHOLD = NOTIFICATION_DELAY - Settings.ONE_DAY_MS
/**
* Check if the intent is from the re-engagement notification
*/
fun isReEngagementNotificationIntent(intent: Intent) =
intent.extras?.containsKey(INTENT_RE_ENGAGEMENT_NOTIFICATION) ?: false
/**
* Schedules the re-engagement notification if needed.
*/
fun setReEngagementNotificationIfNeeded(context: Context) {
val instanceWorkManager = WorkManager.getInstance(context)
if (!context.settings().shouldSetReEngagementNotification()) {
return
}
val notificationWork = OneTimeWorkRequest.Builder(ReEngagementNotificationWorker::class.java)
.setInitialDelay(NOTIFICATION_DELAY, TimeUnit.MILLISECONDS)
.build()
instanceWorkManager.beginUniqueWork(
NOTIFICATION_WORK_NAME,
ExistingWorkPolicy.KEEP,
notificationWork,
).enqueue()
}
@VisibleForTesting
internal fun isActiveUser(settings: Settings): Boolean {
if (System.currentTimeMillis() - settings.lastBrowseActivity > INACTIVE_USER_THRESHOLD) {
return false
}
return true
}
}
}

@ -77,6 +77,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
const val FOUR_HOURS_MS = 60 * 60 * 4 * 1000L const val FOUR_HOURS_MS = 60 * 60 * 4 * 1000L
const val ONE_DAY_MS = 60 * 60 * 24 * 1000L const val ONE_DAY_MS = 60 * 60 * 24 * 1000L
const val TWO_DAYS_MS = 2 * ONE_DAY_MS
const val THREE_DAYS_MS = 3 * ONE_DAY_MS const val THREE_DAYS_MS = 3 * ONE_DAY_MS
const val ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L const val ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L
const val ONE_MONTH_MS = (60 * 60 * 24 * 365 * 1000L) / 12 const val ONE_MONTH_MS = (60 * 60 * 24 * 365 * 1000L) / 12
@ -405,10 +406,13 @@ class Settings(private val appContext: Context) : PreferencesHolder {
* Indicates the last time when the user was interacting with the [BrowserFragment], * Indicates the last time when the user was interacting with the [BrowserFragment],
* This is useful to determine if the user has to start on the [HomeFragment] * This is useful to determine if the user has to start on the [HomeFragment]
* or it should go directly to the [BrowserFragment]. * or it should go directly to the [BrowserFragment].
*
* This value defaults to 0L because we want to know if the user never had any interaction
* with the [BrowserFragment]
*/ */
var lastBrowseActivity by longPreference( var lastBrowseActivity by longPreference(
appContext.getPreferenceKey(R.string.pref_key_last_browse_activity_time), appContext.getPreferenceKey(R.string.pref_key_last_browse_activity_time),
default = timeNowInMillis(), default = 0L,
) )
/** /**
@ -579,6 +583,34 @@ class Settings(private val appContext: Context) : PreferencesHolder {
return !defaultBrowserNotificationDisplayed && !isDefaultBrowserBlocking() return !defaultBrowserNotificationDisplayed && !isDefaultBrowserBlocking()
} }
var reEngagementNotificationShown by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_re_engagement_notification_shown),
default = false,
)
/**
* Check if we should set the re-engagement notification.
*/
fun shouldSetReEngagementNotification(): Boolean {
return numberOfAppLaunches <= 1 && !reEngagementNotificationShown
}
/**
* Check if we should show the re-engagement notification.
*/
fun shouldShowReEngagementNotification(): Boolean {
return !reEngagementNotificationShown && reEngagementNotificationEnabled && !isDefaultBrowserBlocking()
}
/**
* Indicates if the re-engagement notification feature is enabled
*/
var reEngagementNotificationEnabled by lazyFeatureFlagPreference(
key = appContext.getPreferenceKey(R.string.pref_key_re_engagement_notification_enabled),
default = { FxNimbus.features.reEngagementNotification.value(appContext).enabled },
featureFlag = true,
)
val shouldUseAutoBatteryTheme by booleanPreference( val shouldUseAutoBatteryTheme by booleanPreference(
appContext.getPreferenceKey(R.string.pref_key_auto_battery_theme), appContext.getPreferenceKey(R.string.pref_key_auto_battery_theme),
default = false, default = false,

@ -67,6 +67,8 @@
<string name="pref_key_last_browse_activity_time" translatable="false">pref_key_last_browse_activity_time</string> <string name="pref_key_last_browse_activity_time" translatable="false">pref_key_last_browse_activity_time</string>
<string name="pref_key_last_cfr_shown_time" translatable="false">pref_key_last_cfr_shown_time</string> <string name="pref_key_last_cfr_shown_time" translatable="false">pref_key_last_cfr_shown_time</string>
<string name="pref_key_should_show_default_browser_notification" translatable="false">pref_key_should_show_default_browser_notification</string> <string name="pref_key_should_show_default_browser_notification" translatable="false">pref_key_should_show_default_browser_notification</string>
<string name="pref_key_re_engagement_notification_shown" translatable="false">pref_key_re_engagement_notification_shown</string>
<string name="pref_key_re_engagement_notification_enabled" translatable="false">pref_key_re_engagement_notification_enabled</string>
<string name="pref_key_is_first_run" translatable="false">pref_key_is_first_run</string> <string name="pref_key_is_first_run" translatable="false">pref_key_is_first_run</string>
<string name="pref_key_home_blocklist">pref_key_home_blocklist</string> <string name="pref_key_home_blocklist">pref_key_home_blocklist</string>

@ -1072,6 +1072,11 @@
<!-- Text shown in the notification that pops up to remind the user to set fenix as default browser. <!-- Text shown in the notification that pops up to remind the user to set fenix as default browser.
%1$s is a placeholder that will be replaced by the app name (Fenix). --> %1$s is a placeholder that will be replaced by the app name (Fenix). -->
<string name="notification_default_browser_text">Make %1$s your default browser</string> <string name="notification_default_browser_text">Make %1$s your default browser</string>
<!-- Title shown in the notification that pops up to re-engage the user -->
<string name="notification_re_engagement_title">Try private browsing</string>
<!-- Text shown in the notification that pops up to re-engage the user.
%1$s is a placeholder that will be replaced by the app name. -->
<string name="notification_re_engagement_text">Browse with no saved cookies or history in %1$s</string>
<!-- Snackbar --> <!-- Snackbar -->
<!-- Text shown in snackbar when user deletes a collection --> <!-- Text shown in snackbar when user deletes a collection -->

@ -10,6 +10,7 @@ import io.mockk.Called
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
import mozilla.components.concept.engine.EngineSession
import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -18,9 +19,12 @@ import org.junit.Assert.assertNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
@RunWith(FenixRobolectricTestRunner::class) @RunWith(FenixRobolectricTestRunner::class)
class DefaultBrowserIntentProcessorTest { class DefaultBrowserIntentProcessorTest {
@ -63,4 +67,42 @@ class DefaultBrowserIntentProcessorTest {
verify { navController wasNot Called } verify { navController wasNot Called }
verify { out wasNot Called } verify { out wasNot Called }
} }
@Test
fun `process re-engagement notification intents`() {
val navController: NavController = mockk(relaxed = true)
val out: Intent = mockk()
val activity: HomeActivity = mockk(relaxed = true)
val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
val intent = Intent().apply {
putExtra("org.mozilla.fenix.re-engagement.intent", true)
}
every { activity.applicationContext } returns testContext
every { activity.browsingModeManager } returns browsingModeManager
assertNull(Events.reEngagementNotifTapped.testGetValue())
val result = DefaultBrowserIntentProcessor(activity)
.process(intent, navController, out)
assert(result)
assertNotNull(Events.reEngagementNotifTapped.testGetValue())
verify {
activity.openToBrowserAndLoad(
searchTermOrURL = ReEngagementNotificationWorker.NOTIFICATION_TARGET_URL,
newTab = true,
from = BrowserDirection.FromGlobal,
customTabSessionId = null,
engine = null,
forceSearch = false,
flags = EngineSession.LoadUrlFlags.external(),
requestDesktopMode = false,
historyMetadata = null,
)
}
verify { navController wasNot Called }
verify { out wasNot Called }
}
} }

@ -0,0 +1,44 @@
/* 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 io.mockk.spyk
import junit.framework.TestCase.assertFalse
import mozilla.components.support.test.robolectric.testContext
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class ReEngagementNotificationWorkerTest {
lateinit var settings: Settings
@Before
fun setUp() {
settings = Settings(testContext)
}
@Test
fun `GIVEN last browser activity THEN determine if the user is active correctly`() {
val localSetting = spyk(settings)
localSetting.lastBrowseActivity = System.currentTimeMillis()
assert(ReEngagementNotificationWorker.isActiveUser(localSetting))
localSetting.lastBrowseActivity = System.currentTimeMillis() - Settings.FOUR_HOURS_MS
assert(ReEngagementNotificationWorker.isActiveUser(localSetting))
localSetting.lastBrowseActivity = System.currentTimeMillis() - Settings.ONE_DAY_MS
assertFalse(ReEngagementNotificationWorker.isActiveUser(localSetting))
localSetting.lastBrowseActivity = 0
assertFalse(ReEngagementNotificationWorker.isActiveUser(localSetting))
localSetting.lastBrowseActivity = -1000
assertFalse(ReEngagementNotificationWorker.isActiveUser(localSetting))
}
}

@ -768,6 +768,66 @@ class SettingsTest {
assertFalse(localSetting.shouldShowDefaultBrowserNotification()) assertFalse(localSetting.shouldShowDefaultBrowserNotification())
} }
@Test
fun `GIVEN re-engagement notification shown and number of app launch THEN should set re-engagement notification returns correct value`() {
val localSetting = spyk(settings)
localSetting.reEngagementNotificationShown = false
localSetting.numberOfAppLaunches = 0
assert(localSetting.shouldSetReEngagementNotification())
localSetting.numberOfAppLaunches = 1
assert(localSetting.shouldSetReEngagementNotification())
localSetting.numberOfAppLaunches = 2
assertFalse(localSetting.shouldSetReEngagementNotification())
localSetting.reEngagementNotificationShown = true
localSetting.numberOfAppLaunches = 0
assertFalse(localSetting.shouldSetReEngagementNotification())
}
@Test
fun `GIVEN re-engagement notification shown and is default browser THEN should show re-engagement notification returns correct value`() {
val localSetting = spyk(settings)
every { localSetting.isDefaultBrowserBlocking() } returns false
every { localSetting.reEngagementNotificationEnabled } returns false
localSetting.reEngagementNotificationShown = false
assertFalse(localSetting.shouldShowReEngagementNotification())
every { localSetting.reEngagementNotificationEnabled } returns true
localSetting.reEngagementNotificationShown = false
assert(localSetting.shouldShowReEngagementNotification())
every { localSetting.reEngagementNotificationEnabled } returns false
localSetting.reEngagementNotificationShown = true
assertFalse(localSetting.shouldShowReEngagementNotification())
every { localSetting.reEngagementNotificationEnabled } returns true
localSetting.reEngagementNotificationShown = true
assertFalse(localSetting.shouldShowReEngagementNotification())
every { localSetting.isDefaultBrowserBlocking() } returns true
every { localSetting.reEngagementNotificationEnabled } returns false
localSetting.reEngagementNotificationShown = false
assertFalse(localSetting.shouldShowReEngagementNotification())
every { localSetting.reEngagementNotificationEnabled } returns true
localSetting.reEngagementNotificationShown = false
assertFalse(localSetting.shouldShowReEngagementNotification())
every { localSetting.reEngagementNotificationEnabled } returns false
localSetting.reEngagementNotificationShown = true
assertFalse(localSetting.shouldShowReEngagementNotification())
every { localSetting.reEngagementNotificationEnabled } returns true
localSetting.reEngagementNotificationShown = true
assertFalse(localSetting.shouldShowReEngagementNotification())
}
@Test @Test
fun inactiveTabsAreEnabled() { fun inactiveTabsAreEnabled() {
// When just created // When just created

@ -257,6 +257,14 @@ features:
value: value:
enabled: true enabled: true
re-engagement-notification:
description: A feature that shows the re-enagement notification if the user is inactive.
variables:
enabled:
description: If true, the re-engagement notification is shown to the inactive user.
type: Boolean
default: true
types: types:
objects: objects:
MessageData: MessageData:

Loading…
Cancel
Save