diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index d43ca5c4af..14de0b6676 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -86,6 +86,7 @@ import org.mozilla.fenix.ext.setNavigationIcon import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor +import org.mozilla.fenix.home.intent.DefaultBrowserIntentProcessor import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor @@ -94,6 +95,7 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections +import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.PerformanceInflater import org.mozilla.fenix.perf.ProfilerMarkers @@ -158,7 +160,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics), StartSearchIntentProcessor(components.analytics.metrics), OpenBrowserIntentProcessor(this, ::getIntentSessionId), - OpenSpecificTabIntentProcessor(this) + OpenSpecificTabIntentProcessor(this), + DefaultBrowserIntentProcessor(this) ) } @@ -337,6 +340,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } settings().wasDefaultBrowserOnLastResume = settings().isDefaultBrowser() + + DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext) } } 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 new file mode 100644 index 0000000000..8479bd169b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessor.kt @@ -0,0 +1,34 @@ +/* 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.home.intent + +import android.content.Intent +import androidx.navigation.NavController +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.ext.openSetDefaultBrowserOption +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker.Companion.isDefaultBrowserNotificationIntent + +/** + * When the default browser notification is tapped we need to launch [openSetDefaultBrowserOption] + * + * This should only happens once in a user's lifetime since once the user taps on the default browser + * notification, [settings.shouldShowDefaultBrowserNotification] will return false + */ +class DefaultBrowserIntentProcessor( + private val activity: HomeActivity +) : HomeIntentProcessor { + + override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { + return if (isDefaultBrowserNotificationIntent(intent)) { + activity.openSetDefaultBrowserOption() + activity.settings().defaultBrowserNotificationDisplayed = true + + 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 new file mode 100644 index 0000000000..eabed8de38 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/onboarding/DefaultBrowserNotificationWorker.kt @@ -0,0 +1,126 @@ +/* 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.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 +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import java.util.concurrent.TimeUnit +import mozilla.components.support.base.ids.SharedIdsHelper +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.utils.Settings + +class DefaultBrowserNotificationWorker( + context: Context, + workerParameters: WorkerParameters +) : Worker(context, workerParameters) { + + override fun doWork(): Result { + ensureChannelExists() + NotificationManagerCompat.from(applicationContext) + .notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification()) + return Result.success() + } + + /** + * Build the default browser notification. + */ + private fun buildNotification(): Notification { + val channelId = ensureChannelExists() + val intent = Intent(applicationContext, HomeActivity::class.java) + intent.putExtra(INTENT_DEFAULT_BROWSER_NOTIFICATION, true) + + val pendingIntent = PendingIntent.getActivity( + applicationContext, + SharedIdsHelper.getNextIdForTag(applicationContext, NOTIFICATION_PENDING_INTENT_TAG), + intent, + 0 + ) + + 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_default_browser_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() + } + } + + /** + * Make sure a notification channel for default browser notification exists. + * + * Returns the channel id to be used for notifications. + */ + private fun ensureChannelExists(): String { + 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) + } + + 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" + private const val NOTIFICATION_WORK_NAME = "org.mozilla.fenix.default.browser.work" + private const val NOTIFICATION_DELAY = Settings.ONE_DAY_MS + + fun isDefaultBrowserNotificationIntent(intent: Intent) = + intent.extras?.containsKey(INTENT_DEFAULT_BROWSER_NOTIFICATION) ?: false + + fun setDefaultBrowserNotificationIfNeeded(context: Context) { + val instanceWorkManager = WorkManager.getInstance(context) + + if (!context.settings().shouldShowDefaultBrowserNotification()) { + // cancel notification work if already default browser + instanceWorkManager.cancelUniqueWork(NOTIFICATION_WORK_NAME) + return + } + + val notificationWork = OneTimeWorkRequest.Builder(DefaultBrowserNotificationWorker::class.java) + .setInitialDelay(NOTIFICATION_DELAY, TimeUnit.MILLISECONDS) + .build() + + instanceWorkManager.beginUniqueWork( + NOTIFICATION_WORK_NAME, + ExistingWorkPolicy.REPLACE, + notificationWork + ).enqueue() + } + } +} 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 7c27923227..0e8a09c355 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -484,6 +484,15 @@ class Settings(private val appContext: Context) : PreferencesHolder { return browsers.isDefaultBrowser } + var defaultBrowserNotificationDisplayed by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_should_show_default_browser_notification), + default = false + ) + + fun shouldShowDefaultBrowserNotification(): Boolean { + return !defaultBrowserNotificationDisplayed && !isDefaultBrowser() + } + 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 2861fe18cf..e45acd9302 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -67,6 +67,7 @@ pref_key_last_review_prompt_shown_time pref_key_last_browse_activity_time pref_key_last_cfr_shown_time + pref_key_should_show_default_browser_notification pref_key_telemetry diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eea4bc6332..07ff420cef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1022,6 +1022,12 @@ Delete and Open Powered By + + Marketing + + Make %1$s your default browser + + Collection deleted 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 new file mode 100644 index 0000000000..ae63c0d634 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/home/intent/DefaultBrowserIntentProcessorTest.kt @@ -0,0 +1,52 @@ +/* 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.home.intent + +import android.content.Intent +import androidx.navigation.NavController +import io.mockk.Called +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertFalse +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class DefaultBrowserIntentProcessorTest { + + @Test + fun `do not process blank intents`() { + val navController: NavController = mockk() + val out: Intent = mockk() + val result = DefaultBrowserIntentProcessor(mockk()).process(Intent(), navController, out) + + assertFalse(result) + verify { navController wasNot Called } + verify { out wasNot Called } + } + + @Test + fun `process default browser notification intents`() { + val navController: NavController = mockk(relaxed = true) + val out: Intent = mockk() + val activity: HomeActivity = mockk() + + val intent = Intent().apply { + putExtra("org.mozilla.fenix.default.browser.intent", true) + } + every { activity.startActivity(any()) } returns Unit + every { activity.applicationContext } returns testContext + + val result = DefaultBrowserIntentProcessor(activity).process(intent, navController, out) + + assert(result) + verify { navController wasNot Called } + verify { out wasNot Called } + } +} 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 83aaaba85f..968e787077 100644 --- a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt +++ b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt @@ -745,4 +745,24 @@ class SettingsTest { assertFalse(localSetting.shouldStartOnHome()) } + + @Test + fun `GIVEN shownDefaultBrowserNotification and isDefaultBrowser WHEN calling shouldShowDefaultBrowserNotification THEN return correct value`() { + val localSetting = spyk(settings) + every { localSetting.isDefaultBrowser() } returns false + + localSetting.defaultBrowserNotificationDisplayed = false + assert(localSetting.shouldShowDefaultBrowserNotification()) + + localSetting.defaultBrowserNotificationDisplayed = true + assertFalse(localSetting.shouldShowDefaultBrowserNotification()) + + every { localSetting.isDefaultBrowser() } returns true + + localSetting.defaultBrowserNotificationDisplayed = false + assertFalse(localSetting.shouldShowDefaultBrowserNotification()) + + localSetting.defaultBrowserNotificationDisplayed = true + assertFalse(localSetting.shouldShowDefaultBrowserNotification()) + } }