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