JSS Article 4 - Job Scheduler Service - cancel

Keywords: Java Android xml Attribute

Source Code Analysis Based on Android 7.1.1

premise

Next, let's look at cancel-related services in the JobService Service Service service:
  1. // Cancel all job s for specified device users!
  2. void cancelJobsForUser(int userHandle) {
  3. List<JobStatus> jobsForUser;
  4. synchronized (mLock) {
  5. jobsForUser = mJobs.getJobsByUser(userHandle);
  6. }
  7. for (int i=0; i<jobsForUser.size(); i++) {
  8. JobStatus toRemove = jobsForUser.get(i);
  9. cancelJobImpl(toRemove, null);
  10. }
  11. }
  12. // Cancel all job s that specify package s and uid s!
  13. void cancelJobsForPackageAndUid(String pkgName, int uid) {
  14. List<JobStatus> jobsForUid;
  15. synchronized (mLock) {
  16. jobsForUid = mJobs.getJobsByUid(uid);
  17. }
  18. for (int i = jobsForUid.size() - 1; i >= 0; i--) {
  19. final JobStatus job = jobsForUid.get(i);
  20. if (job.getSourcePackageName().equals(pkgName)) {
  21. cancelJobImpl(job, null);
  22. }
  23. }
  24. }
  25. // Cancel all job s for applications that specify uid!
  26. public void cancelJobsForUid(int uid, boolean forceAll) {
  27. List<JobStatus> jobsForUid;
  28. synchronized (mLock) {
  29. jobsForUid = mJobs.getJobsByUid(uid);
  30. }
  31. for (int i=0; i<jobsForUid.size(); i++) {
  32. JobStatus toRemove = jobsForUid.get(i);
  33. if (!forceAll) {
  34. String packageName = toRemove.getServiceComponent().getPackageName();
  35. try {
  36. if (ActivityManagerNative.getDefault().getAppStartMode(uid, packageName)
  37. != ActivityManager.APP_START_MODE_DISABLED) {
  38. continue;
  39. }
  40. } catch (RemoteException e) {
  41. }
  42. }
  43. cancelJobImpl(toRemove, null);
  44. }
  45. }
  46. // Cancel the job whose id of the application specified uid is jobId!
  47. public void cancelJob(int uid, int jobId) {
  48. JobStatus toCancel;
  49. synchronized (mLock) {
  50. toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
  51. }
  52. if (toCancel != null) {
  53. cancelJobImpl(toCancel, null);
  54. }
  55. }
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:
  1. private void cancelJobImpl(JobStatus cancelled, JobStatus incomingJob) {
  2. if (DEBUG) Slog.d(TAG, "CANCEL: " + cancelled.toShortString());
  3. // Stop monitoring this job. WritteBack is true!
  4. stopTrackingJob(cancelled, incomingJob, true /* writeBack */);
  5. synchronized (mLock) {
  6. // Remove from pending queue.
  7. // Remove from mPending Jobs!
  8. if (mPendingJobs.remove(cancelled)) {
  9. mJobPackageTracker.noteNonpending(cancelled);
  10. }
  11. // Cancel if running.
  12. // If it's running, stop the job
  13. stopJobOnServiceContextLocked(cancelled, JobParameters.REASON_CANCELED);
  14. reportActive();
  15. }
  16. }
Let's look at it one by one.

1.1 JSS.stopTrackingJob

Stop stopTracking Job as follows:
  1. private boolean stopTrackingJob(JobStatus jobStatus, JobStatus incomingJob,
  2. boolean writeBack) {
  3. synchronized (mLock) {
  4. // Remove from store as well as controllers.
  5. // Remove the job from JobStore first, because writeBack is true, you need to update the jobxs.xml file!
  6. final boolean removed = mJobs.remove(jobStatus, writeBack);
  7. if (removed && mReadyToRock) {
  8. for (int i=0; i<mControllers.size(); i++) {
  9. // Notify the controller, cancel track!
  10. StateController controller = mControllers.get(i);
  11. controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
  12. }
  13. }
  14. return removed;
  15. }
  16. }
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!
  1. private boolean stopJobOnServiceContextLocked(JobStatus job, int reason) {
  2. for (int i=0; i<mActiveServices.size(); i++) {
  3. JobServiceContext jsc = mActiveServices.get(i);
  4. final JobStatus executing = jsc.getRunningJob();
  5. if (executing != null && executing.matches(job.getUid(), job.getJobId())) {
  6. jsc.cancelExecutingJob(reason);
  7. return true;
  8. }
  9. }
  10. return false;
  11. }
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
  1. void cancelExecutingJob(int reason) {
  2. mCallbackHandler.obtainMessage(MSG_CANCEL, reason, 0 /* unused */).sendToTarget();
  3. }
