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