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.app.Notification.EXTRA_SUMMARIZED_CONTENT 21 import android.content.Context 22 import android.content.pm.LauncherApps 23 import android.graphics.Typeface 24 import android.graphics.drawable.AnimatedImageDrawable 25 import android.os.Handler 26 import android.service.notification.NotificationListenerService.Ranking 27 import android.service.notification.NotificationListenerService.RankingMap 28 import android.text.SpannableString 29 import android.text.Spanned 30 import android.text.TextUtils 31 import android.text.style.ImageSpan 32 import android.text.style.StyleSpan 33 import com.android.internal.R 34 import com.android.internal.widget.ConversationLayout 35 import com.android.internal.widget.MessagingImageMessage 36 import com.android.internal.widget.MessagingLayout 37 import com.android.systemui.dagger.SysUISingleton 38 import com.android.systemui.dagger.qualifiers.Main 39 import com.android.systemui.plugins.statusbar.StatusBarStateController 40 import com.android.systemui.shade.ShadeDisplayAware 41 import com.android.systemui.statusbar.notification.collection.NotificationEntry 42 import com.android.systemui.statusbar.notification.collection.inflation.BindEventManager 43 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection 44 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener 45 import com.android.systemui.statusbar.notification.headsup.HeadsUpManager 46 import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener 47 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 48 import com.android.systemui.statusbar.notification.row.NotificationContentView 49 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinderLogger 50 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 51 import com.android.systemui.util.children 52 import java.util.concurrent.ConcurrentHashMap 53 import javax.inject.Inject 54 55 /** Populates additional information in conversation notifications */ 56 class ConversationNotificationProcessor 57 @Inject 58 constructor( 59 @ShadeDisplayAware private val context: Context, 60 private val launcherApps: LauncherApps, 61 private val conversationNotificationManager: ConversationNotificationManager, 62 ) { 63 fun processNotification( 64 entry: NotificationEntry, 65 recoveredBuilder: Notification.Builder, 66 logger: NotificationRowContentBinderLogger, 67 ): Notification.MessagingStyle? { 68 val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null 69 messagingStyle.conversationType = 70 if (entry.ranking.channel.isImportantConversation) 71 Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT 72 else if (entry.ranking.isConversation) 73 Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL 74 else Notification.MessagingStyle.CONVERSATION_TYPE_LEGACY 75 entry.ranking.conversationShortcutInfo?.let { shortcutInfo -> 76 logger.logAsyncTaskProgress(entry.logKey, "getting shortcut icon") 77 messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo) 78 shortcutInfo.label?.let { label -> messagingStyle.conversationTitle = label } 79 } 80 if (NmSummarizationUiFlag.isEnabled) { 81 if (!TextUtils.isEmpty(entry.ranking.summarization)) { 82 val icon = context.getDrawable(R.drawable.ic_notification_summarization)?.mutate() 83 val imageSpan = 84 icon?.let { 85 it.setBounds( 86 /* left= */ 0, 87 /* top= */ 0, 88 icon.getIntrinsicWidth(), 89 icon.getIntrinsicHeight(), 90 ) 91 ImageSpan(it, ImageSpan.ALIGN_CENTER) 92 } 93 val decoratedSummary = 94 SpannableString("x " + entry.ranking.summarization).apply { 95 setSpan( 96 /* what = */ imageSpan, 97 /* start = */ 0, 98 /* end = */ 1, 99 /* flags = */ Spanned.SPAN_INCLUSIVE_EXCLUSIVE, 100 ) 101 entry.ranking.summarization?.let { 102 setSpan( 103 /* what = */ StyleSpan(Typeface.ITALIC), 104 /* start = */ 2, 105 /* end = */ it.length + 2, 106 /* flags = */ Spanned.SPAN_EXCLUSIVE_INCLUSIVE, 107 ) 108 } 109 } 110 entry.sbn.notification.extras.putCharSequence( 111 EXTRA_SUMMARIZED_CONTENT, 112 decoratedSummary, 113 ) 114 } else { 115 entry.sbn.notification.extras.putCharSequence(EXTRA_SUMMARIZED_CONTENT, null) 116 } 117 } 118 119 messagingStyle.unreadMessageCount = 120 conversationNotificationManager.getUnreadCount(entry, recoveredBuilder) 121 return messagingStyle 122 } 123 } 124 125 /** 126 * Tracks state related to animated images inside of notifications. Ex: starting and stopping 127 * animations to conserve CPU and memory. 128 */ 129 @SysUISingleton 130 class AnimatedImageNotificationManager 131 @Inject 132 constructor( 133 private val notifCollection: CommonNotifCollection, 134 private val bindEventManager: BindEventManager, 135 private val headsUpManager: HeadsUpManager, 136 private val statusBarStateController: StatusBarStateController, 137 ) { 138 139 private var isStatusBarExpanded = false 140 141 /** Begins listening to state changes and updating animations accordingly. */ bindnull142 fun bind() { 143 headsUpManager.addListener( 144 object : OnHeadsUpChangedListener { 145 override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { 146 updateAnimatedImageDrawables(entry) 147 } 148 } 149 ) 150 statusBarStateController.addCallback( 151 object : StatusBarStateController.StateListener { 152 override fun onExpandedChanged(isExpanded: Boolean) { 153 isStatusBarExpanded = isExpanded 154 notifCollection.allNotifs.forEach(::updateAnimatedImageDrawables) 155 } 156 } 157 ) 158 bindEventManager.addListener(::updateAnimatedImageDrawables) 159 } 160 updateAnimatedImageDrawablesnull161 private fun updateAnimatedImageDrawables(entry: NotificationEntry) = 162 entry.row?.let { row -> 163 updateAnimatedImageDrawables(row, animating = row.isHeadsUp || isStatusBarExpanded) 164 } 165 updateAnimatedImageDrawablesnull166 private fun updateAnimatedImageDrawables(row: ExpandableNotificationRow, animating: Boolean) = 167 (row.layouts?.asSequence() ?: emptySequence()) 168 .flatMap { layout -> layout.allViews.asSequence() } viewnull169 .flatMap { view -> 170 (view as? ConversationLayout)?.messagingGroups?.asSequence() 171 ?: (view as? MessagingLayout)?.messagingGroups?.asSequence() 172 ?: emptySequence() 173 } messagingGroupnull174 .flatMap { messagingGroup -> messagingGroup.messageContainer.children } viewnull175 .mapNotNull { view -> 176 (view as? MessagingImageMessage)?.let { imageMessage -> 177 imageMessage.drawable as? AnimatedImageDrawable 178 } 179 } animatedImageDrawablenull180 .forEach { animatedImageDrawable -> 181 if (animating) animatedImageDrawable.start() else animatedImageDrawable.stop() 182 } 183 } 184 185 /** 186 * Tracks state related to conversation notifications, and updates the UI of existing notifications 187 * when necessary. 188 * 189 * TODO(b/214083332) Refactor this class to use the right coordinators and controllers 190 */ 191 @SysUISingleton 192 class ConversationNotificationManager 193 @Inject 194 constructor( 195 bindEventManager: BindEventManager, 196 @ShadeDisplayAware private val context: Context, 197 private val notifCollection: CommonNotifCollection, 198 @Main private val mainHandler: Handler, 199 ) { 200 // Need this state to be thread safe, since it's accessed from the ui thread 201 // (NotificationEntryListener) and a bg thread (NotificationRowContentBinder) 202 private val states = ConcurrentHashMap<String, ConversationState>() 203 204 private var notifPanelCollapsed = true 205 updateNotificationRankingnull206 private fun updateNotificationRanking(rankingMap: RankingMap) { 207 fun getLayouts(view: NotificationContentView) = 208 sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild) 209 val ranking = Ranking() 210 val activeConversationEntries = 211 states.keys.asSequence().mapNotNull { notifCollection.getEntry(it) } 212 for (entry in activeConversationEntries) { 213 if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) { 214 val important = ranking.channel.isImportantConversation 215 var changed = false 216 entry.row 217 ?.layouts 218 ?.asSequence() 219 ?.flatMap(::getLayouts) 220 ?.mapNotNull { it as? ConversationLayout } 221 ?.filterNot { it.isImportantConversation == important } 222 ?.forEach { layout -> 223 changed = true 224 if (important && entry.isMarkedForUserTriggeredMovement) { 225 // delay this so that it doesn't animate in until after 226 // the notif has been moved in the shade 227 mainHandler.postDelayed( 228 { layout.setIsImportantConversation(important, true) }, 229 IMPORTANCE_ANIMATION_DELAY.toLong(), 230 ) 231 } else { 232 layout.setIsImportantConversation(important, false) 233 } 234 } 235 } 236 } 237 } 238 onEntryViewBoundnull239 fun onEntryViewBound(entry: NotificationEntry) { 240 if (!entry.ranking.isConversation) { 241 return 242 } 243 fun updateCount(isExpanded: Boolean) { 244 if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded)) { 245 resetCount(entry.key) 246 entry.row?.let(::resetBadgeUi) 247 } 248 } 249 entry.row?.setOnExpansionChangedListener { isExpanded -> 250 if (entry.row?.isShown == true && isExpanded) { 251 entry.row.performOnIntrinsicHeightReached { updateCount(isExpanded) } 252 } else { 253 updateCount(isExpanded) 254 } 255 } 256 updateCount(entry.row?.isExpanded == true) 257 } 258 259 init { 260 notifCollection.addCollectionListener( 261 object : NotifCollectionListener { onRankingUpdatenull262 override fun onRankingUpdate(ranking: RankingMap) = 263 updateNotificationRanking(ranking) 264 265 override fun onEntryRemoved(entry: NotificationEntry, reason: Int) = 266 removeTrackedEntry(entry) 267 } 268 ) 269 bindEventManager.addListener(::onEntryViewBound) 270 } 271 272 private fun ConversationState.shouldIncrementUnread(newBuilder: Notification.Builder) = 273 if (notification.flags and Notification.FLAG_ONLY_ALERT_ONCE != 0) { 274 false 275 } else { 276 val oldBuilder = Notification.Builder.recoverBuilder(context, notification) 277 Notification.areStyledNotificationsVisiblyDifferent(oldBuilder, newBuilder) 278 } 279 getUnreadCountnull280 fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int = 281 states 282 .compute(entry.key) { _, state -> 283 val newCount = 284 state?.run { 285 if (shouldIncrementUnread(recoveredBuilder)) unreadCount + 1 286 else unreadCount 287 } ?: 1 288 ConversationState(newCount, entry.sbn.notification) 289 }!! 290 .unreadCount 291 onNotificationPanelExpandStateChangednull292 fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) { 293 notifPanelCollapsed = isCollapsed 294 if (isCollapsed) return 295 296 // When the notification panel is expanded, reset the counters of any expanded 297 // conversations 298 val expanded = 299 states 300 .asSequence() 301 .mapNotNull { (key, _) -> 302 notifCollection.getEntry(key)?.let { entry -> 303 if (entry.row?.isExpanded == true) key to entry else null 304 } 305 } 306 .toMap() 307 states.replaceAll { key, state -> 308 if (expanded.contains(key)) state.copy(unreadCount = 0) else state 309 } 310 // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the 311 // lambda if threads are in contention. 312 expanded.values.asSequence().mapNotNull { it.row }.forEach(::resetBadgeUi) 313 } 314 resetCountnull315 private fun resetCount(key: String) { 316 states.compute(key) { _, state -> state?.copy(unreadCount = 0) } 317 } 318 removeTrackedEntrynull319 private fun removeTrackedEntry(entry: NotificationEntry) { 320 states.remove(entry.key) 321 } 322 resetBadgeUinull323 private fun resetBadgeUi(row: ExpandableNotificationRow): Unit = 324 (row.layouts?.asSequence() ?: emptySequence()) 325 .flatMap { layout -> layout.allViews.asSequence() } viewnull326 .mapNotNull { view -> view as? ConversationLayout } convoLayoutnull327 .forEach { convoLayout -> convoLayout.setUnreadCount(0) } 328 329 private data class ConversationState(val unreadCount: Int, val notification: Notification) 330 331 companion object { 332 private const val IMPORTANCE_ANIMATION_DELAY = 333 StackStateAnimator.ANIMATION_DURATION_STANDARD + 334 StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE + 335 100 336 } 337 } 338