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