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