From 2499c0a0bd0df9a00c27903c5e8d8f461f84fc8b Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Fri, 5 May 2023 12:34:09 +0100 Subject: [PATCH 1/6] Notify users when news are updated --- core/notifications/build.gradle.kts | 1 + .../src/main/AndroidManifest.xml | 4 +- .../notifications/AndroidSystemNotifier.kt | 115 +++++++++++++++++- .../src/main/res/values/strings.xml | 8 +- feature/foryou/build.gradle.kts | 1 + .../feature/foryou/ForYouScreen.kt | 22 ++++ gradle/libs.versions.toml | 1 + 7 files changed, 144 insertions(+), 8 deletions(-) diff --git a/core/notifications/build.gradle.kts b/core/notifications/build.gradle.kts index 608e59a38..86094c2c8 100644 --- a/core/notifications/build.gradle.kts +++ b/core/notifications/build.gradle.kts @@ -24,6 +24,7 @@ android { } dependencies { + implementation(project(":core:common")) implementation(project(":core:model")) implementation(libs.kotlinx.coroutines.android) diff --git a/core/notifications/src/main/AndroidManifest.xml b/core/notifications/src/main/AndroidManifest.xml index 31c889874..5f602c346 100644 --- a/core/notifications/src/main/AndroidManifest.xml +++ b/core/notifications/src/main/AndroidManifest.xml @@ -14,4 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - + + + diff --git a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt index 00d97fcb3..6b4c53ef3 100644 --- a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt +++ b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt @@ -16,17 +16,126 @@ package com.google.samples.apps.nowinandroid.core.notifications +import android.Manifest.permission +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.InboxStyle +import androidx.core.app.NotificationManagerCompat import com.google.samples.apps.nowinandroid.core.model.data.NewsResource +import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +private const val NEWS_NOTIFICATION_SUMMARY_ID = 1 +private const val NEWS_NOTIFICATION_CHANNEL_ID = "" +private const val NEWS_NOTIFICATION_GROUP = "NEWS_NOTIFICATIONS" + /** * Implementation of [Notifier] that displays notifications in the system tray. */ @Singleton -class AndroidSystemNotifier @Inject constructor() : Notifier { +class AndroidSystemNotifier @Inject constructor( + @ApplicationContext private val context: Context, +) : Notifier { + + override fun onNewsAdded( + newsResources: List, + ) = with(context) { + if (ActivityCompat.checkSelfPermission( + this, + permission.POST_NOTIFICATIONS, + ) != PackageManager.PERMISSION_GRANTED + ) { + return + } + + val newsNotifications = newsResources.map { newsResource -> + newsNotification { + setSmallIcon( + com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification, + ) + .setContentTitle(newsResource.title) + .setContentText(newsResource.content) + .setGroup(NEWS_NOTIFICATION_GROUP) + } + } + val summaryNotification = newsNotification { + val title = getString( + R.string.news_notification_group_summary, + newsNotifications.size, + ) + setContentTitle(title) + .setContentText(title) + .setSmallIcon( + com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification, + ) + // Build summary info into InboxStyle template. + .setStyle(newsInboxStyle(newsResources, title)) + .setGroup(NEWS_NOTIFICATION_GROUP) + .setGroupSummary(true) + .build() + } + + with(NotificationManagerCompat.from(this)) { + newsNotifications.forEachIndexed { index, notification -> + notify(newsResources[index].id.hashCode(), notification) + } + notify(NEWS_NOTIFICATION_SUMMARY_ID, summaryNotification) + } + } + + /** + * Creates an inbox style summary notification for news updates + */ + private fun newsInboxStyle( + newsResources: List, + title: String, + ): InboxStyle = newsResources + // Show at most 5 lines + .take(5) + .fold(InboxStyle()) { inboxStyle, newsResource -> + inboxStyle.addLine(newsResource.title) + } + .setBigContentTitle(title) + .setSummaryText(title) +} + +/** + * Creates a notification for configured for news updates + */ +private fun Context.newsNotification( + block: NotificationCompat.Builder.() -> Unit, +): Notification { + ensureNotificationChannel() + return NotificationCompat.Builder( + this, + NEWS_NOTIFICATION_CHANNEL_ID, + ) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .apply(block) + .build() +} + +/** + * Ensures the a notification channel is is present if applicable + */ +private fun Context.ensureNotificationChannel() { + if (VERSION.SDK_INT < VERSION_CODES.O) return - override fun onNewsAdded(newsResources: List) { - // TODO, create notification and display to the user + val channel = NotificationChannel( + NEWS_NOTIFICATION_CHANNEL_ID, + getString(R.string.news_notification_channel_name), + NotificationManager.IMPORTANCE_DEFAULT, + ).apply { + description = getString(R.string.news_notification_channel_description) } + // Register the channel with the system + NotificationManagerCompat.from(this).createNotificationChannel(channel) } diff --git a/core/notifications/src/main/res/values/strings.xml b/core/notifications/src/main/res/values/strings.xml index e3fd73ff8..a3f8a4e61 100644 --- a/core/notifications/src/main/res/values/strings.xml +++ b/core/notifications/src/main/res/values/strings.xml @@ -15,8 +15,8 @@ limitations under the License. --> - Now in Android - Sync - Background tasks for Now in Android - + Now in Android + News updates + The latest updates on what\'s new in Android + %1$d news updates diff --git a/feature/foryou/build.gradle.kts b/feature/foryou/build.gradle.kts index 8c6747dd1..6cd5216d6 100644 --- a/feature/foryou/build.gradle.kts +++ b/feature/foryou/build.gradle.kts @@ -29,4 +29,5 @@ android { dependencies { implementation(libs.kotlinx.datetime) implementation(libs.androidx.activity.compose) + implementation(libs.accompanist.permissions) } diff --git a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt index 06c73c971..a7059e420 100644 --- a/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt +++ b/feature/foryou/src/main/java/com/google/samples/apps/nowinandroid/feature/foryou/ForYouScreen.kt @@ -16,6 +16,8 @@ package com.google.samples.apps.nowinandroid.feature.foryou +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import androidx.activity.compose.ReportDrawnWhen import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -57,6 +59,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -73,6 +76,9 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.util.trace import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus.Denied +import com.google.accompanist.permissions.rememberPermissionState import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton @@ -199,6 +205,7 @@ internal fun ForYouScreen( } } TrackScreenViewEvent(screenName = "ForYou") + requestNotificationsPermission() } /** @@ -383,6 +390,21 @@ fun TopicIcon( ) } +@Composable +@OptIn(ExperimentalPermissionsApi::class) +private fun requestNotificationsPermission() { + if (VERSION.SDK_INT < VERSION_CODES.TIRAMISU) return + val notificationsPermissionState = rememberPermissionState( + android.Manifest.permission.POST_NOTIFICATIONS, + ) + LaunchedEffect(notificationsPermissionState) { + val status = notificationsPermissionState.status + if (status is Denied && !status.shouldShowRationale) { + notificationsPermissionState.launchPermissionRequest() + } + } +} + @DevicePreviews @Composable fun ForYouScreenPopulatedFeed( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccfdeca99..d3af7678f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,7 @@ turbine = "0.12.1" [libraries] accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-testharness = { group = "com.google.accompanist", name = "accompanist-testharness", version.ref = "accompanist" } +accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } From 8a3a16de212763e8e5b37ab54cda31c1f2b8e35f Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Fri, 5 May 2023 13:21:14 +0100 Subject: [PATCH 2/6] Add pending intent for opening notification links --- core/notifications/build.gradle.kts | 1 + .../notifications/AndroidSystemNotifier.kt | 32 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/core/notifications/build.gradle.kts b/core/notifications/build.gradle.kts index 86094c2c8..012c6f3f3 100644 --- a/core/notifications/build.gradle.kts +++ b/core/notifications/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(project(":core:model")) implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.browser) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.core.ktx) diff --git a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt index 6b4c53ef3..c8f3ed9b1 100644 --- a/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt +++ b/core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt @@ -20,12 +20,16 @@ import android.Manifest.permission import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context import android.content.pm.PackageManager +import android.net.Uri import android.os.Build.VERSION import android.os.Build.VERSION_CODES +import androidx.browser.customtabs.CustomTabsIntent import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.EXTRA_NOTIFICATION_ID import androidx.core.app.NotificationCompat.InboxStyle import androidx.core.app.NotificationManagerCompat import com.google.samples.apps.nowinandroid.core.model.data.NewsResource @@ -33,6 +37,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +private const val NEWS_NOTIFICATION_REQUEST_CODE = 0 private const val NEWS_NOTIFICATION_SUMMARY_ID = 1 private const val NEWS_NOTIFICATION_CHANNEL_ID = "" private const val NEWS_NOTIFICATION_GROUP = "NEWS_NOTIFICATIONS" @@ -52,9 +57,7 @@ class AndroidSystemNotifier @Inject constructor( this, permission.POST_NOTIFICATIONS, ) != PackageManager.PERMISSION_GRANTED - ) { - return - } + ) return val newsNotifications = newsResources.map { newsResource -> newsNotification { @@ -63,7 +66,9 @@ class AndroidSystemNotifier @Inject constructor( ) .setContentTitle(newsResource.title) .setContentText(newsResource.content) + .setContentIntent(newsPendingIntent(newsResource)) .setGroup(NEWS_NOTIFICATION_GROUP) + .setAutoCancel(true) } } val summaryNotification = newsNotification { @@ -80,6 +85,7 @@ class AndroidSystemNotifier @Inject constructor( .setStyle(newsInboxStyle(newsResources, title)) .setGroup(NEWS_NOTIFICATION_GROUP) .setGroupSummary(true) + .setAutoCancel(true) .build() } @@ -139,3 +145,23 @@ private fun Context.ensureNotificationChannel() { // Register the channel with the system NotificationManagerCompat.from(this).createNotificationChannel(channel) } + +private fun Context.newsPendingIntent(newsResource: NewsResource): PendingIntent? = + PendingIntent.getActivity( + this, + NEWS_NOTIFICATION_REQUEST_CODE, + // TODO: Read color from material theme to style the chrome custom tab + // this is currently only readable from composition. + CustomTabsIntent.Builder() + .build() + .apply { + intent.data = Uri.parse(newsResource.url) + if (VERSION.SDK_INT < VERSION_CODES.O) { + intent.putExtra( + EXTRA_NOTIFICATION_ID, + newsResource.id.hashCode(), + ) + } + }.intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) From ef97cb941c72ac89c1c1790db708966b3d8bbdd9 Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Sat, 6 May 2023 11:01:31 +0100 Subject: [PATCH 3/6] Route notification deep link through for you screen --- app/src/main/AndroidManifest.xml | 6 +- .../repository/OfflineFirstNewsRepository.kt | 4 +- .../core/notifications/NoOpNotifier.kt | 2 +- .../core/notifications/Notifier.kt | 2 +- ...ystemNotifier.kt => SystemTrayNotifier.kt} | 109 ++++++++++-------- .../testing/notifications/TestNotifier.kt | 2 +- .../feature/foryou/ForYouScreenTest.kt | 14 +++ .../feature/foryou/ForYouScreen.kt | 47 +++++++- .../feature/foryou/ForYouViewModel.kt | 40 +++++++ .../foryou/navigation/ForYouNavigation.kt | 16 ++- .../feature/foryou/ForYouViewModelTest.kt | 33 ++++++ 11 files changed, 217 insertions(+), 58 deletions(-) rename core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/{AndroidSystemNotifier.kt => SystemTrayNotifier.kt} (61%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99c233910..0b0482c13 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,9 +42,13 @@ android:exported="true"> - + + +