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.app.Activity;
     20 import android.app.admin.DevicePolicyManager;
     21 import android.content.pm.ApplicationInfo;
     22 import android.content.pm.PackageManager;
     23 import android.content.pm.PackageManager.NameNotFoundException;
     24 import android.content.res.TypedArray;
     25 import android.content.res.XmlResourceParser;
     26 import android.os.Bundle;
     27 import android.os.PersistableBundle;
     28 import android.os.RemoteException;
     29 import android.service.restrictions.RestrictionsReceiver;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.util.Xml;
     33 
     34 import com.android.internal.R;
     35 import com.android.internal.util.XmlUtils;
     36 
     37 import org.xmlpull.v1.XmlPullParser;
     38 import org.xmlpull.v1.XmlPullParserException;
     39 
     40 import java.io.IOException;
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.List;
     44 
     45 /**
     46  * Provides a mechanism for apps to query restrictions imposed by an entity that
     47  * manages the user. Apps can also send permission requests to a local or remote
     48  * device administrator to override default app-specific restrictions or any other
     49  * operation that needs explicit authorization from the administrator.
     50  * <p>
     51  * Apps can expose a set of restrictions via an XML file specified in the manifest.
     52  * <p>
     53  * If the user has an active Restrictions Provider, dynamic requests can be made in
     54  * addition to the statically imposed restrictions. Dynamic requests are app-specific
     55  * and can be expressed via a predefined set of request types.
     56  * <p>
     57  * The RestrictionsManager forwards the dynamic requests to the active
     58  * Restrictions Provider. The Restrictions Provider can respond back to requests by calling
     59  * {@link #notifyPermissionResponse(String, PersistableBundle)}, when
     60  * a response is received from the administrator of the device or user.
     61  * The response is relayed back to the application via a protected broadcast,
     62  * {@link #ACTION_PERMISSION_RESPONSE_RECEIVED}.
     63  * <p>
     64  * Static restrictions are specified by an XML file referenced by a meta-data attribute
     65  * in the manifest. This enables applications as well as any web administration consoles
     66  * to be able to read the list of available restrictions from the apk.
     67  * <p>
     68  * The syntax of the XML format is as follows:
     69  * <pre>
     70  * &lt;?xml version="1.0" encoding="utf-8"?&gt;
     71  * &lt;restrictions xmlns:android="http://schemas.android.com/apk/res/android" &gt;
     72  *     &lt;restriction
     73  *         android:key="string"
     74  *         android:title="string resource"
     75  *         android:restrictionType=["bool" | "string" | "integer"
     76  *                                         | "choice" | "multi-select" | "hidden"
     77  *                                         | "bundle" | "bundle_array"]
     78  *         android:description="string resource"
     79  *         android:entries="string-array resource"
     80  *         android:entryValues="string-array resource"
     81  *         android:defaultValue="reference" &gt;
     82  *             &lt;restriction ... /&gt;
     83  *             ...
     84  *     &lt;/restriction&gt;
     85  *     &lt;restriction ... /&gt;
     86  *     ...
     87  * &lt;/restrictions&gt;
     88  * </pre>
     89  * <p>
     90  * The attributes for each restriction depend on the restriction type.
     91  * <p>
     92  * <ul>
     93  * <li><code>key</code>, <code>title</code> and <code>restrictionType</code> are mandatory.</li>
     94  * <li><code>entries</code> and <code>entryValues</code> are required if <code>restrictionType
     95  * </code> is <code>choice</code> or <code>multi-select</code>.</li>
     96  * <li><code>defaultValue</code> is optional and its type depends on the
     97  * <code>restrictionType</code></li>
     98  * <li><code>hidden</code> type must have a <code>defaultValue</code> and will
     99  * not be shown to the administrator. It can be used to pass along data that cannot be modified,
    100  * such as a version code.</li>
    101  * <li><code>description</code> is meant to describe the restriction in more detail to the
    102  * administrator controlling the values, if the title is not sufficient.</li>
    103  * </ul>
    104  * <p>
    105  * Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested
    106  * restriction elements.
    107  * <p>
    108  * In your manifest's <code>application</code> section, add the meta-data tag to point to
    109  * the restrictions XML file as shown below:
    110  * <pre>
    111  * &lt;application ... &gt;
    112  *     &lt;meta-data android:name="android.content.APP_RESTRICTIONS"
    113  *                   android:resource="@xml/app_restrictions" /&gt;
    114  *     ...
    115  * &lt;/application&gt;
    116  * </pre>
    117  *
    118  * @see RestrictionEntry
    119  * @see RestrictionsReceiver
    120  * @see DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName)
    121  * @see DevicePolicyManager#setApplicationRestrictions(ComponentName, String, Bundle)
    122  */
    123 public class RestrictionsManager {
    124 
    125     private static final String TAG = "RestrictionsManager";
    126 
    127     /**
    128      * Broadcast intent delivered when a response is received for a permission request. The
    129      * application should not interrupt the user by coming to the foreground if it isn't
    130      * currently in the foreground. It can either post a notification informing
    131      * the user of the response or wait until the next time the user launches the app.
    132      * <p>
    133      * For instance, if the user requested permission to make an in-app purchase,
    134      * the app can post a notification that the request had been approved or denied.
    135      * <p>
    136      * The broadcast Intent carries the following extra:
    137      * {@link #EXTRA_RESPONSE_BUNDLE}.
    138      */
    139     public static final String ACTION_PERMISSION_RESPONSE_RECEIVED =
    140             "android.content.action.PERMISSION_RESPONSE_RECEIVED";
    141 
    142     /**
    143      * Broadcast intent sent to the Restrictions Provider to handle a permission request from
    144      * an app. It will have the following extras: {@link #EXTRA_PACKAGE_NAME},
    145      * {@link #EXTRA_REQUEST_TYPE}, {@link #EXTRA_REQUEST_ID} and {@link #EXTRA_REQUEST_BUNDLE}.
    146      * The Restrictions Provider will handle the request and respond back to the
    147      * RestrictionsManager, when a response is available, by calling
    148      * {@link #notifyPermissionResponse}.
    149      * <p>
    150      * The BroadcastReceiver must require the {@link android.Manifest.permission#BIND_DEVICE_ADMIN}
    151      * permission to ensure that only the system can send the broadcast.
    152      */
    153     public static final String ACTION_REQUEST_PERMISSION =
    154             "android.content.action.REQUEST_PERMISSION";
    155 
    156     /**
    157      * Activity intent that is optionally implemented by the Restrictions Provider package
    158      * to challenge for an administrator PIN or password locally on the device. Apps will
    159      * call this intent using {@link Activity#startActivityForResult}. On a successful
    160      * response, {@link Activity#onActivityResult} will return a resultCode of
    161      * {@link Activity#RESULT_OK}.
    162      * <p>
    163      * The intent must contain {@link #EXTRA_REQUEST_BUNDLE} as an extra and the bundle must
    164      * contain at least {@link #REQUEST_KEY_MESSAGE} for the activity to display.
    165      * <p>
    166      * @see #createLocalApprovalIntent()
    167      */
    168     public static final String ACTION_REQUEST_LOCAL_APPROVAL =
    169             "android.content.action.REQUEST_LOCAL_APPROVAL";
    170 
    171     /**
    172      * The package name of the application making the request.
    173      * <p>
    174      * Type: String
    175      */
    176     public static final String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME";
    177 
    178     /**
    179      * The request type passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
    180      * <p>
    181      * Type: String
    182      */
    183     public static final String EXTRA_REQUEST_TYPE = "android.content.extra.REQUEST_TYPE";
    184 
    185     /**
    186      * The request ID passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
    187      * <p>
    188      * Type: String
    189      */
    190     public static final String EXTRA_REQUEST_ID = "android.content.extra.REQUEST_ID";
    191 
    192     /**
    193      * The request bundle passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast.
    194      * <p>
    195      * Type: {@link PersistableBundle}
    196      */
    197     public static final String EXTRA_REQUEST_BUNDLE = "android.content.extra.REQUEST_BUNDLE";
    198 
    199     /**
    200      * Contains a response from the administrator for specific request.
    201      * The bundle contains the following information, at least:
    202      * <ul>
    203      * <li>{@link #REQUEST_KEY_ID}: The request ID.</li>
    204      * <li>{@link #RESPONSE_KEY_RESULT}: The response result.</li>
    205      * </ul>
    206      * <p>
    207      * Type: {@link PersistableBundle}
    208      */
    209     public static final String EXTRA_RESPONSE_BUNDLE = "android.content.extra.RESPONSE_BUNDLE";
    210 
    211     /**
    212      * Request type for a simple question, with a possible title and icon.
    213      * <p>
    214      * Required keys are: {@link #REQUEST_KEY_MESSAGE}
    215      * <p>
    216      * Optional keys are
    217      * {@link #REQUEST_KEY_DATA}, {@link #REQUEST_KEY_ICON}, {@link #REQUEST_KEY_TITLE},
    218      * {@link #REQUEST_KEY_APPROVE_LABEL} and {@link #REQUEST_KEY_DENY_LABEL}.
    219      */
    220     public static final String REQUEST_TYPE_APPROVAL = "android.request.type.approval";
    221 
    222     /**
    223      * Key for request ID contained in the request bundle.
    224      * <p>
    225      * App-generated request ID to identify the specific request when receiving
    226      * a response. This value is returned in the {@link #EXTRA_RESPONSE_BUNDLE}.
    227      * <p>
    228      * Type: String
    229      */
    230     public static final String REQUEST_KEY_ID = "android.request.id";
    231 
    232     /**
    233      * Key for request data contained in the request bundle.
    234      * <p>
    235      * Optional, typically used to identify the specific data that is being referred to,
    236      * such as the unique identifier for a movie or book. This is not used for display
    237      * purposes and is more like a cookie. This value is returned in the
    238      * {@link #EXTRA_RESPONSE_BUNDLE}.
    239      * <p>
    240      * Type: String
    241      */
    242     public static final String REQUEST_KEY_DATA = "android.request.data";
    243 
    244     /**
    245      * Key for request title contained in the request bundle.
    246      * <p>
    247      * Optional, typically used as the title of any notification or dialog presented
    248      * to the administrator who approves the request.
    249      * <p>
    250      * Type: String
    251      */
    252     public static final String REQUEST_KEY_TITLE = "android.request.title";
    253 
    254     /**
    255      * Key for request message contained in the request bundle.
    256      * <p>
    257      * Required, shown as the actual message in a notification or dialog presented
    258      * to the administrator who approves the request.
    259      * <p>
    260      * Type: String
    261      */
    262     public static final String REQUEST_KEY_MESSAGE = "android.request.mesg";
    263 
    264     /**
    265      * Key for request icon contained in the request bundle.
    266      * <p>
    267      * Optional, shown alongside the request message presented to the administrator
    268      * who approves the request. The content must be a compressed image such as a
    269      * PNG or JPEG, as a byte array.
    270      * <p>
    271      * Type: byte[]
    272      */
    273     public static final String REQUEST_KEY_ICON = "android.request.icon";
    274 
    275     /**
    276      * Key for request approval button label contained in the request bundle.
    277      * <p>
    278      * Optional, may be shown as a label on the positive button in a dialog or
    279      * notification presented to the administrator who approves the request.
    280      * <p>
    281      * Type: String
    282      */
    283     public static final String REQUEST_KEY_APPROVE_LABEL = "android.request.approve_label";
    284 
    285     /**
    286      * Key for request rejection button label contained in the request bundle.
    287      * <p>
    288      * Optional, may be shown as a label on the negative button in a dialog or
    289      * notification presented to the administrator who approves the request.
    290      * <p>
    291      * Type: String
    292      */
    293     public static final String REQUEST_KEY_DENY_LABEL = "android.request.deny_label";
    294 
    295     /**
    296      * Key for issuing a new request, contained in the request bundle. If this is set to true,
    297      * the Restrictions Provider must make a new request. If it is false or not specified, then
    298      * the Restrictions Provider can return a cached response that has the same requestId, if
    299      * available. If there's no cached response, it will issue a new one to the administrator.
    300      * <p>
    301      * Type: boolean
    302      */
    303     public static final String REQUEST_KEY_NEW_REQUEST = "android.request.new_request";
    304 
    305     /**
    306      * Key for the response result in the response bundle sent to the application, for a permission
    307      * request. It indicates the status of the request. In some cases an additional message might
    308      * be available in {@link #RESPONSE_KEY_MESSAGE}, to be displayed to the user.
    309      * <p>
    310      * Type: int
    311      * <p>
    312      * Possible values: {@link #RESULT_APPROVED}, {@link #RESULT_DENIED},
    313      * {@link #RESULT_NO_RESPONSE}, {@link #RESULT_UNKNOWN_REQUEST} or
    314      * {@link #RESULT_ERROR}.
    315      */
    316     public static final String RESPONSE_KEY_RESULT = "android.response.result";
    317 
    318     /**
    319      * Response result value indicating that the request was approved.
    320      */
    321     public static final int RESULT_APPROVED = 1;
    322 
    323     /**
    324      * Response result value indicating that the request was denied.
    325      */
    326     public static final int RESULT_DENIED = 2;
    327 
    328     /**
    329      * Response result value indicating that the request has not received a response yet.
    330      */
    331     public static final int RESULT_NO_RESPONSE = 3;
    332 
    333     /**
    334      * Response result value indicating that the request is unknown, when it's not a new
    335      * request.
    336      */
    337     public static final int RESULT_UNKNOWN_REQUEST = 4;
    338 
    339     /**
    340      * Response result value indicating an error condition. Additional error code might be available
    341      * in the response bundle, for the key {@link #RESPONSE_KEY_ERROR_CODE}. There might also be
    342      * an associated error message in the response bundle, for the key
    343      * {@link #RESPONSE_KEY_MESSAGE}.
    344      */
    345     public static final int RESULT_ERROR = 5;
    346 
    347     /**
    348      * Error code indicating that there was a problem with the request.
    349      * <p>
    350      * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
    351      */
    352     public static final int RESULT_ERROR_BAD_REQUEST = 1;
    353 
    354     /**
    355      * Error code indicating that there was a problem with the network.
    356      * <p>
    357      * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
    358      */
    359     public static final int RESULT_ERROR_NETWORK = 2;
    360 
    361     /**
    362      * Error code indicating that there was an internal error.
    363      * <p>
    364      * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle.
    365      */
    366     public static final int RESULT_ERROR_INTERNAL = 3;
    367 
    368     /**
    369      * Key for the optional error code in the response bundle sent to the application.
    370      * <p>
    371      * Type: int
    372      * <p>
    373      * Possible values: {@link #RESULT_ERROR_BAD_REQUEST}, {@link #RESULT_ERROR_NETWORK} or
    374      * {@link #RESULT_ERROR_INTERNAL}.
    375      */
    376     public static final String RESPONSE_KEY_ERROR_CODE = "android.response.errorcode";
    377 
    378     /**
    379      * Key for the optional message in the response bundle sent to the application.
    380      * <p>
    381      * Type: String
    382      */
    383     public static final String RESPONSE_KEY_MESSAGE = "android.response.msg";
    384 
    385     /**
    386      * Key for the optional timestamp of when the administrator responded to the permission
    387      * request. It is an represented in milliseconds since January 1, 1970 00:00:00.0 UTC.
    388      * <p>
    389      * Type: long
    390      */
    391     public static final String RESPONSE_KEY_RESPONSE_TIMESTAMP = "android.response.timestamp";
    392 
    393     /**
    394      * Name of the meta-data entry in the manifest that points to the XML file containing the
    395      * application's available restrictions.
    396      * @see #getManifestRestrictions(String)
    397      */
    398     public static final String META_DATA_APP_RESTRICTIONS = "android.content.APP_RESTRICTIONS";
    399 
    400     private static final String TAG_RESTRICTION = "restriction";
    401 
    402     private final Context mContext;
    403     private final IRestrictionsManager mService;
    404 
    405     /**
    406      * @hide
    407      */
    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             Log.w(TAG, "Couldn't reach service");
    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             Log.w(TAG, "Couldn't reach service");
    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             Log.w(TAG, "Couldn't reach service");
    481         }
    482     }
    483 
    484     public Intent createLocalApprovalIntent() {
    485         try {
    486             if (mService != null) {
    487                 return mService.createLocalApprovalIntent();
    488             }
    489         } catch (RemoteException re) {
    490             Log.w(TAG, "Couldn't reach service");
    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             Log.w(TAG, "Couldn't reach service");
    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