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.os.Handler 23 import android.service.notification.NotificationListenerService.Ranking 24 import android.service.notification.NotificationListenerService.RankingMap 25 import com.android.internal.statusbar.NotificationVisibility 26 import com.android.internal.widget.ConversationLayout 27 import com.android.systemui.dagger.qualifiers.Main 28 import com.android.systemui.statusbar.notification.collection.NotificationEntry 29 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 30 import com.android.systemui.statusbar.notification.row.NotificationContentView 31 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 32 import com.android.systemui.statusbar.phone.NotificationGroupManager 33 import java.util.concurrent.ConcurrentHashMap 34 import javax.inject.Inject 35 import javax.inject.Singleton 36 37 /** Populates additional information in conversation notifications */ 38 class ConversationNotificationProcessor @Inject constructor( 39 private val launcherApps: LauncherApps, 40 private val conversationNotificationManager: ConversationNotificationManager 41 ) { 42 fun processNotification(entry: NotificationEntry, recoveredBuilder: Notification.Builder) { 43 val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return 44 messagingStyle.conversationType = 45 if (entry.ranking.channel.isImportantConversation) 46 Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT 47 else 48 Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL 49 entry.ranking.shortcutInfo?.let { shortcutInfo -> 50 messagingStyle.shortcutIcon = launcherApps.getShortcutIcon(shortcutInfo) 51 shortcutInfo.label?.let { label -> 52 messagingStyle.conversationTitle = label 53 } 54 } 55 messagingStyle.unreadMessageCount = 56 conversationNotificationManager.getUnreadCount(entry, recoveredBuilder) 57 } 58 } 59 60 /** 61 * Tracks state related to conversation notifications, and updates the UI of existing notifications 62 * when necessary. 63 */ 64 @Singleton 65 class ConversationNotificationManager @Inject constructor( 66 private val notificationEntryManager: NotificationEntryManager, 67 private val notificationGroupManager: NotificationGroupManager, 68 private val context: Context, 69 @Main private val mainHandler: Handler 70 ) { 71 // Need this state to be thread safe, since it's accessed from the ui thread 72 // (NotificationEntryListener) and a bg thread (NotificationContentInflater) 73 private val states = ConcurrentHashMap<String, ConversationState>() 74 75 private var notifPanelCollapsed = true 76 77 init { 78 notificationEntryManager.addNotificationEntryListener(object : NotificationEntryListener { onNotificationRankingUpdatednull79 override fun onNotificationRankingUpdated(rankingMap: RankingMap) { 80 fun getLayouts(view: NotificationContentView) = 81 sequenceOf(view.contractedChild, view.expandedChild, view.headsUpChild) 82 val ranking = Ranking() 83 val activeConversationEntries = states.keys.asSequence() 84 .mapNotNull { notificationEntryManager.getActiveNotificationUnfiltered(it) } 85 for (entry in activeConversationEntries) { 86 if (rankingMap.getRanking(entry.sbn.key, ranking) && ranking.isConversation) { 87 val important = ranking.channel.isImportantConversation 88 val layouts = entry.row?.layouts?.asSequence() 89 ?.flatMap(::getLayouts) 90 ?.mapNotNull { it as? ConversationLayout } 91 ?: emptySequence() 92 var changed = false 93 for (layout in layouts) { 94 if (important == layout.isImportantConversation) { 95 continue 96 } 97 changed = true 98 if (important && entry.isMarkedForUserTriggeredMovement) { 99 // delay this so that it doesn't animate in until after 100 // the notif has been moved in the shade 101 mainHandler.postDelayed({ 102 layout.setIsImportantConversation( 103 important, true /* animate */) 104 }, IMPORTANCE_ANIMATION_DELAY.toLong()) 105 } else { 106 layout.setIsImportantConversation(important) 107 } 108 } 109 if (changed) { 110 notificationGroupManager.updateIsolation(entry) 111 } 112 } 113 } 114 } 115 onEntryInflatednull116 override fun onEntryInflated(entry: NotificationEntry) { 117 if (!entry.ranking.isConversation) return 118 fun updateCount(isExpanded: Boolean) { 119 if (isExpanded && (!notifPanelCollapsed || entry.isPinnedAndExpanded())) { 120 resetCount(entry.key) 121 entry.row?.let(::resetBadgeUi) 122 } 123 } 124 entry.row?.setOnExpansionChangedListener { isExpanded -> 125 if (entry.row?.isShown == true && isExpanded) { 126 entry.row.performOnIntrinsicHeightReached { 127 updateCount(isExpanded) 128 } 129 } else { 130 updateCount(isExpanded) 131 } 132 } 133 updateCount(entry.row?.isExpanded == true) 134 } 135 onEntryReinflatednull136 override fun onEntryReinflated(entry: NotificationEntry) = onEntryInflated(entry) 137 138 override fun onEntryRemoved( 139 entry: NotificationEntry, 140 visibility: NotificationVisibility?, 141 removedByUser: Boolean, 142 reason: Int 143 ) = removeTrackedEntry(entry) 144 }) 145 } 146 147 fun getUnreadCount(entry: NotificationEntry, recoveredBuilder: Notification.Builder): Int = 148 states.compute(entry.key) { _, state -> 149 val newCount = state?.run { 150 val old = Notification.Builder.recoverBuilder(context, notification) 151 val increment = Notification 152 .areStyledNotificationsVisiblyDifferent(old, recoveredBuilder) 153 if (increment) unreadCount + 1 else unreadCount 154 } ?: 1 155 ConversationState(newCount, entry.sbn.notification) 156 }!!.unreadCount 157 onNotificationPanelExpandStateChangednull158 fun onNotificationPanelExpandStateChanged(isCollapsed: Boolean) { 159 notifPanelCollapsed = isCollapsed 160 if (isCollapsed) return 161 162 // When the notification panel is expanded, reset the counters of any expanded 163 // conversations 164 val expanded = states 165 .asSequence() 166 .mapNotNull { (key, _) -> 167 notificationEntryManager.getActiveNotificationUnfiltered(key) 168 ?.let { entry -> 169 if (entry.row?.isExpanded == true) key to entry 170 else null 171 } 172 } 173 .toMap() 174 states.replaceAll { key, state -> 175 if (expanded.contains(key)) state.copy(unreadCount = 0) 176 else state 177 } 178 // Update UI separate from the replaceAll call, since ConcurrentHashMap may re-run the 179 // lambda if threads are in contention. 180 expanded.values.asSequence().mapNotNull { it.row }.forEach(::resetBadgeUi) 181 } 182 resetCountnull183 private fun resetCount(key: String) { 184 states.compute(key) { _, state -> state?.copy(unreadCount = 0) } 185 } 186 removeTrackedEntrynull187 private fun removeTrackedEntry(entry: NotificationEntry) { 188 states.remove(entry.key) 189 } 190 resetBadgeUinull191 private fun resetBadgeUi(row: ExpandableNotificationRow): Unit = 192 (row.layouts?.asSequence() ?: emptySequence()) 193 .flatMap { layout -> layout.allViews.asSequence() } viewnull194 .mapNotNull { view -> view as? ConversationLayout } convoLayoutnull195 .forEach { convoLayout -> convoLayout.setUnreadCount(0) } 196 197 private data class ConversationState(val unreadCount: Int, val notification: Notification) 198 199 companion object { 200 private const val IMPORTANCE_ANIMATION_DELAY = 201 StackStateAnimator.ANIMATION_DURATION_STANDARD + 202 StackStateAnimator.ANIMATION_DURATION_PRIORITY_CHANGE + 203 100 204 } 205 } 206