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
diff --git a/core/java/android/app/job/JobInfo.java b/core/java/android/app/job/JobInfo.java
index 0d9e778..5aa4f79 100644
--- a/core/java/android/app/job/JobInfo.java
+++ b/core/java/android/app/job/JobInfo.java
@@ -85,6 +85,7 @@
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 @@
return service;
}
+ /** @hide */
+ public int getPriority() {
+ return priority;
+ }
+
/**
* Whether this job needs the device to be plugged in.
*/
@@ -220,6 +226,7 @@
backoffPolicy = in.readInt();
hasEarlyConstraint = in.readInt() == 1;
hasLateConstraint = in.readInt() == 1;
+ priority = in.readInt();
}
private JobInfo(JobInfo.Builder b) {
@@ -238,6 +245,7 @@
backoffPolicy = b.mBackoffPolicy;
hasEarlyConstraint = b.mHasEarlyConstraint;
hasLateConstraint = b.mHasLateConstraint;
+ priority = b.mPriority;
}
@Override
@@ -262,6 +270,7 @@
out.writeInt(backoffPolicy);
out.writeInt(hasEarlyConstraint ? 1 : 0);
out.writeInt(hasLateConstraint ? 1 : 0);
+ out.writeInt(priority);
}
public static final Creator<JobInfo> CREATOR = new Creator<JobInfo>() {
@@ -286,6 +295,7 @@
private int mJobId;
private PersistableBundle mExtras = PersistableBundle.EMPTY;
private ComponentName mJobService;
+ private int mPriority;
// Requirements.
private boolean mRequiresCharging;
private boolean mRequiresDeviceIdle;
@@ -318,6 +328,14 @@
}
/**
+ * @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 7ee39f5..a0a60e8 100644
--- a/core/java/android/app/job/JobParameters.java
+++ b/core/java/android/app/job/JobParameters.java
@@ -28,10 +28,22 @@
*/
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,
@@ -50,6 +62,14 @@
}
/**
+ * 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
* never be null. If you did not set any extras this will be an empty bundle.
@@ -78,6 +98,12 @@
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 @@
dest.writePersistableBundle(extras);
dest.writeStrongBinder(callback);
dest.writeInt(overrideDeadlineExpired ? 1 : 0);
+ dest.writeInt(stopReason);
}
public static final Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
diff --git a/services/core/java/com/android/server/job/JobSchedulerService.java b/services/core/java/com/android/server/job/JobSchedulerService.java
index 4eabe36..3113b75 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.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.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 @@
*/
final ArrayList<JobStatus> mPendingJobs = new ArrayList<>();
- int[] mStartedUsers = EmptyArray.INT;
+ final ArrayList<Integer> mStartedUsers = new ArrayList<>();
final JobHandler mHandler;
final JobSchedulerStub mJobSchedulerStub;
@@ -161,9 +158,8 @@
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 @@
}
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 @@
@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 @@
// Remove from pending queue.
mPendingJobs.remove(cancelled);
// Cancel if running.
- stopJobOnServiceContextLocked(cancelled);
+ stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED);
reportActive();
}
}
@@ -357,7 +344,7 @@
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 @@
if (mPendingJobs.size() <= 0) {
for (int i=0; i<mActiveServices.size(); i++) {
JobServiceContext jsc = mActiveServices.get(i);
- if (!jsc.isAvailable()) {
+ if (jsc.getRunningJob() != null) {
active = true;
break;
}
@@ -429,24 +416,17 @@
@Override
public void onBootPhase(int phase) {
if (PHASE_SYSTEM_SERVICES_READY == phase) {
- // Register for package removals and user removals.
+ // Register br for package removals and user removals.
final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
getContext().registerReceiverAsUser(
mBroadcastReceiver, UserHandle.ALL, filter, null, null);
-
final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED);
userFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
userFilter.addAction(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED);
getContext().registerReceiverAsUser(
mBroadcastReceiver, UserHandle.ALL, userFilter, null, null);
-
- final IntentFilter storageFilter = new IntentFilter();
- storageFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
- getContext().registerReceiverAsUser(
- mBroadcastReceiver, UserHandle.ALL, storageFilter, null, null);
-
- mPowerManager = getContext().getSystemService(PowerManager.class);
+ mPowerManager = (PowerManager)getContext().getSystemService(Context.POWER_SERVICE);
try {
ActivityManagerNative.getDefault().registerUidObserver(mUidObserver,
ActivityManager.UID_OBSERVER_IDLE);
@@ -526,12 +506,12 @@
return removed;
}
- private boolean stopJobOnServiceContextLocked(JobStatus job) {
+ private boolean stopJobOnServiceContextLocked(JobStatus job, int reason) {
for (int i=0; i<mActiveServices.size(); i++) {
JobServiceContext jsc = mActiveServices.get(i);
final JobStatus executing = jsc.getRunningJob();
if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
- jsc.cancelExecutingJob();
+ jsc.cancelExecutingJob(reason);
return true;
}
}
@@ -730,6 +710,7 @@
*/
private void queueReadyJobsForExecutionLockedH() {
ArraySet<JobStatus> jobs = mJobs.getJobs();
+ mPendingJobs.clear();
if (DEBUG) {
Slog.d(TAG, "queuing all ready jobs for execution:");
}
@@ -740,8 +721,9 @@
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 @@
* 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<JobStatus> runnableJobs = null;
@@ -800,8 +783,9 @@
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 @@
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 @@
* - 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 @@
* - 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,46 +846,121 @@
// If device is idle, we will not schedule jobs to run.
return;
}
- Iterator<JobStatus> 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<mActiveServices.size(); i++) {
- JobServiceContext jsc = mActiveServices.get(i);
- final JobStatus running = jsc.getRunningJob();
- if (running != null && running.matches(nextPending.getUid(),
- nextPending.getJobId())) {
- // Already running this job for this uId, skip.
- availableContext = null;
- break;
- }
- if (jsc.isAvailable()) {
- availableContext = jsc;
- }
- }
- if (availableContext != null) {
- if (DEBUG) {
- Slog.d(TAG, "About to run job "
- + nextPending.getJob().getService().toString());
- }
- if (!availableContext.executeRunnableJob(nextPending)) {
- if (DEBUG) {
- Slog.d(TAG, "Error executing " + nextPending);
- }
- mJobs.remove(nextPending);
- }
- it.remove();
- }
- }
+ assignJobsToContextsH();
reportActive();
}
}
}
/**
+ * Takes jobs from pending queue and runs them on available contexts.
+ * If no contexts are available, preempts lower priority jobs to
+ * run higher priority ones.
+ * Lock on mJobs before calling this function.
+ */
+ private void assignJobsToContextsH() {
+ if (DEBUG) {
+ Slog.d(TAG, printPendingQueue());
+ }
+
+ // This array essentially stores the state of mActiveServices array.
+ // ith index stores the job present on the ith JobServiceContext.
+ // We manipulate this array until we arrive at what jobs should be running on
+ // what JobServiceContext.
+ JobStatus[] contextIdToJobMap = new JobStatus[MAX_JOB_CONTEXTS_COUNT];
+ // Indicates whether we need to act on this jobContext id
+ boolean[] act = new boolean[MAX_JOB_CONTEXTS_COUNT];
+ int[] preferredUidForContext = new int[MAX_JOB_CONTEXTS_COUNT];
+ for (int i=0; i<mActiveServices.size(); i++) {
+ contextIdToJobMap[i] = mActiveServices.get(i).getRunningJob();
+ preferredUidForContext[i] = mActiveServices.get(i).getPreferredUid();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, printContextIdToJobMap(contextIdToJobMap, "running jobs initial"));
+ }
+ Iterator<JobStatus> 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<mActiveServices.size(); i++) {
+ JobStatus job = contextIdToJobMap[i];
+ int preferredUid = preferredUidForContext[i];
+ if (job == null && (preferredUid == nextPending.getUid() ||
+ preferredUid == JobServiceContext.NO_PREFERRED_UID) ) {
+ minPriorityContextId = i;
+ break;
+ }
+ if (job.getUid() != nextPending.getUid()) {
+ continue;
+ }
+ if (job.getPriority() >= 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<mActiveServices.size(); i++) {
+ boolean preservePreferredUid = false;
+ if (act[i]) {
+ JobStatus js = mActiveServices.get(i).getRunningJob();
+ if (js != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "preempting job: " + mActiveServices.get(i).getRunningJob());
+ }
+ // preferredUid will be set to uid of currently running job.
+ mActiveServices.get(i).preemptExecutingJob();
+ preservePreferredUid = true;
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "About to run job on context "
+ + String.valueOf(i) + ", job: " + contextIdToJobMap[i]);
+ }
+ if (!mActiveServices.get(i).executeRunnableJob(contextIdToJobMap[i])) {
+ Slog.d(TAG, "Error executing " + contextIdToJobMap[i]);
+ }
+ mPendingJobs.remove(contextIdToJobMap[i]);
+ }
+ }
+ if (!preservePreferredUid) {
+ mActiveServices.get(i).clearPreferredUid();
+ }
+ }
+ }
+
+ int findJobContextIdFromMap(JobStatus jobStatus, JobStatus[] map) {
+ for (int i=0; i<map.length; i++) {
+ if (map[i] != null && map[i].matches(jobStatus.getUid(), jobStatus.getJobId())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
* Binder stub trampoline implementation
*/
final class JobSchedulerStub extends IJobScheduler.Stub {
@@ -935,8 +976,7 @@
final IPackageManager pm = AppGlobals.getPackageManager();
final ComponentName service = job.getService();
try {
- ServiceInfo si = pm.getServiceInfo(service,
- PackageManager.MATCH_DEBUG_TRIAGED_MISSING, UserHandle.getUserId(uid));
+ ServiceInfo si = pm.getServiceInfo(service, 0, UserHandle.getUserId(uid));
if (si == null) {
throw new IllegalArgumentException("No such service " + service);
}
@@ -1049,12 +1089,41 @@
Binder.restoreCallingIdentity(identityToken);
}
}
+ };
+
+ private String printContextIdToJobMap(JobStatus[] map, String initial) {
+ StringBuilder s = new StringBuilder(initial + ": ");
+ for (int i=0; i<map.length; i++) {
+ s.append("(")
+ .append(map[i] == null? -1: map[i].getJobId())
+ .append(map[i] == null? -1: map[i].getUid())
+ .append(")" );
+ }
+ return s.toString();
+ }
+
+ private String printPendingQueue() {
+ StringBuilder s = new StringBuilder("Pending queue: ");
+ Iterator<JobStatus> 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<mStartedUsers.size(); i++) {
+ pw.print("u" + mStartedUsers.get(i) + " ");
+ }
+ pw.println();
pw.println("Registered jobs:");
if (mJobs.size() > 0) {
ArraySet<JobStatus> jobs = mJobs.getJobs();
@@ -1070,15 +1139,12 @@
mControllers.get(i).dumpControllerState(pw);
}
pw.println();
- pw.println("Pending:");
- for (int i=0; i<mPendingJobs.size(); i++) {
- pw.println(mPendingJobs.get(i).hashCode());
- }
+ pw.println(printPendingQueue());
pw.println();
pw.println("Active jobs:");
for (int i=0; i<mActiveServices.size(); i++) {
JobServiceContext jsc = mActiveServices.get(i);
- if (jsc.isAvailable()) {
+ if (jsc.getRunningJob() == null) {
continue;
} else {
final long timeout = jsc.getTimeoutElapsed();
diff --git a/services/core/java/com/android/server/job/JobServiceContext.java b/services/core/java/com/android/server/job/JobServiceContext.java
index 5376043..5935319e 100644
--- a/services/core/java/com/android/server/job/JobServiceContext.java
+++ b/services/core/java/com/android/server/job/JobServiceContext.java
@@ -59,7 +59,6 @@
* To mitigate this, tearing down the context removes all messages from the handler, including any
* tardy {@link #MSG_CANCEL}s. Additionally, we avoid sending duplicate onStopJob()
* calls to the client after they've specified jobFinished().
- *
*/
public class JobServiceContext extends IJobCallback.Stub implements ServiceConnection {
private static final boolean DEBUG = JobSchedulerService.DEBUG;
@@ -95,6 +94,8 @@
/** Shutdown the job. Used when the client crashes and we can't die gracefully.*/
private static final int MSG_SHUTDOWN_EXECUTION = 4;
+ public static final int NO_PREFERRED_UID = -1;
+
private final Handler mCallbackHandler;
/** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
private final JobCompletedListener mCompletedListener;
@@ -117,7 +118,8 @@
* Writes can only be done from the handler thread, or {@link #executeRunnableJob(JobStatus)}.
*/
private JobStatus mRunningJob;
- /** Binder to the client service. */
+ /** Used to store next job to run when current job is to be preempted. */
+ private int mPreferredUid;
IJobService service;
private final Object mLock = new Object();
@@ -138,17 +140,19 @@
@VisibleForTesting
JobServiceContext(Context context, IBatteryStats batteryStats,
- JobCompletedListener completedListener, Looper looper) {
+ JobCompletedListener completedListener, Looper looper) {
mContext = context;
mBatteryStats = batteryStats;
mCallbackHandler = new JobServiceHandler(looper);
mCompletedListener = completedListener;
mAvailable = true;
+ mVerb = VERB_FINISHED;
+ mPreferredUid = NO_PREFERRED_UID;
}
/**
- * Give a job to this context for execution. Callers must first check {@link #isAvailable()}
- * to make sure this is a valid context.
+ * Give a job to this context for execution. Callers must first check {@link #getRunningJob()}
+ * and ensure it is null to make sure this is a valid context.
* @param job The status of the job that we are going to run.
* @return True if the job is valid and is running. False if the job cannot be executed.
*/
@@ -159,6 +163,8 @@
return false;
}
+ mPreferredUid = NO_PREFERRED_UID;
+
mRunningJob = job;
final boolean isDeadlineExpired =
job.hasDeadlineConstraint() &&
@@ -206,17 +212,22 @@
}
/** Called externally when a job that was scheduled for execution should be cancelled. */
- void cancelExecutingJob() {
- mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget();
+ void cancelExecutingJob(int reason) {
+ mCallbackHandler.obtainMessage(MSG_CANCEL, reason, 0 /* unused */).sendToTarget();
}
- /**
- * @return Whether this context is available to handle incoming work.
- */
- boolean isAvailable() {
- synchronized (mLock) {
- return mAvailable;
- }
+ void preemptExecutingJob() {
+ Message m = mCallbackHandler.obtainMessage(MSG_CANCEL);
+ m.arg1 = JobParameters.REASON_PREEMPT;
+ m.sendToTarget();
+ }
+
+ int getPreferredUid() {
+ return mPreferredUid;
+ }
+
+ void clearPreferredUid() {
+ mPreferredUid = NO_PREFERRED_UID;
}
long getExecutionStartTimeElapsed() {
@@ -344,6 +355,11 @@
}
break;
case MSG_CANCEL:
+ mParams.setStopReason(message.arg1);
+ if (message.arg1 == JobParameters.REASON_PREEMPT) {
+ mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
+ NO_PREFERRED_UID;
+ }
handleCancelH();
break;
case MSG_TIMEOUT:
@@ -481,6 +497,7 @@
/** Process MSG_TIMEOUT here. */
private void handleOpTimeoutH() {
+ mParams.setStopReason(JobParameters.REASON_TIMEOUT);
switch (mVerb) {
case VERB_BINDING:
Slog.e(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() +
diff --git a/services/core/java/com/android/server/job/JobStore.java b/services/core/java/com/android/server/job/JobStore.java
index 472e8f6..b3f1b07 100644
--- a/services/core/java/com/android/server/job/JobStore.java
+++ b/services/core/java/com/android/server/job/JobStore.java
@@ -312,7 +312,7 @@
Slog.d(TAG, "Saving job " + jobStatus.getJobId());
}
out.startTag(null, "job");
- addIdentifierAttributesToJobTag(out, jobStatus);
+ addAttributesToJobTag(out, jobStatus);
writeConstraintsToXml(out, jobStatus);
writeExecutionCriteriaToXml(out, jobStatus);
writeBundleToXml(jobStatus.getExtras(), out);
@@ -337,13 +337,16 @@
}
}
- /** Write out a tag with data comprising the required fields of this job and its client. */
- private void addIdentifierAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
+ /** Write out a tag with data comprising the required fields and priority of this job and
+ * its client.
+ */
+ private void addAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
throws IOException {
out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
+ out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
}
private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
@@ -361,9 +364,9 @@
PersistableBundle copy = (PersistableBundle) bundle.clone();
Set<String> 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 @@
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 c02611f..c8ab9c0 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 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 eed326e..248cf66 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -102,6 +102,8 @@
</intent-filter>
</receiver>
+ <service android:name="com.android.server.job.MockPriorityJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
<instrumentation
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 312b1b0..ae0a25e 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -179,7 +179,21 @@
loaded.getEarliestRunTime() <= newNowElapsed + TEN_SECONDS);
// Assert late runtime was clamped to be now + period*2.
assertTrue("Early runtime wasn't correctly clamped.",
- loaded.getEarliestRunTime() <= newNowElapsed + TEN_SECONDS*2);
+ loaded.getEarliestRunTime() <= newNowElapsed + TEN_SECONDS * 2);
+ }
+
+ public void testPriorityPersisted() throws Exception {
+ JobInfo.Builder b = new Builder(92, mComponent)
+ .setOverrideDeadline(5000)
+ .setPriority(42)
+ .setPersisted(true);
+ final JobStatus js = new JobStatus(b.build(), SOME_UID);
+ mTaskStoreUnderTest.add(js);
+ Thread.sleep(IO_WAIT);
+ final ArraySet<JobStatus> jobStatusSet = new ArraySet<JobStatus>();
+ 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 0000000..3ea86f2
--- /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<Event> executedEvents = new ArrayList<Event>();
+
+ 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<Event> 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 0000000..63bccfa
--- /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<Event> 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);
+ }
+}