API Review: Improvements to RestrictionsManager API

Use an activity intent for local approval instead of a type.
Use PeristableBundle instead of Bundle.
Pass requestId as an explicit argument in cases where it's required.

Bug: 16400892
Change-Id: Id882033f17c39aa9cd63a7eeb73bb7b51f98cf5b
diff --git a/Android.mk b/Android.mk
index 6e51575..79d13d9 100644
--- a/Android.mk
+++ b/Android.mk
@@ -446,6 +446,7 @@
 	frameworks/base/core/java/android/os/DropBoxManager.aidl \
 	frameworks/base/core/java/android/os/ParcelFileDescriptor.aidl \
 	frameworks/base/core/java/android/os/ParcelUuid.aidl \
+        frameworks/base/core/java/android/os/PersistableBundle.aidl \
 	frameworks/base/core/java/android/print/PrinterInfo.aidl \
 	frameworks/base/core/java/android/print/PageRange.aidl \
 	frameworks/base/core/java/android/print/PrintAttributes.aidl \
diff --git a/api/current.txt b/api/current.txt
index 6e25c2a..e6efcea 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6660,7 +6660,7 @@
   public abstract class AbstractRestrictionsProvider extends android.content.BroadcastReceiver {
     ctor public AbstractRestrictionsProvider();
     method public void onReceive(android.content.Context, android.content.Intent);
-    method public abstract void requestPermission(android.content.Context, java.lang.String, java.lang.String, android.os.Bundle);
+    method public abstract void requestPermission(android.content.Context, java.lang.String, java.lang.String, java.lang.String, android.os.PersistableBundle);
   }
 
   public abstract class AbstractThreadedSyncAdapter {
@@ -8050,14 +8050,17 @@
 
   public class RestrictionsManager {
     method public android.os.Bundle getApplicationRestrictions();
+    method public android.content.Intent getLocalApprovalIntent();
     method public java.util.List<android.content.RestrictionEntry> getManifestRestrictions(java.lang.String);
     method public boolean hasRestrictionsProvider();
-    method public void notifyPermissionResponse(java.lang.String, android.os.Bundle);
-    method public void requestPermission(java.lang.String, android.os.Bundle);
-    field public static final java.lang.String ACTION_PERMISSION_RESPONSE_RECEIVED = "android.intent.action.PERMISSION_RESPONSE_RECEIVED";
+    method public void notifyPermissionResponse(java.lang.String, android.os.PersistableBundle);
+    method public void requestPermission(java.lang.String, java.lang.String, android.os.PersistableBundle);
+    field public static final java.lang.String ACTION_PERMISSION_RESPONSE_RECEIVED = "android.content.action.PERMISSION_RESPONSE_RECEIVED";
+    field public static final java.lang.String ACTION_REQUEST_LOCAL_APPROVAL = "android.content.action.REQUEST_LOCAL_APPROVAL";
     field public static final java.lang.String ACTION_REQUEST_PERMISSION = "android.content.action.REQUEST_PERMISSION";
     field public static final java.lang.String EXTRA_PACKAGE_NAME = "android.content.extra.PACKAGE_NAME";
     field public static final java.lang.String EXTRA_REQUEST_BUNDLE = "android.content.extra.REQUEST_BUNDLE";
+    field public static final java.lang.String EXTRA_REQUEST_ID = "android.content.extra.REQUEST_ID";
     field public static final java.lang.String EXTRA_REQUEST_TYPE = "android.content.extra.REQUEST_TYPE";
     field public static final java.lang.String EXTRA_RESPONSE_BUNDLE = "android.content.extra.RESPONSE_BUNDLE";
     field public static final java.lang.String META_DATA_APP_RESTRICTIONS = "android.content.APP_RESTRICTIONS";
@@ -8070,7 +8073,6 @@
     field public static final java.lang.String REQUEST_KEY_NEW_REQUEST = "android.request.new_request";
     field public static final java.lang.String REQUEST_KEY_TITLE = "android.request.title";
     field public static final java.lang.String REQUEST_TYPE_APPROVAL = "android.request.type.approval";
-    field public static final java.lang.String REQUEST_TYPE_LOCAL_APPROVAL = "android.request.type.local_approval";
     field public static final java.lang.String RESPONSE_KEY_ERROR_CODE = "android.response.errorcode";
     field public static final java.lang.String RESPONSE_KEY_MESSAGE = "android.response.msg";
     field public static final java.lang.String RESPONSE_KEY_RESPONSE_TIMESTAMP = "android.response.timestamp";
diff --git a/core/java/android/content/AbstractRestrictionsProvider.java b/core/java/android/content/AbstractRestrictionsProvider.java
index 3272970..2b40870 100644
--- a/core/java/android/content/AbstractRestrictionsProvider.java
+++ b/core/java/android/content/AbstractRestrictionsProvider.java
@@ -17,8 +17,8 @@
 package android.content;
 
 import android.app.admin.DevicePolicyManager;
-import android.os.Bundle;
 import android.os.IBinder;
+import android.os.PersistableBundle;
 
 /**
  * Abstract implementation of a Restrictions Provider BroadcastReceiver. To implement a
@@ -30,7 +30,7 @@
  * The function of a Restrictions Provider is to transport permission requests from apps on this
  * device to an administrator (most likely on a remote device or computer) and deliver back
  * responses. The response should be sent back to the app via
- * {@link RestrictionsManager#notifyPermissionResponse(String, Bundle)}.
+ * {@link RestrictionsManager#notifyPermissionResponse(String, PersistableBundle)}.
  *
  * @see RestrictionsManager
  */
@@ -59,7 +59,7 @@
      * @see RestrictionsManager#REQUEST_KEY_ID
      */
     public abstract void requestPermission(Context context,
-            String packageName, String requestType, Bundle request);
+            String packageName, String requestType, String requestId, PersistableBundle request);
 
     /**
      * Intercept standard Restrictions Provider broadcasts.  Implementations
@@ -73,8 +73,10 @@
         if (RestrictionsManager.ACTION_REQUEST_PERMISSION.equals(action)) {
             String packageName = intent.getStringExtra(RestrictionsManager.EXTRA_PACKAGE_NAME);
             String requestType = intent.getStringExtra(RestrictionsManager.EXTRA_REQUEST_TYPE);
-            Bundle request = intent.getBundleExtra(RestrictionsManager.EXTRA_REQUEST_BUNDLE);
-            requestPermission(context, packageName, requestType, request);
+            String requestId = intent.getStringExtra(RestrictionsManager.EXTRA_REQUEST_ID);
+            PersistableBundle request = (PersistableBundle)
+                    intent.getParcelableExtra(RestrictionsManager.EXTRA_REQUEST_BUNDLE);
+            requestPermission(context, packageName, requestType, requestId, request);
         }
     }
 }
diff --git a/core/java/android/content/IRestrictionsManager.aidl b/core/java/android/content/IRestrictionsManager.aidl
index b1c0a3a..495ac2e 100644
--- a/core/java/android/content/IRestrictionsManager.aidl
+++ b/core/java/android/content/IRestrictionsManager.aidl
@@ -16,7 +16,9 @@
 
 package android.content;
 
+import android.content.Intent;
 import android.os.Bundle;
+import android.os.PersistableBundle;
 
 /**
  * Interface used by the RestrictionsManager
@@ -25,6 +27,8 @@
 interface IRestrictionsManager {
     Bundle getApplicationRestrictions(in String packageName);
     boolean hasRestrictionsProvider();
-    void requestPermission(in String packageName, in String requestTemplate, in Bundle requestData);
-    void notifyPermissionResponse(in String packageName, in Bundle response);
+    void requestPermission(in String packageName, in String requestType, in String requestId,
+            in PersistableBundle requestData);
+    void notifyPermissionResponse(in String packageName, in PersistableBundle response);
+    Intent getLocalApprovalIntent();
 }
diff --git a/core/java/android/content/RestrictionsManager.java b/core/java/android/content/RestrictionsManager.java
index 5ae10cfc..c1226c0 100644
--- a/core/java/android/content/RestrictionsManager.java
+++ b/core/java/android/content/RestrictionsManager.java
@@ -16,6 +16,7 @@
 
 package android.content;
 
+import android.app.Activity;
 import android.app.admin.DevicePolicyManager;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
@@ -23,6 +24,7 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.RemoteException;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -51,7 +53,7 @@
  * <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, Bundle)}, when
+ * {@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}.
@@ -126,14 +128,15 @@
      * {@link #EXTRA_RESPONSE_BUNDLE}.
      */
     public static final String ACTION_PERMISSION_RESPONSE_RECEIVED =
-            "android.intent.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} 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}.
+     * {@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.
@@ -142,17 +145,45 @@
             "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 #getLocalApprovalIntent()
+     */
+    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";
 
@@ -163,14 +194,15 @@
      * <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_ID} and {@link #REQUEST_KEY_MESSAGE}.
+     * Required keys are: {@link #REQUEST_KEY_MESSAGE}
      * <p>
      * Optional keys are
      * {@link #REQUEST_KEY_DATA}, {@link #REQUEST_KEY_ICON}, {@link #REQUEST_KEY_TITLE},
@@ -179,22 +211,6 @@
     public static final String REQUEST_TYPE_APPROVAL = "android.request.type.approval";
 
     /**
-     * Request type for a local password challenge. This is a way for an app to ask
-     * the administrator to override an operation that is restricted on the device, such
-     * as configuring Wi-Fi access points. It is most useful for situations where there
-     * is no network connectivity for a remote administrator's response. The normal user of the
-     * device is not expected to know the password. The challenge is meant for the administrator.
-     * <p>
-     * Required keys are
-     * {@link #REQUEST_KEY_ID} and {@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_LOCAL_APPROVAL = "android.request.type.local_approval";
-
-    /**
      * Key for request ID contained in the request bundle.
      * <p>
      * App-generated request ID to identify the specific request when receiving
@@ -240,9 +256,10 @@
      * Key for request icon contained in the request bundle.
      * <p>
      * Optional, shown alongside the request message presented to the administrator
-     * who approves the request.
+     * who approves the request. The content must be a compressed image such as a
+     * PNG or JPEG, as a byte array.
      * <p>
-     * Type: Bitmap
+     * Type: byte[]
      */
     public static final String REQUEST_KEY_ICON = "android.request.icon";
 
@@ -403,7 +420,7 @@
 
     /**
      * Called by an application to check if there is an active Restrictions Provider. If
-     * there isn't, {@link #requestPermission(String, Bundle)} is not available.
+     * there isn't, {@link #requestPermission(String, String, PersistableBundle)} is not available.
      *
      * @return whether there is an active Restrictions Provider.
      */
@@ -428,40 +445,54 @@
      * 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 request A Bundle containing the data corresponding to the specified request
+     * @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, Bundle request) {
+    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");
         }
-        if (!request.containsKey(REQUEST_KEY_ID)) {
-            throw new IllegalArgumentException("REQUEST_KEY_ID must be specified");
-        }
         try {
             if (mService != null) {
-                mService.requestPermission(mContext.getPackageName(), requestType, request);
+                mService.requestPermission(mContext.getPackageName(), requestType, requestId,
+                        request);
             }
         } catch (RemoteException re) {
             Log.w(TAG, "Couldn't reach service");
         }
     }
 
+    public Intent getLocalApprovalIntent() {
+        try {
+            if (mService != null) {
+                return mService.getLocalApprovalIntent();
+            }
+        } catch (RemoteException re) {
+            Log.w(TAG, "Couldn't reach service");
+        }
+        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.
+     * @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, Bundle response) {
+    public void notifyPermissionResponse(String packageName, PersistableBundle response) {
         if (packageName == null) {
             throw new NullPointerException("packageName cannot be null");
         }
diff --git a/core/java/android/os/PersistableBundle.aidl b/core/java/android/os/PersistableBundle.aidl
new file mode 100644
index 0000000..5b05873
--- /dev/null
+++ b/core/java/android/os/PersistableBundle.aidl
@@ -0,0 +1,20 @@
+/*
+**
+** Copyright 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.os;
+
+parcelable PersistableBundle;
diff --git a/services/restrictions/java/com/android/server/restrictions/RestrictionsManagerService.java b/services/restrictions/java/com/android/server/restrictions/RestrictionsManagerService.java
index 4fe30e6..fb29b6a 100644
--- a/services/restrictions/java/com/android/server/restrictions/RestrictionsManagerService.java
+++ b/services/restrictions/java/com/android/server/restrictions/RestrictionsManagerService.java
@@ -40,6 +40,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IUserManager;
+import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -103,7 +104,8 @@
 
         @Override
         public void requestPermission(final String packageName, final String requestType,
-                final Bundle requestData) throws RemoteException {
+                final String requestId,
+                final PersistableBundle requestData) throws RemoteException {
             if (DEBUG) {
                 Log.i(LOG_TAG, "requestPermission");
             }
@@ -127,6 +129,7 @@
                     intent.setComponent(restrictionsProvider);
                     intent.putExtra(RestrictionsManager.EXTRA_PACKAGE_NAME, packageName);
                     intent.putExtra(RestrictionsManager.EXTRA_REQUEST_TYPE, requestType);
+                    intent.putExtra(RestrictionsManager.EXTRA_REQUEST_ID, requestId);
                     intent.putExtra(RestrictionsManager.EXTRA_REQUEST_BUNDLE, requestData);
                     mContext.sendBroadcastAsUser(intent, new UserHandle(userHandle));
                 } finally {
@@ -136,7 +139,40 @@
         }
 
         @Override
-        public void notifyPermissionResponse(String packageName, Bundle response)
+        public Intent getLocalApprovalIntent() throws RemoteException {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "requestPermission");
+            }
+            final int userHandle = UserHandle.getCallingUserId();
+            if (mDpm != null) {
+                long ident = Binder.clearCallingIdentity();
+                try {
+                    ComponentName restrictionsProvider =
+                            mDpm.getRestrictionsProvider(userHandle);
+                    // Check if there is a restrictions provider
+                    if (restrictionsProvider == null) {
+                        throw new IllegalStateException(
+                            "Cannot request permission without a restrictions provider registered");
+                    }
+                    String providerPackageName = restrictionsProvider.getPackageName();
+                    Intent intent = new Intent(RestrictionsManager.ACTION_REQUEST_LOCAL_APPROVAL);
+                    intent.setPackage(providerPackageName);
+                    ResolveInfo ri = AppGlobals.getPackageManager().resolveIntent(intent,
+                            null /* resolvedType */, 0 /* flags */, userHandle);
+                    if (ri != null && ri.activityInfo != null && ri.activityInfo.exported) {
+                        intent.setComponent(new ComponentName(ri.activityInfo.packageName,
+                                ri.activityInfo.name));
+                        return intent;
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(ident);
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public void notifyPermissionResponse(String packageName, PersistableBundle response)
                 throws RemoteException {
             // Check caller
             int callingUid = Binder.getCallingUid();