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