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