Skip to content

Commit 3b1b1ea

Browse files
authored
Merge pull request #712 from android/tj/news-notifications
Notify users when news are updated
2 parents 391354a + 6e6abd0 commit 3b1b1ea

File tree

18 files changed

+369
-45
lines changed

18 files changed

+369
-45
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,13 @@
4242
android:exported="true">
4343
<intent-filter>
4444
<action android:name="android.intent.action.MAIN" />
45-
4645
<category android:name="android.intent.category.LAUNCHER" />
4746
</intent-filter>
47+
<intent-filter>
48+
<data
49+
android:scheme="https"
50+
android:host="www.nowinandroid.apps.samples.google.com" />
51+
</intent-filter>
4852
</activity>
4953

5054
<!-- Disable Firebase analytics by default. This setting is overwritten for the `prod`

core/data/src/main/java/com/google/samples/apps/nowinandroid/core/data/repository/OfflineFirstNewsRepository.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,11 @@ class OfflineFirstNewsRepository @Inject constructor(
129129
.first()
130130
.map(PopulatedNewsResource::asExternalModel)
131131

132-
if (addedNewsResources.isNotEmpty()) notifier.onNewsAdded(addedNewsResources)
132+
if (addedNewsResources.isNotEmpty()) {
133+
notifier.postNewsNotifications(
134+
newsResources = addedNewsResources,
135+
)
136+
}
133137
}
134138
},
135139
)

core/notifications/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ android {
2424
}
2525

