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