Let's go on and enter JobService Handler:

1.2.2 JSH.MSG_CANCEL

JobService Hander handles MSG_CANCEL messages!
  1. private class JobServiceHandler extends Handler {
  2. JobServiceHandler(Looper looper) {
  3. super(looper);
  4. }
  5. @Override
  6. public void handleMessage(Message message) {
  7.  
  8. ... ... ... ...
  9. case MSG_CANCEL: // Accepted MSG_CANCEL.
  10. if (mVerb == VERB_FINISHED) {
  11. if (DEBUG) {
  12. Slog.d(TAG,
  13. "Trying to process cancel for torn-down context, ignoring.");
  14. }
  15. return;
  16. }
  17. mParams.setStopReason(message.arg1); // reason is JobParameters.REASON_CANCELED
  18. if (message.arg1 == JobParameters.REASON_PREEMPT) { // No entry, because this is not a priority replacement!
  19. mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
  20. NO_PREFERRED_UID;
  21. }
  22. handleCancelH();
  23. break;
  24. case MSG_TIMEOUT:
  25. handleOpTimeoutH();
  26. break;
  27. case MSG_SHUTDOWN_EXECUTION:
  28. closeAndCleanupJobH(true /* needsReschedule */);
  29. break;
  30. default:
  31. Slog.e(TAG, "Unrecognised message: " + message);
  32. }
  33. }
  34.   ... ... ... ... ...
  35. }
Call the handleCancelH method to cancel Job:

1.2.3 JSH.handleCancelH

  1. /**
  2. * A job can be in various states when a cancel request comes in:
  3. * VERB_BINDING -> Cancelled before bind completed. Mark as cancelled and wait for
  4. * {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)}
  5. * _STARTING -> Mark as cancelled and wait for
  6. * {@link JobServiceContext#acknowledgeStartMessage(int, boolean)}
  7. * _EXECUTING -> call {@link #sendStopMessageH}}, but only if there are no callbacks
  8. * in the message queue.
  9. * _ENDING -> No point in doing anything here, so we ignore.
  10. */
  11. private void handleCancelH() {
  12. if (JobSchedulerService.DEBUG) {
  13. Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " "
  14. + VERB_STRINGS[mVerb]);
  15. }
  16. switch (mVerb) {
  17. case VERB_BINDING: // If the job status is VERB_BINDING or VERB_STARTING, set mCancelled to true directly!
  18. case VERB_STARTING:
  19. mCancelled.set(true);
  20. break;
  21. case VERB_EXECUTING: // If the state of job is VERB_EXECUTING,
  22. if (hasMessages(MSG_CALLBACK)) { // Determine whether the client has invoked the jobFinished method, there is a direct return!
  23. // If the client has called jobFinished, ignore this cancel.
  24. return;
  25. }
  26. sendStopMessageH(); //
  27. break;
  28. case VERB_STOPPING:
  29. // Nada.
  30. break;
  31. default:
  32. Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb);
  33. break;
  34. }
  35. }
Let's look at the sendStopMessageH method:

1.2.4 JSH.sendStopMessageH

  1. /**
  2. * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
  3. * VERB_STOPPING.
  4. */
  5. private void sendStopMessageH() {
  6. removeOpTimeOut();
  7. if (mVerb != VERB_EXECUTING) { // If the state of job is no longer VERB_EXECUTING, then clear the resources and resume initialization!
  8. Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
  9. closeAndCleanupJobH(false /* reschedule */);
  10. return;
  11. }
  12. try {
  13. mVerb = VERB_STOPPING; // Otherwise, the job status is set to VERB_STOPPING.
  14. scheduleOpTimeOut();
  15. service.stopJob(mParams); // Call the stop Job of jobService and stop the service!
  16. } catch (RemoteException e) {
  17. Slog.e(TAG, "Error sending onStopJob to client.", e);
  18. closeAndCleanupJobH(false /* reschedule */);
  19. }
  20. }
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:
  1. public abstract class JobService extends Service {
  2. ... ... ... ...
  3. private static final String TAG = "JobService";
  4. static final class JobInterface extends IJobService.Stub {
  5. final WeakReference<JobService> mService;
  6. JobInterface(JobService service) {
  7. mService = new WeakReference<>(service);
  8. }
  9. @Override
  10. public void startJob(JobParameters jobParams) throws RemoteException {
  11. JobService service = mService.get();
  12. if (service != null) {
  13. service.ensureHandler();
  14. Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams);
  15. m.sendToTarget();
  16. }
  17. }
  18. @Override
  19. public void stopJob(JobParameters jobParams) throws RemoteException {
  20. JobService service = mService.get();
  21. if (service != null) {
  22. service.ensureHandler(); // Ensure that Handler has been created
  23. Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams); // Send MSG_STOP_JOB to JobHandler!
  24. m.sendToTarget();
  25. }
  26. }
  27. }
  28. ... ... ... ...
  29. }
