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