813 lines
33 KiB
Java
813 lines
33 KiB
Java
/*
|
|
* 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;
|
|
}
|
|
|
|
}
|