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