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: