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