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