1 /*
<lambda>null2 * Copyright (C) 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 * 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 package com.android.systemui.statusbar.notification.domain.interactor
17
18 import android.app.Notification.CallStyle.CALL_TYPE_INCOMING
19 import android.app.Notification.CallStyle.CALL_TYPE_ONGOING
20 import android.app.Notification.CallStyle.CALL_TYPE_SCREENING
21 import android.app.Notification.CallStyle.CALL_TYPE_UNKNOWN
22 import android.app.Notification.EXTRA_CALL_TYPE
23 import android.app.Notification.FLAG_ONGOING_EVENT
24 import android.app.PendingIntent
25 import android.content.Context
26 import android.graphics.drawable.Icon
27 import android.service.notification.StatusBarNotification
28 import android.util.ArrayMap
29 import com.android.app.tracing.traceSection
30 import com.android.internal.logging.InstanceId
31 import com.android.systemui.dagger.qualifiers.Main
32 import com.android.systemui.statusbar.StatusBarIconView
33 import com.android.systemui.statusbar.notification.collection.GroupEntry
34 import com.android.systemui.statusbar.notification.collection.NotificationEntry
35 import com.android.systemui.statusbar.notification.collection.PipelineEntry
36 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
37 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
38 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
39 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
40 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels
41 import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
42 import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
43 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
44 import com.android.systemui.statusbar.notification.shared.CallType
45 import javax.inject.Inject
46 import kotlinx.coroutines.flow.update
47
48 /**
49 * Logic for passing information from the
50 * [com.android.systemui.statusbar.notification.collection.NotifPipeline] to the presentation
51 * layers.
52 */
53 class RenderNotificationListInteractor
54 @Inject
55 constructor(
56 private val repository: ActiveNotificationListRepository,
57 private val sectionStyleProvider: SectionStyleProvider,
58 @Main private val context: Context,
59 ) {
60 /**
61 * Sets the current list of rendered notification entries as displayed in the notification list.
62 */
63 fun setRenderedList(entries: List<PipelineEntry>) {
64 traceSection("RenderNotificationListInteractor.setRenderedList") {
65 repository.activeNotifications.update { existingModels ->
66 buildActiveNotificationsStore(existingModels, sectionStyleProvider, context) {
67 entries.forEach(::addPipelineEntry)
68 setRankingsMap(entries)
69 }
70 }
71 }
72 }
73 }
74
buildActiveNotificationsStorenull75 private fun buildActiveNotificationsStore(
76 existingModels: ActiveNotificationsStore,
77 sectionStyleProvider: SectionStyleProvider,
78 context: Context,
79 block: ActiveNotificationsStoreBuilder.() -> Unit,
80 ): ActiveNotificationsStore =
81 ActiveNotificationsStoreBuilder(existingModels, sectionStyleProvider, context)
82 .apply(block)
83 .build()
84
85 private class ActiveNotificationsStoreBuilder(
86 private val existingModels: ActiveNotificationsStore,
87 private val sectionStyleProvider: SectionStyleProvider,
88 private val context: Context,
89 ) {
90 private val builder = ActiveNotificationsStore.Builder()
91
92 fun build(): ActiveNotificationsStore = builder.build()
93
94 /**
95 * Convert a [PipelineEntry] into [ActiveNotificationEntryModel]s, and add them to the
96 * [ActiveNotificationsStore]. Special care is taken to avoid re-allocating models if the result
97 * would be identical to an existing model (at the expense of additional computations).
98 */
99 fun addPipelineEntry(entry: PipelineEntry) {
100 when (entry) {
101 is GroupEntry -> {
102 entry.summary?.let { summary ->
103 val summaryModel = summary.toModel()
104 val childModels = entry.children.map { it.toModel() }
105 builder.addNotifGroup(
106 existingModels.createOrReuse(
107 key = entry.key,
108 summary = summaryModel,
109 children = childModels,
110 )
111 )
112 }
113 }
114 else -> {
115 entry.representativeEntry?.let { notifEntry ->
116 builder.addIndividualNotif(notifEntry.toModel())
117 }
118 }
119 }
120 }
121
122 fun setRankingsMap(entries: List<PipelineEntry>) {
123 builder.setRankingsMap(flatMapToRankingsMap(entries))
124 }
125
126 fun flatMapToRankingsMap(entries: List<PipelineEntry>): Map<String, Int> {
127 val result = ArrayMap<String, Int>()
128 for (entry in entries) {
129 if (entry is NotificationEntry) {
130 entry.representativeEntry?.let { representativeEntry ->
131 result[representativeEntry.key] = representativeEntry.ranking.rank
132 }
133 } else if (entry is GroupEntry) {
134 entry.summary?.let { summary -> result[summary.key] = summary.ranking.rank }
135 for (child in entry.children) {
136 result[child.key] = child.ranking.rank
137 }
138 }
139 }
140 return result
141 }
142
143 private fun NotificationEntry.toModel(): ActiveNotificationModel {
144 val promotedContent =
145 if (PromotedNotificationContentModel.featureFlagEnabled()) {
146 promotedNotificationContentModels
147 } else {
148 null
149 }
150
151 return existingModels.createOrReuse(
152 key = key,
153 groupKey = sbn.groupKey,
154 whenTime = sbn.notification.`when`,
155 isForegroundService = sbn.notification.isForegroundService,
156 isOngoingEvent = (sbn.notification.flags and FLAG_ONGOING_EVENT) != 0,
157 isAmbient = sectionStyleProvider.isMinimized(this),
158 isRowDismissed = isRowDismissed,
159 isSilent = sectionStyleProvider.isSilent(this),
160 isLastMessageFromReply = isLastMessageFromReply,
161 isSuppressedFromStatusBar = shouldSuppressStatusBar(),
162 isPulsing = showingPulsing(),
163 aodIcon = icons.aodIcon?.sourceIcon,
164 shelfIcon = icons.shelfIcon?.sourceIcon,
165 statusBarIcon = icons.statusBarIcon?.sourceIcon,
166 statusBarChipIconView = icons.statusBarChipIcon,
167 uid = sbn.uid,
168 packageName = sbn.packageName,
169 appName = sbn.notification.loadHeaderAppName(context) ?: "",
170 contentIntent = sbn.notification.contentIntent,
171 instanceId = sbn.instanceId,
172 isGroupSummary = sbn.notification.isGroupSummary,
173 bucket = bucket,
174 callType = sbn.toCallType(),
175 promotedContent = promotedContent,
176 )
177 }
178 }
179
createOrReusenull180 private fun ActiveNotificationsStore.createOrReuse(
181 key: String,
182 groupKey: String?,
183 whenTime: Long,
184 isForegroundService: Boolean,
185 isOngoingEvent: Boolean,
186 isAmbient: Boolean,
187 isRowDismissed: Boolean,
188 isSilent: Boolean,
189 isLastMessageFromReply: Boolean,
190 isSuppressedFromStatusBar: Boolean,
191 isPulsing: Boolean,
192 aodIcon: Icon?,
193 shelfIcon: Icon?,
194 statusBarIcon: Icon?,
195 statusBarChipIconView: StatusBarIconView?,
196 uid: Int,
197 packageName: String,
198 appName: String,
199 contentIntent: PendingIntent?,
200 instanceId: InstanceId?,
201 isGroupSummary: Boolean,
202 bucket: Int,
203 callType: CallType,
204 promotedContent: PromotedNotificationContentModels?,
205 ): ActiveNotificationModel {
206 return individuals[key]?.takeIf {
207 it.isCurrent(
208 key = key,
209 groupKey = groupKey,
210 whenTime = whenTime,
211 isForegroundService = isForegroundService,
212 isOngoingEvent = isOngoingEvent,
213 isAmbient = isAmbient,
214 isRowDismissed = isRowDismissed,
215 isSilent = isSilent,
216 isLastMessageFromReply = isLastMessageFromReply,
217 isSuppressedFromStatusBar = isSuppressedFromStatusBar,
218 isPulsing = isPulsing,
219 aodIcon = aodIcon,
220 shelfIcon = shelfIcon,
221 statusBarIcon = statusBarIcon,
222 statusBarChipIconView = statusBarChipIconView,
223 uid = uid,
224 instanceId = instanceId,
225 isGroupSummary = isGroupSummary,
226 packageName = packageName,
227 appName = appName,
228 contentIntent = contentIntent,
229 bucket = bucket,
230 callType = callType,
231 promotedContent = promotedContent,
232 )
233 }
234 ?: ActiveNotificationModel(
235 key = key,
236 groupKey = groupKey,
237 whenTime = whenTime,
238 isForegroundService = isForegroundService,
239 isOngoingEvent = isOngoingEvent,
240 isAmbient = isAmbient,
241 isRowDismissed = isRowDismissed,
242 isSilent = isSilent,
243 isLastMessageFromReply = isLastMessageFromReply,
244 isSuppressedFromStatusBar = isSuppressedFromStatusBar,
245 isPulsing = isPulsing,
246 aodIcon = aodIcon,
247 shelfIcon = shelfIcon,
248 statusBarIcon = statusBarIcon,
249 statusBarChipIconView = statusBarChipIconView,
250 uid = uid,
251 instanceId = instanceId,
252 isGroupSummary = isGroupSummary,
253 packageName = packageName,
254 appName = appName,
255 contentIntent = contentIntent,
256 bucket = bucket,
257 callType = callType,
258 promotedContent = promotedContent,
259 )
260 }
261
ActiveNotificationModelnull262 private fun ActiveNotificationModel.isCurrent(
263 key: String,
264 groupKey: String?,
265 whenTime: Long,
266 isForegroundService: Boolean,
267 isOngoingEvent: Boolean,
268 isAmbient: Boolean,
269 isRowDismissed: Boolean,
270 isSilent: Boolean,
271 isLastMessageFromReply: Boolean,
272 isSuppressedFromStatusBar: Boolean,
273 isPulsing: Boolean,
274 aodIcon: Icon?,
275 shelfIcon: Icon?,
276 statusBarIcon: Icon?,
277 statusBarChipIconView: StatusBarIconView?,
278 uid: Int,
279 packageName: String,
280 appName: String,
281 contentIntent: PendingIntent?,
282 instanceId: InstanceId?,
283 isGroupSummary: Boolean,
284 bucket: Int,
285 callType: CallType,
286 promotedContent: PromotedNotificationContentModels?,
287 ): Boolean {
288 return when {
289 key != this.key -> false
290 groupKey != this.groupKey -> false
291 whenTime != this.whenTime -> false
292 isForegroundService != this.isForegroundService -> false
293 isOngoingEvent != this.isOngoingEvent -> false
294 isAmbient != this.isAmbient -> false
295 isRowDismissed != this.isRowDismissed -> false
296 isSilent != this.isSilent -> false
297 isLastMessageFromReply != this.isLastMessageFromReply -> false
298 isSuppressedFromStatusBar != this.isSuppressedFromStatusBar -> false
299 isPulsing != this.isPulsing -> false
300 aodIcon != this.aodIcon -> false
301 shelfIcon != this.shelfIcon -> false
302 statusBarIcon != this.statusBarIcon -> false
303 statusBarChipIconView != this.statusBarChipIconView -> false
304 uid != this.uid -> false
305 instanceId != this.instanceId -> false
306 isGroupSummary != this.isGroupSummary -> false
307 packageName != this.packageName -> false
308 appName != this.appName -> false
309 contentIntent != this.contentIntent -> false
310 bucket != this.bucket -> false
311 callType != this.callType -> false
312 // QQQ: Do we need to do the same `isCurrent` thing within the content model to avoid
313 // recreating the active notification model constantly?
314 promotedContent != this.promotedContent -> false
315 else -> true
316 }
317 }
318
createOrReusenull319 private fun ActiveNotificationsStore.createOrReuse(
320 key: String,
321 summary: ActiveNotificationModel,
322 children: List<ActiveNotificationModel>,
323 ): ActiveNotificationGroupModel {
324 return groups[key]?.takeIf { it.isCurrent(key, summary, children) }
325 ?: ActiveNotificationGroupModel(key, summary, children)
326 }
327
ActiveNotificationGroupModelnull328 private fun ActiveNotificationGroupModel.isCurrent(
329 key: String,
330 summary: ActiveNotificationModel,
331 children: List<ActiveNotificationModel>,
332 ): Boolean {
333 return when {
334 key != this.key -> false
335 summary != this.summary -> false
336 children != this.children -> false
337 else -> true
338 }
339 }
340
toCallTypenull341 private fun StatusBarNotification.toCallType(): CallType =
342 when (this.notification.extras.getInt(EXTRA_CALL_TYPE, -1)) {
343 -1 -> CallType.None
344 CALL_TYPE_INCOMING -> CallType.Incoming
345 CALL_TYPE_ONGOING -> CallType.Ongoing
346 CALL_TYPE_SCREENING -> CallType.Screening
347 CALL_TYPE_UNKNOWN -> CallType.Unknown
348 else -> CallType.Unknown
349 }
350