• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.permissioncontroller.ecm
18 
19 import android.annotation.SuppressLint
20 import android.app.AlertDialog
21 import android.app.AppOpsManager
22 import android.app.Dialog
23 import android.app.ecm.EnhancedConfirmationManager
24 import android.content.Context
25 import android.content.DialogInterface
26 import android.content.Intent
27 import android.content.pm.PackageManager
28 import android.os.Build
29 import android.os.Bundle
30 import android.os.Process
31 import android.os.UserHandle
32 import android.permission.flags.Flags
33 import android.text.Html
34 import android.text.method.LinkMovementMethod
35 import android.view.LayoutInflater
36 import android.view.View
37 import android.widget.TextView
38 import androidx.annotation.Keep
39 import androidx.annotation.RequiresApi
40 import androidx.fragment.app.DialogFragment
41 import androidx.fragment.app.FragmentActivity
42 import com.android.modules.utils.build.SdkLevel
43 import com.android.permissioncontroller.Constants.EXTRA_IS_ECM_IN_APP
44 import com.android.permissioncontroller.DeviceUtils
45 import com.android.permissioncontroller.R
46 import com.android.permissioncontroller.ecm.EnhancedConfirmationStatsLogUtils.DialogResult
47 import com.android.permissioncontroller.permission.ui.wear.WearEnhancedConfirmationDialogFragment
48 import com.android.permissioncontroller.permission.utils.KotlinUtils
49 import com.android.permissioncontroller.permission.utils.PermissionMapping
50 import com.android.permissioncontroller.permission.utils.Utils
51 import com.android.role.controller.model.Roles
52 
53 @Keep
54 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
55 class EnhancedConfirmationDialogActivity : FragmentActivity() {
56     companion object {
57         private const val KEY_WAS_CLEAR_RESTRICTION_ALLOWED = "KEY_WAS_CLEAR_RESTRICTION_ALLOWED"
58         private const val REASON_PHONE_STATE = "phone_state"
59     }
60 
61     private var wasClearRestrictionAllowed: Boolean = false
62     private var dialogResult: DialogResult = DialogResult.Cancelled
63 
64     override fun onCreate(savedInstanceState: Bundle?) {
65         super.onCreate(savedInstanceState)
66         if (!SdkLevel.isAtLeastV() || !Flags.enhancedConfirmationModeApisEnabled()) {
67             finish()
68             return
69         }
70 
71         if (savedInstanceState != null) {
72             wasClearRestrictionAllowed =
73                 savedInstanceState.getBoolean(KEY_WAS_CLEAR_RESTRICTION_ALLOWED)
74             return
75         }
76 
77         val uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID)
78         val packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
79         val settingIdentifier = intent.getStringExtra(Intent.EXTRA_SUBJECT)
80         val isEcmInApp = intent.getBooleanExtra(EXTRA_IS_ECM_IN_APP, false)
81         val reason = intent.getStringExtra(Intent.EXTRA_REASON)
82 
83         require(uid != Process.INVALID_UID) { "EXTRA_UID cannot be null or invalid" }
84         require(!packageName.isNullOrEmpty()) { "EXTRA_PACKAGE_NAME cannot be null or empty" }
85         require(!settingIdentifier.isNullOrEmpty()) { "EXTRA_SUBJECT cannot be null or empty" }
86         wasClearRestrictionAllowed =
87             setClearRestrictionAllowed(packageName, UserHandle.getUserHandleForUid(uid))
88 
89         val setting = Setting.fromIdentifier(this, settingIdentifier, isEcmInApp, reason)
90         if (
91             SettingType.fromIdentifier(this, settingIdentifier, isEcmInApp, reason) ==
92                 SettingType.BLOCKED_DUE_TO_PHONE_STATE &&
93                 !Flags.unknownCallPackageInstallBlockingEnabled()
94         ) {
95             finish()
96             return
97         }
98 
99         if (DeviceUtils.isWear(this)) {
100             WearEnhancedConfirmationDialogFragment.newInstance(setting.title, setting.message)
101                 .show(supportFragmentManager, WearEnhancedConfirmationDialogFragment.TAG)
102         } else {
103             EnhancedConfirmationDialogFragment.newInstance(setting.title, setting.message)
104                 .show(supportFragmentManager, EnhancedConfirmationDialogFragment.TAG)
105         }
106     }
107 
108     override fun onSaveInstanceState(outState: Bundle) {
109         super.onSaveInstanceState(outState)
110         outState.putBoolean(KEY_WAS_CLEAR_RESTRICTION_ALLOWED, wasClearRestrictionAllowed)
111     }
112 
113     @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
114     private fun setClearRestrictionAllowed(packageName: String, user: UserHandle): Boolean {
115         val userContext = createContextAsUser(user, 0)
116         val ecm = Utils.getSystemServiceSafe(userContext, EnhancedConfirmationManager::class.java)
117         try {
118             val wasClearRestrictionAllowed = ecm.isClearRestrictionAllowed(packageName)
119             ecm.setClearRestrictionAllowed(packageName)
120             return wasClearRestrictionAllowed
121         } catch (e: PackageManager.NameNotFoundException) {
122             throw IllegalArgumentException("unknown package: $packageName")
123         }
124     }
125 
126     private data class Setting(val title: String?, val message: CharSequence?) {
127         companion object {
128             fun fromIdentifier(
129                 context: Context,
130                 settingIdentifier: String,
131                 isEcmInApp: Boolean,
132                 reason: String?,
133             ): Setting {
134                 val settingType =
135                     SettingType.fromIdentifier(context, settingIdentifier, isEcmInApp, reason)
136                 val label =
137                     when (settingType) {
138                         SettingType.PLATFORM_PERMISSION ->
139                             KotlinUtils.getPermGroupLabel(
140                                 context,
141                                 PermissionMapping.getGroupOfPlatformPermission(settingIdentifier)!!,
142                             )
143                         SettingType.PLATFORM_PERMISSION_GROUP ->
144                             KotlinUtils.getPermGroupLabel(context, settingIdentifier)
145                         SettingType.ROLE ->
146                             context.getString(
147                                 Roles.get(context)[settingIdentifier]!!.shortLabelResource
148                             )
149                         SettingType.BLOCKED_DUE_TO_PHONE_STATE,
150                         SettingType.OTHER -> null
151                     }
152                 var title: String?
153                 var message: CharSequence?
154                 if (settingType == SettingType.BLOCKED_DUE_TO_PHONE_STATE) {
155                     title = settingType.titleRes?.let { context.getString(it) }
156                     val settingMessage = getPhoneStateSettingMessage(context, settingIdentifier)
157                     message = settingType.messageRes?.let { context.getString(it, settingMessage) }
158                 } else {
159                     val url =
160                         context.getString(R.string.help_url_action_disabled_by_restricted_settings)
161                     title = (settingType.titleRes?.let { context.getString(it, label) })
162                     message =
163                         settingType.messageRes?.let { Html.fromHtml(context.getString(it, url), 0) }
164                 }
165                 return Setting(title, message)
166             }
167 
168             private fun getPhoneStateSettingMessage(
169                 context: Context,
170                 settingsIdentifier: String,
171             ): String {
172                 return context.getString(
173                     when (settingsIdentifier) {
174                         AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE ->
175                             R.string.enhanced_confirmation_phone_state_dialog_a11y_desc
176                         AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES ->
177                             R.string.enhanced_confirmation_phone_state_dialog_install_desc
178                         else -> R.string.enhanced_confirmation_phone_state_dialog_generic_desc
179                     }
180                 )
181             }
182         }
183     }
184 
185     private enum class SettingType(val titleRes: Int?, val messageRes: Int?) {
186         PLATFORM_PERMISSION(
187             R.string.enhanced_confirmation_dialog_title_permission,
188             R.string.enhanced_confirmation_dialog_desc_permission,
189         ),
190         PLATFORM_PERMISSION_GROUP(
191             R.string.enhanced_confirmation_dialog_title_permission,
192             R.string.enhanced_confirmation_dialog_desc_permission,
193         ),
194         ROLE(
195             R.string.enhanced_confirmation_dialog_title_role,
196             R.string.enhanced_confirmation_dialog_desc_role,
197         ),
198         OTHER(
199             R.string.enhanced_confirmation_dialog_title_settings_default,
200             R.string.enhanced_confirmation_dialog_desc_settings_default,
201         ),
202         BLOCKED_DUE_TO_PHONE_STATE(
203             R.string.enhanced_confirmation_phone_state_dialog_title,
204             R.string.enhanced_confirmation_phone_state_dialog_desc,
205         );
206 
207         companion object {
208             fun fromIdentifier(
209                 context: Context,
210                 settingIdentifier: String,
211                 isEcmInApp: Boolean,
212                 restrictionReason: String?,
213             ): SettingType {
214                 return when {
215                     restrictionReason == REASON_PHONE_STATE -> BLOCKED_DUE_TO_PHONE_STATE
216                     !isEcmInApp -> OTHER
217                     PermissionMapping.isRuntimePlatformPermission(settingIdentifier) &&
218                         PermissionMapping.getGroupOfPlatformPermission(settingIdentifier) != null ->
219                         PLATFORM_PERMISSION
220                     PermissionMapping.isPlatformPermissionGroup(settingIdentifier) ->
221                         PLATFORM_PERMISSION_GROUP
222                     settingIdentifier.startsWith("android.app.role.") &&
223                         Roles.get(context).containsKey(settingIdentifier) -> ROLE
224                     else -> OTHER
225                 }
226             }
227         }
228     }
229 
230     fun onDialogResult(dialogResult: DialogResult) {
231         this.dialogResult = dialogResult
232         setResult(
233             RESULT_OK,
234             Intent().apply { putExtra(Intent.EXTRA_RETURN_RESULT, dialogResult.statsLogValue) },
235         )
236         finish()
237     }
238 
239     override fun onDestroy() {
240         super.onDestroy()
241         if (isFinishing) {
242             EnhancedConfirmationStatsLogUtils.logDialogResultReported(
243                 uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID),
244                 settingIdentifier = intent.getStringExtra(Intent.EXTRA_SUBJECT)!!,
245                 firstShowForApp = !wasClearRestrictionAllowed,
246                 dialogResult = dialogResult,
247             )
248         }
249     }
250 
251     class EnhancedConfirmationDialogFragment() : DialogFragment() {
252         companion object {
253             val TAG = EnhancedConfirmationDialogFragment::class.simpleName
254             private const val KEY_TITLE = "KEY_TITLE"
255             private const val KEY_MESSAGE = "KEY_MESSAGE"
256 
257             fun newInstance(title: String? = null, message: CharSequence? = null) =
258                 EnhancedConfirmationDialogFragment().apply {
259                     arguments =
260                         Bundle().apply {
261                             putString(KEY_TITLE, title)
262                             putCharSequence(KEY_MESSAGE, message)
263                         }
264                 }
265         }
266 
267         private lateinit var dialogActivity: EnhancedConfirmationDialogActivity
268 
269         override fun onAttach(context: Context) {
270             super.onAttach(context)
271             dialogActivity = context as EnhancedConfirmationDialogActivity
272         }
273 
274         override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
275             val title = arguments!!.getString(KEY_TITLE)
276             val message = arguments!!.getCharSequence(KEY_MESSAGE)
277 
278             return AlertDialog.Builder(dialogActivity)
279                 .setView(createDialogView(dialogActivity, title, message))
280                 .setPositiveButton(R.string.dialog_close) { _, _ ->
281                     dialogActivity.onDialogResult(DialogResult.Okay)
282                 }
283                 .create()
284         }
285 
286         override fun onCancel(dialog: DialogInterface) {
287             super.onCancel(dialog)
288             dialogActivity.onDialogResult(DialogResult.Cancelled)
289         }
290 
291         @SuppressLint("InflateParams")
292         private fun createDialogView(
293             context: Context,
294             title: String?,
295             message: CharSequence?,
296         ): View =
297             LayoutInflater.from(context)
298                 .inflate(R.layout.enhanced_confirmation_dialog, null)
299                 .apply {
300                     title?.let {
301                         requireViewById<TextView>(R.id.enhanced_confirmation_dialog_title).text = it
302                     }
303                     message?.let {
304                         val descTextView =
305                             requireViewById<TextView>(R.id.enhanced_confirmation_dialog_desc)
306                         descTextView.text = it
307                         descTextView.movementMethod = LinkMovementMethod.getInstance()
308                     }
309                 }
310     }
311 }
312