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.os.Handler
23 import android.service.notification.NotificationListenerService.Ranking
24 import android.service.notification.NotificationListenerService.RankingMap
25 import com.android.internal.statusbar.NotificationVisibility
26 import com.android.internal.widget.ConversationLayout
27 import com.android.systemui.dagger.qualifiers.Main
28 import com.android.systemui.statusbar.notification.collection.NotificationEntry
29 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
30 import com.android.systemui.statusbar.notification.row.NotificationContentView
31 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
32 import com.android.systemui.statusbar.phone.NotificationGroupManager
33 import java.util.concurrent.ConcurrentHashMap
34 import javax.inject.Inject
35 import javax.inject.Singleton
36 
37 /** Populates additional information in conversation notifications */
38 class ConversationNotificationProcessor @Inject constructor(
39     private val launcherApps: LauncherApps,
40     private val conversationNotificationManager: ConversationNotificationManager
41 ) {
42     fun processNotification(entry: NotificationEntry, recoveredBuilder: Notification.Builder) {
43         val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return
44         messagingStyle.conversationType =
45                 if (entry.ranking.channel.isImportantConversation)
46                     Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
47                 else
48                     Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL
49         entry.ranking.shortcutInfo?.let { shortcutInfo ->
50             messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo)
51             shortcutInfo.label?.let { label ->
52                 messagingStyle.conversationTitle = label
53             }
54         }
55         messagingStyle.unreadMessageCount =
56                 conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
57     }
58 }
59 
60 /**
61  * Tracks state related to conversation notifications, and updates the UI of existing notifications
62  * when necessary.
63  */
64 @Singleton
65 class ConversationNotificationManager @Inject constructor(
66     private val notificationEntryManager: NotificationEntryManager,
67     private val notificationGroupManager: NotificationGroupManager,
68     private val context: Context,
69     @Main private val mainHandler: Handler
70 ) {
71     // Need this state to be thread safe, since it's accessed from the ui thread
72     // (NotificationEntryListener) and a bg thread (NotificationContentInflater)
73     private val states = ConcurrentHashMap<String, ConversationState>()
74 
75     private var notifPanelCollapsed = true
76 
77     init {
78         notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener {
onNotificationRankingUpdatednull79             override fun onNotificationRankingUpdated(rankingMap: RankingMap) {
80                 fun getLayouts(view: NotificationContentView) =
81                         sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild)
82                 val ranking = Ranking()
83                 val activeConversationEntries = states.keys.asSequence()
84                         .mapNotNull { notificationEntryManager.getActiveNotificationUnfiltered(it) }
85                 for (entry in activeConversationEntries) {
86                     if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) {
87                         val important = ranking.channel.isImportantConversation
88                         val layouts = entry.row?.layouts?.asSequence()
89                                 ?.flatMap(::getLayouts)
90                                 ?.mapNotNull { it as? ConversationLayout }
91                                 ?: emptySequence()
92                         var changed = false
93                         for (layout in layouts) {
94                             if (important == layout.isImportantConversation) {
95                                 continue
96                             }
97                             changed = true
98                             if (important && entry.isMarkedForUserTriggeredMovement) {
99                                 // delay this so that it doesn't animate in until after
100                                 // the notif has been moved in the shade
101                                 mainHandler.postDelayed({
102                                     layout.setIsImportantConversation(
103                                             important, true /* animate */)
104                                 }, IMPORTANCE_ANIMATION_DELAY.toLong())
105                             } else {
106                                 layout.setIsImportantConversation(important)
107                             }
108                         }
109                         if (changed) {
110                             notificationGroupManager.updateIsolation(entry)
111                         }
112                     }
113                 }
114             }
115 
onEntryInflatednull116             override fun onEntryInflated(entry: NotificationEntry) {
117                 if (!entry.ranking.isConversation) return
118                 fun updateCount(isExpanded: Boolean) {
119                     if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded())) {
120                         resetCount(entry.key)
121                         entry.row?.let(::resetBadgeUi)
122                     }
123                 }
124                 entry.row?.setOnExpansionChangedListener { isExpanded ->
125                     if (entry.row?.isShown == true && isExpanded) {
126                         entry.row.performOnIntrinsicHeightReached {
127                             updateCount(isExpanded)
128                         }
129                     } else {
130                         updateCount(isExpanded)
131                     }
132                 }
133                 updateCount(entry.row?.isExpanded == true)
134             }
135 
onEntryReinflatednull136             override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry)
137 
138             override fun onEntryRemoved(
139                 entry: NotificationEntry,
140                 visibility: NotificationVisibility?,
141                 removedByUser: Boolean,
142                 reason: Int
143             ) = removeTrackedEntry(entry)
144         })
145     }
146 
147     fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int =
148             states.compute(entry.key) { _, state ->
149                 val newCount = state?.run {
150                     val old = Notification.Builder.recoverBuilder(context, notification)
151                     val increment = Notification
152                             .areStyledNotificationsVisiblyDifferent(old, recoveredBuilder)
153                     if (increment) unreadCount + 1 else unreadCount
154                 } ?: 1
155                 ConversationState(newCount, entry.sbn.notification)
156             }!!.unreadCount
157 
onNotificationPanelExpandStateChangednull158     fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) {
159         notifPanelCollapsed = isCollapsed
160         if (isCollapsed) return
161 
162         // When the notification panel is expanded, reset the counters of any expanded
163         // conversations
164         val expanded = states
165                 .asSequence()
166                 .mapNotNull { (key, _) ->
167                     notificationEntryManager.getActiveNotificationUnfiltered(key)
168                             ?.let { entry ->
169                                 if (entry.row?.isExpanded == true) key to entry
170                                 else null
171                             }
172                 }
173                 .toMap()
174         states.replaceAll { key, state ->
175             if (expanded.contains(key)) state.copy(unreadCount = 0)
176             else state
177         }
178         // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the
179         // lambda if threads are in contention.
180         expanded.values.asSequence().mapNotNull { it.row }.forEach(::resetBadgeUi)
181     }
182 
resetCountnull183     private fun resetCount(key: String) {
184         states.compute(key) { _, state -> state?.copy(unreadCount = 0) }
185     }
186 
removeTrackedEntrynull187     private fun removeTrackedEntry(entry: NotificationEntry) {
188         states.remove(entry.key)
189     }
190 
resetBadgeUinull191     private fun resetBadgeUi(row: ExpandableNotificationRow): Unit =
192             (row.layouts?.asSequence() ?: emptySequence())
193                     .flatMap { layout -> layout.allViews.asSequence() }
viewnull194                     .mapNotNull { view -> view as? ConversationLayout }
convoLayoutnull195                     .forEach { convoLayout -> convoLayout.setUnreadCount(0) }
196 
197     private data class ConversationState(val unreadCount: Int, val notification: Notification)
198 
199     companion object {
200         private const val IMPORTANCE_ANIMATION_DELAY =
201                 StackStateAnimator.ANIMATION_DURATION_STANDARD +
202                 StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE +
203                 100
204     }
205 }
206