1 /*
<lambda>null2 * 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.qualifiers.Background
39 import com.android.systemui.dagger.qualifiers.Main
40 import com.android.systemui.plugins.NotificationPersonExtractorPlugin
41 import com.android.systemui.statusbar.NotificationListener
42 import com.android.systemui.statusbar.NotificationLockscreenUserManager
43 import com.android.systemui.statusbar.notification.NotificationEntryListener
44 import com.android.systemui.statusbar.notification.NotificationEntryManager
45 import com.android.systemui.statusbar.notification.collection.NotificationEntry
46 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
47 import com.android.systemui.statusbar.policy.ExtensionController
48 import java.util.ArrayDeque
49 import java.util.concurrent.Executor
50 import javax.inject.Inject
51 import javax.inject.Singleton
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 @Singleton
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)
extractornull72 .withCallback { extractor ->
73 plugin = extractor
74 }
75 .build()
76 .get()
77 }
78
extractPersonnull79 override fun extractPerson(sbn: StatusBarNotification) =
80 plugin?.extractPerson(sbn)?.run {
81 PersonModel(key, sbn.user.identifier, name, avatar, clickRunnable)
82 }
83
extractPersonKeynull84 override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn)
85
86 override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
87 plugin?.isPersonNotification(sbn) ?: false
88 }
89
90 @Singleton
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(sbn, ranking)
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.shortcutInfo?.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 shortcutInfo?.let { shortcutInfo ->
242 iconFactory.getConversationDrawable(
243 shortcutInfo,
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(sbn, ranking)
253 return if (type != TYPE_NON_PERSON) key else null
254 }
255 }
256
NotificationLockscreenUserManagernull257 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
migrateActivePersonnull276 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
removeActivePersonnull287 fun removeActivePerson(key: PersonKey) {
288 activePeople.remove(key)
289 }
290
addActivePersonnull291 fun addActivePerson(person: PersonModel): Boolean {
292 activePeople[person.key] = person
293 return inactivePeople.removeIf { it.key == person.key }
294 }
295
getPeopleHubModelnull296 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
<lambda>null306 private fun ViewGroup.childrenWithId(id: Int): Sequence<View> = children.filter { it.id == id }
307
extractAvatarFromRownull308 fun extractAvatarFromRow(entry: NotificationEntry): Drawable? =
309 entry.row
310 ?.childrenWithId(R.id.expanded)
311 ?.mapNotNull { it as? ViewGroup }
<lambda>null312 ?.flatMap {
313 it.childrenWithId(com.android.internal.R.id.status_bar_latest_event_content)
314 }
<lambda>null315 ?.mapNotNull {
316 it.findViewById<ViewGroup>(com.android.internal.R.id.notification_messaging)
317 }
messagesViewnull318 ?.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