diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt index b40247c7e..a67263889 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt @@ -14,8 +14,8 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import org.mozilla.fenix.R -import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.components.toolbar.ToolbarMenu +import org.mozilla.fenix.theme.ThemeManager class CustomTabToolbarMenu( private val context: Context, @@ -148,7 +148,7 @@ class CustomTabToolbarMenu( SimpleBrowserMenuItem( { val appName = context.getString(R.string.app_name) - context.getString(R.string.browser_menu_powered_by, appName).toUpperCase() + context.getString(R.string.browser_menu_powered_by2, appName).toUpperCase() }(), ToolbarMenu.CAPTION_TEXT_SIZE, ThemeManager.resolveAttribute(R.attr.primaryText, context) diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index d96675070..6ad59dd71 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -46,6 +46,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { private val customTabsIntegration = ViewBoundFeatureWrapper() private val hideToolbarFeature = ViewBoundFeatureWrapper() + @Suppress("LongMethod") override fun initializeUI(view: View): Session? { return super.initializeUI(view)?.also { val activity = requireActivity() @@ -100,6 +101,14 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), BackHandler { manifest ) ) + } else { + viewLifecycleOwner.lifecycle.addObserver( + PoweredByNotification( + activity.applicationContext, + requireComponents.core.store, + customTabSessionId + ) + ) } } diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt b/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt new file mode 100644 index 000000000..7a79fe61f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt @@ -0,0 +1,96 @@ +/* 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.customtabs + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.BADGE_ICON_NONE +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import mozilla.components.browser.state.selector.findCustomTab +import mozilla.components.browser.state.state.ExternalAppType +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.base.ids.cancel +import mozilla.components.support.base.ids.notify +import org.mozilla.fenix.R + +/** + * Displays a "Powered by Firefox Preview" notification when a Trusted Web Activity is running. + */ +class PoweredByNotification( + private val applicationContext: Context, + private val store: BrowserStore, + private val customTabId: String +) : LifecycleObserver { + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() { + if (store.state.findCustomTab(customTabId)?.config?.externalAppType === ExternalAppType.TRUSTED_WEB_ACTIVITY) { + NotificationManagerCompat.from(applicationContext) + .notify(applicationContext, NOTIFICATION_TAG, buildNotification()) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun onPause() { + NotificationManagerCompat.from(applicationContext) + .cancel(applicationContext, NOTIFICATION_TAG) + } + + /** + * Build the notification with site controls to be displayed while the web app is active. + */ + private fun buildNotification(): Notification { + val channelId = ensureChannelExists() + + with(applicationContext) { + val appName = getString(R.string.app_name) + return NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.ic_status_logo) + .setContentTitle(getString(R.string.browser_menu_powered_by2, appName)) + .setBadgeIconType(BADGE_ICON_NONE) + .setColor(ContextCompat.getColor(this, R.color.primary_text_light_theme)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setShowWhen(false) + .setOngoing(true) + .build() + } + } + + /** + * Make sure a notification channel for the powered by notifications exists. + * + * Returns the channel id to be used for notifications. + */ + private fun ensureChannelExists(): String { + if (SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager: NotificationManager = applicationContext.getSystemService()!! + + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + applicationContext.getString(R.string.mozac_feature_pwa_site_controls_notification_channel), + NotificationManager.IMPORTANCE_MIN + ) + + notificationManager.createNotificationChannel(channel) + } + + return NOTIFICATION_CHANNEL_ID + } + + companion object { + private const val NOTIFICATION_CHANNEL_ID = "Powered By" + private const val NOTIFICATION_TAG = "PoweredBy" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91e9f658d..c876e757d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -91,6 +91,9 @@ POWERED BY %1$s + + Powered by %1$s @@ -639,6 +642,8 @@ Open Delete and Open + + Powered By Collection deleted diff --git a/app/src/test/java/org/mozilla/fenix/customtabs/PoweredByNotificationTest.kt b/app/src/test/java/org/mozilla/fenix/customtabs/PoweredByNotificationTest.kt new file mode 100644 index 000000000..1b1b36564 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/customtabs/PoweredByNotificationTest.kt @@ -0,0 +1,59 @@ +/* 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.customtabs + +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.CustomTabConfig +import mozilla.components.browser.state.state.ExternalAppType +import mozilla.components.browser.state.state.createCustomTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.TestApplication +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(application = TestApplication::class) +class PoweredByNotificationTest { + + @Test + fun `register receiver on resume`() { + val config = CustomTabConfig(externalAppType = ExternalAppType.TRUSTED_WEB_ACTIVITY) + val store = BrowserStore( + BrowserState( + customTabs = listOf( + createCustomTab("https://mozilla.org", config = config) + ) + ) + ) + + val feature = PoweredByNotification(testContext, store, "session-id") + feature.onResume() + } + + @Test + fun `don't register receiver if not in a TWA`() { + val config = CustomTabConfig(externalAppType = ExternalAppType.PROGRESSIVE_WEB_APP) + val store = BrowserStore( + BrowserState( + customTabs = listOf( + createCustomTab("https://mozilla.org", config = config) + ) + ) + ) + + val feature = PoweredByNotification(testContext, store, "session-id") + feature.onResume() + } + + @Test + fun `unregister receiver on pause`() { + val feature = PoweredByNotification(testContext, mock(), "session-id") + feature.onPause() + } +}