Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Doo z z z z z e

Ralf
September 04, 2017

Doo z z z z z e

With Android Marshmallow Google introduced two new power-saving modes: Doze and App Standby. True to their intent, both provide longer battery life and better performance. Their effectiveness further improved in Android Nougat. However, these modes carry some limitations developers should be aware of. To further improve power-saving Android O will be even more restrictive and disable common implicit broadcasts. Many apps rely on implicit broadcasts so if you're using those, supporting older and newer devices at the same time is going to be more challenging.

This talk will explain why these new APIs are better. We will discuss the given tools and dive into the new APIs. Finally, we will explore effective solutions to avoid duplicating code over and over again for different SDK versions.

Ralf

September 04, 2017
Tweet

More Decks by Ralf

Other Decks in Programming

Transcript

  1. Motivation • Every app has some kind of repeated work

    • Don’t waste battery unnecessarily • App should still work correctly even in Doze mode or in App Standby • App should still work on Android O • Scheduling repeated work is quite a headache with 3 different APIs all doing similar things
  2. Doze & App Standby • Doze → Long idle battery

    life • Doze Lite (with Android N) → Screen off battery life • App Standby → Idle apps when the device is unplugged https://www.youtube.com/watch?v=hbLAzwhBjFE
  3. Doze & App Standby • Special cases to interrupt Doze

    ◦ High priority GCM / FCM messages ◦ Notification interaction ◦ SMS ◦ Special APIs ◦ Whitelist app https://www.youtube.com/watch?v=hbLAzwhBjFE
  4. Android O • Background service limitations ◦ Use foreground services

    ◦ Android O stops background services for apps in the background ◦ Can’t start background services from an app in background • Broadcast removal ◦ Whitelist ◦ android.net.conn.CONNECTIVITY_CHANGE https://developer.android.com/preview/features/background.html
  5. Android O - Custom Intents <action android:name="com.evernote.action.CREATE_NEW_NOTE" /> <permission android:name="com.evernote.android.permission.APP_EVENT"

    android:label="@string/app_name" android:description="@string/description" android:protectionLevel="signature" /> <uses-permission android:name="com.evernote.android.permission.APP_EVENT"/>
  6. Android O - Custom Intents <action android:name="com.evernote.action.CREATE_NEW_NOTE" /> <permission android:name="com.evernote.android.permission.APP_EVENT"

    android:label="@string/app_name" android:description="@string/description" android:protectionLevel="signature" /> <uses-permission android:name="com.evernote.android.permission.APP_EVENT"/> fun Context.sendEvernoteBroadcast(intent: Intent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sendBroadcast(intent, EvernoteContract.PERMISSION) } else { sendBroadcast(intent) } }
  7. Requirements • Show the reminder in a notification • Sync

    reminders with the server • Support all devices • Be a good citizen
  8. Reminder job • Must be exact • Can have multiple

    at the same time • AlarmManager is the correct API
  9. AlarmManager + Available on all devices + Easy to send

    broadcast to start a service delayed - API behavior differs between platform versions - A lot of boilerplate - Device state ignored - …
  10. Reminder job <receiver android:name=".reminder.ReminderReceiver" android:exported="false"/> public class ReminderReceiver extends BroadcastReceiver

    { private static final String EXTRA_ID = "EXTRA_ID"; @Override public void onReceive(Context context, Intent intent) { int id = intent.getIntExtra(EXTRA_ID, -1); if (id < 0) { return; } Reminder reminder = ReminderEngine.instance().getReminderById(id); if (reminder != null) { ReminderEngine.instance().showReminder(reminder); } } }
  11. Reminder job <receiver android:name=".reminder.ReminderReceiver" android:exported="false"/> public class ReminderReceiver extends BroadcastReceiver

    { private static final String EXTRA_ID = "EXTRA_ID"; @Override public void onReceive(Context context, Intent intent) { int id = intent.getIntExtra(EXTRA_ID, -1); if (id < 0) { return; } Reminder reminder = ReminderEngine.instance().getReminderById(id); if (reminder != null) { ReminderEngine.instance().showReminder(reminder); } } }
  12. API 14-18 public static void schedule(Context context, Reminder reminder) {

    Intent intent = new Intent(context, ReminderReceiver.class); intent.putExtra(EXTRA_ID, reminder.getId()); int requestCode = reminder.getId(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; PendingIntent pendingIntent = PendingIntent .getBroadcast(context, requestCode, intent, flags); AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC_WAKEUP, reminder.getTimestamp(), pendingIntent); }
  13. API 14-18 public static void schedule(Context context, Reminder reminder) {

    Intent intent = new Intent(context, ReminderReceiver.class); intent.putExtra(EXTRA_ID, reminder.getId()); int requestCode = reminder.getId(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; PendingIntent pendingIntent = PendingIntent .getBroadcast(context, requestCode, intent, flags); AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC_WAKEUP, reminder.getTimestamp(), pendingIntent); }
  14. API 19-22 public static void schedule(Context context, Reminder reminder) {

    Intent intent = new Intent(context, ReminderReceiver.class); intent.putExtra(EXTRA_ID, reminder.getId()); int requestCode = reminder.getId(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; PendingIntent pendingIntent = PendingIntent .getBroadcast(context, requestCode, intent, flags); AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.set(AlarmManager.RTC_WAKEUP, reminder.getTimestamp(), pendingIntent); }
  15. API 19-22 public static void schedule(Context context, Reminder reminder) {

    Intent intent = new Intent(context, ReminderReceiver.class); intent.putExtra(EXTRA_ID, reminder.getId()); int requestCode = reminder.getId(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; PendingIntent pendingIntent = PendingIntent .getBroadcast(context, requestCode, intent, flags); AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.setExact(AlarmManager.RTC_WAKEUP, reminder.getTimestamp(), pendingIntent); }
  16. API 23-26 public static void schedule(Context context, Reminder reminder) {

    Intent intent = new Intent(context, ReminderReceiver.class); intent.putExtra(EXTRA_ID, reminder.getId()); int requestCode = reminder.getId(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; PendingIntent pendingIntent = PendingIntent .getBroadcast(context, requestCode, intent, flags); AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.setExact(AlarmManager.RTC_WAKEUP, reminder.getTimestamp(), pendingIntent); }
  17. API 23-26 public static void schedule(Context context, Reminder reminder) {

    Intent intent = new Intent(context, ReminderReceiver.class); intent.putExtra(EXTRA_ID, reminder.getId()); int requestCode = reminder.getId(); int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT; PendingIntent pendingIntent = PendingIntent .getBroadcast(context, requestCode, intent, flags); AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, reminder.getTimestamp(), pendingIntent); }
  18. Reminder job public static void schedule(Context context, Reminder reminder) {

    // … if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, when, pendingIntent); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { manager.setExact(AlarmManager.RTC_WAKEUP, when, pendingIntent); } else { manager.set(AlarmManager.RTC_WAKEUP, when, pendingIntent); } }
  19. Sync job • Can be inexact, but must be repeating

    • Run job only on an unmetered network and only if device is charging • Don’t wake up the device to sync data • JobScheduler and GCM Network Manager work best, AlarmManager is fallback
  20. JobScheduler + Easy to use with fluent API + Respects

    device state - Only on API 21+ (some features only API 24+ or 26+) - Platform bugs - Still a lot of boilerplate code
  21. public class SyncJob extends JobService { @Override public boolean onStartJob(JobParameters

    params) { // called on main thread new Thread(() -> { try { new SyncEngine().syncReminders(); } catch (IOException e) { e.printStackTrace(); } finally { jobFinished(params, false); // don't forget to call } }).start(); return true; // we have background work } @Override public boolean onStopJob(JobParameters params) { return false; // don't reschedule } } API 21-23
  22. private static final int JOB_ID = 1; public static void

    schedule(Context context) { long interval = TimeUnit.HOURS.toMillis(6); JobInfo jobInfo = new JobInfo.Builder(JOB_ID, new ComponentName(context, SyncJob.class)) .setRequiresCharging(true) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setPersisted(true) .setPeriodic(interval) .build(); JobScheduler jobScheduler = (JobScheduler) context .getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.schedule(jobInfo); } API 21-23
  23. private static final int JOB_ID = 1; public static void

    schedule(Context context) { long interval = TimeUnit.HOURS.toMillis(6); JobInfo jobInfo = new JobInfo.Builder(JOB_ID, new ComponentName(context, SyncJob.class)) .setRequiresCharging(true) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setPersisted(true) .setPeriodic(interval) .build(); JobScheduler jobScheduler = (JobScheduler) context .getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.schedule(jobInfo); } API 21-23
  24. private static final int JOB_ID = 1; public static void

    schedule(Context context) { long interval = TimeUnit.HOURS.toMillis(6); long flex = TimeUnit.HOURS.toMillis(3); JobInfo jobInfo = new JobInfo.Builder(JOB_ID, new ComponentName(context, SyncJob.class)) .setRequiresCharging(true) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) .setPersisted(true) .setPeriodic(interval, flex) .build(); JobScheduler jobScheduler = (JobScheduler) context .getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.schedule(jobInfo); } API 24-26
  25. GCM Network Manager + Similar API like JobScheduler + minSdkVersion

    9 - Part of Google’s Play Services SDK - Can’t be used without Play Services being pre-installed (most Chinese devices) - API contains some gotchas
  26. public class SyncJob extends GcmTaskService { @Override public int onRunTask(TaskParams

    taskParams) { try { new SyncEngine().syncReminders(); return GcmNetworkManager.RESULT_SUCCESS; } catch (IOException e) { e.printStackTrace(); return GcmNetworkManager.RESULT_FAILURE; } } } With Google Play Services
  27. private static final String TAG = "SyncJob"; public void schedule(Context

    context) { long interval = TimeUnit.HOURS.toMillis(6); long flex = TimeUnit.HOURS.toMillis(3); PeriodicTask task = new PeriodicTask.Builder() .setTag(TAG) .setService(SyncJob.class) .setRequiresCharging(true) .setRequiredNetwork(Task.NETWORK_STATE_UNMETERED) .setPersisted(true) .setUpdateCurrent(true) .setPeriod(interval / 1_000) .setFlex(flex / 1_000) .build(); GcmNetworkManager.getInstance(context).schedule(task); } With Google Play Services
  28. private static final String TAG = "SyncJob"; public void schedule(Context

    context) { long interval = TimeUnit.HOURS.toMillis(6); long flex = TimeUnit.HOURS.toMillis(3); PeriodicTask task = new PeriodicTask.Builder() .setTag(TAG) .setService(SyncJob.class) .setRequiresCharging(true) .setRequiredNetwork(Task.NETWORK_STATE_UNMETERED) .setPersisted(true) .setUpdateCurrent(true) .setPeriod(interval / 1_000) .setFlex(flex / 1_000) .build(); GcmNetworkManager.getInstance(context).schedule(task); } With Google Play Services
  29. Sync job public void schedule(Context context) { if (Build.VERSION.SDK_INT >=

    Build.VERSION_CODES.N) { scheduleWithJobScheduler24(context); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { scheduleWithJobScheduler21(context); } else if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) { scheduleWithGcmNetworkManager(context); } else { scheduleWithAlarmManager(context); } }
  30. (Firebase JobDispatcher) + Wrapper for job scheduling engines - Setup

    is difficult - Library not maintained - Only GCM NetworkManager supported - Many open issues → I would not recommend using it
  31. (JobIntentService) + Replacement for IntentService respecting the App state (uses

    JobScheduler on Android O+) + Handles wakelocks for you - Only for running tasks now - Once a job is enqueued, you cannot cancel it
  32. Conclusion • A lot of boilerplate to achieve something that

    should be simple • It should be much easier
  33. android-job • Single API to use the JobSchduler, GCM Network

    Manager and AlarmManager • All API 26 features supported • Automatically chooses best API to run a job • Less boilerplate, e.g. no manifest entry needed • No reflection used • Maintained and improved continuously • … and more
  34. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  35. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  36. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  37. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  38. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  39. API PersistableBundleCompat extras = new PersistableBundleCompat(); extras.putString("key", "Hello world"); Bundle

    transientExtras = new Bundle(); transientExtras.putParcelable("parcelable", Uri.parse("file://path")); .setExtras(extras) .setTransientExtras(transientExtras)
  40. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  41. API int jobId = new JobRequest.Builder(ReminderJob.TAG) .setExecutionWindow(30_000L, 40_000L) .setExact(30_000L) .startNow()

    .setPeriodic(TimeUnit.HOURS.toMillis(3), TimeUnit.HOURS.toMillis(1)) .setBackoffCriteria(5_000L, JobRequest.BackoffPolicy.EXPONENTIAL) .setRequiresCharging(true) .setRequiresDeviceIdle(false) .setRequiredNetworkType(JobRequest.NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true) .setRequirementsEnforced(true) .setExtras(extras) .setTransientExtras(transientExtras) .setUpdateCurrent(true) .build() .schedule();
  42. Setup dependencies { compile 'com.evernote:android-job:1.2.0' } public class App extends

    Application { @Override public void onCreate() { super.onCreate(); JobManager.create(this).addJobCreator(new ReminderJobCreator()); } }
  43. Setup dependencies { compile 'com.evernote:android-job:1.2.0' } public class App extends

    Application { @Override public void onCreate() { super.onCreate(); JobManager.create(this).addJobCreator(new ReminderJobCreator()); } }
  44. Setup public class ReminderJobCreator implements JobCreator { @Override public Job

    create(String tag) { switch (tag) { case ReminderJob.TAG: return new ReminderJob(); case SyncJob.TAG: return new SyncJob(); default: return null; } } }
  45. Reminder job public class ReminderJob extends Job { public static

    final String TAG = "ReminderJob"; private static final String EXTRA_ID = "EXTRA_ID"; @NonNull @Override protected Result onRunJob(Params params) { int id = params.getExtras().getInt(EXTRA_ID, -1); Reminder reminder = ReminderEngine.instance().getReminderById(id); if (reminder == null) { return Result.FAILURE; } ReminderEngine.instance().showReminder(reminder); return Result.SUCCESS; } }
  46. Reminder job public class ReminderJob extends Job { public static

    final String TAG = "ReminderJob"; private static final String EXTRA_ID = "EXTRA_ID"; @NonNull @Override protected Result onRunJob(Params params) { int id = params.getExtras().getInt(EXTRA_ID, -1); Reminder reminder = ReminderEngine.instance().getReminderById(id); if (reminder == null) { return Result.FAILURE; } ReminderEngine.instance().showReminder(reminder); return Result.SUCCESS; } }
  47. public static int schedule(@NonNull Reminder reminder) { PersistableBundleCompat extras =

    new PersistableBundleCompat(); extras.putInt(EXTRA_ID, reminder.getId()); long time = Math.max(1L, reminder.getTimestamp() - System.currentTimeMillis()); return new JobRequest.Builder(TAG) .setExact(time) .setExtras(extras) .setUpdateCurrent(false) .build() .schedule(); } Reminder job
  48. Reminder job public static int schedule(@NonNull Reminder reminder) { PersistableBundleCompat

    extras = new PersistableBundleCompat(); extras.putInt(EXTRA_ID, reminder.getId()); long time = Math.max(1L, reminder.getTimestamp() - System.currentTimeMillis()); return new JobRequest.Builder(TAG) .setExact(time) .setExtras(extras) .setUpdateCurrent(false) .build() .schedule(); }
  49. Sync job public class SyncJob extends Job { public static

    final String TAG = "SyncJob"; @NonNull @Override protected Result onRunJob(Params params) { new SyncEngine().syncReminders(); return Result.SUCCESS; } }
  50. Sync job public static int schedule() { Set<JobRequest> jobRequests =

    JobManager.instance().getAllJobRequestsForTag(TAG); if (!jobRequests.isEmpty()) { return jobRequests.iterator().next().getJobId(); } long interval = TimeUnit.HOURS.toMillis(6); // every 6 hours long flex = TimeUnit.HOURS.toMillis(3); // wait 3 hours before job runs again return new JobRequest.Builder(TAG) .setPeriodic(interval, flex) .setUpdateCurrent(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .setRequiresCharging(true) .setRequirementsEnforced(true) .build() .schedule(); }
  51. Sync job public static int schedule() { Set<JobRequest> jobRequests =

    JobManager.instance().getAllJobRequestsForTag(TAG); if (!jobRequests.isEmpty()) { return jobRequests.iterator().next().getJobId(); } long interval = TimeUnit.HOURS.toMillis(6); // every 6 hours long flex = TimeUnit.HOURS.toMillis(3); // wait 3 hours before job runs again return new JobRequest.Builder(TAG) .setPeriodic(interval, flex) .setUpdateCurrent(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .setRequiresCharging(true) .setRequirementsEnforced(true) .build() .schedule(); }
  52. Sync job public static int schedule() { Set<JobRequest> jobRequests =

    JobManager.instance().getAllJobRequestsForTag(TAG); if (!jobRequests.isEmpty()) { return jobRequests.iterator().next().getJobId(); } long interval = TimeUnit.HOURS.toMillis(6); // every 6 hours long flex = TimeUnit.HOURS.toMillis(3); // wait 3 hours before job runs again return new JobRequest.Builder(TAG) .setPeriodic(interval, flex) .setUpdateCurrent(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .setRequiresCharging(true) .setRequirementsEnforced(true) .build() .schedule(); }
  53. Sync job public static int schedule() { Set<JobRequest> jobRequests =

    JobManager.instance().getAllJobRequestsForTag(TAG); if (!jobRequests.isEmpty()) { return jobRequests.iterator().next().getJobId(); } long interval = TimeUnit.HOURS.toMillis(6); // every 6 hours long flex = TimeUnit.HOURS.toMillis(3); // wait 3 hours before job runs again return new JobRequest.Builder(TAG) .setPeriodic(interval, flex) .setUpdateCurrent(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .setRequiresCharging(true) .setRequirementsEnforced(true) .build() .schedule(); }
  54. Sync job public static int schedule() { Set<JobRequest> jobRequests =

    JobManager.instance().getAllJobRequestsForTag(TAG); if (!jobRequests.isEmpty()) { return jobRequests.iterator().next().getJobId(); } long interval = TimeUnit.HOURS.toMillis(6); // every 6 hours long flex = TimeUnit.HOURS.toMillis(3); // wait 3 hours before job runs again return new JobRequest.Builder(TAG) .setPeriodic(interval, flex) .setUpdateCurrent(true) .setRequiredNetworkType(JobRequest.NetworkType.UNMETERED) .setRequiresCharging(true) .setRequirementsEnforced(true) .build() .schedule(); }
  55. FAQ Which API is used? • Exact job → AlarmManager

    • API 21+ → JobScheduler • Play Services installed → GcmNetworkManager • Else → AlarmManager
  56. FAQ What happens after a reboot? • All jobs are

    rescheduled if necessary → even after a force close → even after the Play Services were updated → even after the app was updated
  57. FAQ How can I run an asynchronous operation? • Block

    the background thread override fun onRunJob(params: Params): Result { //... observable .filter { it.didItemsChange } ... .blockingGet() return Result.SUCCESS }
  58. FAQ What happens if my job crashes? • The result

    is treated as failure and the job is not rescheduled
  59. FAQ Can I reuse jobs? • No, always create a

    new instance in your job creator
  60. FAQ What are transient jobs? • Jobs are persisted whereas

    transient jobs will only run as long as your process keeps running val bundle = Bundle().apply { putParcelable("uri", Uri.parse("file://something")) putSerializable("enum", JobRequest.NetworkType.CONNECTED) } JobRequest.Builder(TAG) .startNow() .setTransientExtras(bundle) .build() .schedule()
  61. FAQ How can I run a job once a day?

    • Use the DailyJob class
  62. FAQ How can I run a job once a day?

    class CollectCleanUpJob : DailyJob() { companion object { const val TAG = "CollectCleanUpJob" fun schedule() { val builder = JobRequest.Builder(TAG).setRequiresDeviceIdle(true) DailyJob.schedule(builder, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(7)) } } override fun onRunDailyJob(params: Params): DailyJobResult { return DailyJobResult.SUCCESS } }
  63. FAQ How can I run a job once a day?

    class CollectCleanUpJob : DailyJob() { companion object { const val TAG = "CollectCleanUpJob" fun schedule() { val builder = JobRequest.Builder(TAG).setRequiresDeviceIdle(true) DailyJob.schedule(builder, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(7)) } } override fun onRunDailyJob(params: Params): DailyJobResult { return DailyJobResult.SUCCESS } }
  64. FAQ How can I run a job once a day?

    class CollectCleanUpJob : DailyJob() { companion object { const val TAG = "CollectCleanUpJob" fun schedule() { val builder = JobRequest.Builder(TAG).setRequiresDeviceIdle(true) DailyJob.schedule(builder, TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(7)) } } override fun onRunDailyJob(params: Params): DailyJobResult { return DailyJobResult.SUCCESS } }
  65. FAQ How can I test jobs? JobRequest.Builder(TAG) .apply { if

    (type == IMMEDIATELY) { startNow() } else { setExecutionWindow(TimeUnit.SECONDS.toMillis(30), TimeUnit.SECONDS.toMillis(60)) } } .setExtras(bundle) .build() .schedule()
  66. FAQ How can I test jobs? > adb shell dumpsys

    jobscheduler | grep com.company.app JOB #u0a84/1: 3520a16 com.evernote.android.job.demo.gcm/com.evernote.android.job.v21.PlatformJobService u0a84 tag=*job*/com.evernote.android.job.demo.gcm/com.evernote.android.job.v21.PlatformJobService Source: uid=u0a84 user=0 pkg=com.evernote.android.job.demo.gcm Service: com.evernote.android.job.demo.gcm/com.evernote.android.job.v21.PlatformJobService JOB #u0a84/2: c052f97 com.evernote.android.job.demo.gcm/com.evernote.android.job.v21.PlatformJobService u0a84 tag=*job*/com.evernote.android.job.demo.gcm/com.evernote.android.job.v21.PlatformJobService Source: uid=u0a84 user=0 pkg=com.evernote.android.job.demo.gcm Service: com.evernote.android.job.demo.gcm/com.evernote.android.job.v21.PlatformJobService > adb shell cmd jobscheduler run -f com.company.app 1
  67. FAQ How can I test Doze? > adb shell dumpsys

    deviceidle help > adb shell dumpsys deviceidle force-idle [light|deep] > adb shell dumpsys deviceidle unforce
  68. android-job Tips & Tricks • Look at the samples in

    the README • Read the FAQ • Uncertain? Having problems? → Create an issue
  69. Conclusion • Android APIs force you to ◦ do the

    same thing multiple times ◦ write a lot of boilerplate ◦ catch many gotchas • android-job to the rescue