1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.server.job;
18 
19 import android.app.ActivityManager;
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.IJobCallback;
23 import android.app.job.IJobService;
24 import android.app.job.JobWorkItem;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.ServiceConnection;
29 import android.net.Uri;
30 import android.os.Binder;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.Looper;
34 import android.os.Message;
35 import android.os.PowerManager;
36 import android.os.RemoteException;
37 import android.os.SystemClock;
38 import android.os.UserHandle;
39 import android.os.WorkSource;
40 import android.util.Slog;
41 import android.util.TimeUtils;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.app.IBatteryStats;
46 import com.android.server.job.controllers.JobStatus;
47 
48 /**
49  * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this
50  * class.
51  *
52  * There are two important interactions into this class from the
53  * {@link com.android.server.job.JobSchedulerService}. To execute a job and to cancel a job.
54  * - Execution of a new job is handled by the {@link #mAvailable}. This bit is flipped once when a
55  * job lands, and again when it is complete.
56  * - Cancelling is trickier, because there are also interactions from the client. It's possible
57  * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a
58  * {@link #doCancelLocked} after the client has already finished. This is handled by having
59  * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelLocked} check whether
60  * the context is still valid.
61  * To mitigate this, we avoid sending duplicate onStopJob()
62  * calls to the client after they've specified jobFinished().
63  */
64 public final class JobServiceContext implements ServiceConnection {
65     private static final boolean DEBUG = JobSchedulerService.DEBUG;
66     private static final String TAG = "JobServiceContext";
67     /** Amount of time a job is allowed to execute for before being considered timed-out. */
68     private static final long EXECUTING_TIMESLICE_MILLIS = 10 * 60 * 1000;  // 10mins.
69     /** Amount of time the JobScheduler waits for the initial service launch+bind. */
70     private static final long OP_BIND_TIMEOUT_MILLIS = 18 * 1000;
71     /** Amount of time the JobScheduler will wait for a response from an app for a message. */
72     private static final long OP_TIMEOUT_MILLIS = 8 * 1000;
73 
74     private static final String[] VERB_STRINGS = {
75             "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING", "VERB_FINISHED"
76     };
77 
78     // States that a job occupies while interacting with the client.
79     static final int VERB_BINDING = 0;
80     static final int VERB_STARTING = 1;
81     static final int VERB_EXECUTING = 2;
82     static final int VERB_STOPPING = 3;
83     static final int VERB_FINISHED = 4;
84 
85     // Messages that result from interactions with the client service.
86     /** System timed out waiting for a response. */
87     private static final int MSG_TIMEOUT = 0;
88 
89     public static final int NO_PREFERRED_UID = -1;
90 
91     private final Handler mCallbackHandler;
92     /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
93     private final JobCompletedListener mCompletedListener;
94     /** Used for service binding, etc. */
95     private final Context mContext;
96     private final Object mLock;
97     private final IBatteryStats mBatteryStats;
98     private final JobPackageTracker mJobPackageTracker;
99     private PowerManager.WakeLock mWakeLock;
100 
101     // Execution state.
102     private JobParameters mParams;
103     @VisibleForTesting
104     int mVerb;
105     private boolean mCancelled;
106 
107     /**
108      * All the information maintained about the job currently being executed.
109      *
110      * Any reads (dereferences) not done from the handler thread must be synchronized on
111      * {@link #mLock}.
112      * Writes can only be done from the handler thread, or {@link #executeRunnableJob(JobStatus)}.
113      */
114     private JobStatus mRunningJob;
115     private JobCallback mRunningCallback;
116     /** Used to store next job to run when current job is to be preempted. */
117     private int mPreferredUid;
118     IJobService service;
119 
120     /**
121      * Whether this context is free. This is set to false at the start of execution, and reset to
122      * true when execution is complete.
123      */
124     @GuardedBy("mLock")
125     private boolean mAvailable;
126     /** Track start time. */
127     private long mExecutionStartTimeElapsed;
128     /** Track when job will timeout. */
129     private long mTimeoutElapsed;
130 
131     // Debugging: reason this job was last stopped.
132     public String mStoppedReason;
133 
134     // Debugging: time this job was last stopped.
135     public long mStoppedTime;
136 
137     final class JobCallback extends IJobCallback.Stub {
138         public String mStoppedReason;
139         public long mStoppedTime;
140 
141         @Override
acknowledgeStartMessage(int jobId, boolean ongoing)142         public void acknowledgeStartMessage(int jobId, boolean ongoing) {
143             doAcknowledgeStartMessage(this, jobId, ongoing);
144         }
145 
146         @Override
acknowledgeStopMessage(int jobId, boolean reschedule)147         public void acknowledgeStopMessage(int jobId, boolean reschedule) {
148             doAcknowledgeStopMessage(this, jobId, reschedule);
149         }
150 
151         @Override
dequeueWork(int jobId)152         public JobWorkItem dequeueWork(int jobId) {
153             return doDequeueWork(this, jobId);
154         }
155 
156         @Override
completeWork(int jobId, int workId)157         public boolean completeWork(int jobId, int workId) {
158             return doCompleteWork(this, jobId, workId);
159         }
160 
161         @Override
jobFinished(int jobId, boolean reschedule)162         public void jobFinished(int jobId, boolean reschedule) {
163             doJobFinished(this, jobId, reschedule);
164         }
165     }
166 
JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper)167     JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats,
168             JobPackageTracker tracker, Looper looper) {
169         this(service.getContext(), service.getLock(), batteryStats, tracker, service, looper);
170     }
171 
172     @VisibleForTesting
JobServiceContext(Context context, Object lock, IBatteryStats batteryStats, JobPackageTracker tracker, JobCompletedListener completedListener, Looper looper)173     JobServiceContext(Context context, Object lock, IBatteryStats batteryStats,
174             JobPackageTracker tracker, JobCompletedListener completedListener, Looper looper) {
175         mContext = context;
176         mLock = lock;
177         mBatteryStats = batteryStats;
178         mJobPackageTracker = tracker;
179         mCallbackHandler = new JobServiceHandler(looper);
180         mCompletedListener = completedListener;
181         mAvailable = true;
182         mVerb = VERB_FINISHED;
183         mPreferredUid = NO_PREFERRED_UID;
184     }
185 
186     /**
187      * Give a job to this context for execution. Callers must first check {@link #getRunningJobLocked()}
188      * and ensure it is null to make sure this is a valid context.
189      * @param job The status of the job that we are going to run.
190      * @return True if the job is valid and is running. False if the job cannot be executed.
191      */
executeRunnableJob(JobStatus job)192     boolean executeRunnableJob(JobStatus job) {
193         synchronized (mLock) {
194             if (!mAvailable) {
195                 Slog.e(TAG, "Starting new runnable but context is unavailable > Error.");
196                 return false;
197             }
198 
199             mPreferredUid = NO_PREFERRED_UID;
200 
201             mRunningJob = job;
202             mRunningCallback = new JobCallback();
203             final boolean isDeadlineExpired =
204                     job.hasDeadlineConstraint() &&
205                             (job.getLatestRunTimeElapsed() < SystemClock.elapsedRealtime());
206             Uri[] triggeredUris = null;
207             if (job.changedUris != null) {
208                 triggeredUris = new Uri[job.changedUris.size()];
209                 job.changedUris.toArray(triggeredUris);
210             }
211             String[] triggeredAuthorities = null;
212             if (job.changedAuthorities != null) {
213                 triggeredAuthorities = new String[job.changedAuthorities.size()];
214                 job.changedAuthorities.toArray(triggeredAuthorities);
215             }
216             final JobInfo ji = job.getJob();
217             mParams = new JobParameters(mRunningCallback, job.getJobId(), ji.getExtras(),
218                     ji.getTransientExtras(), ji.getClipData(), ji.getClipGrantFlags(),
219                     isDeadlineExpired, triggeredUris, triggeredAuthorities);
220             mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();
221 
222             mVerb = VERB_BINDING;
223             scheduleOpTimeOutLocked();
224             final Intent intent = new Intent().setComponent(job.getServiceComponent());
225             boolean binding = mContext.bindServiceAsUser(intent, this,
226                     Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND,
227                     new UserHandle(job.getUserId()));
228             if (!binding) {
229                 if (DEBUG) {
230                     Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable.");
231                 }
232                 mRunningJob = null;
233                 mRunningCallback = null;
234                 mParams = null;
235                 mExecutionStartTimeElapsed = 0L;
236                 mVerb = VERB_FINISHED;
237                 removeOpTimeOutLocked();
238                 return false;
239             }
240             try {
241                 mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid());
242             } catch (RemoteException e) {
243                 // Whatever.
244             }
245             mJobPackageTracker.noteActive(job);
246             mAvailable = false;
247             mStoppedReason = null;
248             mStoppedTime = 0;
249             return true;
250         }
251     }
252 
253     /**
254      * Used externally to query the running job. Will return null if there is no job running.
255      */
getRunningJobLocked()256     JobStatus getRunningJobLocked() {
257         return mRunningJob;
258     }
259 
260     /** Called externally when a job that was scheduled for execution should be cancelled. */
cancelExecutingJobLocked(int reason, String debugReason)261     void cancelExecutingJobLocked(int reason, String debugReason) {
262         doCancelLocked(reason, debugReason);
263     }
264 
preemptExecutingJobLocked()265     void preemptExecutingJobLocked() {
266         doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption");
267     }
268 
getPreferredUid()269     int getPreferredUid() {
270         return mPreferredUid;
271     }
272 
clearPreferredUid()273     void clearPreferredUid() {
274         mPreferredUid = NO_PREFERRED_UID;
275     }
276 
getExecutionStartTimeElapsed()277     long getExecutionStartTimeElapsed() {
278         return mExecutionStartTimeElapsed;
279     }
280 
getTimeoutElapsed()281     long getTimeoutElapsed() {
282         return mTimeoutElapsed;
283     }
284 
timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId)285     boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId) {
286         final JobStatus executing = getRunningJobLocked();
287         if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId())
288                 && (pkgName == null || pkgName.equals(executing.getSourcePackageName()))
289                 && (!matchJobId || jobId == executing.getJobId())) {
290             if (mVerb == VERB_EXECUTING) {
291                 mParams.setStopReason(JobParameters.REASON_TIMEOUT);
292                 sendStopMessageLocked("force timeout from shell");
293                 return true;
294             }
295         }
296         return false;
297     }
298 
doJobFinished(JobCallback cb, int jobId, boolean reschedule)299     void doJobFinished(JobCallback cb, int jobId, boolean reschedule) {
300         doCallback(cb, reschedule, "app called jobFinished");
301     }
302 
doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule)303     void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
304         doCallback(cb, reschedule, null);
305     }
306 
doAcknowledgeStartMessage(JobCallback cb, int jobId, boolean ongoing)307     void doAcknowledgeStartMessage(JobCallback cb, int jobId, boolean ongoing) {
308         doCallback(cb, ongoing, "finished start");
309     }
310 
doDequeueWork(JobCallback cb, int jobId)311     JobWorkItem doDequeueWork(JobCallback cb, int jobId) {
312         final long ident = Binder.clearCallingIdentity();
313         try {
314             synchronized (mLock) {
315                 assertCallerLocked(cb);
316                 if (mVerb == VERB_STOPPING || mVerb == VERB_FINISHED) {
317                     // This job is either all done, or on its way out.  Either way, it
318                     // should not dispatch any more work.  We will pick up any remaining
319                     // work the next time we start the job again.
320                     return null;
321                 }
322                 final JobWorkItem work = mRunningJob.dequeueWorkLocked();
323                 if (work == null && !mRunningJob.hasExecutingWorkLocked()) {
324                     // This will finish the job.
325                     doCallbackLocked(false, "last work dequeued");
326                 }
327                 return work;
328             }
329         } finally {
330             Binder.restoreCallingIdentity(ident);
331         }
332     }
333 
doCompleteWork(JobCallback cb, int jobId, int workId)334     boolean doCompleteWork(JobCallback cb, int jobId, int workId) {
335         final long ident = Binder.clearCallingIdentity();
336         try {
337             synchronized (mLock) {
338                 assertCallerLocked(cb);
339                 return mRunningJob.completeWorkLocked(ActivityManager.getService(), workId);
340             }
341         } finally {
342             Binder.restoreCallingIdentity(ident);
343         }
344     }
345 
346     /**
347      * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
348      * we intend to send to the client - we stop sending work when the service is unbound so until
349      * then we keep the wakelock.
350      * @param name The concrete component name of the service that has been connected.
351      * @param service The IBinder of the Service's communication channel,
352      */
353     @Override
onServiceConnected(ComponentName name, IBinder service)354     public void onServiceConnected(ComponentName name, IBinder service) {
355         JobStatus runningJob;
356         synchronized (mLock) {
357             // This isn't strictly necessary b/c the JobServiceHandler is running on the main
358             // looper and at this point we can't get any binder callbacks from the client. Better
359             // safe than sorry.
360             runningJob = mRunningJob;
361 
362             if (runningJob == null || !name.equals(runningJob.getServiceComponent())) {
363                 closeAndCleanupJobLocked(true /* needsReschedule */,
364                         "connected for different component");
365                 return;
366             }
367             this.service = IJobService.Stub.asInterface(service);
368             final PowerManager pm =
369                     (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
370             PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
371                     runningJob.getTag());
372             wl.setWorkSource(new WorkSource(runningJob.getSourceUid()));
373             wl.setReferenceCounted(false);
374             wl.acquire();
375 
376             // We use a new wakelock instance per job.  In rare cases there is a race between
377             // teardown following job completion/cancellation and new job service spin-up
378             // such that if we simply assign mWakeLock to be the new instance, we orphan
379             // the currently-live lock instead of cleanly replacing it.  Watch for this and
380             // explicitly fast-forward the release if we're in that situation.
381             if (mWakeLock != null) {
382                 Slog.w(TAG, "Bound new job " + runningJob + " but live wakelock " + mWakeLock
383                         + " tag=" + mWakeLock.getTag());
384                 mWakeLock.release();
385             }
386             mWakeLock = wl;
387             doServiceBoundLocked();
388         }
389     }
390 
391     /** If the client service crashes we reschedule this job and clean up. */
392     @Override
onServiceDisconnected(ComponentName name)393     public void onServiceDisconnected(ComponentName name) {
394         synchronized (mLock) {
395             closeAndCleanupJobLocked(true /* needsReschedule */, "unexpectedly disconnected");
396         }
397     }
398 
399     /**
400      * This class is reused across different clients, and passes itself in as a callback. Check
401      * whether the client exercising the callback is the client we expect.
402      * @return True if the binder calling is coming from the client we expect.
403      */
verifyCallerLocked(JobCallback cb)404     private boolean verifyCallerLocked(JobCallback cb) {
405         if (mRunningCallback != cb) {
406             if (DEBUG) {
407                 Slog.d(TAG, "Stale callback received, ignoring.");
408             }
409             return false;
410         }
411         return true;
412     }
413 
assertCallerLocked(JobCallback cb)414     private void assertCallerLocked(JobCallback cb) {
415         if (!verifyCallerLocked(cb)) {
416             StringBuilder sb = new StringBuilder(128);
417             sb.append("Caller no longer running");
418             if (cb.mStoppedReason != null) {
419                 sb.append(", last stopped ");
420                 TimeUtils.formatDuration(SystemClock.elapsedRealtime() - cb.mStoppedTime, sb);
421                 sb.append(" because: ");
422                 sb.append(cb.mStoppedReason);
423             }
424             throw new SecurityException(sb.toString());
425         }
426     }
427 
428     /**
429      * Scheduling of async messages (basically timeouts at this point).
430      */
431     private class JobServiceHandler extends Handler {
JobServiceHandler(Looper looper)432         JobServiceHandler(Looper looper) {
433             super(looper);
434         }
435 
436         @Override
handleMessage(Message message)437         public void handleMessage(Message message) {
438             switch (message.what) {
439                 case MSG_TIMEOUT:
440                     synchronized (mLock) {
441                         if (message.obj == mRunningCallback) {
442                             handleOpTimeoutLocked();
443                         } else {
444                             JobCallback jc = (JobCallback)message.obj;
445                             StringBuilder sb = new StringBuilder(128);
446                             sb.append("Ignoring timeout of no longer active job");
447                             if (jc.mStoppedReason != null) {
448                                 sb.append(", stopped ");
449                                 TimeUtils.formatDuration(SystemClock.elapsedRealtime()
450                                         - jc.mStoppedTime, sb);
451                                 sb.append(" because: ");
452                                 sb.append(jc.mStoppedReason);
453                             }
454                             Slog.w(TAG, sb.toString());
455                         }
456                     }
457                     break;
458                 default:
459                     Slog.e(TAG, "Unrecognised message: " + message);
460             }
461         }
462     }
463 
doServiceBoundLocked()464     void doServiceBoundLocked() {
465         removeOpTimeOutLocked();
466         handleServiceBoundLocked();
467     }
468 
doCallback(JobCallback cb, boolean reschedule, String reason)469     void doCallback(JobCallback cb, boolean reschedule, String reason) {
470         final long ident = Binder.clearCallingIdentity();
471         try {
472             synchronized (mLock) {
473                 if (!verifyCallerLocked(cb)) {
474                     return;
475                 }
476                 doCallbackLocked(reschedule, reason);
477             }
478         } finally {
479             Binder.restoreCallingIdentity(ident);
480         }
481     }
482 
doCallbackLocked(boolean reschedule, String reason)483     void doCallbackLocked(boolean reschedule, String reason) {
484         if (DEBUG) {
485             Slog.d(TAG, "doCallback of : " + mRunningJob
486                     + " v:" + VERB_STRINGS[mVerb]);
487         }
488         removeOpTimeOutLocked();
489 
490         if (mVerb == VERB_STARTING) {
491             handleStartedLocked(reschedule);
492         } else if (mVerb == VERB_EXECUTING ||
493                 mVerb == VERB_STOPPING) {
494             handleFinishedLocked(reschedule, reason);
495         } else {
496             if (DEBUG) {
497                 Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
498             }
499         }
500     }
501 
doCancelLocked(int arg1, String debugReason)502     void doCancelLocked(int arg1, String debugReason) {
503         if (mVerb == VERB_FINISHED) {
504             if (DEBUG) {
505                 Slog.d(TAG,
506                         "Trying to process cancel for torn-down context, ignoring.");
507             }
508             return;
509         }
510         mParams.setStopReason(arg1);
511         if (arg1 == JobParameters.REASON_PREEMPT) {
512             mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
513                     NO_PREFERRED_UID;
514         }
515         handleCancelLocked(debugReason);
516     }
517 
518     /** Start the job on the service. */
handleServiceBoundLocked()519     private void handleServiceBoundLocked() {
520         if (DEBUG) {
521             Slog.d(TAG, "handleServiceBound for " + mRunningJob.toShortString());
522         }
523         if (mVerb != VERB_BINDING) {
524             Slog.e(TAG, "Sending onStartJob for a job that isn't pending. "
525                     + VERB_STRINGS[mVerb]);
526             closeAndCleanupJobLocked(false /* reschedule */, "started job not pending");
527             return;
528         }
529         if (mCancelled) {
530             if (DEBUG) {
531                 Slog.d(TAG, "Job cancelled while waiting for bind to complete. "
532                         + mRunningJob);
533             }
534             closeAndCleanupJobLocked(true /* reschedule */, "cancelled while waiting for bind");
535             return;
536         }
537         try {
538             mVerb = VERB_STARTING;
539             scheduleOpTimeOutLocked();
540             service.startJob(mParams);
541         } catch (Exception e) {
542             // We catch 'Exception' because client-app malice or bugs might induce a wide
543             // range of possible exception-throw outcomes from startJob() and its handling
544             // of the client's ParcelableBundle extras.
545             Slog.e(TAG, "Error sending onStart message to '" +
546                     mRunningJob.getServiceComponent().getShortClassName() + "' ", e);
547         }
548     }
549 
550     /**
551      * State behaviours.
552      * VERB_STARTING   -> Successful start, change job to VERB_EXECUTING and post timeout.
553      *     _PENDING    -> Error
554      *     _EXECUTING  -> Error
555      *     _STOPPING   -> Error
556      */
handleStartedLocked(boolean workOngoing)557     private void handleStartedLocked(boolean workOngoing) {
558         switch (mVerb) {
559             case VERB_STARTING:
560                 mVerb = VERB_EXECUTING;
561                 if (!workOngoing) {
562                     // Job is finished already so fast-forward to handleFinished.
563                     handleFinishedLocked(false, "onStartJob returned false");
564                     return;
565                 }
566                 if (mCancelled) {
567                     if (DEBUG) {
568                         Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete.");
569                     }
570                     // Cancelled *while* waiting for acknowledgeStartMessage from client.
571                     handleCancelLocked(null);
572                     return;
573                 }
574                 scheduleOpTimeOutLocked();
575                 break;
576             default:
577                 Slog.e(TAG, "Handling started job but job wasn't starting! Was "
578                         + VERB_STRINGS[mVerb] + ".");
579                 return;
580         }
581     }
582 
583     /**
584      * VERB_EXECUTING  -> Client called jobFinished(), clean up and notify done.
585      *     _STOPPING   -> Successful finish, clean up and notify done.
586      *     _STARTING   -> Error
587      *     _PENDING    -> Error
588      */
handleFinishedLocked(boolean reschedule, String reason)589     private void handleFinishedLocked(boolean reschedule, String reason) {
590         switch (mVerb) {
591             case VERB_EXECUTING:
592             case VERB_STOPPING:
593                 closeAndCleanupJobLocked(reschedule, reason);
594                 break;
595             default:
596                 Slog.e(TAG, "Got an execution complete message for a job that wasn't being" +
597                         "executed. Was " + VERB_STRINGS[mVerb] + ".");
598         }
599     }
600 
601     /**
602      * A job can be in various states when a cancel request comes in:
603      * VERB_BINDING    -> Cancelled before bind completed. Mark as cancelled and wait for
604      *                    {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)}
605      *     _STARTING   -> Mark as cancelled and wait for
606      *                    {@link JobServiceContext#doAcknowledgeStartMessage}
607      *     _EXECUTING  -> call {@link #sendStopMessageLocked}}, but only if there are no callbacks
608      *                      in the message queue.
609      *     _ENDING     -> No point in doing anything here, so we ignore.
610      */
handleCancelLocked(String reason)611     private void handleCancelLocked(String reason) {
612         if (JobSchedulerService.DEBUG) {
613             Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " "
614                     + VERB_STRINGS[mVerb]);
615         }
616         switch (mVerb) {
617             case VERB_BINDING:
618             case VERB_STARTING:
619                 mCancelled = true;
620                 applyStoppedReasonLocked(reason);
621                 break;
622             case VERB_EXECUTING:
623                 sendStopMessageLocked(reason);
624                 break;
625             case VERB_STOPPING:
626                 // Nada.
627                 break;
628             default:
629                 Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb);
630                 break;
631         }
632     }
633 
634     /** Process MSG_TIMEOUT here. */
handleOpTimeoutLocked()635     private void handleOpTimeoutLocked() {
636         switch (mVerb) {
637             case VERB_BINDING:
638                 Slog.w(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() +
639                         ", dropping.");
640                 closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while binding");
641                 break;
642             case VERB_STARTING:
643                 // Client unresponsive - wedged or failed to respond in time. We don't really
644                 // know what happened so let's log it and notify the JobScheduler
645                 // FINISHED/NO-RETRY.
646                 Slog.w(TAG, "No response from client for onStartJob " +
647                         mRunningJob != null ? mRunningJob.toShortString() : "<null>");
648                 closeAndCleanupJobLocked(false /* needsReschedule */, "timed out while starting");
649                 break;
650             case VERB_STOPPING:
651                 // At least we got somewhere, so fail but ask the JobScheduler to reschedule.
652                 Slog.w(TAG, "No response from client for onStopJob " +
653                         mRunningJob != null ? mRunningJob.toShortString() : "<null>");
654                 closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping");
655                 break;
656             case VERB_EXECUTING:
657                 // Not an error - client ran out of time.
658                 Slog.i(TAG, "Client timed out while executing (no jobFinished received), " +
659                         "sending onStop: "  +
660                         mRunningJob != null ? mRunningJob.toShortString() : "<null>");
661                 mParams.setStopReason(JobParameters.REASON_TIMEOUT);
662                 sendStopMessageLocked("timeout while executing");
663                 break;
664             default:
665                 Slog.e(TAG, "Handling timeout for an invalid job state: " +
666                         mRunningJob != null ? mRunningJob.toShortString() : "<null>"
667                         + ", dropping.");
668                 closeAndCleanupJobLocked(false /* needsReschedule */, "invalid timeout");
669         }
670     }
671 
672     /**
673      * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
674      * VERB_STOPPING.
675      */
sendStopMessageLocked(String reason)676     private void sendStopMessageLocked(String reason) {
677         removeOpTimeOutLocked();
678         if (mVerb != VERB_EXECUTING) {
679             Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
680             closeAndCleanupJobLocked(false /* reschedule */, reason);
681             return;
682         }
683         try {
684             mVerb = VERB_STOPPING;
685             scheduleOpTimeOutLocked();
686             service.stopJob(mParams);
687         } catch (RemoteException e) {
688             Slog.e(TAG, "Error sending onStopJob to client.", e);
689             // The job's host app apparently crashed during the job, so we should reschedule.
690             closeAndCleanupJobLocked(true /* reschedule */, "host crashed when trying to stop");
691         }
692     }
693 
694     /**
695      * The provided job has finished, either by calling
696      * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
697      * or from acknowledging the stop message we sent. Either way, we're done tracking it and
698      * we want to clean up internally.
699      */
closeAndCleanupJobLocked(boolean reschedule, String reason)700     private void closeAndCleanupJobLocked(boolean reschedule, String reason) {
701         final JobStatus completedJob;
702         if (mVerb == VERB_FINISHED) {
703             return;
704         }
705         applyStoppedReasonLocked(reason);
706         completedJob = mRunningJob;
707         mJobPackageTracker.noteInactive(completedJob);
708         try {
709             mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(),
710                     mRunningJob.getSourceUid());
711         } catch (RemoteException e) {
712             // Whatever.
713         }
714         if (mWakeLock != null) {
715             mWakeLock.release();
716         }
717         mContext.unbindService(JobServiceContext.this);
718         mWakeLock = null;
719         mRunningJob = null;
720         mRunningCallback = null;
721         mParams = null;
722         mVerb = VERB_FINISHED;
723         mCancelled = false;
724         service = null;
725         mAvailable = true;
726         removeOpTimeOutLocked();
727         mCompletedListener.onJobCompletedLocked(completedJob, reschedule);
728     }
729 
applyStoppedReasonLocked(String reason)730     private void applyStoppedReasonLocked(String reason) {
731         if (reason != null && mStoppedReason == null) {
732             mStoppedReason = reason;
733             mStoppedTime = SystemClock.elapsedRealtime();
734             if (mRunningCallback != null) {
735                 mRunningCallback.mStoppedReason = mStoppedReason;
736                 mRunningCallback.mStoppedTime = mStoppedTime;
737             }
738         }
739     }
740 
741     /**
742      * Called when sending a message to the client, over whose execution we have no control. If
743      * we haven't received a response in a certain amount of time, we want to give up and carry
744      * on with life.
745      */
scheduleOpTimeOutLocked()746     private void scheduleOpTimeOutLocked() {
747         removeOpTimeOutLocked();
748 
749         final long timeoutMillis;
750         switch (mVerb) {
751             case VERB_EXECUTING:
752                 timeoutMillis = EXECUTING_TIMESLICE_MILLIS;
753                 break;
754 
755             case VERB_BINDING:
756                 timeoutMillis = OP_BIND_TIMEOUT_MILLIS;
757                 break;
758 
759             default:
760                 timeoutMillis = OP_TIMEOUT_MILLIS;
761                 break;
762         }
763         if (DEBUG) {
764             Slog.d(TAG, "Scheduling time out for '" +
765                     mRunningJob.getServiceComponent().getShortClassName() + "' jId: " +
766                     mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s");
767         }
768         Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT, mRunningCallback);
769         mCallbackHandler.sendMessageDelayed(m, timeoutMillis);
770         mTimeoutElapsed = SystemClock.elapsedRealtime() + timeoutMillis;
771     }
772 
773 
removeOpTimeOutLocked()774     private void removeOpTimeOutLocked() {
775         mCallbackHandler.removeMessages(MSG_TIMEOUT);
776     }
777 }
778