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