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