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

New test suite for jobs with ClipData that grants permissions.

Test: this.

Change-Id: I172ea435f45b1b8a0356873f07b57281b177f485
diff --git a/tests/JobScheduler/AndroidTest.xml b/tests/JobScheduler/AndroidTest.xml
index 57a2f30..619edfb 100644
--- a/tests/JobScheduler/AndroidTest.xml
+++ b/tests/JobScheduler/AndroidTest.xml
@@ -17,6 +17,7 @@
     <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsJobSchedulerTestCases.apk" />
+        <option name="test-file-name" value="CtsJobSchedulerJobPerm.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.jobscheduler.cts" />
diff --git a/tests/JobScheduler/jobperm/Android.mk b/tests/JobScheduler/jobperm/Android.mk
new file mode 100644
index 0000000..97bfd3e
--- /dev/null
+++ b/tests/JobScheduler/jobperm/Android.mk
@@ -0,0 +1,33 @@
+# Copyright (C) 2017 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Don't include this package in any target.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    compatibility-device-util \
+
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src) \
+
+# Tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts
+
+LOCAL_PACKAGE_NAME := CtsJobSchedulerJobPerm
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/JobScheduler/jobperm/AndroidManifest.xml b/tests/JobScheduler/jobperm/AndroidManifest.xml
new file mode 100755
index 0000000..14eb02b
--- /dev/null
+++ b/tests/JobScheduler/jobperm/AndroidManifest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.jobscheduler.cts.jobperm">
+
+    <!--
+    An app that declares a permission that requires a matching signature to
+    access.
+    -->
+    <permission android:name="android.jobscheduler.cts.jobperm.perm"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="android.jobscheduler.cts.jobperm.perm" />
+
+    <application>
+        <!-- Need a way for another app to try to access the permission. So create a content
+        provider which is enforced by the permission -->
+        <provider android:name=".JobPermProvider"
+            android:authorities="android.jobscheduler.cts.jobperm.provider"
+            android:exported="true">
+            <path-permission
+                android:pathPrefix="/protected"
+                android:readPermission="android.jobscheduler.cts.jobperm.perm"
+                android:writePermission="android.jobscheduler.cts.jobperm.perm" />
+            <grant-uri-permission android:pathPattern=".*" />
+        </provider>
+    </application>
+</manifest>
diff --git a/tests/JobScheduler/jobperm/src/android/jobscheduler/cts/jobperm/JobPermProvider.java b/tests/JobScheduler/jobperm/src/android/jobscheduler/cts/jobperm/JobPermProvider.java
new file mode 100644
index 0000000..5b3bac7
--- /dev/null
+++ b/tests/JobScheduler/jobperm/src/android/jobscheduler/cts/jobperm/JobPermProvider.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 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.jobscheduler.cts.jobperm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/**
+ * Empty content provider, all permissions are enforced in manifest
+ */
+public class JobPermProvider extends ContentProvider {
+    @Override
+    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
+        if (method == null) {
+            return null;
+        }
+        switch (method) {
+            case "grant": {
+                Uri uri = extras.getParcelable("uri");
+                getContext().grantUriPermission(arg, uri,
+                        Intent.FLAG_GRANT_READ_URI_PERMISSION |
+                                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+                return null;
+            }
+            case "revoke": {
+                Uri uri = extras.getParcelable("uri");
+                getContext().revokeUriPermission(arg, uri,
+                        Intent.FLAG_GRANT_READ_URI_PERMISSION |
+                                Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+                return null;
+
+            }
+        }
+        return super.call(method, arg, extras);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        // do nothing
+        return 0;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return "got/theMIME";
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return false;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        return ParcelFileDescriptor.open(
+                new File("/dev/null"), ParcelFileDescriptor.MODE_READ_ONLY);
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/MockJobService.java b/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
index 4f549f8..e914ee2 100644
--- a/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
+++ b/tests/JobScheduler/src/android/jobscheduler/MockJobService.java
@@ -19,6 +19,10 @@
 import android.annotation.TargetApi;
 import android.app.job.JobParameters;
 import android.app.job.JobService;
+import android.content.ClipData;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Process;
 import android.util.Log;
 
 import java.util.concurrent.CountDownLatch;
@@ -46,7 +50,17 @@
     public boolean onStartJob(JobParameters params) {
         Log.i(TAG, "Test job executing: " + params.getJobId());
 
-        TestEnvironment.getTestEnvironment().notifyExecution(params);
+        int permCheckRead = PackageManager.PERMISSION_DENIED;
+        int permCheckWrite = PackageManager.PERMISSION_DENIED;
+        ClipData clip = params.getClipData();
+        if (clip != null) {
+            permCheckRead = checkUriPermission(clip.getItemAt(0).getUri(), Process.myPid(),
+                    Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION);
+            permCheckWrite = checkUriPermission(clip.getItemAt(0).getUri(), Process.myPid(),
+                    Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+        }
+
+        TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead, permCheckWrite);
         return false;  // No work to do.
     }
 
@@ -67,6 +81,8 @@
 
         private CountDownLatch mLatch;
         private JobParameters mExecutedJobParameters;
+        private int mExecutedPermCheckRead;
+        private int mExecutedPermCheckWrite;
 
         public static TestEnvironment getTestEnvironment() {
             if (kTestEnvironment == null) {
@@ -79,6 +95,14 @@
             return mExecutedJobParameters;
         }
 
+        public int getLastPermCheckRead() {
+            return mExecutedPermCheckRead;
+        }
+
+        public int getLastPermCheckWrite() {
+            return mExecutedPermCheckWrite;
+        }
+
         /**
          * Block the test thread, waiting on the JobScheduler to execute some previously scheduled
          * job on this service.
@@ -97,9 +121,11 @@
             return !mLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
         }
 
-        private void notifyExecution(JobParameters params) {
-            Log.d(TAG, "Job executed:" + params.getJobId());
+        private void notifyExecution(JobParameters params, int permCheckRead, int permCheckWrite) {
+            //Log.d(TAG, "Job executed:" + params.getJobId());
             mExecutedJobParameters = params;
+            mExecutedPermCheckRead = permCheckRead;
+            mExecutedPermCheckWrite = permCheckWrite;
             mLatch.countDown();
         }
 
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/ClipDataJobTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/ClipDataJobTest.java
new file mode 100644
index 0000000..9a8b642
--- /dev/null
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/ClipDataJobTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2017 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.jobscheduler.cts;
+
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.content.ClipData;
+import android.content.ContentProviderClient;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.SystemClock;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+/**
+ * Schedules jobs with the {@link android.app.job.JobScheduler} that grant permissions through
+ * ClipData.
+ */
+@TargetApi(26)
+public class ClipDataJobTest extends ConstraintTest {
+    private static final String TAG = "ClipDataJobTest";
+
+    /** Unique identifier for the job scheduled by this suite of tests. */
+    public static final int CLIP_DATA_JOB_ID = ClipDataJobTest.class.hashCode();
+
+    static final String MY_PACKAGE = "android.jobscheduler.cts";
+
+    static final String JOBPERM_PACKAGE = "android.jobscheduler.cts.jobperm";
+    static final String JOBPERM_AUTHORITY = "android.jobscheduler.cts.jobperm.provider";
+    static final String JOBPERM_PERM = "android.jobscheduler.cts.jobperm.perm";
+
+    private JobInfo.Builder mBuilder;
+    private Uri mFirstUri;
+    private Bundle mFirstUriBundle;
+    private ClipData mClipData;
+    private ContentProviderClient mProvider;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mBuilder = new JobInfo.Builder(CLIP_DATA_JOB_ID, kJobServiceComponent);
+        mFirstUri = Uri.parse("content://" + JOBPERM_AUTHORITY + "/protected/foo");
+        mFirstUriBundle = new Bundle();
+        mFirstUriBundle.putParcelable("uri", mFirstUri);
+        mClipData = new ClipData("JobPerm", new String[] { "application/*" },
+                new ClipData.Item(mFirstUri));
+        mProvider = getContext().getContentResolver().acquireContentProviderClient(mFirstUri);
+        String res = SystemUtil.runShellCommand(getInstrumentation(), "cmd activity set-inactive "
+                + mContext.getPackageName() + " false");
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mProvider.close();
+        mJobScheduler.cancel(CLIP_DATA_JOB_ID);
+        // Put storage service back in to normal operation.
+        SystemUtil.runShellCommand(getInstrumentation(), "cmd devicestoragemonitor reset");
+    }
+
+    // Note we are just using storage state as a way to control when the job gets executed.
+    void setStorageState(boolean low) throws Exception {
+        String res;
+        if (low) {
+            res = SystemUtil.runShellCommand(getInstrumentation(),
+                    "cmd devicestoragemonitor force-low -f");
+        } else {
+            res = SystemUtil.runShellCommand(getInstrumentation(),
+                    "cmd devicestoragemonitor force-not-low -f");
+        }
+        int seq = Integer.parseInt(res.trim());
+        long startTime = SystemClock.elapsedRealtime();
+
+        // Wait for the storage update to be processed by job scheduler before proceeding.
+        int curSeq;
+        do {
+            curSeq = Integer.parseInt(SystemUtil.runShellCommand(getInstrumentation(),
+                    "cmd jobscheduler get-storage-seq").trim());
+            if (curSeq == seq) {
+                return;
+            }
+        } while ((SystemClock.elapsedRealtime()-startTime) < 1000);
+
+        fail("Timed out waiting for job scheduler: expected seq=" + seq + ", cur=" + curSeq);
+    }
+
+    void waitPermissionRevoke(Uri uri, int access, long timeout) {
+        long startTime = SystemClock.elapsedRealtime();
+        while (getContext().checkUriPermission(uri, Process.myPid(), Process.myUid(), access)
+                 != PackageManager.PERMISSION_GRANTED) {
+            try {
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+            }
+            if ((SystemClock.elapsedRealtime()-startTime) >= timeout) {
+                fail("Timed out waiting for permission revoke");
+            }
+        }
+    }
+
+    /**
+     * Test basic granting of URI permissions associated with jobs.
+     */
+    public void testClipDataGrant() throws Exception {
+        // Start out with storage low, so job is enqueued but not executed yet.
+        setStorageState(true);
+
+        // We need to get a permission grant so that we can grant it to ourself.
+        mProvider.call("grant", MY_PACKAGE, mFirstUriBundle);
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+
+        // Schedule the job, the system should now also be holding a URI grant for us.
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(mBuilder.setRequiresStorageNotLow(true)
+                .setClipData(mClipData, Intent.FLAG_GRANT_READ_URI_PERMISSION
+                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION).build());
+
+        // Remove the explicit grant, we should still have a grant due to the job.
+        mProvider.call("revoke", MY_PACKAGE, mFirstUriBundle);
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+
+        // Now allow the job to run and wait for it.
+        setStorageState(false);
+        assertTrue("Job with storage not low constraint did not fire when storage not low.",
+                kTestEnvironment.awaitExecution());
+
+        // Make sure the job still had the permission granted.
+        assertEquals(PackageManager.PERMISSION_GRANTED, kTestEnvironment.getLastPermCheckRead());
+        assertEquals(PackageManager.PERMISSION_GRANTED, kTestEnvironment.getLastPermCheckWrite());
+
+        // And wait for everything to be cleaned up.
+        waitPermissionRevoke(mFirstUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, 5000);
+    }
+
+    /**
+     * Test that we correctly fail when trying to grant permissions to things we don't
+     * have access to.
+     */
+    public void testClipDataGrant_Failed() throws Exception {
+        try {
+            mJobScheduler.schedule(mBuilder.setRequiresStorageNotLow(true)
+                    .setClipData(mClipData, Intent.FLAG_GRANT_READ_URI_PERMISSION
+                            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION).build());
+        } catch (SecurityException e) {
+            return;
+        }
+
+        fail("Security exception not thrown");
+    }
+
+    /**
+     * Test basic granting of URI permissions associated with jobs and are correctly
+     * retained when rescheduling the job.
+     */
+    public void testClipDataGrantReschedule() throws Exception {
+        // We need to get a permission grant so that we can grant it to ourself.
+        mProvider.call("grant", MY_PACKAGE, mFirstUriBundle);
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+
+        // Schedule the job, the system should now also be holding a URI grant for us.
+        kTestEnvironment.setExpectedExecutions(1);
+        mJobScheduler.schedule(mBuilder.setMinimumLatency(60*60*1000)
+                .setClipData(mClipData, Intent.FLAG_GRANT_READ_URI_PERMISSION
+                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION).build());
+
+        // Remove the explicit grant, we should still have a grant due to the job.
+        mProvider.call("revoke", MY_PACKAGE, mFirstUriBundle);
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_READ_URI_PERMISSION));
+        assertEquals(PackageManager.PERMISSION_GRANTED,
+                getContext().checkUriPermission(mFirstUri, Process.myPid(),
+                        Process.myUid(), Intent.FLAG_GRANT_WRITE_URI_PERMISSION));
+
+        // Now reschedule the job to have it happen right now.
+        mJobScheduler.schedule(mBuilder.setMinimumLatency(0)
+                .setClipData(mClipData, Intent.FLAG_GRANT_READ_URI_PERMISSION
+                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION).build());
+        assertTrue("Job with storage not low constraint did not fire when storage not low.",
+                kTestEnvironment.awaitExecution());
+
+        // Make sure the job still had the permission granted.
+        assertEquals(PackageManager.PERMISSION_GRANTED, kTestEnvironment.getLastPermCheckRead());
+        assertEquals(PackageManager.PERMISSION_GRANTED, kTestEnvironment.getLastPermCheckWrite());
+
+        // And wait for everything to be cleaned up.
+        waitPermissionRevoke(mFirstUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, 5000);
+    }
+}
diff --git a/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java b/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java
index c72af8f..f40dd66 100644
--- a/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java
+++ b/tests/JobScheduler/src/android/jobscheduler/cts/StorageConstraintTest.java
@@ -24,7 +24,7 @@
 import com.android.compatibility.common.util.SystemUtil;
 
 /**
- * Schedules jobs with the {@link android.app.job.JobScheduler} that have battery constraints.
+ * Schedules jobs with the {@link android.app.job.JobScheduler} that have storage constraints.
  */
 @TargetApi(26)
 public class StorageConstraintTest extends ConstraintTest {
@@ -63,7 +63,7 @@
         int seq = Integer.parseInt(res.trim());
         long startTime = SystemClock.elapsedRealtime();
 
-        // Wait for the battery update to be processed by job scheduler before proceeding.
+        // Wait for the storage update to be processed by job scheduler before proceeding.
         int curSeq;
         do {
             curSeq = Integer.parseInt(SystemUtil.runShellCommand(getInstrumentation(),