1 /*
<lambda>null2 * Copyright (C) 2023 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.stack.ui.view
18
19 import android.service.notification.NotificationListenerService
20 import androidx.annotation.VisibleForTesting
21 import com.android.app.tracing.coroutines.TrackTracer
22 import com.android.app.tracing.coroutines.launchTraced as launch
23 import com.android.internal.statusbar.IStatusBarService
24 import com.android.internal.statusbar.NotificationVisibility
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Application
27 import com.android.systemui.dagger.qualifiers.Background
28 import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger
29 import com.android.systemui.statusbar.notification.logging.nano.Notifications
30 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
31 import com.android.systemui.statusbar.notification.stack.ExpandableViewState
32 import java.util.concurrent.Callable
33 import java.util.concurrent.ConcurrentHashMap
34 import javax.inject.Inject
35 import kotlinx.coroutines.CoroutineDispatcher
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.channels.BufferOverflow
38 import kotlinx.coroutines.channels.Channel
39 import kotlinx.coroutines.channels.consumeEach
40 import kotlinx.coroutines.withContext
41
42 @VisibleForTesting const val UNKNOWN_RANK = -1
43
44 @SysUISingleton
45 class NotificationStatsLoggerImpl
46 @Inject
47 constructor(
48 @Application private val applicationScope: CoroutineScope,
49 @Background private val bgDispatcher: CoroutineDispatcher,
50 private val notificationListenerService: NotificationListenerService,
51 private val notificationPanelLogger: NotificationPanelLogger,
52 private val statusBarService: IStatusBarService,
53 ) : NotificationStatsLogger {
54 private val expansionStates: MutableMap<String, ExpansionState> =
55 ConcurrentHashMap<String, ExpansionState>()
56 @VisibleForTesting
57 val lastReportedExpansionValues: MutableMap<String, Boolean> =
58 ConcurrentHashMap<String, Boolean>()
59
60 private val visibilityLogger =
61 Channel<VisibilityAction>(capacity = 2, onBufferOverflow = BufferOverflow.DROP_OLDEST)
62
63 init {
64 applicationScope.launch { consumeVisibilityActions() }
65 }
66
67 private suspend fun consumeVisibilityActions() {
68 val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>()
69
70 visibilityLogger.consumeEach { action ->
71 val newVisibilities =
72 when (action) {
73 is VisibilityAction.Change -> action.visibilities
74 is VisibilityAction.Clear -> emptyMap()
75 }
76
77 val newlyVisible = newVisibilities - lastLoggedVisibilities.keys
78 val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys
79
80 maybeLogVisibilityChanges(newlyVisible, noLongerVisible, action.activeCount)
81 updateExpansionStates(newlyVisible, noLongerVisible)
82 TrackTracer.instantForGroup("Notifications", "Active", action.activeCount)
83 TrackTracer.instantForGroup("Notifications", "Visible", newVisibilities.size)
84
85 lastLoggedVisibilities.clear()
86 lastLoggedVisibilities.putAll(newVisibilities)
87 }
88 }
89
90 override fun onNotificationLocationsChanged(
91 locationsProvider: Callable<Map<String, Int>>,
92 notificationRanks: Map<String, Int>,
93 ) {
94 visibilityLogger.trySend(
95 VisibilityAction.Change(
96 visibilities =
97 combine(
98 visibilities = locationsProvider.call(),
99 rankingsMap = notificationRanks,
100 ),
101 activeCount = notificationRanks.size,
102 )
103 )
104 }
105
106 override fun onNotificationExpansionChanged(
107 key: String,
108 isExpanded: Boolean,
109 location: Int,
110 isUserAction: Boolean,
111 ) {
112 val expansionState =
113 ExpansionState(
114 key = key,
115 isExpanded = isExpanded,
116 isUserAction = isUserAction,
117 location = location,
118 )
119 expansionStates[key] = expansionState
120 maybeLogNotificationExpansionChange(expansionState)
121 }
122
123 private fun maybeLogNotificationExpansionChange(expansionState: ExpansionState) {
124 if (expansionState.visible.not()) {
125 // Only log visible expansion changes
126 return
127 }
128
129 val loggedExpansionValue: Boolean? = lastReportedExpansionValues[expansionState.key]
130 if (loggedExpansionValue == null && !expansionState.isExpanded) {
131 // Consider the Notification initially collapsed, so only expanded is logged in the
132 // first time.
133 return
134 }
135
136 if (loggedExpansionValue != null && loggedExpansionValue == expansionState.isExpanded) {
137 // We have already logged this state, don't log it again
138 return
139 }
140
141 logNotificationExpansionChange(expansionState)
142 lastReportedExpansionValues[expansionState.key] = expansionState.isExpanded
143 }
144
145 private fun logNotificationExpansionChange(expansionState: ExpansionState) =
146 applicationScope.launch {
147 withContext(bgDispatcher) {
148 statusBarService.onNotificationExpansionChanged(
149 /* key = */ expansionState.key,
150 /* userAction = */ expansionState.isUserAction,
151 /* expanded = */ expansionState.isExpanded,
152 /* notificationLocation = */ expansionState.location
153 .toNotificationLocation()
154 .ordinal,
155 )
156 }
157 }
158
159 override fun onLockscreenOrShadeInteractive(
160 isOnLockScreen: Boolean,
161 activeNotifications: List<ActiveNotificationModel>,
162 ) {
163 applicationScope.launch {
164 withContext(bgDispatcher) {
165 notificationPanelLogger.logPanelShown(
166 isOnLockScreen,
167 activeNotifications.toNotificationProto(),
168 )
169 }
170 }
171 }
172
173 override fun onLockscreenOrShadeNotInteractive(
174 activeNotifications: List<ActiveNotificationModel>
175 ) {
176 visibilityLogger.trySend(VisibilityAction.Clear(activeCount = activeNotifications.size))
177 }
178
179 override fun onNotificationRemoved(key: String) {
180 // No need to track expansion states for Notifications that are removed.
181 expansionStates.remove(key)
182 lastReportedExpansionValues.remove(key)
183 }
184
185 override fun onNotificationUpdated(key: String) {
186 // When the Notification is updated, we should consider it as not yet logged.
187 lastReportedExpansionValues.remove(key)
188 }
189
190 private fun combine(
191 visibilities: Map<String, Int>,
192 rankingsMap: Map<String, Int>,
193 ): Map<String, VisibilityState> =
194 visibilities.mapValues { entry ->
195 VisibilityState(entry.key, entry.value, rankingsMap[entry.key] ?: UNKNOWN_RANK)
196 }
197
198 private suspend fun maybeLogVisibilityChanges(
199 newlyVisible: Map<String, VisibilityState>,
200 noLongerVisible: Map<String, VisibilityState>,
201 activeNotifCount: Int,
202 ) {
203 if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
204 return
205 }
206
207 val newlyVisibleAr =
208 newlyVisible.mapToNotificationVisibilitiesAr(visible = true, count = activeNotifCount)
209
210 val noLongerVisibleAr =
211 noLongerVisible.mapToNotificationVisibilitiesAr(
212 visible = false,
213 count = activeNotifCount,
214 )
215
216 withContext(bgDispatcher) {
217 statusBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr)
218 if (newlyVisible.isNotEmpty()) {
219 notificationListenerService.setNotificationsShown(newlyVisible.keys.toTypedArray())
220 }
221 }
222 }
223
224 private fun updateExpansionStates(
225 newlyVisible: Map<String, VisibilityState>,
226 noLongerVisible: Map<String, VisibilityState>,
227 ) {
228 expansionStates.forEach { (key, expansionState) ->
229 if (newlyVisible.contains(key)) {
230 val newState =
231 expansionState.copy(
232 visible = true,
233 location = newlyVisible.getValue(key).location,
234 )
235 expansionStates[key] = newState
236 maybeLogNotificationExpansionChange(newState)
237 }
238
239 if (noLongerVisible.contains(key)) {
240 expansionStates[key] =
241 expansionState.copy(
242 visible = false,
243 location = noLongerVisible.getValue(key).location,
244 )
245 }
246 }
247 }
248
249 private sealed class VisibilityAction(open val activeCount: Int) {
250 data class Change(
251 val visibilities: Map<String, VisibilityState>,
252 override val activeCount: Int,
253 ) : VisibilityAction(activeCount)
254
255 data class Clear(override val activeCount: Int) : VisibilityAction(activeCount)
256 }
257
258 private data class VisibilityState(val key: String, val location: Int, val rank: Int)
259
260 private data class ExpansionState(
261 val key: String,
262 val isUserAction: Boolean,
263 val isExpanded: Boolean,
264 val visible: Boolean,
265 val location: Int,
266 ) {
267 constructor(
268 key: String,
269 isExpanded: Boolean,
270 location: Int,
271 isUserAction: Boolean,
272 ) : this(
273 key = key,
274 isExpanded = isExpanded,
275 isUserAction = isUserAction,
276 visible = isVisibleLocation(location),
277 location = location,
278 )
279 }
280
281 private fun Map<String, VisibilityState>.mapToNotificationVisibilitiesAr(
282 visible: Boolean,
283 count: Int,
284 ): Array<NotificationVisibility> =
285 this.map { (key, state) ->
286 NotificationVisibility.obtain(
287 /* key = */ key,
288 /* rank = */ state.rank,
289 /* count = */ count,
290 /* visible = */ visible,
291 /* location = */ state.location.toNotificationLocation(),
292 )
293 }
294 .toTypedArray()
295 }
296
toNotificationLocationnull297 private fun Int.toNotificationLocation(): NotificationVisibility.NotificationLocation {
298 return when (this) {
299 ExpandableViewState.LOCATION_FIRST_HUN ->
300 NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP
301 ExpandableViewState.LOCATION_HIDDEN_TOP ->
302 NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP
303 ExpandableViewState.LOCATION_MAIN_AREA ->
304 NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA
305 ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING ->
306 NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING
307 ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN ->
308 NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN
309 ExpandableViewState.LOCATION_GONE ->
310 NotificationVisibility.NotificationLocation.LOCATION_GONE
311 else -> NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN
312 }
313 }
314
toNotificationProtonull315 private fun List<ActiveNotificationModel>.toNotificationProto(): Notifications.NotificationList {
316 val notificationList = Notifications.NotificationList()
317 val protoArray: Array<Notifications.Notification> =
318 map { notification ->
319 Notifications.Notification().apply {
320 uid = notification.uid
321 packageName = notification.packageName
322 notification.instanceId?.let { instanceId = it.id }
323 // TODO(b/308623704) check if we can set groupInstanceId as well
324 isGroupSummary = notification.isGroupSummary
325 section = NotificationPanelLogger.toNotificationSection(notification.bucket)
326 }
327 }
328 .toTypedArray()
329
330 if (protoArray.isNotEmpty()) {
331 notificationList.notifications = protoArray
332 }
333
334 return notificationList
335 }
336
isVisibleLocationnull337 private fun isVisibleLocation(location: Int): Boolean =
338 location and ExpandableViewState.VISIBLE_LOCATIONS != 0
339