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