From 5db09084c8e4efc6311754243c39962fc8e7a766 Mon Sep 17 00:00:00 2001 From: Shreyas Basarge Date: Thu, 7 Jan 2016 13:38:29 +0000 Subject: [PATCH] Added priority to JobScheduler scheduling Priority can be assigned to jobs. Higher priority jobs can preempt lower priority ones. Reason for calling onStopJob (timeout, preempt, etc.) is set on the JobParameters object. Reference: https://docs.google.com/document/d/1fuVO5rBCkODx8wjk6uulFCP1Uzfx7IVsw2EyKKrGqVA Change-Id: Ic36016514cec076984d44086316d8d00d896b3aa --- core/java/android/app/job/JobInfo.java | 18 ++ core/java/android/app/job/JobParameters.java | 27 ++ .../server/job/JobSchedulerService.java | 286 +++++++++++------- .../android/server/job/JobServiceContext.java | 45 ++- .../java/com/android/server/job/JobStore.java | 22 +- .../server/job/controllers/JobStatus.java | 4 + .../tests/servicestests/AndroidManifest.xml | 2 + .../com/android/server/job/JobStoreTest.java | 16 +- .../server/job/MockPriorityJobService.java | 106 +++++++ .../server/job/PrioritySchedulingTest.java | 119 ++++++++ 10 files changed, 513 insertions(+), 132 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/job/MockPriorityJobService.java create mode 100644 services/tests/servicestests/src/com/android/server/job/PrioritySchedulingTest.java diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java index 0d9e7787a236c..5aa4f799fb29c 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -85,6 +85,7 @@ public class JobInfo implements Parcelable { private final long intervalMillis; private final long initialBackoffMillis; private final int backoffPolicy; + private final int priority; /** * Unique job id associated with this class. This is assigned to your job by the scheduler. @@ -107,6 +108,11 @@ public class JobInfo implements Parcelable { return service; } + /** @hide */ + public int getPriority() { + return priority; + } + /** * Whether this job needs the device to be plugged in. */ @@ -220,6 +226,7 @@ public class JobInfo implements Parcelable { backoffPolicy = in.readInt(); hasEarlyConstraint = in.readInt() == 1; hasLateConstraint = in.readInt() == 1; + priority = in.readInt(); } private JobInfo(JobInfo.Builder b) { @@ -238,6 +245,7 @@ public class JobInfo implements Parcelable { backoffPolicy = b.mBackoffPolicy; hasEarlyConstraint = b.mHasEarlyConstraint; hasLateConstraint = b.mHasLateConstraint; + priority = b.mPriority; } @Override @@ -262,6 +270,7 @@ public class JobInfo implements Parcelable { out.writeInt(backoffPolicy); out.writeInt(hasEarlyConstraint ? 1 : 0); out.writeInt(hasLateConstraint ? 1 : 0); + out.writeInt(priority); } public static final Creator CREATOR = new Creator() { @@ -286,6 +295,7 @@ public class JobInfo implements Parcelable { private int mJobId; private PersistableBundle mExtras = PersistableBundle.EMPTY; private ComponentName mJobService; + private int mPriority; // Requirements. private boolean mRequiresCharging; private boolean mRequiresDeviceIdle; @@ -317,6 +327,14 @@ public class JobInfo implements Parcelable { mJobId = jobId; } + /** + * @hide + */ + public Builder setPriority(int priority) { + mPriority = priority; + return this; + } + /** * Set optional extras. This is persisted, so we only allow primitive types. * @param extras Bundle containing extras you want the scheduler to hold on to for you. diff --git a/core/java/android/app/job/JobParameters.java b/core/java/android/app/job/JobParameters.java index 7ee39f5cd9399..a0a60e8c53a02 100644 --- a/core/java/android/app/job/JobParameters.java +++ b/core/java/android/app/job/JobParameters.java @@ -28,10 +28,22 @@ import android.os.PersistableBundle; */ public class JobParameters implements Parcelable { + /** @hide */ + public static final int REASON_CANCELED = 0; + /** @hide */ + public static final int REASON_CONSTRAINTS_NOT_SATISFIED = 1; + /** @hide */ + public static final int REASON_PREEMPT = 2; + /** @hide */ + public static final int REASON_TIMEOUT = 3; + /** @hide */ + public static final int REASON_DEVICE_IDLE = 4; + private final int jobId; private final PersistableBundle extras; private final IBinder callback; private final boolean overrideDeadlineExpired; + private int stopReason; // Default value of stopReason is REASON_CANCELED /** @hide */ public JobParameters(IBinder callback, int jobId, PersistableBundle extras, @@ -49,6 +61,14 @@ public class JobParameters implements Parcelable { return jobId; } + /** + * Reason onStopJob() was called on this job. + * @hide + */ + public int getStopReason() { + return stopReason; + } + /** * @return The extras you passed in when constructing this job with * {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will @@ -78,6 +98,12 @@ public class JobParameters implements Parcelable { extras = in.readPersistableBundle(); callback = in.readStrongBinder(); overrideDeadlineExpired = in.readInt() == 1; + stopReason = in.readInt(); + } + + /** @hide */ + public void setStopReason(int reason) { + stopReason = reason; } @Override @@ -91,6 +117,7 @@ public class JobParameters implements Parcelable { dest.writePersistableBundle(extras); dest.writeStrongBinder(callback); dest.writeInt(overrideDeadlineExpired ? 1 : 0); + dest.writeInt(stopReason); } public static final Creator CREATOR = new Creator() { diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java index 4eabe3684e4f7..3113b757b7a76 100644 --- a/services/core/java/com/android/server/job/JobSchedulerService.java +++ b/services/core/java/com/android/server/job/JobSchedulerService.java @@ -16,14 +16,21 @@ package com.android.server.job; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + import android.app.ActivityManager; import android.app.ActivityManagerNative; import android.app.AppGlobals; import android.app.IUidObserver; -import android.app.job.IJobScheduler; import android.app.job.JobInfo; +import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; +import android.app.job.IJobScheduler; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -47,7 +54,6 @@ import android.util.Slog; import android.util.SparseArray; import com.android.internal.app.IBatteryStats; -import com.android.internal.util.ArrayUtils; import com.android.server.DeviceIdleController; import com.android.server.LocalServices; import com.android.server.job.controllers.AppIdleController; @@ -58,15 +64,6 @@ import com.android.server.job.controllers.JobStatus; import com.android.server.job.controllers.StateController; import com.android.server.job.controllers.TimeController; -import libcore.util.EmptyArray; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - /** * Responsible for taking jobs representing work to be performed by a client app, and determining * based on the criteria specified when that job should be run against the client application's @@ -130,7 +127,7 @@ public class JobSchedulerService extends com.android.server.SystemService */ final ArrayList mPendingJobs = new ArrayList<>(); - int[] mStartedUsers = EmptyArray.INT; + final ArrayList mStartedUsers = new ArrayList<>(); final JobHandler mHandler; final JobSchedulerStub mJobSchedulerStub; @@ -161,9 +158,8 @@ public class JobSchedulerService extends com.android.server.SystemService private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - Slog.d(TAG, "Receieved: " + action); - if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { + Slog.d(TAG, "Receieved: " + intent.getAction()); + if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { // If this is an outright uninstall rather than the first half of an // app update sequence, cancel the jobs associated with the app. if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { @@ -173,21 +169,18 @@ public class JobSchedulerService extends com.android.server.SystemService } cancelJobsForUid(uidRemoved, true); } - } else if (Intent.ACTION_USER_REMOVED.equals(action)) { + } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); if (DEBUG) { Slog.d(TAG, "Removing jobs for user: " + userId); } cancelJobsForUser(userId); - } else if (PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED.equals(action) - || PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(action)) { + } else if (PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction()) + || PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction())) { updateIdleMode(mPowerManager != null ? (mPowerManager.isDeviceIdleMode() - || mPowerManager.isLightDeviceIdleMode()) + || mPowerManager.isLightDeviceIdleMode()) : false); - } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { - // Kick off pending jobs for any apps that re-appeared - mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); } } }; @@ -209,20 +202,14 @@ public class JobSchedulerService extends com.android.server.SystemService @Override public void onStartUser(int userHandle) { - mStartedUsers = ArrayUtils.appendInt(mStartedUsers, userHandle); - // Let's kick any outstanding jobs for this user. - mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); - } - - @Override - public void onUnlockUser(int userHandle) { + mStartedUsers.add(userHandle); // Let's kick any outstanding jobs for this user. mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); } @Override public void onStopUser(int userHandle) { - mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle); + mStartedUsers.remove(Integer.valueOf(userHandle)); } /** @@ -329,7 +316,7 @@ public class JobSchedulerService extends com.android.server.SystemService // Remove from pending queue. mPendingJobs.remove(cancelled); // Cancel if running. - stopJobOnServiceContextLocked(cancelled); + stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED); reportActive(); } } @@ -357,7 +344,7 @@ public class JobSchedulerService extends com.android.server.SystemService JobServiceContext jsc = mActiveServices.get(i); final JobStatus executing = jsc.getRunningJob(); if (executing != null) { - jsc.cancelExecutingJob(); + jsc.cancelExecutingJob(JobParameters.REASON_DEVICE_IDLE); } } } else { @@ -382,7 +369,7 @@ public class JobSchedulerService extends com.android.server.SystemService if (mPendingJobs.size() <= 0) { for (int i=0; i jobs = mJobs.getJobs(); + mPendingJobs.clear(); if (DEBUG) { Slog.d(TAG, "queuing all ready jobs for execution:"); } @@ -740,8 +721,9 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, " queued " + job.toShortString()); } mPendingJobs.add(job); - } else if (isReadyToBeCancelledLocked(job)) { - stopJobOnServiceContextLocked(job); + } else if (areJobConstraintsNotSatisfied(job)) { + stopJobOnServiceContextLocked(job, + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } if (DEBUG) { @@ -764,8 +746,9 @@ public class JobSchedulerService extends com.android.server.SystemService * TODO: It would be nice to consolidate these sort of high-level policies somewhere. */ private void maybeQueueReadyJobsForExecutionLockedH() { + mPendingJobs.clear(); int chargingCount = 0; - int idleCount = 0; + int idleCount = 0; int backoffCount = 0; int connectivityCount = 0; List runnableJobs = null; @@ -800,8 +783,9 @@ public class JobSchedulerService extends com.android.server.SystemService runnableJobs = new ArrayList<>(); } runnableJobs.add(job); - } else if (isReadyToBeCancelledLocked(job)) { - stopJobOnServiceContextLocked(job); + } else if (areJobConstraintsNotSatisfied(job)) { + stopJobOnServiceContextLocked(job, + JobParameters.REASON_CONSTRAINTS_NOT_SATISFIED); } } if (backoffCount > 0 || @@ -820,11 +804,6 @@ public class JobSchedulerService extends com.android.server.SystemService Slog.d(TAG, "maybeQueueReadyJobsForExecutionLockedH: Not running anything."); } } - if (DEBUG) { - Slog.d(TAG, "idle=" + idleCount + " connectivity=" + - connectivityCount + " charging=" + chargingCount + " tot=" + - runnableJobs.size()); - } } /** @@ -833,31 +812,18 @@ public class JobSchedulerService extends com.android.server.SystemService * - It's not pending. * - It's not already running on a JSC. * - The user that requested the job is running. - * - The component is enabled and runnable. */ private boolean isReadyToBeExecutedLocked(JobStatus job) { final boolean jobReady = job.isReady(); final boolean jobPending = mPendingJobs.contains(job); final boolean jobActive = isCurrentlyActiveLocked(job); - - final int userId = job.getUserId(); - final boolean userStarted = ArrayUtils.contains(mStartedUsers, userId); - final boolean componentPresent; - try { - componentPresent = (AppGlobals.getPackageManager().getServiceInfo( - job.getServiceComponent(), PackageManager.MATCH_DEBUG_TRIAGED_MISSING, - userId) != null); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - + final boolean userRunning = mStartedUsers.contains(job.getUserId()); if (DEBUG) { Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString() + " ready=" + jobReady + " pending=" + jobPending - + " active=" + jobActive + " userStarted=" + userStarted - + " componentPresent=" + componentPresent); + + " active=" + jobActive + " userRunning=" + userRunning); } - return userStarted && componentPresent && jobReady && !jobPending && !jobActive; + return userRunning && jobReady && !jobPending && !jobActive; } /** @@ -865,7 +831,7 @@ public class JobSchedulerService extends com.android.server.SystemService * - It's not ready * - It's running on a JSC. */ - private boolean isReadyToBeCancelledLocked(JobStatus job) { + private boolean areJobConstraintsNotSatisfied(JobStatus job) { return !job.isReady() && isCurrentlyActiveLocked(job); } @@ -880,45 +846,120 @@ public class JobSchedulerService extends com.android.server.SystemService // If device is idle, we will not schedule jobs to run. return; } - Iterator it = mPendingJobs.iterator(); if (DEBUG) { Slog.d(TAG, "pending queue: " + mPendingJobs.size() + " jobs."); } - while (it.hasNext()) { - JobStatus nextPending = it.next(); - JobServiceContext availableContext = null; - for (int i=0; i it = mPendingJobs.iterator(); + while (it.hasNext()) { + JobStatus nextPending = it.next(); + + // If job is already running, go to next job. + int jobRunningContext = findJobContextIdFromMap(nextPending, contextIdToJobMap); + if (jobRunningContext != -1) { + continue; + } + + // Find a context for nextPending. The context should be available OR + // it should have lowest priority among all running jobs + // (sharing the same Uid as nextPending) + int minPriority = Integer.MAX_VALUE; + int minPriorityContextId = -1; + for (int i=0; i= nextPending.getPriority()) { + continue; + } + if (minPriority > nextPending.getPriority()) { + minPriority = nextPending.getPriority(); + minPriorityContextId = i; + } + } + if (minPriorityContextId != -1) { + contextIdToJobMap[minPriorityContextId] = nextPending; + act[minPriorityContextId] = true; + } + } + if (DEBUG) { + Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs final")); + } + for (int i=0; i it = mPendingJobs.iterator(); + while (it.hasNext()) { + JobStatus js = it.next(); + s.append("(") + .append(js.getJob().getId()) + .append(", ") + .append(js.getUid()) + .append(") "); + } + return s.toString(); } void dumpInternal(PrintWriter pw) { final long now = SystemClock.elapsedRealtime(); synchronized (mJobs) { - pw.println("Started users: " + Arrays.toString(mStartedUsers)); + pw.print("Started users: "); + for (int i=0; i 0) { ArraySet jobs = mJobs.getJobs(); @@ -1070,15 +1139,12 @@ public class JobSchedulerService extends com.android.server.SystemService mControllers.get(i).dumpControllerState(pw); } pw.println(); - pw.println("Pending:"); - for (int i=0; i keySet = bundle.keySet(); for (String key: keySet) { - PersistableBundle b = copy.getPersistableBundle(key); - if (b != null) { - PersistableBundle bCopy = deepCopyBundle(b, maxDepth-1); + Object o = copy.get(key); + if (o instanceof PersistableBundle) { + PersistableBundle bCopy = deepCopyBundle((PersistableBundle) o, maxDepth-1); copy.putPersistableBundle(key, bCopy); } } @@ -540,11 +543,16 @@ public class JobStore { JobInfo.Builder jobBuilder; int uid; - // Read out job identifier attributes. + // Read out job identifier attributes and priority. try { jobBuilder = buildBuilderFromXml(parser); jobBuilder.setPersisted(true); uid = Integer.valueOf(parser.getAttributeValue(null, "uid")); + + String priority = parser.getAttributeValue(null, "priority"); + if (priority != null) { + jobBuilder.setPriority(Integer.valueOf(priority)); + } } catch (NumberFormatException e) { Slog.e(TAG, "Error parsing job's required fields, skipping"); return null; diff --git a/services/core/java/com/android/server/job/controllers/JobStatus.java b/services/core/java/com/android/server/job/controllers/JobStatus.java index c02611f4fdcee..c8ab9c0fd0c4d 100644 --- a/services/core/java/com/android/server/job/controllers/JobStatus.java +++ b/services/core/java/com/android/server/job/controllers/JobStatus.java @@ -165,6 +165,10 @@ public class JobStatus { public PersistableBundle getExtras() { return job.getExtras(); } + + public int getPriority() { + return job.getPriority(); + } public boolean hasConnectivityConstraint() { return job.getNetworkType() == JobInfo.NETWORK_TYPE_ANY; diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index eed326e4a4c49..248cf664734c7 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -102,6 +102,8 @@ + jobStatusSet = new ArraySet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet); + JobStatus loaded = jobStatusSet.iterator().next(); + assertEquals("Priority not correctly persisted.", 42, loaded.getPriority()); } /** diff --git a/services/tests/servicestests/src/com/android/server/job/MockPriorityJobService.java b/services/tests/servicestests/src/com/android/server/job/MockPriorityJobService.java new file mode 100644 index 0000000000000..3ea86f2e9ac0b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/job/MockPriorityJobService.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 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 com.android.server.job; + +import android.annotation.TargetApi; +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.util.Log; + +import java.util.ArrayList; + +@TargetApi(24) +public class MockPriorityJobService extends JobService { + private static final String TAG = "MockPriorityJobService"; + + @Override + public void onCreate() { + super.onCreate(); + Log.e(TAG, "Created test service."); + } + + @Override + public boolean onStartJob(JobParameters params) { + Log.i(TAG, "Test job executing: " + params.getJobId()); + TestEnvironment.getTestEnvironment().executedEvents.add( + new TestEnvironment.Event(TestEnvironment.EVENT_START_JOB, params.getJobId())); + return true; // Job not finished + } + + @Override + public boolean onStopJob(JobParameters params) { + Log.i(TAG, "Test job stop executing: " + params.getJobId()); + int reason = params.getStopReason(); + int event = TestEnvironment.EVENT_STOP_JOB; + Log.d(TAG, "stop reason: " + String.valueOf(reason)); + if (reason == JobParameters.REASON_PREEMPT) { + event = TestEnvironment.EVENT_PREEMPT_JOB; + Log.d(TAG, "preempted " + String.valueOf(params.getJobId())); + } + TestEnvironment.getTestEnvironment().executedEvents + .add(new TestEnvironment.Event(event, params.getJobId())); + return false; // Do not reschedule + } + + public static class TestEnvironment { + + public static final int EVENT_START_JOB = 0; + public static final int EVENT_PREEMPT_JOB = 1; + public static final int EVENT_STOP_JOB = 2; + + private static TestEnvironment kTestEnvironment; + + private ArrayList executedEvents = new ArrayList(); + + public static TestEnvironment getTestEnvironment() { + if (kTestEnvironment == null) { + kTestEnvironment = new TestEnvironment(); + } + return kTestEnvironment; + } + + public static class Event { + public int event; + public int jobId; + + public Event() { + } + + public Event(int event, int jobId) { + this.event = event; + this.jobId = jobId; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Event) { + Event otherEvent = (Event) other; + return otherEvent.event == event && otherEvent.jobId == jobId; + } + return false; + } + } + + public void setUp() { + executedEvents.clear(); + } + + public ArrayList getExecutedEvents() { + return executedEvents; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/job/PrioritySchedulingTest.java b/services/tests/servicestests/src/com/android/server/job/PrioritySchedulingTest.java new file mode 100644 index 0000000000000..63bccfa014131 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/job/PrioritySchedulingTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2015 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 com.android.server.job; + +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.content.ComponentName; +import android.content.Context; +import android.test.AndroidTestCase; +import com.android.server.job.MockPriorityJobService.TestEnvironment; +import com.android.server.job.MockPriorityJobService.TestEnvironment.Event; + +import java.util.ArrayList; + +@TargetApi(24) +public class PrioritySchedulingTest extends AndroidTestCase { + /** Environment that notifies of JobScheduler callbacks. */ + static TestEnvironment kTestEnvironment = TestEnvironment.getTestEnvironment(); + /** Handle for the service which receives the execution callbacks from the JobScheduler. */ + static ComponentName kJobServiceComponent; + JobScheduler mJobScheduler; + + @Override + public void setUp() throws Exception { + super.setUp(); + kTestEnvironment.setUp(); + kJobServiceComponent = new ComponentName(getContext(), MockPriorityJobService.class); + mJobScheduler = (JobScheduler) getContext().getSystemService(Context.JOB_SCHEDULER_SERVICE); + mJobScheduler.cancelAll(); + } + + @Override + public void tearDown() throws Exception { + mJobScheduler.cancelAll(); + super.tearDown(); + } + + public void testLowerPriorityJobPreempted() throws Exception { + JobInfo job1 = new JobInfo.Builder(111, kJobServiceComponent) + .setPriority(1) + .setOverrideDeadline(7000L) + .build(); + JobInfo job2 = new JobInfo.Builder(222, kJobServiceComponent) + .setPriority(1) + .setOverrideDeadline(7000L) + .build(); + JobInfo job3 = new JobInfo.Builder(333, kJobServiceComponent) + .setPriority(1) + .setOverrideDeadline(7000L) + .build(); + JobInfo job4 = new JobInfo.Builder(444, kJobServiceComponent) + .setPriority(2) + .setMinimumLatency(2000L) + .setOverrideDeadline(7000L) + .build(); + mJobScheduler.schedule(job1); + mJobScheduler.schedule(job2); + mJobScheduler.schedule(job3); + mJobScheduler.schedule(job4); + Thread.sleep(10000); // Wait for job 4 to preempt one of the lower priority jobs + + Event job4Execution = new Event(TestEnvironment.EVENT_START_JOB, 444); + ArrayList executedEvents = kTestEnvironment.getExecutedEvents(); + boolean wasJob4Executed = executedEvents.contains(job4Execution); + boolean wasSomeJobPreempted = false; + for (Event event: executedEvents) { + if (event.event == TestEnvironment.EVENT_PREEMPT_JOB) { + wasSomeJobPreempted = true; + break; + } + } + assertTrue("No job was preempted.", wasSomeJobPreempted); + assertTrue("Lower priority jobs were not preempted.", wasJob4Executed); + } + + public void testHigherPriorityJobNotPreempted() throws Exception { + JobInfo job1 = new JobInfo.Builder(111, kJobServiceComponent) + .setPriority(2) + .setOverrideDeadline(7000L) + .build(); + JobInfo job2 = new JobInfo.Builder(222, kJobServiceComponent) + .setPriority(2) + .setOverrideDeadline(7000L) + .build(); + JobInfo job3 = new JobInfo.Builder(333, kJobServiceComponent) + .setPriority(2) + .setOverrideDeadline(7000L) + .build(); + JobInfo job4 = new JobInfo.Builder(444, kJobServiceComponent) + .setPriority(1) + .setMinimumLatency(2000L) + .setOverrideDeadline(7000L) + .build(); + mJobScheduler.schedule(job1); + mJobScheduler.schedule(job2); + mJobScheduler.schedule(job3); + mJobScheduler.schedule(job4); + Thread.sleep(10000); // Wait for job 4 to preempt one of the higher priority jobs + + Event job4Execution = new Event(TestEnvironment.EVENT_START_JOB, 444); + boolean wasJob4Executed = kTestEnvironment.getExecutedEvents().contains(job4Execution); + assertFalse("Higher priority job was preempted.", wasJob4Executed); + } +}