| /* |
| * 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() |