/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.app.ecm; import static android.annotation.SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TargetApi; import android.app.AppOpsManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Build; import android.os.RemoteException; import android.permission.flags.Flags; import android.util.ArraySet; import androidx.annotation.NonNull; import java.lang.annotation.Retention; /** * This class provides the core API for ECM (Enhanced Confirmation Mode). ECM is a feature that * restricts access to protected **settings** (i.e., sensitive resources) by restricted **apps** * (apps from from dangerous sources, such as sideloaded packages or packages downloaded from a web * browser). * *

Specifically, this class provides the ability to: * *

    *
  1. Check whether a setting is restricted from an app ({@link #isRestricted}) *
  2. Get an intent that will open the "Restricted setting" dialog ({@link * #createRestrictedSettingDialogIntent}) (a dialog that informs the user that the operation * they've attempted to perform is restricted) *
  3. Check whether an app is eligible to have its restriction status cleared ({@link * #isClearRestrictionAllowed}) *
  4. Clear an app's restriction status (i.e., un-restrict it). ({@link #clearRestriction}) *
* *

Methods of this class will generally accept an app (identified by a packageName and a user) * and a "setting" (a string representing the "sensitive resource") as arguments. ECM's exact * behavior will generally depend on what restriction state ECM considers each setting and app. For * example: * *

    *
  1. A setting may be considered by ECM to be either **protected** or **not protected**. In * general, this should be considered hardcoded into ECM's implementation: nothing can * "protect" or "unprotect" a setting. *
  2. An app may be considered as being **not restricted** or **restricted**. A restricted app * will be restricted from accessing all protected settings. Whether ECM considers any * particular app restricted is an implementation detail of ECM. However, the user is able to * clear any restricted app's restriction status (i.e, un-restrict it), after which ECM will * consider the app **not restricted**. *
* * Why is ECM needed? Consider the following (pre-ECM) scenario: * *
    *
  1. The user downloads and installs an apk file from a browser. *
  2. The user opens Settings -> Accessibility *
  3. The user tries to register the app as an accessibility service. *
  4. The user is shown a permission prompt "Allow _ to have full control of your device?" *
  5. The user clicks "Allow" *
  6. The downloaded app now has full control of the device. *
* * The purpose of ECM is to add more friction to this scenario. * *

With ECM, this scenario becomes: * *

    *
  1. The user downloads and installs an apk file from a browser. *
  2. The user goes into Settings -> Accessibility. *
  3. The user tries to register the app as an accessibility service. *
  4. The user is presented with a "Restricted setting" dialog explaining that the attempted * action has been restricted. (No "allow" button is shown, but a link is given to a screen * with intentionally-obscure instructions on how to proceed.) *
  5. The user must now navigate to Settings -> Apps -> [app] *
  6. The user then must click on "..." (top-right corner hamburger menu), then click "Allow * restricted settings" *
  7. The user goes (again) into Settings -> Accessibility and (again) tries to register the app * as an accessibility service. *
  8. The user is shown a permission prompt "Allow _ to have full control of your device?" *
  9. The user clicks "Allow" *
  10. The downloaded app now has full control of the device. *
* * And, expanding on the above scenario, the role that this class plays is as follows: * *
    *
  1. The user downloads and installs an apk file from a browser. *
  2. The user goes into Settings -> Accessibility. *

    **This screen then calls {@link #isRestricted}, which checks whether each app listed * on-screen is restricted from the accessibility service setting. It uses this to visually * "gray out" restricted apps.** *

  3. The user tries to register the app as an accessibility service. *

    **This screen then calls {@link #createRestrictedSettingDialogIntent} and starts the * intent. This opens the "Restricted setting" dialog.** *

  4. The user is presented with a "Restricted setting" dialog explaining that the attempted * action is restricted. (No "allow" button is shown, but a link is given to a screen with * intentionally-obscure instructions on how to proceed.) *

    **Upon opening, this dialog marks the app as eligible to have its restriction status * cleared.** *

  5. The user must now navigate to Settings -> Apps -> [app]. *

    **This screen calls {@link #isClearRestrictionAllowed} to check whether the app is * eligible to have its restriction status cleared. If this returns {@code true}, this screen * should then show a "Allow restricted setting" button inside the top-right hamburger menu.** *

  6. The user then must click on "..." (top-right corner hamburger menu), then click "Allow * restricted settings". *

    **In response, this screen should now call {@link #clearRestriction}.** *

  7. The user goes (again) into Settings -> Accessibility and (again) tries to register the app * as an accessibility service. *
  8. The user is shown a permission prompt "Allow _ to have full control of your device?" *
  9. The user clicks "Allow" *
  10. The downloaded app now has full control of the device. *
* * @hide */ @SystemApi @FlaggedApi(Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED) @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @SystemService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE) public final class EnhancedConfirmationManager { /* * At the API level, we use the following terminology: * * - The capability of an app to access a setting may be considered (by ECM) to be *restricted* * or *not restricted*. * - A setting may be considered (by ECM) to be *protected* or *not protected*. * - The state of an app may be considered (by ECM) to be *restricted* or *not restricted* * * In this implementation, however, the state of an app is considered either **guarded** or * **not guarded**; these terms can generally be considered synonymous with **restricted** and * **not restricted**. (Keeping in mind that, the capability of any app to access any * non-protected setting will always be considered "not restricted", even if the state of the * app is considered "restricted".). An app can also be in a third state: **guarded and * acknowledged**, which corresponds with an app that is restricted and is eligible to have its * restriction status cleared. * * Currently, the ECM state of any given app is stored in the OP_ACCESS_RESTRICTED_SETTINGS * appop (though this may change in the future): * * - MODE_ALLOWED means the app is explicitly **not guarded**. (U- default) * - MODE_ERRORED means the app is explicitly **guarded**. (Only settable in U-.) * - MODE_IGNORED means the app is explicitly **guarded and acknowledged**. (An app enters this * state as soon as the "Restricted setting" dialog has been shown to the user. If an app is * in this state, Settings is now allowed to provide the user with the option to clear the * restriction.) * - MODE_DEFAULT means the app's ECM state should be decided lazily. (V+ default) (That is, * each time a caller checks whether or not an app is considered guarded by ECM, we'll run an * heuristic to determine this.) * * Some notes on compatibility: * * - On U-, MODE_ALLOWED is the default mode of OP_ACCESS_RESTRICTED_SETTINGS. On both U- and * V+, this is also the mode after the app's restriction has been cleared. * - In U-, the mode needed to be explicitly set (for example, by a browser that allows a * dangerous app to be installed) to MODE_ERRORED to indicate that an app is guarded. In V+, * we no longer allow an app to be placed into MODE_ERRORED, but for compatibility, we still * recognize MODE_ERRORED to indicate that an app is explicitly guarded. * - In V+, the default mode is MODE_DEFAULT. Unlike U-, this potentially affects *all* apps, * not just the ones which have been explicitly marked as **guarded**. * * Regarding ECM "setting"s: a setting may be any abstract resource identified by a string. ECM * may consider any particular setting **protected** or **not protected**. For now, the set of * protected settings is hardcoded, but this may evolve in the future. * * TODO(b/320512579): These methods currently enforce UPDATE_APP_OPS_STATS, * UPDATE_APP_OPS_STATS, and, for setter methods, MANAGE_APP_OPS_MODES. We should add * RequiresPermission annotations, but we can't, because some of these permissions are hidden * API. Either upgrade these to SystemApi or enforce a different permission, then add the * appropriate RequiresPermission annotation. */ /** * Shows the "Restricted setting" dialog. Opened when a setting is blocked. */ @SdkConstant(BROADCAST_INTENT_ACTION) public static final String ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG = "android.app.ecm.action.SHOW_ECM_RESTRICTED_SETTING_DIALOG"; /** A map of ECM states to their corresponding app op states */ @Retention(java.lang.annotation.RetentionPolicy.SOURCE) @IntDef(prefix = {"ECM_STATE_"}, value = {EcmState.ECM_STATE_NOT_GUARDED, EcmState.ECM_STATE_GUARDED, EcmState.ECM_STATE_GUARDED_AND_ACKNOWLEDGED, EcmState.ECM_STATE_IMPLICIT}) private @interface EcmState { int ECM_STATE_NOT_GUARDED = AppOpsManager.MODE_ALLOWED; int ECM_STATE_GUARDED = AppOpsManager.MODE_ERRORED; int ECM_STATE_GUARDED_AND_ACKNOWLEDGED = AppOpsManager.MODE_IGNORED; int ECM_STATE_IMPLICIT = AppOpsManager.MODE_DEFAULT; } private static final String LOG_TAG = EnhancedConfirmationManager.class.getSimpleName(); private static final ArraySet PROTECTED_SETTINGS = new ArraySet<>(); static { PROTECTED_SETTINGS.add(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE); // TODO(b/310654015): Add other explicitly protected settings } private final @NonNull Context mContext; private final PackageManager mPackageManager; private final @NonNull IEnhancedConfirmationManager mService; /** * @hide */ public EnhancedConfirmationManager(@NonNull Context context, @NonNull IEnhancedConfirmationManager service) { mContext = context; mPackageManager = context.getPackageManager(); mService = service; } /** * Check whether a setting is restricted from an app. * *

This is {@code true} when the setting is a protected setting (i.e., a sensitive resource), * and the app is restricted (i.e., considered dangerous), and the user has not yet cleared the * app's restriction status (i.e., by clicking "Allow restricted settings" for this app). * * @param packageName package name of the application to check for * @param settingIdentifier identifier of the resource to check to check for * @return {@code true} if the setting is restricted from the app * @throws NameNotFoundException if the provided package was not found */ @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public boolean isRestricted(@NonNull String packageName, @NonNull String settingIdentifier) throws NameNotFoundException { try { return mService.isRestricted(packageName, settingIdentifier, mContext.getUser().getIdentifier()); } catch (IllegalArgumentException e) { throw new NameNotFoundException(packageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Clear an app's restriction status (i.e., un-restrict it). * *

After this is called, the app will no longer be restricted from accessing any protected * setting by ECM. This method should be called when the user clicks "Allow restricted settings" * for the app. * * @param packageName package name of the application to remove protection from * @throws NameNotFoundException if the provided package was not found */ @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public void clearRestriction(@NonNull String packageName) throws NameNotFoundException { try { mService.clearRestriction(packageName, mContext.getUser().getIdentifier()); } catch (IllegalArgumentException e) { throw new NameNotFoundException(packageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Check whether the provided app is eligible to have its restriction status cleared (i.e., the * app is restricted, and the "Restricted setting" dialog has been presented to the user). * *

The Settings UI should use method this to check whether to present the user with the * "Allow restricted settings" button. * * @param packageName package name of the application to check for * @return {@code true} if the settings UI should present the user with the ability to clear * restrictions from the provided app * @throws NameNotFoundException if the provided package was not found */ @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public boolean isClearRestrictionAllowed(@NonNull String packageName) throws NameNotFoundException { try { return mService.isClearRestrictionAllowed(packageName, mContext.getUser().getIdentifier()); } catch (IllegalArgumentException e) { throw new NameNotFoundException(packageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Mark the app as eligible to have its restriction status cleared. * *

This should be called from the "Restricted setting" dialog (which {@link * #createRestrictedSettingDialogIntent} directs to) upon being presented to the user. * * @param packageName package name of the application which should be considered acknowledged * @throws NameNotFoundException if the provided package was not found */ @RequiresPermission(android.Manifest.permission.MANAGE_ENHANCED_CONFIRMATION_STATES) public void setClearRestrictionAllowed(@NonNull String packageName) throws NameNotFoundException { try { mService.setClearRestrictionAllowed(packageName, mContext.getUser().getIdentifier()); } catch (IllegalArgumentException e) { throw new NameNotFoundException(packageName); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Gets an intent that will open the "Restricted setting" dialog for the specified package * and setting. * *

The "Restricted setting" dialog is a dialog that informs the user that the operation * they've attempted to perform is restricted, and provides them with a link explaining how to * proceed. * * @param packageName package name of the restricted application * @param settingIdentifier identifier of the restricted setting * @throws NameNotFoundException if the provided package was not found */ public @NonNull Intent createRestrictedSettingDialogIntent(@NonNull String packageName, @NonNull String settingIdentifier) throws NameNotFoundException { Intent intent = new Intent(ACTION_SHOW_ECM_RESTRICTED_SETTING_DIALOG); intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName); intent.putExtra(Intent.EXTRA_UID, getPackageUid(packageName)); intent.putExtra(Intent.EXTRA_SUBJECT, settingIdentifier); return intent; } private int getPackageUid(String packageName) throws NameNotFoundException { return mPackageManager.getApplicationInfoAsUser(packageName, /* flags */ 0, mContext.getUser()).uid; } }