1 /*
<lambda>null2 * Copyright (C) 2024 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 * http://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.android.systemui.statusbar.notification.promoted
18
19 import android.app.Notification
20 import android.app.Notification.BigPictureStyle
21 import android.app.Notification.BigTextStyle
22 import android.app.Notification.CallStyle
23 import android.app.Notification.EXTRA_BIG_TEXT
24 import android.app.Notification.EXTRA_CALL_PERSON
25 import android.app.Notification.EXTRA_CHRONOMETER_COUNT_DOWN
26 import android.app.Notification.EXTRA_PROGRESS
27 import android.app.Notification.EXTRA_PROGRESS_INDETERMINATE
28 import android.app.Notification.EXTRA_PROGRESS_MAX
29 import android.app.Notification.EXTRA_SUB_TEXT
30 import android.app.Notification.EXTRA_TEXT
31 import android.app.Notification.EXTRA_TITLE
32 import android.app.Notification.EXTRA_TITLE_BIG
33 import android.app.Notification.EXTRA_VERIFICATION_ICON
34 import android.app.Notification.EXTRA_VERIFICATION_TEXT
35 import android.app.Notification.InboxStyle
36 import android.app.Notification.ProgressStyle
37 import android.app.Person
38 import android.content.Context
39 import android.graphics.drawable.Icon
40 import com.android.systemui.Flags
41 import com.android.systemui.dagger.SysUISingleton
42 import com.android.systemui.shade.ShadeDisplayAware
43 import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE
44 import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType
45 import com.android.systemui.statusbar.notification.collection.NotificationEntry
46 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT
47 import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
48 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
49 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Companion.isPromotedForStatusBarChip
50 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress
51 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
52 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
53 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
54 import com.android.systemui.statusbar.notification.row.shared.ImageModel
55 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
56 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.MediumSquare
57 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass.SmallSquare
58 import com.android.systemui.statusbar.notification.row.shared.SkeletonImageTransform
59 import com.android.systemui.util.time.SystemClock
60 import javax.inject.Inject
61
62 interface PromotedNotificationContentExtractor {
63 fun extractContent(
64 entry: NotificationEntry,
65 recoveredBuilder: Notification.Builder,
66 @RedactionType redactionType: Int,
67 imageModelProvider: ImageModelProvider,
68 ): PromotedNotificationContentModels?
69 }
70
71 @SysUISingleton
72 class PromotedNotificationContentExtractorImpl
73 @Inject
74 constructor(
75 @ShadeDisplayAware private val context: Context,
76 private val skeletonImageTransform: SkeletonImageTransform,
77 private val systemClock: SystemClock,
78 private val logger: PromotedNotificationLogger,
79 ) : PromotedNotificationContentExtractor {
extractContentnull80 override fun extractContent(
81 entry: NotificationEntry,
82 recoveredBuilder: Notification.Builder,
83 @RedactionType redactionType: Int,
84 imageModelProvider: ImageModelProvider,
85 ): PromotedNotificationContentModels? {
86 if (!PromotedNotificationContentModel.featureFlagEnabled()) {
87 logger.logExtractionSkipped(entry, "feature flags disabled")
88 return null
89 }
90
91 val notification = entry.sbn.notification
92 if (notification == null) {
93 logger.logExtractionFailed(entry, "entry.sbn.notification is null")
94 return null
95 }
96
97 // The status bar chips rely on this extractor, so take them into account for promotion.
98 if (!isPromotedForStatusBarChip(notification)) {
99 logger.logExtractionSkipped(entry, "isPromotedOngoing returned false")
100 return null
101 }
102
103 val privateVersion =
104 extractPrivateContent(
105 key = entry.key,
106 notification = notification,
107 recoveredBuilder = recoveredBuilder,
108 lastAudiblyAlertedMs = entry.lastAudiblyAlertedMs,
109 imageModelProvider = imageModelProvider,
110 )
111 val publicVersion =
112 if (redactionType == REDACTION_TYPE_NONE) {
113 privateVersion
114 } else {
115 notification.publicVersion?.let { publicNotification ->
116 createAppDefinedPublicVersion(
117 privateModel = privateVersion,
118 publicNotification = publicNotification,
119 imageModelProvider = imageModelProvider,
120 )
121 } ?: createDefaultPublicVersion(privateModel = privateVersion)
122 }
123 return PromotedNotificationContentModels(
124 privateVersion = privateVersion,
125 publicVersion = publicVersion,
126 )
127 .also { logger.logExtractionSucceeded(entry, it) }
128 }
129
copyNonSensitiveFieldsnull130 private fun copyNonSensitiveFields(
131 privateModel: PromotedNotificationContentModel,
132 publicBuilder: PromotedNotificationContentModel.Builder,
133 ) {
134 publicBuilder.smallIcon = privateModel.smallIcon
135 publicBuilder.iconLevel = privateModel.iconLevel
136 publicBuilder.appName = privateModel.appName
137 publicBuilder.time = privateModel.time
138 publicBuilder.lastAudiblyAlertedMs = privateModel.lastAudiblyAlertedMs
139 publicBuilder.profileBadgeResId = privateModel.profileBadgeResId
140 publicBuilder.colors = privateModel.colors
141 }
142
createDefaultPublicVersionnull143 private fun createDefaultPublicVersion(
144 privateModel: PromotedNotificationContentModel
145 ): PromotedNotificationContentModel =
146 PromotedNotificationContentModel.Builder(key = privateModel.identity.key)
147 .also {
148 it.style =
149 if (privateModel.style == Style.Ineligible) Style.Ineligible else Style.Base
150 copyNonSensitiveFields(privateModel, it)
151 }
152 .build()
153
createAppDefinedPublicVersionnull154 private fun createAppDefinedPublicVersion(
155 privateModel: PromotedNotificationContentModel,
156 publicNotification: Notification,
157 imageModelProvider: ImageModelProvider,
158 ): PromotedNotificationContentModel =
159 PromotedNotificationContentModel.Builder(key = privateModel.identity.key)
160 .also { publicBuilder ->
161 val notificationStyle = publicNotification.notificationStyle
162 publicBuilder.style =
163 when {
164 privateModel.style == Style.Ineligible -> Style.Ineligible
165 notificationStyle == CallStyle::class.java -> Style.CollapsedCall
166 else -> Style.CollapsedBase
167 }
168 copyNonSensitiveFields(privateModel = privateModel, publicBuilder = publicBuilder)
169 publicBuilder.shortCriticalText = publicNotification.shortCriticalText()
170 publicBuilder.subText = publicNotification.subText()
171 // The standard public version is extracted as a collapsed notification,
172 // so avoid using bigTitle or bigText, and instead get the collapsed versions.
173 publicBuilder.title = publicNotification.title(notificationStyle, expanded = false)
174 publicBuilder.text = publicNotification.text()
175 publicBuilder.skeletonLargeIcon =
176 publicNotification.skeletonLargeIcon(imageModelProvider)
177 // Only CallStyle has styled content that shows in the collapsed version.
178 if (publicBuilder.style == Style.Call) {
179 extractCallStyleContent(publicNotification, publicBuilder, imageModelProvider)
180 }
181 }
182 .build()
183
extractPrivateContentnull184 private fun extractPrivateContent(
185 key: String,
186 notification: Notification,
187 recoveredBuilder: Notification.Builder,
188 lastAudiblyAlertedMs: Long,
189 imageModelProvider: ImageModelProvider,
190 ): PromotedNotificationContentModel {
191
192 val contentBuilder = PromotedNotificationContentModel.Builder(key)
193
194 // TODO: Pitch a fit if style is unsupported or mandatory fields are missing once
195 // FLAG_PROMOTED_ONGOING is set reliably and we're not testing status bar chips.
196
197 contentBuilder.wasPromotedAutomatically =
198 notification.extras.getBoolean(EXTRA_WAS_AUTOMATICALLY_PROMOTED, false)
199 contentBuilder.smallIcon = notification.smallIconModel(imageModelProvider)
200 contentBuilder.iconLevel = notification.iconLevel
201 contentBuilder.appName = notification.loadHeaderAppName(context)
202 contentBuilder.subText = notification.subText()
203 contentBuilder.time = notification.extractWhen()
204 contentBuilder.shortCriticalText = notification.shortCriticalText()
205 contentBuilder.lastAudiblyAlertedMs = lastAudiblyAlertedMs
206 contentBuilder.profileBadgeResId = null // TODO
207 contentBuilder.title = notification.title(recoveredBuilder.style?.javaClass)
208 contentBuilder.text = notification.text(recoveredBuilder.style?.javaClass)
209 contentBuilder.skeletonLargeIcon = notification.skeletonLargeIcon(imageModelProvider)
210 contentBuilder.oldProgress = notification.oldProgress()
211
212 val colorsFromNotif = recoveredBuilder.getColors(/* isHeader= */ false)
213 contentBuilder.colors =
214 PromotedNotificationContentModel.Colors(
215 backgroundColor = colorsFromNotif.backgroundColor,
216 primaryTextColor = colorsFromNotif.primaryTextColor,
217 )
218
219 recoveredBuilder.extractStyleContent(notification, contentBuilder, imageModelProvider)
220
221 return contentBuilder.build()
222 }
223
smallIconModelnull224 private fun Notification.smallIconModel(imageModelProvider: ImageModelProvider): ImageModel? =
225 imageModelProvider.getImageModel(smallIcon, SmallSquare)
226
227 private fun Notification.title(): CharSequence? = getCharSequenceExtraUnlessEmpty(EXTRA_TITLE)
228
229 private fun Notification.bigTitle(): CharSequence? =
230 getCharSequenceExtraUnlessEmpty(EXTRA_TITLE_BIG)
231
232 private fun Notification.callPerson(): Person? =
233 extras?.getParcelable(EXTRA_CALL_PERSON, Person::class.java)
234
235 private fun Notification.title(
236 styleClass: Class<out Notification.Style>?,
237 expanded: Boolean = true,
238 ): CharSequence? {
239 // bigTitle is only used in the expanded form of 3 styles.
240 return when (styleClass) {
241 BigTextStyle::class.java,
242 BigPictureStyle::class.java,
243 InboxStyle::class.java -> if (expanded) bigTitle() else null
244 CallStyle::class.java -> callPerson()?.name?.takeUnlessEmpty()
245 else -> null
246 } ?: title()
247 }
248
textnull249 private fun Notification.text(): CharSequence? = getCharSequenceExtraUnlessEmpty(EXTRA_TEXT)
250
251 private fun Notification.bigText(): CharSequence? =
252 getCharSequenceExtraUnlessEmpty(EXTRA_BIG_TEXT)
253
254 private fun Notification.text(styleClass: Class<out Notification.Style>?): CharSequence? {
255 return when (styleClass) {
256 BigTextStyle::class.java -> bigText()
257 else -> null
258 } ?: text()
259 }
260
subTextnull261 private fun Notification.subText(): String? = getStringExtraUnlessEmpty(EXTRA_SUB_TEXT)
262
263 private fun Notification.shortCriticalText(): String? {
264 if (!android.app.Flags.apiRichOngoing()) {
265 return null
266 }
267 if (shortCriticalText != null) {
268 return shortCriticalText
269 }
270 if (Flags.promoteNotificationsAutomatically()) {
271 return getStringExtraUnlessEmpty(EXTRA_AUTOMATICALLY_EXTRACTED_SHORT_CRITICAL_TEXT)
272 }
273 return null
274 }
275
chronometerCountDownnull276 private fun Notification.chronometerCountDown(): Boolean =
277 extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false
278
279 private fun Notification.skeletonLargeIcon(
280 imageModelProvider: ImageModelProvider
281 ): ImageModel? =
282 getLargeIcon()?.let {
283 imageModelProvider.getImageModel(it, MediumSquare, skeletonImageTransform)
284 }
285
oldProgressnull286 private fun Notification.oldProgress(): OldProgress? {
287 val progress = progress() ?: return null
288 val max = progressMax() ?: return null
289 val isIndeterminate = progressIndeterminate() ?: return null
290
291 return OldProgress(progress = progress, max = max, isIndeterminate = isIndeterminate)
292 }
293
progressnull294 private fun Notification.progress(): Int? = extras?.getInt(EXTRA_PROGRESS)
295
296 private fun Notification.progressMax(): Int? = extras?.getInt(EXTRA_PROGRESS_MAX)
297
298 private fun Notification.progressIndeterminate(): Boolean? =
299 extras?.getBoolean(EXTRA_PROGRESS_INDETERMINATE)
300
301 private fun Notification.extractWhen(): When? {
302 val whenTime = getWhen()
303
304 return when {
305 showsChronometer() -> {
306 When.Chronometer(
307 elapsedRealtimeMillis =
308 whenTime + systemClock.elapsedRealtime() - systemClock.currentTimeMillis(),
309 isCountDown = chronometerCountDown(),
310 )
311 }
312
313 showsTime() -> When.Time(currentTimeMillis = whenTime)
314
315 else -> null
316 }
317 }
318
skeletonVerificationIconnull319 private fun Notification.skeletonVerificationIcon(
320 imageModelProvider: ImageModelProvider
321 ): ImageModel? =
322 extras.getParcelable(EXTRA_VERIFICATION_ICON, Icon::class.java)?.let {
323 imageModelProvider.getImageModel(it, SmallSquare, skeletonImageTransform)
324 }
325
verificationTextnull326 private fun Notification.verificationText(): CharSequence? =
327 getCharSequenceExtraUnlessEmpty(EXTRA_VERIFICATION_TEXT)
328
329 private fun Notification.Builder.extractStyleContent(
330 notification: Notification,
331 contentBuilder: PromotedNotificationContentModel.Builder,
332 imageModelProvider: ImageModelProvider,
333 ) {
334 val style = this.style
335
336 contentBuilder.style =
337 when (style) {
338 null -> Style.Base
339
340 is BigPictureStyle -> {
341 Style.BigPicture
342 }
343
344 is BigTextStyle -> {
345 Style.BigText
346 }
347
348 is CallStyle -> {
349 extractCallStyleContent(notification, contentBuilder, imageModelProvider)
350 Style.Call
351 }
352
353 is ProgressStyle -> {
354 style.extractContent(contentBuilder)
355 Style.Progress
356 }
357
358 else -> Style.Ineligible
359 }
360 }
361
extractCallStyleContentnull362 private fun extractCallStyleContent(
363 notification: Notification,
364 contentBuilder: PromotedNotificationContentModel.Builder,
365 imageModelProvider: ImageModelProvider,
366 ) {
367 contentBuilder.verificationIcon = notification.skeletonVerificationIcon(imageModelProvider)
368 contentBuilder.verificationText = notification.verificationText()
369 }
370
ProgressStylenull371 private fun ProgressStyle.extractContent(
372 contentBuilder: PromotedNotificationContentModel.Builder
373 ) {
374 // TODO: Create NotificationProgressModel.toSkeleton, or something similar.
375 contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt())
376 }
377 }
378
getCharSequenceExtraUnlessEmptynull379 private fun Notification.getCharSequenceExtraUnlessEmpty(key: String): CharSequence? =
380 extras?.getCharSequence(key)?.takeUnlessEmpty()
381
382 private fun Notification.getStringExtraUnlessEmpty(key: String): String? =
383 extras?.getString(key)?.takeUnlessEmpty()
384
385 private fun <T : CharSequence> T.takeUnlessEmpty(): T? = takeUnless { it.isEmpty() }
386