/* * 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 android.app.Activity; import android.app.admin.DevicePolicyManager; 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.Bundle; import android.os.PersistableBundle; import android.os.RemoteException; 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. *

* Apps can expose a set of restrictions via an XML file specified in the manifest. *

* 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. *

* 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}. *

* 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. *

* The syntax of the XML format is as follows: *

 * <?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>
 * 
*

* The attributes for each restriction depend on the restriction type. *

*

*

* Only restrictions of type {@code bundle} and {@code bundle_array} can have one or multiple nested * restriction elements. *

* In your manifest's application section, add the meta-data tag to point to * the restrictions XML file as shown below: *

 * <application ... >
 *     <meta-data android:name="android.content.APP_RESTRICTIONS"
 *                   android:resource="@xml/app_restrictions" />
 *     ...
 * </application>
 * 
* * @see RestrictionEntry * @see RestrictionsReceiver * @see DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName) * @see DevicePolicyManager#setApplicationRestrictions(ComponentName, String, Bundle) */ 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. *

* 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. *

* 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}. *

* 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}. *

* 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. *

* @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. *

* 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. *

* 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. *

* 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. *

* 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: *

*

* 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. *

* Required keys are: {@link #REQUEST_KEY_MESSAGE} *

* 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. *

* App-generated request ID to identify the specific request when receiving * a response. This value is returned in the {@link #EXTRA_RESPONSE_BUNDLE}. *

* Type: String */ public static final String REQUEST_KEY_ID = "android.request.id"; /** * Key for request data contained in the request bundle. *

* 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}. *

* Type: String */ public static final String REQUEST_KEY_DATA = "android.request.data"; /** * Key for request title contained in the request bundle. *

* Optional, typically used as the title of any notification or dialog presented * to the administrator who approves the request. *

* Type: String */ public static final String REQUEST_KEY_TITLE = "android.request.title"; /** * Key for request message contained in the request bundle. *

* Required, shown as the actual message in a notification or dialog presented * to the administrator who approves the request. *

* Type: String */ public static final String REQUEST_KEY_MESSAGE = "android.request.mesg"; /** * Key for request icon contained in the request bundle. *

* 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. *

* Type: byte[] */ public static final String REQUEST_KEY_ICON = "android.request.icon"; /** * Key for request approval button label contained in the request bundle. *

* Optional, may be shown as a label on the positive button in a dialog or * notification presented to the administrator who approves the request. *

* 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. *

* Optional, may be shown as a label on the negative button in a dialog or * notification presented to the administrator who approves the request. *

* 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. *

* 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. *

* Type: int *

* 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. *

* 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. *

* 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. *

* 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. *

* Type: int *

* 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. *

* 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. *

* 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; 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. */ public Bundle getApplicationRestrictions() { try { if (mService != null) { return mService.getApplicationRestrictions(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() { try { if (mService != null) { return mService.createLocalApprovalIntent(); } } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } return null; } /** * 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 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 loadManifestRestrictions(String packageName, XmlResourceParser xml) { Context appContext; try { appContext = mContext.createPackageContext(packageName, 0 /* flags */); } catch (NameNotFoundException nnfe) { return null; } ArrayList 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 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: * * * * * * * * * * *
RestrictionEntryBundle
{@link RestrictionEntry#TYPE_BOOLEAN}{@link Bundle#putBoolean}
{@link RestrictionEntry#TYPE_CHOICE}, * {@link RestrictionEntry#TYPE_MULTI_SELECT}{@link Bundle#putStringArray}
{@link RestrictionEntry#TYPE_INTEGER}{@link Bundle#putInt}
{@link RestrictionEntry#TYPE_STRING}{@link Bundle#putString}
{@link RestrictionEntry#TYPE_BUNDLE}{@link Bundle#putBundle}
{@link RestrictionEntry#TYPE_BUNDLE_ARRAY}{@link Bundle#putParcelableArray}
* @param entries list of restrictions */ public static Bundle convertRestrictionsToBundle(List 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: restrictions = entry.getRestrictions(); Bundle[] bundleArray = new Bundle[restrictions.length]; for (int i = 0; i < restrictions.length; i++) { bundleArray[i] = addRestrictionToBundle(new Bundle(), restrictions[i]); } bundle.putParcelableArray(entry.getKey(), bundleArray); break; default: throw new IllegalArgumentException( "Unsupported restrictionEntry type: " + entry.getType()); } return bundle; } }