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" }