2626
dependencies {
27+
implementation(project(":core:common"))
2728
implementation(project(":core:model"))
2829

2930
implementation(libs.kotlinx.coroutines.android)
31+
implementation(libs.androidx.browser)
3032
implementation(libs.androidx.compose.runtime)
3133
implementation(libs.androidx.core.ktx)
3234

core/notifications/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,6 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
-->
17-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>
17+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
18+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
19+
</manifest>

core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/AndroidSystemNotifier.kt

Lines changed: 0 additions & 32 deletions
This file was deleted.

core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/NoOpNotifier.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ import javax.inject.Inject
2323
* Implementation of [Notifier] which does nothing. Useful for tests and previews.
2424
*/
2525
class NoOpNotifier @Inject constructor() : Notifier {
26-
override fun onNewsAdded(newsResources: List<NewsResource>) = Unit
26+
override fun postNewsNotifications(newsResources: List<NewsResource>) = Unit
2727
}

core/notifications/src/main/java/com/google/samples/apps/nowinandroid/core/notifications/Notifier.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
2222
* Interface for creating notifications in the app
2323
*/
2424
interface Notifier {
25-
fun onNewsAdded(newsResources: List<NewsResource>)
25+
fun postNewsNotifications(newsResources: List<NewsResource>)
2626
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.samples.apps.nowinandroid.core.notifications
18+
19+
import android.Manifest.permission
20+
import android.app.Notification
21+
import android.app.NotificationChannel
22+
import android.app.NotificationManager
23+
import android.app.PendingIntent
24+
import android.content.ComponentName
25+
import android.content.Context
26+
import android.content.Intent
27+
import android.content.pm.PackageManager
28+
import android.os.Build.VERSION
29+
import android.os.Build.VERSION_CODES
30+
import androidx.core.app.ActivityCompat
31+
import androidx.core.app.NotificationCompat
32+
import androidx.core.app.NotificationCompat.InboxStyle
33+
import androidx.core.app.NotificationManagerCompat
34+
import androidx.core.net.toUri
35+
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
36+
import dagger.hilt.android.qualifiers.ApplicationContext
37+
import javax.inject.Inject
38+
import javax.inject.Singleton
39+
40+
private const val MAX_NUM_NOTIFICATIONS = 5
41+
private const val TARGET_ACTIVITY_NAME = "com.google.samples.apps.nowinandroid.MainActivity"
42+
private const val NEWS_NOTIFICATION_REQUEST_CODE = 0
43+
private const val NEWS_NOTIFICATION_SUMMARY_ID = 1
44+
private const val NEWS_NOTIFICATION_CHANNEL_ID = ""
45+
private const val NEWS_NOTIFICATION_GROUP = "NEWS_NOTIFICATIONS"
46+
private const val DEEP_LINK_SCHEME_AND_HOST = "https://www.nowinandroid.apps.samples.google.com"
47+
private const val FOR_YOU_PATH = "foryou"
48+
49+
/**
50+
* Implementation of [Notifier] that displays notifications in the system tray.
51+
*/
52+
@Singleton
53+
class SystemTrayNotifier @Inject constructor(
54+
@ApplicationContext private val context: Context,
55+
) : Notifier {
56+
57+
override fun postNewsNotifications(
58+
newsResources: List<NewsResource>,
59+
) = with(context) {
60+
if (ActivityCompat.checkSelfPermission(
61+
this,
62+
permission.POST_NOTIFICATIONS,
63+
) != PackageManager.PERMISSION_GRANTED
64+
) {
65+
return
66+
}
67+
68+
val truncatedNewsResources = newsResources
69+
.take(MAX_NUM_NOTIFICATIONS)
70+
71+
val newsNotifications = truncatedNewsResources
72+
.map { newsResource ->
73+
createNewsNotification {
74+
setSmallIcon(
75+
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification,
76+
)
77+
.setContentTitle(newsResource.title)
78+
.setContentText(newsResource.content)
79+
.setContentIntent(newsPendingIntent(newsResource))
80+
.setGroup(NEWS_NOTIFICATION_GROUP)
81+
.setAutoCancel(true)
82+
}
83+
}
84+
val summaryNotification = createNewsNotification {
85+
val title = getString(
86+
R.string.news_notification_group_summary,
87+
truncatedNewsResources.size,
88+
)
89+
setContentTitle(title)
90+
.setContentText(title)
91+
.setSmallIcon(
92+
com.google.samples.apps.nowinandroid.core.common.R.drawable.ic_nia_notification,
93+
)
94+
// Build summary info into InboxStyle template.
95+
.setStyle(newsNotificationStyle(truncatedNewsResources, title))
96+
.setGroup(NEWS_NOTIFICATION_GROUP)
97+
.setGroupSummary(true)
98+
.setAutoCancel(true)
99+
.build()
100+
}
101+
102+
// Send the notifications
103+
val notificationManager = NotificationManagerCompat.from(this)
104+
newsNotifications.forEachIndexed { index, notification ->
105+
notificationManager.notify(
106+
truncatedNewsResources[index].id.hashCode(),
107+
notification,
108+
)
109+
}
110+
notificationManager.notify(NEWS_NOTIFICATION_SUMMARY_ID, summaryNotification)
111+
}
112+
113+
/**
114+
* Creates an inbox style summary notification for news updates
115+
*/
116+
private fun newsNotificationStyle(
117+
newsResources: List<NewsResource>,
118+
title: String,
119+
): InboxStyle = newsResources
120+
.fold(InboxStyle()) { inboxStyle, newsResource ->
121+
inboxStyle.addLine(newsResource.title)
122+
}
123+
.setBigContentTitle(title)
124+
.setSummaryText(title)
125+
}
126+
127+
/**
128+
* Creates a notification for configured for news updates
129+
*/
130+
private fun Context.createNewsNotification(
131+
block: NotificationCompat.Builder.() -> Unit,
132+
): Notification {
133+
ensureNotificationChannelExists()
134+
return NotificationCompat.Builder(
135+
this,
136+
NEWS_NOTIFICATION_CHANNEL_ID,
137+
)
138+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
139+
.apply(block)
140+
.build()
141+
}
142+
143+
/**
144+
* Ensures the a notification channel is is present if applicable
145+
*/
146+
private fun Context.ensureNotificationChannelExists() {
147+
if (VERSION.SDK_INT < VERSION_CODES.O) return
148+
149+
val channel = NotificationChannel(
150+
NEWS_NOTIFICATION_CHANNEL_ID,
151+
getString(R.string.news_notification_channel_name),
152+
NotificationManager.IMPORTANCE_DEFAULT,
153+
).apply {
154+
description = getString(R.string.news_notification_channel_description)
155+
}
156+
// Register the channel with the system
157+
NotificationManagerCompat.from(this).createNotificationChannel(channel)
158+
}
159+
160+
private fun Context.newsPendingIntent(
161+
newsResource: NewsResource,
162+
): PendingIntent? = PendingIntent.getActivity(
163+
this,
164+
NEWS_NOTIFICATION_REQUEST_CODE,
165+
Intent().apply {
166+
action = Intent.ACTION_VIEW
167+
data = newsResource.newsDeepLinkUri()
168+
component = ComponentName(
169+
packageName,
170+
TARGET_ACTIVITY_NAME,
171+
)
172+
},
173+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
174+
)
175+
176+
private fun NewsResource.newsDeepLinkUri() = "$DEEP_LINK_SCHEME_AND_HOST/$FOR_YOU_PATH/$id".toUri()

core/notifications/src/main/res/values/strings.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
limitations under the License.
1616
-->
1717
<resources>
18-
<string name="sync_notification_title">Now in Android</string>
19-
<string name="sync_notification_channel_name">Sync</string>
20-
<string name="sync_notification_channel_description">Background tasks for Now in Android</string>
21-
18+
<string name="news_notification_title">Now in Android</string>
19+
<string name="news_notification_channel_name">News updates</string>
20+
<string name="news_notification_channel_description">The latest updates on what\'s new in Android</string>
21+
<string name="news_notification_group_summary">%1$d news updates</string>
2222
</resources>

core/notifications/src/prod/java/com/google/samples/apps/nowinandroid/core/notifications/NotificationsModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,6 @@ import dagger.hilt.components.SingletonComponent
2626
abstract class NotificationsModule {
2727
@Binds
2828
abstract fun bindNotifier(
29-
notifier: AndroidSystemNotifier,
29+
notifier: SystemTrayNotifier,
3030
): Notifier
3131
}

0 commit comments

Comments
 (0)