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                         boolean workOngoing = JobServiceEngine.this.onStartJob(params);
169                         ackStartMessage(params, workOngoing);
170                     } catch (Exception e) {
171                         Log.e(TAG, "Error while executing job: " + params.getJobId());
172                         throw new RuntimeException(e);
173                     }
174                     break;
175                 }
176                 case MSG_STOP_JOB: {
177                     final JobParameters params = (JobParameters) msg.obj;
178                     try {
179                         boolean ret = JobServiceEngine.this.onStopJob(params);
180                         ackStopMessage(params, ret);
181                     } catch (Exception e) {
182                         Log.e(TAG, "Application unable to handle onStopJob.", e);
183                         throw new RuntimeException(e);
184                     }
185                     break;
186                 }
187                 case MSG_JOB_FINISHED: {
188                     final JobParameters params = (JobParameters) msg.obj;
189                     final boolean needsReschedule = (msg.arg2 == 1);
190                     IJobCallback callback = params.getCallback();
191                     if (callback != null) {
192                         try {
193                             callback.jobFinished(params.getJobId(), needsReschedule);
194                         } catch (RemoteException e) {
195                             Log.e(TAG, "Error reporting job finish to system: binder has gone" +
196                                     "away.");
197                         }
198                     } else {
199                         Log.e(TAG, "finishJob() called for a nonexistent job id.");
200                     }
201                     break;
202                 }
203                 case MSG_GET_TRANSFERRED_DOWNLOAD_BYTES: {
204                     final SomeArgs args = (SomeArgs) msg.obj;
205                     final JobParameters params = (JobParameters) args.arg1;
206                     final JobWorkItem item = (JobWorkItem) args.arg2;
207                     try {
208                         long ret = JobServiceEngine.this.getTransferredDownloadBytes(params, item);
209                         ackGetTransferredDownloadBytesMessage(params, item, ret);
210                     } catch (Exception e) {
211                         Log.e(TAG, "Application unable to handle getTransferredDownloadBytes.", e);
212                         throw new RuntimeException(e);
213                     }
214                     args.recycle();
215                     break;
216                 }
217                 case MSG_GET_TRANSFERRED_UPLOAD_BYTES: {
218                     final SomeArgs args = (SomeArgs) msg.obj;
219                     final JobParameters params = (JobParameters) args.arg1;
220                     final JobWorkItem item = (JobWorkItem) args.arg2;
221                     try {
222                         long ret = JobServiceEngine.this.getTransferredUploadBytes(params, item);
223                         ackGetTransferredUploadBytesMessage(params, item, ret);
224                     } catch (Exception e) {
225                         Log.e(TAG, "Application unable to handle getTransferredUploadBytes.", e);
226                         throw new RuntimeException(e);
227                     }
228                     args.recycle();
229                     break;
230                 }
231                 case MSG_UPDATE_TRANSFERRED_NETWORK_BYTES: {
232                     final SomeArgs args = (SomeArgs) msg.obj;
233                     final JobParameters params = (JobParameters) args.arg1;
234                     IJobCallback callback = params.getCallback();
235                     if (callback != null) {
236                         try {
237                             callback.updateTransferredNetworkBytes(params.getJobId(),
238                                     (JobWorkItem) args.arg2, args.argl1, args.argl2);
239                         } catch (RemoteException e) {
240                             Log.e(TAG, "Error updating data transfer progress to system:"
241                                     + " binder has gone away.");
242                         }
243                     } else {
244                         Log.e(TAG, "updateDataTransferProgress() called for a nonexistent job id.");
245                     }
246                     args.recycle();
247                     break;
248                 }
249                 case MSG_UPDATE_ESTIMATED_NETWORK_BYTES: {
250                     final SomeArgs args = (SomeArgs) msg.obj;
251                     final JobParameters params = (JobParameters) args.arg1;
252                     IJobCallback callback = params.getCallback();
253                     if (callback != null) {
254                         try {
255                             callback.updateEstimatedNetworkBytes(params.getJobId(),
256                                     (JobWorkItem) args.arg2, args.argl1, args.argl2);
257                         } catch (RemoteException e) {
258                             Log.e(TAG, "Error updating estimated transfer size to system:"
259                                     + " binder has gone away.");
260                         }
261                     } else {
262                         Log.e(TAG,
263                                 "updateEstimatedNetworkBytes() called for a nonexistent job id.");
264                     }
265                     args.recycle();
266                     break;
267                 }
268                 case MSG_SET_NOTIFICATION: {
269                     final SomeArgs args = (SomeArgs) msg.obj;
270                     final JobParameters params = (JobParameters) args.arg1;
271                     final Notification notification = (Notification) args.arg2;
272                     IJobCallback callback = params.getCallback();
273                     if (callback != null) {
274                         try {
275                             callback.setNotification(params.getJobId(),
276                                     args.argi1, notification, args.argi2);
277                         } catch (RemoteException e) {
278                             Log.e(TAG, "Error providing notification: binder has gone away.");
279                         }
280                     } else {
281                         Log.e(TAG, "setNotification() called for a nonexistent job.");
282                     }
283                     args.recycle();
284                     break;
285                 }
286                 case MSG_INFORM_OF_NETWORK_CHANGE: {
287                     final JobParameters params = (JobParameters) msg.obj;
288                     try {
289                         JobServiceEngine.this.onNetworkChanged(params);
290                     } catch (Exception e) {
291                         Log.e(TAG, "Error while executing job: " + params.getJobId());
292                         throw new RuntimeException(e);
293                     }
294                     break;
295                 }
296                 default:
297                     Log.e(TAG, "Unrecognised message received.");
298                     break;
299             }
300         }
301 
ackGetTransferredDownloadBytesMessage(@onNull JobParameters params, @Nullable JobWorkItem item, long progress)302         private void ackGetTransferredDownloadBytesMessage(@NonNull JobParameters params,
303                 @Nullable JobWorkItem item, long progress) {
304             final IJobCallback callback = params.getCallback();
305             final int jobId = params.getJobId();
306             final int workId = item == null ? -1 : item.getWorkId();
307             if (callback != null) {
308                 try {
309                     callback.acknowledgeGetTransferredDownloadBytesMessage(jobId, workId, progress);
310                 } catch (RemoteException e) {
311                     Log.e(TAG, "System unreachable for returning progress.");
312                 }
313             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
314                 Log.d(TAG, "Attempting to ack a job that has already been processed.");
315             }
316         }
317 
ackGetTransferredUploadBytesMessage(@onNull JobParameters params, @Nullable JobWorkItem item, long progress)318         private void ackGetTransferredUploadBytesMessage(@NonNull JobParameters params,
319                 @Nullable JobWorkItem item, long progress) {
320             final IJobCallback callback = params.getCallback();
321             final int jobId = params.getJobId();
322             final int workId = item == null ? -1 : item.getWorkId();
323             if (callback != null) {
324                 try {
325                     callback.acknowledgeGetTransferredUploadBytesMessage(jobId, workId, progress);
326                 } catch (RemoteException e) {
327                     Log.e(TAG, "System unreachable for returning progress.");
328                 }
329             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
330                 Log.d(TAG, "Attempting to ack a job that has already been processed.");
331             }
332         }
333 
ackStartMessage(JobParameters params, boolean workOngoing)334         private void ackStartMessage(JobParameters params, boolean workOngoing) {
335             final IJobCallback callback = params.getCallback();
336             final int jobId = params.getJobId();
337             if (callback != null) {
338                 try {
339                     callback.acknowledgeStartMessage(jobId, workOngoing);
340                 } catch (RemoteException e) {
341                     Log.e(TAG, "System unreachable for starting job.");
342                 }
343             } else {
344                 if (Log.isLoggable(TAG, Log.DEBUG)) {
345                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
346                 }
347             }
348         }
349 
ackStopMessage(JobParameters params, boolean reschedule)350         private void ackStopMessage(JobParameters params, boolean reschedule) {
351             final IJobCallback callback = params.getCallback();
352             final int jobId = params.getJobId();
353             if (callback != null) {
354                 try {
355                     callback.acknowledgeStopMessage(jobId, reschedule);
356                 } catch(RemoteException e) {
357                     Log.e(TAG, "System unreachable for stopping job.");
358                 }
359             } else {
360                 if (Log.isLoggable(TAG, Log.DEBUG)) {
361                     Log.d(TAG, "Attempting to ack a job that has already been processed.");
362                 }
363             }
364         }
365     }
366 
367     /**
368      * Create a new engine, ready for use.
369      *
370      * @param service The {@link Service} that is creating this engine and in which it will run.
371      */
JobServiceEngine(Service service)372     public JobServiceEngine(Service service) {
373         mBinder = new JobInterface(this);
374         mHandler = new JobHandler(service.getMainLooper());
375     }
376 
377     /**
378      * Retrieve the engine's IPC interface that should be returned by
379      * {@link Service#onBind(Intent)}.
380      */
getBinder()381     public final IBinder getBinder() {
382         return mBinder.asBinder();
383     }
384 
385     /**
386      * Engine's report that a job has started.  See
387      * {@link JobService#onStartJob(JobParameters) JobService.onStartJob} for more information.
388      */
onStartJob(JobParameters params)389     public abstract boolean onStartJob(JobParameters params);
390 
391     /**
392      * Engine's report that a job has stopped.  See
393      * {@link JobService#onStopJob(JobParameters) JobService.onStopJob} for more information.
394      */
onStopJob(JobParameters params)395     public abstract boolean onStopJob(JobParameters params);
396 
397     /**
398      * Call in to engine to report that a job has finished executing.  See
399      * {@link JobService#jobFinished(JobParameters, boolean)} for more information.
400      */
jobFinished(JobParameters params, boolean needsReschedule)401     public void jobFinished(JobParameters params, boolean needsReschedule) {
402         if (params == null) {
403             throw new NullPointerException("params");
404         }
405         Message m = Message.obtain(mHandler, MSG_JOB_FINISHED, params);
406         m.arg2 = needsReschedule ? 1 : 0;
407         m.sendToTarget();
408     }
409 
410     /**
411      * Engine's report that the network for the job has changed.
412      *
413      * @see JobService#onNetworkChanged(JobParameters)
414      */
onNetworkChanged(@onNull JobParameters params)415     public void onNetworkChanged(@NonNull JobParameters params) {
416         Log.w(TAG, "onNetworkChanged() not implemented. Must override in a subclass.");
417     }
418 
419     /**
420      * Engine's request to get how much data has been downloaded.
421      *
422      * @hide
423      * @see JobService#getTransferredDownloadBytes()
424      */
425     @BytesLong
getTransferredDownloadBytes(@onNull JobParameters params, @Nullable JobWorkItem item)426     public long getTransferredDownloadBytes(@NonNull JobParameters params,
427             @Nullable JobWorkItem item) {
428         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
429             throw new RuntimeException("Not implemented. Must override in a subclass.");
430         }
431         return 0;
432     }
433 
434     /**
435      * Engine's request to get how much data has been uploaded.
436      *
437      * @hide
438      * @see JobService#getTransferredUploadBytes()
439      */
440     @BytesLong
getTransferredUploadBytes(@onNull JobParameters params, @Nullable JobWorkItem item)441     public long getTransferredUploadBytes(@NonNull JobParameters params,
442             @Nullable JobWorkItem item) {
443         if (Compatibility.isChangeEnabled(THROW_ON_INVALID_DATA_TRANSFER_IMPLEMENTATION)) {
444             throw new RuntimeException("Not implemented. Must override in a subclass.");
445         }
446         return 0;
447     }
448 
449     /**
450      * Call in to engine to report data transfer progress.
451      *
452      * @see JobService#updateTransferredNetworkBytes(JobParameters, long, long)
453      * @see JobService#updateTransferredNetworkBytes(JobParameters, JobWorkItem, long, long)
454      */
updateTransferredNetworkBytes(@onNull JobParameters params, @Nullable JobWorkItem item, @BytesLong long downloadBytes, @BytesLong long uploadBytes)455     public void updateTransferredNetworkBytes(@NonNull JobParameters params,
456             @Nullable JobWorkItem item,
457             @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
458         if (params == null) {
459             throw new NullPointerException("params");
460         }
461         SomeArgs args = SomeArgs.obtain();
462         args.arg1 = params;
463         args.arg2 = item;
464         args.argl1 = downloadBytes;
465         args.argl2 = uploadBytes;
466         mHandler.obtainMessage(MSG_UPDATE_TRANSFERRED_NETWORK_BYTES, args).sendToTarget();
467     }
468 
469     /**
470      * Call in to engine to report data transfer progress.
471      *
472      * @see JobService#updateEstimatedNetworkBytes(JobParameters, long, long)
473      * @see JobService#updateEstimatedNetworkBytes(JobParameters, JobWorkItem, long, long)
474      */
updateEstimatedNetworkBytes(@onNull JobParameters params, @Nullable JobWorkItem item, @BytesLong long downloadBytes, @BytesLong long uploadBytes)475     public void updateEstimatedNetworkBytes(@NonNull JobParameters params,
476             @Nullable JobWorkItem item,
477             @BytesLong long downloadBytes, @BytesLong long uploadBytes) {
478         if (params == null) {
479             throw new NullPointerException("params");
480         }
481         SomeArgs args = SomeArgs.obtain();
482         args.arg1 = params;
483         args.arg2 = item;
484         args.argl1 = downloadBytes;
485         args.argl2 = uploadBytes;
486         mHandler.obtainMessage(MSG_UPDATE_ESTIMATED_NETWORK_BYTES, args).sendToTarget();
487     }
488 
489     /**
490      * Give JobScheduler a notification to tie to this job's lifecycle.
491      *
492      * @see JobService#setNotification(JobParameters, int, Notification, int)
493      */
setNotification(@onNull JobParameters params, int notificationId, @NonNull Notification notification, @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy)494     public void setNotification(@NonNull JobParameters params, int notificationId,
495             @NonNull Notification notification,
496             @JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
497         if (params == null) {
498             throw new NullPointerException("params");
499         }
500         if (notification == null) {
501             throw new NullPointerException("notification");
502         }
503         SomeArgs args = SomeArgs.obtain();
504         args.arg1 = params;
505         args.arg2 = notification;
506         args.argi1 = notificationId;
507         args.argi2 = jobEndNotificationPolicy;
508         mHandler.obtainMessage(MSG_SET_NOTIFICATION, args).sendToTarget();
509     }
510 }
511