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.app.Activity;
20import android.app.admin.DevicePolicyManager;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.res.TypedArray;
25import android.content.res.XmlResourceParser;
26import android.os.Bundle;
27import android.os.PersistableBundle;
28import android.os.RemoteException;
29import android.service.restrictions.RestrictionsReceiver;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.util.Xml;
33
34import com.android.internal.R;
35import com.android.internal.util.XmlUtils;
36
37import org.xmlpull.v1.XmlPullParser;
38import org.xmlpull.v1.XmlPullParserException;
39
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.Arrays;
43import 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 */
123public 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     */
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     */
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     */
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     */
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
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     */
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     */
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
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
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
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     */
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
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