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