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