• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2020 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
18 
19 import android.app.Notification
20 import android.app.Notification.EXTRA_SUMMARIZED_CONTENT
21 import android.content.Context
22 import android.content.pm.LauncherApps
23 import android.graphics.Typeface
24 import android.graphics.drawable.AnimatedImageDrawable
25 import android.os.Handler
26 import android.service.notification.NotificationListenerService.Ranking
27 import android.service.notification.NotificationListenerService.RankingMap
28 import android.text.SpannableString
29 import android.text.Spanned
30 import android.text.TextUtils
31 import android.text.style.ImageSpan
32 import android.text.style.StyleSpan
33 import com.android.internal.R
34 import com.android.internal.widget.ConversationLayout
35 import com.android.internal.widget.MessagingImageMessage
36 import com.android.internal.widget.MessagingLayout
37 import com.android.systemui.dagger.SysUISingleton
38 import com.android.systemui.dagger.qualifiers.Main
39 import com.android.systemui.plugins.statusbar.StatusBarStateController
40 import com.android.systemui.shade.ShadeDisplayAware
41 import com.android.systemui.statusbar.notification.collection.NotificationEntry
42 import com.android.systemui.statusbar.notification.collection.inflation.BindEventManager
43 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
44 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
45 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
46 import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener
47 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
48 import com.android.systemui.statusbar.notification.row.NotificationContentView
49 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinderLogger
50 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
51 import com.android.systemui.util.children
52 import java.util.concurrent.ConcurrentHashMap
53 import javax.inject.Inject
54 
55 /** Populates additional information in conversation notifications */
56 class ConversationNotificationProcessor
57 @Inject
58 constructor(
59     @ShadeDisplayAware private val context: Context,
60     private val launcherApps: LauncherApps,
61     private val conversationNotificationManager: ConversationNotificationManager,
62 ) {
63     fun processNotification(
64         entry: NotificationEntry,
65         recoveredBuilder: Notification.Builder,
66         logger: NotificationRowContentBinderLogger,
67     ): Notification.MessagingStyle? {
68         val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null
69         messagingStyle.conversationType =
70             if (entry.ranking.channel.isImportantConversation)
71                 Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
72             else if (entry.ranking.isConversation)
73                 Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL
74             else Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY
75         entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->
76             logger.logAsyncTaskProgress(entry.logKey, "getting shortcut icon")
77             messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
78             shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label }
79         }
80         if (NmSummarizationUiFlag.isEnabled) {
81             if (!TextUtils.isEmpty(entry.ranking.summarization)) {
82                 val icon = context.getDrawable(R.drawable.ic_notification_summarization)?.mutate()
83                 val imageSpan =
84                     icon?.let {
85                         it.setBounds(
86                             /* left= */ 0,
87                             /* top= */ 0,
88                             icon.getIntrinsicWidth(),
89                             icon.getIntrinsicHeight(),
90                         )
91                         ImageSpan(it, ImageSpan.ALIGN_CENTER)
92                     }
93                 val decoratedSummary =
94                     SpannableString("x " + entry.ranking.summarization).apply {
95                         setSpan(
96                             /* what = */ imageSpan,
97                             /* start = */ 0,
98                             /* end = */ 1,
99                             /* flags = */ Spanned.SPAN_INCLUSIVE_EXCLUSIVE,
100                         )
101                         entry.ranking.summarization?.let {
102                             setSpan(
103                                 /* what = */ StyleSpan(Typeface.ITALIC),
104                                 /* start = */ 2,
105                                 /* end = */ it.length + 2,
106                                 /* flags = */ Spanned.SPAN_EXCLUSIVE_INCLUSIVE,
107                             )
108                         }
109                     }
110                 entry.sbn.notification.extras.putCharSequence(
111                     EXTRA_SUMMARIZED_CONTENT,
112                     decoratedSummary,
113                 )
114             } else {
115                 entry.sbn.notification.extras.putCharSequence(EXTRA_SUMMARIZED_CONTENT, null)
116             }
117         }
118 
119         messagingStyle.unreadMessageCount =
120             conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
121         return messagingStyle
122     }
123 }
124 
125 /**
126  * Tracks state related to animated images inside of notifications. Ex: starting and stopping
127  * animations to conserve CPU and memory.
128  */
129 @SysUISingleton
130 class AnimatedImageNotificationManager
131 @Inject
132 constructor(
133     private val notifCollection: CommonNotifCollection,
134     private val bindEventManager: BindEventManager,
135     private val headsUpManager: HeadsUpManager,
136     private val statusBarStateController: StatusBarStateController,
137 ) {
138 
139     private var isStatusBarExpanded = false
140 
141     /** Begins listening to state changes and updating animations accordingly. */
bindnull142     fun bind() {
143         headsUpManager.addListener(
144             object : OnHeadsUpChangedListener {
145                 override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
146                     updateAnimatedImageDrawables(entry)
147                 }
148             }
149         )
150         statusBarStateController.addCallback(
151             object : StatusBarStateController.StateListener {
152                 override fun onExpandedChanged(isExpanded: Boolean) {
153                     isStatusBarExpanded = isExpanded
154                     notifCollection.allNotifs.forEach(::updateAnimatedImageDrawables)
155                 }
156             }
157         )
158         bindEventManager.addListener(::updateAnimatedImageDrawables)
159     }
160 
updateAnimatedImageDrawablesnull161     private fun updateAnimatedImageDrawables(entry: NotificationEntry) =
162         entry.row?.let { row ->
163             updateAnimatedImageDrawables(row, animating = row.isHeadsUp || isStatusBarExpanded)
164         }
165 
updateAnimatedImageDrawablesnull166     private fun updateAnimatedImageDrawables(row: ExpandableNotificationRow, animating: Boolean) =
167         (row.layouts?.asSequence() ?: emptySequence())
168             .flatMap { layout -> layout.allViews.asSequence() }
viewnull169             .flatMap { view ->
170                 (view as? ConversationLayout)?.messagingGroups?.asSequence()
171                     ?: (view as? MessagingLayout)?.messagingGroups?.asSequence()
172                     ?: emptySequence()
173             }
messagingGroupnull174             .flatMap { messagingGroup -> messagingGroup.messageContainer.children }
viewnull175             .mapNotNull { view ->
176                 (view as? MessagingImageMessage)?.let { imageMessage ->
177                     imageMessage.drawable as? AnimatedImageDrawable
178                 }
179             }
animatedImageDrawablenull180             .forEach { animatedImageDrawable ->
181                 if (animating) animatedImageDrawable.start() else animatedImageDrawable.stop()
182             }
183 }
184 
185 /**
186  * Tracks state related to conversation notifications, and updates the UI of existing notifications
187  * when necessary.
188  *
189  * TODO(b/214083332) Refactor this class to use the right coordinators and controllers
190  */
191 @SysUISingleton
192 class ConversationNotificationManager
193 @Inject
194 constructor(
195     bindEventManager: BindEventManager,
196     @ShadeDisplayAware private val context: Context,
197     private val notifCollection: CommonNotifCollection,
198     @Main private val mainHandler: Handler,
199 ) {
200     // Need this state to be thread safe, since it's accessed from the ui thread
201     // (NotificationEntryListener) and a bg thread (NotificationRowContentBinder)
202     private val states = ConcurrentHashMap<String, ConversationState>()
203 
204     private var notifPanelCollapsed = true
205 
updateNotificationRankingnull206     private fun updateNotificationRanking(rankingMap: RankingMap) {
207         fun getLayouts(view: NotificationContentView) =
208             sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild)
209         val ranking = Ranking()
210         val activeConversationEntries =
211             states.keys.asSequence().mapNotNull { notifCollection.getEntry(it) }
212         for (entry in activeConversationEntries) {
213             if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) {
214                 val important = ranking.channel.isImportantConversation
215                 var changed = false
216                 entry.row
217                     ?.layouts
218                     ?.asSequence()
219                     ?.flatMap(::getLayouts)
220                     ?.mapNotNull { it as? ConversationLayout }
221                     ?.filterNot { it.isImportantConversation == important }
222                     ?.forEach { layout ->
223                         changed = true
224                         if (important && entry.isMarkedForUserTriggeredMovement) {
225                             // delay this so that it doesn't animate in until after
226                             // the notif has been moved in the shade
227                             mainHandler.postDelayed(
228                                 { layout.setIsImportantConversation(important, true) },
229                                 IMPORTANCE_ANIMATION_DELAY.toLong(),
230                             )
231                         } else {
232                             layout.setIsImportantConversation(important, false)
233                         }
234                     }
235             }
236         }
237     }
238 
onEntryViewBoundnull239     fun onEntryViewBound(entry: NotificationEntry) {
240         if (!entry.ranking.isConversation) {
241             return
242         }
243         fun updateCount(isExpanded: Boolean) {
244             if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded)) {
245                 resetCount(entry.key)
246                 entry.row?.let(::resetBadgeUi)
247             }
248         }
249         entry.row?.setOnExpansionChangedListener { isExpanded ->
250             if (entry.row?.isShown == true && isExpanded) {
251                 entry.row.performOnIntrinsicHeightReached { updateCount(isExpanded) }
252             } else {
253                 updateCount(isExpanded)
254             }
255         }
256         updateCount(entry.row?.isExpanded == true)
257     }
258 
259     init {
260         notifCollection.addCollectionListener(
261             object : NotifCollectionListener {
onRankingUpdatenull262                 override fun onRankingUpdate(ranking: RankingMap) =
263                     updateNotificationRanking(ranking)
264 
265                 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) =
266                     removeTrackedEntry(entry)
267             }
268         )
269         bindEventManager.addListener(::onEntryViewBound)
270     }
271 
272     private fun ConversationState.shouldIncrementUnread(newBuilder: Notification.Builder) =
273         if (notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) {
274             false
275         } else {
276             val oldBuilder = Notification.Builder.recoverBuilder(context, notification)
277             Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder)
278         }
279 
getUnreadCountnull280     fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int =
281         states
282             .compute(entry.key) { _, state ->
283                 val newCount =
284                     state?.run {
285                         if (shouldIncrementUnread(recoveredBuilder)) unreadCount + 1
286                         else unreadCount
287                     } ?: 1
288                 ConversationState(newCount, entry.sbn.notification)
289             }!!
290             .unreadCount
291 
onNotificationPanelExpandStateChangednull292     fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) {
293         notifPanelCollapsed = isCollapsed
294         if (isCollapsed) return
295 
296         // When the notification panel is expanded, reset the counters of any expanded
297         // conversations
298         val expanded =
299             states
300                 .asSequence()
301                 .mapNotNull { (key, _) ->
302                     notifCollection.getEntry(key)?.let { entry ->
303                         if (entry.row?.isExpanded == true) key to entry else null
304                     }
305                 }
306                 .toMap()
307         states.replaceAll { key, state ->
308             if (expanded.contains(key)) state.copy(unreadCount = 0) else state
309         }
310         // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the
311         // lambda if threads are in contention.
312         expanded.values.asSequence().mapNotNull { it.row }.forEach(::resetBadgeUi)
313     }
314 
resetCountnull315     private fun resetCount(key: String) {
316         states.compute(key) { _, state -> state?.copy(unreadCount = 0) }
317     }
318 
removeTrackedEntrynull319     private fun removeTrackedEntry(entry: NotificationEntry) {
320         states.remove(entry.key)
321     }
322 
resetBadgeUinull323     private fun resetBadgeUi(row: ExpandableNotificationRow): Unit =
324         (row.layouts?.asSequence() ?: emptySequence())
325             .flatMap { layout -> layout.allViews.asSequence() }
viewnull326             .mapNotNull { view -> view as? ConversationLayout }
convoLayoutnull327             .forEach { convoLayout -> convoLayout.setUnreadCount(0) }
328 
329     private data class ConversationState(val unreadCount: Int, val notification: Notification)
330 
331     companion object {
332         private const val IMPORTANCE_ANIMATION_DELAY =
333             StackStateAnimator.ANIMATION_DURATION_STANDARD +
334                 StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE +
335                 100
336     }
337 }
338