• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.unsafeAssertInNewMode()
94         onIconUpdateRequiredListeners += listener
95     }
96 
97     fun removeIconsUpdateListener(listener: OnIconUpdateRequiredListener) {
98         StatusBarConnectedDisplays.unsafeAssertInNewMode()
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.unsafeAssertInNewMode()
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 (!StatusBarConnectedDisplays.isEnabled) {
166                 sbChipIcon = iconBuilder.createIconView(entry)
167                 sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
168             } else {
169                 sbChipIcon = null
170             }
171 
172             // Construct the shelf icon view.
173             val shelfIcon = iconBuilder.createIconView(entry)
174             shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
175             shelfIcon.visibility = View.INVISIBLE
176 
177             // Construct the aod icon view.
178             val aodIcon = iconBuilder.createIconView(entry)
179             aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
180             aodIcon.setIncreasedSize(true)
181 
182             // Set the icon views' icons
183             val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
184 
185             try {
186                 setIcon(entry, normalIconDescriptor, sbIcon)
187                 if (sbChipIcon != null) {
188                     setIcon(entry, normalIconDescriptor, sbChipIcon)
189                 }
190                 setIcon(entry, sensitiveIconDescriptor, shelfIcon)
191                 setIcon(entry, sensitiveIconDescriptor, aodIcon)
192                 entry.icons =
193                     IconPack.buildPack(sbIcon, sbChipIcon, shelfIcon, aodIcon, entry.icons)
194             } catch (e: InflationException) {
195                 entry.icons = IconPack.buildEmptyPack(entry.icons)
196                 throw e
197             }
198         }
199 
200     /** Update the [StatusBarIconView] for the given [NotificationEntry]. */
201     fun updateSbIcon(entry: NotificationEntry, iconView: StatusBarIconView) =
202         traceSection("IconManager.updateSbIcon") {
203             StatusBarConnectedDisplays.unsafeAssertInNewMode()
204 
205             val (normalIconDescriptor, _) = getIconDescriptors(entry)
206             val notificationContentDescription =
207                 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) }
208             iconView.setNotification(entry.sbn, notificationContentDescription)
209             setIcon(entry, normalIconDescriptor, iconView)
210         }
211 
212     /**
213      * Update the notification icons.
214      *
215      * @param entry the notification to read the icon from.
216      * @throws InflationException Exception if required icons are not valid or specified
217      */
218     @Throws(InflationException::class)
219     fun updateIcons(entry: NotificationEntry, usingCache: Boolean = false) =
220         traceSection("IconManager.updateIcons") {
221             if (!entry.icons.areIconsAvailable) {
222                 return@traceSection
223             }
224 
225             if (usingCache && !Flags.notificationsBackgroundIcons()) {
226                 Log.wtf(
227                     TAG,
228                     "Updating using the cache is not supported when the " +
229                         "notifications_background_icons flag is off",
230                 )
231             }
232             if (!usingCache || !Flags.notificationsBackgroundIcons()) {
233                 entry.icons.smallIconDescriptor = null
234                 entry.icons.peopleAvatarDescriptor = null
235             }
236 
237             if (StatusBarConnectedDisplays.isEnabled) {
238                 onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) }
239             }
240 
241             val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
242             val notificationContentDescription =
243                 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) }
244 
245             entry.icons.statusBarIcon?.let {
246                 it.setNotification(entry.sbn, notificationContentDescription)
247                 setIcon(entry, normalIconDescriptor, it)
248             }
249 
250             entry.icons.statusBarChipIcon?.let {
251                 it.setNotification(entry.sbn, notificationContentDescription)
252                 setIcon(entry, normalIconDescriptor, it)
253             }
254 
255             entry.icons.shelfIcon?.let {
256                 it.setNotification(entry.sbn, notificationContentDescription)
257                 setIcon(entry, sensitiveIconDescriptor, it)
258             }
259 
260             entry.icons.aodIcon?.let {
261                 it.setNotification(entry.sbn, notificationContentDescription)
262                 setIcon(entry, sensitiveIconDescriptor, it)
263             }
264         }
265 
266     private fun updateIconsSafe(entry: NotificationEntry) {
267         try {
268             updateIcons(entry)
269         } catch (e: InflationException) {
270             // TODO This should mark the entire row as involved in an inflation error
271             Log.e(TAG, "Unable to update icon", e)
272         }
273     }
274 
275     @Throws(InflationException::class)
276     private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> {
277         val iconDescriptor = getIconDescriptor(entry, redact = false)
278         val sensitiveDescriptor =
279             if (entry.isSensitive.value) {
280                 getIconDescriptor(entry, redact = true)
281             } else {
282                 iconDescriptor
283             }
284         return Pair(iconDescriptor, sensitiveDescriptor)
285     }
286 
287     @Throws(InflationException::class)
288     private fun getIconDescriptor(entry: NotificationEntry, redact: Boolean): StatusBarIcon {
289         val showPeopleAvatar = !redact && isImportantConversation(entry)
290 
291         // If the descriptor is already cached, return it
292         getCachedIconDescriptor(entry, showPeopleAvatar)?.also {
293             return it
294         }
295 
296         val n = entry.sbn.notification
297         val (icon: Icon?, type: StatusBarIcon.Type) =
298             if (showPeopleAvatar) {
299                 createPeopleAvatar(entry) to StatusBarIcon.Type.PeopleAvatar
300             } else {
301                 n.smallIcon to StatusBarIcon.Type.NotifSmallIcon
302             }
303         if (icon == null) {
304             throw InflationException("No icon in notification from ${entry.sbn.packageName}")
305         }
306 
307         val sbi = icon.toStatusBarIcon(entry, type)
308         cacheIconDescriptor(entry, sbi)
309         return sbi
310     }
311 
312     private fun getCachedIconDescriptor(
313         entry: NotificationEntry,
314         showPeopleAvatar: Boolean,
315     ): StatusBarIcon? {
316         val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor
317         val smallIconDescriptor = entry.icons.smallIconDescriptor
318 
319         // If cached, return corresponding cached values
320         return when {
321             showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor
322             smallIconDescriptor != null -> smallIconDescriptor
323             else -> null
324         }
325     }
326 
327     private fun cacheIconDescriptor(entry: NotificationEntry, descriptor: StatusBarIcon) {
328         if (android.app.Flags.notificationsRedesignAppIcons()) {
329             // Although we're not actually using the app icon in the status bar, let's make sure
330             // we cache the icon all the time when the flag is on.
331             when (descriptor.type) {
332                 StatusBarIcon.Type.PeopleAvatar -> entry.icons.peopleAvatarDescriptor = descriptor
333                 // When notificationsUseAppIcon is enabled, the app icon overrides the small icon.
334                 // But either way, it's a good idea to cache the descriptor.
335                 else -> entry.icons.smallIconDescriptor = descriptor
336             }
337         } else if (isImportantConversation(entry)) {
338             // Old approach: cache only if important conversation.
339             if (descriptor.type == StatusBarIcon.Type.PeopleAvatar) {
340                 entry.icons.peopleAvatarDescriptor = descriptor
341             } else {
342                 entry.icons.smallIconDescriptor = descriptor
343             }
344         }
345     }
346 
347     @Throws(InflationException::class)
348     private fun setIcon(
349         entry: NotificationEntry,
350         iconDescriptor: StatusBarIcon,
351         iconView: StatusBarIconView,
352     ) {
353         iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor))
354         iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
355         if (!iconView.set(iconDescriptor)) {
356             throw InflationException("Couldn't create icon $iconDescriptor")
357         }
358     }
359 
360     private fun Icon.toStatusBarIcon(
361         entry: NotificationEntry,
362         type: StatusBarIcon.Type,
363     ): StatusBarIcon {
364         val n = entry.sbn.notification
365         return StatusBarIcon(
366             entry.sbn.user,
367             entry.sbn.packageName,
368             /* icon = */ this,
369             n.iconLevel,
370             n.number,
371             iconBuilder.getIconContentDescription(n),
372             type,
373         )
374     }
375 
376     private suspend fun getLauncherShortcutIconForPeopleAvatar(entry: NotificationEntry) =
377         withContext(bgCoroutineContext) {
378             var icon: Icon? = null
379             val shortcut = entry.ranking.conversationShortcutInfo
380             if (shortcut != null) {
381                 try {
382                     icon = launcherApps.getShortcutIcon(shortcut)
383                 } catch (e: Exception) {
384                     Log.e(
385                         TAG,
386                         "Error calling LauncherApps#getShortcutIcon for notification $entry: $e",
387                     )
388                 }
389             }
390 
391             // Once we have the icon, updating it should happen on the main thread.
392             if (icon != null) {
393                 withContext(mainCoroutineContext) {
394                     val iconDescriptor =
395                         icon.toStatusBarIcon(entry, StatusBarIcon.Type.PeopleAvatar)
396 
397                     // Cache the value
398                     entry.icons.peopleAvatarDescriptor = iconDescriptor
399 
400                     // Update the icons using the cached value
401                     updateIcons(entry = entry, usingCache = true)
402                 }
403             }
404         }
405 
406     @Throws(InflationException::class)
407     private fun createPeopleAvatar(entry: NotificationEntry): Icon {
408         var ic: Icon? = null
409 
410         if (Flags.notificationsBackgroundIcons()) {
411             // Ideally we want to get the icon from launcher, but this is a binder transaction that
412             // may take longer so let's kick it off on a background thread and use a placeholder in
413             // the meantime.
414             // Cancel the previous job if necessary.
415             launcherPeopleAvatarIconJobs[entry.key]?.cancel()
416             launcherPeopleAvatarIconJobs[entry.key] =
417                 applicationCoroutineScope
418                     .launch { getLauncherShortcutIconForPeopleAvatar(entry) }
419                     .apply { invokeOnCompletion { launcherPeopleAvatarIconJobs.remove(entry.key) } }
420         } else {
421             val shortcut = entry.ranking.conversationShortcutInfo
422             if (shortcut != null) {
423                 ic = launcherApps.getShortcutIcon(shortcut)
424             }
425         }
426 
427         // Try to extract from message
428         if (ic == null) {
429             val extras: Bundle = entry.sbn.notification.extras
430             val messages =
431                 MessagingStyle.Message.getMessagesFromBundleArray(
432                     extras.getParcelableArray(Notification.EXTRA_MESSAGES)
433                 )
434             val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON)
435             for (i in messages.indices.reversed()) {
436                 val message = messages[i]
437                 val sender = message.senderPerson
438                 if (sender != null && sender !== user) {
439                     ic = message.senderPerson!!.icon
440                     break
441                 }
442             }
443         }
444 
445         // Fall back to notification large icon if available
446         if (ic == null) {
447             ic = entry.sbn.notification.getLargeIcon()
448         }
449 
450         // Revert to small icon if still not available
451         if (ic == null) {
452             ic = entry.sbn.notification.smallIcon
453         }
454         if (ic == null) {
455             throw InflationException("No icon in notification from " + entry.sbn.packageName)
456         }
457         return ic
458     }
459 
460     /**
461      * Determines if this icon shows a conversation based on the sensitivity of the icon, its
462      * context and the user's indicated sensitivity preference. If we're using a fall back icon of
463      * the small icon, we don't consider this to be showing a conversation
464      *
465      * @param iconView The icon that shows the conversation.
466      */
467     private fun showsConversation(
468         entry: NotificationEntry,
469         iconView: StatusBarIconView,
470         iconDescriptor: StatusBarIcon,
471     ): Boolean {
472         val usedInSensitiveContext =
473             iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
474         val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
475         return isImportantConversation(entry) &&
476             !isSmallIcon &&
477             (!usedInSensitiveContext || !entry.isSensitive.value)
478     }
479 
480     private fun isImportantConversation(entry: NotificationEntry): Boolean {
481         // Also verify that the Notification is MessagingStyle, since we're going to access
482         // MessagingStyle-specific data (EXTRA_MESSAGES, EXTRA_MESSAGING_PERSON).
483         return entry.ranking.channel != null &&
484             entry.ranking.channel.isImportantConversation &&
485             entry.sbn.notification.isStyle(MessagingStyle::class.java) &&
486             entry.key !in unimportantConversationKeys
487     }
488 
489     override fun setUnimportantConversations(keys: Collection<String>) {
490         val newKeys = keys.toSet()
491         val changed = unimportantConversationKeys != newKeys
492         unimportantConversationKeys = newKeys
493         if (changed) {
494             recalculateForImportantConversationChange()
495         }
496     }
497 }
498 
499 private const val TAG = "IconManager"
500 
501 interface ConversationIconManager {
502     /**
503      * Sets the complete current set of notification keys which should (for the purposes of icon
504      * presentation) be considered unimportant. This tells the icon manager to remove the avatar of
505      * a group from which the priority notification has been removed.
506      */
setUnimportantConversationsnull507     fun setUnimportantConversations(keys: Collection<String>)
508 }
509