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
17package com.android.server.job;
18
19import android.app.ActivityManager;
20import android.app.job.JobInfo;
21import android.app.job.JobParameters;
22import android.app.job.IJobCallback;
23import android.app.job.IJobService;
24import android.app.job.JobWorkItem;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.content.ServiceConnection;
29import android.net.Uri;
30import android.os.Binder;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.Looper;
34import android.os.Message;
35import android.os.PowerManager;
36import android.os.RemoteException;
37import android.os.SystemClock;
38import android.os.UserHandle;
39import android.os.WorkSource;
40import android.util.Slog;
41import android.util.TimeUtils;
42
43import com.android.internal.annotations.GuardedBy;
44import com.android.internal.annotations.VisibleForTesting;
45import com.android.internal.app.IBatteryStats;
46import 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 */
64public 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
142        public void acknowledgeStartMessage(int jobId, boolean ongoing) {
143            doAcknowledgeStartMessage(this, jobId, ongoing);
144        }
145
146        @Override
147        public void acknowledgeStopMessage(int jobId, boolean reschedule) {
148            doAcknowledgeStopMessage(this, jobId, reschedule);
149        }
150
151        @Override
152        public JobWorkItem dequeueWork(int jobId) {
153            return doDequeueWork(this, jobId);
154        }
155
156        @Override
157        public boolean completeWork(int jobId, int workId) {
158            return doCompleteWork(this, jobId, workId);
159        }
160
161        @Override
162        public void jobFinished(int jobId, boolean reschedule) {
163            doJobFinished(this, jobId, reschedule);
164        }
165    }
166
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
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     */
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     */
256    JobStatus getRunningJobLocked() {
257        return mRunningJob;
258    }
259
260    /** Called externally when a job that was scheduled for execution should be cancelled. */
261    void cancelExecutingJobLocked(int reason, String debugReason) {
262        doCancelLocked(reason, debugReason);
263    }
264
265    void preemptExecutingJobLocked() {
266        doCancelLocked(JobParameters.REASON_PREEMPT, "cancelled due to preemption");
267    }
268
269    int getPreferredUid() {
270        return mPreferredUid;
271    }
272
273    void clearPreferredUid() {
274        mPreferredUid = NO_PREFERRED_UID;
275    }
276
277    long getExecutionStartTimeElapsed() {
278        return mExecutionStartTimeElapsed;
279    }
280
281    long getTimeoutElapsed() {
282        return mTimeoutElapsed;
283    }
284
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
299    void doJobFinished(JobCallback cb, int jobId, boolean reschedule) {
300        doCallback(cb, reschedule, "app called jobFinished");
301    }
302
303    void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) {
304        doCallback(cb, reschedule, null);
305    }
306
307    void doAcknowledgeStartMessage(JobCallback cb, int jobId, boolean ongoing) {
308        doCallback(cb, ongoing, "finished start");
309    }
310
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
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
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
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     */
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
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 {
432        JobServiceHandler(Looper looper) {
433            super(looper);
434        }
435
436        @Override
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
464    void doServiceBoundLocked() {
465        removeOpTimeOutLocked();
466        handleServiceBoundLocked();
467    }
468
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
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
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. */
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     */
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     */
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     */
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. */
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     */
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     */
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
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     */
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
774    private void removeOpTimeOutLocked() {
775        mCallbackHandler.removeMessages(MSG_TIMEOUT);
776    }
777}
778