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