From 5c40dba3c1ff7f86fe29c30c312771bbd644411e Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Tue, 13 Aug 2019 09:57:55 -0400 Subject: [PATCH 1/4] Closes #796: Integrate push component with Firebase support --- app/build.gradle | 5 +++ app/src/main/AndroidManifest.xml | 15 +++++++ .../reference/browser/BrowserApplication.kt | 5 +++ .../browser/components/BackgroundServices.kt | 40 +++++++++++++++++++ .../reference/browser/push/FirebasePush.kt | 11 +++++ buildSrc/src/main/java/Dependencies.kt | 3 ++ 6 files changed, 79 insertions(+) create mode 100644 app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt diff --git a/app/build.gradle b/app/build.gradle index 285f0d57d..9ba1e4a21 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -182,6 +182,7 @@ dependencies { implementation Deps.mozilla_concept_toolbar implementation Deps.mozilla_concept_storage implementation Deps.mozilla_concept_sync + implementation Deps.mozilla_concept_push implementation Deps.mozilla_browser_engine_gecko_nightly @@ -211,6 +212,7 @@ dependencies { implementation Deps.mozilla_feature_tabs implementation Deps.mozilla_feature_downloads implementation Deps.mozilla_feature_prompts + implementation Deps.mozilla_feature_push implementation Deps.mozilla_feature_pwa implementation Deps.mozilla_feature_qr implementation Deps.mozilla_feature_readerview @@ -228,7 +230,10 @@ dependencies { implementation Deps.mozilla_support_ktx implementation Deps.mozilla_support_rustlog implementation Deps.mozilla_support_rusthttp + implementation Deps.mozilla_lib_crash + implementation Deps.mozilla_lib_push_firebase + implementation Deps.thirdparty_sentry implementation Deps.kotlin_stdlib diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d113c59e..d33e0a548 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -104,6 +104,21 @@ android:label="@string/settings" android:theme="@style/AppTheme" android:parentActivityName=".BrowserActivity" /> + + + + + + + + + diff --git a/app/src/main/java/org/mozilla/reference/browser/BrowserApplication.kt b/app/src/main/java/org/mozilla/reference/browser/BrowserApplication.kt index b984e5ed3..c50b819b7 100644 --- a/app/src/main/java/org/mozilla/reference/browser/BrowserApplication.kt +++ b/app/src/main/java/org/mozilla/reference/browser/BrowserApplication.kt @@ -5,6 +5,7 @@ package org.mozilla.reference.browser import android.app.Application +import mozilla.components.concept.push.PushProcessor import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.sink.AndroidLogSink import mozilla.components.support.ktx.android.content.isMainProcess @@ -36,6 +37,10 @@ open class BrowserApplication : Application() { components.analytics.initializeGlean() components.analytics.initializeExperiments() + + components.backgroundServices.pushFeature?.let { + PushProcessor.install(it) + } } override fun onTrimMemory(level: Int) { diff --git a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt index c5818eeec..179a18cfe 100644 --- a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt @@ -6,18 +6,24 @@ package org.mozilla.reference.browser.components import android.content.Context import android.os.Build +import androidx.lifecycle.ProcessLifecycleOwner import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.PlacesHistoryStorage +import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.DeviceCapability import mozilla.components.concept.sync.DeviceType +import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.feature.push.AutoPushFeature +import mozilla.components.feature.push.PushConfig import mozilla.components.service.fxa.DeviceConfig import mozilla.components.service.fxa.ServerConfig import mozilla.components.service.fxa.SyncConfig import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider +import org.mozilla.reference.browser.push.FirebasePush /** * Component group for background services. These are components that need to be accessed from @@ -61,5 +67,39 @@ class BackgroundServices( setOf("https://identity.mozilla.com/apps/oldsync") ).also { CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } + + // We don't need the push service unless we're signed in. + it.register(object : AccountObserver { + override fun onAuthenticated(account: OAuthAccount, newAccount: Boolean) { + pushService.start(context) + } + + override fun onLoggedOut() { + pushService.stop() + } + }, ProcessLifecycleOwner.get(), false) + } + + val pushFeature by lazy { + pushConfig?.let { config -> + AutoPushFeature(context, pushService, config) + } } + + /** + * The push configuration data class used to initialize the AutoPushFeature. + * + * If we have the `project_id` resource, then we know that the Firebase configuration and API + * keys are available for the FCM service to be used. + */ + private val pushConfig by lazy { + val resId = context.resources.getIdentifier("project_id", "string", context.packageName) + if (resId == 0) { + return@lazy null + } + val projectId = context.resources.getString(resId) + PushConfig(projectId) + } + + private val pushService by lazy { FirebasePush() } } diff --git a/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt b/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt new file mode 100644 index 000000000..52246aa6e --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt @@ -0,0 +1,11 @@ +/* + * 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.reference.browser.push + +import mozilla.components.lib.push.firebase.AbstractFirebasePushService + +class FirebasePush : AbstractFirebasePushService() diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 0254c9b2e..c168e8f30 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -39,6 +39,7 @@ object Deps { const val mozilla_concept_toolbar = "org.mozilla.components:concept-toolbar:${Versions.mozilla_android_components}" const val mozilla_concept_storage = "org.mozilla.components:concept-storage:${Versions.mozilla_android_components}" const val mozilla_concept_sync = "org.mozilla.components:concept-sync:${Versions.mozilla_android_components}" + const val mozilla_concept_push = "org.mozilla.components:concept-push:${Versions.mozilla_android_components}" const val mozilla_browser_awesomebar = "org.mozilla.components:browser-awesomebar:${Versions.mozilla_android_components}" const val mozilla_browser_engine_gecko = "org.mozilla.components:browser-engine-gecko:${Versions.mozilla_android_components}" @@ -71,6 +72,7 @@ object Deps { const val mozilla_feature_downloads = "org.mozilla.components:feature-downloads:${Versions.mozilla_android_components}" const val mozilla_feature_storage = "org.mozilla.components:feature-storage:${Versions.mozilla_android_components}" const val mozilla_feature_prompts = "org.mozilla.components:feature-prompts:${Versions.mozilla_android_components}" + const val mozilla_feature_push = "org.mozilla.components:feature-push:${Versions.mozilla_android_components}" const val mozilla_feature_pwa = "org.mozilla.components:feature-pwa:${Versions.mozilla_android_components}" const val mozilla_feature_qr = "org.mozilla.components:feature-qr:${Versions.mozilla_android_components}" const val mozilla_feature_readerview = "org.mozilla.components:feature-readerview:${Versions.mozilla_android_components}" @@ -90,6 +92,7 @@ object Deps { const val mozilla_support_rusthttp = "org.mozilla.components:support-rusthttp:${Versions.mozilla_android_components}" const val mozilla_lib_crash = "org.mozilla.components:lib-crash:${Versions.mozilla_android_components}" + const val mozilla_lib_push_firebase = "org.mozilla.components:lib-push-firebase:${Versions.mozilla_android_components}" const val thirdparty_sentry = "io.sentry:sentry-android:${Versions.thirdparty_sentry}" From 780dd1f6eb911167368bbd18d1edca36aa547d6a Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Mon, 12 Aug 2019 10:08:37 -0400 Subject: [PATCH 2/4] Closes #105: Add Send Tab feature to receive a tab --- app/build.gradle | 1 + .../reference/browser/NotificationManager.kt | 126 ++++++++++++++++++ .../browser/components/BackgroundServices.kt | 11 ++ app/src/main/res/drawable/ic_status_logo.xml | 12 ++ app/src/main/res/values/strings.xml | 12 ++ buildSrc/src/main/java/Dependencies.kt | 1 + 6 files changed, 163 insertions(+) create mode 100644 app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt create mode 100644 app/src/main/res/drawable/ic_status_logo.xml diff --git a/app/build.gradle b/app/build.gradle index 9ba1e4a21..4179f23ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -207,6 +207,7 @@ dependencies { implementation Deps.mozilla_feature_sitepermissions implementation Deps.mozilla_feature_intent implementation Deps.mozilla_feature_search + implementation Deps.mozilla_feature_sendtab implementation Deps.mozilla_feature_session implementation Deps.mozilla_feature_toolbar implementation Deps.mozilla_feature_tabs diff --git a/app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt b/app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt new file mode 100644 index 000000000..a67eb7a49 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/NotificationManager.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.reference.browser + +import android.annotation.TargetApi +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager as AndroidNotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.TabData +import mozilla.components.support.base.log.logger.Logger + +/** + * Manages notification channels and allows displaying different types of notifications. + */ +class NotificationManager(private val context: Context) { + companion object { + const val RECEIVE_TABS_TAG = "ReceivedTabs" + const val RECEIVE_TABS_CHANNEL_ID = "ReceivedTabsChannel" + } + + init { + // Create the notification channels we are going to use, but only on API 26+ because the NotificationChannel + // class is new and not in the support library. + if (SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel( + RECEIVE_TABS_CHANNEL_ID, + // Pick 'high' because this is a user-triggered action that is expected to be part of a continuity flow. + // That is, user is expected to be waiting for this notification on their device; make it obvious. + AndroidNotificationManager.IMPORTANCE_HIGH, + // Name and description are shown in the 'app notifications' settings for the app. + context.getString(R.string.fxa_received_tab_channel_name), + context.getString(R.string.fxa_received_tab_channel_description) + ) + } + } + + private val logger = Logger("NotificationManager") + fun showReceivedTabs(device: Device?, tabs: List) { + // In the future, experiment with displaying multiple tabs from the same device as as Notification Groups. + // For now, a single notification per tab received will suffice. + logger.debug("Showing ${tabs.size} tab(s) received from deviceID=${device?.id}") + tabs.forEach { tab -> + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(tab.url)).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + val pendingIntent: PendingIntent = + PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + + val builder = NotificationCompat.Builder(context, RECEIVE_TABS_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_status_logo) + .setSendTabTitle(context, device, tab) + .setWhen(System.currentTimeMillis()) + .setContentText(tab.url) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_SOUND) + + if (SDK_INT >= Build.VERSION_CODES.M) { + builder.setCategory(Notification.CATEGORY_REMINDER) + } + + // Pick a random ID for this notification so that different tabs do not clash. + @SuppressWarnings("MagicNumber") + val notificationId = (Math.random() * 100).toInt() + + with(NotificationManagerCompat.from(context)) { + notify(RECEIVE_TABS_TAG, notificationId, builder.build()) + } + } + } + + @TargetApi(Build.VERSION_CODES.O) + private fun createNotificationChannel( + channelId: String, + importance: Int, + channelName: String, + channelDescription: String + ) { + val channel = NotificationChannel(channelId, channelName, importance).apply { + description = channelDescription + } + // Register the channel with the system. Once this is done, we can't change importance or other notification + // channel behaviour. We will be able to change 'name' and 'description' if we so choose. + val notificationManager: AndroidNotificationManager = + ContextCompat.getSystemService(context, AndroidNotificationManager::class.java)!! + notificationManager.createNotificationChannel(channel) + } +} + +private fun NotificationCompat.Builder.setSendTabTitle( + context: Context, + device: Device?, + tab: TabData +): NotificationCompat.Builder { + device?.let { + setContentTitle( + context.getString( + R.string.fxa_tab_received_from_notification_name, + it.displayName + ) + ) + return this + } + + if (tab.title.isEmpty()) { + setContentTitle(context.getString(R.string.fxa_tab_received_notification_name)) + } else { + setContentTitle(tab.title) + } + return this +} diff --git a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt index 179a18cfe..9c384e895 100644 --- a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt @@ -17,6 +17,7 @@ import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.feature.push.AutoPushFeature import mozilla.components.feature.push.PushConfig +import mozilla.components.feature.sendtab.SendTabFeature import mozilla.components.service.fxa.DeviceConfig import mozilla.components.service.fxa.ServerConfig import mozilla.components.service.fxa.SyncConfig @@ -24,6 +25,7 @@ import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider import org.mozilla.reference.browser.push.FirebasePush +import org.mozilla.reference.browser.NotificationManager /** * Component group for background services. These are components that need to be accessed from @@ -66,6 +68,11 @@ class BackgroundServices( // See https://github.com/mozilla-mobile/android-components/issues/3732 setOf("https://identity.mozilla.com/apps/oldsync") ).also { + // Initializing the feature allows it to start observing events as needed. + SendTabFeature(it) { device, tabs -> + notificationManager.showReceivedTabs(device, tabs) + } + CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } // We don't need the push service unless we're signed in. @@ -102,4 +109,8 @@ class BackgroundServices( } private val pushService by lazy { FirebasePush() } + + private val notificationManager by lazy { + NotificationManager(context) + } } diff --git a/app/src/main/res/drawable/ic_status_logo.xml b/app/src/main/res/drawable/ic_status_logo.xml new file mode 100644 index 000000000..b8c538a59 --- /dev/null +++ b/app/src/main/res/drawable/ic_status_logo.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17fb91d62..7206a559f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -155,4 +155,16 @@ Share with… Copied! + + + Received tabs + + Notifications for tabs received from other Firefox devices. + + Tab Received + + Tabs Received + + Tab from %s + diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index c168e8f30..1a0fb2ea6 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -66,6 +66,7 @@ object Deps { const val mozilla_feature_sitepermissions = "org.mozilla.components:feature-sitepermissions:${Versions.mozilla_android_components}" const val mozilla_feature_intent = "org.mozilla.components:feature-intent:${Versions.mozilla_android_components}" const val mozilla_feature_search = "org.mozilla.components:feature-search:${Versions.mozilla_android_components}" + const val mozilla_feature_sendtab = "org.mozilla.components:feature-sendtab:${Versions.mozilla_android_components}" const val mozilla_feature_session = "org.mozilla.components:feature-session:${Versions.mozilla_android_components}" const val mozilla_feature_toolbar = "org.mozilla.components:feature-toolbar:${Versions.mozilla_android_components}" const val mozilla_feature_tabs = "org.mozilla.components:feature-tabs:${Versions.mozilla_android_components}" From 74ec6add316fd6037075907bbf4a4e8d7b5e6320 Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Tue, 13 Aug 2019 12:43:50 -0400 Subject: [PATCH 3/4] Integration between SendTabFeature and PushFeature --- .../browser/components/BackgroundServices.kt | 58 ++++++++++--------- app/src/main/res/values/strings.xml | 2 - 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt index 9c384e895..2f8235bc6 100644 --- a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt @@ -56,35 +56,29 @@ class BackgroundServices( syncPeriodInMinutes = 240L ) // four hours - val accountManager = FxaAccountManager( - context, - serverConfig, - deviceConfig, - syncConfig, - // We don't need to specify this explicitly, but `syncConfig` may be disabled due to an 'experiments' - // flag. In that case, sync scope necessary for syncing won't be acquired during authentication - // unless we explicitly specify it below. - // This is a good example of an information leak at the API level. - // See https://github.com/mozilla-mobile/android-components/issues/3732 - setOf("https://identity.mozilla.com/apps/oldsync") - ).also { - // Initializing the feature allows it to start observing events as needed. - SendTabFeature(it) { device, tabs -> - notificationManager.showReceivedTabs(device, tabs) - } - - CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } + val accountManager by lazy { + FxaAccountManager( + context, + serverConfig, + deviceConfig, + syncConfig, + // We don't need to specify this explicitly, but `syncConfig` may be disabled due to an 'experiments' + // flag. In that case, sync scope necessary for syncing won't be acquired during authentication + // unless we explicitly specify it below. + // This is a good example of an information leak at the API level. + // See https://github.com/mozilla-mobile/android-components/issues/3732 + setOf("https://identity.mozilla.com/apps/oldsync") + ).also { + // We don't need the push service unless we're signed in. + it.register(pushServiceObserver, ProcessLifecycleOwner.get(), false) - // We don't need the push service unless we're signed in. - it.register(object : AccountObserver { - override fun onAuthenticated(account: OAuthAccount, newAccount: Boolean) { - pushService.start(context) + // Initializing the feature allows it to start observing events as needed. + SendTabFeature(it, pushFeature) { device, tabs -> + notificationManager.showReceivedTabs(device, tabs) } - override fun onLoggedOut() { - pushService.stop() - } - }, ProcessLifecycleOwner.get(), false) + CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } + } } val pushFeature by lazy { @@ -113,4 +107,16 @@ class BackgroundServices( private val notificationManager by lazy { NotificationManager(context) } + + private val pushServiceObserver by lazy { + object : AccountObserver { + override fun onAuthenticated(account: OAuthAccount, newAccount: Boolean) { + pushService.start(context) + } + + override fun onLoggedOut() { + pushService.stop() + } + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7206a559f..e92469120 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,8 +162,6 @@ Notifications for tabs received from other Firefox devices. Tab Received - - Tabs Received Tab from %s From 33c395ee4085ca304ee0015873da9576f17ae896 Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Wed, 6 Nov 2019 18:02:27 -0500 Subject: [PATCH 4/4] Address review comments for Send Tab integration --- app/build.gradle | 4 +- .../reference/browser/BrowserActivity.kt | 3 +- .../reference/browser/IntentRequestCodes.kt | 12 + .../reference/browser/NotificationManager.kt | 229 ++++++++++++------ .../browser/components/BackgroundServices.kt | 13 +- .../reference/browser/push/FirebasePush.kt | 8 +- .../telemetry/DataReportingNotification.kt | 101 -------- app/src/main/res/drawable/ic_status_logo.xml | 12 - buildSrc/src/main/java/Dependencies.kt | 4 +- 9 files changed, 187 insertions(+), 199 deletions(-) create mode 100644 app/src/main/java/org/mozilla/reference/browser/IntentRequestCodes.kt delete mode 100644 app/src/main/java/org/mozilla/reference/browser/telemetry/DataReportingNotification.kt delete mode 100644 app/src/main/res/drawable/ic_status_logo.xml diff --git a/app/build.gradle b/app/build.gradle index 4179f23ad..1b66a53fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -242,8 +242,8 @@ dependencies { implementation Deps.androidx_appcompat implementation Deps.androidx_constraintlayout - implementation Deps.androidx_preference - implementation Deps.androidx_work_runtime + implementation Deps.androidx_preference_ktx + implementation Deps.androidx_work_runtime_ktx implementation Deps.google_material androidTestImplementation Deps.uiautomator diff --git a/app/src/main/java/org/mozilla/reference/browser/BrowserActivity.kt b/app/src/main/java/org/mozilla/reference/browser/BrowserActivity.kt index b17641f2c..7c3d04445 100644 --- a/app/src/main/java/org/mozilla/reference/browser/BrowserActivity.kt +++ b/app/src/main/java/org/mozilla/reference/browser/BrowserActivity.kt @@ -25,7 +25,6 @@ import org.mozilla.reference.browser.browser.CrashIntegration import org.mozilla.reference.browser.ext.components import org.mozilla.reference.browser.ext.isCrashReportActive import org.mozilla.reference.browser.tabs.TabsTouchHelper -import org.mozilla.reference.browser.telemetry.DataReportingNotification /** * Activity that holds the [BrowserFragment]. @@ -61,7 +60,7 @@ open class BrowserActivity : AppCompatActivity() { lifecycle.addObserver(crashIntegration) } - DataReportingNotification.checkAndNotifyPolicy(this) + NotificationManager.checkAndNotifyPolicy(this) } override fun onBackPressed() { diff --git a/app/src/main/java/org/mozilla/reference/browser/IntentRequestCodes.kt b/app/src/main/java/org/mozilla/reference/browser/IntentRequestCodes.kt new file mode 100644 index 000000000..3c37121c7 --- /dev/null +++ b/app/src/main/java/org/mozilla/reference/browser/IntentRequestCodes.kt @@ -0,0 +1,12 @@ +/* + * 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.reference.browser + +object IntentRequestCodes { + const val REQUEST_CODE_DATA_REPORTING = 0 + const val REQUEST_CODE_SEND_TAB = 1 +} diff --git a/app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt b/app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt index a67eb7a49..9f2fa73df 100644 --- a/app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt +++ b/app/src/main/java/org/mozilla/reference/browser/NotificationManager.kt @@ -1,8 +1,6 @@ -/* - * 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/. - */ +/* 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.reference.browser @@ -13,55 +11,77 @@ import android.app.NotificationManager as AndroidNotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import android.os.Build import android.os.Build.VERSION.SDK_INT import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager import mozilla.components.concept.sync.Device import mozilla.components.concept.sync.TabData import mozilla.components.support.base.log.logger.Logger +import org.mozilla.reference.browser.IntentRequestCodes.REQUEST_CODE_DATA_REPORTING +import org.mozilla.reference.browser.IntentRequestCodes.REQUEST_CODE_SEND_TAB /** - * Manages notification channels and allows displaying different types of notifications. + * Manages notification channels and allows displaying different supported types of notifications. */ -class NotificationManager(private val context: Context) { - companion object { - const val RECEIVE_TABS_TAG = "ReceivedTabs" - const val RECEIVE_TABS_CHANNEL_ID = "ReceivedTabsChannel" - } +object NotificationManager { - init { - // Create the notification channels we are going to use, but only on API 26+ because the NotificationChannel - // class is new and not in the support library. - if (SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel( - RECEIVE_TABS_CHANNEL_ID, - // Pick 'high' because this is a user-triggered action that is expected to be part of a continuity flow. - // That is, user is expected to be waiting for this notification on their device; make it obvious. - AndroidNotificationManager.IMPORTANCE_HIGH, - // Name and description are shown in the 'app notifications' settings for the app. - context.getString(R.string.fxa_received_tab_channel_name), - context.getString(R.string.fxa_received_tab_channel_description) - ) - } - } + // Send Tab + private const val RECEIVE_TABS_TAG = "org.mozilla.reference.browser.receivedTabs" + private const val RECEIVE_TABS_CHANNEL_ID = "org.mozilla.reference.browser.ReceivedTabsChannel" + + // Data Reporting + private const val PRIVACY_NOTICE_URL = "https://www.mozilla.org/en-US/privacy/firefox/" + private const val DATA_REPORTING_VERSION = 1 + private const val DATA_REPORTING_TAG = "org.mozilla.reference.browser.DataReporting" + private const val DATA_REPORTING_NOTIFICATION_ID = 1 + private const val PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion" + private const val PREFS_POLICY_NOTIFIED_TIME = + "datareporting.policy.dataSubmissionPolicyNotifiedTime" + // Default + private const val NOTIFICATION_CHANNEL_ID = "default-notification-channel" + + // Use an incrementing notification ID since they have the same tag. + private var notificationIdCount = 0 private val logger = Logger("NotificationManager") - fun showReceivedTabs(device: Device?, tabs: List) { + + fun showReceivedTabs(context: Context, device: Device?, tabs: List) { // In the future, experiment with displaying multiple tabs from the same device as as Notification Groups. // For now, a single notification per tab received will suffice. logger.debug("Showing ${tabs.size} tab(s) received from deviceID=${device?.id}") + tabs.forEach { tab -> val intent = Intent(Intent.ACTION_VIEW, Uri.parse(tab.url)).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK } - val pendingIntent: PendingIntent = - PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT) + val pendingIntent: PendingIntent = PendingIntent.getActivity( + context, + REQUEST_CODE_SEND_TAB, + intent, + PendingIntent.FLAG_ONE_SHOT + ) + val importance = if (SDK_INT >= Build.VERSION_CODES.N) { + // We pick 'IMPORTANCE_HIGH' priority because this is a user-triggered action that is + // expected to be part of a continuity flow. That is, user is expected to be waiting for + // this notification on their device; make it obvious. + AndroidNotificationManager.IMPORTANCE_HIGH + } else { + null + } + val channelId = getNotificationChannelId( + context, + RECEIVE_TABS_CHANNEL_ID, + context.getString(R.string.fxa_received_tab_channel_name), + context.getString(R.string.fxa_received_tab_channel_description), + importance + ) - val builder = NotificationCompat.Builder(context, RECEIVE_TABS_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_status_logo) + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_notification) .setSendTabTitle(context, device, tab) .setWhen(System.currentTimeMillis()) .setContentText(tab.url) @@ -69,58 +89,131 @@ class NotificationManager(private val context: Context) { .setAutoCancel(true) .setPriority(NotificationCompat.PRIORITY_HIGH) .setDefaults(Notification.DEFAULT_VIBRATE or Notification.DEFAULT_SOUND) + .setCategory(NotificationCompat.CATEGORY_REMINDER) - if (SDK_INT >= Build.VERSION_CODES.M) { - builder.setCategory(Notification.CATEGORY_REMINDER) - } + NotificationManagerCompat.from(context).notify( + RECEIVE_TABS_TAG, + notificationIdCount++, + builder.build() + ) + } + } - // Pick a random ID for this notification so that different tabs do not clash. - @SuppressWarnings("MagicNumber") - val notificationId = (Math.random() * 100).toInt() + fun checkAndNotifyPolicy(context: Context) { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + val currentVersion = preferences.getInt(PREFS_POLICY_VERSION, -1) - with(NotificationManagerCompat.from(context)) { - notify(RECEIVE_TABS_TAG, notificationId, builder.build()) - } + if (currentVersion < 1) { + // This is a first run, so notify user about data policy. + notifyDataPolicy(context, preferences) + } + } + + /** + * Launch a notification of the data policy, and record notification time and version. + */ + private fun notifyDataPolicy(context: Context, preferences: SharedPreferences) { + val resources = context.resources + + val notificationTitle = resources.getString(R.string.datareporting_notification_title) + val notificationSummary = resources.getString(R.string.datareporting_notification_summary) + + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(PRIVACY_NOTICE_URL) + setPackage(context.packageName) + } + + val pendingIntent = + PendingIntent.getActivity(context, REQUEST_CODE_DATA_REPORTING, intent, 0) + + val notificationBuilder = NotificationCompat.Builder( + context, + getNotificationChannelId(context) + ).apply { + setContentTitle(notificationTitle) + setContentText(notificationSummary) + setSmallIcon(R.drawable.ic_notification) + setAutoCancel(true) + setContentIntent(pendingIntent) + setStyle(NotificationCompat.BigTextStyle().bigText(notificationSummary)) + } + + NotificationManagerCompat.from(context) + .notify(DATA_REPORTING_TAG, DATA_REPORTING_NOTIFICATION_ID, notificationBuilder.build()) + + preferences.edit() + .putLong(PREFS_POLICY_NOTIFIED_TIME, System.currentTimeMillis()) + .putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION) + .apply() + } + + private fun getNotificationChannelId( + context: Context, + channelId: String = NOTIFICATION_CHANNEL_ID, + channelName: String = context.resources.getString(R.string.default_notification_channel), + description: String? = null, + channelImportance: Int? = null + ): String { + if (SDK_INT >= Build.VERSION_CODES.O) { + val importance = channelImportance ?: AndroidNotificationManager.IMPORTANCE_DEFAULT + createNotificationChannelIfNeeded( + context, + channelId, + channelName, + description, + importance + ) } + + return channelId } @TargetApi(Build.VERSION_CODES.O) - private fun createNotificationChannel( + private fun createNotificationChannelIfNeeded( + context: Context, channelId: String, - importance: Int, channelName: String, - channelDescription: String + channelDescription: String?, + importance: Int = AndroidNotificationManager.IMPORTANCE_DEFAULT ) { - val channel = NotificationChannel(channelId, channelName, importance).apply { + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as AndroidNotificationManager + + if (null != notificationManager.getNotificationChannel(channelId)) { + return + } + + val channel = NotificationChannel( + channelId, + channelName, + importance + ).apply { description = channelDescription } - // Register the channel with the system. Once this is done, we can't change importance or other notification - // channel behaviour. We will be able to change 'name' and 'description' if we so choose. - val notificationManager: AndroidNotificationManager = - ContextCompat.getSystemService(context, AndroidNotificationManager::class.java)!! + notificationManager.createNotificationChannel(channel) } -} -private fun NotificationCompat.Builder.setSendTabTitle( - context: Context, - device: Device?, - tab: TabData -): NotificationCompat.Builder { - device?.let { - setContentTitle( - context.getString( - R.string.fxa_tab_received_from_notification_name, - it.displayName + private fun NotificationCompat.Builder.setSendTabTitle( + context: Context, + device: Device?, + tab: TabData + ): NotificationCompat.Builder { + device?.let { + setContentTitle( + context.getString( + R.string.fxa_tab_received_from_notification_name, + it.displayName + ) ) - ) - return this - } + return this + } - if (tab.title.isEmpty()) { - setContentTitle(context.getString(R.string.fxa_tab_received_notification_name)) - } else { - setContentTitle(tab.title) + if (tab.title.isEmpty()) { + setContentTitle(context.getString(R.string.fxa_tab_received_notification_name)) + } else { + setContentTitle(tab.title) + } + return this } - return this } diff --git a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt index 2f8235bc6..f06c8547b 100644 --- a/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/reference/browser/components/BackgroundServices.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.PlacesHistoryStorage import mozilla.components.concept.sync.AccountObserver +import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.DeviceCapability import mozilla.components.concept.sync.DeviceType import mozilla.components.concept.sync.OAuthAccount @@ -74,7 +75,7 @@ class BackgroundServices( // Initializing the feature allows it to start observing events as needed. SendTabFeature(it, pushFeature) { device, tabs -> - notificationManager.showReceivedTabs(device, tabs) + NotificationManager.showReceivedTabs(context, device, tabs) } CoroutineScope(Dispatchers.Main).launch { it.initAsync().await() } @@ -104,14 +105,12 @@ class BackgroundServices( private val pushService by lazy { FirebasePush() } - private val notificationManager by lazy { - NotificationManager(context) - } - private val pushServiceObserver by lazy { object : AccountObserver { - override fun onAuthenticated(account: OAuthAccount, newAccount: Boolean) { - pushService.start(context) + override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { + if (authType != AuthType.Existing) { + pushService.start(context) + } } override fun onLoggedOut() { diff --git a/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt b/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt index 52246aa6e..69f62c4af 100644 --- a/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt +++ b/app/src/main/java/org/mozilla/reference/browser/push/FirebasePush.kt @@ -1,8 +1,6 @@ -/* - * 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/. - */ +/* 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.reference.browser.push diff --git a/app/src/main/java/org/mozilla/reference/browser/telemetry/DataReportingNotification.kt b/app/src/main/java/org/mozilla/reference/browser/telemetry/DataReportingNotification.kt deleted file mode 100644 index c65a8c2e7..000000000 --- a/app/src/main/java/org/mozilla/reference/browser/telemetry/DataReportingNotification.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* 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.reference.browser.telemetry - -import android.annotation.TargetApi -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.Uri -import android.os.Build -import android.preference.PreferenceManager -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import org.mozilla.reference.browser.R - -/** - * Data Reporting notification to be shown on the first app start and whenever the data policy version changes. - */ -object DataReportingNotification { - private const val PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime" - private const val PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion" - - private const val PRIVACY_NOTICE_URL = "https://www.mozilla.org/en-US/privacy/firefox/" - - private const val DATA_REPORTING_VERSION = 1 - - private const val NOTIFICATION_ID = 1 - - private const val NOTIFICATION_CHANNEL_ID = "default-notification-channel" - - fun checkAndNotifyPolicy(context: Context) { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - val currentVersion = preferences.getInt(PREFS_POLICY_VERSION, -1) - - if (currentVersion < 1) { - // This is a first run, so notify user about data policy. - notifyDataPolicy(context, preferences) - } - } - - /** - * Launch a notification of the data policy, and record notification time and version. - */ - private fun notifyDataPolicy(context: Context, preferences: SharedPreferences) { - val resources = context.resources - - val notificationTitle = resources.getString(R.string.datareporting_notification_title) - val notificationSummary = resources.getString(R.string.datareporting_notification_summary) - - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(PRIVACY_NOTICE_URL) - setPackage(context.packageName) - } - - val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) - - val notificationBuilder = NotificationCompat.Builder(context, getNotificationChannelId(context)) - .setContentTitle(notificationTitle) - .setContentText(notificationSummary) - .setSmallIcon(R.drawable.ic_notification) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - .setStyle(NotificationCompat.BigTextStyle().bigText(notificationSummary)) - - NotificationManagerCompat.from(context) - .notify(NOTIFICATION_ID, notificationBuilder.build()) - - preferences.edit() - .putLong(PREFS_POLICY_NOTIFIED_TIME, System.currentTimeMillis()) - .putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION) - .apply() - } - - private fun getNotificationChannelId(context: Context): String { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannelIfNeeded(context) - } - - return NOTIFICATION_CHANNEL_ID - } - - @TargetApi(Build.VERSION_CODES.O) - private fun createNotificationChannelIfNeeded(context: Context) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (null != notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)) { - return - } - - val channel = NotificationChannel( - NOTIFICATION_CHANNEL_ID, - context.resources.getString(R.string.default_notification_channel), - NotificationManager.IMPORTANCE_DEFAULT) - - notificationManager.createNotificationChannel(channel) - } -} diff --git a/app/src/main/res/drawable/ic_status_logo.xml b/app/src/main/res/drawable/ic_status_logo.xml deleted file mode 100644 index b8c538a59..000000000 --- a/app/src/main/res/drawable/ic_status_logo.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 1a0fb2ea6..aa4fffa04 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -99,8 +99,8 @@ object Deps { const val androidx_appcompat = "androidx.appcompat:appcompat:${Versions.androidx_appcompat}" const val androidx_constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.androidx_constraintlayout}" - const val androidx_preference = "androidx.preference:preference-ktx:${Versions.androidx_preference}" - const val androidx_work_runtime = "androidx.work:work-runtime-ktx:${Versions.workmanager}" + const val androidx_preference_ktx = "androidx.preference:preference-ktx:${Versions.androidx_preference}" + const val androidx_work_runtime_ktx = "androidx.work:work-runtime-ktx:${Versions.workmanager}" const val google_material = "com.google.android.material:material:${Versions.google_material}" const val tools_androidgradle = "com.android.tools.build:gradle:${Versions.android_gradle_plugin}"