Merge "Added priority to JobScheduler scheduling"

This commit is contained in:
Shreyas Basarge
2016-01-15 13:54:08 +00:00
committed by Android (Google) Code Review
10 changed files with 513 additions and 132 deletions

View File

@@ -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<JobInfo> CREATOR = new Creator<JobInfo>() {
@@ -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.

View File

@@ -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<JobParameters> CREATOR = new Creator<JobParameters>() {

View File

@@ -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<JobStatus> mPendingJobs = new ArrayList<>();
int[] mStartedUsers = EmptyArray.INT;
final ArrayList<Integer> 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<mActiveServices.size(); i++) {
JobServiceContext jsc = mActiveServices.get(i);
if (!jsc.isAvailable()) {
if (jsc.getRunningJob() != null) {
active = true;
break;
}
@@ -429,24 +416,17 @@ public class JobSchedulerService extends com.android.server.SystemService
@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 @@ public class JobSchedulerService extends com.android.server.SystemService
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;
}
}
@@ -731,6 +711,7 @@ public class JobSchedulerService extends com.android.server.SystemService
*/
private void queueReadyJobsForExecutionLockedH() {
ArraySet<JobStatus> 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<JobStatus> 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<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
*/
@@ -936,8 +977,7 @@ public class JobSchedulerService extends com.android.server.SystemService
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);
}
@@ -1050,12 +1090,41 @@ public class JobSchedulerService extends com.android.server.SystemService
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();
@@ -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<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();

View File

@@ -59,7 +59,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
* 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
/** 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
* 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
@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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
return false;
}
mPreferredUid = NO_PREFERRED_UID;
mRunningJob = job;
final boolean isDeadlineExpired =
job.hasDeadlineConstraint() &&
@@ -206,17 +212,22 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
}
/** 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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
}
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 @@ public class JobServiceContext extends IJobCallback.Stub implements ServiceConne
/** 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() +

View File

@@ -312,7 +312,7 @@ public class JobStore {
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 @@ public class JobStore {
}
}
/** 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 @@ public class JobStore {
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);
}
}
@@ -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;

View File

@@ -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;

View File

@@ -102,6 +102,8 @@
</intent-filter>
</receiver>
<service android:name="com.android.server.job.MockPriorityJobService"
android:permission="android.permission.BIND_JOB_SERVICE" />
</application>
<instrumentation

View File

@@ -179,7 +179,21 @@ public class JobStoreTest extends AndroidTestCase {
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());
}
/**

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}