Enter the JobHandler method:

1.2.4.2 JobService.JobHandler

Processing MSG_STOP_JOB sent through Binder before:
  1. class JobHandler extends Handler {
  2. JobHandler(Looper looper) {
  3. super(looper);
  4. }
  5. @Override
  6. public void handleMessage(Message msg) {
  7. final JobParameters params = (JobParameters) msg.obj;
  8. switch (msg.what) {
  9. ... ... ... ...
  10. case MSG_STOP_JOB:
  11. try {
  12. // Here is the return value of onStopJob for JobService!
  13. boolean ret = JobService.this.onStopJob(params);
  14. ackStopMessage(params, ret);
  15. } catch (Exception e) {
  16. Log.e(TAG, "Application unable to handle onStopJob.", e);
  17. throw new RuntimeException(e);
  18. }
  19. break;
  20. ... ... ... ...
  21. default:
  22. Log.e(TAG, "Unrecognised message received.");
  23. break;
  24. }
  25. }
  26. ... ... ... ...   
  27. private void ackStopMessage(JobParameters params, boolean reschedule) {
  28. final IJobCallback callback = params.getCallback();
  29. final int jobId = params.getJobId();
  30. if (callback != null) {
  31. try {
  32. // Send the id of the stopped job and the return value of onStopJob to JobService Context!
  33. callback.acknowledgeStopMessage(jobId, reschedule);
  34. } catch(RemoteException e) {
  35. Log.e(TAG, "System unreachable for stopping job.");
  36. }
  37. } else {
  38. if (Log.isLoggable(TAG, Log.DEBUG)) {
  39. Log.d(TAG, "Attempting to ack a job that has already been processed.");
  40. }
  41. }
  42. }
  43. }
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:
  1. @Override
  2. public void acknowledgeStopMessage(int jobId, boolean reschedule) {
  3. if (!verifyCallingUid()) {
  4. return;
  5. }
  6. mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0)
  7. .sendToTarget();
  8. }
Let's go into the JobService Handler method and see:

1.2.4.4 JSC.JobServiceHandler

  1. private class JobServiceHandler extends Handler {
  2. @Override
  3. public void handleMessage(Message message) {
  4. switch (message.what) {
  5. ... ... ... ...
  6. case MSG_CALLBACK:
  7. if (DEBUG) {
  8. Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob
  9. + " v:" + VERB_STRINGS[mVerb]);
  10. }
  11. removeOpTimeOut();
  12. if (mVerb == VERB_STARTING) {
  13. final boolean workOngoing = message.arg2 == 1;
  14. handleStartedH(workOngoing);
  15. } else if (mVerb == VERB_EXECUTING ||
  16. mVerb == VERB_STOPPING) { // Enter here!
  17. final boolean reschedule = message.arg2 == 1;
  18. handleFinishedH(reschedule);
  19. } else {
  20. if (DEBUG) {
  21. Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
  22. }
  23. }
  24. break;
  25. ... ... ... ...
  26. default:
  27. Slog.e(TAG, "Unrecognised message: " + message);
  28. }
  29. }
  30. ... ... ... ...
  31. /**
  32. * VERB_EXECUTING -> Client called jobFinished(), clean up and notify done.
  33. * _STOPPING -> Successful finish, clean up and notify done.
  34. * _STARTING -> Error
  35. * _PENDING -> Error
  36. */
  37. private void handleFinishedH(boolean reschedule) {
  38. switch (mVerb) {
  39. case VERB_EXECUTING:
  40. case VERB_STOPPING:
  41. closeAndCleanupJobH(reschedule);
  42. break;
  43. default:
  44. Slog.e(TAG, "Got an execution complete message for a job that wasn't being" +
  45. "executed. Was " + VERB_STRINGS[mVerb] + ".");
  46. }
  47. }
  48. ... ... ... ...
  49. }
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!
  1. private void closeAndCleanupJobH(boolean reschedule) {
  2. final JobStatus completedJob;
  3. synchronized (mLock) {
  4. if (mVerb == VERB_FINISHED) {
  5. return;
  6. }
  7. completedJob = mRunningJob;
  8. mJobPackageTracker.noteInactive(completedJob);
  9. try {
  10. mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(),
  11. mRunningJob.getSourceUid());
  12. } catch (RemoteException e) {
  13. // Whatever.
  14. }
  15. if (mWakeLock != null) {
  16. mWakeLock.release();
  17. }
  18. mContext.unbindService(JobServiceContext.this); // Unbound jobSerivce
  19. mWakeLock = null;
  20. mRunningJob = null; // The value of mRunningJob is set to null!
  21. mParams = null; // JobParamters is null
  22. mVerb = VERB_FINISHED; // The state value of mVerb is VERB_FINISHED
  23. mCancelled.set(false); // mCancelled set to false
  24. service = null; // The JobService proxy object of the application is set to null
  25. mAvailable = true; // mAvailable is set to true, indicating that this JobService can be assigned to other job s
  26. }
  27. removeOpTimeOut();
  28. removeMessages(MSG_CALLBACK); // Clean-up of processed MSG
  29. removeMessages(MSG_SERVICE_BOUND);
  30. removeMessages(MSG_CANCEL);
  31. removeMessages(MSG_SHUTDOWN_EXECUTION);
  32. // Callback to the onJobCompleted method of JobScheduler Service!
  33. mCompletedListener.onJobCompleted(completedJob, reschedule);
  34. }
