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