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(),