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