Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 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.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 | |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 99 | // TODO: This doesn't belong here |
| 100 | shelfIcon.setOnVisibilityChangedListener { newVisibility: Int -> |
Steve Elliott | 1bf998e | 2020-06-22 19:46:50 -0400 | [diff] [blame] | 101 | entry.setShelfIconVisible(newVisibility == View.VISIBLE) |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 102 | } |
Steve Elliott | 1bf998e | 2020-06-22 19:46:50 -0400 | [diff] [blame] | 103 | shelfIcon.visibility = View.INVISIBLE |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 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 | |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 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 | ) { |
Selim Cinek | 9ed6e04 | 2020-03-26 15:45:51 -0700 | [diff] [blame] | 247 | iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor)) |
Kevin Han | 3742322 | 2020-03-10 16:05:07 -0700 | [diff] [blame] | 248 | iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP) |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 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? { |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 256 | var ic: Icon? = null |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 257 | |
Alex Mang | e2baab7 | 2020-06-09 18:50:49 -0700 | [diff] [blame] | 258 | val shortcut = entry.ranking.shortcutInfo |
| 259 | if (shortcut != null) { |
| 260 | ic = launcherApps.getShortcutIcon(shortcut) |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 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 | |
Alex Mang | e2baab7 | 2020-06-09 18:50:49 -0700 | [diff] [blame] | 279 | // Fall back to notification large icon if available |
| 280 | if (ic == null) { |
| 281 | ic = entry.sbn.notification.getLargeIcon() |
| 282 | } |
| 283 | |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 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 | /** |
Selim Cinek | 9ed6e04 | 2020-03-26 15:45:51 -0700 | [diff] [blame] | 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 |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 298 | * |
Selim Cinek | 9ed6e04 | 2020-03-26 15:45:51 -0700 | [diff] [blame] | 299 | * @param iconView The icon that shows the conversation. |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 300 | */ |
Selim Cinek | 9ed6e04 | 2020-03-26 15:45:51 -0700 | [diff] [blame] | 301 | private fun showsConversation( |
| 302 | entry: NotificationEntry, |
| 303 | iconView: StatusBarIconView, |
| 304 | iconDescriptor: StatusBarIcon |
| 305 | ): Boolean { |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 306 | val usedInSensitiveContext = |
| 307 | iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon |
Selim Cinek | 9ed6e04 | 2020-03-26 15:45:51 -0700 | [diff] [blame] | 308 | val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) |
Kevin Han | 3742322 | 2020-03-10 16:05:07 -0700 | [diff] [blame] | 309 | return isImportantConversation(entry) && !isSmallIcon && |
| 310 | (!usedInSensitiveContext || !entry.isSensitive) |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 311 | } |
| 312 | |
| 313 | private fun isImportantConversation(entry: NotificationEntry): Boolean { |
| 314 | return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation |
| 315 | } |
Ned Burns | d8b5154 | 2020-03-13 20:52:43 -0400 | [diff] [blame] | 316 | } |
| 317 | |
| 318 | private const val TAG = "IconManager" |