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

A guide to Android Background Work

A guide to Android Background Work

It’s hard to keep track each year of the best practices to perform background work on Android: behavior changes targeting new SDK versions, new permissions, new policies, updated tools, and libraries.

This talk will examine the most popular types of background work developers could need to run and suggest the appropriate tools, along with some tips learned from developing production apps.

Particular attention will be dedicated to WorkManager’s new features and recent platform behavior changes that impact this topic.

After attending this talk you’ll know:
– New rules and behavior changes of most newer Android versions regarding background work;
– What Immedate, Long Running, and Deferrable work are and which tools to use for them;
– New features in WorkManager’s latest releases;
– Real-life experiences with background jobs consistency around different smartphone vendors;

Paolo Rotolo

March 16, 2024
Tweet

More Decks by Paolo Rotolo

Other Decks in Programming

Transcript

  1. Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

    WorkManager Coroutines WorkManager / Foreground Services WorkManager
  2. Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

    WorkManager Coroutines WorkManager / Foreground Services WorkManager
  3. WorkManager class UploadLogsWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext,

    params) { override suspend fun doWork(): Result { try { uploadLogs() } catch(e: HttpException) { return if (e.code == 500) { Result.retry() } else{ Result.failure() } } return Result.success() }
  4. WorkManager class UploadLogsWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext,

    params) { override suspend fun doWork(): Result { try { val workManager = WorkManager .getInstance(applicationContext) val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() workManager.enqueue(workRequest)
  5. val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS

    ).build() repeatInterval repeatInterval repeatInterval can run work can run work can run work
  6. Schedule exact alarms AlarmManager fun setExact (int type, long triggerAtMillis,

    PendingIntent operation) - Use only when exact-time delivery is required
  7. Schedule exact alarms AlarmManager fun setExact (int type, long triggerAtMillis,

    PendingIntent operation) - Use only when exact-time delivery is required - SDK >= 31 will require SCHEDULE_EXACT_ALARM permission
  8. val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS

    ).build() repeatInterval repeatInterval repeatInterval can run work can run work can run work
  9. val workManager = WorkManager .getInstance(applicationContext) val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval

    = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, workRequest)
  10. val workManager = WorkManager .getInstance(applicationContext) val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval

    = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, workRequest)
  11. val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS

    ).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, workRequest) Starting from… WorkManager 2.9.0 (Nov 2023)
  12. val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS

    ).setNextScheduleTimeOverride(newTimeInMillis).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequest) Starting from… WorkManager 2.9.0 (Nov 2023)
  13. val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS

    ).setNextScheduleTimeOverride(newTimeInMillis).build() WorkManager 2.9.0 (Nov 2023) newTimeInMillis repeatInterval repeatInterval can run work can run work can run work
  14. val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS

    ).setNextScheduleTimeOverride(newTimeInMillis).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequest) WorkManager 2.9.0 (Nov 2023)
  15. workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName) val workRequest = PeriodicWorkRequestBuilder<UploadLogsWorker>( repeatInterval = 24, repeatIntervalTimeUnit =

    TimeUnit.HOURS ).setNextScheduleTimeOverride(newTimeInMillis).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequest)
  16. Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

    WorkManager Coroutines WorkManager / Foreground Services WorkManager
  17. class LocalizationWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { while (true) { val position = computeUserPosition() sendPositionToBackend(position) delay(1.minutes) } return Result.success() }
  18. WorkManager under the hood API 23+ API 14-22 Custom AlarmManager

    + BroadcastReceiver JobScheduler + GreedyScheduler
  19. JobScheduler “In Android version LOLLIPOP, jobs had a maximum execution

    time of one minute. Starting with Android version M and ending with Android version R, jobs had a maximum execution time of 10 minutes. Starting from Android version S, jobs will still be stopped after 10 minutes if the system is busy or needs the resources, but if not, jobs may continue running longer than 10 minutes.”
  20. Starting from Android 9 (API 28) - Active - Working

    Set - Frequent - Rare - Restricted No restriction 10 minutes every 2h 10 minutes every 8h 10 minutes every 24h Once for day Jobs Bucket
  21. class LocalizationWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { while (true) { val position = computeUserPosition() sendPositionToBackend(position) delay(1.minutes) } return Result.success() }
  22. class LocalizationWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { setForeground(createForegroundInfo()) while (true) { val position = computeUserPosition() sendPositionToBackend(position) delay(1.minutes) } return Result.success() }
  23. private fun createForegroundInfo(): ForegroundInfo { val title = "Example title"

    // Pending Intent to cancel the worker val intent = WorkManager.getInstance(applicationContext) .createCancelPendingIntent(getId()) createNotificationChannel("channel_id", "Example channel") val notification = NotificationCompat.Builder(applicationContext, "CHANNEL ID") .setContentTitle(title) .setTicker(title) .setContentText("Running in background ... ") .setSmallIcon(R.mipmap.ic_launcher) .setOngoing(true) .addAction(android.R.drawable.ic_delete, "STOP", intent) .build() return ForegroundInfo(42, notification) } private val notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @TargetApi(Build.VERSION_CODES.O) private fun createNotificationChannel( channelId: String,
  24. class LocalizationWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { setForeground(createForegroundInfo()) while (true) { val position = computeUserPosition() sendPositionToBackend(position) delay(1.minutes) } return Result.success() }
  25. Starting from… Android 14 (Target API 34) <application> <service android:name="androidx.work.impl.foreground.SystemForegroundService"

    android:foregroundServiceType="location" tools:node="merge" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" /> Mandatory to specify at least one type
  26. Starting from… Android 14 (Target API 34) camera connectedDevice dataSync

    health location mediaPlayback mediaProjection microphone phoneCall remoteMessaging shortService specialUse systemExempted
  27. Apps in restricted bucket don’t schedule Foreground Services ForegroundServices can

    not be launched from background Starting from… Android 12 (Target API 31)
  28. ForegroundServices can not be launched from background unless... App uses

    CompanionDeviceManager with associated permissions
  29. ForegroundServices can not be launched from background unless... App uses

    CompanionDeviceManager with associated permissions App receives a Geofencing or ActivityRecognition event
  30. ForegroundServices can not be launched from background unless... App uses

    CompanionDeviceManager with associated permissions App receives a Geofencing or ActivityRecognition event App receives an High Priority FCM notification
  31. App receives an High Priority FCM notification - High Priority

    FCM should be used for time-sensitive, user-facing features - App Standby Buckets will not regulate app HP FCM Quotas - Apps that do not successfully post notifications in response to HP FCMs may see messages demoted
  32. App receives an High Priority FCM notification - High Priority

    FCM should be used for time-sensitive, user-facing features - App Standby Buckets will not regulate app HP FCM Quotas - Apps that do not successfully post notifications in response to HP FCMs may see messages demoted
  33. ForegroundServices can not be launched from background unless... App uses

    CompanionDeviceManager with associated permissions App receives a Geofencing or ActivityRecognition event App receives an High Priority FCM notification
  34. ForegroundServices can not be launched from background unless... App uses

    CompanionDeviceManager with associated permissions App receives a Geofencing or ActivityRecognition event App receives an High Priority FCM notification App receives ACTION_BOOT_COMPLETED, ACTION_TIMEZONE_CHANGED System apps Apps with device owners and profile owners permissions Apps with SYSTEM_ALERT_WINDOW permission App is current input method App invokes an Exact Alarm to complete an action the user request User performs an action on a UI element related to the app
  35. Starting from… Android 14 (Target API 34) val networkRequestBuilder =

    NetworkRequest.Builder() .addCapability(NET_CAPABILITY_INTERNET) .addCapability(NET_CAPABILITY_NOT_METERED) // Add or remove capabilities based on your requirements .build() val jobInfo = JobInfo.Builder() // ... .setUserInitiated(true) .setRequiredNetwork(networkRequestBuilder.build()) .setEstimatedNetworkBytes(1024 * 1024 * 1024) // ... .build() Migrate ForegroundServices to UserInitiated Data Transfer Jobs
  36. Starting from… Android 14 (Target API 34) Migrate ForegroundServices to

    UserInitiated Data Transfer Jobs or use WorkManager!
  37. Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

    WorkManager Coroutines WorkManager / Foreground Services WorkManager
  38. class SendMessageWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { try { sendMessage(inputData) } catch (e: NetworkException) { return Result.retry() } return Result.success() }
  39. class SendMessageWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { try { sendMessage(inputData) } catch (e: NetworkException) { return Result.retry() } return Result.success() }
  40. class SendMessageWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { val request = OneTimeWorkRequestBuilder<SendMessageWorker>() .build() workManager.enqueue(request)
  41. class SendMessageWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { val request = OneTimeWorkRequestBuilder<SendMessageWorker>() .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() workManager.enqueue(request)
  42. class SendMessageWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun doWork(): Result { try { sendMessage(inputData) } catch (e: NetworkException) { return Result.retry() } return Result.success() } workManager.enqueue(request)
  43. class SendMessageWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params)

    { override suspend fun getForegroundInfo(): ForegroundInfo { return createForegroundInfo() } override suspend fun doWork(): Result { try { sendMessage(inputData) } catch (e: NetworkException) { return Result.retry() } workManager.enqueue(request)
  44. Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

    WorkManager Coroutines WorkManager / Foreground Services WorkManager
  45. workManager .getWorkInfosForUniqueWorkFlow(workName) .collect { it.forEach { if (it.runAttemptCount > 0

    && it.state = = WorkInfo.State.ENQUEUED) { log(it.stopReason) } } } Starting from… WorkManager 2.9.0-alpha02
  46. override suspend fun doWork(): Result { try { sendMessage(inputData) }

    catch (e: NetworkException) { return Result.retry() } return Result.success() } Starting from… WorkManager 2.9.0-alpha02
  47. override suspend fun doWork(): Result { logLastStoppedReason(stopReason) try { sendMessage(inputData)

    } catch (e: NetworkException) { return Result.retry() } return Result.success() } Starting from… WorkManager 2.9.0-alpha02
  48. STOP_REASON_NOT_STOPPED STOP_REASON_CANCELLED_BY_APP STOP_REASON_PREEMPT STOP_REASON_TIMEOUT STOP_REASON_DEVICE_STATE STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW STOP_REASON_CONSTRAINT_CHARGING STOP_REASON_CONSTRAINT_CONNECTIVITY STOP_REASON_CONSTRAINT_DEVICE_IDLE STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW

    STOP_REASON_QUOTA STOP_REASON_BACKGROUND_RESTRICTION STOP_REASON_APP_STANDBY STOP_REASON_USER STOP_REASON_SYSTEM_PROCESSING STOP_REASON_ESTIMATED_APP_LAUNCH_TIME_CHANGED Starting from… WorkManager 2.9.0-alpha02