JobServiceContext.java revision 326f230ceba67a9b72d2606f7c503bef6f938ddd
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.JobParameters;
21import android.app.job.IJobCallback;
22import android.app.job.IJobService;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.ServiceConnection;
27import android.os.Binder;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.Looper;
31import android.os.Message;
32import android.os.PowerManager;
33import android.os.RemoteException;
34import android.os.SystemClock;
35import android.os.UserHandle;
36import android.os.WorkSource;
37import android.util.Slog;
38
39import com.android.internal.annotations.GuardedBy;
40import com.android.internal.annotations.VisibleForTesting;
41import com.android.internal.app.IBatteryStats;
42import com.android.server.job.controllers.JobStatus;
43
44import java.util.concurrent.atomic.AtomicBoolean;
45
46/**
47 * Handles client binding and lifecycle of a job. Jobs execute one at a time on an instance of this
48 * class.
49 *
50 * There are two important interactions into this class from the
51 * {@link com.android.server.job.JobSchedulerService}. To execute a job and to cancel a job.
52 * - Execution of a new job is handled by the {@link #mAvailable}. This bit is flipped once when a
53 * job lands, and again when it is complete.
54 * - Cancelling is trickier, because there are also interactions from the client. It's possible
55 * the {@link com.android.server.job.JobServiceContext.JobServiceHandler} tries to process a
56 * {@link #MSG_CANCEL} after the client has already finished. This is handled by having
57 * {@link com.android.server.job.JobServiceContext.JobServiceHandler#handleCancelH} check whether
58 * the context is still valid.
59 * To mitigate this, tearing down the context removes all messages from the handler, including any
60 * tardy {@link #MSG_CANCEL}s. Additionally, we avoid sending duplicate onStopJob()
61 * calls to the client after they've specified jobFinished().
62 *
63 */
64public class JobServiceContext extends IJobCallback.Stub implements ServiceConnection {
65    private static final boolean DEBUG = false;
66    private static final String TAG = "JobServiceContext";
67    /** Define the maximum # of jobs allowed to run on a service at once. */
68    private static final int defaultMaxActiveJobsPerService =
69            ActivityManager.isLowRamDeviceStatic() ? 1 : 3;
70    /** Amount of time a job is allowed to execute for before being considered timed-out. */
71    private static final long EXECUTING_TIMESLICE_MILLIS = 60 * 1000;
72    /** Amount of time the JobScheduler will wait for a response from an app for a message. */
73    private static final long OP_TIMEOUT_MILLIS = 8 * 1000;
74
75    private static final String[] VERB_STRINGS = {
76            "VERB_BINDING", "VERB_STARTING", "VERB_EXECUTING", "VERB_STOPPING"
77    };
78
79    // States that a job occupies while interacting with the client.
80    static final int VERB_BINDING = 0;
81    static final int VERB_STARTING = 1;
82    static final int VERB_EXECUTING = 2;
83    static final int VERB_STOPPING = 3;
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    /** Received a callback from client. */
89    private static final int MSG_CALLBACK = 1;
90    /** Run through list and start any ready jobs.*/
91    private static final int MSG_SERVICE_BOUND = 2;
92    /** Cancel a job. */
93    private static final int MSG_CANCEL = 3;
94    /** Shutdown the job. Used when the client crashes and we can't die gracefully.*/
95    private static final int MSG_SHUTDOWN_EXECUTION = 4;
96
97    private final Handler mCallbackHandler;
98    /** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
99    private final JobCompletedListener mCompletedListener;
100    /** Used for service binding, etc. */
101    private final Context mContext;
102    private final IBatteryStats mBatteryStats;
103    private PowerManager.WakeLock mWakeLock;
104
105    // Execution state.
106    private JobParameters mParams;
107    @VisibleForTesting
108    int mVerb;
109    private AtomicBoolean mCancelled = new AtomicBoolean();
110
111    /** All the information maintained about the job currently being executed. */
112    private JobStatus mRunningJob;
113    /** Binder to the client service. */
114    IJobService service;
115
116    private final Object mLock = new Object();
117    /**
118     * Whether this context is free. This is set to false at the start of execution, and reset to
119     * true when execution is complete.
120     */
121    @GuardedBy("mLock")
122    private boolean mAvailable;
123    /** Track start time. */
124    private long mExecutionStartTimeElapsed;
125    /** Track when job will timeout. */
126    private long mTimeoutElapsed;
127
128    JobServiceContext(JobSchedulerService service, IBatteryStats batteryStats, Looper looper) {
129        this(service.getContext(), batteryStats, service, looper);
130    }
131
132    @VisibleForTesting
133    JobServiceContext(Context context, IBatteryStats batteryStats,
134            JobCompletedListener completedListener, Looper looper) {
135        mContext = context;
136        mBatteryStats = batteryStats;
137        mCallbackHandler = new JobServiceHandler(looper);
138        mCompletedListener = completedListener;
139        mAvailable = true;
140    }
141
142    /**
143     * Give a job to this context for execution. Callers must first check {@link #isAvailable()}
144     * to make sure this is a valid context.
145     * @param job The status of the job that we are going to run.
146     * @return True if the job is valid and is running. False if the job cannot be executed.
147     */
148    boolean executeRunnableJob(JobStatus job) {
149        synchronized (mLock) {
150            if (!mAvailable) {
151                Slog.e(TAG, "Starting new runnable but context is unavailable > Error.");
152                return false;
153            }
154
155            mRunningJob = job;
156            mParams = new JobParameters(this, job.getJobId(), job.getExtras(),
157                    !job.isConstraintsSatisfied());
158            mExecutionStartTimeElapsed = SystemClock.elapsedRealtime();
159
160            mVerb = VERB_BINDING;
161            scheduleOpTimeOut();
162            final Intent intent = new Intent().setComponent(job.getServiceComponent());
163            boolean binding = mContext.bindServiceAsUser(intent, this,
164                    Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND,
165                    new UserHandle(job.getUserId()));
166            if (!binding) {
167                if (DEBUG) {
168                    Slog.d(TAG, job.getServiceComponent().getShortClassName() + " unavailable.");
169                }
170                mRunningJob = null;
171                mParams = null;
172                mExecutionStartTimeElapsed = 0L;
173                removeOpTimeOut();
174                return false;
175            }
176            try {
177                mBatteryStats.noteJobStart(job.getName(), job.getUid());
178            } catch (RemoteException e) {
179                // Whatever.
180            }
181            mAvailable = false;
182            return true;
183        }
184    }
185
186    /**
187     * Used externally to query the running job. Will return null if there is no job running.
188     * Be careful when using this function, at any moment it's possible that the job returned may
189     * stop executing.
190     */
191    JobStatus getRunningJob() {
192        synchronized (mLock) {
193            return mRunningJob;
194        }
195    }
196
197    /** Called externally when a job that was scheduled for execution should be cancelled. */
198    void cancelExecutingJob() {
199        mCallbackHandler.obtainMessage(MSG_CANCEL).sendToTarget();
200    }
201
202    /**
203     * @return Whether this context is available to handle incoming work.
204     */
205    boolean isAvailable() {
206        synchronized (mLock) {
207            return mAvailable;
208        }
209    }
210
211    long getExecutionStartTimeElapsed() {
212        return mExecutionStartTimeElapsed;
213    }
214
215    long getTimeoutElapsed() {
216        return mTimeoutElapsed;
217    }
218
219    @Override
220    public void jobFinished(int jobId, boolean reschedule) {
221        if (!verifyCallingUid()) {
222            return;
223        }
224        mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0)
225                .sendToTarget();
226    }
227
228    @Override
229    public void acknowledgeStopMessage(int jobId, boolean reschedule) {
230        if (!verifyCallingUid()) {
231            return;
232        }
233        mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, reschedule ? 1 : 0)
234                .sendToTarget();
235    }
236
237    @Override
238    public void acknowledgeStartMessage(int jobId, boolean ongoing) {
239        if (!verifyCallingUid()) {
240            return;
241        }
242        mCallbackHandler.obtainMessage(MSG_CALLBACK, jobId, ongoing ? 1 : 0).sendToTarget();
243    }
244
245    /**
246     * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work
247     * we intend to send to the client - we stop sending work when the service is unbound so until
248     * then we keep the wakelock.
249     * @param name The concrete component name of the service that has been connected.
250     * @param service The IBinder of the Service's communication channel,
251     */
252    @Override
253    public void onServiceConnected(ComponentName name, IBinder service) {
254        if (!name.equals(mRunningJob.getServiceComponent())) {
255            mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
256            return;
257        }
258        this.service = IJobService.Stub.asInterface(service);
259        final PowerManager pm =
260                (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
261        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mRunningJob.getTag());
262        mWakeLock.setWorkSource(new WorkSource(mRunningJob.getUid()));
263        mWakeLock.setReferenceCounted(false);
264        mWakeLock.acquire();
265        mCallbackHandler.obtainMessage(MSG_SERVICE_BOUND).sendToTarget();
266    }
267
268    /** If the client service crashes we reschedule this job and clean up. */
269    @Override
270    public void onServiceDisconnected(ComponentName name) {
271        mCallbackHandler.obtainMessage(MSG_SHUTDOWN_EXECUTION).sendToTarget();
272    }
273
274    /**
275     * This class is reused across different clients, and passes itself in as a callback. Check
276     * whether the client exercising the callback is the client we expect.
277     * @return True if the binder calling is coming from the client we expect.
278     */
279    private boolean verifyCallingUid() {
280        if (mRunningJob == null || Binder.getCallingUid() != mRunningJob.getUid()) {
281            if (DEBUG) {
282                Slog.d(TAG, "Stale callback received, ignoring.");
283            }
284            return false;
285        }
286        return true;
287    }
288
289    /**
290     * Handles the lifecycle of the JobService binding/callbacks, etc. The convention within this
291     * class is to append 'H' to each function name that can only be called on this handler. This
292     * isn't strictly necessary because all of these functions are private, but helps clarity.
293     */
294    private class JobServiceHandler extends Handler {
295        JobServiceHandler(Looper looper) {
296            super(looper);
297        }
298
299        @Override
300        public void handleMessage(Message message) {
301            switch (message.what) {
302                case MSG_SERVICE_BOUND:
303                    removeOpTimeOut();
304                    handleServiceBoundH();
305                    break;
306                case MSG_CALLBACK:
307                    if (DEBUG) {
308                        Slog.d(TAG, "MSG_CALLBACK of : " + mRunningJob + " v:" +
309                                (mVerb >= 0 ? VERB_STRINGS[mVerb] : "[invalid]"));
310                    }
311                    removeOpTimeOut();
312
313                    if (mVerb == VERB_STARTING) {
314                        final boolean workOngoing = message.arg2 == 1;
315                        handleStartedH(workOngoing);
316                    } else if (mVerb == VERB_EXECUTING ||
317                            mVerb == VERB_STOPPING) {
318                        final boolean reschedule = message.arg2 == 1;
319                        handleFinishedH(reschedule);
320                    } else {
321                        if (DEBUG) {
322                            Slog.d(TAG, "Unrecognised callback: " + mRunningJob);
323                        }
324                    }
325                    break;
326                case MSG_CANCEL:
327                    handleCancelH();
328                    break;
329                case MSG_TIMEOUT:
330                    handleOpTimeoutH();
331                    break;
332                case MSG_SHUTDOWN_EXECUTION:
333                    closeAndCleanupJobH(true /* needsReschedule */);
334                    break;
335                default:
336                    Slog.e(TAG, "Unrecognised message: " + message);
337            }
338        }
339
340        /** Start the job on the service. */
341        private void handleServiceBoundH() {
342            if (DEBUG) {
343                Slog.d(TAG, "MSG_SERVICE_BOUND for " + mRunningJob.toShortString());
344            }
345            if (mVerb != VERB_BINDING) {
346                Slog.e(TAG, "Sending onStartJob for a job that isn't pending. "
347                        + VERB_STRINGS[mVerb]);
348                closeAndCleanupJobH(false /* reschedule */);
349                return;
350            }
351            if (mCancelled.get()) {
352                if (DEBUG) {
353                    Slog.d(TAG, "Job cancelled while waiting for bind to complete. "
354                            + mRunningJob);
355                }
356                closeAndCleanupJobH(true /* reschedule */);
357                return;
358            }
359            try {
360                mVerb = VERB_STARTING;
361                scheduleOpTimeOut();
362                service.startJob(mParams);
363            } catch (RemoteException e) {
364                Slog.e(TAG, "Error sending onStart message to '" +
365                        mRunningJob.getServiceComponent().getShortClassName() + "' ", e);
366            }
367        }
368
369        /**
370         * State behaviours.
371         * VERB_STARTING   -> Successful start, change job to VERB_EXECUTING and post timeout.
372         *     _PENDING    -> Error
373         *     _EXECUTING  -> Error
374         *     _STOPPING   -> Error
375         */
376        private void handleStartedH(boolean workOngoing) {
377            switch (mVerb) {
378                case VERB_STARTING:
379                    mVerb = VERB_EXECUTING;
380                    if (!workOngoing) {
381                        // Job is finished already so fast-forward to handleFinished.
382                        handleFinishedH(false);
383                        return;
384                    }
385                    if (mCancelled.get()) {
386                        if (DEBUG) {
387                            Slog.d(TAG, "Job cancelled while waiting for onStartJob to complete.");
388                        }
389                        // Cancelled *while* waiting for acknowledgeStartMessage from client.
390                        handleCancelH();
391                        return;
392                    }
393                    scheduleOpTimeOut();
394                    break;
395                default:
396                    Slog.e(TAG, "Handling started job but job wasn't starting! Was "
397                            + VERB_STRINGS[mVerb] + ".");
398                    return;
399            }
400        }
401
402        /**
403         * VERB_EXECUTING  -> Client called jobFinished(), clean up and notify done.
404         *     _STOPPING   -> Successful finish, clean up and notify done.
405         *     _STARTING   -> Error
406         *     _PENDING    -> Error
407         */
408        private void handleFinishedH(boolean reschedule) {
409            switch (mVerb) {
410                case VERB_EXECUTING:
411                case VERB_STOPPING:
412                    closeAndCleanupJobH(reschedule);
413                    break;
414                default:
415                    Slog.e(TAG, "Got an execution complete message for a job that wasn't being" +
416                            "executed. Was " + VERB_STRINGS[mVerb] + ".");
417            }
418        }
419
420        /**
421         * A job can be in various states when a cancel request comes in:
422         * VERB_BINDING    -> Cancelled before bind completed. Mark as cancelled and wait for
423         *                    {@link #onServiceConnected(android.content.ComponentName, android.os.IBinder)}
424         *     _STARTING   -> Mark as cancelled and wait for
425         *                    {@link JobServiceContext#acknowledgeStartMessage(int, boolean)}
426         *     _EXECUTING  -> call {@link #sendStopMessageH}}, but only if there are no callbacks
427         *                      in the message queue.
428         *     _ENDING     -> No point in doing anything here, so we ignore.
429         */
430        private void handleCancelH() {
431            if (mRunningJob == null) {
432                if (DEBUG) {
433                    Slog.d(TAG, "Trying to process cancel for torn-down context, ignoring.");
434                }
435                return;
436            }
437            if (JobSchedulerService.DEBUG) {
438                Slog.d(TAG, "Handling cancel for: " + mRunningJob.getJobId() + " "
439                        + VERB_STRINGS[mVerb]);
440            }
441            switch (mVerb) {
442                case VERB_BINDING:
443                case VERB_STARTING:
444                    mCancelled.set(true);
445                    break;
446                case VERB_EXECUTING:
447                    if (hasMessages(MSG_CALLBACK)) {
448                        // If the client has called jobFinished, ignore this cancel.
449                        return;
450                    }
451                    sendStopMessageH();
452                    break;
453                case VERB_STOPPING:
454                    // Nada.
455                    break;
456                default:
457                    Slog.e(TAG, "Cancelling a job without a valid verb: " + mVerb);
458                    break;
459            }
460        }
461
462        /** Process MSG_TIMEOUT here. */
463        private void handleOpTimeoutH() {
464            switch (mVerb) {
465                case VERB_BINDING:
466                    Slog.e(TAG, "Time-out while trying to bind " + mRunningJob.toShortString() +
467                            ", dropping.");
468                    closeAndCleanupJobH(false /* needsReschedule */);
469                    break;
470                case VERB_STARTING:
471                    // Client unresponsive - wedged or failed to respond in time. We don't really
472                    // know what happened so let's log it and notify the JobScheduler
473                    // FINISHED/NO-RETRY.
474                    Slog.e(TAG, "No response from client for onStartJob '" +
475                            mRunningJob.toShortString());
476                    closeAndCleanupJobH(false /* needsReschedule */);
477                    break;
478                case VERB_STOPPING:
479                    // At least we got somewhere, so fail but ask the JobScheduler to reschedule.
480                    Slog.e(TAG, "No response from client for onStopJob, '" +
481                            mRunningJob.toShortString());
482                    closeAndCleanupJobH(true /* needsReschedule */);
483                    break;
484                case VERB_EXECUTING:
485                    // Not an error - client ran out of time.
486                    Slog.i(TAG, "Client timed out while executing (no jobFinished received)." +
487                            " sending onStop. "  + mRunningJob.toShortString());
488                    sendStopMessageH();
489                    break;
490                default:
491                    Slog.e(TAG, "Handling timeout for an invalid job state: " +
492                            mRunningJob.toShortString() + ", dropping.");
493                    closeAndCleanupJobH(false /* needsReschedule */);
494            }
495        }
496
497        /**
498         * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
499         * VERB_STOPPING.
500         */
501        private void sendStopMessageH() {
502            removeOpTimeOut();
503            if (mVerb != VERB_EXECUTING) {
504                Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
505                closeAndCleanupJobH(false /* reschedule */);
506                return;
507            }
508            try {
509                mVerb = VERB_STOPPING;
510                scheduleOpTimeOut();
511                service.stopJob(mParams);
512            } catch (RemoteException e) {
513                Slog.e(TAG, "Error sending onStopJob to client.", e);
514                closeAndCleanupJobH(false /* reschedule */);
515            }
516        }
517
518        /**
519         * The provided job has finished, either by calling
520         * {@link android.app.job.JobService#jobFinished(android.app.job.JobParameters, boolean)}
521         * or from acknowledging the stop message we sent. Either way, we're done tracking it and
522         * we want to clean up internally.
523         */
524        private void closeAndCleanupJobH(boolean reschedule) {
525            final JobStatus completedJob = mRunningJob;
526            synchronized (mLock) {
527                try {
528                    mBatteryStats.noteJobFinish(mRunningJob.getName(), mRunningJob.getUid());
529                } catch (RemoteException e) {
530                    // Whatever.
531                }
532                if (mWakeLock != null) {
533                    mWakeLock.release();
534                }
535                mContext.unbindService(JobServiceContext.this);
536                mWakeLock = null;
537                mRunningJob = null;
538                mParams = null;
539                mVerb = -1;
540                mCancelled.set(false);
541                service = null;
542                mAvailable = true;
543            }
544            removeOpTimeOut();
545            removeMessages(MSG_CALLBACK);
546            removeMessages(MSG_SERVICE_BOUND);
547            removeMessages(MSG_CANCEL);
548            removeMessages(MSG_SHUTDOWN_EXECUTION);
549            mCompletedListener.onJobCompleted(completedJob, reschedule);
550        }
551    }
552
553    /**
554     * Called when sending a message to the client, over whose execution we have no control. If
555     * we haven't received a response in a certain amount of time, we want to give up and carry
556     * on with life.
557     */
558    private void scheduleOpTimeOut() {
559        removeOpTimeOut();
560
561        final long timeoutMillis = (mVerb == VERB_EXECUTING) ?
562                EXECUTING_TIMESLICE_MILLIS : OP_TIMEOUT_MILLIS;
563        if (DEBUG) {
564            Slog.d(TAG, "Scheduling time out for '" +
565                    mRunningJob.getServiceComponent().getShortClassName() + "' jId: " +
566                    mParams.getJobId() + ", in " + (timeoutMillis / 1000) + " s");
567        }
568        Message m = mCallbackHandler.obtainMessage(MSG_TIMEOUT);
569        mCallbackHandler.sendMessageDelayed(m, timeoutMillis);
570        mTimeoutElapsed = SystemClock.elapsedRealtime() + timeoutMillis;
571    }
572
573
574    private void removeOpTimeOut() {
575        mCallbackHandler.removeMessages(MSG_TIMEOUT);
576    }
577}
578