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.dagger.SysUISingleton 31 import com.android.systemui.statusbar.StatusBarIconView 32 import com.android.systemui.statusbar.notification.InflationException 33 import com.android.systemui.statusbar.notification.collection.NotificationEntry 34 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection 35 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 36 import com.android.systemui.util.traceSection 37 import javax.inject.Inject 38 39 /** 40 * Inflates and updates icons associated with notifications 41 * 42 * Notifications are represented by icons in a few different places -- in the status bar, in the 43 * notification shelf, in AOD, etc. This class is in charge of inflating the views that hold these 44 * icons and keeping the icon assets themselves up to date as notifications change. 45 * 46 * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. 47 * Long-term, it should probably live somewhere in the content inflation pipeline. 48 */ 49 @SysUISingleton 50 class IconManager @Inject constructor( 51 private val notifCollection: CommonNotifCollection, 52 private val launcherApps: LauncherApps, 53 private val iconBuilder: IconBuilder 54 ) : ConversationIconManager { 55 private var unimportantConversationKeys: Set<String> = emptySet() 56 57 fun attach() { 58 notifCollection.addCollectionListener(entryListener) 59 } 60 61 private val entryListener = object : NotifCollectionListener { 62 override fun onEntryInit(entry: NotificationEntry) { 63 entry.addOnSensitivityChangedListener(sensitivityListener) 64 } 65 66 override fun onEntryCleanUp(entry: NotificationEntry) { 67 entry.removeOnSensitivityChangedListener(sensitivityListener) 68 } 69 70 override fun onRankingApplied() { 71 // rankings affect whether a conversation is important, which can change the icons 72 recalculateForImportantConversationChange() 73 } 74 } 75 76 private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener { 77 entry -> updateIconsSafe(entry) 78 } 79 80 private fun recalculateForImportantConversationChange() { 81 for (entry in notifCollection.allNotifs) { 82 val isImportant = isImportantConversation(entry) 83 if (entry.icons.areIconsAvailable && 84 isImportant != entry.icons.isImportantConversation 85 ) { 86 updateIconsSafe(entry) 87 } 88 entry.icons.isImportantConversation = isImportant 89 } 90 } 91 92 /** 93 * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the 94 * result in [NotificationEntry.getIcons]. 95 * 96 * @throws InflationException Exception if required icons are not valid or specified 97 */ 98 @Throws(InflationException::class) 99 fun createIcons(entry: NotificationEntry) = traceSection("IconManager.createIcons") { 100 // Construct the status bar icon view. 101 val sbIcon = iconBuilder.createIconView(entry) 102 sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 103 104 // Construct the shelf icon view. 105 val shelfIcon = iconBuilder.createIconView(entry) 106 shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 107 shelfIcon.visibility = View.INVISIBLE 108 109 // Construct the aod icon view. 110 val aodIcon = iconBuilder.createIconView(entry) 111 aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 112 aodIcon.setIncreasedSize(true) 113 114 // Construct the centered icon view. 115 val centeredIcon = if (entry.sbn.notification.isMediaNotification) { 116 iconBuilder.createIconView(entry).apply { 117 scaleType = ImageView.ScaleType.CENTER_INSIDE 118 } 119 } else { 120 null 121 } 122 123 // Set the icon views' icons 124 val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) 125 126 try { 127 setIcon(entry, normalIconDescriptor, sbIcon) 128 setIcon(entry, sensitiveIconDescriptor, shelfIcon) 129 setIcon(entry, sensitiveIconDescriptor, aodIcon) 130 if (centeredIcon != null) { 131 setIcon(entry, normalIconDescriptor, centeredIcon) 132 } 133 entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons) 134 } catch (e: InflationException) { 135 entry.icons = IconPack.buildEmptyPack(entry.icons) 136 throw e 137 } 138 } 139 140 /** 141 * Update the notification icons. 142 * 143 * @param entry the notification to read the icon from. 144 * @throws InflationException Exception if required icons are not valid or specified 145 */ 146 @Throws(InflationException::class) 147 fun updateIcons(entry: NotificationEntry) = traceSection("IconManager.updateIcons") { 148 if (!entry.icons.areIconsAvailable) { 149 return@traceSection 150 } 151 entry.icons.smallIconDescriptor = null 152 entry.icons.peopleAvatarDescriptor = null 153 154 val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) 155 156 entry.icons.statusBarIcon?.let { 157 it.notification = entry.sbn 158 setIcon(entry, normalIconDescriptor, it) 159 } 160 161 entry.icons.shelfIcon?.let { 162 it.notification = entry.sbn 163 setIcon(entry, normalIconDescriptor, it) 164 } 165 166 entry.icons.aodIcon?.let { 167 it.notification = entry.sbn 168 setIcon(entry, sensitiveIconDescriptor, it) 169 } 170 171 entry.icons.centeredIcon?.let { 172 it.notification = entry.sbn 173 setIcon(entry, sensitiveIconDescriptor, it) 174 } 175 } 176 177 private fun updateIconsSafe(entry: NotificationEntry) { 178 try { 179 updateIcons(entry) 180 } catch (e: InflationException) { 181 // TODO This should mark the entire row as involved in an inflation error 182 Log.e(TAG, "Unable to update icon", e) 183 } 184 } 185 186 @Throws(InflationException::class) 187 private fun getIconDescriptors( 188 entry: NotificationEntry 189 ): Pair<StatusBarIcon, StatusBarIcon> { 190 val iconDescriptor = getIconDescriptor(entry, false /* redact */) 191 val sensitiveDescriptor = if (entry.isSensitive) { 192 getIconDescriptor(entry, true /* redact */) 193 } else { 194 iconDescriptor 195 } 196 return Pair(iconDescriptor, sensitiveDescriptor) 197 } 198 199 @Throws(InflationException::class) 200 private fun getIconDescriptor( 201 entry: NotificationEntry, 202 redact: Boolean 203 ): StatusBarIcon { 204 val n = entry.sbn.notification 205 val showPeopleAvatar = isImportantConversation(entry) && !redact 206 207 val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor 208 val smallIconDescriptor = entry.icons.smallIconDescriptor 209 210 // If cached, return corresponding cached values 211 if (showPeopleAvatar && peopleAvatarDescriptor != null) { 212 return peopleAvatarDescriptor 213 } else if (!showPeopleAvatar && smallIconDescriptor != null) { 214 return smallIconDescriptor 215 } 216 217 val icon = 218 (if (showPeopleAvatar) { 219 createPeopleAvatar(entry) 220 } else { 221 n.smallIcon 222 }) ?: throw InflationException( 223 "No icon in notification from " + entry.sbn.packageName) 224 225 val ic = StatusBarIcon( 226 entry.sbn.user, 227 entry.sbn.packageName, 228 icon, 229 n.iconLevel, 230 n.number, 231 iconBuilder.getIconContentDescription(n)) 232 233 // Cache if important conversation. 234 if (isImportantConversation(entry)) { 235 if (showPeopleAvatar) { 236 entry.icons.peopleAvatarDescriptor = ic 237 } else { 238 entry.icons.smallIconDescriptor = ic 239 } 240 } 241 242 return ic 243 } 244 245 @Throws(InflationException::class) 246 private fun setIcon( 247 entry: NotificationEntry, 248 iconDescriptor: StatusBarIcon, 249 iconView: StatusBarIconView 250 ) { 251 iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor)) 252 iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP) 253 if (!iconView.set(iconDescriptor)) { 254 throw InflationException("Couldn't create icon $iconDescriptor") 255 } 256 } 257 258 @Throws(InflationException::class) 259 private fun createPeopleAvatar(entry: NotificationEntry): Icon? { 260 var ic: Icon? = null 261 262 val shortcut = entry.ranking.conversationShortcutInfo 263 if (shortcut != null) { 264 ic = launcherApps.getShortcutIcon(shortcut) 265 } 266 267 // Fall back to extract from message 268 if (ic == null) { 269 val extras: Bundle = entry.sbn.notification.extras 270 val messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray( 271 extras.getParcelableArray(Notification.EXTRA_MESSAGES)) 272 val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON) 273 for (i in messages.indices.reversed()) { 274 val message = messages[i] 275 val sender = message.senderPerson 276 if (sender != null && sender !== user) { 277 ic = message.senderPerson!!.icon 278 break 279 } 280 } 281 } 282 283 // Fall back to notification large icon if available 284 if (ic == null) { 285 ic = entry.sbn.notification.getLargeIcon() 286 } 287 288 // Revert to small icon if still not available 289 if (ic == null) { 290 ic = entry.sbn.notification.smallIcon 291 } 292 if (ic == null) { 293 throw InflationException("No icon in notification from " + entry.sbn.packageName) 294 } 295 return ic 296 } 297 298 /** 299 * Determines if this icon shows a conversation based on the sensitivity of the icon, its 300 * context and the user's indicated sensitivity preference. If we're using a fall back icon 301 * of the small icon, we don't consider this to be showing a conversation 302 * 303 * @param iconView The icon that shows the conversation. 304 */ 305 private fun showsConversation( 306 entry: NotificationEntry, 307 iconView: StatusBarIconView, 308 iconDescriptor: StatusBarIcon 309 ): Boolean { 310 val usedInSensitiveContext = 311 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon 312 val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) 313 return isImportantConversation(entry) && !isSmallIcon && 314 (!usedInSensitiveContext || !entry.isSensitive) 315 } 316 317 private fun isImportantConversation(entry: NotificationEntry): Boolean { 318 return entry.ranking.channel != null && 319 entry.ranking.channel.isImportantConversation && 320 entry.key !in unimportantConversationKeys 321 } 322 323 override fun setUnimportantConversations(keys: Collection<String>) { 324 val newKeys = keys.toSet() 325 val changed = unimportantConversationKeys != newKeys 326 unimportantConversationKeys = newKeys 327 if (changed) { 328 recalculateForImportantConversationChange() 329 } 330 } 331 } 332 333 private const val TAG = "IconManager" 334 335 interface ConversationIconManager { 336 /** 337 * Sets the complete current set of notification keys which should (for the purposes of icon 338 * presentation) be considered unimportant. This tells the icon manager to remove the avatar 339 * of a group from which the priority notification has been removed. 340 */ 341 fun setUnimportantConversations(keys: Collection<String>) 342 }