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