diff --git a/.experimenter.yaml b/.experimenter.yaml
index 04d51d391..6ae24eaa4 100644
--- a/.experimenter.yaml
+++ b/.experimenter.yaml
@@ -71,6 +71,14 @@ nimbus-validation:
settings-title:
type: string
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:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true
diff --git a/app/metrics.yaml b/app/metrics.yaml
index 366853f34..9be2bed32 100644
--- a/app/metrics.yaml
+++ b/app/metrics.yaml
@@ -206,6 +206,32 @@ events:
- https://github.com/mozilla-mobile/fenix/issues/27779
data_reviews:
- 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:
- technical
notification_emails:
diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
index 36da2108d..aa972e5b8 100644
--- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
+++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt
@@ -103,6 +103,7 @@ import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDir
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.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance
@@ -377,6 +378,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.appStore.dispatch(AppAction.ResumedMetricsAction)
DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext)
+ ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
index f4e02b49a..ce42a1c24 100644
--- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
+++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt
@@ -261,6 +261,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
subscribeToTabCollections()
+ updateLastBrowseActivity()
}
override fun onStop() {
diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessor.kt
index 8952a240e..ae739089b 100644
--- a/app/src/main/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessor.kt
+++ b/app/src/main/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessor.kt
@@ -6,12 +6,17 @@ package org.mozilla.fenix.home.intent
import android.content.Intent
import androidx.navigation.NavController
+import mozilla.components.concept.engine.EngineSession
import mozilla.telemetry.glean.private.NoExtras
+import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.settings
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]
@@ -24,12 +29,26 @@ class DefaultBrowserIntentProcessor(
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
- return if (isDefaultBrowserNotificationIntent(intent)) {
- activity.openSetDefaultBrowserOption()
- Events.defaultBrowserNotifTapped.record(NoExtras())
- true
- } else {
- false
+ return when {
+ isDefaultBrowserNotificationIntent(intent) -> {
+ Events.defaultBrowserNotifTapped.record(NoExtras())
+
+ activity.openSetDefaultBrowserOption()
+ 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
}
}
}
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/DefaultBrowserNotificationWorker.kt b/app/src/main/java/org/mozilla/fenix/onboarding/DefaultBrowserNotificationWorker.kt
index 746554bfe..5cc3a105f 100644
--- a/app/src/main/java/org/mozilla/fenix/onboarding/DefaultBrowserNotificationWorker.kt
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/DefaultBrowserNotificationWorker.kt
@@ -5,12 +5,9 @@
package org.mozilla.fenix.onboarding
import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
-import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@@ -22,7 +19,6 @@ import androidx.work.WorkerParameters
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.ids.SharedIdsHelper
import org.mozilla.fenix.GleanMetrics.Events
-import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
@@ -36,9 +32,15 @@ class DefaultBrowserNotificationWorker(
) : Worker(context, workerParameters) {
override fun doWork(): Result {
- val channelId = ensureChannelExists()
+ val channelId = ensureMarketingChannelExists(applicationContext)
+
NotificationManagerCompat.from(applicationContext)
- .notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(channelId))
+ .notify(
+ NOTIFICATION_TAG,
+ DEFAULT_BROWSER_NOTIFICATION_ID,
+ buildNotification(channelId),
+ )
+
Events.defaultBrowserNotifShown.record(NoExtras())
// 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 {
- 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 INTENT_DEFAULT_BROWSER_NOTIFICATION = "org.mozilla.fenix.default.browser.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.default.browser.tag"
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/MarketingNotificationHelper.kt b/app/src/main/java/org/mozilla/fenix/onboarding/MarketingNotificationHelper.kt
new file mode 100644
index 000000000..980f1d95c
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/MarketingNotificationHelper.kt
@@ -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
+}
diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt b/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt
new file mode 100644
index 000000000..e58a6b737
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt
@@ -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
+ }
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
index fe406ec6a..12283f5b0 100644
--- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
+++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
@@ -77,6 +77,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
const val FOUR_HOURS_MS = 60 * 60 * 4 * 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 ONE_WEEK_MS = 60 * 60 * 24 * 7 * 1000L
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],
* This is useful to determine if the user has to start on the [HomeFragment]
* 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(
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()
}
+ 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(
appContext.getPreferenceKey(R.string.pref_key_auto_battery_theme),
default = false,
diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml
index d4055aa47..8c02486dd 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -67,6 +67,8 @@
pref_key_last_browse_activity_time
pref_key_last_cfr_shown_time
pref_key_should_show_default_browser_notification
+ pref_key_re_engagement_notification_shown
+ pref_key_re_engagement_notification_enabled
pref_key_is_first_run
pref_key_home_blocklist
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 412904735..0781ab472 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1072,6 +1072,11 @@
Make %1$s your default browser
+
+ Try private browsing
+
+ Browse with no saved cookies or history in %1$s
diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessorTest.kt
index 16a91aaab..b9be14dae 100644
--- a/app/src/test/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessorTest.kt
@@ -10,6 +10,7 @@ import io.mockk.Called
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
+import mozilla.components.concept.engine.EngineSession
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertFalse
@@ -18,9 +19,12 @@ import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
@RunWith(FenixRobolectricTestRunner::class)
class DefaultBrowserIntentProcessorTest {
@@ -63,4 +67,42 @@ class DefaultBrowserIntentProcessorTest {
verify { navController 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 }
+ }
}
diff --git a/app/src/test/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorkerTest.kt b/app/src/test/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorkerTest.kt
new file mode 100644
index 000000000..9b758f96c
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorkerTest.kt
@@ -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))
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
index da854c2ba..0b66c046d 100644
--- a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
@@ -768,6 +768,66 @@ class SettingsTest {
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
fun inactiveTabsAreEnabled() {
// When just created
diff --git a/nimbus.fml.yaml b/nimbus.fml.yaml
index 2e92cfe8f..4c9daba3f 100644
--- a/nimbus.fml.yaml
+++ b/nimbus.fml.yaml
@@ -257,6 +257,14 @@ features:
value:
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:
objects:
MessageData: