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