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