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