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