1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.notification.people 18 19 import android.app.Notification 20 import android.content.Context 21 import android.content.pm.LauncherApps 22 import android.content.pm.PackageManager 23 import android.content.pm.UserInfo 24 import android.graphics.drawable.Drawable 25 import android.os.UserManager 26 import android.service.notification.NotificationListenerService 27 import android.service.notification.NotificationListenerService.REASON_SNOOZED 28 import android.service.notification.StatusBarNotification 29 import android.util.IconDrawableFactory 30 import android.util.SparseArray 31 import android.view.View 32 import android.view.ViewGroup 33 import android.widget.ImageView 34 import com.android.internal.statusbar.NotificationVisibility 35 import com.android.internal.widget.MessagingGroup 36 import com.android.settingslib.notification.ConversationIconFactory 37 import com.android.systemui.R 38 import com.android.systemui.dagger.SysUISingleton 39 import com.android.systemui.dagger.qualifiers.Background 40 import com.android.systemui.dagger.qualifiers.Main 41 import com.android.systemui.plugins.NotificationPersonExtractorPlugin 42 import com.android.systemui.statusbar.NotificationListener 43 import com.android.systemui.statusbar.NotificationLockscreenUserManager 44 import com.android.systemui.statusbar.notification.NotificationEntryListener 45 import com.android.systemui.statusbar.notification.NotificationEntryManager 46 import com.android.systemui.statusbar.notification.collection.NotificationEntry 47 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON 48 import com.android.systemui.statusbar.policy.ExtensionController 49 import java.util.ArrayDeque 50 import java.util.concurrent.Executor 51 import javax.inject.Inject 52 53 private const val MAX_STORED_INACTIVE_PEOPLE = 10 54 55 interface NotificationPersonExtractor { 56 fun extractPerson(sbn: StatusBarNotification): PersonModel? 57 fun extractPersonKey(sbn: StatusBarNotification): String? 58 fun isPersonNotification(sbn: StatusBarNotification): Boolean 59 } 60 61 @SysUISingleton 62 class NotificationPersonExtractorPluginBoundary @Inject constructor( 63 extensionController: ExtensionController 64 ) : NotificationPersonExtractor { 65 66 private var plugin: NotificationPersonExtractorPlugin? = null 67 68 init { 69 plugin = extensionController 70 .newExtension(NotificationPersonExtractorPlugin::class.java) 71 .withPlugin(NotificationPersonExtractorPlugin::class.java) 72 .withCallback { extractor -> 73 plugin = extractor 74 } 75 .build() 76 .get() 77 } 78 79 override fun extractPerson(sbn: StatusBarNotification) = 80 plugin?.extractPerson(sbn)?.run { 81 PersonModel(key, sbn.user.identifier, name, avatar, clickRunnable) 82 } 83 84 override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn) 85 86 override fun isPersonNotification(sbn: StatusBarNotification): Boolean = 87 plugin?.isPersonNotification(sbn) ?: false 88 } 89 90 @SysUISingleton 91 class PeopleHubDataSourceImpl @Inject constructor( 92 private val notificationEntryManager: NotificationEntryManager, 93 private val extractor: NotificationPersonExtractor, 94 private val userManager: UserManager, 95 launcherApps: LauncherApps, 96 packageManager: PackageManager, 97 context: Context, 98 private val notificationListener: NotificationListener, 99 @Background private val bgExecutor: Executor, 100 @Main private val mainExecutor: Executor, 101 private val notifLockscreenUserMgr: NotificationLockscreenUserManager, 102 private val peopleNotificationIdentifier: PeopleNotificationIdentifier 103 ) : DataSource<PeopleHubModel> { 104 105 private var userChangeSubscription: Subscription? = null 106 private val dataListeners = mutableListOf<DataListener<PeopleHubModel>>() 107 private val peopleHubManagerForUser = SparseArray<PeopleHubManager>() 108 109 private val iconFactory = run { 110 val appContext = context.applicationContext 111 ConversationIconFactory( 112 appContext, 113 launcherApps, 114 packageManager, 115 IconDrawableFactory.newInstance(appContext), 116 appContext.resources.getDimensionPixelSize( 117 R.dimen.notification_guts_conversation_icon_size 118 ) 119 ) 120 } 121 122 private val notificationEntryListener = object : NotificationEntryListener { 123 override fun onEntryInflated(entry: NotificationEntry) = addVisibleEntry(entry) 124 125 override fun onEntryReinflated(entry: NotificationEntry) = addVisibleEntry(entry) 126 127 override fun onPostEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry) 128 129 override fun onEntryRemoved( 130 entry: NotificationEntry, 131 visibility: NotificationVisibility?, 132 removedByUser: Boolean, 133 reason: Int 134 ) = removeVisibleEntry(entry, reason) 135 } 136 137 private fun removeVisibleEntry(entry: NotificationEntry, reason: Int) { 138 (extractor.extractPersonKey(entry.sbn) ?: entry.extractPersonKey())?.let { key -> 139 val userId = entry.sbn.user.identifier 140 bgExecutor.execute { 141 val parentId = userManager.getProfileParent(userId)?.id ?: userId 142 mainExecutor.execute { 143 if (reason == REASON_SNOOZED) { 144 if (peopleHubManagerForUser[parentId]?.migrateActivePerson(key) == true) { 145 updateUi() 146 } 147 } else { 148 peopleHubManagerForUser[parentId]?.removeActivePerson(key) 149 } 150 } 151 } 152 } 153 } 154 155 private fun addVisibleEntry(entry: NotificationEntry) { 156 entry.extractPerson()?.let { personModel -> 157 val userId = entry.sbn.user.identifier 158 bgExecutor.execute { 159 val parentId = userManager.getProfileParent(userId)?.id ?: userId 160 mainExecutor.execute { 161 val manager = peopleHubManagerForUser[parentId] 162 ?: PeopleHubManager().also { peopleHubManagerForUser.put(parentId, it) } 163 if (manager.addActivePerson(personModel)) { 164 updateUi() 165 } 166 } 167 } 168 } 169 } 170 171 override fun registerListener(listener: DataListener<PeopleHubModel>): Subscription { 172 val register = dataListeners.isEmpty() 173 dataListeners.add(listener) 174 if (register) { 175 userChangeSubscription = notifLockscreenUserMgr.registerListener( 176 object : NotificationLockscreenUserManager.UserChangedListener { 177 override fun onUserChanged(userId: Int) = updateUi() 178 override fun onCurrentProfilesChanged( 179 currentProfiles: SparseArray<UserInfo>? 180 ) = updateUi() 181 }) 182 notificationEntryManager.addNotificationEntryListener(notificationEntryListener) 183 } else { 184 getPeopleHubModelForCurrentUser()?.let(listener::onDataChanged) 185 } 186 return object : Subscription { 187 override fun unsubscribe() { 188 dataListeners.remove(listener) 189 if (dataListeners.isEmpty()) { 190 userChangeSubscription?.unsubscribe() 191 userChangeSubscription = null 192 notificationEntryManager 193 .removeNotificationEntryListener(notificationEntryListener) 194 } 195 } 196 } 197 } 198 199 private fun getPeopleHubModelForCurrentUser(): PeopleHubModel? { 200 val currentUserId = notifLockscreenUserMgr.currentUserId 201 val model = peopleHubManagerForUser[currentUserId]?.getPeopleHubModel() 202 ?: return null 203 val currentProfiles = notifLockscreenUserMgr.currentProfiles 204 return model.copy(people = model.people.filter { person -> 205 currentProfiles[person.userId]?.isQuietModeEnabled == false 206 }) 207 } 208 209 private fun updateUi() { 210 val model = getPeopleHubModelForCurrentUser() ?: return 211 for (listener in dataListeners) { 212 listener.onDataChanged(model) 213 } 214 } 215 216 private fun NotificationEntry.extractPerson(): PersonModel? { 217 val type = peopleNotificationIdentifier.getPeopleNotificationType(this) 218 if (type == TYPE_NON_PERSON) { 219 return null 220 } 221 val clickRunnable = Runnable { notificationListener.unsnoozeNotification(key) } 222 val extras = sbn.notification.extras 223 val name = ranking.conversationShortcutInfo?.label 224 ?: extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE) 225 ?: extras.getCharSequence(Notification.EXTRA_TITLE) 226 ?: return null 227 val drawable = ranking.getIcon(iconFactory, sbn) 228 ?: iconFactory.getConversationDrawable( 229 extractAvatarFromRow(this), 230 sbn.packageName, 231 sbn.uid, 232 ranking.channel.isImportantConversation 233 ) 234 return PersonModel(key, sbn.user.identifier, name, drawable, clickRunnable) 235 } 236 237 private fun NotificationListenerService.Ranking.getIcon( 238 iconFactory: ConversationIconFactory, 239 sbn: StatusBarNotification 240 ): Drawable? = 241 conversationShortcutInfo?.let { conversationShortcutInfo -> 242 iconFactory.getConversationDrawable( 243 conversationShortcutInfo, 244 sbn.packageName, 245 sbn.uid, 246 channel.isImportantConversation 247 ) 248 } 249 250 private fun NotificationEntry.extractPersonKey(): PersonKey? { 251 // TODO migrate to shortcut id when snoozing is conversation wide 252 val type = peopleNotificationIdentifier.getPeopleNotificationType(this) 253 return if (type != TYPE_NON_PERSON) key else null 254 } 255 } 256 257 private fun NotificationLockscreenUserManager.registerListener( 258 listener: NotificationLockscreenUserManager.UserChangedListener 259 ): Subscription { 260 addUserChangedListener(listener) 261 return object : Subscription { 262 override fun unsubscribe() { 263 removeUserChangedListener(listener) 264 } 265 } 266 } 267 268 class PeopleHubManager { 269 270 // People currently visible in the notification shade, and so are not in the hub 271 private val activePeople = mutableMapOf<PersonKey, PersonModel>() 272 273 // People that were once "active" and have been dismissed, and so can be displayed in the hub 274 private val inactivePeople = ArrayDeque<PersonModel>(MAX_STORED_INACTIVE_PEOPLE) 275 276 fun migrateActivePerson(key: PersonKey): Boolean { 277 activePeople.remove(key)?.let { data -> 278 if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) { 279 inactivePeople.removeLast() 280 } 281 inactivePeople.addFirst(data) 282 return true 283 } 284 return false 285 } 286 287 fun removeActivePerson(key: PersonKey) { 288 activePeople.remove(key) 289 } 290 291 fun addActivePerson(person: PersonModel): Boolean { 292 activePeople[person.key] = person 293 return inactivePeople.removeIf { it.key == person.key } 294 } 295 296 fun getPeopleHubModel(): PeopleHubModel = PeopleHubModel(inactivePeople) 297 } 298 299 private val ViewGroup.children 300 get(): Sequence<View> = sequence { 301 for (i in 0 until childCount) { 302 yield(getChildAt(i)) 303 } 304 } 305 306 private fun ViewGroup.childrenWithId(id: Int): Sequence<View> = children.filter { it.id == id } 307 308 fun extractAvatarFromRow(entry: NotificationEntry): Drawable? = 309 entry.row 310 ?.childrenWithId(R.id.expanded) 311 ?.mapNotNull { it as? ViewGroup } 312 ?.flatMap { 313 it.childrenWithId(com.android.internal.R.id.status_bar_latest_event_content) 314 } 315 ?.mapNotNull { 316 it.findViewById<ViewGroup>(com.android.internal.R.id.notification_messaging) 317 } 318 ?.mapNotNull { messagesView -> 319 messagesView.children 320 .mapNotNull { it as? MessagingGroup } 321 .lastOrNull() 322 ?.findViewById<ImageView>(com.android.internal.R.id.message_icon) 323 ?.drawable 324 } 325 ?.firstOrNull() 326