• 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.content.Context
21 import android.content.pm.LauncherApps
22 import android.graphics.drawable.AnimatedImageDrawable
23 import android.os.Handler
24 import android.service.notification.NotificationListenerService.Ranking
25 import android.service.notification.NotificationListenerService.RankingMap
26 import com.android.internal.statusbar.NotificationVisibility
27 import com.android.internal.widget.ConversationLayout
28 import com.android.internal.widget.MessagingImageMessage
29 import com.android.internal.widget.MessagingLayout
30 import com.android.systemui.dagger.SysUISingleton
31 import com.android.systemui.dagger.qualifiers.Main
32 import com.android.systemui.plugins.statusbar.StatusBarStateController
33 import com.android.systemui.statusbar.notification.collection.NotificationEntry
34 import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy
35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
36 import com.android.systemui.statusbar.notification.row.NotificationContentView
37 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
38 import com.android.systemui.statusbar.policy.HeadsUpManager
39 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
40 import com.android.systemui.util.children
41 import java.util.concurrent.ConcurrentHashMap
42 import javax.inject.Inject
43 
44 /** Populates additional information in conversation notifications */
45 class ConversationNotificationProcessor @Inject constructor(
46     private val launcherApps: LauncherApps,
47     private val conversationNotificationManager: ConversationNotificationManager
48 ) {
49     fun processNotification(entry: NotificationEntry, recoveredBuilder: Notification.Builder) {
50         val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return
51         messagingStyle.conversationType =
52                 if (entry.ranking.channel.isImportantConversation)
53                     Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
54                 else
55                     Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL
56         entry.ranking.conversationShortcutInfo?.let { shortcutInfo ->
57             messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
58             shortcutInfo.label?.let { label ->
59                 messagingStyle.conversationTitle = label
60             }
61         }
62         messagingStyle.unreadMessageCount =
63                 conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
64     }
65 }
66 
67 /**
68  * Tracks state related to animated images inside of notifications. Ex: starting and stopping
69  * animations to conserve CPU and memory.
70  */
71 @SysUISingleton
72 class AnimatedImageNotificationManager @Inject constructor(
73     private val notificationEntryManager: NotificationEntryManager,
74     private val headsUpManager: HeadsUpManager,
75     private val statusBarStateController: StatusBarStateController
76 ) {
77 
78     private var isStatusBarExpanded = false
79 
80     /** Begins listening to state changes and updating animations accordingly. */
bindnull81     fun bind() {
82         headsUpManager.addListener(object : OnHeadsUpChangedListener {
83             override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
84                 entry.row?.let { row ->
85                     updateAnimatedImageDrawables(row, animating = isHeadsUp || isStatusBarExpanded)
86                 }
87             }
88         })
89         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
90             override fun onExpandedChanged(isExpanded: Boolean) {
91                 isStatusBarExpanded = isExpanded
92                 notificationEntryManager.activeNotificationsForCurrentUser.forEach { entry ->
93                     entry.row?.let { row ->
94                         updateAnimatedImageDrawables(row, animating = isExpanded || row.isHeadsUp)
95                     }
96                 }
97             }
98         })
99         notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
100             override fun onEntryInflated(entry: NotificationEntry) {
101                 entry.row?.let { row ->
102                     updateAnimatedImageDrawables(
103                             row,
104                             animating = isStatusBarExpanded || row.isHeadsUp)
105                 }
106             }
107             override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry)
108         })
109     }
110 
updateAnimatedImageDrawablesnull111     private fun updateAnimatedImageDrawables(row: ExpandableNotificationRow, animating: Boolean) =
112             (row.layouts?.asSequence() ?: emptySequence())
113                     .flatMap { layout -> layout.allViews.asSequence() }
viewnull114                     .flatMap { view ->
115                         (view as? ConversationLayout)?.messagingGroups?.asSequence()
116                                 ?: (view as? MessagingLayout)?.messagingGroups?.asSequence()
117                                 ?: emptySequence()
118                     }
messagingGroupnull119                     .flatMap { messagingGroup -> messagingGroup.messageContainer.children }
viewnull120                     .mapNotNull { view ->
121                         (view as? MessagingImageMessage)
122                                 ?.let { imageMessage ->
123                                     imageMessage.drawable as? AnimatedImageDrawable
124                                 }
125                     }
animatedImageDrawablenull126                     .forEach { animatedImageDrawable ->
127                         if (animating) animatedImageDrawable.start()
128                         else animatedImageDrawable.stop()
129                     }
130 }
131 
132 /**
133  * Tracks state related to conversation notifications, and updates the UI of existing notifications
134  * when necessary.
135  */
136 @SysUISingleton
137 class ConversationNotificationManager @Inject constructor(
138     private val notificationEntryManager: NotificationEntryManager,
139     private val notificationGroupManager: NotificationGroupManagerLegacy,
140     private val context: Context,
141     @Main private val mainHandler: Handler
142 ) {
143     // Need this state to be thread safe, since it's accessed from the ui thread
144     // (NotificationEntryListener) and a bg thread (NotificationContentInflater)
145     private val states = ConcurrentHashMap<String, ConversationState>()
146 
147     private var notifPanelCollapsed = true
148 
149     init {
150         notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
onNotificationRankingUpdatednull151             override fun onNotificationRankingUpdated(rankingMap: RankingMap) {
152                 fun getLayouts(view: NotificationContentView) =
153                         sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild)
154                 val ranking = Ranking()
155                 val activeConversationEntries = states.keys.asSequence()
156                         .mapNotNull { notificationEntryManager.getActiveNotificationUnfiltered(it) }
157                 for (entry in activeConversationEntries) {
158                     if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) {
159                         val important = ranking.channel.isImportantConversation
160                         var changed = false
161                         entry.row?.layouts?.asSequence()
162                                 ?.flatMap(::getLayouts)
163                                 ?.mapNotNull { it as? ConversationLayout }
164                                 ?.filterNot { it.isImportantConversation == important }
165                                 ?.forEach { layout ->
166                                     changed = true
167                                     if (important && entry.isMarkedForUserTriggeredMovement) {
168                                         // delay this so that it doesn't animate in until after
169                                         // the notif has been moved in the shade
170                                         mainHandler.postDelayed(
171                                                 {
172                                                     layout.setIsImportantConversation(
173                                                             important,
174                                                             true)
175                                                 },
176                                                 IMPORTANCE_ANIMATION_DELAY.toLong())
177                                     } else {
178                                         layout.setIsImportantConversation(important, false)
179                                     }
180                                 }
181                         if (changed) {
182                             notificationGroupManager.updateIsolation(entry)
183                         }
184                     }
185                 }
186             }
187 
onEntryInflatednull188             override fun onEntryInflated(entry: NotificationEntry) {
189                 if (!entry.ranking.isConversation) {
190                     return
191                 }
192                 fun updateCount(isExpanded: Boolean) {
193                     if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded)) {
194                         resetCount(entry.key)
195                         entry.row?.let(::resetBadgeUi)
196                     }
197                 }
198                 entry.row?.setOnExpansionChangedListener { isExpanded ->
199                     if (entry.row?.isShown == true && isExpanded) {
200                         entry.row.performOnIntrinsicHeightReached {
201                             updateCount(isExpanded)
202                         }
203                     } else {
204                         updateCount(isExpanded)
205                     }
206                 }
207                 updateCount(entry.row?.isExpanded == true)
208             }
209 
onEntryReinflatednull210             override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry)
211 
212             override fun onEntryRemoved(
213                 entry: NotificationEntry,
214                 visibility: NotificationVisibility?,
215                 removedByUser: Boolean,
216                 reason: Int
217             ) = removeTrackedEntry(entry)
218         })
219     }
220 
221     private fun ConversationState.shouldIncrementUnread(newBuilder: Notification.Builder) =
222             if (notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) {
223                 false
224             } else {
225                 val oldBuilder = Notification.Builder.recoverBuilder(context, notification)
226                 Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder)
227             }
228 
getUnreadCountnull229     fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int =
230             states.compute(entry.key) { _, state ->
231                 val newCount = state?.run {
232                     if (shouldIncrementUnread(recoveredBuilder)) unreadCount + 1 else unreadCount
233                 } ?: 1
234                 ConversationState(newCount, entry.sbn.notification)
235             }!!.unreadCount
236 
onNotificationPanelExpandStateChangednull237     fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) {
238         notifPanelCollapsed = isCollapsed
239         if (isCollapsed) return
240 
241         // When the notification panel is expanded, reset the counters of any expanded
242         // conversations
243         val expanded = states
244                 .asSequence()
245                 .mapNotNull { (key, _) ->
246                     notificationEntryManager.getActiveNotificationUnfiltered(key)
247                             ?.let { entry ->
248                                 if (entry.row?.isExpanded == true) key to entry
249                                 else null
250                             }
251                 }
252                 .toMap()
253         states.replaceAll { key, state ->
254             if (expanded.contains(key)) state.copy(unreadCount = 0)
255             else state
256         }
257         // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the
258         // lambda if threads are in contention.
259         expanded.values.asSequence().mapNotNull { it.row }.forEach(::resetBadgeUi)
260     }
261 
resetCountnull262     private fun resetCount(key: String) {
263         states.compute(key) { _, state -> state?.copy(unreadCount = 0) }
264     }
265 
removeTrackedEntrynull266     private fun removeTrackedEntry(entry: NotificationEntry) {
267         states.remove(entry.key)
268     }
269 
resetBadgeUinull270     private fun resetBadgeUi(row: ExpandableNotificationRow): Unit =
271             (row.layouts?.asSequence() ?: emptySequence())
272                     .flatMap { layout -> layout.allViews.asSequence() }
viewnull273                     .mapNotNull { view -> view as? ConversationLayout }
convoLayoutnull274                     .forEach { convoLayout -> convoLayout.setUnreadCount(0) }
275 
276     private data class ConversationState(val unreadCount: Int, val notification: Notification)
277 
278     companion object {
279         private const val IMPORTANCE_ANIMATION_DELAY =
280                 StackStateAnimator.ANIMATION_DURATION_STANDARD +
281                 StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE +
282                 100
283     }
284 }
285