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.privacy 18 19 import android.Manifest 20 import android.app.ActivityManager 21 import android.app.Dialog 22 import android.content.ComponentName 23 import android.content.Context 24 import android.content.Intent 25 import android.content.pm.PackageManager 26 import android.location.LocationManager 27 import android.os.UserHandle 28 import android.permission.PermissionGroupUsage 29 import android.permission.PermissionManager 30 import androidx.annotation.MainThread 31 import androidx.annotation.WorkerThread 32 import androidx.core.view.isVisible 33 import com.android.internal.logging.UiEventLogger 34 import com.android.systemui.animation.DialogTransitionAnimator 35 import com.android.systemui.appops.AppOpsController 36 import com.android.systemui.dagger.SysUISingleton 37 import com.android.systemui.dagger.qualifiers.Background 38 import com.android.systemui.dagger.qualifiers.Main 39 import com.android.systemui.plugins.ActivityStarter 40 import com.android.systemui.privacy.logging.PrivacyLogger 41 import com.android.systemui.settings.UserTracker 42 import com.android.systemui.statusbar.policy.KeyguardStateController 43 import java.util.concurrent.Executor 44 import javax.inject.Inject 45 46 private val defaultDialogProvider = 47 object : PrivacyDialogControllerV2.DialogProvider { 48 override fun makeDialog( 49 context: Context, 50 list: List<PrivacyDialogV2.PrivacyElement>, 51 manageApp: (String, Int, Intent) -> Unit, 52 closeApp: (String, Int) -> Unit, 53 openPrivacyDashboard: () -> Unit 54 ): PrivacyDialogV2 { 55 return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) 56 } 57 } 58 59 /** 60 * Controller for [PrivacyDialogV2]. 61 * 62 * This controller shows and dismissed the dialog, as well as determining the information to show in 63 * it. 64 */ 65 @SysUISingleton 66 class PrivacyDialogControllerV2( 67 private val permissionManager: PermissionManager, 68 private val packageManager: PackageManager, 69 private val locationManager: LocationManager, 70 private val privacyItemController: PrivacyItemController, 71 private val userTracker: UserTracker, 72 private val activityStarter: ActivityStarter, 73 private val backgroundExecutor: Executor, 74 private val uiExecutor: Executor, 75 private val privacyLogger: PrivacyLogger, 76 private val keyguardStateController: KeyguardStateController, 77 private val appOpsController: AppOpsController, 78 private val uiEventLogger: UiEventLogger, 79 private val dialogTransitionAnimator: DialogTransitionAnimator, 80 private val dialogProvider: DialogProvider 81 ) { 82 83 @Inject 84 constructor( 85 permissionManager: PermissionManager, 86 packageManager: PackageManager, 87 locationManager: LocationManager, 88 privacyItemController: PrivacyItemController, 89 userTracker: UserTracker, 90 activityStarter: ActivityStarter, 91 @Background backgroundExecutor: Executor, 92 @Main uiExecutor: Executor, 93 privacyLogger: PrivacyLogger, 94 keyguardStateController: KeyguardStateController, 95 appOpsController: AppOpsController, 96 uiEventLogger: UiEventLogger, 97 dialogTransitionAnimator: DialogTransitionAnimator 98 ) : this( 99 permissionManager, 100 packageManager, 101 locationManager, 102 privacyItemController, 103 userTracker, 104 activityStarter, 105 backgroundExecutor, 106 uiExecutor, 107 privacyLogger, 108 keyguardStateController, 109 appOpsController, 110 uiEventLogger, 111 dialogTransitionAnimator, 112 defaultDialogProvider 113 ) 114 115 private var dialog: Dialog? = null 116 117 private val onDialogDismissed = 118 object : PrivacyDialogV2.OnDialogDismissed { onDialogDismissednull119 override fun onDialogDismissed() { 120 privacyLogger.logPrivacyDialogDismissed() 121 uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED) 122 dialog = null 123 } 124 } 125 126 @WorkerThread closeAppnull127 private fun closeApp(packageName: String, userId: Int) { 128 uiEventLogger.log( 129 PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP, 130 userId, 131 packageName 132 ) 133 privacyLogger.logCloseAppFromDialog(packageName, userId) 134 ActivityManager.getService().stopAppForUser(packageName, userId) 135 } 136 137 @MainThread manageAppnull138 private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) { 139 uiEventLogger.log( 140 PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS, 141 userId, 142 packageName 143 ) 144 privacyLogger.logStartSettingsActivityFromDialog(packageName, userId) 145 startActivity(navigationIntent) 146 } 147 148 @MainThread openPrivacyDashboardnull149 private fun openPrivacyDashboard() { 150 uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD) 151 privacyLogger.logStartPrivacyDashboardFromDialog() 152 startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE)) 153 } 154 155 @MainThread startActivitynull156 private fun startActivity(navigationIntent: Intent) { 157 if (!keyguardStateController.isUnlocked) { 158 // If we are locked, hide the dialog so the user can unlock 159 dialog?.hide() 160 } 161 // startActivity calls internally startActivityDismissingKeyguard 162 activityStarter.startActivity(navigationIntent, true) { 163 if (ActivityManager.isStartResultSuccessful(it)) { 164 dismissDialog() 165 } else { 166 dialog?.show() 167 } 168 } 169 } 170 171 @WorkerThread getStartViewPermissionUsageIntentnull172 private fun getStartViewPermissionUsageIntent( 173 context: Context, 174 packageName: String, 175 permGroupName: String, 176 attributionTag: CharSequence?, 177 isAttributionSupported: Boolean 178 ): Intent? { 179 // We should only limit this intent to location provider 180 if ( 181 attributionTag != null && 182 isAttributionSupported && 183 locationManager.isProviderPackage(null, packageName, attributionTag.toString()) 184 ) { 185 val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE) 186 intent.setPackage(packageName) 187 intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName) 188 intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString())) 189 intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true) 190 val resolveInfo = 191 packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0)) 192 if ( 193 resolveInfo?.activityInfo?.permission == 194 Manifest.permission.START_VIEW_PERMISSION_USAGE 195 ) { 196 intent.component = ComponentName(packageName, resolveInfo.activityInfo.name) 197 return intent 198 } 199 } 200 return null 201 } 202 getDefaultManageAppPermissionsIntentnull203 fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent { 204 val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS) 205 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) 206 intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId)) 207 return intent 208 } 209 210 @WorkerThread permGroupUsagenull211 private fun permGroupUsage(): List<PermissionGroupUsage> { 212 return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted) 213 } 214 215 /** 216 * Show the [PrivacyDialogV2] 217 * 218 * This retrieves the permission usage from [PermissionManager] and creates a new 219 * [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show. 220 * 221 * This list will be filtered by [filterAndSelect]. Only types available by 222 * [PrivacyItemController] will be shown. 223 * 224 * @param context A context to use to create the dialog. 225 * @see filterAndSelect 226 */ showDialognull227 fun showDialog(context: Context, privacyChip: OngoingPrivacyChip? = null) { 228 dismissDialog() 229 backgroundExecutor.execute { 230 val usage = permGroupUsage() 231 val userInfos = userTracker.userProfiles 232 privacyLogger.logUnfilteredPermGroupUsage(usage) 233 val items = 234 usage.mapNotNull { 235 val userInfo = 236 userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) } 237 if ( 238 isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall) 239 ) { 240 // Only try to get the app name if we actually need it 241 val appName = 242 if (it.isPhoneCall) { 243 "" 244 } else { 245 getLabelForPackage(it.packageName, it.uid) 246 } 247 val userId = UserHandle.getUserId(it.uid) 248 val viewUsageIntent = 249 getStartViewPermissionUsageIntent( 250 context, 251 it.packageName, 252 it.permissionGroupName, 253 it.attributionTag, 254 // attributionLabel is set only when subattribution policies 255 // are supported and satisfied 256 it.attributionLabel != null 257 ) 258 PrivacyDialogV2.PrivacyElement( 259 permGroupToPrivacyType(it.permissionGroupName)!!, 260 it.packageName, 261 userId, 262 appName, 263 it.attributionTag, 264 it.attributionLabel, 265 it.proxyLabel, 266 it.lastAccessTimeMillis, 267 it.isActive, 268 it.isPhoneCall, 269 viewUsageIntent != null, 270 it.permissionGroupName, 271 viewUsageIntent 272 ?: getDefaultManageAppPermissionsIntent(it.packageName, userId) 273 ) 274 } else { 275 null 276 } 277 } 278 uiExecutor.execute { 279 val elements = filterAndSelect(items) 280 if (elements.isNotEmpty()) { 281 val d = 282 dialogProvider.makeDialog( 283 context, 284 elements, 285 this::manageApp, 286 this::closeApp, 287 this::openPrivacyDashboard 288 ) 289 d.setShowForAllUsers(true) 290 d.addOnDismissListener(onDialogDismissed) 291 if (privacyChip != null) { 292 val controller = getPrivacyDialogController(privacyChip) 293 if (controller == null) { 294 d.show() 295 } else { 296 dialogTransitionAnimator.show(d, controller) 297 } 298 } else { 299 d.show() 300 } 301 privacyLogger.logShowDialogV2Contents(elements) 302 dialog = d 303 } else { 304 privacyLogger.logEmptyDialog() 305 } 306 } 307 } 308 } 309 getPrivacyDialogControllernull310 private fun getPrivacyDialogController( 311 source: OngoingPrivacyChip 312 ): DialogTransitionAnimator.Controller? { 313 val delegate = 314 DialogTransitionAnimator.Controller.fromView(source.launchableContentView) 315 ?: return null 316 return object : DialogTransitionAnimator.Controller by delegate { 317 override fun shouldAnimateExit() = source.isVisible 318 } 319 } 320 321 /** Dismisses the dialog */ dismissDialognull322 fun dismissDialog() { 323 dialog?.dismiss() 324 } 325 326 @WorkerThread getLabelForPackagenull327 private fun getLabelForPackage(packageName: String, uid: Int): CharSequence { 328 return try { 329 packageManager 330 .getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid)) 331 .loadLabel(packageManager) 332 } catch (_: PackageManager.NameNotFoundException) { 333 privacyLogger.logLabelNotFound(packageName) 334 packageName 335 } 336 } 337 permGroupToPrivacyTypenull338 private fun permGroupToPrivacyType(group: String): PrivacyType? { 339 return when (group) { 340 Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA 341 Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE 342 Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION 343 else -> null 344 } 345 } 346 isAvailablenull347 private fun isAvailable(group: String): Boolean { 348 return when (group) { 349 Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable 350 Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable 351 Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable 352 else -> false 353 } 354 } 355 356 /** 357 * Filters the list of elements to show. 358 * 359 * For each privacy type, it'll return all active elements. If there are no active elements, 360 * it'll return the most recent access 361 */ filterAndSelectnull362 private fun filterAndSelect( 363 list: List<PrivacyDialogV2.PrivacyElement> 364 ): List<PrivacyDialogV2.PrivacyElement> { 365 return list 366 .groupBy { it.type } 367 .toSortedMap() 368 .flatMap { (_, elements) -> 369 val actives = elements.filter { it.isActive } 370 if (actives.isNotEmpty()) { 371 actives.sortedByDescending { it.lastActiveTimestamp } 372 } else { 373 elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) } 374 ?: emptyList() 375 } 376 } 377 } 378 379 /** 380 * Interface to create a [PrivacyDialogV2]. 381 * 382 * Can be used to inject a mock creator. 383 */ 384 interface DialogProvider { 385 /** Create a [PrivacyDialogV2]. */ makeDialognull386 fun makeDialog( 387 context: Context, 388 list: List<PrivacyDialogV2.PrivacyElement>, 389 manageApp: (String, Int, Intent) -> Unit, 390 closeApp: (String, Int) -> Unit, 391 openPrivacyDashboard: () -> Unit 392 ): PrivacyDialogV2 393 } 394 } 395