• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static android.app.job.JobScheduler.THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION;
20 
21 import android.annotation.BytesLong;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.Notification;
25 import android.app.Service;
26 import android.compat.Compatibility;
27 import android.content.Intent;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.Looper;
31 import android.os.Message;
32 import android.os.RemoteException;
33 import android.util.Log;
34 
35 import com.android.internal.os.SomeArgs;
36 
37 import java.lang.ref.WeakReference;
38 
39 /**
40  * Helper for implementing a {@link android.app.Service} that interacts with
41  * {@link JobScheduler}.  This is not intended for use by regular applications, but
42  * allows frameworks built on top of the platform to create their own
43  * {@link android.app.Service} that interact with {@link JobScheduler} as well as
44  * add in additional functionality.  If you just want to execute jobs normally, you
45  * should instead be looking at {@link JobService}.
46  */
47 public abstract class JobServiceEngine {
48     private static final String TAG = "JobServiceEngine";
49 
50     /**
51      * Identifier for a message that will result in a call to
52      * {@link #onStartJob(android.app.job.JobParameters)}.
53      */
54     private static final int MSG_EXECUTE_JOB = 0;
55     /**
56      * Message that will result in a call to {@link #onStopJob(android.app.job.JobParameters)}.
57      */
58     private static final int MSG_STOP_JOB = 1;
59     /**
60      * Message that the client has completed execution of this job.
61      */
62     private static final int MSG_JOB_FINISHED = 2;
63     /**
64      * Message that will result in a call to
65      * {@link #getTransferredDownloadBytes(JobParameters, JobWorkItem)}.
66      */
67     private static final int MSG_GET_TRANSFERRED_DOWNLOAD_BYTES = 3;
68     /**
69      * Message that will result in a call to
70      * {@link #getTransferredUploadBytes(JobParameters, JobWorkItem)}.
71      */
72     private static final int MSG_GET_TRANSFERRED_UPLOAD_BYTES = 4;
73     /** Message that the client wants to update JobScheduler of the data transfer progress. */
74     private static final int MSG_UPDATE_TRANSFERRED_NETWORK_BYTES = 5;
75     /** Message that the client wants to update JobScheduler of the estimated transfer size. */
76     private static final int MSG_UPDATE_ESTIMATED_NETWORK_BYTES = 6;
77     /** Message that the client wants to give JobScheduler a notification to tie to the job. */
78     private static final int MSG_SET_NOTIFICATION = 7;
79     /** Message that the network to use has changed. */
80     private static final int MSG_INFORM_OF_NETWORK_CHANGE = 8;
81 
82     private final IJobService mBinder;
83 
84     /**
85      * Handler we post jobs to. Responsible for calling into the client logic, and handling the
86      * callback to the system.
87      */
88     JobHandler mHandler;
89 
90     static final class JobInterface extends IJobService.Stub {
91         final WeakReference<JobServiceEngine> mService;
92 
JobInterface(JobServiceEngine service)93         JobInterface(JobServiceEngine service) {
94             mService = new WeakReference<>(service);
95         }
96 
97         @Override
getTransferredDownloadBytes(@onNull JobParameters jobParams, @Nullable JobWorkItem jobWorkItem)98         public void getTransferredDownloadBytes(@NonNull JobParameters jobParams,
99                 @Nullable JobWorkItem jobWorkItem) throws RemoteException {
100             JobServiceEngine service = mService.get();
101             if (service != null) {
102                 SomeArgs args = SomeArgs.obtain();
103                 args.arg1 = jobParams;
104                 args.arg2 = jobWorkItem;
105                 service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_DOWNLOAD_BYTES, args)
106                         .sendToTarget();
107             }
108         }
109 
110         @Override
getTransferredUploadBytes(@onNull JobParameters jobParams, @Nullable JobWorkItem jobWorkItem)111         public void getTransferredUploadBytes(@NonNull JobParameters jobParams,
112                 @Nullable JobWorkItem jobWorkItem) throws RemoteException {
113             JobServiceEngine service = mService.get();
114             if (service != null) {
115                 SomeArgs args = SomeArgs.obtain();
116                 args.arg1 = jobParams;
117                 args.arg2 = jobWorkItem;
118                 service.mHandler.obtainMessage(MSG_GET_TRANSFERRED_UPLOAD_BYTES, args)
119                         .sendToTarget();
120             }
121         }
122 
123         @Override
startJob(JobParameters jobParams)124         public void startJob(JobParameters jobParams) throws RemoteException {
125             JobServiceEngine service = mService.get();
126             if (service != null) {
127                 Message m = Message.obtain(service.mHandler, MSG_EXECUTE_JOB, jobParams);
128                 m.sendToTarget();
129             }
130         }
131 
132         @Override
onNetworkChanged(JobParameters jobParams)133         public void onNetworkChanged(JobParameters jobParams) throws RemoteException {
134             JobServiceEngine service = mService.get();
135             if (service != null) {
136                 service.mHandler.removeMessages(MSG_INFORM_OF_NETWORK_CHANGE);
137                 service.mHandler.obtainMessage(MSG_INFORM_OF_NETWORK_CHANGE, jobParams)
138                         .sendToTarget();
139             }
140         }
141 
142         @Override
stopJob(JobParameters jobParams)143         public void stopJob(JobParameters jobParams) throws RemoteException {
144             JobServiceEngine service = mService.get();
145             if (service != null) {
146                 Message m = Message.obtain(service.mHandler, MSG_STOP_JOB, jobParams);
147                 m.sendToTarget();
148             }
149         }
150     }
151 
152     /**
153      * Runs on application's main thread - callbacks are meant to offboard work to some other
154      * (app-specified) mechanism.
155      * @hide
156      */
157     class JobHandler extends Handler {
JobHandler(Looper looper)158         JobHandler(Looper looper) {
159             super(looper);
160         }
161 
162         @Override
handleMessage(Message msg)163         public void handleMessage(Message msg) {
164             switch (msg.what) {
165                 case MSG_EXECUTE_JOB: {
166                     final JobParameters params = (JobParameters) msg.obj;
167                     try {
168                         params.enableCleaner();
169                         boolean workOngoing = JobServiceEngine.this.onStartJob(params);
170                         if (!workOngoing) {
171                             params.disableCleaner();
172                         }
173                         ackStartMessage(params, workOngoing);
174                     } catch (Exception e) {
175                         Log.e(TAG, "Error while executing job: " + params.getJobId());
176                         throw new RuntimeException(e);
177                     }
178                     break;
179                 }
180                 case MSG_STOP_JOB: {
181                     final JobParameters params = (JobParameters) msg.obj;
182                     try {
183                         boolean ret = JobServiceEngine.this.onStopJob(params);
184                         ackStopMessage(params, ret);
185                     } catch (Exception e) {
186                         Log.e(TAG, "Application unable to handle onStopJob.", e);
187                         throw new RuntimeException(e);
188                     }
189                     break;
190                 }
191                 case MSG_JOB_FINISHED: {
192                     final JobParameters params = (JobParameters) msg.obj;
193                     final boolean needsReschedule = (msg.arg2 == 1);
194                     IJobCallback callback = params.getCallback();
195                     if (callback != null) {
196                         try {
197                             params.disableCleaner();
198                             callback.jobFinished(params.getJobId(), needsReschedule);
199                         } catch (RemoteException e) {
200                             Log.e(TAG, "Error reporting job finish to system: binder has gone" +
201                                     "away.");
202                         }
203                     } else {
204                         Log.e(TAG, "finishJob() called for a nonexistent job id.");
205                     }
206                     break;
207                 }
208                 case MSG_GET_TRANSFERRED_DOWNLOAD_BYTES: {
209                     final SomeArgs args = (SomeArgs) msg.obj;
210                     final JobParameters params = (JobParameters) args.arg1;
211                     final JobWorkItem item = (JobWorkItem) args.arg2;
212                     try {
213                         long ret = JobServiceEngine.this.getTransferredDownloadBytes(params, item);
214                         ackGetTransferredDownloadBytesMessage(params, item, ret);
215                     } catch (Exception e) {
216                         Log.e(TAG, "Application unable to handle getTransferredDownloadBytes.", e);
217                         throw new RuntimeException(e);
218                     }
219                     args.recycle();
220                     break;
221                 }
222                 case MSG_GET_TRANSFERRED_UPLOAD_BYTES: {
223                     final SomeArgs args = (SomeArgs) msg.obj;
224                     final JobParameters params = (JobParameters) args.arg1;
225                     final JobWorkItem item = (JobWorkItem) args.arg2;
226                     try {
227                         long ret = JobServiceEngine.this.getTransferredUploadBytes(params, item);
228                         ackGetTransferredUploadBytesMessage(params, item, ret);
229                     } catch (Exception e) {
230                         Log.e(TAG, "Application unable to handle getTransferredUploadBytes.", e);
231                         throw new RuntimeException(e);
232                     }
233                     args.recycle();
234                     break;
235                 }
236                 case MSG_UPDATE_TRANSFERRED_NETWORK_BYTES: {
237                     final SomeArgs args = (SomeArgs) msg.obj;
238                     final JobParameters params = (JobParameters) args.arg1;
239                     IJobCallback callback = params.getCallback();
240                     if (callback != null) {
241                         try {
242                             callback.updateTransferredNetworkBytes(params.getJobId(),
243                                     (JobWorkItem) args.arg2, args.argl1, args.argl2);
244                         } catch (RemoteException e) {
245                             Log.e(TAG, "Error updating data transfer progress to system:"
246                                     + " binder has gone away.");
247                         }
248                     } else {
249                         Log.e(TAG, "updateDataTransferProgress() called for a nonexistent job id.");
250                     }
251                     args.recycle();
252                     break;
253                 }
254                 case MSG_UPDATE_ESTIMATED_NETWORK_BYTES: {
255                     final SomeArgs args = (SomeArgs) msg.obj;
256                     final JobParameters params = (JobParameters) args.arg1;
257                     IJobCallback callback = params.getCallback();
258                     if (callback != null) {
259                         try {
260                             callback.updateEstimatedNetworkBytes(params.getJobId(),
261                                     (JobWorkItem) args.arg2, args.argl1, args.argl2);
262                         } catch (RemoteException e) {
263                             Log.e(TAG, "Error updating estimated transfer size to system:"
264                                     + " binder has gone away.");
265                         }
266                     } else {
267                         Log.e(TAG,
268                                 "updateEstimatedNetworkBytes() called for a nonexistent job id.");
269                     }
270                     args.recycle();
271                     break;
272                 }
273                 case MSG_SET_NOTIFICATION: {
274                     final SomeArgs args = (SomeArgs) msg.obj;
275                     final JobParameters params = (JobParameters) args.arg1;
276                     final Notification notification = (Notification) args.arg2;
277                     IJobCallback callback = params.getCallback();
278                     if (callback != null) {
279                         try {
280                             callback.setNotification(params.getJobId(),
281                                     args.argi1, notification, args.argi2);
282                         } catch (RemoteException e) {
283                             Log.e(TAG, "Error providing notification: binder has gone away.");
284                         }
285                     } else {
286                         Log.e(TAG, "setNotification() called for a nonexistent job.");
287                     }
288                     args.recycle();
289                     break;
290                 }
291                 case MSG_INFORM_OF_NETWORK_CHANGE: {
292                     final JobParameters params = (JobParameters) msg.obj;
293                     try {
294                         JobServiceEngine.this.onNetworkChanged(params);
295                     } catch (Exception e) {
296                         Log.e(TAG, "Error while executing job: " + params.getJobId());
297                         throw new RuntimeException(e);
298                     }
299                     break;
300                 }
301                 default:
302                     Log.e(TAG, "Unrecognised message received.");
303                     break;
304             }
305         }
306 
ackGetTransferredDownloadBytesMessage(@onNull JobParameters params, @Nullable JobWorkItem item, long progress)307         private void ackGetTransferredDownloadBytesMessage(@NonNull JobParameters params,
308                 @Nullable JobWorkItem item, long progress) {
309             final IJobCallback callback = params.getCallback();
310             final int jobId = params.getJobId();
311             final int workId = item == null ? -1 : item.getWorkId();
312             if (callback != null) {
313                 try {
314                     callback.acknowledgeGetTransferredDownloadBytesMessage(jobId, workId, progress);
315                 } catch (RemoteException e) {
316                     Log.e(TAG, "System unreachable for returning progress.");
317                 }
318             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
319                 Log.d(TAG, "Attempting to ack a job that has already been processed.");
320             }
321         }
322 
ackGetTransferredUploadBytesMessage(@onNull JobParameters params, @Nullable JobWorkItem item, long progress)323         private void ackGetTransferredUploadBytesMessage(@NonNull JobParameters params,
324                 @Nullable JobWorkItem item, long progress) {
325             final IJobCallback callback = params.getCallback();
326             final int jobId = params.getJobId();
327             final int workId = item == null ? -1 : item.getWorkId();
328             if (callback != null) {
329                 try {
330                     callback.acknowledgeGetTransferredUploadBytesMessage(jobId, workId, progress);
331                 } catch (RemoteException e) {
332                     Log.e(TAG, "System unreachable for returning progress.");
333                 }
334             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
335                 Log.d(TAG, "Attempting to ack a job that has already been processed.");
336             }
337         }
338 
ackStartMessage(JobParameters params, boolean workOngoing)339         private void ackStartMessage(JobParameters params, boolean workOngoing) {
340             final IJobCallback callback = params.getCallback();
341             final int jobId = params.getJobId();
342             if (callback != null) {
343                 try {
344                     callback.acknowledgeStartMessage(jobId, workOngoing);
345                 } catch (RemoteException e) {
346                     Log.e(TAG, "System unreachable for starting job.");
347                 }
348             } else {
349                 if (Log.isLoggable(TAG, Log.DEBUG)) {
350                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
351                 }
352             }
353         }
354 
ackStopMessage(JobParameters params, boolean reschedule)355         private void ackStopMessage(JobParameters params, boolean reschedule) {
356             final IJobCallback callback = params.getCallback();
357             final int jobId = params.getJobId();
358             if (callback != null) {
359                 try {
360                     callback.acknowledgeStopMessage(jobId, reschedule);
361                 } catch(RemoteException e) {
362                     Log.e(TAG, "System unreachable for stopping job.");
363                 }
364             } else {
365                 if (Log.isLoggable(TAG, Log.DEBUG)) {
366                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
367                 }
368             }
369         }
370     }
371 
372     /**
373      * Create a new engine, ready for use.
374      *
375      * @param service The {@link Service} that is creating this engine and in which it will run.
376      */
JobServiceEngine(Service service)377     public JobServiceEngine(Service service) {
378         mBinder = new JobInterface(this);
379         mHandler = new JobHandler(service.getMainLooper());
380     }
381 
382     /**
383      * Retrieve the engine's IPC interface that should be returned by
384      * {@link Service#onBind(Intent)}.
385      */
getBinder()386     public final IBinder getBinder() {
387         return mBinder.asBinder();
388     }
389 
390     /**
391      * Engine's report that a job has started.  See
392      * {@link JobService#onStartJob(JobParameters) JobService.onStartJob} for more information.
393      */
onStartJob(JobParameters params)394     public abstract boolean onStartJob(JobParameters params);
395 
396     /**
397      * Engine's report that a job has stopped.  See
398      * {@link JobService#onStopJob(JobParameters) JobService.onStopJob} for more information.
399      */
onStopJob(JobParameters params)400     public abstract boolean onStopJob(JobParameters params);
401 
402     /**
403      * Call in to engine to report that a job has finished executing.  See
404      * {@link JobService#jobFinished(JobParameters, boolean)} for more information.
405      */
jobFinished(JobParameters params, boolean needsReschedule)406     public void jobFinished(JobParameters params, boolean needsReschedule) {
407         if (params == null) {
408             throw new NullPointerException("params");
409         }
410         Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
411         m.arg2 = needsReschedule ? 1 : 0;
412         m.sendToTarget();
413     }
414 
415     /**
416      * Engine's report that the network for the job has changed.
417      *
418      * @see JobService#onNetworkChanged(JobParameters)
419      */
onNetworkChanged(@onNull JobParameters params)420     public void onNetworkChanged(@NonNull JobParameters params) {
421         Log.w(TAG, "onNetworkChanged() not implemented. Must override in a subclass.");
422     }
423 
424     /**
425      * Engine's request to get how much data has been downloaded.
426      *
427      * @hide
428      * @see JobService#getTransferredDownloadBytes()
429      */
430     @BytesLong
getTransferredDownloadBytes(@onNull JobParameters params, @Nullable JobWorkItem item)431     public long getTransferredDownloadBytes(@NonNull JobParameters params,
432             @Nullable JobWorkItem item) {
433         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
434             throw new RuntimeException("Not implemented. Must override in a subclass.");
435         }
436         return 0;
437     }
438 
439     /**
440      * Engine's request to get how much data has been uploaded.
441      *
442      * @hide
443      * @see JobService#getTransferredUploadBytes()
444      */
445     @BytesLong
getTransferredUploadBytes(@onNull JobParameters params, @Nullable JobWorkItem item)446     public long getTransferredUploadBytes(@NonNull JobParameters params,
447             @Nullable JobWorkItem item) {
448         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
449             throw new RuntimeException("Not implemented. Must override in a subclass.");
450         }
451         return 0;
452     }
453 
454     /**
455      * Call in to engine to report data transfer progress.
456      *
457      * @see JobService#updateTransferredNetworkBytes(JobParameters, long, long)
458      * @see JobService#updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)
459      */
updateTransferredNetworkBytes(@onNull JobParameters params, @Nullable JobWorkItem item, @BytesLong long downloadBytes, @BytesLong long uploadBytes)460     public void updateTransferredNetworkBytes(@NonNull JobParameters params,
461             @Nullable JobWorkItem item,
462             @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
463         if (params == null) {
464             throw new NullPointerException("params");
465         }
466         SomeArgs args = SomeArgs.obtain();
467         args.arg1 = params;
468         args.arg2 = item;
469         args.argl1 = downloadBytes;
470         args.argl2 = uploadBytes;
471         mHandler.obtainMessage(MSG_UPDATE_TRANSFERRED_NETWORK_BYTES, args).sendToTarget();
472     }
473 
474     /**
475      * Call in to engine to report data transfer progress.
476      *
477      * @see JobService#updateEstimatedNetworkBytes(JobParameters, long, long)
478      * @see JobService#updateEstimatedNetworkBytes(JobParameters, JobWorkItem, long, long)
479      */
updateEstimatedNetworkBytes(@onNull JobParameters params, @Nullable JobWorkItem item, @BytesLong long downloadBytes, @BytesLong long uploadBytes)480     public void updateEstimatedNetworkBytes(@NonNull JobParameters params,
481             @Nullable JobWorkItem item,
482             @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
483         if (params == null) {
484             throw new NullPointerException("params");
485         }
486         SomeArgs args = SomeArgs.obtain();
487         args.arg1 = params;
488         args.arg2 = item;
489         args.argl1 = downloadBytes;
490         args.argl2 = uploadBytes;
491         mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
492     }
493 
494     /**
495      * Give JobScheduler a notification to tie to this job's lifecycle.
496      *
497      * @see JobService#setNotification(JobParameters, int, Notification, int)
498      */
setNotification(@onNull JobParameters params, int notificationId, @NonNull Notification notification, @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy)499     public void setNotification(@NonNull JobParameters params, int notificationId,
500             @NonNull Notification notification,
501             @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
502         if (params == null) {
503             throw new NullPointerException("params");
504         }
505         if (notification == null) {
506             throw new NullPointerException("notification");
507         }
508         SomeArgs args = SomeArgs.obtain();
509         args.arg1 = params;
510         args.arg2 = notification;
511         args.argi1 = notificationId;
512         args.argi2 = jobEndNotificationPolicy;
513         mHandler.obtainMessage(MSG_SET_NOTIFICATION, args).sendToTarget();
514     }
515 }
516