• 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.Person
21 import android.content.pm.LauncherApps
22 import android.graphics.drawable.Icon
23 import android.os.Build
24 import android.os.Bundle
25 import android.util.Log
26 import android.view.View
27 import android.widget.ImageView
28 import com.android.internal.statusbar.StatusBarIcon
29 import com.android.systemui.R
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.statusbar.StatusBarIconView
32 import com.android.systemui.statusbar.notification.InflationException
33 import com.android.systemui.statusbar.notification.collection.NotificationEntry
34 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
35 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
36 import javax.inject.Inject
37 
38 /**
39  * Inflates and updates icons associated with notifications
40  *
41  * Notifications are represented by icons in a few different places -- in the status bar, in the
42  * notification shelf, in AOD, etc. This class is in charge of inflating the views that hold these
43  * icons and keeping the icon assets themselves up to date as notifications change.
44  *
45  * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry.
46  *  Long-term, it should probably live somewhere in the content inflation pipeline.
47  */
48 @SysUISingleton
49 class IconManager @Inject constructor(
50     private val notifCollection: CommonNotifCollection,
51     private val launcherApps: LauncherApps,
52     private val iconBuilder: IconBuilder
53 ) : ConversationIconManager {
54     private var unimportantConversationKeys: Set<String> = emptySet()
55 
56     fun attach() {
57         notifCollection.addCollectionListener(entryListener)
58     }
59 
60     private val entryListener = object : NotifCollectionListener {
61         override fun onEntryInit(entry: NotificationEntry) {
62             entry.addOnSensitivityChangedListener(sensitivityListener)
63         }
64 
65         override fun onEntryCleanUp(entry: NotificationEntry) {
66             entry.removeOnSensitivityChangedListener(sensitivityListener)
67         }
68 
69         override fun onRankingApplied() {
70             // rankings affect whether a conversation is important, which can change the icons
71             recalculateForImportantConversationChange()
72         }
73     }
74 
75     private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener {
76         entry -> updateIconsSafe(entry)
77     }
78 
79     private fun recalculateForImportantConversationChange() {
80         for (entry in notifCollection.allNotifs) {
81             val isImportant = isImportantConversation(entry)
82             if (entry.icons.areIconsAvailable &&
83                 isImportant != entry.icons.isImportantConversation
84             ) {
85                 updateIconsSafe(entry)
86             }
87             entry.icons.isImportantConversation = isImportant
88         }
89     }
90 
91     /**
92      * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the
93      * result in [NotificationEntry.getIcons].
94      *
95      * @throws InflationException Exception if required icons are not valid or specified
96      */
97     @Throws(InflationException::class)
98     fun createIcons(entry: NotificationEntry) {
99         // Construct the status bar icon view.
100         val sbIcon = iconBuilder.createIconView(entry)
101         sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
102 
103         // Construct the shelf icon view.
104         val shelfIcon = iconBuilder.createIconView(entry)
105         shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
106         shelfIcon.visibility = View.INVISIBLE
107 
108         // Construct the aod icon view.
109         val aodIcon = iconBuilder.createIconView(entry)
110         aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
111         aodIcon.setIncreasedSize(true)
112 
113         // Construct the centered icon view.
114         val centeredIcon = if (entry.sbn.notification.isMediaNotification) {
115             iconBuilder.createIconView(entry).apply {
116                 scaleType = ImageView.ScaleType.CENTER_INSIDE
117             }
118         } else {
119             null
120         }
121 
122         // Set the icon views' icons
123         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
124 
125         try {
126             setIcon(entry, normalIconDescriptor, sbIcon)
127             setIcon(entry, sensitiveIconDescriptor, shelfIcon)
128             setIcon(entry, sensitiveIconDescriptor, aodIcon)
129             if (centeredIcon != null) {
130                 setIcon(entry, normalIconDescriptor, centeredIcon)
131             }
132             entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons)
133         } catch (e: InflationException) {
134             entry.icons = IconPack.buildEmptyPack(entry.icons)
135             throw e
136         }
137     }
138 
139     /**
140      * Update the notification icons.
141      *
142      * @param entry the notification to read the icon from.
143      * @throws InflationException Exception if required icons are not valid or specified
144      */
145     @Throws(InflationException::class)
146     fun updateIcons(entry: NotificationEntry) {
147         if (!entry.icons.areIconsAvailable) {
148             return
149         }
150         entry.icons.smallIconDescriptor = null
151         entry.icons.peopleAvatarDescriptor = null
152 
153         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
154 
155         entry.icons.statusBarIcon?.let {
156             it.notification = entry.sbn
157             setIcon(entry, normalIconDescriptor, it)
158         }
159 
160         entry.icons.shelfIcon?.let {
161             it.notification = entry.sbn
162             setIcon(entry, normalIconDescriptor, it)
163         }
164 
165         entry.icons.aodIcon?.let {
166             it.notification = entry.sbn
167             setIcon(entry, sensitiveIconDescriptor, it)
168         }
169 
170         entry.icons.centeredIcon?.let {
171             it.notification = entry.sbn
172             setIcon(entry, sensitiveIconDescriptor, it)
173         }
174     }
175 
176     private fun updateIconsSafe(entry: NotificationEntry) {
177         try {
178             updateIcons(entry)
179         } catch (e: InflationException) {
180             // TODO This should mark the entire row as involved in an inflation error
181             Log.e(TAG, "Unable to update icon", e)
182         }
183     }
184 
185     @Throws(InflationException::class)
186     private fun getIconDescriptors(
187         entry: NotificationEntry
188     ): Pair<StatusBarIcon, StatusBarIcon> {
189         val iconDescriptor = getIconDescriptor(entry, false /* redact */)
190         val sensitiveDescriptor = if (entry.isSensitive) {
191             getIconDescriptor(entry, true /* redact */)
192         } else {
193             iconDescriptor
194         }
195         return Pair(iconDescriptor, sensitiveDescriptor)
196     }
197 
198     @Throws(InflationException::class)
199     private fun getIconDescriptor(
200         entry: NotificationEntry,
201         redact: Boolean
202     ): StatusBarIcon {
203         val n = entry.sbn.notification
204         val showPeopleAvatar = isImportantConversation(entry) && !redact
205 
206         val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor
207         val smallIconDescriptor = entry.icons.smallIconDescriptor
208 
209         // If cached, return corresponding cached values
210         if (showPeopleAvatar && peopleAvatarDescriptor != null) {
211             return peopleAvatarDescriptor
212         } else if (!showPeopleAvatar && smallIconDescriptor != null) {
213             return smallIconDescriptor
214         }
215 
216         val icon =
217                 (if (showPeopleAvatar) {
218                     createPeopleAvatar(entry)
219                 } else {
220                     n.smallIcon
221                 }) ?: throw InflationException(
222                         "No icon in notification from " + entry.sbn.packageName)
223 
224         val ic = StatusBarIcon(
225                 entry.sbn.user,
226                 entry.sbn.packageName,
227                 icon,
228                 n.iconLevel,
229                 n.number,
230                 iconBuilder.getIconContentDescription(n))
231 
232         // Cache if important conversation.
233         if (isImportantConversation(entry)) {
234             if (showPeopleAvatar) {
235                 entry.icons.peopleAvatarDescriptor = ic
236             } else {
237                 entry.icons.smallIconDescriptor = ic
238             }
239         }
240 
241         return ic
242     }
243 
244     @Throws(InflationException::class)
245     private fun setIcon(
246         entry: NotificationEntry,
247         iconDescriptor: StatusBarIcon,
248         iconView: StatusBarIconView
249     ) {
250         iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor))
251         iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
252         if (!iconView.set(iconDescriptor)) {
253             throw InflationException("Couldn't create icon $iconDescriptor")
254         }
255     }
256 
257     @Throws(InflationException::class)
258     private fun createPeopleAvatar(entry: NotificationEntry): Icon? {
259         var ic: Icon? = null
260 
261         val shortcut = entry.ranking.conversationShortcutInfo
262         if (shortcut != null) {
263             ic = launcherApps.getShortcutIcon(shortcut)
264         }
265 
266         // Fall back to extract from message
267         if (ic == null) {
268             val extras: Bundle = entry.sbn.notification.extras
269             val messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(
270                     extras.getParcelableArray(Notification.EXTRA_MESSAGES))
271             val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON)
272             for (i in messages.indices.reversed()) {
273                 val message = messages[i]
274                 val sender = message.senderPerson
275                 if (sender != null && sender !== user) {
276                     ic = message.senderPerson!!.icon
277                     break
278                 }
279             }
280         }
281 
282         // Fall back to notification large icon if available
283         if (ic == null) {
284             ic = entry.sbn.notification.getLargeIcon()
285         }
286 
287         // Revert to small icon if still not available
288         if (ic == null) {
289             ic = entry.sbn.notification.smallIcon
290         }
291         if (ic == null) {
292             throw InflationException("No icon in notification from " + entry.sbn.packageName)
293         }
294         return ic
295     }
296 
297     /**
298      * Determines if this icon shows a conversation based on the sensitivity of the icon, its
299      * context and the user's indicated sensitivity preference. If we're using a fall back icon
300      * of the small icon, we don't consider this to be showing a conversation
301      *
302      * @param iconView The icon that shows the conversation.
303      */
304     private fun showsConversation(
305         entry: NotificationEntry,
306         iconView: StatusBarIconView,
307         iconDescriptor: StatusBarIcon
308     ): Boolean {
309         val usedInSensitiveContext =
310                 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
311         val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
312         return isImportantConversation(entry) && !isSmallIcon &&
313                 (!usedInSensitiveContext || !entry.isSensitive)
314     }
315 
316     private fun isImportantConversation(entry: NotificationEntry): Boolean {
317         return entry.ranking.channel != null &&
318                 entry.ranking.channel.isImportantConversation &&
319                 entry.key !in unimportantConversationKeys
320     }
321 
322     override fun setUnimportantConversations(keys: Collection<String>) {
323         val newKeys = keys.toSet()
324         val changed = unimportantConversationKeys != newKeys
325         unimportantConversationKeys = newKeys
326         if (changed) {
327             recalculateForImportantConversationChange()
328         }
329     }
330 }
331 
332 private const val TAG = "IconManager"
333 
334 interface ConversationIconManager {
335     /**
336      * Sets the complete current set of notification keys which should (for the purposes of icon
337      * presentation) be considered unimportant.  This tells the icon manager to remove the avatar
338      * of a group from which the priority notification has been removed.
339      */
setUnimportantConversationsnull340     fun setUnimportantConversations(keys: Collection<String>)
341 }