• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.permissioncontroller.safetycenter.ui
18 
19 import android.Manifest.permission_group.CAMERA as PERMISSION_GROUP_CAMERA
20 import android.Manifest.permission_group.LOCATION as PERMISSION_GROUP_LOCATION
21 import android.Manifest.permission_group.MICROPHONE as PERMISSION_GROUP_MICROPHONE
22 import android.content.Intent
23 import android.os.Build
24 import android.permission.PermissionGroupUsage
25 import android.permission.PermissionManager
26 import android.safetycenter.SafetyCenterEntry
27 import android.safetycenter.SafetyCenterIssue
28 import android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID
29 import android.safetycenter.SafetyCenterStatus
30 import android.safetycenter.config.SafetyCenterConfig
31 import android.safetycenter.config.SafetySource
32 import androidx.annotation.RequiresApi
33 import com.android.permissioncontroller.Constants
34 import com.android.permissioncontroller.PermissionControllerStatsLog
35 import com.android.permissioncontroller.PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ISSUE_STATE__ACTIVE
36 import com.android.permissioncontroller.PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ISSUE_STATE__DISMISSED
37 import com.android.permissioncontroller.PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ISSUE_STATE__ISSUE_STATE_UNKNOWN
38 import com.android.permissioncontroller.permission.utils.Utils
39 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants
40 import com.android.permissioncontroller.safetycenter.SafetyCenterConstants.EXTRA_SETTINGS_FRAGMENT_ARGS_KEY
41 import com.android.safetycenter.internaldata.SafetyCenterIds
42 import java.math.BigInteger
43 import java.security.MessageDigest
44 
45 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
46 class InteractionLogger
47 private constructor(
48     private val noLogSourceIds: Set<String?>
49 ) {
50     var sessionId: Long = Constants.INVALID_SESSION_ID
51     var viewType: ViewType = ViewType.UNKNOWN
52     var navigationSource: NavigationSource = NavigationSource.UNKNOWN
53     var navigationSensor: Sensor = Sensor.UNKNOWN
54     var groupId: String? = null
55 
56     private val viewedIssueIds: MutableSet<String> = mutableSetOf()
57 
58     constructor(
59         safetyCenterConfig: SafetyCenterConfig?
60     ) : this(extractNoLogSourceIds(safetyCenterConfig))
61 
recordnull62     fun record(action: Action) {
63         writeAtom(action)
64     }
65 
recordIssueViewednull66     fun recordIssueViewed(issue: SafetyCenterIssue, isDismissed: Boolean) {
67         if (viewedIssueIds.contains(issue.id)) {
68             return
69         }
70 
71         recordForIssue(Action.SAFETY_ISSUE_VIEWED, issue, isDismissed)
72         viewedIssueIds.add(issue.id)
73     }
74 
clearViewedIssuesnull75     fun clearViewedIssues() {
76         viewedIssueIds.clear()
77     }
78 
recordForIssuenull79     fun recordForIssue(action: Action, issue: SafetyCenterIssue, isDismissed: Boolean) {
80         val decodedId = SafetyCenterIds.issueIdFromString(issue.id)
81         writeAtom(
82             action,
83             LogSeverityLevel.fromIssueSeverityLevel(issue.severityLevel),
84             sourceId = decodedId.safetyCenterIssueKey.safetySourceId,
85             sourceProfileType =
86                 SafetySourceProfileType.fromUserId(decodedId.safetyCenterIssueKey.userId),
87             issueTypeId = decodedId.issueTypeId,
88             issueState =
89                 if (isDismissed) {
90                     SAFETY_CENTER_INTERACTION_REPORTED__ISSUE_STATE__DISMISSED
91                 } else {
92                     SAFETY_CENTER_INTERACTION_REPORTED__ISSUE_STATE__ACTIVE
93                 }
94         )
95     }
96 
recordForEntrynull97     fun recordForEntry(action: Action, entry: SafetyCenterEntry) {
98         val decodedId = SafetyCenterIds.entryIdFromString(entry.id)
99         writeAtom(
100             action,
101             LogSeverityLevel.fromEntrySeverityLevel(entry.severityLevel),
102             sourceId = decodedId.safetySourceId,
103             sourceProfileType = SafetySourceProfileType.fromUserId(decodedId.userId)
104         )
105     }
106 
recordForSensornull107     fun recordForSensor(action: Action, sensor: Sensor) {
108         writeAtom(action = action, sensor = sensor)
109     }
110 
writeAtomnull111     private fun writeAtom(
112         action: Action,
113         severityLevel: LogSeverityLevel = LogSeverityLevel.UNKNOWN,
114         sourceId: String? = null,
115         sourceProfileType: SafetySourceProfileType = SafetySourceProfileType.UNKNOWN,
116         issueTypeId: String? = null,
117         issueState: Int = SAFETY_CENTER_INTERACTION_REPORTED__ISSUE_STATE__ISSUE_STATE_UNKNOWN,
118         sensor: Sensor = Sensor.UNKNOWN,
119     ) {
120         if (noLogSourceIds.contains(sourceId)) {
121             return
122         }
123 
124         // WARNING: Be careful when logging severity levels. If the severity level being recorded
125         // is at all influenced by a logging-disallowed source, we should not record it. At the
126         // moment, we do not record overall severity levels in this atom, but leaving this note for
127         // future implementors.
128 
129         PermissionControllerStatsLog.write(
130             PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED,
131             sessionId,
132             action.statsLogValue,
133             viewType.statsLogValue,
134             navigationSource.statsLogValue,
135             severityLevel.statsLogValue,
136             encodeStringId(sourceId),
137             sourceProfileType.statsLogValue,
138             encodeStringId(issueTypeId),
139             (if (sensor != Sensor.UNKNOWN) sensor else navigationSensor).statsLogValue,
140             encodeStringId(groupId),
141             issueState
142         )
143     }
144 
145     private companion object {
146         /**
147          * Encodes a string into an long ID. The ID is a SHA-256 of the string, truncated to 64
148          * bits.
149          */
encodeStringIdnull150         private fun encodeStringId(id: String?): Long {
151             if (id == null) return 0
152 
153             val digest = MessageDigest.getInstance("MD5")
154             digest.update(id.toByteArray())
155 
156             // Truncate to the size of a long
157             return BigInteger(digest.digest()).toLong()
158         }
159 
extractNoLogSourceIdsnull160         private fun extractNoLogSourceIds(safetyCenterConfig: SafetyCenterConfig?): Set<String?> {
161             if (safetyCenterConfig == null) return setOf()
162 
163             return safetyCenterConfig.safetySourcesGroups
164                 .asSequence()
165                 .flatMap { it.safetySources }
166                 .filterNot { it.isLoggable() }
167                 .map { it.id }
168                 .toSet()
169         }
170 
isLoggablenull171         private fun SafetySource.isLoggable(): Boolean =
172             try {
173                 isLoggingAllowed
174             } catch (ex: UnsupportedOperationException) {
175                 // isLoggingAllowed will throw if you call it on a static source :(
176                 // Default to logging all sources that don't support this config value.
177                 true
178             }
179     }
180 }
181 
182 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
183 enum class Action(val statsLogValue: Int) {
184     UNKNOWN(
185         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ACTION__ACTION_UNKNOWN
186     ),
187     SAFETY_CENTER_VIEWED(
188         PermissionControllerStatsLog
189             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__SAFETY_CENTER_VIEWED
190     ),
191     SAFETY_ISSUE_VIEWED(
192         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ACTION__SAFETY_ISSUE_VIEWED
193     ),
194     SCAN_INITIATED(
195         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ACTION__SCAN_INITIATED
196     ),
197     ISSUE_PRIMARY_ACTION_CLICKED(
198         PermissionControllerStatsLog
199             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__ISSUE_PRIMARY_ACTION_CLICKED
200     ),
201     ISSUE_SECONDARY_ACTION_CLICKED(
202         PermissionControllerStatsLog
203             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__ISSUE_SECONDARY_ACTION_CLICKED
204     ),
205     ISSUE_DISMISS_CLICKED(
206         PermissionControllerStatsLog
207             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__ISSUE_DISMISS_CLICKED
208     ),
209     MORE_ISSUES_CLICKED(
210         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ACTION__MORE_ISSUES_CLICKED
211     ),
212     ENTRY_CLICKED(
213         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__ACTION__ENTRY_CLICKED
214     ),
215     ENTRY_ICON_ACTION_CLICKED(
216         PermissionControllerStatsLog
217             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__ENTRY_ICON_ACTION_CLICKED
218     ),
219     STATIC_ENTRY_CLICKED(
220         PermissionControllerStatsLog
221             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__STATIC_ENTRY_CLICKED
222     ),
223     PRIVACY_CONTROL_TOGGLE_CLICKED(
224         PermissionControllerStatsLog
225             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__PRIVACY_CONTROL_TOGGLE_CLICKED
226     ),
227     SENSOR_PERMISSION_REVOKE_CLICKED(
228         PermissionControllerStatsLog
229             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__SENSOR_PERMISSION_REVOKE_CLICKED
230     ),
231     SENSOR_PERMISSION_SEE_USAGES_CLICKED(
232         PermissionControllerStatsLog
233             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__SENSOR_PERMISSION_SEE_USAGES_CLICKED
234     ),
235     REVIEW_SETTINGS_CLICKED(
236         PermissionControllerStatsLog
237             .SAFETY_CENTER_INTERACTION_REPORTED__ACTION__REVIEW_SETTINGS_CLICKED
238     )
239 }
240 
241 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
242 enum class ViewType(val statsLogValue: Int) {
243     UNKNOWN(
244         PermissionControllerStatsLog
245             .SAFETY_CENTER_INTERACTION_REPORTED__VIEW_TYPE__VIEW_TYPE_UNKNOWN
246     ),
247     FULL(PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__VIEW_TYPE__FULL),
248     QUICK_SETTINGS(
249         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__VIEW_TYPE__QUICK_SETTINGS
250     ),
251     SUBPAGE(PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__VIEW_TYPE__SUBPAGE)
252 }
253 
254 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
255 enum class NavigationSource(val statsLogValue: Int) {
256     UNKNOWN(
257         PermissionControllerStatsLog
258             .SAFETY_CENTER_INTERACTION_REPORTED__NAVIGATION_SOURCE__SOURCE_UNKNOWN
259     ),
260     NOTIFICATION(
261         PermissionControllerStatsLog
262             .SAFETY_CENTER_INTERACTION_REPORTED__NAVIGATION_SOURCE__NOTIFICATION
263     ),
264     QUICK_SETTINGS_TILE(
265         PermissionControllerStatsLog
266             .SAFETY_CENTER_INTERACTION_REPORTED__NAVIGATION_SOURCE__QUICK_SETTINGS_TILE
267     ),
268     SETTINGS(
269         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__NAVIGATION_SOURCE__SETTINGS
270     ),
271     SENSOR_INDICATOR(
272         PermissionControllerStatsLog
273             .SAFETY_CENTER_INTERACTION_REPORTED__NAVIGATION_SOURCE__SENSOR_INDICATOR
274     ),
275     SAFETY_CENTER(
276         PermissionControllerStatsLog
277             .SAFETY_CENTER_INTERACTION_REPORTED__NAVIGATION_SOURCE__SAFETY_CENTER
278     );
279 
addToIntentnull280     fun addToIntent(intent: Intent) {
281         intent.putExtra(SafetyCenterConstants.EXTRA_NAVIGATION_SOURCE, this.toString())
282     }
283 
284     companion object {
285         @JvmStatic
fromIntentnull286         fun fromIntent(intent: Intent): NavigationSource =
287             when (intent.action) {
288                 Intent.ACTION_SAFETY_CENTER -> fromSafetyCenterIntent(intent)
289                 Intent.ACTION_VIEW_SAFETY_CENTER_QS -> fromQuickSettingsIntent(intent)
290                 else -> UNKNOWN
291             }
292 
fromSafetyCenterIntentnull293         private fun fromSafetyCenterIntent(intent: Intent): NavigationSource {
294             val intentNavigationSource =
295                 intent.getStringExtra(SafetyCenterConstants.EXTRA_NAVIGATION_SOURCE)
296             val sourceIssueId = intent.getStringExtra(EXTRA_SAFETY_SOURCE_ISSUE_ID)
297             val searchKey = intent.getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)
298 
299             return if (sourceIssueId != null) {
300                 NOTIFICATION
301             } else if (searchKey != null) {
302                 SETTINGS
303             } else if (intentNavigationSource != null) {
304                 valueOf(intentNavigationSource)
305             } else {
306                 UNKNOWN
307             }
308         }
309 
fromQuickSettingsIntentnull310         private fun fromQuickSettingsIntent(intent: Intent): NavigationSource {
311             val usages =
312                 intent.getParcelableArrayListExtra(
313                     PermissionManager.EXTRA_PERMISSION_USAGES,
314                     PermissionGroupUsage::class.java
315                 )
316 
317             return if (usages != null && usages.isNotEmpty()) {
318                 SENSOR_INDICATOR
319             } else {
320                 QUICK_SETTINGS_TILE
321             }
322         }
323     }
324 }
325 
326 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
327 enum class LogSeverityLevel(val statsLogValue: Int) {
328     UNKNOWN(
329         PermissionControllerStatsLog
330             .SAFETY_CENTER_INTERACTION_REPORTED__SEVERITY_LEVEL__SAFETY_SEVERITY_LEVEL_UNKNOWN
331     ),
332     UNSPECIFIED(
333         PermissionControllerStatsLog
334             .SAFETY_CENTER_INTERACTION_REPORTED__SEVERITY_LEVEL__SAFETY_SEVERITY_UNSPECIFIED
335     ),
336     OK(
337         PermissionControllerStatsLog
338             .SAFETY_CENTER_INTERACTION_REPORTED__SEVERITY_LEVEL__SAFETY_SEVERITY_OK
339     ),
340     RECOMMENDATION(
341         PermissionControllerStatsLog
342             .SAFETY_CENTER_INTERACTION_REPORTED__SEVERITY_LEVEL__SAFETY_SEVERITY_RECOMMENDATION
343     ),
344     CRITICAL_WARNING(
345         PermissionControllerStatsLog
346             .SAFETY_CENTER_INTERACTION_REPORTED__SEVERITY_LEVEL__SAFETY_SEVERITY_CRITICAL_WARNING
347     );
348 
349     companion object {
350         @JvmStatic
fromOverallSeverityLevelnull351         fun fromOverallSeverityLevel(overallLevel: Int): LogSeverityLevel =
352             when (overallLevel) {
353                 SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_UNKNOWN -> UNKNOWN
354                 SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_OK -> OK
355                 SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_RECOMMENDATION -> RECOMMENDATION
356                 SafetyCenterStatus.OVERALL_SEVERITY_LEVEL_CRITICAL_WARNING -> CRITICAL_WARNING
357                 else -> UNKNOWN
358             }
359 
360         @JvmStatic
fromIssueSeverityLevelnull361         fun fromIssueSeverityLevel(issueLevel: Int): LogSeverityLevel =
362             when (issueLevel) {
363                 SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_OK -> OK
364                 SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_RECOMMENDATION -> RECOMMENDATION
365                 SafetyCenterIssue.ISSUE_SEVERITY_LEVEL_CRITICAL_WARNING -> CRITICAL_WARNING
366                 else -> UNKNOWN
367             }
368 
369         @JvmStatic
fromEntrySeverityLevelnull370         fun fromEntrySeverityLevel(entryLevel: Int): LogSeverityLevel =
371             when (entryLevel) {
372                 SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_OK -> OK
373                 SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_RECOMMENDATION -> RECOMMENDATION
374                 SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_CRITICAL_WARNING -> CRITICAL_WARNING
375                 SafetyCenterEntry.ENTRY_SEVERITY_LEVEL_UNSPECIFIED -> UNSPECIFIED
376                 else -> UNKNOWN
377             }
378     }
379 }
380 
381 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
382 enum class SafetySourceProfileType(val statsLogValue: Int) {
383     UNKNOWN(
384         PermissionControllerStatsLog
385             .SAFETY_CENTER_INTERACTION_REPORTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_UNKNOWN
386     ),
387     PERSONAL(
388         PermissionControllerStatsLog
389             .SAFETY_CENTER_INTERACTION_REPORTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_PERSONAL
390     ),
391     MANAGED(
392         PermissionControllerStatsLog
393             .SAFETY_CENTER_INTERACTION_REPORTED__SAFETY_SOURCE_PROFILE_TYPE__PROFILE_TYPE_MANAGED
394     );
395 
396     companion object {
397         @JvmStatic
fromUserIdnull398         fun fromUserId(userId: Int): SafetySourceProfileType =
399             if (Utils.isUserManagedProfile(userId)) MANAGED else PERSONAL
400     }
401 }
402 
403 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
404 enum class Sensor(val statsLogValue: Int) {
405     UNKNOWN(
406         PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__SENSOR__SENSOR_UNKNOWN
407     ),
408     MICROPHONE(PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__SENSOR__MICROPHONE),
409     CAMERA(PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__SENSOR__CAMERA),
410     LOCATION(PermissionControllerStatsLog.SAFETY_CENTER_INTERACTION_REPORTED__SENSOR__LOCATION);
411 
412     companion object {
413         @JvmStatic
414         fun fromIntent(intent: Intent): Sensor {
415             if (intent.action != Intent.ACTION_VIEW_SAFETY_CENTER_QS) return UNKNOWN
416 
417             val usages =
418                 intent.getParcelableArrayListExtra(
419                     PermissionManager.EXTRA_PERMISSION_USAGES,
420                     PermissionGroupUsage::class.java
421                 )
422 
423             // Multiple usages may be in effect, but we can only log one. Log unknown in this
424             // scenario until we have a better solution (an explicit value approved for
425             // logging).
426             if (usages != null && usages.size > 1) return UNKNOWN
427 
428             return fromPermissionGroupUsage(usages?.firstOrNull())
429         }
430 
431         @JvmStatic
432         fun fromPermissionGroupUsage(usage: PermissionGroupUsage?) =
433             fromPermissionGroupName(usage?.permissionGroupName)
434 
435         @JvmStatic
436         fun fromPermissionGroupName(permissionGroupName: String?) =
437             when (permissionGroupName) {
438                 PERMISSION_GROUP_CAMERA -> CAMERA
439                 PERMISSION_GROUP_MICROPHONE -> MICROPHONE
440                 PERMISSION_GROUP_LOCATION -> LOCATION
441                 else -> UNKNOWN
442             }
443     }
444 }
445