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