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