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