diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java index b899710c4feb6..9ad35d4c15564 100644 --- a/core/java/android/app/job/JobInfo.java +++ b/core/java/android/app/job/JobInfo.java @@ -91,6 +91,7 @@ public class JobInfo implements Parcelable { private final long flexMillis; 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. @@ -113,6 +114,11 @@ public class JobInfo implements Parcelable { return service; } + /** @hide */ + public int getPriority() { + return priority; + } + /** * Whether this job needs the device to be plugged in. */ @@ -237,6 +243,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) { @@ -256,6 +263,7 @@ public class JobInfo implements Parcelable { backoffPolicy = b.mBackoffPolicy; hasEarlyConstraint = b.mHasEarlyConstraint; hasLateConstraint = b.mHasLateConstraint; + priority = b.mPriority; } @Override @@ -281,6 +289,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() { @@ -305,6 +314,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; @@ -337,6 +347,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 3530d80577c71..a6db6137e54cd 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:"); } @@ -741,8 +722,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) { @@ -765,8 +747,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; @@ -801,8 +784,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 || @@ -821,11 +805,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()); - } } /** @@ -834,31 +813,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; } /** @@ -866,7 +832,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); } @@ -881,45 +847,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(); @@ -1071,15 +1140,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); } } @@ -541,11 +544,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 060a93ef8ed92..56d92d535531e 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); + } +}