$30 off During Our Annual Pro Sale. View Details »

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. Doo z z z z z e
    Ralf Wondratschek
    @vRallev
    2017-09-04

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. Doo z z z z z e

    View Slide

  6. Motivation

    View Slide

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

    View Slide

  8. Doze & App Standby

    View Slide

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

    View Slide

  10. 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

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. Android O - Custom Intents

    View Slide

  14. Android O - Custom Intents

    android:label="@string/app_name"
    android:description="@string/description"
    android:protectionLevel="signature" />

    View Slide

  15. Android O - Custom Intents

    android:label="@string/app_name"
    android:description="@string/description"
    android:protectionLevel="signature" />

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. API 21-23


    android:name=".sync.SyncJob"
    android:permission="android.permission.BIND_JOB_SERVICE"/>

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. 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

    View Slide

  36. 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

    View Slide

  37. Flex parameter explained

    View Slide

  38. Flex parameter explained

    View Slide

  39. 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

    View Slide

  40. android:name=".sync.SyncJob"
    android:exported="true"
    android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">




    With Google Play Services

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

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

    View Slide

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

    View Slide

  46. (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

    View Slide

  47. (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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  51. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  57. 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)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  65. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  73. Who is using it?

    View Slide

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

    View Slide

  75. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  86. FAQ
    How can I test jobs?

    View Slide

  87. 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

    View Slide

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

    View Slide

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

    View Slide

  90. 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

    View Slide

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

    View Slide