blob: 08be4f8724153771db41e02f7f645b7524497705 [file] [log] [blame]
/*
* 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)
}
}
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))
iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP)
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 const val TAG = "IconManager"