• 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.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         shelfIcon.visibility = View.INVISIBLE
99 
100         // Construct the aod icon view.
101         val aodIcon = iconBuilder.createIconView(entry)
102         aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE
103         aodIcon.setIncreasedSize(true)
104 
105         // Construct the centered icon view.
106         val centeredIcon = if (entry.sbn.notification.isMediaNotification) {
107             iconBuilder.createIconView(entry).apply {
108                 scaleType = ImageView.ScaleType.CENTER_INSIDE
109             }
110         } else {
111             null
112         }
113 
114         // Set the icon views' icons
115         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
116 
117         try {
118             setIcon(entry, normalIconDescriptor, sbIcon)
119             setIcon(entry, sensitiveIconDescriptor, shelfIcon)
120             setIcon(entry, sensitiveIconDescriptor, aodIcon)
121             if (centeredIcon != null) {
122                 setIcon(entry, normalIconDescriptor, centeredIcon)
123             }
124             entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons)
125         } catch (e: InflationException) {
126             entry.icons = IconPack.buildEmptyPack(entry.icons)
127             throw e
128         }
129     }
130 
131     /**
132      * Update the notification icons.
133      *
134      * @param entry the notification to read the icon from.
135      * @throws InflationException Exception if required icons are not valid or specified
136      */
137     @Throws(InflationException::class)
138     fun updateIcons(entry: NotificationEntry) {
139         if (!entry.icons.areIconsAvailable) {
140             return
141         }
142         entry.icons.smallIconDescriptor = null
143         entry.icons.peopleAvatarDescriptor = null
144 
145         val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry)
146 
147         entry.icons.statusBarIcon?.let {
148             it.notification = entry.sbn
149             setIcon(entry, normalIconDescriptor, it)
150         }
151 
152         entry.icons.shelfIcon?.let {
153             it.notification = entry.sbn
154             setIcon(entry, normalIconDescriptor, it)
155         }
156 
157         entry.icons.aodIcon?.let {
158             it.notification = entry.sbn
159             setIcon(entry, sensitiveIconDescriptor, it)
160         }
161 
162         entry.icons.centeredIcon?.let {
163             it.notification = entry.sbn
164             setIcon(entry, sensitiveIconDescriptor, it)
165         }
166     }
167 
168     private fun updateIconsSafe(entry: NotificationEntry) {
169         try {
170             updateIcons(entry)
171         } catch (e: InflationException) {
172             // TODO This should mark the entire row as involved in an inflation error
173             Log.e(TAG, "Unable to update icon", e)
174         }
175     }
176 
177     @Throws(InflationException::class)
178     private fun getIconDescriptors(
179         entry: NotificationEntry
180     ): Pair<StatusBarIcon, StatusBarIcon> {
181         val iconDescriptor = getIconDescriptor(entry, false /* redact */)
182         val sensitiveDescriptor = if (entry.isSensitive) {
183             getIconDescriptor(entry, true /* redact */)
184         } else {
185             iconDescriptor
186         }
187         return Pair(iconDescriptor, sensitiveDescriptor)
188     }
189 
190     @Throws(InflationException::class)
191     private fun getIconDescriptor(
192         entry: NotificationEntry,
193         redact: Boolean
194     ): StatusBarIcon {
195         val n = entry.sbn.notification
196         val showPeopleAvatar = isImportantConversation(entry) && !redact
197 
198         val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor
199         val smallIconDescriptor = entry.icons.smallIconDescriptor
200 
201         // If cached, return corresponding cached values
202         if (showPeopleAvatar && peopleAvatarDescriptor != null) {
203             return peopleAvatarDescriptor
204         } else if (!showPeopleAvatar && smallIconDescriptor != null) {
205             return smallIconDescriptor
206         }
207 
208         val icon =
209                 (if (showPeopleAvatar) {
210                     createPeopleAvatar(entry)
211                 } else {
212                     n.smallIcon
213                 }) ?: throw InflationException(
214                         "No icon in notification from " + entry.sbn.packageName)
215 
216         val ic = StatusBarIcon(
217                 entry.sbn.user,
218                 entry.sbn.packageName,
219                 icon,
220                 n.iconLevel,
221                 n.number,
222                 iconBuilder.getIconContentDescription(n))
223 
224         // Cache if important conversation.
225         if (isImportantConversation(entry)) {
226             if (showPeopleAvatar) {
227                 entry.icons.peopleAvatarDescriptor = ic
228             } else {
229                 entry.icons.smallIconDescriptor = ic
230             }
231         }
232 
233         return ic
234     }
235 
236     @Throws(InflationException::class)
237     private fun setIcon(
238         entry: NotificationEntry,
239         iconDescriptor: StatusBarIcon,
240         iconView: StatusBarIconView
241     ) {
242         iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor))
243         iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
244         if (!iconView.set(iconDescriptor)) {
245             throw InflationException("Couldn't create icon $iconDescriptor")
246         }
247     }
248 
249     @Throws(InflationException::class)
250     private fun createPeopleAvatar(entry: NotificationEntry): Icon? {
251         var ic: Icon? = null
252 
253         val shortcut = entry.ranking.conversationShortcutInfo
254         if (shortcut != null) {
255             ic = launcherApps.getShortcutIcon(shortcut)
256         }
257 
258         // Fall back to extract from message
259         if (ic == null) {
260             val extras: Bundle = entry.sbn.notification.extras
261             val messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(
262                     extras.getParcelableArray(Notification.EXTRA_MESSAGES))
263             val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON)
264             for (i in messages.indices.reversed()) {
265                 val message = messages[i]
266                 val sender = message.senderPerson
267                 if (sender != null && sender !== user) {
268                     ic = message.senderPerson!!.icon
269                     break
270                 }
271             }
272         }
273 
274         // Fall back to notification large icon if available
275         if (ic == null) {
276             ic = entry.sbn.notification.getLargeIcon()
277         }
278 
279         // Revert to small icon if still not available
280         if (ic == null) {
281             ic = entry.sbn.notification.smallIcon
282         }
283         if (ic == null) {
284             throw InflationException("No icon in notification from " + entry.sbn.packageName)
285         }
286         return ic
287     }
288 
289     /**
290      * Determines if this icon shows a conversation based on the sensitivity of the icon, its
291      * context and the user's indicated sensitivity preference. If we're using a fall back icon
292      * of the small icon, we don't consider this to be showing a conversation
293      *
294      * @param iconView The icon that shows the conversation.
295      */
296     private fun showsConversation(
297         entry: NotificationEntry,
298         iconView: StatusBarIconView,
299         iconDescriptor: StatusBarIcon
300     ): Boolean {
301         val usedInSensitiveContext =
302                 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
303         val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
304         return isImportantConversation(entry) && !isSmallIcon &&
305                 (!usedInSensitiveContext || !entry.isSensitive)
306     }
307 
308     private fun isImportantConversation(entry: NotificationEntry): Boolean {
309         return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation
310     }
311 }
312 
313 private const val TAG = "IconManager"