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.assertInNewMode() 94 onIconUpdateRequiredListeners += listener 95 } 96 97 fun removeIconsUpdateListener(listener: OnIconUpdateRequiredListener) { 98 StatusBarConnectedDisplays.assertInNewMode() 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.assertInNewMode() 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 ( 166 Flags.statusBarCallChipNotificationIcon() && !StatusBarConnectedDisplays.isEnabled 167 ) { 168 sbChipIcon = iconBuilder.createIconView(entry) 169 sbChipIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 170 } else { 171 sbChipIcon = null 172 } 173 174 // Construct the shelf icon view. 175 val shelfIcon = iconBuilder.createIconView(entry) 176 shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 177 shelfIcon.visibility = View.INVISIBLE 178 179 // Construct the aod icon view. 180 val aodIcon = iconBuilder.createIconView(entry) 181 aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE 182 aodIcon.setIncreasedSize(true) 183 184 // Set the icon views' icons 185 val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) 186 187 try { 188 setIcon(entry, normalIconDescriptor, sbIcon) 189 if (Flags.statusBarCallChipNotificationIcon() && sbChipIcon != null) { 190 setIcon(entry, normalIconDescriptor, sbChipIcon) 191 } 192 setIcon(entry, sensitiveIconDescriptor, shelfIcon) 193 setIcon(entry, sensitiveIconDescriptor, aodIcon) 194 entry.icons = 195 IconPack.buildPack(sbIcon, sbChipIcon, shelfIcon, aodIcon, entry.icons) 196 } catch (e: InflationException) { 197 entry.icons = IconPack.buildEmptyPack(entry.icons) 198 throw e 199 } 200 } 201 202 /** Update the [StatusBarIconView] for the given [NotificationEntry]. */ 203 fun updateSbIcon(entry: NotificationEntry, iconView: StatusBarIconView) = 204 traceSection("IconManager.updateSbIcon") { 205 StatusBarConnectedDisplays.assertInNewMode() 206 207 val (normalIconDescriptor, _) = getIconDescriptors(entry) 208 val notificationContentDescription = 209 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) } 210 iconView.setNotification(entry.sbn, notificationContentDescription) 211 setIcon(entry, normalIconDescriptor, iconView) 212 } 213 214 /** 215 * Update the notification icons. 216 * 217 * @param entry the notification to read the icon from. 218 * @throws InflationException Exception if required icons are not valid or specified 219 */ 220 @Throws(InflationException::class) 221 fun updateIcons(entry: NotificationEntry, usingCache: Boolean = false) = 222 traceSection("IconManager.updateIcons") { 223 if (!entry.icons.areIconsAvailable) { 224 return@traceSection 225 } 226 227 if (StatusBarConnectedDisplays.isEnabled) { 228 onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) } 229 } 230 231 if (usingCache && !Flags.notificationsBackgroundIcons()) { 232 Log.wtf( 233 TAG, 234 "Updating using the cache is not supported when the " + 235 "notifications_background_icons flag is off", 236 ) 237 } 238 if (!usingCache || !Flags.notificationsBackgroundIcons()) { 239 entry.icons.smallIconDescriptor = null 240 entry.icons.peopleAvatarDescriptor = null 241 } 242 243 val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) 244 val notificationContentDescription = 245 entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) } 246 247 entry.icons.statusBarIcon?.let { 248 it.setNotification(entry.sbn, notificationContentDescription) 249 setIcon(entry, normalIconDescriptor, it) 250 } 251 252 entry.icons.statusBarChipIcon?.let { 253 it.setNotification(entry.sbn, notificationContentDescription) 254 setIcon(entry, normalIconDescriptor, it) 255 } 256 257 entry.icons.shelfIcon?.let { 258 it.setNotification(entry.sbn, notificationContentDescription) 259 setIcon(entry, sensitiveIconDescriptor, it) 260 } 261 262 entry.icons.aodIcon?.let { 263 it.setNotification(entry.sbn, notificationContentDescription) 264 setIcon(entry, sensitiveIconDescriptor, it) 265 } 266 } 267 268 private fun updateIconsSafe(entry: NotificationEntry) { 269 try { 270 updateIcons(entry) 271 } catch (e: InflationException) { 272 // TODO This should mark the entire row as involved in an inflation error 273 Log.e(TAG, "Unable to update icon", e) 274 } 275 } 276 277 @Throws(InflationException::class) 278 private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> { 279 val iconDescriptor = getIconDescriptor(entry, redact = false) 280 val sensitiveDescriptor = 281 if (entry.isSensitive.value) { 282 getIconDescriptor(entry, redact = true) 283 } else { 284 iconDescriptor 285 } 286 return Pair(iconDescriptor, sensitiveDescriptor) 287 } 288 289 @Throws(InflationException::class) 290 private fun getIconDescriptor(entry: NotificationEntry, redact: Boolean): StatusBarIcon { 291 val showPeopleAvatar = !redact && isImportantConversation(entry) 292 293 // If the descriptor is already cached, return it 294 getCachedIconDescriptor(entry, showPeopleAvatar)?.also { 295 return it 296 } 297 298 val n = entry.sbn.notification 299 val (icon: Icon?, type: StatusBarIcon.Type) = 300 if (showPeopleAvatar) { 301 createPeopleAvatar(entry) to StatusBarIcon.Type.PeopleAvatar 302 } else { 303 n.smallIcon to StatusBarIcon.Type.NotifSmallIcon 304 } 305 if (icon == null) { 306 throw InflationException("No icon in notification from ${entry.sbn.packageName}") 307 } 308 309 val sbi = icon.toStatusBarIcon(entry, type) 310 cacheIconDescriptor(entry, sbi) 311 return sbi 312 } 313 314 private fun getCachedIconDescriptor( 315 entry: NotificationEntry, 316 showPeopleAvatar: Boolean, 317 ): StatusBarIcon? { 318 val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor 319 val smallIconDescriptor = entry.icons.smallIconDescriptor 320 321 // If cached, return corresponding cached values 322 return when { 323 showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor 324 smallIconDescriptor != null -> smallIconDescriptor 325 else -> null 326 } 327 } 328 329 private fun cacheIconDescriptor(entry: NotificationEntry, descriptor: StatusBarIcon) { 330 if (android.app.Flags.notificationsRedesignAppIcons()) { 331 // Although we're not actually using the app icon in the status bar, let's make sure 332 // we cache the icon all the time when the flag is on. 333 when (descriptor.type) { 334 StatusBarIcon.Type.PeopleAvatar -> entry.icons.peopleAvatarDescriptor = descriptor 335 // When notificationsUseAppIcon is enabled, the app icon overrides the small icon. 336 // But either way, it's a good idea to cache the descriptor. 337 else -> entry.icons.smallIconDescriptor = descriptor 338 } 339 } else if (isImportantConversation(entry)) { 340 // Old approach: cache only if important conversation. 341 if (descriptor.type == StatusBarIcon.Type.PeopleAvatar) { 342 entry.icons.peopleAvatarDescriptor = descriptor 343 } else { 344 entry.icons.smallIconDescriptor = descriptor 345 } 346 } 347 } 348 349 @Throws(InflationException::class) 350 private fun setIcon( 351 entry: NotificationEntry, 352 iconDescriptor: StatusBarIcon, 353 iconView: StatusBarIconView, 354 ) { 355 iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor)) 356 iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP) 357 if (!iconView.set(iconDescriptor)) { 358 throw InflationException("Couldn't create icon $iconDescriptor") 359 } 360 } 361 362 private fun Icon.toStatusBarIcon( 363 entry: NotificationEntry, 364 type: StatusBarIcon.Type, 365 ): StatusBarIcon { 366 val n = entry.sbn.notification 367 return StatusBarIcon( 368 entry.sbn.user, 369 entry.sbn.packageName, 370 /* icon = */ this, 371 n.iconLevel, 372 n.number, 373 iconBuilder.getIconContentDescription(n), 374 type, 375 ) 376 } 377 378 private suspend fun getLauncherShortcutIconForPeopleAvatar(entry: NotificationEntry) = 379 withContext(bgCoroutineContext) { 380 var icon: Icon? = null 381 val shortcut = entry.ranking.conversationShortcutInfo 382 if (shortcut != null) { 383 try { 384 icon = launcherApps.getShortcutIcon(shortcut) 385 } catch (e: Exception) { 386 Log.e( 387 TAG, 388 "Error calling LauncherApps#getShortcutIcon for notification $entry: $e", 389 ) 390 } 391 } 392 393 // Once we have the icon, updating it should happen on the main thread. 394 if (icon != null) { 395 withContext(mainCoroutineContext) { 396 val iconDescriptor = 397 icon.toStatusBarIcon(entry, StatusBarIcon.Type.PeopleAvatar) 398 399 // Cache the value 400 entry.icons.peopleAvatarDescriptor = iconDescriptor 401 402 // Update the icons using the cached value 403 updateIcons(entry = entry, usingCache = true) 404 } 405 } 406 } 407 408 @Throws(InflationException::class) 409 private fun createPeopleAvatar(entry: NotificationEntry): Icon { 410 var ic: Icon? = null 411 412 if (Flags.notificationsBackgroundIcons()) { 413 // Ideally we want to get the icon from launcher, but this is a binder transaction that 414 // may take longer so let's kick it off on a background thread and use a placeholder in 415 // the meantime. 416 // Cancel the previous job if necessary. 417 launcherPeopleAvatarIconJobs[entry.key]?.cancel() 418 launcherPeopleAvatarIconJobs[entry.key] = 419 applicationCoroutineScope 420 .launch { getLauncherShortcutIconForPeopleAvatar(entry) } 421 .apply { invokeOnCompletion { launcherPeopleAvatarIconJobs.remove(entry.key) } } 422 } else { 423 val shortcut = entry.ranking.conversationShortcutInfo 424 if (shortcut != null) { 425 ic = launcherApps.getShortcutIcon(shortcut) 426 } 427 } 428 429 // Try to extract from message 430 if (ic == null) { 431 val extras: Bundle = entry.sbn.notification.extras 432 val messages = 433 MessagingStyle.Message.getMessagesFromBundleArray( 434 extras.getParcelableArray(Notification.EXTRA_MESSAGES) 435 ) 436 val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON) 437 for (i in messages.indices.reversed()) { 438 val message = messages[i] 439 val sender = message.senderPerson 440 if (sender != null && sender !== user) { 441 ic = message.senderPerson!!.icon 442 break 443 } 444 } 445 } 446 447 // Fall back to notification large icon if available 448 if (ic == null) { 449 ic = entry.sbn.notification.getLargeIcon() 450 } 451 452 // Revert to small icon if still not available 453 if (ic == null) { 454 ic = entry.sbn.notification.smallIcon 455 } 456 if (ic == null) { 457 throw InflationException("No icon in notification from " + entry.sbn.packageName) 458 } 459 return ic 460 } 461 462 /** 463 * Determines if this icon shows a conversation based on the sensitivity of the icon, its 464 * context and the user's indicated sensitivity preference. If we're using a fall back icon of 465 * the small icon, we don't consider this to be showing a conversation 466 * 467 * @param iconView The icon that shows the conversation. 468 */ 469 private fun showsConversation( 470 entry: NotificationEntry, 471 iconView: StatusBarIconView, 472 iconDescriptor: StatusBarIcon, 473 ): Boolean { 474 val usedInSensitiveContext = 475 iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon 476 val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) 477 return isImportantConversation(entry) && 478 !isSmallIcon && 479 (!usedInSensitiveContext || !entry.isSensitive.value) 480 } 481 482 private fun isImportantConversation(entry: NotificationEntry): Boolean { 483 // Also verify that the Notification is MessagingStyle, since we're going to access 484 // MessagingStyle-specific data (EXTRA_MESSAGES, EXTRA_MESSAGING_PERSON). 485 return entry.ranking.channel != null && 486 entry.ranking.channel.isImportantConversation && 487 entry.sbn.notification.isStyle(MessagingStyle::class.java) && 488 entry.key !in unimportantConversationKeys 489 } 490 491 override fun setUnimportantConversations(keys: Collection<String>) { 492 val newKeys = keys.toSet() 493 val changed = unimportantConversationKeys != newKeys 494 unimportantConversationKeys = newKeys 495 if (changed) { 496 recalculateForImportantConversationChange() 497 } 498 } 499 } 500 501 private const val TAG = "IconManager" 502 503 interface ConversationIconManager { 504 /** 505 * Sets the complete current set of notification keys which should (for the purposes of icon 506 * presentation) be considered unimportant. This tells the icon manager to remove the avatar of 507 * a group from which the priority notification has been removed. 508 */ setUnimportantConversationsnull509 fun setUnimportantConversations(keys: Collection<String>) 510 } 511