blob: 13f7a53f5e54df16d63ae164195a87f3b0e91e07 [file] [log] [blame]
Ned Burnsd8b51542020-03-13 20:52:43 -04001/*
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
17package com.android.systemui.statusbar.notification.icon
18
19import android.app.Notification
20import android.app.Person
21import android.content.pm.LauncherApps
22import android.graphics.drawable.Icon
23import android.os.Build
24import android.os.Bundle
25import android.util.Log
26import android.view.View
27import android.widget.ImageView
28import com.android.internal.statusbar.StatusBarIcon
29import com.android.systemui.R
30import com.android.systemui.statusbar.StatusBarIconView
31import com.android.systemui.statusbar.notification.InflationException
32import com.android.systemui.statusbar.notification.collection.NotificationEntry
33import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
34import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
35import 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 */
47class 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
Ned Burnsd8b51542020-03-13 20:52:43 -040099 // TODO: This doesn't belong here
100 shelfIcon.setOnVisibilityChangedListener { newVisibility: Int ->
Steve Elliott1bf998e2020-06-22 19:46:50 -0400101 entry.setShelfIconVisible(newVisibility == View.VISIBLE)
Ned Burnsd8b51542020-03-13 20:52:43 -0400102 }
Steve Elliott1bf998e2020-06-22 19:46:50 -0400103 shelfIcon.visibility = View.INVISIBLE
Ned Burnsd8b51542020-03-13 20:52:43 -0400104
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
Ned Burnsd8b51542020-03-13 20:52:43 -0400173 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 ) {
Selim Cinek9ed6e042020-03-26 15:45:51 -0700247 iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor))
Kevin Han37423222020-03-10 16:05:07 -0700248 iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
Ned Burnsd8b51542020-03-13 20:52:43 -0400249 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? {
Ned Burnsd8b51542020-03-13 20:52:43 -0400256 var ic: Icon? = null
Ned Burnsd8b51542020-03-13 20:52:43 -0400257
Alex Mange2baab72020-06-09 18:50:49 -0700258 val shortcut = entry.ranking.shortcutInfo
259 if (shortcut != null) {
260 ic = launcherApps.getShortcutIcon(shortcut)
Ned Burnsd8b51542020-03-13 20:52:43 -0400261 }
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
Alex Mange2baab72020-06-09 18:50:49 -0700279 // Fall back to notification large icon if available
280 if (ic == null) {
281 ic = entry.sbn.notification.getLargeIcon()
282 }
283
Ned Burnsd8b51542020-03-13 20:52:43 -0400284 // 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 /**
Selim Cinek9ed6e042020-03-26 15:45:51 -0700295 * 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
Ned Burnsd8b51542020-03-13 20:52:43 -0400298 *
Selim Cinek9ed6e042020-03-26 15:45:51 -0700299 * @param iconView The icon that shows the conversation.
Ned Burnsd8b51542020-03-13 20:52:43 -0400300 */
Selim Cinek9ed6e042020-03-26 15:45:51 -0700301 private fun showsConversation(
302 entry: NotificationEntry,
303 iconView: StatusBarIconView,
304 iconDescriptor: StatusBarIcon
305 ): Boolean {
Ned Burnsd8b51542020-03-13 20:52:43 -0400306 val usedInSensitiveContext =
307 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon
Selim Cinek9ed6e042020-03-26 15:45:51 -0700308 val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
Kevin Han37423222020-03-10 16:05:07 -0700309 return isImportantConversation(entry) && !isSmallIcon &&
310 (!usedInSensitiveContext || !entry.isSensitive)
Ned Burnsd8b51542020-03-13 20:52:43 -0400311 }
312
313 private fun isImportantConversation(entry: NotificationEntry): Boolean {
314 return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation
315 }
Ned Burnsd8b51542020-03-13 20:52:43 -0400316}
317
318private const val TAG = "IconManager"