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