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