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 android.app.job;
     18 
     19 import android.app.Service;
     20 import android.content.Intent;
     21 import android.os.Handler;
     22 import android.os.IBinder;
     23 import android.os.Looper;
     24 import android.os.Message;
     25 import android.os.RemoteException;
     26 import android.util.Log;
     27 
     28 import com.android.internal.annotations.GuardedBy;
     29 
     30 /**
     31  * <p>Entry point for the callback from the {@link android.app.job.JobScheduler}.</p>
     32  * <p>This is the base class that handles asynchronous requests that were previously scheduled. You
     33  * are responsible for overriding {@link JobService#onStartJob(JobParameters)}, which is where
     34  * you will implement your job logic.</p>
     35  * <p>This service executes each incoming job on a {@link android.os.Handler} running on your
     36  * application's main thread. This means that you <b>must</b> offload your execution logic to
     37  * another thread/handler/{@link android.os.AsyncTask} of your choosing. Not doing so will result
     38  * in blocking any future callbacks from the JobManager - specifically
     39  * {@link #onStopJob(android.app.job.JobParameters)}, which is meant to inform you that the
     40  * scheduling requirements are no longer being met.</p>
     41  */
     42 public abstract class JobService extends Service {
     43     private static final String TAG = "JobService";
     44 
     45     /**
     46      * Job services must be protected with this permission:
     47      *
     48      * <pre class="prettyprint">
     49      *     <service android:name="MyJobService"
     50      *              android:permission="android.permission.BIND_JOB_SERVICE" >
     51      *         ...
     52      *     </service>
     53      * </pre>
     54      *
     55      * <p>If a job service is declared in the manifest but not protected with this
     56      * permission, that service will be ignored by the OS.
     57      */
     58     public static final String PERMISSION_BIND =
     59             "android.permission.BIND_JOB_SERVICE";
     60 
     61     /**
     62      * Identifier for a message that will result in a call to
     63      * {@link #onStartJob(android.app.job.JobParameters)}.
     64      */
     65     private final int MSG_EXECUTE_JOB = 0;
     66     /**
     67      * Message that will result in a call to {@link #onStopJob(android.app.job.JobParameters)}.
     68      */
     69     private final int MSG_STOP_JOB = 1;
     70     /**
     71      * Message that the client has completed execution of this job.
     72      */
     73     private final int MSG_JOB_FINISHED = 2;
     74 
     75     /** Lock object for {@link #mHandler}. */
     76     private final Object mHandlerLock = new Object();
     77 
     78     /**
     79      * Handler we post jobs to. Responsible for calling into the client logic, and handling the
     80      * callback to the system.
     81      */
     82     @GuardedBy("mHandlerLock")
     83     JobHandler mHandler;
     84 
     85     /** Binder for this service. */
     86     IJobService mBinder = new IJobService.Stub() {
     87         @Override
     88         public void startJob(JobParameters jobParams) {
     89             ensureHandler();
     90             Message m = Message.obtain(mHandler, MSG_EXECUTE_JOB, jobParams);
     91             m.sendToTarget();
     92         }
     93         @Override
     94         public void stopJob(JobParameters jobParams) {
     95             ensureHandler();
     96             Message m = Message.obtain(mHandler, MSG_STOP_JOB, jobParams);
     97             m.sendToTarget();
     98         }
     99     };
    100 
    101     /** @hide */
    102     void ensureHandler() {
    103         synchronized (mHandlerLock) {
    104             if (mHandler == null) {
    105                 mHandler = new JobHandler(getMainLooper());
    106             }
    107         }
    108     }
    109 
    110     /**
    111      * Runs on application's main thread - callbacks are meant to offboard work to some other
    112      * (app-specified) mechanism.
    113      * @hide
    114      */
    115     class JobHandler extends Handler {
    116         JobHandler(Looper looper) {
    117             super(looper);
    118         }
    119 
    120         @Override
    121         public void handleMessage(Message msg) {
    122             final JobParameters params = (JobParameters) msg.obj;
    123             switch (msg.what) {
    124                 case MSG_EXECUTE_JOB:
    125                     try {
    126                         boolean workOngoing = JobService.this.onStartJob(params);
    127                         ackStartMessage(params, workOngoing);
    128                     } catch (Exception e) {
    129                         Log.e(TAG, "Error while executing job: " + params.getJobId());
    130                         throw new RuntimeException(e);
    131                     }
    132                     break;
    133                 case MSG_STOP_JOB:
    134                     try {
    135                         boolean ret = JobService.this.onStopJob(params);
    136                         ackStopMessage(params, ret);
    137                     } catch (Exception e) {
    138                         Log.e(TAG, "Application unable to handle onStopJob.", e);
    139                         throw new RuntimeException(e);
    140                     }
    141                     break;
    142                 case MSG_JOB_FINISHED:
    143                     final boolean needsReschedule = (msg.arg2 == 1);
    144                     IJobCallback callback = params.getCallback();
    145                     if (callback != null) {
    146                         try {
    147                             callback.jobFinished(params.getJobId(), needsReschedule);
    148                         } catch (RemoteException e) {
    149                             Log.e(TAG, "Error reporting job finish to system: binder has gone" +
    150                                     "away.");
    151                         }
    152                     } else {
    153                         Log.e(TAG, "finishJob() called for a nonexistent job id.");
    154                     }
    155                     break;
    156                 default:
    157                     Log.e(TAG, "Unrecognised message received.");
    158                     break;
    159             }
    160         }
    161 
    162         private void ackStartMessage(JobParameters params, boolean workOngoing) {
    163             final IJobCallback callback = params.getCallback();
    164             final int jobId = params.getJobId();
    165             if (callback != null) {
    166                 try {
    167                      callback.acknowledgeStartMessage(jobId, workOngoing);
    168                 } catch(RemoteException e) {
    169                     Log.e(TAG, "System unreachable for starting job.");
    170                 }
    171             } else {
    172                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    173                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
    174                 }
    175             }
    176         }
    177 
    178         private void ackStopMessage(JobParameters params, boolean reschedule) {
    179             final IJobCallback callback = params.getCallback();
    180             final int jobId = params.getJobId();
    181             if (callback != null) {
    182                 try {
    183                     callback.acknowledgeStopMessage(jobId, reschedule);
    184                 } catch(RemoteException e) {
    185                     Log.e(TAG, "System unreachable for stopping job.");
    186                 }
    187             } else {
    188                 if (Log.isLoggable(TAG, Log.DEBUG)) {
    189                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
    190                 }
    191             }
    192         }
    193     }
    194 
    195     /** @hide */
    196     public final IBinder onBind(Intent intent) {
    197         return mBinder.asBinder();
    198     }
    199 
    200     /**
    201      * Override this method with the callback logic for your job. Any such logic needs to be
    202      * performed on a separate thread, as this function is executed on your application's main
    203      * thread.
    204      *
    205      * @param params Parameters specifying info about this job, including the extras bundle you
    206      *               optionally provided at job-creation time.
    207      * @return True if your service needs to process the work (on a separate thread). False if
    208      * there's no more work to be done for this job.
    209      */
    210     public abstract boolean onStartJob(JobParameters params);
    211 
    212     /**
    213      * This method is called if the system has determined that you must stop execution of your job
    214      * even before you've had a chance to call {@link #jobFinished(JobParameters, boolean)}.
    215      *
    216      * <p>This will happen if the requirements specified at schedule time are no longer met. For
    217      * example you may have requested WiFi with
    218      * {@link android.app.job.JobInfo.Builder#setRequiredNetworkType(int)}, yet while your
    219      * job was executing the user toggled WiFi. Another example is if you had specified
    220      * {@link android.app.job.JobInfo.Builder#setRequiresDeviceIdle(boolean)}, and the phone left its
    221      * idle maintenance window. You are solely responsible for the behaviour of your application
    222      * upon receipt of this message; your app will likely start to misbehave if you ignore it. One
    223      * immediate repercussion is that the system will cease holding a wakelock for you.</p>
    224      *
    225      * @param params Parameters specifying info about this job.
    226      * @return True to indicate to the JobManager whether you'd like to reschedule this job based
    227      * on the retry criteria provided at job creation-time. False to drop the job. Regardless of
    228      * the value returned, your job must stop executing.
    229      */
    230     public abstract boolean onStopJob(JobParameters params);
    231 
    232     /**
    233      * Callback to inform the JobManager you've finished executing. This can be called from any
    234      * thread, as it will ultimately be run on your application's main thread. When the system
    235      * receives this message it will release the wakelock being held.
    236      * <p>
    237      *     You can specify post-execution behaviour to the scheduler here with
    238      *     <code>needsReschedule </code>. This will apply a back-off timer to your job based on
    239      *     the default, or what was set with
    240      *     {@link android.app.job.JobInfo.Builder#setBackoffCriteria(long, int)}. The original
    241      *     requirements are always honoured even for a backed-off job. Note that a job running in
    242      *     idle mode will not be backed-off. Instead what will happen is the job will be re-added
    243      *     to the queue and re-executed within a future idle maintenance window.
    244      * </p>
    245      *
    246      * @param params Parameters specifying system-provided info about this job, this was given to
    247      *               your application in {@link #onStartJob(JobParameters)}.
    248      * @param needsReschedule True if this job should be rescheduled according to the back-off
    249      *                        criteria specified at schedule-time. False otherwise.
    250      */
    251     public final void jobFinished(JobParameters params, boolean needsReschedule) {
    252         ensureHandler();
    253         Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
    254         m.arg2 = needsReschedule ? 1 : 0;
    255         m.sendToTarget();
    256     }
    257 }