Slide 1

Slide 1 text

Doo z z z z z e Ralf Wondratschek @vRallev 2017-09-04

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Doo z z z z z e

Slide 6

Slide 6 text

Motivation

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Doze & App Standby

Slide 9

Slide 9 text

https://developer.android.com/images/training/doze.png

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Android O - Custom Intents

Slide 14

Slide 14 text

Android O - Custom Intents

Slide 15

Slide 15 text

Android O - Custom Intents fun Context.sendEvernoteBroadcast(intent: Intent) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { sendBroadcast(intent, EvernoteContract.PERMISSION) } else { sendBroadcast(intent) } }

Slide 16

Slide 16 text

Demo - Reminder app https://github.com/vRallev/job-sample

Slide 17

Slide 17 text

Requirements ● Show the reminder in a notification ● Sync reminders with the server ● Support all devices ● Be a good citizen

Slide 18

Slide 18 text

Reminder job ● Must be exact ● Can have multiple at the same time ● AlarmManager is the correct API

Slide 19

Slide 19 text

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 - …

Slide 20

Slide 20 text

Reminder job 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); } } }

Slide 21

Slide 21 text

Reminder job 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); } } }

Slide 22

Slide 22 text

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); }

Slide 23

Slide 23 text

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); }

Slide 24

Slide 24 text

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); }

Slide 25

Slide 25 text

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); }

Slide 26

Slide 26 text

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); }

Slide 27

Slide 27 text

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); }

Slide 28

Slide 28 text

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); } }

Slide 29

Slide 29 text

AlarmManager https://plus.google.com/+AndroidDevelopers/posts/GdNrQciPwqo

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

API 21-23

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Flex parameter explained

Slide 38

Slide 38 text

Flex parameter explained

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

With Google Play Services

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

API 14-19 ● Use AlarmManager ● As much fun as the reminder job…

Slide 45

Slide 45 text

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); } }

Slide 46

Slide 46 text

(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

Slide 47

Slide 47 text

(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

Slide 48

Slide 48 text

Conclusion ● A lot of boilerplate to achieve something that should be simple

Slide 49

Slide 49 text

Conclusion ● A lot of boilerplate to achieve something that should be simple ● It should be much easier

Slide 50

Slide 50 text

android-job Fork me on GitHub github.com/evernote/

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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();

Slide 53

Slide 53 text

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();

Slide 54

Slide 54 text

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();

Slide 55

Slide 55 text

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();

Slide 56

Slide 56 text

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();

Slide 57

Slide 57 text

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)

Slide 58

Slide 58 text

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();

Slide 59

Slide 59 text

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();

Slide 60

Slide 60 text

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()); } }

Slide 61

Slide 61 text

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()); } }

Slide 62

Slide 62 text

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; } } }

Slide 63

Slide 63 text

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; } }

Slide 64

Slide 64 text

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; } }

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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(); }

Slide 67

Slide 67 text

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; } }

Slide 68

Slide 68 text

Sync job public static int schedule() { Set 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(); }

Slide 69

Slide 69 text

Sync job public static int schedule() { Set 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(); }

Slide 70

Slide 70 text

Sync job public static int schedule() { Set 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(); }

Slide 71

Slide 71 text

Sync job public static int schedule() { Set 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(); }

Slide 72

Slide 72 text

Sync job public static int schedule() { Set 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(); }

Slide 73

Slide 73 text

Who is using it?

Slide 74

Slide 74 text

FAQ Which API is used? ● Exact job → AlarmManager ● API 21+ → JobScheduler ● Play Services installed → GcmNetworkManager ● Else → AlarmManager

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

FAQ My periodic jobs aren’t running as expected. ● Keep Doze and the system in mind

Slide 77

Slide 77 text

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 }

Slide 78

Slide 78 text

FAQ What happens if my job crashes? ● The result is treated as failure and the job is not rescheduled

Slide 79

Slide 79 text

FAQ Can I reuse jobs? ● No, always create a new instance in your job creator

Slide 80

Slide 80 text

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()

Slide 81

Slide 81 text

FAQ How can I run a job once a day? ● Use the DailyJob class

Slide 82

Slide 82 text

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 } }

Slide 83

Slide 83 text

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 } }

Slide 84

Slide 84 text

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 } }

Slide 85

Slide 85 text

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()

Slide 86

Slide 86 text

FAQ How can I test jobs?

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

FAQ How can I test Doze? > adb shell dumpsys deviceidle help > adb shell dumpsys deviceidle force-idle [light|deep] > adb shell dumpsys deviceidle unforce

Slide 89

Slide 89 text

android-job Tips & Tricks ● Look at the samples in the README ● Read the FAQ ● Uncertain? Having problems? → Create an issue

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

twitter.com/vRallev google.com/+vRallev Doo z z z z z e