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 android.app.ecm; 18 19 import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION; 20 21 import android.annotation.FlaggedApi; 22 import android.annotation.IntDef; 23 import android.annotation.RequiresPermission; 24 import android.annotation.SdkConstant; 25 import android.annotation.SystemApi; 26 import android.annotation.SystemService; 27 import android.annotation.TargetApi; 28 import android.app.AppOpsManager; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.pm.PackageManager; 32 import android.content.pm.PackageManager.NameNotFoundException; 33 import android.os.Build; 34 import android.os.RemoteException; 35 import android.permission.flags.Flags; 36 import android.util.ArraySet; 37 38 import androidx.annotation.NonNull; 39 40 import java.lang.annotation.Retention; 41 42 /** 43 * This class provides the core API for ECM (Enhanced Confirmation Mode). ECM is a feature that 44 * restricts access to protected **settings** (i.e., sensitive resources) by restricted **apps** 45 * (apps from from dangerous sources, such as sideloaded packages or packages downloaded from a web 46 * browser). 47 * 48 * <p>Specifically, this class provides the ability to: 49 * 50 * <ol> 51 * <li>Check whether a setting is restricted from an app ({@link #isRestricted}) 52 * <li>Get an intent that will open the "Restricted setting" dialog ({@link 53 * #createRestrictedSettingDialogIntent}) (a dialog that informs the user that the operation 54 * they've attempted to perform is restricted) 55 * <li>Check whether an app is eligible to have its restriction status cleared ({@link 56 * #isClearRestrictionAllowed}) 57 * <li>Clear an app's restriction status (i.e., un-restrict it). ({@link #clearRestriction}) 58 * </ol> 59 * 60 * <p>Methods of this class will generally accept an app (identified by a packageName and a user) 61 * and a "setting" (a string representing the "sensitive resource") as arguments. ECM's exact 62 * behavior will generally depend on what restriction state ECM considers each setting and app. For 63 * example: 64 * 65 * <ol> 66 * <li>A setting may be considered by ECM to be either **protected** or **not protected**. In 67 * general, this should be considered hardcoded into ECM's implementation: nothing can 68 * "protect" or "unprotect" a setting. 69 * <li>An app may be considered as being **not restricted** or **restricted**. A restricted app 70 * will be restricted from accessing all protected settings. Whether ECM considers any 71 * particular app restricted is an implementation detail of ECM. However, the user is able to 72 * clear any restricted app's restriction status (i.e, un-restrict it), after which ECM will 73 * consider the app **not restricted**. 74 * </ol> 75 * 76 * Why is ECM needed? Consider the following (pre-ECM) scenario: 77 * 78 * <ol> 79 * <li>The user downloads and installs an apk file from a browser. 80 * <li>The user opens Settings -> Accessibility 81 * <li>The user tries to register the app as an accessibility service. 82 * <li>The user is shown a permission prompt "Allow _ to have full control of your device?" 83 * <li>The user clicks "Allow" 84 * <li>The downloaded app now has full control of the device. 85 * </ol> 86 * 87 * The purpose of ECM is to add more friction to this scenario. 88 * 89 * <p>With ECM, this scenario becomes: 90 * 91 * <ol> 92 * <li>The user downloads and installs an apk file from a browser. 93 * <li>The user goes into Settings -> Accessibility. 94 * <li>The user tries to register the app as an accessibility service. 95 * <li>The user is presented with a "Restricted setting" dialog explaining that the attempted 96 * action has been restricted. (No "allow" button is shown, but a link is given to a screen 97 * with intentionally-obscure instructions on how to proceed.) 98 * <li>The user must now navigate to Settings -> Apps -> [app] 99 * <li>The user then must click on "..." (top-right corner hamburger menu), then click "Allow 100 * restricted settings" 101 * <li>The user goes (again) into Settings -> Accessibility and (again) tries to register the app 102 * as an accessibility service. 103 * <li>The user is shown a permission prompt "Allow _ to have full control of your device?" 104 * <li>The user clicks "Allow" 105 * <li>The downloaded app now has full control of the device. 106 * </ol> 107 * 108 * And, expanding on the above scenario, the role that this class plays is as follows: 109 * 110 * <ol> 111 * <li>The user downloads and installs an apk file from a browser. 112 * <li>The user goes into Settings -> Accessibility. 113 * <p>**This screen then calls {@link #isRestricted}, which checks whether each app listed 114 * on-screen is restricted from the accessibility service setting. It uses this to visually 115 * "gray out" restricted apps.** 116 * <li>The user tries to register the app as an accessibility service. 117 * <p>**This screen then calls {@link #createRestrictedSettingDialogIntent} and starts the 118 * intent. This opens the "Restricted setting" dialog.** 119 * <li>The user is presented with a "Restricted setting" dialog explaining that the attempted 120 * action is restricted. (No "allow" button is shown, but a link is given to a screen with 121 * intentionally-obscure instructions on how to proceed.) 122 * <p>**Upon opening, this dialog marks the app as eligible to have its restriction status 123 * cleared.** 124 * <li>The user must now navigate to Settings -> Apps -> [app]. 125 * <p>**This screen calls {@link #isClearRestrictionAllowed} to check whether the app is 126 * eligible to have its restriction status cleared. If this returns {@code true}, this screen 127 * should then show a "Allow restricted setting" button inside the top-right hamburger menu.** 128 * <li>The user then must click on "..." (top-right corner hamburger menu), then click "Allow 129 * restricted settings". 130 * <p>**In response, this screen should now call {@link #clearRestriction}.** 131 * <li>The user goes (again) into Settings -> Accessibility and (again) tries to register the app 132 * as an accessibility service. 133 * <li>The user is shown a permission prompt "Allow _ to have full control of your device?" 134 * <li>The user clicks "Allow" 135 * <li>The downloaded app now has full control of the device. 136 * </ol> 137 * 138 * @hide 139 */ 140 @SystemApi 141 @FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED) 142 @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) 143 @SystemService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE) 144 public final class EnhancedConfirmationManager { 145 /* 146 * At the API level, we use the following terminology: 147 * 148 * - The capability of an app to access a setting may be considered (by ECM) to be *restricted* 149 * or *not restricted*. 150 * - A setting may be considered (by ECM) to be *protected* or *not protected*. 151 * - The state of an app may be considered (by ECM) to be *restricted* or *not restricted* 152 * 153 * In this implementation, however, the state of an app is considered either **guarded** or 154 * **not guarded**; these terms can generally be considered synonymous with **restricted** and 155 * **not restricted**. (Keeping in mind that, the capability of any app to access any 156 * non-protected setting will always be considered "not restricted", even if the state of the 157 * app is considered "restricted".). An app can also be in a third state: **guarded and 158 * acknowledged**, which corresponds with an app that is restricted and is eligible to have its 159 * restriction status cleared. 160 * 161 * Currently, the ECM state of any given app is stored in the OP_ACCESS_RESTRICTED_SETTINGS 162 * appop (though this may change in the future): 163 * 164 * - MODE_ALLOWED means the app is explicitly **not guarded**. (U- default) 165 * - MODE_ERRORED means the app is explicitly **guarded**. (Only settable in U-.) 166 * - MODE_IGNORED means the app is explicitly **guarded and acknowledged**. (An app enters this 167 * state as soon as the "Restricted setting" dialog has been shown to the user. If an app is 168 * in this state, Settings is now allowed to provide the user with the option to clear the 169 * restriction.) 170 * - MODE_DEFAULT means the app's ECM state should be decided lazily. (V+ default) (That is, 171 * each time a caller checks whether or not an app is considered guarded by ECM, we'll run an 172 * heuristic to determine this.) 173 * 174 * Some notes on compatibility: 175 * 176 * - On U-, MODE_ALLOWED is the default mode of OP_ACCESS_RESTRICTED_SETTINGS. On both U- and 177 * V+, this is also the mode after the app's restriction has been cleared. 178 * - In U-, the mode needed to be explicitly set (for example, by a browser that allows a 179 * dangerous app to be installed) to MODE_ERRORED to indicate that an app is guarded. In V+, 180 * we no longer allow an app to be placed into MODE_ERRORED, but for compatibility, we still 181 * recognize MODE_ERRORED to indicate that an app is explicitly guarded. 182 * - In V+, the default mode is MODE_DEFAULT. Unlike U-, this potentially affects *all* apps, 183 * not just the ones which have been explicitly marked as **guarded**. 184 * 185 * Regarding ECM "setting"s: a setting may be any abstract resource identified by a string. ECM 186 * may consider any particular setting **protected** or **not protected**. For now, the set of 187 * protected settings is hardcoded, but this may evolve in the future. 188 * 189 * TODO(b/320512579): These methods currently enforce UPDATE_APP_OPS_STATS, 190 * UPDATE_APP_OPS_STATS, and, for setter methods, MANAGE_APP_OPS_MODES. We should add 191 * RequiresPermission annotations, but we can't, because some of these permissions are hidden 192 * API. Either upgrade these to SystemApi or enforce a different permission, then add the 193 * appropriate RequiresPermission annotation. 194 */ 195 196 /** 197 * Shows the "Restricted setting" dialog. Opened when a setting is blocked. 198 */ 199 @SdkConstant(BROADCAST_INTENT_ACTION) 200 public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG = 201 "android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG"; 202 203 /** A map of ECM states to their corresponding app op states */ 204 @Retention(java.lang.annotation.RetentionPolicy.SOURCE) 205 @IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED, 206 EcmState.ECM_STATE_GUARDED, EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED, 207 EcmState.ECM_STATE_IMPLICIT}) 208 private @interface EcmState { 209 int ECM_STATE_NOT_GUARDED = AppOpsManager.MODE_ALLOWED; 210 int ECM_STATE_GUARDED = AppOpsManager.MODE_ERRORED; 211 int ECM_STATE_GUARDED_AND_ACKNOWLEDGED = AppOpsManager.MODE_IGNORED; 212 int ECM_STATE_IMPLICIT = AppOpsManager.MODE_DEFAULT; 213 } 214 215 private static final String LOG_TAG = EnhancedConfirmationManager.class.getSimpleName(); 216 217 private static final ArraySet<String> PROTECTED_SETTINGS = new ArraySet<>(); 218 219 static { 220 PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE); 221 // TODO(b/310654015): Add other explicitly protected settings 222 } 223 224 private final @NonNull Context mContext; 225 private final PackageManager mPackageManager; 226 227 private final @NonNull IEnhancedConfirmationManager mService; 228 229 /** 230 * @hide 231 */ EnhancedConfirmationManager(@onNull Context context, @NonNull IEnhancedConfirmationManager service)232 public EnhancedConfirmationManager(@NonNull Context context, 233 @NonNull IEnhancedConfirmationManager service) { 234 mContext = context; 235 mPackageManager = context.getPackageManager(); 236 mService = service; 237 } 238 239 /** 240 * Check whether a setting is restricted from an app. 241 * 242 * <p>This is {@code true} when the setting is a protected setting (i.e., a sensitive resource), 243 * and the app is restricted (i.e., considered dangerous), and the user has not yet cleared the 244 * app's restriction status (i.e., by clicking "Allow restricted settings" for this app). 245 * 246 * @param packageName package name of the application to check for 247 * @param settingIdentifier identifier of the resource to check to check for 248 * @return {@code true} if the setting is restricted from the app 249 * @throws NameNotFoundException if the provided package was not found 250 */ 251 @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) isRestricted(@onNull String packageName, @NonNull String settingIdentifier)252 public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier) 253 throws NameNotFoundException { 254 try { 255 return mService.isRestricted(packageName, settingIdentifier, 256 mContext.getUser().getIdentifier()); 257 } catch (IllegalArgumentException e) { 258 throw new NameNotFoundException(packageName); 259 } catch (RemoteException e) { 260 throw e.rethrowFromSystemServer(); 261 } 262 } 263 264 /** 265 * Clear an app's restriction status (i.e., un-restrict it). 266 * 267 * <p>After this is called, the app will no longer be restricted from accessing any protected 268 * setting by ECM. This method should be called when the user clicks "Allow restricted settings" 269 * for the app. 270 * 271 * @param packageName package name of the application to remove protection from 272 * @throws NameNotFoundException if the provided package was not found 273 */ 274 @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) clearRestriction(@onNull String packageName)275 public void clearRestriction(@NonNull String packageName) throws NameNotFoundException { 276 try { 277 mService.clearRestriction(packageName, mContext.getUser().getIdentifier()); 278 } catch (IllegalArgumentException e) { 279 throw new NameNotFoundException(packageName); 280 } catch (RemoteException e) { 281 throw e.rethrowFromSystemServer(); 282 } 283 } 284 285 /** 286 * Check whether the provided app is eligible to have its restriction status cleared (i.e., the 287 * app is restricted, and the "Restricted setting" dialog has been presented to the user). 288 * 289 * <p>The Settings UI should use method this to check whether to present the user with the 290 * "Allow restricted settings" button. 291 * 292 * @param packageName package name of the application to check for 293 * @return {@code true} if the settings UI should present the user with the ability to clear 294 * restrictions from the provided app 295 * @throws NameNotFoundException if the provided package was not found 296 */ 297 @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) isClearRestrictionAllowed(@onNull String packageName)298 public boolean isClearRestrictionAllowed(@NonNull String packageName) 299 throws NameNotFoundException { 300 try { 301 return mService.isClearRestrictionAllowed(packageName, 302 mContext.getUser().getIdentifier()); 303 } catch (IllegalArgumentException e) { 304 throw new NameNotFoundException(packageName); 305 } catch (RemoteException e) { 306 throw e.rethrowFromSystemServer(); 307 } 308 } 309 310 /** 311 * Mark the app as eligible to have its restriction status cleared. 312 * 313 * <p>This should be called from the "Restricted setting" dialog (which {@link 314 * #createRestrictedSettingDialogIntent} directs to) upon being presented to the user. 315 * 316 * @param packageName package name of the application which should be considered acknowledged 317 * @throws NameNotFoundException if the provided package was not found 318 */ 319 @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) setClearRestrictionAllowed(@onNull String packageName)320 public void setClearRestrictionAllowed(@NonNull String packageName) 321 throws NameNotFoundException { 322 try { 323 mService.setClearRestrictionAllowed(packageName, mContext.getUser().getIdentifier()); 324 } catch (IllegalArgumentException e) { 325 throw new NameNotFoundException(packageName); 326 } catch (RemoteException e) { 327 throw e.rethrowFromSystemServer(); 328 } 329 } 330 331 /** 332 * Gets an intent that will open the "Restricted setting" dialog for the specified package 333 * and setting. 334 * 335 * <p>The "Restricted setting" dialog is a dialog that informs the user that the operation 336 * they've attempted to perform is restricted, and provides them with a link explaining how to 337 * proceed. 338 * 339 * @param packageName package name of the restricted application 340 * @param settingIdentifier identifier of the restricted setting 341 * @throws NameNotFoundException if the provided package was not found 342 */ createRestrictedSettingDialogIntent(@onNull String packageName, @NonNull String settingIdentifier)343 public @NonNull Intent createRestrictedSettingDialogIntent(@NonNull String packageName, 344 @NonNull String settingIdentifier) throws NameNotFoundException { 345 Intent intent = new Intent(ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG); 346 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); 347 intent.putExtra(Intent.EXTRA_UID, getPackageUid(packageName)); 348 intent.putExtra(Intent.EXTRA_SUBJECT, settingIdentifier); 349 return intent; 350 } 351 getPackageUid(String packageName)352 private int getPackageUid(String packageName) throws NameNotFoundException { 353 return mPackageManager.getApplicationInfoAsUser(packageName, /* flags */ 0, 354 mContext.getUser()).uid; 355 } 356 } 357