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