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