blob: 964853cde69559679cc5e38af80b60ca3d530d5d [file] [log] [blame]
/*
* 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.jobscheduler;
import android.annotation.TargetApi;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.job.JobWorkItem;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Process;
import android.util.Log;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Handles callback from the framework {@link android.app.job.JobScheduler}. The behaviour of this
* class is configured through the static
* {@link TestEnvironment}.
*/
@TargetApi(21)
public class MockJobService extends JobService {
private static final String TAG = "MockJobService";
/** Wait this long before timing out the test. */
private static final long DEFAULT_TIMEOUT_MILLIS = 30000L; // 30 seconds.
private JobParameters mParams;
ArrayList<JobWorkItem> mReceivedWork = new ArrayList<>();
ArrayList<JobWorkItem> mPendingCompletions = new ArrayList<>();
private boolean mWaitingForStop;
@Override
public void onDestroy() {
super.onDestroy();
Log.i(TAG, "Destroying test service");
if (TestEnvironment.getTestEnvironment().getExpectedWork() != null) {
TestEnvironment.getTestEnvironment().notifyExecution(mParams, 0, 0, mReceivedWork,
null);
}
}
@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Created test service.");
}
@Override
public boolean onStartJob(JobParameters params) {
Log.i(TAG, "Test job executing: " + params.getJobId());
mParams = 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);
}
TestWorkItem[] expectedWork = TestEnvironment.getTestEnvironment().getExpectedWork();
if (expectedWork != null) {
try {
if (!TestEnvironment.getTestEnvironment().awaitDoWork()) {
TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
permCheckWrite, null, "Spent too long waiting to start executing work");
return false;
}
} catch (InterruptedException e) {
TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
permCheckWrite, null, "Failed waiting for work: " + e);
return false;
}
JobWorkItem work;
int index = 0;
while ((work = params.dequeueWork()) != null) {
Log.i(TAG, "Received work #" + index + ": " + work.getIntent());
mReceivedWork.add(work);
int flags = 0;
if (index < expectedWork.length) {
TestWorkItem expected = expectedWork[index];
int grantFlags = work.getIntent().getFlags();
if (expected.requireUrisGranted != null) {
for (int ui = 0; ui < expected.requireUrisGranted.length; ui++) {
if ((grantFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
if (checkUriPermission(expected.requireUrisGranted[ui],
Process.myPid(), Process.myUid(),
Intent.FLAG_GRANT_READ_URI_PERMISSION)
!= PackageManager.PERMISSION_GRANTED) {
TestEnvironment.getTestEnvironment().notifyExecution(params,
permCheckRead, permCheckWrite, null,
"Expected read permission but not granted: "
+ expected.requireUrisGranted[ui]
+ " @ #" + index);
return false;
}
}
if ((grantFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
if (checkUriPermission(expected.requireUrisGranted[ui],
Process.myPid(), Process.myUid(),
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
!= PackageManager.PERMISSION_GRANTED) {
TestEnvironment.getTestEnvironment().notifyExecution(params,
permCheckRead, permCheckWrite, null,
"Expected write permission but not granted: "
+ expected.requireUrisGranted[ui]
+ " @ #" + index);
return false;
}
}
}
}
if (expected.requireUrisNotGranted != null) {
// XXX note no delay here, current impl will have fully revoked the
// permission by the time we return from completing the last work.
for (int ui = 0; ui < expected.requireUrisNotGranted.length; ui++) {
if ((grantFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
if (checkUriPermission(expected.requireUrisNotGranted[ui],
Process.myPid(), Process.myUid(),
Intent.FLAG_GRANT_READ_URI_PERMISSION)
!= PackageManager.PERMISSION_DENIED) {
TestEnvironment.getTestEnvironment().notifyExecution(params,
permCheckRead, permCheckWrite, null,
"Not expected read permission but granted: "
+ expected.requireUrisNotGranted[ui]
+ " @ #" + index);
return false;
}
}
if ((grantFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
if (checkUriPermission(expected.requireUrisNotGranted[ui],
Process.myPid(), Process.myUid(),
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
!= PackageManager.PERMISSION_DENIED) {
TestEnvironment.getTestEnvironment().notifyExecution(params,
permCheckRead, permCheckWrite, null,
"Not expected write permission but granted: "
+ expected.requireUrisNotGranted[ui]
+ " @ #" + index);
return false;
}
}
}
}
flags = expected.flags;
if ((flags & TestWorkItem.FLAG_WAIT_FOR_STOP) != 0) {
Log.i(TAG, "Now waiting to stop");
mWaitingForStop = true;
TestEnvironment.getTestEnvironment().notifyWaitingForStop();
return true;
}
if ((flags & TestWorkItem.FLAG_COMPLETE_NEXT) != 0) {
if (!processNextPendingCompletion()) {
TestEnvironment.getTestEnvironment().notifyExecution(params,
0, 0, null,
"Expected to complete next pending work but there was none: "
+ " @ #" + index);
return false;
}
}
}
if ((flags & TestWorkItem.FLAG_DELAY_COMPLETE_PUSH_BACK) != 0) {
mPendingCompletions.add(work);
} else if ((flags & TestWorkItem.FLAG_DELAY_COMPLETE_PUSH_TOP) != 0) {
mPendingCompletions.add(0, work);
} else {
mParams.completeWork(work);
}
if (index < expectedWork.length) {
TestWorkItem expected = expectedWork[index];
if (expected.subitems != null) {
final TestWorkItem[] sub = expected.subitems;
final JobInfo ji = expected.jobInfo;
final JobScheduler js = (JobScheduler) getSystemService(
Context.JOB_SCHEDULER_SERVICE);
for (int subi = 0; subi < sub.length; subi++) {
js.enqueue(ji, new JobWorkItem(sub[subi].intent));
}
}
}
index++;
}
if (processNextPendingCompletion()) {
// We had some pending completions, clean them all out...
while (processNextPendingCompletion()) {
}
// ...and we need to do a final dequeue to complete the job, which should not
// return any remaining work.
if ((work = params.dequeueWork()) != null) {
TestEnvironment.getTestEnvironment().notifyExecution(params,
0, 0, null,
"Expected no remaining work after dequeue pending, but got: " + work);
}
}
Log.i(TAG, "Done with all work at #" + index);
// We don't notifyExecution here because we want to make sure the job properly
// stops itself.
return true;
} else {
boolean continueAfterStart
= TestEnvironment.getTestEnvironment().handleContinueAfterStart();
try {
if (!TestEnvironment.getTestEnvironment().awaitDoJob()) {
TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
permCheckWrite, null, "Spent too long waiting to start job");
return false;
}
} catch (InterruptedException e) {
TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
permCheckWrite, null, "Failed waiting to start job: " + e);
return false;
}
TestEnvironment.getTestEnvironment().notifyExecution(params, permCheckRead,
permCheckWrite, null, null);
return continueAfterStart;
}
}
boolean processNextPendingCompletion() {
if (mPendingCompletions.size() <= 0) {
return false;
}
JobWorkItem next = mPendingCompletions.remove(0);
mParams.completeWork(next);
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.i(TAG, "Received stop callback");
TestEnvironment.getTestEnvironment().notifyStopped();
return mWaitingForStop;
}
public static final class TestWorkItem {
/**
* Stop processing work for now, waiting for the service to be stopped.
*/
public static final int FLAG_WAIT_FOR_STOP = 1<<0;
/**
* Don't complete this work now, instead push it on the back of the stack of
* pending completions.
*/
public static final int FLAG_DELAY_COMPLETE_PUSH_BACK = 1<<1;
/**
* Don't complete this work now, instead insert to the top of the stack of
* pending completions.
*/
public static final int FLAG_DELAY_COMPLETE_PUSH_TOP = 1<<2;
/**
* Complete next pending completion on the stack before completing this one.
*/
public static final int FLAG_COMPLETE_NEXT = 1<<3;
public final Intent intent;
public final JobInfo jobInfo;
public final int flags;
public final int deliveryCount;
public final TestWorkItem[] subitems;
public final Uri[] requireUrisGranted;
public final Uri[] requireUrisNotGranted;
public TestWorkItem(Intent _intent) {
intent = _intent;
jobInfo = null;
flags = 0;
deliveryCount = 1;
subitems = null;
requireUrisGranted = null;
requireUrisNotGranted = null;
}
public TestWorkItem(Intent _intent, int _flags) {
intent = _intent;
jobInfo = null;
flags = _flags;
deliveryCount = 1;
subitems = null;
requireUrisGranted = null;
requireUrisNotGranted = null;
}
public TestWorkItem(Intent _intent, int _flags, int _deliveryCount) {
intent = _intent;
jobInfo = null;
flags = _flags;
deliveryCount = _deliveryCount;
subitems = null;
requireUrisGranted = null;
requireUrisNotGranted = null;
}
public TestWorkItem(Intent _intent, JobInfo _jobInfo, TestWorkItem[] _subitems) {
intent = _intent;
jobInfo = _jobInfo;
flags = 0;
deliveryCount = 1;
subitems = _subitems;
requireUrisGranted = null;
requireUrisNotGranted = null;
}
public TestWorkItem(Intent _intent, Uri[] _requireUrisGranted,
Uri[] _requireUrisNotGranted) {
intent = _intent;
jobInfo = null;
flags = 0;
deliveryCount = 1;
subitems = null;
requireUrisGranted = _requireUrisGranted;
requireUrisNotGranted = _requireUrisNotGranted;
}
@Override
public String toString() {
return "TestWorkItem { " + intent + " dc=" + deliveryCount + " }";
}
}
/**
* Configures the expected behaviour for each test. This object is shared across consecutive
* tests, so to clear state each test is responsible for calling
* {@link TestEnvironment#setUp()}.
*/
public static final class TestEnvironment {
private static TestEnvironment kTestEnvironment;
//public static final int INVALID_JOB_ID = -1;
private CountDownLatch mLatch;
private CountDownLatch mWaitingForStopLatch;
private CountDownLatch mDoJobLatch;
private CountDownLatch mStoppedLatch;
private CountDownLatch mDoWorkLatch;
private TestWorkItem[] mExpectedWork;
private boolean mContinueAfterStart;
private JobParameters mExecutedJobParameters;
private int mExecutedPermCheckRead;
private int mExecutedPermCheckWrite;
private ArrayList<JobWorkItem> mExecutedReceivedWork;
private String mExecutedErrorMessage;
public static TestEnvironment getTestEnvironment() {
if (kTestEnvironment == null) {
kTestEnvironment = new TestEnvironment();
}
return kTestEnvironment;
}
public TestWorkItem[] getExpectedWork() {
return mExpectedWork;
}
public JobParameters getLastJobParameters() {
return mExecutedJobParameters;
}
public int getLastPermCheckRead() {
return mExecutedPermCheckRead;
}
public int getLastPermCheckWrite() {
return mExecutedPermCheckWrite;
}
public ArrayList<JobWorkItem> getLastReceivedWork() {
return mExecutedReceivedWork;
}
public String getLastErrorMessage() {
return mExecutedErrorMessage;
}
/**
* Block the test thread, waiting on the JobScheduler to execute some previously scheduled
* job on this service.
*/
public boolean awaitExecution() throws InterruptedException {
return awaitExecution(DEFAULT_TIMEOUT_MILLIS);
}
public boolean awaitExecution(long timeoutMillis) throws InterruptedException {
final boolean executed = mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
if (getLastErrorMessage() != null) {
Assert.fail(getLastErrorMessage());
}
return executed;
}
/**
* Block the test thread, expecting to timeout but still listening to ensure that no jobs
* land in the interim.
* @return True if the latch timed out waiting on an execution.
*/
public boolean awaitTimeout() throws InterruptedException {
return awaitTimeout(DEFAULT_TIMEOUT_MILLIS);
}
public boolean awaitTimeout(long timeoutMillis) throws InterruptedException {
return !mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
}
public boolean awaitWaitingForStop() throws InterruptedException {
return mWaitingForStopLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
public boolean awaitDoWork() throws InterruptedException {
return mDoWorkLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
public boolean awaitDoJob() throws InterruptedException {
if (mDoJobLatch == null) {
return true;
}
return mDoJobLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
public boolean awaitStopped() throws InterruptedException {
return mStoppedLatch.await(DEFAULT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
}
private void notifyExecution(JobParameters params, int permCheckRead, int permCheckWrite,
ArrayList<JobWorkItem> receivedWork, String errorMsg) {
//Log.d(TAG, "Job executed:" + params.getJobId());
mExecutedJobParameters = params;
mExecutedPermCheckRead = permCheckRead;
mExecutedPermCheckWrite = permCheckWrite;
mExecutedReceivedWork = receivedWork;
mExecutedErrorMessage = errorMsg;
mLatch.countDown();
}
private void notifyWaitingForStop() {
mWaitingForStopLatch.countDown();
}
private void notifyStopped() {
if (mStoppedLatch != null) {
mStoppedLatch.countDown();
}
}
public void setExpectedExecutions(int numExecutions) {
// For no executions expected, set count to 1 so we can still block for the timeout.
if (numExecutions == 0) {
mLatch = new CountDownLatch(1);
} else {
mLatch = new CountDownLatch(numExecutions);
}
mWaitingForStopLatch = null;
mDoJobLatch = null;
mStoppedLatch = null;
mDoWorkLatch = null;
mExpectedWork = null;
mContinueAfterStart = false;
}
public void setExpectedWaitForStop() {
mWaitingForStopLatch = new CountDownLatch(1);
}
public void setExpectedWork(TestWorkItem[] work) {
mExpectedWork = work;
mDoWorkLatch = new CountDownLatch(1);
}
public void setExpectedStopped() {
mStoppedLatch = new CountDownLatch(1);
}
public void readyToWork() {
mDoWorkLatch.countDown();
}
public void setExpectedWaitForRun() {
mDoJobLatch = new CountDownLatch(1);
}
public void readyToRun() {
mDoJobLatch.countDown();
}
public void setContinueAfterStart() {
mContinueAfterStart = true;
}
public boolean handleContinueAfterStart() {
boolean res = mContinueAfterStart;
mContinueAfterStart = false;
return res;
}
/** Called in each testCase#setup */
public void setUp() {
mLatch = null;
mExecutedJobParameters = null;
}
}
}