Home | History | Annotate | Download | only in job
      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 
     17 package com.android.server.job;
     18 
     19 import android.app.ActivityManager;
     20 import android.app.job.JobParameters;
     21 import android.app.job.IJobCallback;
     22 import android.app.job.IJobService;
     23 import android.content.ComponentName;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.ServiceConnection;
     27 import android.os.Binder;
     28 import android.os.Handler;
     29 import android.os.IBinder;
     30 import android.os.Looper;
     31 import android.os.Message;
     32 import android.os.PowerManager;
     33 import android.os.RemoteException;
     34 import android.os.SystemClock;
     35 import android.os.UserHandle;
     36 import android.os.WorkSource;
     37 import android.util.Slog;
     38 
     39 import com.android.internal.annotations.GuardedBy;
     40 import com.android.internal.annotations.VisibleForTesting;
     41 import com.android.internal.app.IBatteryStats;
     42 import com.android.server.job.controllers.JobStatus;
     43 
     44 import 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  */
     64 public 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