• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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