From 2499c0a0bd0df9a00c27903c5e8d8f461f84fc8b Mon Sep 17 00:00:00 2001 From: TJ Dahunsi Date: Fri, 5 May 2023 12:34:09 +0100 Subject: [PATCH] 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" }