1 /*
<lambda>null2  * Copyright (C) 2020 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.icon
18 
19 import android.app.Notification
20 import android.app.Notification.MessagingStyle
21 import android.app.Person
22 import android.content.Context
23 import android.content.pm.LauncherApps
24 import android.graphics.drawable.Icon
25 import android.os.Build
26 import android.os.Bundle
27 import android.util.Log
28 import android.view.View
29 import android.widget.ImageView
30 import com.android.app.tracing.coroutines.launchTraced as launch
31 import com.android.app.tracing.traceSection
32 import com.android.internal.statusbar.StatusBarIcon
33 import com.android.systemui.Flags
34 import com.android.systemui.dagger.SysUISingleton
35 import com.android.systemui.dagger.qualifiers.Application
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.dagger.qualifiers.Main
38 import com.android.systemui.res.R
39 import com.android.systemui.statusbar.StatusBarIconView
40 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
41 import com.android.systemui.statusbar.notification.InflationException
42 import com.android.systemui.statusbar.notification.collection.NotificationEntry
43 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
44 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
45 import java.util.concurrent.ConcurrentHashMap
46 import javax.inject.Inject
47 import kotlin.coroutines.CoroutineContext
48 import kotlinx.coroutines.CoroutineScope
49 import kotlinx.coroutines.Job
50 import kotlinx.coroutines.withContext
51 
52 /**
53  * Inflates and updates icons associated with notifications
54  *
55  * Notifications are represented by icons in a few different places -- in the status bar, in the
56  * notification shelf, in AOD, etc. This class is in charge of inflating the views that hold these
57  * icons and keeping the icon assets themselves up to date as notifications change.
58  *
59  * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry.
60  *   Long-term, it should probably live somewhere in the content inflation pipeline.
61  */
62 @SysUISingleton
63 class IconManager
64 @Inject
65 constructor(
66     private val notifCollection: CommonNotifCollection,
67     private val launcherApps: LauncherApps,
68     private val iconBuilder: IconBuilder,
69     @Application private val applicationCoroutineScope: CoroutineScope,
70     @Background private val bgCoroutineContext: CoroutineContext,
71     @Main private val mainCoroutineContext: CoroutineContext,
72 ) : ConversationIconManager {
73 
74     /**
75      * A listener that is notified when a [NotificationEntry] has been updated and the associated
76      * icons have to be updated as well.
77      */
78     fun interface OnIconUpdateRequiredListener {
79         fun onIconUpdateRequired(entry: NotificationEntry)
80     }
81 
82     private val onIconUpdateRequiredListeners = mutableSetOf<OnIconUpdateRequiredListener>()
83 
84     private var unimportantConversationKeys: Set<String> = emptySet()
85     /**
86      * A map of running jobs for fetching the person avatar from launcher. The key is the
87      * notification entry key.
88      */
89     private var launcherPeopleAvatarIconJobs: ConcurrentHashMap<String, Job> =
90         ConcurrentHashMap<String, Job>()
91 
92     fun addIconsUpdateListener(listener: OnIconUpdateRequiredListener) {
93         StatusBarConnectedDisplays.assertInNewMode()
94         onIconUpdateRequiredListeners += listener
95     }
96 
97     fun removeIconsUpdateListener(listener: OnIconUpdateRequiredListener) {
98         StatusBarConnectedDisplays.assertInNewMode()
99         onIconUpdateRequiredListeners -= listener
100     }
101 
102     fun attach() {
103         notifCollection.addCollectionListener(entryListener)
104     }
105 
106     private val entryListener =
107         object : NotifCollectionListener {
108             override fun onEntryInit(entry: NotificationEntry) {
109                 entry.addOnSensitivityChangedListener(sensitivityListener)
110             }
111 
112             override fun onEntryCleanUp(entry: NotificationEntry) {
113                 entry.removeOnSensitivityChangedListener(sensitivityListener)
114             }
115 
116             override fun onRankingApplied() {
117                 // rankings affect whether a conversation is important, which can change the icons
118                 recalculateForImportantConversationChange()
119             }
120         }
121 
122     private val sensitivityListener =
123         NotificationEntry.OnSensitivityChangedListener { entry -> updateIconsSafe(entry) }
124 
125     private fun recalculateForImportantConversationChange() {
126         for (entry in notifCollection.allNotifs) {
127             val isImportant = isImportantConversation(entry)
128             if (
129                 entry.icons.areIconsAvailable && isImportant != entry.icons.isImportantConversation
130             ) {
131                 updateIconsSafe(entry)
132             }
133             entry.icons.isImportantConversation = isImportant
134         }
135     }
136 
137     /**
138      * Inflate the [StatusBarIconView] for the given [NotificationEntry], using the specified
139      * [Context].
140      */
141     fun createSbIconView(context: Context, entry: NotificationEntry): StatusBarIconView =
142         traceSection("IconManager.createSbIconView") {
143             StatusBarConnectedDisplays.assertInNewMode()
144 
145             val sbIcon = iconBuilder.createIconView(entry, context)
146             sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
147             val (normalIconDescriptor, _) = getIconDescriptors(entry)
148             setIcon(entry, normalIconDescriptor, sbIcon)
149             return sbIcon
150         }
151 
152     /**
153      * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the
154      * result in [NotificationEntry.getIcons].
155      *
156      * @throws InflationException Exception if required icons are not valid or specified
157      */
158     @Throws(InflationException::class)
159     fun createIcons(entry: NotificationEntry) =
160         traceSection("IconManager.createIcons") {
161             // Construct the status bar icon view.
162             val sbIcon = iconBuilder.createIconView(entry)
163             sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
164             val sbChipIcon: StatusBarIconView?
165             if (
166                 Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled
167             ) {
168                 sbChipIcon = iconBuilder.createIconView(entry)
169                 sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
170             } else {
171                 sbChipIcon = null
172             }
173 
174             // Construct the shelf icon view.
175             val shelfIcon = iconBuilder.createIconView(entry)
176             shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
177             shelfIcon.visibility = View.INVISIBLE
178 
179             // Construct the aod icon view.
180             val aodIcon = iconBuilder.createIconView(entry)
181             aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
182             aodIcon.setIncreasedSize(true)
183 
184             // Set the icon views' icons
185             val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
186 
187             try {
188                 setIcon(entry, normalIconDescriptor, sbIcon)
189                 if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) {
190                     setIcon(entry, normalIconDescriptor, sbChipIcon)
191                 }
192                 setIcon(entry, sensitiveIconDescriptor, shelfIcon)
193                 setIcon(entry, sensitiveIconDescriptor, aodIcon)
194                 entry.icons =
195                     IconPack.buildPack(sbIcon, sbChipIcon, shelfIcon, aodIcon, entry.icons)
196             } catch (e: InflationException) {
197                 entry.icons = IconPack.buildEmptyPack(entry.icons)
198                 throw e
199             }
200         }
201 
202     /** Update the [StatusBarIconView] for the given [NotificationEntry]. */
203     fun updateSbIcon(entry: NotificationEntry, iconView: StatusBarIconView) =
204         traceSection("IconManager.updateSbIcon") {
205             StatusBarConnectedDisplays.assertInNewMode()
206 
207             val (normalIconDescriptor, _) = getIconDescriptors(entry)
208             val notificationContentDescription =
209                 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) }
210             iconView.setNotification(entry.sbn, notificationContentDescription)
211             setIcon(entry, normalIconDescriptor, iconView)
212         }
213 
214     /**
215      * Update the notification icons.
216      *
217      * @param entry the notification to read the icon from.
218      * @throws InflationException Exception if required icons are not valid or specified
219      */
220     @Throws(InflationException::class)
221     fun updateIcons(entry: NotificationEntry, usingCache: Boolean = false) =
222         traceSection("IconManager.updateIcons") {
223             if (!entry.icons.areIconsAvailable) {
224                 return@traceSection
225             }
226 
227             if (StatusBarConnectedDisplays.isEnabled) {
228                 onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) }
229             }
230 
231             if (usingCache && !Flags.notificationsBackgroundIcons()) {
232                 Log.wtf(
233                     TAG,
234                     "Updating using the cache is not supported when the " +
235                         "notifications_background_icons flag is off",
236                 )
237             }
238             if (!usingCache || !Flags.notificationsBackgroundIcons()) {
239                 entry.icons.smallIconDescriptor = null
240                 entry.icons.peopleAvatarDescriptor = null
241             }
242 
243             val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
244             val notificationContentDescription =
245                 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) }
246 
247             entry.icons.statusBarIcon?.let {
248                 it.setNotification(entry.sbn, notificationContentDescription)
249                 setIcon(entry, normalIconDescriptor, it)
250             }
251 
252             entry.icons.statusBarChipIcon?.let {
253                 it.setNotification(entry.sbn, notificationContentDescription)
254                 setIcon(entry, normalIconDescriptor, it)
255             }
256 
257             entry.icons.shelfIcon?.let {
258                 it.setNotification(entry.sbn, notificationContentDescription)
259                 setIcon(entry, sensitiveIconDescriptor, it)
260             }
261 
262             entry.icons.aodIcon?.let {
263                 it.setNotification(entry.sbn, notificationContentDescription)
264                 setIcon(entry, sensitiveIconDescriptor, it)
265             }
266         }
267 
268     private fun updateIconsSafe(entry: NotificationEntry) {
269         try {
270             updateIcons(entry)
271         } catch (e: InflationException) {
272             // TODO This should mark the entire row as involved in an inflation error
273             Log.e(TAG, "Unable to update icon", e)
274         }
275     }
276 
277     @Throws(InflationException::class)
278     private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> {
279         val iconDescriptor = getIconDescriptor(entry, redact = false)
280         val sensitiveDescriptor =
281             if (entry.isSensitive.value) {
282                 getIconDescriptor(entry, redact = true)
283             } else {
284                 iconDescriptor
285             }
286         return Pair(iconDescriptor, sensitiveDescriptor)
287     }
288 
289     @Throws(InflationException::class)
290     private fun getIconDescriptor(entry: NotificationEntry, redact: Boolean): StatusBarIcon {
291         val showPeopleAvatar = !redact && isImportantConversation(entry)
292 
293         // If the descriptor is already cached, return it
294         getCachedIconDescriptor(entry, showPeopleAvatar)?.also {
295             return it
296         }
297 
298         val n = entry.sbn.notification
299         val (icon: Icon?, type: StatusBarIcon.Type) =
300             if (showPeopleAvatar) {
301                 createPeopleAvatar(entry) to StatusBarIcon.Type.PeopleAvatar
302             } else {
303                 n.smallIcon to StatusBarIcon.Type.NotifSmallIcon
304             }
305         if (icon == null) {
306             throw InflationException("No icon in notification from ${entry.sbn.packageName}")
307         }
308 
309         val sbi = icon.toStatusBarIcon(entry, type)
310         cacheIconDescriptor(entry, sbi)
311         return sbi
312     }
313 
314     private fun getCachedIconDescriptor(
315         entry: NotificationEntry,
316         showPeopleAvatar: Boolean,
317     ): StatusBarIcon? {
318         val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor
319         val smallIconDescriptor = entry.icons.smallIconDescriptor
320 
321         // If cached, return corresponding cached values
322         return when {
323             showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor
324             smallIconDescriptor != null -> smallIconDescriptor
325             else -> null
326         }
327     }
328 
329     private fun cacheIconDescriptor(entry: NotificationEntry, descriptor: StatusBarIcon) {
330         if (android.app.Flags.notificationsRedesignAppIcons()) {
331             // Although we're not actually using the app icon in the status bar, let's make sure
332             // we cache the icon all the time when the flag is on.
333             when (descriptor.type) {
334                 StatusBarIcon.Type.PeopleAvatar -> entry.icons.peopleAvatarDescriptor = descriptor
335                 // When notificationsUseAppIcon is enabled, the app icon overrides the small icon.
336                 // But either way, it's a good idea to cache the descriptor.
337                 else -> entry.icons.smallIconDescriptor = descriptor
338             }
339         } else if (isImportantConversation(entry)) {
340             // Old approach: cache only if important conversation.
341             if (descriptor.type == StatusBarIcon.Type.PeopleAvatar) {
342                 entry.icons.peopleAvatarDescriptor = descriptor
343             } else {
344                 entry.icons.smallIconDescriptor = descriptor
345             }
346         }
347     }
348 
349     @Throws(InflationException::class)
350     private fun setIcon(
351         entry: NotificationEntry,
352         iconDescriptor: StatusBarIcon,
353         iconView: StatusBarIconView,
354     ) {
355         iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor))
356         iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
357         if (!iconView.set(iconDescriptor)) {
358             throw InflationException("Couldn't create icon $iconDescriptor")
359         }
360     }
361 
362     private fun Icon.toStatusBarIcon(
363         entry: NotificationEntry,
364         type: StatusBarIcon.Type,
365     ): StatusBarIcon {
366         val n = entry.sbn.notification
367         return StatusBarIcon(
368             entry.sbn.user,
369             entry.sbn.packageName,
370             /* icon = */ this,
371             n.iconLevel,
372             n.number,
373             iconBuilder.getIconContentDescription(n),
374             type,
375         )
376     }
377 
378     private suspend fun getLauncherShortcutIconForPeopleAvatar(entry: NotificationEntry) =
379         withContext(bgCoroutineContext) {
380             var icon: Icon? = null
381             val shortcut = entry.ranking.conversationShortcutInfo
382             if (shortcut != null) {
383                 try {
384                     icon = launcherApps.getShortcutIcon(shortcut)
385                 } catch (e: Exception) {
386                     Log.e(
387                         TAG,
388                         "Error calling LauncherApps#getShortcutIcon for notification $entry: $e",
389                     )
390                 }
391             }
392 
393             // Once we have the icon, updating it should happen on the main thread.
394             if (icon != null) {
395                 withContext(mainCoroutineContext) {
396                     val iconDescriptor =
397                         icon.toStatusBarIcon(entry, StatusBarIcon.Type.PeopleAvatar)
398 
399                     // Cache the value
400                     entry.icons.peopleAvatarDescriptor = iconDescriptor
401 
402                     // Update the icons using the cached value
403                     updateIcons(entry = entry, usingCache = true)
404                 }
405             }
406         }
407 
408     @Throws(InflationException::class)
409     private fun createPeopleAvatar(entry: NotificationEntry): Icon {
410         var ic: Icon? = null
411 
412         if (Flags.notificationsBackgroundIcons()) {
413             // Ideally we want to get the icon from launcher, but this is a binder transaction that
414             // may take longer so let's kick it off on a background thread and use a placeholder in
415             // the meantime.
416             // Cancel the previous job if necessary.
417             launcherPeopleAvatarIconJobs[entry.key]?.cancel()
418             launcherPeopleAvatarIconJobs[entry.key] =
419                 applicationCoroutineScope
420                     .launch { getLauncherShortcutIconForPeopleAvatar(entry) }
421                     .apply { invokeOnCompletion { launcherPeopleAvatarIconJobs.remove(entry.key) } }
422         } else {
423             val shortcut = entry.ranking.conversationShortcutInfo
424             if (shortcut != null) {
425                 ic = launcherApps.getShortcutIcon(shortcut)
426             }
427         }
428 
429         // Try to extract from message
430         if (ic == null) {
431             val extras: Bundle = entry.sbn.notification.extras
432             val messages =
433                 MessagingStyle.Message.getMessagesFromBundleArray(
434                     extras.getParcelableArray(Notification.EXTRA_MESSAGES)
435                 )
436             val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON)
437             for (i in messages.indices.reversed()) {
438                 val message = messages[i]
439                 val sender = message.senderPerson
440                 if (sender != null && sender !== user) {
441                     ic = message.senderPerson!!.icon
442                     break
443                 }
444             }
445         }
446 
447         // Fall back to notification large icon if available
448         if (ic == null) {
449             ic = entry.sbn.notification.getLargeIcon()
450         }
451 
452         // Revert to small icon if still not available
453         if (ic == null) {
454             ic = entry.sbn.notification.smallIcon
455         }
456         if (ic == null) {
457             throw InflationException("No icon in notification from " + entry.sbn.packageName)
458         }
459         return ic
460     }
461 
462     /**
463      * Determines if this icon shows a conversation based on the sensitivity of the icon, its
464      * context and the user's indicated sensitivity preference. If we're using a fall back icon of
465      * the small icon, we don't consider this to be showing a conversation
466      *
467      * @param iconView The icon that shows the conversation.
468      */
469     private fun showsConversation(
470         entry: NotificationEntry,
471         iconView: StatusBarIconView,
472         iconDescriptor: StatusBarIcon,
473     ): Boolean {
474         val usedInSensitiveContext =
475             iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
476         val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
477         return isImportantConversation(entry) &&
478             !isSmallIcon &&
479             (!usedInSensitiveContext || !entry.isSensitive.value)
480     }
481 
482     private fun isImportantConversation(entry: NotificationEntry): Boolean {
483         // Also verify that the Notification is MessagingStyle, since we're going to access
484         // MessagingStyle-specific data (EXTRA_MESSAGES, EXTRA_MESSAGING_PERSON).
485         return entry.ranking.channel != null &&
486             entry.ranking.channel.isImportantConversation &&
487             entry.sbn.notification.isStyle(MessagingStyle::class.java) &&
488             entry.key !in unimportantConversationKeys
489     }
490 
491     override fun setUnimportantConversations(keys: Collection<String>) {
492         val newKeys = keys.toSet()
493         val changed = unimportantConversationKeys != newKeys
494         unimportantConversationKeys = newKeys
495         if (changed) {
496             recalculateForImportantConversationChange()
497         }
498     }
499 }
500 
501 private const val TAG = "IconManager"
502 
503 interface ConversationIconManager {
504     /**
505      * Sets the complete current set of notification keys which should (for the purposes of icon
506      * presentation) be considered unimportant. This tells the icon manager to remove the avatar of
507      * a group from which the priority notification has been removed.
508      */
setUnimportantConversationsnull509     fun setUnimportantConversations(keys: Collection<String>)
510 }
511