blob: 6062941d44a6e95843832cd6fb5ab6c33835cc2a [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.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.UserInfo
import android.graphics.drawable.Drawable
import android.os.UserManager
import android.service.notification.NotificationListenerService
import android.service.notification.NotificationListenerService.REASON_SNOOZED
import android.service.notification.StatusBarNotification
import android.util.IconDrawableFactory
import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import com.android.internal.widget.MessagingGroup
import com.android.settingslib.notification.ConversationIconFactory
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.NotificationPersonExtractorPlugin
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationLockscreenUserManager
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 com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
import com.android.systemui.statusbar.policy.ExtensionController
import java.util.ArrayDeque
import java.util.concurrent.Executor
import javax.inject.Inject
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
}
@SysUISingleton
class NotificationPersonExtractorPluginBoundary @Inject constructor(
extensionController: ExtensionController
) : 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)?.run {
PersonModel(key, sbn.user.identifier, name, avatar, clickRunnable)
}
override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn)
override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
plugin?.isPersonNotification(sbn) ?: false
}
@SysUISingleton
class PeopleHubDataSourceImpl @Inject constructor(
private val notifCollection: CommonNotifCollection,
private val extractor: NotificationPersonExtractor,
private val userManager: UserManager,
launcherApps: LauncherApps,
packageManager: PackageManager,
context: Context,
private val notificationListener: NotificationListener,
@Background private val bgExecutor: Executor,
@Main private val mainExecutor: Executor,
private val notifLockscreenUserMgr: NotificationLockscreenUserManager,
private val peopleNotificationIdentifier: PeopleNotificationIdentifier
) : DataSource<PeopleHubModel> {
private var userChangeSubscription: Subscription? = null
private val dataListeners = mutableListOf<DataListener<PeopleHubModel>>()
private val peopleHubManagerForUser = SparseArray<PeopleHubManager>()
private val iconFactory = run {
val appContext = context.applicationContext
ConversationIconFactory(
appContext,
launcherApps,
packageManager,
IconDrawableFactory.newInstance(appContext),
appContext.resources.getDimensionPixelSize(
R.dimen.notification_guts_conversation_icon_size
)
)
}
private val notifCollectionListener = object : NotifCollectionListener {
override fun onEntryAdded(entry: NotificationEntry) = addVisibleEntry(entry)
override fun onEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry)
override fun onEntryRemoved(entry: NotificationEntry, reason: Int) =
removeVisibleEntry(entry, reason)
}
private fun removeVisibleEntry(entry: NotificationEntry, reason: Int) {
(extractor.extractPersonKey(entry.sbn) ?: entry.extractPersonKey())?.let { key ->
val userId = entry.sbn.user.identifier
bgExecutor.execute {
val parentId = userManager.getProfileParent(userId)?.id ?: userId
mainExecutor.execute {
if (reason == REASON_SNOOZED) {
if (peopleHubManagerForUser[parentId]?.migrateActivePerson(key) == true) {
updateUi()
}
} else {
peopleHubManagerForUser[parentId]?.removeActivePerson(key)
}
}
}
}
}
private fun addVisibleEntry(entry: NotificationEntry) {
entry.extractPerson()?.let { personModel ->
val userId = entry.sbn.user.identifier
bgExecutor.execute {
val parentId = userManager.getProfileParent(userId)?.id ?: userId
mainExecutor.execute {
val manager = peopleHubManagerForUser[parentId]
?: PeopleHubManager().also { peopleHubManagerForUser.put(parentId, it) }
if (manager.addActivePerson(personModel)) {
updateUi()
}
}
}
}
}
override fun registerListener(listener: DataListener<PeopleHubModel>): Subscription {
val register = dataListeners.isEmpty()
dataListeners.add(listener)
if (register) {
userChangeSubscription = notifLockscreenUserMgr.registerListener(
object : NotificationLockscreenUserManager.UserChangedListener {
override fun onUserChanged(userId: Int) = updateUi()
override fun onCurrentProfilesChanged(
currentProfiles: SparseArray<UserInfo>?
) = updateUi()
})
notifCollection.addCollectionListener(notifCollectionListener)
} else {
getPeopleHubModelForCurrentUser()?.let(listener::onDataChanged)
}
return object : Subscription {
override fun unsubscribe() {
dataListeners.remove(listener)
if (dataListeners.isEmpty()) {
userChangeSubscription?.unsubscribe()
userChangeSubscription = null
notifCollection.removeCollectionListener(notifCollectionListener)
}
}
}
}
private fun getPeopleHubModelForCurrentUser(): PeopleHubModel? {
val currentUserId = notifLockscreenUserMgr.currentUserId
val model = peopleHubManagerForUser[currentUserId]?.getPeopleHubModel()
?: return null
val currentProfiles = notifLockscreenUserMgr.currentProfiles
return model.copy(people = model.people.filter { person ->
currentProfiles[person.userId]?.isQuietModeEnabled == false
})
}
private fun updateUi() {
val model = getPeopleHubModelForCurrentUser() ?: return
for (listener in dataListeners) {
listener.onDataChanged(model)
}
}
private fun NotificationEntry.extractPerson(): PersonModel? {
val type = peopleNotificationIdentifier.getPeopleNotificationType(this)
if (type == TYPE_NON_PERSON) {
return null
}
val clickRunnable = Runnable { notificationListener.unsnoozeNotification(key) }
val extras = sbn.notification.extras
val name = ranking.conversationShortcutInfo?.label
?: extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)
?: extras.getCharSequence(Notification.EXTRA_TITLE)
?: return null
val drawable = ranking.getIcon(iconFactory, sbn)
?: iconFactory.getConversationDrawable(
extractAvatarFromRow(this),
sbn.packageName,
sbn.uid,
ranking.channel.isImportantConversation
)
return PersonModel(key, sbn.user.identifier, name, drawable, clickRunnable)
}
private fun NotificationListenerService.Ranking.getIcon(
iconFactory: ConversationIconFactory,
sbn: StatusBarNotification
): Drawable? =
conversationShortcutInfo?.let { conversationShortcutInfo ->
iconFactory.getConversationDrawable(
conversationShortcutInfo,
sbn.packageName,
sbn.uid,
channel.isImportantConversation
)
}
private fun NotificationEntry.extractPersonKey(): PersonKey? {
// TODO migrate to shortcut id when snoozing is conversation wide
val type = peopleNotificationIdentifier.getPeopleNotificationType(this)
return if (type != TYPE_NON_PERSON) key else null
}
}
private fun NotificationLockscreenUserManager.registerListener(
listener: NotificationLockscreenUserManager.UserChangedListener
): Subscription {
addUserChangedListener(listener)
return object : Subscription {
override fun unsubscribe() {
removeUserChangedListener(listener)
}
}
}
class PeopleHubManager {
// People currently visible in the notification shade, and so are not in the hub
private val activePeople = mutableMapOf<PersonKey, PersonModel>()
// People that were once "active" and have been dismissed, and so can be displayed in the hub
private val inactivePeople = ArrayDeque<PersonModel>(MAX_STORED_INACTIVE_PEOPLE)
fun migrateActivePerson(key: PersonKey): Boolean {
activePeople.remove(key)?.let { data ->
if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) {
inactivePeople.removeLast()
}
inactivePeople.addFirst(data)
return true
}
return false
}
fun removeActivePerson(key: PersonKey) {
activePeople.remove(key)
}
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 }
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()