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