1 /*
2  * Copyright (C) 2014 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.content;
18 
19 import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM;
20 
21 import android.annotation.NonNull;
22 import android.annotation.SystemService;
23 import android.annotation.UserHandleAware;
24 import android.annotation.WorkerThread;
25 import android.app.Activity;
26 import android.app.admin.DevicePolicyManager;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.content.res.TypedArray;
32 import android.content.res.XmlResourceParser;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.os.PersistableBundle;
36 import android.os.RemoteException;
37 import android.os.UserManager;
38 import android.service.restrictions.RestrictionsReceiver;
39 import android.util.AttributeSet;
40 import android.util.Log;
41 import android.util.Xml;
42 
43 import com.android.internal.R;
44 import com.android.internal.util.XmlUtils;
45 
46 import org.xmlpull.v1.XmlPullParser;
47 import org.xmlpull.v1.XmlPullParserException;
48 
49 import java.io.IOException;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.List;
53 
54 /**
55  * Provides a mechanism for apps to query restrictions imposed by an entity that
56  * manages the user. Apps can also send permission requests to a local or remote
57  * device administrator to override default app-specific restrictions or any other
58  * operation that needs explicit authorization from the administrator.
59  * <p>
60  * Apps can expose a set of restrictions via an XML file specified in the manifest.
61  * <p>
62  * If the user has an active Restrictions Provider, dynamic requests can be made in
63  * addition to the statically imposed restrictions. Dynamic requests are app-specific
64  * and can be expressed via a predefined set of request types.
65  * <p>
66  * The RestrictionsManager forwards the dynamic requests to the active
67  * Restrictions Provider. The Restrictions Provider can respond back to requests by calling
68  * {@link #notifyPermissionResponse(String, PersistableBundle)}, when
69  * a response is received from the administrator of the device or user.
70  * The response is relayed back to the application via a protected broadcast,
71  * {@link #ACTION_PERMISSION_RESPONSE_RECEIVED}.
72  * <p>
73  * Static restrictions are specified by an XML file referenced by a meta-data attribute
74  * in the manifest. This enables applications as well as any web administration consoles
75  * to be able to read the list of available restrictions from the apk.
76  * <p>
77  * The syntax of the XML format is as follows:
78  * <pre>
79  * &lt;?xml version="1.0" encoding="utf-8"?&gt;
80  * &lt;restrictions xmlns:android="http://schemas.android.com/apk/res/android" &gt;
81  *     &lt;restriction
82  *         android:key="string"
83  *         android:title="string resource"
84  *         android:restrictionType=["bool" | "string" | "integer"
85  *                                         | "choice" | "multi-select" | "hidden"
86  *                                         | "bundle" | "bundle_array"]
87  *         android:description="string resource"
88  *         android:entries="string-array resource"
89  *         android:entryValues="string-array resource"
90  *         android:defaultValue="reference" &gt;
91  *             &lt;restriction ... /&gt;
92  *             ...
93  *     &lt;/restriction&gt;
94  *     &lt;restriction ... /&gt;
95  *     ...
96  * &lt;/restrictions&gt;
97  * </pre>
98  * <p>
99  * The attributes for each restriction depend on the restriction type.
100  * <p>
101  * <ul>
102  * <li><code>key</code>, <code>title</code> and <code>restrictionType</code> are mandatory.</li>
103  * <li><code>entries</code> and <code>entryValues</code> are required if <code>restrictionType
104  * </code> is <code>choice</code> or <code>multi-select</code>.</li>
105  * <li><code>defaultValue</code> is optional and its type depends on the
106  * <code>restrictionType</code></li>
107  * <li><code>hidden</code> type must have a <code>defaultValue</code> and will
108  * not be shown to the administrator. It can be used to pass along data that cannot be modified,
109  * such as a version code.</li>
110  * <li><code>description</code> is meant to describe the restriction in more detail to the
111  * administrator controlling the values, if the title is not sufficient.</li>
112  * </ul>
113  * <p>
114  * Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested
115  * restriction elements.
116  * <p>
117  * In your manifest's <code>application</code> section, add the meta-data tag to point to
118  * the restrictions XML file as shown below:
119  * <pre>
120  * &lt;application ... &gt;
121  *     &lt;meta-data android:name="android.content.APP_RESTRICTIONS"
122  *                   android:resource="@xml/app_restrictions" /&gt;
123  *     ...
124  * &lt;/application&gt;
125  * </pre>
126  *
127  * @see RestrictionEntry
128  * @see RestrictionsReceiver
129  * @see DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName)
130  * @see DevicePolicyManager#setApplicationRestrictions(ComponentName, String, Bundle)
131  */
132 @SystemService(Context.RESTRICTIONS_SERVICE)
133 public class RestrictionsManager {
134 
135     private static final String TAG = "RestrictionsManager";
136 
137     /**
138      * Broadcast intent delivered when a response is received for a permission request. The
139      * application should not interrupt the user by coming to the foreground if it isn't
140      * currently in the foreground. It can either post a notification informing
141      * the user of the response or wait until the next time the user launches the app.
142      * <p>
143      * For instance, if the user requested permission to make an in-app purchase,
144      * the app can post a notification that the request had been approved or denied.
145      * <p>
146      * The broadcast Intent carries the following extra:
147      * {@link #EXTRA_RESPONSE_BUNDLE}.
148      */
149     public static final String ACTION_PERMISSION_RESPONSE_RECEIVED =
150             "android.content.action.PERMISSION_RESPONSE_RECEIVED";
151 
152     /**
153      * Broadcast intent sent to the Restrictions Provider to handle a permission request from
154      * an app. It will have the following extras: {@link #EXTRA_PACKAGE_NAME},
155      * {@link #EXTRA_REQUEST_TYPE}, {@link #EXTRA_REQUEST_ID} and {@link #EXTRA_REQUEST_BUNDLE}.
156      * The Restrictions Provider will handle the request and respond back to the
157      * RestrictionsManager, when a response is available, by calling
158      * {@link #notifyPermissionResponse}.
159      * <p>
160      * The BroadcastReceiver must require the {@link android.Manifest.permission#BIND_DEVICE_ADMIN}
161      * permission to ensure that only the system can send the broadcast.
162      */
163     public static final String ACTION_REQUEST_PERMISSION =
164             "android.content.action.REQUEST_PERMISSION";
165 
166     /**
167      * Activity intent that is optionally implemented by the Restrictions Provider package
168      * to challenge for an administrator PIN or password locally on the device. Apps will
169      * call this intent using {@link Activity#startActivityForResult}. On a successful
170      * response, {@link Activity#onActivityResult} will return a resultCode of
171      * {@link Activity#RESULT_OK}.
172      * <p>
173      * The intent must contain {@link #EXTRA_REQUEST_BUNDLE} as an extra and the bundle must
174      * contain at least {@link #REQUEST_KEY_MESSAGE} for the activity to display.
175      * <p>
176      * @see #createLocalApprovalIntent()
177      */
178     public static final String ACTION_REQUEST_LOCAL_APPROVAL =
179             "android.content.action.REQUEST_LOCAL_APPROVAL";
180 
181     /**
182      * The package name of the application making the request.
183      * <p>
184      * Type: String
185      */
186     public static final String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME";
187 
188     /**
189      * The request type passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
190      * <p>
191      * Type: String
192      */
193     public static final String EXTRA_REQUEST_TYPE = "android.content.extra.REQUEST_TYPE";
194 
195     /**
196      * The request ID passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
197      * <p>
198      * Type: String
199      */
200     public static final String EXTRA_REQUEST_ID = "android.content.extra.REQUEST_ID";
201 
202     /**
203      * The request bundle passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
204      * <p>
205      * Type: {@link PersistableBundle}
206      */
207     public static final String EXTRA_REQUEST_BUNDLE = "android.content.extra.REQUEST_BUNDLE";
208 
209     /**
210      * Contains a response from the administrator for specific request.
211      * The bundle contains the following information, at least:
212      * <ul>
213      * <li>{@link #REQUEST_KEY_ID}: The request ID.</li>
214      * <li>{@link #RESPONSE_KEY_RESULT}: The response result.</li>
215      * </ul>
216      * <p>
217      * Type: {@link PersistableBundle}
218      */
219     public static final String EXTRA_RESPONSE_BUNDLE = "android.content.extra.RESPONSE_BUNDLE";
220 
221     /**
222      * Request type for a simple question, with a possible title and icon.
223      * <p>
224      * Required keys are: {@link #REQUEST_KEY_MESSAGE}
225      * <p>
226      * Optional keys are
227      * {@link #REQUEST_KEY_DATA}, {@link #REQUEST_KEY_ICON}, {@link #REQUEST_KEY_TITLE},
228      * {@link #REQUEST_KEY_APPROVE_LABEL} and {@link #REQUEST_KEY_DENY_LABEL}.
229      */
230     public static final String REQUEST_TYPE_APPROVAL = "android.request.type.approval";
231 
232     /**
233      * Key for request ID contained in the request bundle.
234      * <p>
235      * App-generated request ID to identify the specific request when receiving
236      * a response. This value is returned in the {@link #EXTRA_RESPONSE_BUNDLE}.
237      * <p>
238      * Type: String
239      */
240     public static final String REQUEST_KEY_ID = "android.request.id";
241 
242     /**
243      * Key for request data contained in the request bundle.
244      * <p>
245      * Optional, typically used to identify the specific data that is being referred to,
246      * such as the unique identifier for a movie or book. This is not used for display
247      * purposes and is more like a cookie. This value is returned in the
248      * {@link #EXTRA_RESPONSE_BUNDLE}.
249      * <p>
250      * Type: String
251      */
252     public static final String REQUEST_KEY_DATA = "android.request.data";
253 
254     /**
255      * Key for request title contained in the request bundle.
256      * <p>
257      * Optional, typically used as the title of any notification or dialog presented
258      * to the administrator who approves the request.
259      * <p>
260      * Type: String
261      */
262     public static final String REQUEST_KEY_TITLE = "android.request.title";
263 
264     /**
265      * Key for request message contained in the request bundle.
266      * <p>
267      * Required, shown as the actual message in a notification or dialog presented
268      * to the administrator who approves the request.
269      * <p>
270      * Type: String
271      */
272     public static final String REQUEST_KEY_MESSAGE = "android.request.mesg";
273 
274     /**
275      * Key for request icon contained in the request bundle.
276      * <p>
277      * Optional, shown alongside the request message presented to the administrator
278      * who approves the request. The content must be a compressed image such as a
279      * PNG or JPEG, as a byte array.
280      * <p>
281      * Type: byte[]
282      */
283     public static final String REQUEST_KEY_ICON = "android.request.icon";
284 
285     /**
286      * Key for request approval button label contained in the request bundle.
287      * <p>
288      * Optional, may be shown as a label on the positive button in a dialog or
289      * notification presented to the administrator who approves the request.
290      * <p>
291      * Type: String
292      */
293     public static final String REQUEST_KEY_APPROVE_LABEL = "android.request.approve_label";
294 
295     /**
296      * Key for request rejection button label contained in the request bundle.
297      * <p>
298      * Optional, may be shown as a label on the negative button in a dialog or
299      * notification presented to the administrator who approves the request.
300      * <p>
301      * Type: String
302      */
303     public static final String REQUEST_KEY_DENY_LABEL = "android.request.deny_label";
304 
305     /**
306      * Key for issuing a new request, contained in the request bundle. If this is set to true,
307      * the Restrictions Provider must make a new request. If it is false or not specified, then
308      * the Restrictions Provider can return a cached response that has the same requestId, if
309      * available. If there's no cached response, it will issue a new one to the administrator.
310      * <p>
311      * Type: boolean
312      */
313     public static final String REQUEST_KEY_NEW_REQUEST = "android.request.new_request";
314 
315     /**
316      * Key for the response result in the response bundle sent to the application, for a permission
317      * request. It indicates the status of the request. In some cases an additional message might
318      * be available in {@link #RESPONSE_KEY_MESSAGE}, to be displayed to the user.
319      * <p>
320      * Type: int
321      * <p>
322      * Possible values: {@link #RESULT_APPROVED}, {@link #RESULT_DENIED},
323      * {@link #RESULT_NO_RESPONSE}, {@link #RESULT_UNKNOWN_REQUEST} or
324      * {@link #RESULT_ERROR}.
325      */
326     public static final String RESPONSE_KEY_RESULT = "android.response.result";
327 
328     /**
329      * Response result value indicating that the request was approved.
330      */
331     public static final int RESULT_APPROVED = 1;
332 
333     /**
334      * Response result value indicating that the request was denied.
335      */
336     public static final int RESULT_DENIED = 2;
337 
338     /**
339      * Response result value indicating that the request has not received a response yet.
340      */
341     public static final int RESULT_NO_RESPONSE = 3;
342 
343     /**
344      * Response result value indicating that the request is unknown, when it's not a new
345      * request.
346      */
347     public static final int RESULT_UNKNOWN_REQUEST = 4;
348 
349     /**
350      * Response result value indicating an error condition. Additional error code might be available
351      * in the response bundle, for the key {@link #RESPONSE_KEY_ERROR_CODE}. There might also be
352      * an associated error message in the response bundle, for the key
353      * {@link #RESPONSE_KEY_MESSAGE}.
354      */
355     public static final int RESULT_ERROR = 5;
356 
357     /**
358      * Error code indicating that there was a problem with the request.
359      * <p>
360      * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
361      */
362     public static final int RESULT_ERROR_BAD_REQUEST = 1;
363 
364     /**
365      * Error code indicating that there was a problem with the network.
366      * <p>
367      * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
368      */
369     public static final int RESULT_ERROR_NETWORK = 2;
370 
371     /**
372      * Error code indicating that there was an internal error.
373      * <p>
374      * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
375      */
376     public static final int RESULT_ERROR_INTERNAL = 3;
377 
378     /**
379      * Key for the optional error code in the response bundle sent to the application.
380      * <p>
381      * Type: int
382      * <p>
383      * Possible values: {@link #RESULT_ERROR_BAD_REQUEST}, {@link #RESULT_ERROR_NETWORK} or
384      * {@link #RESULT_ERROR_INTERNAL}.
385      */
386     public static final String RESPONSE_KEY_ERROR_CODE = "android.response.errorcode";
387 
388     /**
389      * Key for the optional message in the response bundle sent to the application.
390      * <p>
391      * Type: String
392      */
393     public static final String RESPONSE_KEY_MESSAGE = "android.response.msg";
394 
395     /**
396      * Key for the optional timestamp of when the administrator responded to the permission
397      * request. It is an represented in milliseconds since January 1, 1970 00:00:00.0 UTC.
398      * <p>
399      * Type: long
400      */
401     public static final String RESPONSE_KEY_RESPONSE_TIMESTAMP = "android.response.timestamp";
402 
403     /**
404      * Name of the meta-data entry in the manifest that points to the XML file containing the
405      * application's available restrictions.
406      * @see #getManifestRestrictions(String)
407      */
408     public static final String META_DATA_APP_RESTRICTIONS = "android.content.APP_RESTRICTIONS";
409 
410     private static final String TAG_RESTRICTION = "restriction";
411 
412     private final Context mContext;
413     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
414     private final IRestrictionsManager mService;
415 
416     /**
417      * @hide
418      */
RestrictionsManager(Context context, IRestrictionsManager service)419     public RestrictionsManager(Context context, IRestrictionsManager service) {
420         mContext = context;
421         mService = service;
422     }
423 
424     /**
425      * Returns any available set of application-specific restrictions applicable
426      * to this application.
427      * @return the application restrictions as a Bundle. Returns null if there
428      * are no restrictions.
429      *
430      * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
431      * it is possible for there to be multiple managing apps on the device with the ability to set
432      * restrictions, e.g. a Device Policy Controller (DPC) and a Supervision admin.
433      * This API will only return the restrictions set by the DPCs. To retrieve restrictions
434      * set by all managing apps, use
435      * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead.
436      *
437      * @see DevicePolicyManager
438      */
getApplicationRestrictions()439     public Bundle getApplicationRestrictions() {
440         try {
441             if (mService != null) {
442                 return mService.getApplicationRestrictions(mContext.getPackageName());
443             }
444         } catch (RemoteException re) {
445             throw re.rethrowFromSystemServer();
446         }
447         return null;
448     }
449 
450     /**
451      * Returns a {@link List} containing a {@link Bundle} for each managing agent that has set
452      * restrictions for the current application, the bundle contains any application restrictions
453      * set for the current package. The order of the items in the list is not guaranteed to remain
454      * stable between multiple calls.
455      *
456      * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
457      * it is possible for there to be multiple managing apps on the device with the ability to set
458      * restrictions, e.g. an Enterprise Device Policy Controller (DPC) and a Supervision admin.
459      *
460      * <p>Each {@link Bundle} consists of key-value pairs, as defined by the application,
461      * where the types of values may be:
462      * <ul>
463      * <li>{@code boolean}
464      * <li>{@code int}
465      * <li>{@code String} or {@code String[]}
466      * <li>From {@link android.os.Build.VERSION_CODES#M}, {@code Bundle} or {@code Bundle[]}
467      * </ul>
468      *
469      * <p>NOTE: The method performs disk I/O and shouldn't be called on the main thread
470      *
471      * @return a {@link List} of {@link Bundle} containing the restrictions set by admins for that
472      * package. Returns an empty {@link List} if there are no saved restrictions.
473      *
474      * @see UserManager#KEY_RESTRICTIONS_PENDING
475      * @see DevicePolicyManager
476      */
477     @WorkerThread
478     @UserHandleAware
getApplicationRestrictionsPerAdmin()479     public @NonNull List<Bundle> getApplicationRestrictionsPerAdmin() {
480         try {
481             if (mService != null) {
482                 return mService.getApplicationRestrictionsPerAdminForUser(
483                         mContext.getUserId(), mContext.getPackageName());
484             }
485         } catch (RemoteException re) {
486             throw re.rethrowFromSystemServer();
487         }
488         return null;
489     }
490 
491     /**
492      * Called by an application to check if there is an active Restrictions Provider. If
493      * there isn't, {@link #requestPermission(String, String, PersistableBundle)} is not available.
494      *
495      * @return whether there is an active Restrictions Provider.
496      */
hasRestrictionsProvider()497     public boolean hasRestrictionsProvider() {
498         try {
499             if (mService != null) {
500                 return mService.hasRestrictionsProvider();
501             }
502         } catch (RemoteException re) {
503             throw re.rethrowFromSystemServer();
504         }
505         return false;
506     }
507 
508     /**
509      * Called by an application to request permission for an operation. The contents of the
510      * request are passed in a Bundle that contains several pieces of data depending on the
511      * chosen request type.
512      *
513      * @param requestType The type of request. The type could be one of the
514      * predefined types specified here or a custom type that the specific
515      * Restrictions Provider might understand. For custom types, the type name should be
516      * namespaced to avoid collisions with predefined types and types specified by
517      * other Restrictions Providers.
518      * @param requestId A unique id generated by the app that contains sufficient information
519      * to identify the parameters of the request when it receives the id in the response.
520      * @param request A PersistableBundle containing the data corresponding to the specified request
521      * type. The keys for the data in the bundle depend on the request type.
522      *
523      * @throws IllegalArgumentException if any of the required parameters are missing.
524      */
requestPermission(String requestType, String requestId, PersistableBundle request)525     public void requestPermission(String requestType, String requestId, PersistableBundle request) {
526         if (requestType == null) {
527             throw new NullPointerException("requestType cannot be null");
528         }
529         if (requestId == null) {
530             throw new NullPointerException("requestId cannot be null");
531         }
532         if (request == null) {
533             throw new NullPointerException("request cannot be null");
534         }
535         try {
536             if (mService != null) {
537                 mService.requestPermission(mContext.getPackageName(), requestType, requestId,
538                         request);
539             }
540         } catch (RemoteException re) {
541             throw re.rethrowFromSystemServer();
542         }
543     }
544 
createLocalApprovalIntent()545     public Intent createLocalApprovalIntent() {
546         Intent result = null;
547         try {
548             if (mService != null) {
549                 result = mService.createLocalApprovalIntent();
550                 if (result != null) {
551                     result.prepareToEnterProcess(LOCAL_FLAG_FROM_SYSTEM,
552                             mContext.getAttributionSource());
553                 }
554             }
555         } catch (RemoteException re) {
556             throw re.rethrowFromSystemServer();
557         }
558         return result;
559     }
560 
561     /**
562      * Called by the Restrictions Provider to deliver a response to an application.
563      *
564      * @param packageName the application to deliver the response to. Cannot be null.
565      * @param response the bundle containing the response status, request ID and other information.
566      *                 Cannot be null.
567      *
568      * @throws IllegalArgumentException if any of the required parameters are missing.
569      */
notifyPermissionResponse(String packageName, PersistableBundle response)570     public void notifyPermissionResponse(String packageName, PersistableBundle response) {
571         if (packageName == null) {
572             throw new NullPointerException("packageName cannot be null");
573         }
574         if (response == null) {
575             throw new NullPointerException("request cannot be null");
576         }
577         if (!response.containsKey(REQUEST_KEY_ID)) {
578             throw new IllegalArgumentException("REQUEST_KEY_ID must be specified");
579         }
580         if (!response.containsKey(RESPONSE_KEY_RESULT)) {
581             throw new IllegalArgumentException("RESPONSE_KEY_RESULT must be specified");
582         }
583         try {
584             if (mService != null) {
585                 mService.notifyPermissionResponse(packageName, response);
586             }
587         } catch (RemoteException re) {
588             throw re.rethrowFromSystemServer();
589         }
590     }
591 
592     /**
593      * Parse and return the list of restrictions defined in the manifest for the specified
594      * package, if any.
595      *
596      * @param packageName The application for which to fetch the restrictions list.
597      * @return The list of RestrictionEntry objects created from the XML file specified
598      * in the manifest, or null if none was specified.
599      */
getManifestRestrictions(String packageName)600     public List<RestrictionEntry> getManifestRestrictions(String packageName) {
601         ApplicationInfo appInfo = null;
602         try {
603             appInfo = mContext.getPackageManager().getApplicationInfo(packageName,
604                     PackageManager.GET_META_DATA);
605         } catch (NameNotFoundException pnfe) {
606             throw new IllegalArgumentException("No such package " + packageName);
607         }
608         if (appInfo == null || !appInfo.metaData.containsKey(META_DATA_APP_RESTRICTIONS)) {
609             return null;
610         }
611 
612         XmlResourceParser xml =
613                 appInfo.loadXmlMetaData(mContext.getPackageManager(), META_DATA_APP_RESTRICTIONS);
614         return loadManifestRestrictions(packageName, xml);
615     }
616 
loadManifestRestrictions(String packageName, XmlResourceParser xml)617     private List<RestrictionEntry> loadManifestRestrictions(String packageName,
618             XmlResourceParser xml) {
619         Context appContext;
620         try {
621             appContext = mContext.createPackageContext(packageName, 0 /* flags */);
622         } catch (NameNotFoundException nnfe) {
623             return null;
624         }
625         ArrayList<RestrictionEntry> restrictions = new ArrayList<>();
626         RestrictionEntry restriction;
627 
628         try {
629             int tagType = xml.next();
630             while (tagType != XmlPullParser.END_DOCUMENT) {
631                 if (tagType == XmlPullParser.START_TAG) {
632                     restriction = loadRestrictionElement(appContext, xml);
633                     if (restriction != null) {
634                         restrictions.add(restriction);
635                     }
636                 }
637                 tagType = xml.next();
638             }
639         } catch (XmlPullParserException e) {
640             Log.w(TAG, "Reading restriction metadata for " + packageName, e);
641             return null;
642         } catch (IOException e) {
643             Log.w(TAG, "Reading restriction metadata for " + packageName, e);
644             return null;
645         }
646 
647         return restrictions;
648     }
649 
loadRestrictionElement(Context appContext, XmlResourceParser xml)650     private RestrictionEntry loadRestrictionElement(Context appContext, XmlResourceParser xml)
651             throws IOException, XmlPullParserException {
652         if (xml.getName().equals(TAG_RESTRICTION)) {
653             AttributeSet attrSet = Xml.asAttributeSet(xml);
654             if (attrSet != null) {
655                 TypedArray a = appContext.obtainStyledAttributes(attrSet,
656                         com.android.internal.R.styleable.RestrictionEntry);
657                 return loadRestriction(appContext, a, xml);
658             }
659         }
660         return null;
661     }
662 
loadRestriction(Context appContext, TypedArray a, XmlResourceParser xml)663     private RestrictionEntry loadRestriction(Context appContext, TypedArray a, XmlResourceParser xml)
664             throws IOException, XmlPullParserException {
665         String key = a.getString(R.styleable.RestrictionEntry_key);
666         int restrictionType = a.getInt(
667                 R.styleable.RestrictionEntry_restrictionType, -1);
668         String title = a.getString(R.styleable.RestrictionEntry_title);
669         String description = a.getString(R.styleable.RestrictionEntry_description);
670         int entries = a.getResourceId(R.styleable.RestrictionEntry_entries, 0);
671         int entryValues = a.getResourceId(R.styleable.RestrictionEntry_entryValues, 0);
672 
673         if (restrictionType == -1) {
674             Log.w(TAG, "restrictionType cannot be omitted");
675             return null;
676         }
677 
678         if (key == null) {
679             Log.w(TAG, "key cannot be omitted");
680             return null;
681         }
682 
683         RestrictionEntry restriction = new RestrictionEntry(restrictionType, key);
684         restriction.setTitle(title);
685         restriction.setDescription(description);
686         if (entries != 0) {
687             restriction.setChoiceEntries(appContext, entries);
688         }
689         if (entryValues != 0) {
690             restriction.setChoiceValues(appContext, entryValues);
691         }
692         // Extract the default value based on the type
693         switch (restrictionType) {
694             case RestrictionEntry.TYPE_NULL: // hidden
695             case RestrictionEntry.TYPE_STRING:
696             case RestrictionEntry.TYPE_CHOICE:
697                 restriction.setSelectedString(
698                         a.getString(R.styleable.RestrictionEntry_defaultValue));
699                 break;
700             case RestrictionEntry.TYPE_INTEGER:
701                 restriction.setIntValue(
702                         a.getInt(R.styleable.RestrictionEntry_defaultValue, 0));
703                 break;
704             case RestrictionEntry.TYPE_MULTI_SELECT:
705                 int resId = a.getResourceId(R.styleable.RestrictionEntry_defaultValue, 0);
706                 if (resId != 0) {
707                     restriction.setAllSelectedStrings(
708                             appContext.getResources().getStringArray(resId));
709                 }
710                 break;
711             case RestrictionEntry.TYPE_BOOLEAN:
712                 restriction.setSelectedState(
713                         a.getBoolean(R.styleable.RestrictionEntry_defaultValue, false));
714                 break;
715             case RestrictionEntry.TYPE_BUNDLE:
716             case RestrictionEntry.TYPE_BUNDLE_ARRAY:
717                 final int outerDepth = xml.getDepth();
718                 List<RestrictionEntry> restrictionEntries = new ArrayList<>();
719                 while (XmlUtils.nextElementWithin(xml, outerDepth)) {
720                     RestrictionEntry childEntry = loadRestrictionElement(appContext, xml);
721                     if (childEntry == null) {
722                         Log.w(TAG, "Child entry cannot be loaded for bundle restriction " + key);
723                     } else {
724                         restrictionEntries.add(childEntry);
725                         if (restrictionType == RestrictionEntry.TYPE_BUNDLE_ARRAY
726                                 && childEntry.getType() != RestrictionEntry.TYPE_BUNDLE) {
727                             Log.w(TAG, "bundle_array " + key
728                                     + " can only contain entries of type bundle");
729                         }
730                     }
731                 }
732                 restriction.setRestrictions(restrictionEntries.toArray(new RestrictionEntry[
733                         restrictionEntries.size()]));
734                 break;
735             default:
736                 Log.w(TAG, "Unknown restriction type " + restrictionType);
737         }
738         return restriction;
739     }
740 
741     /**
742      * Converts a list of restrictions to the corresponding bundle, using the following mapping:
743      * <table>
744      *     <tr><th>RestrictionEntry</th><th>Bundle</th></tr>
745      *     <tr><td>{@link RestrictionEntry#TYPE_BOOLEAN}</td><td>{@link Bundle#putBoolean}</td></tr>
746      *     <tr><td>{@link RestrictionEntry#TYPE_CHOICE},
747      *     {@link RestrictionEntry#TYPE_MULTI_SELECT}</td>
748      *     <td>{@link Bundle#putStringArray}</td></tr>
749      *     <tr><td>{@link RestrictionEntry#TYPE_INTEGER}</td><td>{@link Bundle#putInt}</td></tr>
750      *     <tr><td>{@link RestrictionEntry#TYPE_STRING}</td><td>{@link Bundle#putString}</td></tr>
751      *     <tr><td>{@link RestrictionEntry#TYPE_BUNDLE}</td><td>{@link Bundle#putBundle}</td></tr>
752      *     <tr><td>{@link RestrictionEntry#TYPE_BUNDLE_ARRAY}</td>
753      *     <td>{@link Bundle#putParcelableArray}</td></tr>
754      * </table>
755      * @param entries list of restrictions
756      */
convertRestrictionsToBundle(List<RestrictionEntry> entries)757     public static Bundle convertRestrictionsToBundle(List<RestrictionEntry> entries) {
758         final Bundle bundle = new Bundle();
759         for (RestrictionEntry entry : entries) {
760             addRestrictionToBundle(bundle, entry);
761         }
762         return bundle;
763     }
764 
addRestrictionToBundle(Bundle bundle, RestrictionEntry entry)765     private static Bundle addRestrictionToBundle(Bundle bundle, RestrictionEntry entry) {
766         switch (entry.getType()) {
767             case RestrictionEntry.TYPE_BOOLEAN:
768                 bundle.putBoolean(entry.getKey(), entry.getSelectedState());
769                 break;
770             case RestrictionEntry.TYPE_CHOICE:
771             case RestrictionEntry.TYPE_CHOICE_LEVEL:
772             case RestrictionEntry.TYPE_MULTI_SELECT:
773                 bundle.putStringArray(entry.getKey(), entry.getAllSelectedStrings());
774                 break;
775             case RestrictionEntry.TYPE_INTEGER:
776                 bundle.putInt(entry.getKey(), entry.getIntValue());
777                 break;
778             case RestrictionEntry.TYPE_STRING:
779             case RestrictionEntry.TYPE_NULL:
780                 bundle.putString(entry.getKey(), entry.getSelectedString());
781                 break;
782             case RestrictionEntry.TYPE_BUNDLE:
783                 RestrictionEntry[] restrictions = entry.getRestrictions();
784                 Bundle childBundle = convertRestrictionsToBundle(Arrays.asList(restrictions));
785                 bundle.putBundle(entry.getKey(), childBundle);
786                 break;
787             case RestrictionEntry.TYPE_BUNDLE_ARRAY:
788                 RestrictionEntry[] bundleRestrictionArray = entry.getRestrictions();
789                 Bundle[] bundleArray = new Bundle[bundleRestrictionArray.length];
790                 for (int i = 0; i < bundleRestrictionArray.length; i++) {
791                     RestrictionEntry[] bundleRestrictions =
792                             bundleRestrictionArray[i].getRestrictions();
793                     if (bundleRestrictions == null) {
794                         // Non-bundle entry found in bundle array.
795                         Log.w(TAG, "addRestrictionToBundle: " +
796                                 "Non-bundle entry found in bundle array");
797                         bundleArray[i] = new Bundle();
798                     } else {
799                         bundleArray[i] = convertRestrictionsToBundle(Arrays.asList(
800                                 bundleRestrictions));
801                     }
802                 }
803                 bundle.putParcelableArray(entry.getKey(), bundleArray);
804                 break;
805             default:
806                 throw new IllegalArgumentException(
807                         "Unsupported restrictionEntry type: " + entry.getType());
808         }
809         return bundle;
810     }
811 
812 }
813