Slide 1

Slide 1 text

Appdevcon 2024 A guide to Android Background Work Paolo Rotolo

Slide 2

Slide 2 text

Paolo Rotolo • Android Dev @ Nextome • Open source contributor • Piano player

Slide 3

Slide 3 text

Different Types of Work

Slide 4

Slide 4 text

Different Types of Work Immediate Send a message Start a payment flow Upload a photo …

Slide 5

Slide 5 text

Different Types of Work Immediate Long Running Bulk uploads Location tracking Listen to music …

Slide 6

Slide 6 text

Different Types of Work Immediate Long Running Deferrable Log upload Database sync Storage optimisation …

Slide 7

Slide 7 text

Different Types of Work Immediate Long Running Deferrable

Slide 8

Slide 8 text

Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

Slide 9

Slide 9 text

Persistent Work Global scope

Slide 10

Slide 10 text

Persistent Work Global scope Process Death

Slide 11

Slide 11 text

Persistent Work Global scope Process Death Reboot

Slide 12

Slide 12 text

Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

Slide 13

Slide 13 text

Different Types of Work Immediate Long Running Deferrable Persistent Impersistent

Slide 14

Slide 14 text

Immediate Impersistent Work class ProfileViewModel: ViewModel() { fun loadPhotos() { viewModelScope.launch { fetchUserPhotos() } }

Slide 15

Slide 15 text

Async Work class ProfileViewModel: ViewModel() { fun loadPhotos() { viewModelScope.launch { fetchUserPhotos() } }

Slide 16

Slide 16 text

Different Types of Work Immediate Long Running Deferrable Persistent Impersistent Coroutines

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

WorkManager “ The recommended library for persistent work”

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Different Types of Work Deferrable WorkManager

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

WorkManager class UploadLogsWorker( appContext: Context, params: WorkerParameters, ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { try { val workManager = WorkManager .getInstance(applicationContext) val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() workManager.enqueue(workRequest)

Slide 23

Slide 23 text

val workManager = WorkManager .getInstance(applicationContext) val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() workManager.enqueue(workRequest)

Slide 24

Slide 24 text

val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() repeatInterval repeatInterval repeatInterval can run work can run work can run work

Slide 25

Slide 25 text

Schedule exact alarms AlarmManager fun setExact (int type, long triggerAtMillis, PendingIntent operation)

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() repeatInterval repeatInterval repeatInterval can run work can run work can run work

Slide 29

Slide 29 text

val workManager = WorkManager .getInstance(applicationContext) val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).build() workManager.enqueue(workRequest)

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.KEEP, workRequest) ExistingPeriodicWorkPolicy.KEEP ExistingPeriodicWorkPolicy.REPLACE

Slide 32

Slide 32 text

ExistingPeriodicWorkPolicy.KEEP ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE Starting from… WorkManager 2.8.0 (Feb 2023)

Slide 33

Slide 33 text

ExistingPeriodicWorkPolicy.KEEP ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE ExistingPeriodicWorkPolicy.UPDATE Starting from… WorkManager 2.8.0 (Feb 2023) Preserves enqueue time

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

val workRequest = PeriodicWorkRequestBuilder( 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)

Slide 37

Slide 37 text

val workRequest = PeriodicWorkRequestBuilder( 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

Slide 38

Slide 38 text

val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).setNextScheduleTimeOverride(newTimeInMillis).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequest) WorkManager 2.9.0 (Nov 2023)

Slide 39

Slide 39 text

workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName) val workRequest = PeriodicWorkRequestBuilder( repeatInterval = 24, repeatIntervalTimeUnit = TimeUnit.HOURS ).setNextScheduleTimeOverride(newTimeInMillis).build() val uniqueWorkName = "uploadLogWork" workManager.enqueueUniquePeriodicWork(uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequest)

Slide 40

Slide 40 text

Starting from… WorkManager 2.9.0 (Nov 2023) workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName)

Slide 41

Slide 41 text

