| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content; |
| |
| import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; |
| |
| import android.annotation.NonNull; |
| import android.annotation.SystemService; |
| import android.annotation.UserHandleAware; |
| import android.annotation.WorkerThread; |
| import android.app.Activity; |
| import android.app.admin.DevicePolicyManager; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.content.res.TypedArray; |
| import android.content.res.XmlResourceParser; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.PersistableBundle; |
| import android.os.RemoteException; |
| import android.os.UserManager; |
| import android.service.restrictions.RestrictionsReceiver; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.Xml; |
| |
| import com.android.internal.R; |
| import com.android.internal.util.XmlUtils; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Provides a mechanism for apps to query restrictions imposed by an entity that |
| * manages the user. Apps can also send permission requests to a local or remote |
| * device administrator to override default app-specific restrictions or any other |
| * operation that needs explicit authorization from the administrator. |
| * <p> |
| * Apps can expose a set of restrictions via an XML file specified in the manifest. |
| * <p> |
| * If the user has an active Restrictions Provider, dynamic requests can be made in |
| * addition to the statically imposed restrictions. Dynamic requests are app-specific |
| * and can be expressed via a predefined set of request types. |
| * <p> |
| * The RestrictionsManager forwards the dynamic requests to the active |
| * Restrictions Provider. The Restrictions Provider can respond back to requests by calling |
| * {@link #notifyPermissionResponse(String, PersistableBundle)}, when |
| * a response is received from the administrator of the device or user. |
| * The response is relayed back to the application via a protected broadcast, |
| * {@link #ACTION_PERMISSION_RESPONSE_RECEIVED}. |
| * <p> |
| * Static restrictions are specified by an XML file referenced by a meta-data attribute |
| * in the manifest. This enables applications as well as any web administration consoles |
| * to be able to read the list of available restrictions from the apk. |
| * <p> |
| * The syntax of the XML format is as follows: |
| * <pre> |
| * <?xml version="1.0" encoding="utf-8"?> |
| * <restrictions xmlns:android="http://schemas.android.com/apk/res/android" > |
| * <restriction |
| * android:key="string" |
| * android:title="string resource" |
| * android:restrictionType=["bool" | "string" | "integer" |
| * | "choice" | "multi-select" | "hidden" |
| * | "bundle" | "bundle_array"] |
| * android:description="string resource" |
| * android:entries="string-array resource" |
| * android:entryValues="string-array resource" |
| * android:defaultValue="reference" > |
| * <restriction ... /> |
| * ... |
| * </restriction> |
| * <restriction ... /> |
| * ... |
| * </restrictions> |
| * </pre> |
| * <p> |
| * The attributes for each restriction depend on the restriction type. |
| * <p> |
| * <ul> |
| * <li><code>key</code>, <code>title</code> and <code>restrictionType</code> are mandatory.</li> |
| * <li><code>entries</code> and <code>entryValues</code> are required if <code>restrictionType |
| * </code> is <code>choice</code> or <code>multi-select</code>.</li> |
| * <li><code>defaultValue</code> is optional and its type depends on the |
| * <code>restrictionType</code></li> |
| * <li><code>hidden</code> type must have a <code>defaultValue</code> and will |
| * not be shown to the administrator. It can be used to pass along data that cannot be modified, |
| * such as a version code.</li> |
| * <li><code>description</code> is meant to describe the restriction in more detail to the |
| * administrator controlling the values, if the title is not sufficient.</li> |
| * </ul> |
| * <p> |
| * Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested |
| * restriction elements. |
| * <p> |
| * In your manifest's <code>application</code> section, add the meta-data tag to point to |
| * the restrictions XML file as shown below: |
| * <pre> |
| * <application ... > |
| * <meta-data android:name="android.content.APP_RESTRICTIONS" |
| * android:resource="@xml/app_restrictions" /> |
| * ... |
| * </application> |
| * </pre> |
| * |
| * @see RestrictionEntry |
| * @see RestrictionsReceiver |
| * @see DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName) |
| * @see DevicePolicyManager#setApplicationRestrictions(ComponentName, String, Bundle) |
| */ |
| @SystemService(Context.RESTRICTIONS_SERVICE) |
| public class RestrictionsManager { |
| |
| private static final String TAG = "RestrictionsManager"; |
| |
| /** |
| * Broadcast intent delivered when a response is received for a permission request. The |
| * application should not interrupt the user by coming to the foreground if it isn't |
| * currently in the foreground. It can either post a notification informing |
| * the user of the response or wait until the next time the user launches the app. |
| * <p> |
| * For instance, if the user requested permission to make an in-app purchase, |
| * the app can post a notification that the request had been approved or denied. |
| * <p> |
| * The broadcast Intent carries the following extra: |
| * {@link #EXTRA_RESPONSE_BUNDLE}. |
| */ |
| public static final String ACTION_PERMISSION_RESPONSE_RECEIVED = |
| "android.content.action.PERMISSION_RESPONSE_RECEIVED"; |
| |
| /** |
| * Broadcast intent sent to the Restrictions Provider to handle a permission request from |
| * an app. It will have the following extras: {@link #EXTRA_PACKAGE_NAME}, |
| * {@link #EXTRA_REQUEST_TYPE}, {@link #EXTRA_REQUEST_ID} and {@link #EXTRA_REQUEST_BUNDLE}. |
| * The Restrictions Provider will handle the request and respond back to the |
| * RestrictionsManager, when a response is available, by calling |
| * {@link #notifyPermissionResponse}. |
| * <p> |
| * The BroadcastReceiver must require the {@link android.Manifest.permission#BIND_DEVICE_ADMIN} |
| * permission to ensure that only the system can send the broadcast. |
| */ |
| public static final String ACTION_REQUEST_PERMISSION = |
| "android.content.action.REQUEST_PERMISSION"; |
| |
| /** |
| * Activity intent that is optionally implemented by the Restrictions Provider package |
| * to challenge for an administrator PIN or password locally on the device. Apps will |
| * call this intent using {@link Activity#startActivityForResult}. On a successful |
| * response, {@link Activity#onActivityResult} will return a resultCode of |
| * {@link Activity#RESULT_OK}. |
| * <p> |
| * The intent must contain {@link #EXTRA_REQUEST_BUNDLE} as an extra and the bundle must |
| * contain at least {@link #REQUEST_KEY_MESSAGE} for the activity to display. |
| * <p> |
| * @see #createLocalApprovalIntent() |
| */ |
| public static final String ACTION_REQUEST_LOCAL_APPROVAL = |
| "android.content.action.REQUEST_LOCAL_APPROVAL"; |
| |
| /** |
| * The package name of the application making the request. |
| * <p> |
| * Type: String |
| */ |
| public static final String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME"; |
| |
| /** |
| * The request type passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast. |
| * <p> |
| * Type: String |
| */ |
| public static final String EXTRA_REQUEST_TYPE = "android.content.extra.REQUEST_TYPE"; |
| |
| /** |
| * The request ID passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast. |
| * <p> |
| * Type: String |
| */ |
| public static final String EXTRA_REQUEST_ID = "android.content.extra.REQUEST_ID"; |
| |
| /** |
| * The request bundle passed in the {@link #ACTION_REQUEST_PERMISSION} broadcast. |
| * <p> |
| * Type: {@link PersistableBundle} |
| */ |
| public static final String EXTRA_REQUEST_BUNDLE = "android.content.extra.REQUEST_BUNDLE"; |
| |
| /** |
| * Contains a response from the administrator for specific request. |
| * The bundle contains the following information, at least: |
| * <ul> |
| * <li>{@link #REQUEST_KEY_ID}: The request ID.</li> |
| * <li>{@link #RESPONSE_KEY_RESULT}: The response result.</li> |
| * </ul> |
| * <p> |
| * Type: {@link PersistableBundle} |
| */ |
| public static final String EXTRA_RESPONSE_BUNDLE = "android.content.extra.RESPONSE_BUNDLE"; |
| |
| /** |
| * Request type for a simple question, with a possible title and icon. |
| * <p> |
| * Required keys are: {@link #REQUEST_KEY_MESSAGE} |
| * <p> |
| * Optional keys are |
| * {@link #REQUEST_KEY_DATA}, {@link #REQUEST_KEY_ICON}, {@link #REQUEST_KEY_TITLE}, |
| * {@link #REQUEST_KEY_APPROVE_LABEL} and {@link #REQUEST_KEY_DENY_LABEL}. |
| */ |
| public static final String REQUEST_TYPE_APPROVAL = "android.request.type.approval"; |
| |
| /** |
| * Key for request ID contained in the request bundle. |
| * <p> |
| * App-generated request ID to identify the specific request when receiving |
| * a response. This value is returned in the {@link #EXTRA_RESPONSE_BUNDLE}. |
| * <p> |
| * Type: String |
| */ |
| public static final String REQUEST_KEY_ID = "android.request.id"; |
| |
| /** |
| * Key for request data contained in the request bundle. |
| * <p> |
| * Optional, typically used to identify the specific data that is being referred to, |
| * such as the unique identifier for a movie or book. This is not used for display |
| * purposes and is more like a cookie. This value is returned in the |
| * {@link #EXTRA_RESPONSE_BUNDLE}. |
| * <p> |
| * Type: String |
| */ |
| public static final String REQUEST_KEY_DATA = "android.request.data"; |
| |
| /** |
| * Key for request title contained in the request bundle. |
| * <p> |
| * Optional, typically used as the title of any notification or dialog presented |
| * to the administrator who approves the request. |
| * <p> |
| * Type: String |
| */ |
| public static final String REQUEST_KEY_TITLE = "android.request.title"; |
| |
| /** |
| * Key for request message contained in the request bundle. |
| * <p> |
| * Required, shown as the actual message in a notification or dialog presented |
| * to the administrator who approves the request. |
| * <p> |
| * Type: String |
| */ |
| public static final String REQUEST_KEY_MESSAGE = "android.request.mesg"; |
| |
| /** |
| * Key for request icon contained in the request bundle. |
| * <p> |
| * Optional, shown alongside the request message presented to the administrator |
| * who approves the request. The content must be a compressed image such as a |
| * PNG or JPEG, as a byte array. |
| * <p> |
| * Type: byte[] |
| */ |
| public static final String REQUEST_KEY_ICON = "android.request.icon"; |
| |
| /** |
| * Key for request approval button label contained in the request bundle. |
| * <p> |
| * Optional, may be shown as a label on the positive button in a dialog or |
| * notification presented to the administrator who approves the request. |
| * <p> |
| * Type: String |
| */ |
| public static final String REQUEST_KEY_APPROVE_LABEL = "android.request.approve_label"; |
| |
| /** |
| * Key for request rejection button label contained in the request bundle. |
| * <p> |
| * Optional, may be shown as a label on the negative button in a dialog or |
| * notification presented to the administrator who approves the request. |
| * <p> |
| * Type: String |
| */ |
| public static final String REQUEST_KEY_DENY_LABEL = "android.request.deny_label"; |
| |
| /** |
| * Key for issuing a new request, contained in the request bundle. If this is set to true, |
| * the Restrictions Provider must make a new request. If it is false or not specified, then |
| * the Restrictions Provider can return a cached response that has the same requestId, if |
| * available. If there's no cached response, it will issue a new one to the administrator. |
| * <p> |
| * Type: boolean |
| */ |
| public static final String REQUEST_KEY_NEW_REQUEST = "android.request.new_request"; |
| |
| /** |
| * Key for the response result in the response bundle sent to the application, for a permission |
| * request. It indicates the status of the request. In some cases an additional message might |
| * be available in {@link #RESPONSE_KEY_MESSAGE}, to be displayed to the user. |
| * <p> |
| * Type: int |
| * <p> |
| * Possible values: {@link #RESULT_APPROVED}, {@link #RESULT_DENIED}, |
| * {@link #RESULT_NO_RESPONSE}, {@link #RESULT_UNKNOWN_REQUEST} or |
| * {@link #RESULT_ERROR}. |
| */ |
| public static final String RESPONSE_KEY_RESULT = "android.response.result"; |
| |
| /** |
| * Response result value indicating that the request was approved. |
| */ |
| public static final int RESULT_APPROVED = 1; |
| |
| /** |
| * Response result value indicating that the request was denied. |
| */ |
| public static final int RESULT_DENIED = 2; |
| |
| /** |
| * Response result value indicating that the request has not received a response yet. |
| */ |
| public static final int RESULT_NO_RESPONSE = 3; |
| |
| /** |
| * Response result value indicating that the request is unknown, when it's not a new |
| * request. |
| */ |
| public static final int RESULT_UNKNOWN_REQUEST = 4; |
| |
| /** |
| * Response result value indicating an error condition. Additional error code might be available |
| * in the response bundle, for the key {@link #RESPONSE_KEY_ERROR_CODE}. There might also be |
| * an associated error message in the response bundle, for the key |
| * {@link #RESPONSE_KEY_MESSAGE}. |
| */ |
| public static final int RESULT_ERROR = 5; |
| |
| /** |
| * Error code indicating that there was a problem with the request. |
| * <p> |
| * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle. |
| */ |
| public static final int RESULT_ERROR_BAD_REQUEST = 1; |
| |
| /** |
| * Error code indicating that there was a problem with the network. |
| * <p> |
| * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle. |
| */ |
| public static final int RESULT_ERROR_NETWORK = 2; |
| |
| /** |
| * Error code indicating that there was an internal error. |
| * <p> |
| * Stored in {@link #RESPONSE_KEY_ERROR_CODE} field in the response bundle. |
| */ |
| public static final int RESULT_ERROR_INTERNAL = 3; |
| |
| /** |
| * Key for the optional error code in the response bundle sent to the application. |
| * <p> |
| * Type: int |
| * <p> |
| * Possible values: {@link #RESULT_ERROR_BAD_REQUEST}, {@link #RESULT_ERROR_NETWORK} or |
| * {@link #RESULT_ERROR_INTERNAL}. |
| */ |
| public static final String RESPONSE_KEY_ERROR_CODE = "android.response.errorcode"; |
| |
| /** |
| * Key for the optional message in the response bundle sent to the application. |
| * <p> |
| * Type: String |
| */ |
| public static final String RESPONSE_KEY_MESSAGE = "android.response.msg"; |
| |
| /** |
| * Key for the optional timestamp of when the administrator responded to the permission |
| * request. It is an represented in milliseconds since January 1, 1970 00:00:00.0 UTC. |
| * <p> |
| * Type: long |
| */ |
| public static final String RESPONSE_KEY_RESPONSE_TIMESTAMP = "android.response.timestamp"; |
| |
| /** |
| * Name of the meta-data entry in the manifest that points to the XML file containing the |
| * application's available restrictions. |
| * @see #getManifestRestrictions(String) |
| */ |
| public static final String META_DATA_APP_RESTRICTIONS = "android.content.APP_RESTRICTIONS"; |
| |
| private static final String TAG_RESTRICTION = "restriction"; |
| |
| private final Context mContext; |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| private final IRestrictionsManager mService; |
| |
| /** |
| * @hide |
| */ |
| public RestrictionsManager(Context context, IRestrictionsManager service) { |
| mContext = context; |
| mService = service; |
| } |
| |
| /** |
| * Returns any available set of application-specific restrictions applicable |
| * to this application. |
| * @return the application restrictions as a Bundle. Returns null if there |
| * are no restrictions. |
| * |
| * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, |
| * it is possible for there to be multiple managing apps on the device with the ability to set |
| * restrictions, e.g. a Device Policy Controller (DPC) and a Supervision admin. |
| * This API will only return the restrictions set by the DPCs. To retrieve restrictions |
| * set by all managing apps, use |
| * {@link android.content.RestrictionsManager#getApplicationRestrictionsPerAdmin} instead. |
| * |
| * @see DevicePolicyManager |
| */ |
| public Bundle getApplicationRestrictions() { |
| try { |
| if (mService != null) { |
| return mService.getApplicationRestrictions(mContext.getPackageName()); |
| } |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a {@link List} containing a {@link Bundle} for each managing agent that has set |
| * restrictions for the current application, the bundle contains any application restrictions |
| * set for the current package. The order of the items in the list is not guaranteed to remain |
| * stable between multiple calls. |
| * |
| * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, |
| * it is possible for there to be multiple managing apps on the device with the ability to set |
| * restrictions, e.g. an Enterprise Device Policy Controller (DPC) and a Supervision admin. |
| * |
| * <p>Each {@link Bundle} consists of key-value pairs, as defined by the application, |
| * where the types of values may be: |
| * <ul> |
| * <li>{@code boolean} |
| * <li>{@code int} |
| * <li>{@code String} or {@code String[]} |
| * <li>From {@link android.os.Build.VERSION_CODES#M}, {@code Bundle} or {@code Bundle[]} |
| * </ul> |
| * |
| * <p>NOTE: The method performs disk I/O and shouldn't be called on the main thread |
| * |
| * @return a {@link List} of {@link Bundle} containing the restrictions set by admins for that |
| * package. Returns an empty {@link List} if there are no saved restrictions. |
| * |
| * @see UserManager#KEY_RESTRICTIONS_PENDING |
| * @see DevicePolicyManager |
| */ |
| @WorkerThread |
| @UserHandleAware |
| public @NonNull List<Bundle> getApplicationRestrictionsPerAdmin() { |
| try { |
| if (mService != null) { |
| return mService.getApplicationRestrictionsPerAdminForUser( |
| mContext.getUserId(), mContext.getPackageName()); |
| } |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| return null; |
| } |
| |
| /** |
| * Called by an application to check if there is an active Restrictions Provider. If |
| * there isn't, {@link #requestPermission(String, String, PersistableBundle)} is not available. |
| * |
| * @return whether there is an active Restrictions Provider. |
| */ |
| public boolean hasRestrictionsProvider() { |
| try { |
| if (mService != null) { |
| return mService.hasRestrictionsProvider(); |
| } |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| return false; |
| } |
| |
| /** |
| * Called by an application to request permission for an operation. The contents of the |
| * request are passed in a Bundle that contains several pieces of data depending on the |
| * chosen request type. |
| * |
| * @param requestType The type of request. The type could be one of the |
| * predefined types specified here or a custom type that the specific |
| * Restrictions Provider might understand. For custom types, the type name should be |
| * namespaced to avoid collisions with predefined types and types specified by |
| * other Restrictions Providers. |
| * @param requestId A unique id generated by the app that contains sufficient information |
| * to identify the parameters of the request when it receives the id in the response. |
| * @param request A PersistableBundle containing the data corresponding to the specified request |
| * type. The keys for the data in the bundle depend on the request type. |
| * |
| * @throws IllegalArgumentException if any of the required parameters are missing. |
| */ |
| public void requestPermission(String requestType, String requestId, PersistableBundle request) { |
| if (requestType == null) { |
| throw new NullPointerException("requestType cannot be null"); |
| } |
| if (requestId == null) { |
| throw new NullPointerException("requestId cannot be null"); |
| } |
| if (request == null) { |
| throw new NullPointerException("request cannot be null"); |
| } |
| try { |
| if (mService != null) { |
| mService.requestPermission(mContext.getPackageName(), requestType, requestId, |
| request); |
| } |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| } |
| |
| public Intent createLocalApprovalIntent() { |
| Intent result = null; |
| try { |
| if (mService != null) { |
| result = mService.createLocalApprovalIntent(); |
| if (result != null) { |
| result.prepareToEnterProcess(LOCAL_FLAG_FROM_SYSTEM, |
| mContext.getAttributionSource()); |
| } |
| } |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| return result; |
| } |
| |
| /** |
| * Called by the Restrictions Provider to deliver a response to an application. |
| * |
| * @param packageName the application to deliver the response to. Cannot be null. |
| * @param response the bundle containing the response status, request ID and other information. |
| * Cannot be null. |
| * |
| * @throws IllegalArgumentException if any of the required parameters are missing. |
| */ |
| public void notifyPermissionResponse(String packageName, PersistableBundle response) { |
| if (packageName == null) { |
| throw new NullPointerException("packageName cannot be null"); |
| } |
| if (response == null) { |
| throw new NullPointerException("request cannot be null"); |
| } |
| if (!response.containsKey(REQUEST_KEY_ID)) { |
| throw new IllegalArgumentException("REQUEST_KEY_ID must be specified"); |
| } |
| if (!response.containsKey(RESPONSE_KEY_RESULT)) { |
| throw new IllegalArgumentException("RESPONSE_KEY_RESULT must be specified"); |
| } |
| try { |
| if (mService != null) { |
| mService.notifyPermissionResponse(packageName, response); |
| } |
| } catch (RemoteException re) { |
| throw re.rethrowFromSystemServer(); |
| } |
| } |
| |
| /** |
| * Parse and return the list of restrictions defined in the manifest for the specified |
| * package, if any. |
| * |
| * @param packageName The application for which to fetch the restrictions list. |
| * @return The list of RestrictionEntry objects created from the XML file specified |
| * in the manifest, or null if none was specified. |
| */ |
| public List<RestrictionEntry> getManifestRestrictions(String packageName) { |
| ApplicationInfo appInfo = null; |
| try { |
| appInfo = mContext.getPackageManager().getApplicationInfo(packageName, |
| PackageManager.GET_META_DATA); |
| } catch (NameNotFoundException pnfe) { |
| throw new IllegalArgumentException("No such package " + packageName); |
| } |
| if (appInfo == null || !appInfo.metaData.containsKey(META_DATA_APP_RESTRICTIONS)) { |
| return null; |
| } |
| |
| XmlResourceParser xml = |
| appInfo.loadXmlMetaData(mContext.getPackageManager(), META_DATA_APP_RESTRICTIONS); |
| return loadManifestRestrictions(packageName, xml); |
| } |
| |
| private List<RestrictionEntry> loadManifestRestrictions(String packageName, |
| XmlResourceParser xml) { |
| Context appContext; |
| try { |
| appContext = mContext.createPackageContext(packageName, 0 /* flags */); |
| } catch (NameNotFoundException nnfe) { |
| return null; |
| } |
| ArrayList<RestrictionEntry> restrictions = new ArrayList<>(); |
| RestrictionEntry restriction; |
| |
| try { |
| int tagType = xml.next(); |
| while (tagType != XmlPullParser.END_DOCUMENT) { |
| if (tagType == XmlPullParser.START_TAG) { |
| restriction = loadRestrictionElement(appContext, xml); |
| if (restriction != null) { |
| restrictions.add(restriction); |
| } |
| } |
| tagType = xml.next(); |
| } |
| } catch (XmlPullParserException e) { |
| Log.w(TAG, "Reading restriction metadata for " + packageName, e); |
| return null; |
| } catch (IOException e) { |
| Log.w(TAG, "Reading restriction metadata for " + packageName, e); |
| return null; |
| } |
| |
| return restrictions; |
| } |
| |
| private RestrictionEntry loadRestrictionElement(Context appContext, XmlResourceParser xml) |
| throws IOException, XmlPullParserException { |
| if (xml.getName().equals(TAG_RESTRICTION)) { |
| AttributeSet attrSet = Xml.asAttributeSet(xml); |
| if (attrSet != null) { |
| TypedArray a = appContext.obtainStyledAttributes(attrSet, |
| com.android.internal.R.styleable.RestrictionEntry); |
| return loadRestriction(appContext, a, xml); |
| } |
| } |
| return null; |
| } |
| |
| private RestrictionEntry loadRestriction(Context appContext, TypedArray a, XmlResourceParser xml) |
| throws IOException, XmlPullParserException { |
| String key = a.getString(R.styleable.RestrictionEntry_key); |
| int restrictionType = a.getInt( |
| R.styleable.RestrictionEntry_restrictionType, -1); |
| String title = a.getString(R.styleable.RestrictionEntry_title); |
| String description = a.getString(R.styleable.RestrictionEntry_description); |
| int entries = a.getResourceId(R.styleable.RestrictionEntry_entries, 0); |
| int entryValues = a.getResourceId(R.styleable.RestrictionEntry_entryValues, 0); |
| |
| if (restrictionType == -1) { |
| Log.w(TAG, "restrictionType cannot be omitted"); |
| return null; |
| } |
| |
| if (key == null) { |
| Log.w(TAG, "key cannot be omitted"); |
| return null; |
| } |
| |
| RestrictionEntry restriction = new RestrictionEntry(restrictionType, key); |
| restriction.setTitle(title); |
| restriction.setDescription(description); |
| if (entries != 0) { |
| restriction.setChoiceEntries(appContext, entries); |
| } |
| if (entryValues != 0) { |
| restriction.setChoiceValues(appContext, entryValues); |
| } |
| // Extract the default value based on the type |
| switch (restrictionType) { |
| case RestrictionEntry.TYPE_NULL: // hidden |
| case RestrictionEntry.TYPE_STRING: |
| case RestrictionEntry.TYPE_CHOICE: |
| restriction.setSelectedString( |
| a.getString(R.styleable.RestrictionEntry_defaultValue)); |
| break; |
| case RestrictionEntry.TYPE_INTEGER: |
| restriction.setIntValue( |
| a.getInt(R.styleable.RestrictionEntry_defaultValue, 0)); |
| break; |
| case RestrictionEntry.TYPE_MULTI_SELECT: |
| int resId = a.getResourceId(R.styleable.RestrictionEntry_defaultValue, 0); |
| if (resId != 0) { |
| restriction.setAllSelectedStrings( |
| appContext.getResources().getStringArray(resId)); |
| } |
| break; |
| case RestrictionEntry.TYPE_BOOLEAN: |
| restriction.setSelectedState( |
| a.getBoolean(R.styleable.RestrictionEntry_defaultValue, false)); |
| break; |
| case RestrictionEntry.TYPE_BUNDLE: |
| case RestrictionEntry.TYPE_BUNDLE_ARRAY: |
| final int outerDepth = xml.getDepth(); |
| List<RestrictionEntry> restrictionEntries = new ArrayList<>(); |
| while (XmlUtils.nextElementWithin(xml, outerDepth)) { |
| RestrictionEntry childEntry = loadRestrictionElement(appContext, xml); |
| if (childEntry == null) { |
| Log.w(TAG, "Child entry cannot be loaded for bundle restriction " + key); |
| } else { |
| restrictionEntries.add(childEntry); |
| if (restrictionType == RestrictionEntry.TYPE_BUNDLE_ARRAY |
| && childEntry.getType() != RestrictionEntry.TYPE_BUNDLE) { |
| Log.w(TAG, "bundle_array " + key |
| + " can only contain entries of type bundle"); |
| } |
| } |
| } |
| restriction.setRestrictions(restrictionEntries.toArray(new RestrictionEntry[ |
| restrictionEntries.size()])); |
| break; |
| default: |
| Log.w(TAG, "Unknown restriction type " + restrictionType); |
| } |
| return restriction; |
| } |
| |
| /** |
| * Converts a list of restrictions to the corresponding bundle, using the following mapping: |
| * <table> |
| * <tr><th>RestrictionEntry</th><th>Bundle</th></tr> |
| * <tr><td>{@link RestrictionEntry#TYPE_BOOLEAN}</td><td>{@link Bundle#putBoolean}</td></tr> |
| * <tr><td>{@link RestrictionEntry#TYPE_CHOICE}, |
| * {@link RestrictionEntry#TYPE_MULTI_SELECT}</td> |
| * <td>{@link Bundle#putStringArray}</td></tr> |
| * <tr><td>{@link RestrictionEntry#TYPE_INTEGER}</td><td>{@link Bundle#putInt}</td></tr> |
| * <tr><td>{@link RestrictionEntry#TYPE_STRING}</td><td>{@link Bundle#putString}</td></tr> |
| * <tr><td>{@link RestrictionEntry#TYPE_BUNDLE}</td><td>{@link Bundle#putBundle}</td></tr> |
| * <tr><td>{@link RestrictionEntry#TYPE_BUNDLE_ARRAY}</td> |
| * <td>{@link Bundle#putParcelableArray}</td></tr> |
| * </table> |
| * @param entries list of restrictions |
| */ |
| public static Bundle convertRestrictionsToBundle(List<RestrictionEntry> entries) { |
| final Bundle bundle = new Bundle(); |
| for (RestrictionEntry entry : entries) { |
| addRestrictionToBundle(bundle, entry); |
| } |
| return bundle; |
| } |
| |
| private static Bundle addRestrictionToBundle(Bundle bundle, RestrictionEntry entry) { |
| switch (entry.getType()) { |
| case RestrictionEntry.TYPE_BOOLEAN: |
| bundle.putBoolean(entry.getKey(), entry.getSelectedState()); |
| break; |
| case RestrictionEntry.TYPE_CHOICE: |
| case RestrictionEntry.TYPE_CHOICE_LEVEL: |
| case RestrictionEntry.TYPE_MULTI_SELECT: |
| bundle.putStringArray(entry.getKey(), entry.getAllSelectedStrings()); |
| break; |
| case RestrictionEntry.TYPE_INTEGER: |
| bundle.putInt(entry.getKey(), entry.getIntValue()); |
| break; |
| case RestrictionEntry.TYPE_STRING: |
| case RestrictionEntry.TYPE_NULL: |
| bundle.putString(entry.getKey(), entry.getSelectedString()); |
| break; |
| case RestrictionEntry.TYPE_BUNDLE: |
| RestrictionEntry[] restrictions = entry.getRestrictions(); |
| Bundle childBundle = convertRestrictionsToBundle(Arrays.asList(restrictions)); |
| bundle.putBundle(entry.getKey(), childBundle); |
| break; |
| case RestrictionEntry.TYPE_BUNDLE_ARRAY: |
| RestrictionEntry[] bundleRestrictionArray = entry.getRestrictions(); |
| Bundle[] bundleArray = new Bundle[bundleRestrictionArray.length]; |
| for (int i = 0; i < bundleRestrictionArray.length; i++) { |
| RestrictionEntry[] bundleRestrictions = |
| bundleRestrictionArray[i].getRestrictions(); |
| if (bundleRestrictions == null) { |
| // Non-bundle entry found in bundle array. |
| Log.w(TAG, "addRestrictionToBundle: " + |
| "Non-bundle entry found in bundle array"); |
| bundleArray[i] = new Bundle(); |
| } else { |
| bundleArray[i] = convertRestrictionsToBundle(Arrays.asList( |
| bundleRestrictions)); |
| } |
| } |
| bundle.putParcelableArray(entry.getKey(), bundleArray); |
| break; |
| default: |
| throw new IllegalArgumentException( |
| "Unsupported restrictionEntry type: " + entry.getType()); |
| } |
| return bundle; |
| } |
| |
| } |