blob: 784673e64ff8cdeb8c4401cfce4fd1322e160e9d [file] [log] [blame]
/*
* Copyright (C) 2019 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.people
import android.app.Notification
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.service.notification.StatusBarNotification
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.android.internal.statusbar.NotificationVisibility
import com.android.internal.widget.MessagingGroup
import com.android.launcher3.icons.BaseIconFactory
import com.android.systemui.R
import com.android.systemui.plugins.NotificationPersonExtractorPlugin
import com.android.systemui.statusbar.notification.NotificationEntryListener
import com.android.systemui.statusbar.notification.NotificationEntryManager
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.policy.ExtensionController
import java.util.ArrayDeque
import javax.inject.Inject
import javax.inject.Singleton
private const val MAX_STORED_INACTIVE_PEOPLE = 10
interface NotificationPersonExtractor {
fun extractPerson(sbn: StatusBarNotification): PersonModel?
fun extractPersonKey(sbn: StatusBarNotification): String?
fun isPersonNotification(sbn: StatusBarNotification): Boolean
}
@Singleton
class NotificationPersonExtractorPluginBoundary @Inject constructor(
extensionController: ExtensionController,
private val context: Context
) : NotificationPersonExtractor {
private var plugin: NotificationPersonExtractorPlugin? = null
init {
plugin = extensionController
.newExtension(NotificationPersonExtractorPlugin::class.java)
.withPlugin(NotificationPersonExtractorPlugin::class.java)
.withCallback { extractor ->
plugin = extractor
}
.build()
.get()
}
override fun extractPerson(sbn: StatusBarNotification) =
plugin?.extractPerson(sbn)?.let { data ->
val badged = addBadgeToDrawable(data.avatar, context, sbn.packageName, sbn.user)
PersonModel(data.key, data.name, badged, data.clickIntent)
}
override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn)
override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
plugin?.isPersonNotification(sbn) ?: false
}
@Singleton
class PeopleHubDataSourceImpl @Inject constructor(
private val notificationEntryManager: NotificationEntryManager,
private val peopleHubManager: PeopleHubManager,
private val extractor: NotificationPersonExtractor
) : DataSource<PeopleHubModel> {
private val dataListeners = mutableListOf<DataListener<PeopleHubModel>>()
private val notificationEntryListener = object : NotificationEntryListener {
override fun onEntryInflated(entry: NotificationEntry, inflatedFlags: Int) =
addVisibleEntry(entry)
override fun onEntryReinflated(entry: NotificationEntry) = addVisibleEntry(entry)
override fun onPostEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry)
override fun onEntryRemoved(
entry: NotificationEntry,
visibility: NotificationVisibility?,
removedByUser: Boolean
) = removeVisibleEntry(entry)
}
private fun removeVisibleEntry(entry: NotificationEntry) {
val key = extractor.extractPersonKey(entry.sbn) ?: entry.extractPersonKey()
if (key?.let(peopleHubManager::removeActivePerson) == true) {
updateUi()
}
}
private fun addVisibleEntry(entry: NotificationEntry) {
val personModel = extractor.extractPerson(entry.sbn) ?: entry.extractPerson()
if (personModel?.let(peopleHubManager::addActivePerson) == true) {
updateUi()
}
}
override fun registerListener(listener: DataListener<PeopleHubModel>): Subscription {
val registerWithNotificationEntryManager = dataListeners.isEmpty()
dataListeners.add(listener)
if (registerWithNotificationEntryManager) {
notificationEntryManager.addNotificationEntryListener(notificationEntryListener)
} else {
listener.onDataChanged(peopleHubManager.getPeopleHubModel())
}
return object : Subscription {
override fun unsubscribe() {
dataListeners.remove(listener)
if (dataListeners.isEmpty()) {
notificationEntryManager
.removeNotificationEntryListener(notificationEntryListener)
}
}
}
}
private fun updateUi() {
val model = peopleHubManager.getPeopleHubModel()
for (listener in dataListeners) {
listener.onDataChanged(model)
}
}
}
@Singleton
class PeopleHubManager @Inject constructor() {
private val activePeople = mutableMapOf<PersonKey, PersonModel>()
private val inactivePeople = ArrayDeque<PersonModel>(MAX_STORED_INACTIVE_PEOPLE)
fun removeActivePerson(key: PersonKey): Boolean {
activePeople.remove(key)?.let { data ->
if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) {
inactivePeople.removeLast()
}
inactivePeople.push(data)
return true
}
return false
}
fun addActivePerson(person: PersonModel): Boolean {
activePeople[person.key] = person
return inactivePeople.removeIf { it.key == person.key }
}
fun getPeopleHubModel(): PeopleHubModel = PeopleHubModel(inactivePeople)
}
private val ViewGroup.children
get(): Sequence<View> = sequence {
for (i in 0 until childCount) {
yield(getChildAt(i))
}
}
private fun ViewGroup.childrenWithId(id: Int): Sequence<View> = children.filter { it.id == id }
private fun NotificationEntry.extractPerson(): PersonModel? {
if (!isMessagingNotification()) {
return null
}
val clickIntent = sbn.notification.contentIntent ?: return null
val extras = sbn.notification.extras
val name = extras.getString(Notification.EXTRA_CONVERSATION_TITLE)
?: extras.getString(Notification.EXTRA_TITLE)
?: return null
val drawable = extractAvatarFromRow(this) ?: return null
val badgedAvatar = addBadgeToDrawable(drawable, row.context, sbn.packageName, sbn.user)
return PersonModel(key, name, badgedAvatar, clickIntent)
}
private fun addBadgeToDrawable(
drawable: Drawable,
context: Context,
packageName: String,
user: UserHandle
): Drawable {
val pm = context.packageManager
val appInfo = pm.getApplicationInfoAsUser(packageName, 0, user)
return object : Drawable() {
override fun draw(canvas: Canvas) {
val iconBounds = getBounds()
val factory = object : BaseIconFactory(
context,
0 /* unused */,
iconBounds.width(),
true) {}
val badge = factory.createBadgedIconBitmap(
appInfo.loadIcon(pm),
user,
true,
appInfo.isInstantApp,
null)
val badgeDrawable = BitmapDrawable(context.resources, badge.icon)
.apply {
alpha = drawable.alpha
colorFilter = drawable.colorFilter
val badgeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
15f,
context.resources.displayMetrics
).toInt()
setBounds(
iconBounds.left + (iconBounds.width() - badgeWidth),
iconBounds.top + (iconBounds.height() - badgeWidth),
iconBounds.right,
iconBounds.bottom)
}
drawable.bounds = iconBounds
drawable.draw(canvas)
badgeDrawable.draw(canvas)
}
override fun setAlpha(alpha: Int) {
drawable.alpha = alpha
}
override fun setColorFilter(colorFilter: ColorFilter?) {
drawable.colorFilter = colorFilter
}
@PixelFormat.Opacity
override fun getOpacity(): Int = PixelFormat.OPAQUE
}
}
fun extractAvatarFromRow(entry: NotificationEntry): Drawable? =
entry.row
?.childrenWithId(R.id.expanded)
?.mapNotNull { it as? ViewGroup }
?.flatMap {
it.childrenWithId(com.android.internal.R.id.status_bar_latest_event_content)
}
?.mapNotNull {
it.findViewById<ViewGroup>(com.android.internal.R.id.notification_messaging)
}
?.mapNotNull { messagesView ->
messagesView.children
.mapNotNull { it as? MessagingGroup }
.lastOrNull()
?.findViewById<ImageView>(com.android.internal.R.id.message_icon)
?.drawable
}
?.firstOrNull()
private fun NotificationEntry.extractPersonKey(): PersonKey? =
if (isMessagingNotification()) key else null
private fun NotificationEntry.isMessagingNotification() =
sbn.notification.notificationStyle == Notification.MessagingStyle::class.java