workManager.getWorkInfosForUniqueWorkFlow(uniqueWorkName).collect{ } Starting from… WorkManager 2.9.0 (Nov 2023)

Slide 42

Slide 42 text

workManager.getWorkInfosForUniqueWorkFlow(uniqueWorkName).collect{ val workInfo = it.first() workInfo.nextScheduleTimeMillis } Starting from… WorkManager 2.9.0 (Nov 2023)

Slide 43

Slide 43 text

workManager.getWorkInfosForUniqueWorkFlow(uniqueWorkName).collect{ val workInfo = it.first() workInfo.nextScheduleTimeMillis } Starting from… WorkManager 2.9.0 (Nov 2023) estimated time

Slide 44

Slide 44 text

Different Types of Work Deferrable WorkManager

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Different Types of Work Long Running WorkManager / Foreground Services (> 10 minutes)

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

How long will my Worker run in background?

Slide 49

Slide 49 text

WorkManager under the hood

Slide 50

Slide 50 text

WorkManager under the hood API 23+ API 14-22 Custom AlarmManager + BroadcastReceiver JobScheduler

Slide 51

Slide 51 text

Why don’t we use JobScheduler directly?

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

WorkManager under the hood API 23+ API 14-22 Custom AlarmManager + BroadcastReceiver JobScheduler + GreedyScheduler

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

it’s not so easy

Slide 56

Slide 56 text

it’s not so easy meet App Standby Buckets

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Introducing Foreground Services

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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,

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Foreground Services Limitations and recent API Changes

Slide 64

Slide 64 text

Starting from… Android 10 (Target API 29)

Slide 65

Slide 65 text

Starting from… Android 14 (Target API 34) Mandatory to specify at least one type

Slide 66

Slide 66 text

Starting from… Android 14 (Target API 34) camera connectedDevice dataSync health location mediaPlayback mediaProjection microphone phoneCall remoteMessaging shortService specialUse systemExempted

Slide 67

Slide 67 text

Starting from… Android 13 (Target API 33) TaskManager

Slide 68

Slide 68 text

Apps in restricted bucket don’t schedule Foreground Services Starting from… Android 12 (Target API 31)

Slide 69

Slide 69 text

Apps in restricted bucket don’t schedule Foreground Services ForegroundServices can not be launched from background Starting from… Android 12 (Target API 31)

Slide 70

Slide 70 text

ForegroundServices can not be launched from background unless...

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

ForegroundServices can not be launched from background unless... User turns off Battery Optimisations for the app

Slide 79

Slide 79 text

When to use ForegroundServices directly

Slide 80

Slide 80 text

When to use ForegroundServices directly Media Playing Activity Tracking Location Sharing Voice or Video Calls

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Starting from… Android 14 (Target API 34) Migrate ForegroundServices to UserInitiated Data Transfer Jobs or use WorkManager!

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

Different Types of Work Immediate Persistent Impersistent WorkManager Coroutines

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Expedited Work

Slide 87

Slide 87 text

Expedited Work Importance

Slide 88

Slide 88 text

Expedited Work Importance Speed

Slide 89

Slide 89 text

Expedited Work Importance Speed Quotas

Slide 90

Slide 90 text

Expedited Work Importance Speed Quotas Power Management

Slide 91

Slide 91 text

Expedited Work Importance Speed Quotas Power Management Latency

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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)

Slide 96

Slide 96 text

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)

Slide 97

Slide 97 text

Expedited Work Android 12+ ExpeditedWork < Android 12 Foreground Service under the hood

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

Debugging WorkManager

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

No content

Slide 105

Slide 105 text

But wait, there’s more! meet OEMs customisations

Slide 106

Slide 106 text

No content

Slide 107

Slide 107 text

No content

Slide 108

Slide 108 text

No content

Slide 109

Slide 109 text

Thank you! Questions?

Slide 110

Slide 110 text

Appdevcon 2024 A guide to Android Background Work Paolo Rotolo