Notify users when news are updated

pull/712/head
TJ Dahunsi 2 years ago
parent 553f55f978
commit 2499c0a0bd

@ -24,6 +24,7 @@ android {
} }
dependencies { dependencies {
implementation(project(":core:common"))
implementation(project(":core:model")) implementation(project(":core:model"))
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)

@ -14,4 +14,6 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
</manifest>

@ -16,17 +16,126 @@
package com.google.samples.apps.nowinandroid.core.notifications 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 com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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. * Implementation of [Notifier] that displays notifications in the system tray.
*/ */
@Singleton @Singleton
class AndroidSystemNotifier @Inject constructor() : Notifier { class AndroidSystemNotifier @Inject constructor(
@ApplicationContext private val context: Context,
) : Notifier {
override fun onNewsAdded(
newsResources: List<NewsResource>,
) = 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<NewsResource>,
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<NewsResource>) { val channel = NotificationChannel(
// TODO, create notification and display to the user 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)
} }

@ -15,8 +15,8 @@
limitations under the License. limitations under the License.
--> -->
<resources> <resources>
<string name="sync_notification_title">Now in Android</string> <string name="news_notification_title">Now in Android</string>
<string name="sync_notification_channel_name">Sync</string> <string name="news_notification_channel_name">News updates</string>
<string name="sync_notification_channel_description">Background tasks for Now in Android</string> <string name="news_notification_channel_description">The latest updates on what\'s new in Android</string>
<string name="news_notification_group_summary">%1$d news updates</string>
</resources> </resources>

@ -29,4 +29,5 @@ android {
dependencies { dependencies {
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.accompanist.permissions)
} }

@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.feature.foryou 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.activity.compose.ReportDrawnWhen
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
@ -57,6 +59,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -73,6 +76,9 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.trace import androidx.compose.ui.util.trace
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
@ -199,6 +205,7 @@ internal fun ForYouScreen(
} }
} }
TrackScreenViewEvent(screenName = "ForYou") 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 @DevicePreviews
@Composable @Composable
fun ForYouScreenPopulatedFeed( fun ForYouScreenPopulatedFeed(

@ -55,6 +55,7 @@ turbine = "0.12.1"
[libraries] [libraries]
accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } 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-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" } 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-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }

Loading…
Cancel
Save