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.Notification.MessagingStyle 21 import android.app.Person 22 import android.content.Context 23 import android.content.pm.LauncherApps 24 import android.graphics.drawable.Icon 25 import android.os.Build 26 import android.os.Bundle 27 import android.util.Log 28 import android.view.View 29 import android.widget.ImageView 30 import com.android.app.tracing.coroutines.launchTraced as launch 31 import com.android.app.tracing.traceSection 32 import com.android.internal.statusbar.StatusBarIcon 33 import com.android.systemui.Flags 34 import com.android.systemui.dagger.SysUISingleton 35 import com.android.systemui.dagger.qualifiers.Application 36 import com.android.systemui.dagger.qualifiers.Background 37 import com.android.systemui.dagger.qualifiers.Main 38 import com.android.systemui.res.R 39 import com.android.systemui.statusbar.StatusBarIconView 40 import com.android.systemui.statusbar.core.StatusBarConnectedDisplays 41 import com.android.systemui.statusbar.notification.InflationException 42 import com.android.systemui.statusbar.notification.collection.NotificationEntry 43 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection 44 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 45 import java.util.concurrent.ConcurrentHashMap 46 import javax.inject.Inject 47 import kotlin.coroutines.CoroutineContext 48 import kotlinx.coroutines.CoroutineScope 49 import kotlinx.coroutines.Job 50 import kotlinx.coroutines.withContext 51 52 /** 53 * Inflates and updates icons associated with notifications 54 * 55 * Notifications are represented by icons in a few different places -- in the status bar, in the 56 * notification shelf, in AOD, etc. This class is in charge of inflating the views that hold these 57 * icons and keeping the icon assets themselves up to date as notifications change. 58 * 59 * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. 60 * Long-term, it should probably live somewhere in the content inflation pipeline. 61 */ 62 @SysUISingleton 63 class IconManager 64 @Inject 65 constructor( 66 private val notifCollection: CommonNotifCollection, 67 private val launcherApps: LauncherApps, 68 private val iconBuilder: IconBuilder, 69 @Application private val applicationCoroutineScope: CoroutineScope, 70 @Background private val bgCoroutineContext: CoroutineContext, 71 @Main private val mainCoroutineContext: CoroutineContext, 72 ) : ConversationIconManager { 73 74 /** 75 * A listener that is notified when a [NotificationEntry] has been updated and the associated 76 * icons have to be updated as well. 77 */ 78 fun interface OnIconUpdateRequiredListener { 79 fun onIconUpdateRequired(entry: NotificationEntry) 80 } 81 82 private val onIconUpdateRequiredListeners = mutableSetOf<OnIconUpdateRequiredListener>() 83 84 private var unimportantConversationKeys: Set<String> = emptySet() 85 /** 86 * A map of running jobs for fetching the person avatar from launcher. The key is the 87 * notification entry key. 88 */ 89 private var launcherPeopleAvatarIconJobs: ConcurrentHashMap<String, Job> = 90 ConcurrentHashMap<String, Job>() 91 92 fun addIconsUpdateListener(listener: OnIconUpdateRequiredListener) { 93 StatusBarConnectedDisplays.unsafeAssertInNewMode() 94 onIconUpdateRequiredListeners += listener 95 } 96 97 fun removeIconsUpdateListener(listener: OnIconUpdateRequiredListener) { 98 StatusBarConnectedDisplays.unsafeAssertInNewMode() 99 onIconUpdateRequiredListeners -= listener 100 } 101 102 fun attach() { 103 notifCollection.addCollectionListener(entryListener) 104 } 105 106 private val entryListener = 107 object : NotifCollectionListener { 108 override fun onEntryInit(entry: NotificationEntry) { 109 entry.addOnSensitivityChangedListener(sensitivityListener) 110 } 111 112 override fun onEntryCleanUp(entry: NotificationEntry) { 113 entry.removeOnSensitivityChangedListener(sensitivityListener) 114 } 115 116 override fun onRankingApplied() { 117 // rankings affect whether a conversation is important, which can change the icons 118 recalculateForImportantConversationChange() 119 } 120 } 121 122 private val sensitivityListener = 123 NotificationEntry.OnSensitivityChangedListener { entry -> updateIconsSafe(entry) } 124 125 private fun recalculateForImportantConversationChange() { 126 for (entry in notifCollection.allNotifs) { 127 val isImportant = isImportantConversation(entry) 128 if ( 129 entry.icons.areIconsAvailable && isImportant != entry.icons.isImportantConversation 130 ) { 131 updateIconsSafe(entry) 132 } 133 entry.icons.isImportantConversation = isImportant 134 } 135 } 136 137 /** 138 * Inflate the [StatusBarIconView] for the given [NotificationEntry], using the specified 139 * [Context]. 140 */ 141 fun createSbIconView(context: Context, entry: NotificationEntry): StatusBarIconView = 142 traceSection("IconManager.createSbIconView") { 143 StatusBarConnectedDisplays.unsafeAssertInNewMode() 144 145 val sbIcon = iconBuilder.createIconView(entry, context) 146 sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 147 val (normalIconDescriptor, _) = getIconDescriptors(entry) 148 setIcon(entry, normalIconDescriptor, sbIcon) 149 return sbIcon 150 } 151 152 /** 153 * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the 154 * result in [NotificationEntry.getIcons]. 155 * 156 * @throws InflationException Exception if required icons are not valid or specified 157 */ 158 @Throws(InflationException::class) 159 fun createIcons(entry: NotificationEntry) = 160 traceSection("IconManager.createIcons") { 161 // Construct the status bar icon view. 162 val sbIcon = iconBuilder.createIconView(entry) 163 sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 164 val sbChipIcon: StatusBarIconView? 165 if (!StatusBarConnectedDisplays.isEnabled) { 166 sbChipIcon = iconBuilder.createIconView(entry) 167 sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 168 } else { 169 sbChipIcon = null 170 } 171 172 // Construct the shelf icon view. 173 val shelfIcon = iconBuilder.createIconView(entry) 174 shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 175 shelfIcon.visibility = View.INVISIBLE 176 177 // Construct the aod icon view. 178 val aodIcon = iconBuilder.createIconView(entry) 179 aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 180 aodIcon.setIncreasedSize(true) 181 182 // Set the icon views' icons 183 val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) 184 185 try { 186 setIcon(entry, normalIconDescriptor, sbIcon) 187 if (sbChipIcon != null) { 188 setIcon(entry, normalIconDescriptor, sbChipIcon) 189 } 190 setIcon(entry, sensitiveIconDescriptor, shelfIcon) 191 setIcon(entry, sensitiveIconDescriptor, aodIcon) 192 entry.icons = 193 IconPack.buildPack(sbIcon, sbChipIcon, shelfIcon, aodIcon, entry.icons) 194 } catch (e: InflationException) { 195 entry.icons = IconPack.buildEmptyPack(entry.icons) 196 throw e 197 } 198 } 199 200 /** Update the [StatusBarIconView] for the given [NotificationEntry]. */ 201 fun updateSbIcon(entry: NotificationEntry, iconView: StatusBarIconView) = 202 traceSection("IconManager.updateSbIcon") { 203 StatusBarConnectedDisplays.unsafeAssertInNewMode() 204 205 val (normalIconDescriptor, _) = getIconDescriptors(entry) 206 val notificationContentDescription = 207 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) } 208 iconView.setNotification(entry.sbn, notificationContentDescription) 209 setIcon(entry, normalIconDescriptor, iconView) 210 } 211 212 /** 213 * Update the notification icons. 214 * 215 * @param entry the notification to read the icon from. 216 * @throws InflationException Exception if required icons are not valid or specified 217 */ 218 @Throws(InflationException::class) 219 fun updateIcons(entry: NotificationEntry, usingCache: Boolean = false) = 220 traceSection("IconManager.updateIcons") { 221 if (!entry.icons.areIconsAvailable) { 222 return@traceSection 223 } 224 225 if (usingCache && !Flags.notificationsBackgroundIcons()) { 226 Log.wtf( 227 TAG, 228 "Updating using the cache is not supported when the " + 229 "notifications_background_icons flag is off", 230 ) 231 } 232 if (!usingCache || !Flags.notificationsBackgroundIcons()) { 233 entry.icons.smallIconDescriptor = null 234 entry.icons.peopleAvatarDescriptor = null 235 } 236 237 if (StatusBarConnectedDisplays.isEnabled) { 238 onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) } 239 } 240 241 val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) 242 val notificationContentDescription = 243 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) } 244 245 entry.icons.statusBarIcon?.let { 246 it.setNotification(entry.sbn, notificationContentDescription) 247 setIcon(entry, normalIconDescriptor, it) 248 } 249 250 entry.icons.statusBarChipIcon?.let { 251 it.setNotification(entry.sbn, notificationContentDescription) 252 setIcon(entry, normalIconDescriptor, it) 253 } 254 255 entry.icons.shelfIcon?.let { 256 it.setNotification(entry.sbn, notificationContentDescription) 257 setIcon(entry, sensitiveIconDescriptor, it) 258 } 259 260 entry.icons.aodIcon?.let { 261 it.setNotification(entry.sbn, notificationContentDescription) 262 setIcon(entry, sensitiveIconDescriptor, it) 263 } 264 } 265 266 private fun updateIconsSafe(entry: NotificationEntry) { 267 try { 268 updateIcons(entry) 269 } catch (e: InflationException) { 270 // TODO This should mark the entire row as involved in an inflation error 271 Log.e(TAG, "Unable to update icon", e) 272 } 273 } 274 275 @Throws(InflationException::class) 276 private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> { 277 val iconDescriptor = getIconDescriptor(entry, redact = false) 278 val sensitiveDescriptor = 279 if (entry.isSensitive.value) { 280 getIconDescriptor(entry, redact = true) 281 } else { 282 iconDescriptor 283 } 284 return Pair(iconDescriptor, sensitiveDescriptor) 285 } 286 287 @Throws(InflationException::class) 288 private fun getIconDescriptor(entry: NotificationEntry, redact: Boolean): StatusBarIcon { 289 val showPeopleAvatar = !redact && isImportantConversation(entry) 290 291 // If the descriptor is already cached, return it 292 getCachedIconDescriptor(entry, showPeopleAvatar)?.also { 293 return it 294 } 295 296 val n = entry.sbn.notification 297 val (icon: Icon?, type: StatusBarIcon.Type) = 298 if (showPeopleAvatar) { 299 createPeopleAvatar(entry) to StatusBarIcon.Type.PeopleAvatar 300 } else { 301 n.smallIcon to StatusBarIcon.Type.NotifSmallIcon 302 } 303 if (icon == null) { 304 throw InflationException("No icon in notification from ${entry.sbn.packageName}") 305 } 306 307 val sbi = icon.toStatusBarIcon(entry, type) 308 cacheIconDescriptor(entry, sbi) 309 return sbi 310 } 311 312 private fun getCachedIconDescriptor( 313 entry: NotificationEntry, 314 showPeopleAvatar: Boolean, 315 ): StatusBarIcon? { 316 val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor 317 val smallIconDescriptor = entry.icons.smallIconDescriptor 318 319 // If cached, return corresponding cached values 320 return when { 321 showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor 322 smallIconDescriptor != null -> smallIconDescriptor 323 else -> null 324 } 325 } 326 327 private fun cacheIconDescriptor(entry: NotificationEntry, descriptor: StatusBarIcon) { 328 if (android.app.Flags.notificationsRedesignAppIcons()) { 329 // Although we're not actually using the app icon in the status bar, let's make sure 330 // we cache the icon all the time when the flag is on. 331 when (descriptor.type) { 332 StatusBarIcon.Type.PeopleAvatar -> entry.icons.peopleAvatarDescriptor = descriptor 333 // When notificationsUseAppIcon is enabled, the app icon overrides the small icon. 334 // But either way, it's a good idea to cache the descriptor. 335 else -> entry.icons.smallIconDescriptor = descriptor 336 } 337 } else if (isImportantConversation(entry)) { 338 // Old approach: cache only if important conversation. 339 if (descriptor.type == StatusBarIcon.Type.PeopleAvatar) { 340 entry.icons.peopleAvatarDescriptor = descriptor 341 } else { 342 entry.icons.smallIconDescriptor = descriptor 343 } 344 } 345 } 346 347 @Throws(InflationException::class) 348 private fun setIcon( 349 entry: NotificationEntry, 350 iconDescriptor: StatusBarIcon, 351 iconView: StatusBarIconView, 352 ) { 353 iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor)) 354 iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP) 355 if (!iconView.set(iconDescriptor)) { 356 throw InflationException("Couldn't create icon $iconDescriptor") 357 } 358 } 359 360 private fun Icon.toStatusBarIcon( 361 entry: NotificationEntry, 362 type: StatusBarIcon.Type, 363 ): StatusBarIcon { 364 val n = entry.sbn.notification 365 return StatusBarIcon( 366 entry.sbn.user, 367 entry.sbn.packageName, 368 /* icon = */ this, 369 n.iconLevel, 370 n.number, 371 iconBuilder.getIconContentDescription(n), 372 type, 373 ) 374 } 375 376 private suspend fun getLauncherShortcutIconForPeopleAvatar(entry: NotificationEntry) = 377 withContext(bgCoroutineContext) { 378 var icon: Icon? = null 379 val shortcut = entry.ranking.conversationShortcutInfo 380 if (shortcut != null) { 381 try { 382 icon = launcherApps.getShortcutIcon(shortcut) 383 } catch (e: Exception) { 384 Log.e( 385 TAG, 386 "Error calling LauncherApps#getShortcutIcon for notification $entry: $e", 387 ) 388 } 389 } 390 391 // Once we have the icon, updating it should happen on the main thread. 392 if (icon != null) { 393 withContext(mainCoroutineContext) { 394 val iconDescriptor = 395 icon.toStatusBarIcon(entry, StatusBarIcon.Type.PeopleAvatar) 396 397 // Cache the value 398 entry.icons.peopleAvatarDescriptor = iconDescriptor 399 400 // Update the icons using the cached value 401 updateIcons(entry = entry, usingCache = true) 402 } 403 } 404 } 405 406 @Throws(InflationException::class) 407 private fun createPeopleAvatar(entry: NotificationEntry): Icon { 408 var ic: Icon? = null 409 410 if (Flags.notificationsBackgroundIcons()) { 411 // Ideally we want to get the icon from launcher, but this is a binder transaction that 412 // may take longer so let's kick it off on a background thread and use a placeholder in 413 // the meantime. 414 // Cancel the previous job if necessary. 415 launcherPeopleAvatarIconJobs[entry.key]?.cancel() 416 launcherPeopleAvatarIconJobs[entry.key] = 417 applicationCoroutineScope 418 .launch { getLauncherShortcutIconForPeopleAvatar(entry) } 419 .apply { invokeOnCompletion { launcherPeopleAvatarIconJobs.remove(entry.key) } } 420 } else { 421 val shortcut = entry.ranking.conversationShortcutInfo 422 if (shortcut != null) { 423 ic = launcherApps.getShortcutIcon(shortcut) 424 } 425 } 426 427 // Try to extract from message 428 if (ic == null) { 429 val extras: Bundle = entry.sbn.notification.extras 430 val messages = 431 MessagingStyle.Message.getMessagesFromBundleArray( 432 extras.getParcelableArray(Notification.EXTRA_MESSAGES) 433 ) 434 val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON) 435 for (i in messages.indices.reversed()) { 436 val message = messages[i] 437 val sender = message.senderPerson 438 if (sender != null && sender !== user) { 439 ic = message.senderPerson!!.icon 440 break 441 } 442 } 443 } 444 445 // Fall back to notification large icon if available 446 if (ic == null) { 447 ic = entry.sbn.notification.getLargeIcon() 448 } 449 450 // Revert to small icon if still not available 451 if (ic == null) { 452 ic = entry.sbn.notification.smallIcon 453 } 454 if (ic == null) { 455 throw InflationException("No icon in notification from " + entry.sbn.packageName) 456 } 457 return ic 458 } 459 460 /** 461 * Determines if this icon shows a conversation based on the sensitivity of the icon, its 462 * context and the user's indicated sensitivity preference. If we're using a fall back icon of 463 * the small icon, we don't consider this to be showing a conversation 464 * 465 * @param iconView The icon that shows the conversation. 466 */ 467 private fun showsConversation( 468 entry: NotificationEntry, 469 iconView: StatusBarIconView, 470 iconDescriptor: StatusBarIcon, 471 ): Boolean { 472 val usedInSensitiveContext = 473 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon 474 val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) 475 return isImportantConversation(entry) && 476 !isSmallIcon && 477 (!usedInSensitiveContext || !entry.isSensitive.value) 478 } 479 480 private fun isImportantConversation(entry: NotificationEntry): Boolean { 481 // Also verify that the Notification is MessagingStyle, since we're going to access 482 // MessagingStyle-specific data (EXTRA_MESSAGES, EXTRA_MESSAGING_PERSON). 483 return entry.ranking.channel != null && 484 entry.ranking.channel.isImportantConversation && 485 entry.sbn.notification.isStyle(MessagingStyle::class.java) && 486 entry.key !in unimportantConversationKeys 487 } 488 489 override fun setUnimportantConversations(keys: Collection<String>) { 490 val newKeys = keys.toSet() 491 val changed = unimportantConversationKeys != newKeys 492 unimportantConversationKeys = newKeys 493 if (changed) { 494 recalculateForImportantConversationChange() 495 } 496 } 497 } 498 499 private const val TAG = "IconManager" 500 501 interface ConversationIconManager { 502 /** 503 * Sets the complete current set of notification keys which should (for the purposes of icon 504 * presentation) be considered unimportant. This tells the icon manager to remove the avatar of 505 * a group from which the priority notification has been removed. 506 */ setUnimportantConversationsnull507 fun setUnimportantConversations(keys: Collection<String>) 508 } 509