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