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