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"