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