Restore initialization and prepare for the next time!
1.2.4.5.1 JSC.onServiceDisconnected
Unbound JobService calls onService Disconnected
  1. /** If the client service crashes we reschedule this job and clean up. */
  2. @Override
  3. public void onServiceDisconnected(ComponentName name) {
  4. mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
  5. }
Here we go back to JobService Handler:
  1. case MSG_SHUTDOWN_EXECUTION:
  2. closeAndCleanupJobH(true /* needsReschedule */);
  3. 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.
  1. @Override
  2. public void onJobCompleted(JobStatus jobStatus, boolean needsReschedule) {
  3. if (DEBUG) {
  4. Slog.d(TAG, "Completed " + jobStatus + ", reschedule=" + needsReschedule);
  5. }
  6. // Do not write back immediately if this is a periodic job. The job may get lost if system
  7. // shuts down before it is added back.
  8. // Stop track again, here stopTrackingJob The return value of the false!
  9. if (!stopTrackingJob(jobStatus, null, !jobStatus.getJob().isPeriodic())) {
  10. if (DEBUG) {
  11. Slog.d(TAG, "Could not find job to remove. Was job removed while executing?");
  12. }
  13. // We still want to check for jobs to execute, because this job may have
  14. // scheduled a new job under the same job id, and now we can run it.
  15. // Send MSG_CHECK_JOB_GREEDY, continue with other jobs, and return directly
  16. mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
  17. return;
  18. }
  19. // Note: there is a small window of time in here where, when rescheduling a job,
  20. // we will stop monitoring its content providers. This should be fixed by stopping
  21. // the old job after scheduling the new one, but since we have no lock held here
  22. // that may cause ordering problems if the app removes jobStatus while in here.
  23. if (needsReschedule) {
  24. JobStatus rescheduled = getRescheduleJobForFailure(jobStatus);
  25. startTrackingJob(rescheduled, jobStatus);
  26. } else if (jobStatus.getJob().isPeriodic()) {
  27. JobStatus rescheduledPeriodic = getRescheduleJobForPeriodic(jobStatus);
  28. startTrackingJob(rescheduledPeriodic, jobStatus);
  29. }
  30. reportActive();
  31. mHandler.obtainMessage(MSG_CHECK_JOB_GREEDY).sendToTarget();
  32. }
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.
  1. /**
  2. * Called when we want to remove a JobStatus object that we've finished executing. Returns the
  3. * object removed.
  4. */
  5. private boolean stopTrackingJob(JobStatus jobStatus, JobStatus incomingJob,
  6. boolean writeBack) {
  7. synchronized (mLock) {
  8. // Remove from store as well as controllers.
  9. // Try to remove this job from JobStore. Note that it has been removed before, so removed here is false!
  10. final boolean removed = mJobs.remove(jobStatus, writeBack);
  11. if (removed && mReadyToRock) {
  12. for (int i=0; i<mControllers.size(); i++) {
  13. StateController controller = mControllers.get(i);
  14. // Remove from the tracking queue of Controller!
  15. controller.maybeStopTrackingJobLocked(jobStatus, incomingJob, false);
  16. }
  17. }
  18. return removed;
  19. }
  20. }
As you can see, no matter what the return value of onStopJob is for the user's active cancel task, it will not reschedule!



























Posted by nabeel21 on Fri, 21 Dec 2018 15:54:07 -0800