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