• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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