Source Code Analysis Based on Android 7.1.1
premise
Next, let's look at cancel-related services in the JobService Service Service service:
// Cancel all job s for specified device users!
void cancelJobsForUser(int userHandle) {
List<JobStatus> jobsForUser;
synchronized (mLock) {
jobsForUser = mJobs.getJobsByUser(userHandle);
}
for (int i=0; i<jobsForUser.size(); i++) {
JobStatus toRemove = jobsForUser.get(i);
cancelJobImpl(toRemove, null);
}
}
// Cancel all job s that specify package s and uid s!
void cancelJobsForPackageAndUid(String pkgName, int uid) {
List<JobStatus> jobsForUid;
synchronized (mLock) {
jobsForUid = mJobs.getJobsByUid(uid);
}
for (int i = jobsForUid.size() - 1; i >= 0; i--) {
final JobStatus job = jobsForUid.get(i);
if (job.getSourcePackageName().equals(pkgName)) {
cancelJobImpl(job, null);
}
}
}
// Cancel all job s for applications that specify uid!
public void cancelJobsForUid(int uid, boolean forceAll) {
List<JobStatus> jobsForUid;
synchronized (mLock) {
jobsForUid = mJobs.getJobsByUid(uid);
}
for (int i=0; i<jobsForUid.size(); i++) {
JobStatus toRemove = jobsForUid.get(i);
if (!forceAll) {
String packageName = toRemove.getServiceComponent().getPackageName();
try {
if (ActivityManagerNative.getDefault().getAppStartMode(uid, packageName)
!= ActivityManager.APP_START_MODE_DISABLED) {
continue;
}
} catch (RemoteException e) {
}
}
cancelJobImpl(toRemove, null);
}
}
- // Cancel the job whose id of the application specified uid is jobId!
public void cancelJob(int uid, int jobId) {
JobStatus toCancel;
synchronized (mLock) {
toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
}
if (toCancel != null) {
cancelJobImpl(toCancel, null);
}
}
job Scheduler Service provides many interfaces to cancel jobs, but they all call the same method crazily. Let's continue!
1 JSS.cancelJobImpl
The same cancel interface:
private void cancelJobImpl(JobStatus cancelled, JobStatus incomingJob) {
if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
// Stop monitoring this job. WritteBack is true!
stopTrackingJob(cancelled, incomingJob, true /* writeBack */);
synchronized (mLock) {
// Remove from pending queue.
// Remove from mPending Jobs!
if (mPendingJobs.remove(cancelled)) {
mJobPackageTracker.noteNonpending(cancelled);
}
// Cancel if running.
// If it's running, stop the job
stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED);
reportActive();
}
}
Let's look at it one by one.
1.1 JSS.stopTrackingJob
Stop stopTracking Job as follows:
private boolean stopTrackingJob(JobStatus jobStatus, JobStatus incomingJob,
boolean writeBack) {
synchronized (mLock) {
// Remove from store as well as controllers.
// Remove the job from JobStore first, because writeBack is true, you need to update the jobxs.xml file!
final boolean removed = mJobs.remove(jobStatus, writeBack);
if (removed && mReadyToRock) {
for (int i=0; i<mControllers.size(); i++) {
// Notify the controller, cancel track!
StateController controller = mControllers.get(i);
controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
}
}
return removed;
}
}
Remove the job from the JobStore first, because the writeBack is true, you need to update the jobxs.xml file, notify the controller, cancel the track!
1.2 JSS.stopJobOnServiceContextLocked
If the job is running, cancel it!
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(reason);
return true;
}
}
return false;
}
Each running job is bound to a JobServiceContext. Here we traverse all JobServiceContext, find the job to cancel, and call the cancelExecutingJob method of JobServiceContext:
1.2.1 JSC.cancelExecutingJob
This method is simple, sending MSG_CANCEL message to JobService Handler, reasoning is JobParameters.REASON_CANCELED
void cancelExecutingJob(int reason) {
mCallbackHandler.obtainMessage(MSG_CANCEL, reason, 0 /* unused */).sendToTarget();
}
Let's go on and enter JobService Handler:
1.2.2 JSH.MSG_CANCEL
JobService Hander handles MSG_CANCEL messages!
private class JobServiceHandler extends Handler {
JobServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message message) {
- Â
- ... ... ... ...
case MSG_CANCEL: // Accepted MSG_CANCEL.
if (mVerb == VERB_FINISHED) {
if (DEBUG) {
Slog.d(TAG,
"Trying to process cancel for torn-down context, ignoring.");
}
return;
}
mParams.setStopReason(message.arg1); // reason is JobParameters.REASON_CANCELED
if (message.arg1 == JobParameters.REASON_PREEMPT) { // No entry, because this is not a priority replacement!
mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
NO_PREFERRED_UID;
}
handleCancelH();
break;
case MSG_TIMEOUT:
handleOpTimeoutH();
break;
case MSG_SHUTDOWN_EXECUTION:
closeAndCleanupJobH(true /* needsReschedule */);
break;
default:
Slog.e(TAG, "Unrecognised message: " + message);
}
}
 ... ... ... ... ...
}
Call the handleCancelH method to cancel Job:
1.2.3 JSH.handleCancelH
/**
* A job can be in various states when a cancel request comes in:
* VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for
* {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)}
* _STARTING -> Mark as cancelled and wait for
* {@link JobServiceContext#acknowledgeStartMessage(int, boolean)}
* _EXECUTING -> call {@link #sendStopMessageH}}, but only if there are no callbacks
* in the message queue.
* _ENDING -> No point in doing anything here, so we ignore.
*/
private void handleCancelH() {
if (JobSchedulerService.DEBUG) {
Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " "
+ VERB_STRINGS[mVerb]);
}
switch (mVerb) {
case VERB_BINDING: // If the job status is VERB_BINDING or VERB_STARTING, set mCancelled to true directly!
case VERB_STARTING:
mCancelled.set(true);
break;
case VERB_EXECUTING: // If the state of job is VERB_EXECUTING,
if (hasMessages(MSG_CALLBACK)) { // Determine whether the client has invoked the jobFinished method, there is a direct return!
// If the client has called jobFinished, ignore this cancel.
return;
}
sendStopMessageH(); //
break;
case VERB_STOPPING:
// Nada.
break;
default:
Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb);
break;
}
}
Let's look at the sendStopMessageH method:
1.2.4 JSH.sendStopMessageH
/**
* Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
* VERB_STOPPING.
*/
private void sendStopMessageH() {
removeOpTimeOut();
if (mVerb != VERB_EXECUTING) { // If the state of job is no longer VERB_EXECUTING, then clear the resources and resume initialization!
Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
closeAndCleanupJobH(false /* reschedule */);
return;
}
try {
mVerb = VERB_STOPPING; // Otherwise, the job status is set to VERB_STOPPING.
scheduleOpTimeOut();
service.stopJob(mParams); // Call the stop Job of jobService and stop the service!
} catch (RemoteException e) {
Slog.e(TAG, "Error sending onStopJob to client.", e);
closeAndCleanupJobH(false /* reschedule */);
}
}
Here, according to the action state of job stored in mVerb, the corresponding processing will be done:
-
If the state of job is no longer VERB_EXECUTING, then clear the resources and resume initialization.
- Otherwise, the job status is set to VERB_STOPPING, and the stop Job of the job service is called to stop the service!
1.2.4.1 JobService.stopJob
Stop job through Binder mechanism:
public abstract class JobService extends Service {
... ... ... ...
private static final String TAG = "JobService";
static final class JobInterface extends IJobService.Stub {
final WeakReference<JobService> mService;
JobInterface(JobService service) {
mService = new WeakReference<>(service);
}
@Override
public void startJob(JobParameters jobParams) throws RemoteException {
JobService service = mService.get();
if (service != null) {
service.ensureHandler();
Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams);
m.sendToTarget();
}
}
@Override
public void stopJob(JobParameters jobParams) throws RemoteException {
JobService service = mService.get();
if (service != null) {
service.ensureHandler(); // Ensure that Handler has been created
Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams); // Send MSG_STOP_JOB to JobHandler!
m.sendToTarget();
}
}
}
... ... ... ...
}
Enter the JobHandler method:
1.2.4.2 JobService.JobHandler
Processing MSG_STOP_JOB sent through Binder before:
class JobHandler extends Handler {
JobHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
final JobParameters params = (JobParameters) msg.obj;
switch (msg.what) {
... ... ... ...
case MSG_STOP_JOB:
try {
// Here is the return value of onStopJob for JobService!
boolean ret = JobService.this.onStopJob(params);
ackStopMessage(params, ret);
} catch (Exception e) {
Log.e(TAG, "Application unable to handle onStopJob.", e);
throw new RuntimeException(e);
}
break;
... ... ... ...
default:
Log.e(TAG, "Unrecognised message received.");
break;
}
}
... ... ... ...  Â
private void ackStopMessage(JobParameters params, boolean reschedule) {
final IJobCallback callback = params.getCallback();
final int jobId = params.getJobId();
if (callback != null) {
try {
// Send the id of the stopped job and the return value of onStopJob to JobService Context!
callback.acknowledgeStopMessage(jobId, reschedule);
} catch(RemoteException e) {
Log.e(TAG, "System unreachable for stopping job.");
}
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Attempting to ack a job that has already been processed.");
}
}
}
}
If the onStopJob return value of JobService is true, it means that JobService needs to pull him up again! When we analyzed it earlier, we knew that callback was JobService Context.
1.2.4.3 JSC.acknowledgeStopMessage
Then send MSG_CALLBACK to JobService Handler:
@Override
public void acknowledgeStopMessage(int jobId, boolean reschedule) {
if (!verifyCallingUid()) {
return;
}
mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0)
.sendToTarget();
}
Let's go into the JobService Handler method and see:
1.2.4.4 JSC.JobServiceHandler
private class JobServiceHandler extends Handler {
@Override
public void handleMessage(Message message) {
switch (message.what) {
- ... ... ... ...
case MSG_CALLBACK:
if (DEBUG) {
Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob
+ " v:" + VERB_STRINGS[mVerb]);
}
removeOpTimeOut();
if (mVerb == VERB_STARTING) {
final boolean workOngoing = message.arg2 == 1;
handleStartedH(workOngoing);
} else if (mVerb == VERB_EXECUTING ||
mVerb == VERB_STOPPING) { // Enter here!
final boolean reschedule = message.arg2 == 1;
handleFinishedH(reschedule);
} else {
if (DEBUG) {
Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
}
}
break;
... ... ... ...
default:
Slog.e(TAG, "Unrecognised message: " + message);
}
}
- ... ... ... ...
/**
* VERB_EXECUTING -> Client called jobFinished(), clean up and notify done.
* _STOPPING -> Successful finish, clean up and notify done.
* _STARTING -> Error
* _PENDING -> Error
*/
private void handleFinishedH(boolean reschedule) {
switch (mVerb) {
case VERB_EXECUTING:
case VERB_STOPPING:
closeAndCleanupJobH(reschedule);
break;
default:
Slog.e(TAG, "Got an execution complete message for a job that wasn't being" +
"executed. Was " + VERB_STRINGS[mVerb] + ".");
}
}
... ... ... ...
}
As you can see, the closeAndCleanupJobH method of JSH is finally called:
1.2.4.5 JSH.closeAndCleanupJobH
If the state of job is no longer VERB_EXECUTING, then clean up the resources and restore the initialization; this method is simple, that is, restore the initialization of the attribute value in JobService Context, indicating that no job is running!
private void closeAndCleanupJobH(boolean reschedule) {
final JobStatus completedJob;
synchronized (mLock) {
if (mVerb == VERB_FINISHED) {
return;
}
completedJob = mRunningJob;
mJobPackageTracker.noteInactive(completedJob);
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(),
mRunningJob.getSourceUid());
} catch (RemoteException e) {
// Whatever.
}
if (mWakeLock != null) {
mWakeLock.release();
}
mContext.unbindService(JobServiceContext.this); // Unbound jobSerivce
mWakeLock = null;
mRunningJob = null; // The value of mRunningJob is set to null!
mParams = null; // JobParamters is null
mVerb = VERB_FINISHED; // The state value of mVerb is VERB_FINISHED
mCancelled.set(false); // mCancelled set to false
service = null; // The JobService proxy object of the application is set to null
mAvailable = true; // mAvailable is set to true, indicating that this JobService can be assigned to other job s
}
removeOpTimeOut();
removeMessages(MSG_CALLBACK); // Clean-up of processed MSG
removeMessages(MSG_SERVICE_BOUND);
removeMessages(MSG_CANCEL);
removeMessages(MSG_SHUTDOWN_EXECUTION);
// Callback to the onJobCompleted method of JobScheduler Service!
mCompletedListener.onJobCompleted(completedJob, reschedule);
}
Restore initialization and prepare for the next time!
1.2.4.5.1 JSC.onServiceDisconnected
Unbound JobService calls onService Disconnected
/** If the client service crashes we reschedule this job and clean up. */
@Override
public void onServiceDisconnected(ComponentName name) {
mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
}
Here we go back to JobService Handler:
case MSG_SHUTDOWN_EXECUTION:
closeAndCleanupJobH(true /* needsReschedule */);
break;
Do the second cleaning to prevent the first unsuccessful, in fact, the first clean successful, mVerb value is VERB_FINISHED, the second clean will automatically exit!
1.2.4.5.2 JSS.onJobCompleted
We go into JSS's onJobCompleted method.
@Override
public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
if (DEBUG) {
Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
}
// Do not write back immediately if this is a periodic job. The job may get lost if system
// shuts down before it is added back.
// Stop track again, here
stopTrackingJob The return value of the false!if (!stopTrackingJob(jobStatus, null, !jobStatus.getJob().isPeriodic())) {
if (DEBUG) {
Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
}
// We still want to check for jobs to execute, because this job may have
// scheduled a new job under the same job id, and now we can run it.
// Send MSG_CHECK_JOB_GREEDY, continue with other jobs, and return directly
mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
return;
}
// Note: there is a small window of time in here where, when rescheduling a job,
// we will stop monitoring its content providers. This should be fixed by stopping
// the old job after scheduling the new one, but since we have no lock held here
// that may cause ordering problems if the app removes jobStatus while in here.
if (needsReschedule) {
JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
startTrackingJob(rescheduled, jobStatus);
} else if (jobStatus.getJob().isPeriodic()) {
JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
startTrackingJob(rescheduledPeriodic, jobStatus);
}
reportActive();
mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
}
StopTrackingJob is first called to remove this job from JobStore and controller because it has been removed before, so the return value of this stopTrackingJob is false.
/**
* Called when we want to remove a JobStatus object that we've finished executing. Returns the
* object removed.
*/
private boolean stopTrackingJob(JobStatus jobStatus, JobStatus incomingJob,
boolean writeBack) {
synchronized (mLock) {
// Remove from store as well as controllers.
// Try to remove this job from JobStore. Note that it has been removed before, so removed here is false!
final boolean removed = mJobs.remove(jobStatus, writeBack);
if (removed && mReadyToRock) {
for (int i=0; i<mControllers.size(); i++) {
StateController controller = mControllers.get(i);
// Remove from the tracking queue of Controller!
controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
}
}
return removed;
}
}
As you can see, no matter what the return value of onStopJob is for the user's active cancel task, it will not reschedule!