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"