| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.statusbar.notification.icon |
| |
| import android.app.Notification |
| import android.app.Person |
| import android.content.pm.LauncherApps |
| import android.graphics.drawable.Icon |
| import android.os.Build |
| import android.os.Bundle |
| import android.util.Log |
| import android.view.View |
| import android.widget.ImageView |
| import com.android.internal.statusbar.StatusBarIcon |
| import com.android.systemui.R |
| import com.android.systemui.statusbar.StatusBarIconView |
| import com.android.systemui.statusbar.notification.InflationException |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry |
| import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection |
| import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener |
| import javax.inject.Inject |
| |
| /** |
| * Inflates and updates icons associated with notifications |
| * |
| * Notifications are represented by icons in a few different places -- in the status bar, in the |
| * notification shelf, in AOD, etc. This class is in charge of inflating the views that hold these |
| * icons and keeping the icon assets themselves up to date as notifications change. |
| * |
| * TODO: Much of this code was copied whole-sale in order to get it out of NotificationEntry. |
| * Long-term, it should probably live somewhere in the content inflation pipeline. |
| */ |
| class IconManager @Inject constructor( |
| private val notifCollection: CommonNotifCollection, |
| private val launcherApps: LauncherApps, |
| private val iconBuilder: IconBuilder |
| ) { |
| fun attach() { |
| notifCollection.addCollectionListener(entryListener) |
| } |
| |
| private val entryListener = object : NotifCollectionListener { |
| override fun onEntryInit(entry: NotificationEntry) { |
| entry.addOnSensitivityChangedListener(sensitivityListener) |
| } |
| |
| override fun onEntryCleanUp(entry: NotificationEntry) { |
| entry.removeOnSensitivityChangedListener(sensitivityListener) |
| } |
| |
| override fun onRankingApplied() { |
| // When the sensitivity changes OR when the isImportantConversation status changes, |
| // we need to update the icons |
| for (entry in notifCollection.allNotifs) { |
| val isImportant = isImportantConversation(entry) |
| if (entry.icons.areIconsAvailable && |
| isImportant != entry.icons.isImportantConversation) { |
| updateIconsSafe(entry) |
| } |
| entry.icons.isImportantConversation = isImportant |
| } |
| } |
| } |
| |
| private val sensitivityListener = NotificationEntry.OnSensitivityChangedListener { |
| entry -> updateIconsSafe(entry) |
| } |
| |
| /** |
| * Inflate icon views for each icon variant and assign appropriate icons to them. Stores the |
| * result in [NotificationEntry.getIcons]. |
| * |
| * @throws InflationException Exception if required icons are not valid or specified |
| */ |
| @Throws(InflationException::class) |
| fun createIcons(entry: NotificationEntry) { |
| // Construct the status bar icon view. |
| val sbIcon = iconBuilder.createIconView(entry) |
| sbIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE |
| |
| // Construct the shelf icon view. |
| val shelfIcon = iconBuilder.createIconView(entry) |
| shelfIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE |
| |
| shelfIcon.visibility = View.INVISIBLE |
| // TODO: This doesn't belong here |
| shelfIcon.setOnVisibilityChangedListener { newVisibility: Int -> |
| if (entry.row != null) { |
| entry.row.setShelfIconVisible(newVisibility == View.VISIBLE) |
| } |
| } |
| |
| // Construct the aod icon view. |
| val aodIcon = iconBuilder.createIconView(entry) |
| aodIcon.scaleType = ImageView.ScaleType.CENTER_INSIDE |
| aodIcon.setIncreasedSize(true) |
| |
| // Construct the centered icon view. |
| val centeredIcon = if (entry.sbn.notification.isMediaNotification) { |
| iconBuilder.createIconView(entry).apply { |
| scaleType = ImageView.ScaleType.CENTER_INSIDE |
| } |
| } else { |
| null |
| } |
| |
| // Set the icon views' icons |
| val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) |
| |
| try { |
| setIcon(entry, normalIconDescriptor, sbIcon) |
| setIcon(entry, sensitiveIconDescriptor, shelfIcon) |
| setIcon(entry, sensitiveIconDescriptor, aodIcon) |
| if (centeredIcon != null) { |
| setIcon(entry, normalIconDescriptor, centeredIcon) |
| } |
| entry.icons = IconPack.buildPack(sbIcon, shelfIcon, aodIcon, centeredIcon, entry.icons) |
| } catch (e: InflationException) { |
| entry.icons = IconPack.buildEmptyPack(entry.icons) |
| throw e |
| } |
| } |
| |
| /** |
| * Update the notification icons. |
| * |
| * @param entry the notification to read the icon from. |
| * @throws InflationException Exception if required icons are not valid or specified |
| */ |
| @Throws(InflationException::class) |
| fun updateIcons(entry: NotificationEntry) { |
| if (!entry.icons.areIconsAvailable) { |
| return |
| } |
| entry.icons.smallIconDescriptor = null |
| entry.icons.peopleAvatarDescriptor = null |
| |
| val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) |
| |
| entry.icons.statusBarIcon?.let { |
| it.notification = entry.sbn |
| setIcon(entry, normalIconDescriptor, it) |
| } |
| |
| entry.icons.shelfIcon?.let { |
| it.notification = entry.sbn |
| setIcon(entry, normalIconDescriptor, it) |
| } |
| |
| entry.icons.aodIcon?.let { |
| it.notification = entry.sbn |
| setIcon(entry, sensitiveIconDescriptor, it) |
| } |
| |
| entry.icons.centeredIcon?.let { |
| it.notification = entry.sbn |
| setIcon(entry, sensitiveIconDescriptor, it) |
| } |
| } |
| |
| /** |
| * Updates tags on the icon views to match the posting app's target SDK level |
| * |
| * Note that this method MUST be called after both [createIcons] and [updateIcons]. |
| */ |
| fun updateIconTags(entry: NotificationEntry, targetSdk: Int) { |
| setTagOnIconViews( |
| entry.icons, |
| R.id.icon_is_pre_L, |
| targetSdk < Build.VERSION_CODES.LOLLIPOP) |
| } |
| |
| private fun updateIconsSafe(entry: NotificationEntry) { |
| try { |
| updateIcons(entry) |
| } catch (e: InflationException) { |
| // TODO This should mark the entire row as involved in an inflation error |
| Log.e(TAG, "Unable to update icon", e) |
| } |
| } |
| |
| @Throws(InflationException::class) |
| private fun getIconDescriptors( |
| entry: NotificationEntry |
| ): Pair<StatusBarIcon, StatusBarIcon> { |
| val iconDescriptor = getIconDescriptor(entry, false /* redact */) |
| val sensitiveDescriptor = if (entry.isSensitive) { |
| getIconDescriptor(entry, true /* redact */) |
| } else { |
| iconDescriptor |
| } |
| return Pair(iconDescriptor, sensitiveDescriptor) |
| } |
| |
| @Throws(InflationException::class) |
| private fun getIconDescriptor( |
| entry: NotificationEntry, |
| redact: Boolean |
| ): StatusBarIcon { |
| val n = entry.sbn.notification |
| val showPeopleAvatar = isImportantConversation(entry) && !redact |
| |
| val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor |
| val smallIconDescriptor = entry.icons.smallIconDescriptor |
| |
| // If cached, return corresponding cached values |
| if (showPeopleAvatar && peopleAvatarDescriptor != null) { |
| return peopleAvatarDescriptor |
| } else if (!showPeopleAvatar && smallIconDescriptor != null) { |
| return smallIconDescriptor |
| } |
| |
| val icon = |
| (if (showPeopleAvatar) { |
| createPeopleAvatar(entry) |
| } else { |
| n.smallIcon |
| }) ?: throw InflationException( |
| "No icon in notification from " + entry.sbn.packageName) |
| |
| val ic = StatusBarIcon( |
| entry.sbn.user, |
| entry.sbn.packageName, |
| icon, |
| n.iconLevel, |
| n.number, |
| iconBuilder.getIconContentDescription(n)) |
| |
| // Cache if important conversation. |
| if (isImportantConversation(entry)) { |
| if (showPeopleAvatar) { |
| entry.icons.peopleAvatarDescriptor = ic |
| } else { |
| entry.icons.smallIconDescriptor = ic |
| } |
| } |
| |
| return ic |
| } |
| |
| @Throws(InflationException::class) |
| private fun setIcon( |
| entry: NotificationEntry, |
| iconDescriptor: StatusBarIcon, |
| iconView: StatusBarIconView |
| ) { |
| iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor)) |
| if (!iconView.set(iconDescriptor)) { |
| throw InflationException("Couldn't create icon $iconDescriptor") |
| } |
| } |
| |
| @Throws(InflationException::class) |
| private fun createPeopleAvatar(entry: NotificationEntry): Icon? { |
| // Attempt to extract form shortcut. |
| val conversationId = entry.ranking.channel.conversationId |
| val query = LauncherApps.ShortcutQuery() |
| .setPackage(entry.sbn.packageName) |
| .setQueryFlags( |
| LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC |
| or LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED) |
| .setShortcutIds(listOf(conversationId)) |
| val shortcuts = launcherApps.getShortcuts(query, entry.sbn.user) |
| var ic: Icon? = null |
| if (shortcuts != null && shortcuts.isNotEmpty()) { |
| ic = shortcuts[0].icon |
| } |
| |
| // Fall back to notification large icon if available |
| if (ic == null) { |
| ic = entry.sbn.notification.getLargeIcon() |
| } |
| |
| // Fall back to extract from message |
| if (ic == null) { |
| val extras: Bundle = entry.sbn.notification.extras |
| val messages = Notification.MessagingStyle.Message.getMessagesFromBundleArray( |
| extras.getParcelableArray(Notification.EXTRA_MESSAGES)) |
| val user = extras.getParcelable<Person>(Notification.EXTRA_MESSAGING_PERSON) |
| for (i in messages.indices.reversed()) { |
| val message = messages[i] |
| val sender = message.senderPerson |
| if (sender != null && sender !== user) { |
| ic = message.senderPerson!!.icon |
| break |
| } |
| } |
| } |
| |
| // Revert to small icon if still not available |
| if (ic == null) { |
| ic = entry.sbn.notification.smallIcon |
| } |
| if (ic == null) { |
| throw InflationException("No icon in notification from " + entry.sbn.packageName) |
| } |
| return ic |
| } |
| |
| /** |
| * Determines if this icon shows a conversation based on the sensitivity of the icon, its |
| * context and the user's indicated sensitivity preference. If we're using a fall back icon |
| * of the small icon, we don't consider this to be showing a conversation |
| * |
| * @param iconView The icon that shows the conversation. |
| */ |
| private fun showsConversation( |
| entry: NotificationEntry, |
| iconView: StatusBarIconView, |
| iconDescriptor: StatusBarIcon |
| ): Boolean { |
| val usedInSensitiveContext = |
| iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon |
| val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon) |
| return isImportantConversation(entry) && !isSmallIcon |
| && (!usedInSensitiveContext || !entry.isSensitive) |
| } |
| |
| private fun isImportantConversation(entry: NotificationEntry): Boolean { |
| return entry.ranking.channel != null && entry.ranking.channel.isImportantConversation |
| } |
| |
| private fun setTagOnIconViews(icons: IconPack, key: Int, tag: Any) { |
| icons.statusBarIcon?.setTag(key, tag) |
| icons.shelfIcon?.setTag(key, tag) |
| icons.aodIcon?.setTag(key, tag) |
| icons.centeredIcon?.setTag(key, tag) |
| } |
| } |
| |
| private const val TAG = "IconManager" |