Implement issue #36590595: Add ability to associated a ClipData with JobInfo

Yum!

Also needed to have a Context.revokeUriPermission() variant that is sane,
so reasonable CTS tests can be written.

Test: new ClipDataJobTest added.

Change-Id: Ia3135ea788a6e32c971bae7dab3a844d0ef4139c
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 467ba99..5a7246a 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1790,7 +1790,18 @@
     public void revokeUriPermission(Uri uri, int modeFlags) {
          try {
             ActivityManager.getService().revokeUriPermission(
-                    mMainThread.getApplicationThread(),
+                    mMainThread.getApplicationThread(), null,
+                    ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @Override
+    public void revokeUriPermission(String targetPackage, Uri uri, int modeFlags) {
+        try {
+            ActivityManager.getService().revokeUriPermission(
+                    mMainThread.getApplicationThread(), targetPackage,
                     ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri));
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 0a5e4be..f4d26fd 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -172,7 +172,8 @@
             in IBinder callerToken);
     void grantUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
             int mode, int userId);
-    void revokeUriPermission(in IApplicationThread caller, in Uri uri, int mode, int userId);
+    void revokeUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri,
+            int mode, int userId);
     void setActivityController(in IActivityController watcher, boolean imAMonkey);
     void showWaitingForDebugger(in IApplicationThread who, boolean waiting);
     /*
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 78e4c0d..96eb0ea 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -20,6 +20,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.ClipData;
 import android.content.ComponentName;
 import android.net.Uri;
 import android.os.Bundle;
@@ -197,6 +198,8 @@
     private final int jobId;
     private final PersistableBundle extras;
     private final Bundle transientExtras;
+    private final ClipData clipData;
+    private final int clipGrantFlags;
     private final ComponentName service;
     private final int constraintFlags;
     private final TriggerContentUri[] triggerContentUris;
@@ -240,6 +243,21 @@
     }
 
     /**
+     * ClipData of information that is returned to your application at execution time,
+     * but not persisted by the system.
+     */
+    public ClipData getClipData() {
+        return clipData;
+    }
+
+    /**
+     * Permission grants that go along with {@link #getClipData}.
+     */
+    public int getClipGrantFlags() {
+        return clipGrantFlags;
+    }
+
+    /**
      * Name of the service endpoint that will be called back into by the JobScheduler.
      */
     public ComponentName getService() {
@@ -415,6 +433,13 @@
         jobId = in.readInt();
         extras = in.readPersistableBundle();
         transientExtras = in.readBundle();
+        if (in.readInt() != 0) {
+            clipData = ClipData.CREATOR.createFromParcel(in);
+            clipGrantFlags = in.readInt();
+        } else {
+            clipData = null;
+            clipGrantFlags = 0;
+        }
         service = in.readParcelable(null);
         constraintFlags = in.readInt();
         triggerContentUris = in.createTypedArray(TriggerContentUri.CREATOR);
@@ -439,6 +464,8 @@
         jobId = b.mJobId;
         extras = b.mExtras.deepCopy();
         transientExtras = b.mTransientExtras.deepCopy();
+        clipData = b.mClipData;
+        clipGrantFlags = b.mClipGrantFlags;
         service = b.mJobService;
         constraintFlags = b.mConstraintFlags;
         triggerContentUris = b.mTriggerContentUris != null
@@ -471,6 +498,13 @@
         out.writeInt(jobId);
         out.writePersistableBundle(extras);
         out.writeBundle(transientExtras);
+        if (clipData != null) {
+            out.writeInt(1);
+            clipData.writeToParcel(out, flags);
+            out.writeInt(clipGrantFlags);
+        } else {
+            out.writeInt(0);
+        }
         out.writeParcelable(service, flags);
         out.writeInt(constraintFlags);
         out.writeTypedArray(triggerContentUris, flags);
@@ -597,6 +631,8 @@
         private final ComponentName mJobService;
         private PersistableBundle mExtras = PersistableBundle.EMPTY;
         private Bundle mTransientExtras = Bundle.EMPTY;
+        private ClipData mClipData;
+        private int mClipGrantFlags;
         private int mPriority = PRIORITY_DEFAULT;
         private int mFlags;
         // Requirements.
@@ -669,6 +705,34 @@
         }
 
         /**
+         * Set a {@link ClipData} associated with this Job.
+         *
+         * <p>The main purpose of providing a ClipData is to allow granting of
+         * URI permissions for data associated with the clip.  The exact kind
+         * of permission grant to perform is specified through <var>grantFlags</var>.
+         *
+         * <p>If the ClipData contains items that are Intents, any
+         * grant flags in those Intents will be ignored.  Only flags provided as an argument
+         * to this method are respected, and will be applied to all Uri or
+         * Intent items in the clip (or sub-items of the clip).
+         *
+         * <p>Because setting this property is not compatible with persisted
+         * jobs, doing so will throw an {@link java.lang.IllegalArgumentException} when
+         * {@link android.app.job.JobInfo.Builder#build()} is called.</p>
+         *
+         * @param clip The new clip to set.  May be null to clear the current clip.
+         * @param grantFlags The desired permissions to grant for any URIs.  This should be
+         * a combination of {@link android.content.Intent#FLAG_GRANT_READ_URI_PERMISSION},
+         * {@link android.content.Intent#FLAG_GRANT_WRITE_URI_PERMISSION}, and
+         * {@link android.content.Intent#FLAG_GRANT_PREFIX_URI_PERMISSION}.
+         */
+        public Builder setClipData(ClipData clip, int grantFlags) {
+            mClipData = clip;
+            mClipGrantFlags = grantFlags;
+            return this;
+        }
+
+        /**
          * Set some description of the kind of network type your job needs to have.
          * Not calling this function means the network is not necessary, as the default is
          * {@link #NETWORK_TYPE_NONE}.
@@ -892,25 +956,33 @@
                         "constraints, this is not allowed.");
             }
             // Check that a deadline was not set on a periodic job.
-            if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) {
-                throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
-                        "periodic job.");
+            if (mIsPeriodic) {
+                if (mMaxExecutionDelayMillis != 0L) {
+                    throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " +
+                            "periodic job.");
+                }
+                if (mMinLatencyMillis != 0L) {
+                    throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
+                            "periodic job");
+                }
+                if (mTriggerContentUris != null) {
+                    throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+                            "periodic job");
+                }
             }
-            if (mIsPeriodic && (mMinLatencyMillis != 0L)) {
-                throw new IllegalArgumentException("Can't call setMinimumLatency() on a " +
-                        "periodic job");
-            }
-            if (mIsPeriodic && (mTriggerContentUris != null)) {
-                throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
-                        "periodic job");
-            }
-            if (mIsPersisted && (mTriggerContentUris != null)) {
-                throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
-                        "persisted job");
-            }
-            if (mIsPersisted && !mTransientExtras.isEmpty()) {
-                throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
-                        "persisted job");
+            if (mIsPersisted) {
+                if (mTriggerContentUris != null) {
+                    throw new IllegalArgumentException("Can't call addTriggerContentUri() on a " +
+                            "persisted job");
+                }
+                if (!mTransientExtras.isEmpty()) {
+                    throw new IllegalArgumentException("Can't call setTransientExtras() on a " +
+                            "persisted job");
+                }
+                if (mClipData != null) {
+                    throw new IllegalArgumentException("Can't call setClipData() on a " +
+                            "persisted job");
+                }
             }
             if (mBackoffPolicySet && (mConstraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) {
                 throw new IllegalArgumentException("An idle mode job will not respect any" +
diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java
index ba168b7..8d52d3b 100644
--- a/core/java/android/app/job/JobParameters.java
+++ b/core/java/android/app/job/JobParameters.java
@@ -17,6 +17,7 @@
 package android.app.job;
 
 import android.app.job.IJobCallback;
+import android.content.ClipData;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -44,6 +45,8 @@
     private final int jobId;
     private final PersistableBundle extras;
     private final Bundle transientExtras;
+    private final ClipData clipData;
+    private final int clipGrantFlags;
     private final IBinder callback;
     private final boolean overrideDeadlineExpired;
     private final Uri[] mTriggeredContentUris;
@@ -53,11 +56,14 @@
 
     /** @hide */
     public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
-            Bundle transientExtras, boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
+            Bundle transientExtras, ClipData clipData, int clipGrantFlags,
+            boolean overrideDeadlineExpired, Uri[] triggeredContentUris,
             String[] triggeredContentAuthorities) {
         this.jobId = jobId;
         this.extras = extras;
         this.transientExtras = transientExtras;
+        this.clipData = clipData;
+        this.clipGrantFlags = clipGrantFlags;
         this.callback = callback;
         this.overrideDeadlineExpired = overrideDeadlineExpired;
         this.mTriggeredContentUris = triggeredContentUris;
@@ -98,6 +104,24 @@
     }
 
     /**
+     * @return The clip you passed in when constructing this job with
+     * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be null
+     * if it was not set.
+     */
+    public ClipData getClipData() {
+        return clipData;
+    }
+
+    /**
+     * @return The clip grant flags you passed in when constructing this job with
+     * {@link android.app.job.JobInfo.Builder#setClipData(ClipData, int)}. Will be 0
+     * if it was not set.
+     */
+    public int getClipGrantFlags() {
+        return clipGrantFlags;
+    }
+
+    /**
      * For jobs with {@link android.app.job.JobInfo.Builder#setOverrideDeadline(long)} set, this
      * provides an easy way to tell whether the job is being executed due to the deadline
      * expiring. Note: If the job is running because its deadline expired, it implies that its
@@ -140,6 +164,13 @@
         jobId = in.readInt();
         extras = in.readPersistableBundle();
         transientExtras = in.readBundle();
+        if (in.readInt() != 0) {
+            clipData = ClipData.CREATOR.createFromParcel(in);
+            clipGrantFlags = in.readInt();
+        } else {
+            clipData = null;
+            clipGrantFlags = 0;
+        }
         callback = in.readStrongBinder();
         overrideDeadlineExpired = in.readInt() == 1;
         mTriggeredContentUris = in.createTypedArray(Uri.CREATOR);
@@ -162,6 +193,13 @@
         dest.writeInt(jobId);
         dest.writePersistableBundle(extras);
         dest.writeBundle(transientExtras);
+        if (clipData != null) {
+            dest.writeInt(1);
+            clipData.writeToParcel(dest, flags);
+            dest.writeInt(clipGrantFlags);
+        } else {
+            dest.writeInt(0);
+        }
         dest.writeStrongBinder(callback);
         dest.writeInt(overrideDeadlineExpired ? 1 : 0);
         dest.writeTypedArray(mTriggeredContentUris, flags);
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 1803bbe..dbbfe30 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4078,8 +4078,8 @@
 
     /**
      * Remove all permissions to access a particular content provider Uri
-     * that were previously added with {@link #grantUriPermission}.  The given
-     * Uri will match all previously granted Uris that are the same or a
+     * that were previously added with {@link #grantUriPermission} or <em>any other</em> mechanism.
+     * The given Uri will match all previously granted Uris that are the same or a
      * sub-path of the given Uri.  That is, revoking "content://foo/target" will
      * revoke both "content://foo/target" and "content://foo/target/sub", but not
      * "content://foo".  It will not remove any prefix grants that exist at a
@@ -4089,10 +4089,16 @@
      * regular permission access to a Uri, but had received access to it through
      * a specific Uri permission grant, you could not revoke that grant with this
      * function and a {@link SecurityException} would be thrown.  As of
-     * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this function will not throw a security exception,
-     * but will remove whatever permission grants to the Uri had been given to the app
+     * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this function will not throw a security
+     * exception, but will remove whatever permission grants to the Uri had been given to the app
      * (or none).</p>
      *
+     * <p>Unlike {@link #revokeUriPermission(String, Uri, int)}, this method impacts all permission
+     * grants matching the given Uri, for any package they had been granted to, through any
+     * mechanism this had happened (such as indirectly through the clipboard, activity launch,
+     * service start, etc).  That means this can be potentially dangerous to use, as it can
+     * revoke grants that another app could be strongly expecting to stick around.</p>
+     *
      * @param uri The Uri you would like to revoke access to.
      * @param modeFlags The desired access modes.  Any combination of
      * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
@@ -4105,6 +4111,34 @@
     public abstract void revokeUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags);
 
     /**
+     * Remove permissions to access a particular content provider Uri
+     * that were previously added with {@link #grantUriPermission} for a specific target
+     * package.  The given Uri will match all previously granted Uris that are the same or a
+     * sub-path of the given Uri.  That is, revoking "content://foo/target" will
+     * revoke both "content://foo/target" and "content://foo/target/sub", but not
+     * "content://foo".  It will not remove any prefix grants that exist at a
+     * higher level.
+     *
+     * <p>Unlike {@link #revokeUriPermission(Uri, int)}, this method will <em>only</em>
+     * revoke permissions that had been explicitly granted through {@link #grantUriPermission}
+     * and only for the package specified.  Any matching grants that have happened through
+     * other mechanisms (clipboard, activity launching, service starting, etc) will not be
+     * removed.</p>
+     *
+     * @param toPackage The package you had previously granted access to.
+     * @param uri The Uri you would like to revoke access to.
+     * @param modeFlags The desired access modes.  Any combination of
+     * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION
+     * Intent.FLAG_GRANT_READ_URI_PERMISSION} or
+     * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION
+     * Intent.FLAG_GRANT_WRITE_URI_PERMISSION}.
+     *
+     * @see #grantUriPermission
+     */
+    public abstract void revokeUriPermission(String toPackage, Uri uri,
+            @Intent.AccessUriMode int modeFlags);
+
+    /**
      * Determine whether a particular process and user ID has been granted
      * permission to access a specific URI.  This only checks for permissions
      * that have been explicitly granted -- if the given process/uid has
diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java
index 75784a6..53b021c 100644
--- a/core/java/android/content/ContextWrapper.java
+++ b/core/java/android/content/ContextWrapper.java
@@ -781,6 +781,11 @@
     }
 
     @Override
+    public void revokeUriPermission(String targetPackage, Uri uri, int modeFlags) {
+        mBase.revokeUriPermission(targetPackage, uri, modeFlags);
+    }
+
+    @Override
     public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
         return mBase.checkUriPermission(uri, pid, uid, modeFlags);
     }
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index e82fe03..6f388e2 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -311,6 +311,20 @@
     }
 
     /**
+     * @hide this should probably be the implementation of isEmpty().  To do that we
+     * need to ensure we always use the special empty parcel form when the bundle is
+     * empty.  (This may already be the case, but to be safe we'll do this later when
+     * we aren't trying to stabilize.)
+     */
+    public boolean maybeIsEmpty() {
+        if (isParcelled()) {
+            return isEmptyParcel();
+        } else {
+            return isEmpty();
+        }
+    }
+
+    /**
      * Removes all elements from the mapping of this Bundle.
      */
     public void clear() {
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index c1292e7..9b5ff29 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -1209,4 +1209,18 @@
         }
         return "Bundle[" + mMap.toString() + "]";
     }
+
+    /**
+     * @hide
+     */
+    public synchronized String toShortString() {
+        if (mParcelledData != null) {
+            if (isEmptyParcel()) {
+                return "EMPTY_PARCEL";
+            } else {
+                return "mParcelledData.dataSize=" + mParcelledData.dataSize();
+            }
+        }
+        return mMap.toString();
+    }
 }
diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index 75f9c11..3ed5b17 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -309,4 +309,16 @@
         }
         return "PersistableBundle[" + mMap.toString() + "]";
     }
+
+    /** @hide */
+    synchronized public String toShortString() {
+        if (mParcelledData != null) {
+            if (isEmptyParcel()) {
+                return "EMPTY_PARCEL";
+            } else {
+                return "mParcelledData.dataSize=" + mParcelledData.dataSize();
+            }
+        }
+        return mMap.toString();
+    }
 }