Irbisa · Cheatsheet
Background Work & Services
WorkManager, foreground services, service types, AlarmManager, Doze, background limits
What is a
Service, how do started, bound and foreground services differ, and on which thread does a Service run?What is WorkManager, what does it guarantee, and what does it not promise?
class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { val uri = inputData.getString("uri")?.toUri() ?: return Result.failure() return try { repo.upload(uri) Result.success()…What is the difference between
OneTimeWorkRequestandPeriodicWorkRequest, and why can periodic work not run every five minutes?// Raised to the 15-minute floor: no exception, only a logcat warningval tooFast = PeriodicWorkRequestBuilder<FeedWorker>(5, TimeUnit.MINUTES).build() // Once an hour, somewhere in the last 15 minutes of each hourval feed = PeriodicWorkRequestBuilder<FeedWorker>( 1, TimeUnit.HOURS, // repeat interval…How do you pass input to a Worker and get a result back, and why can you not pass a 2 MB image through
Data?// wrong: IllegalStateException, Data cannot occupy more than 10240 bytes// val bad = workDataOf("image" to jpegBytes) // right: the bytes go to disk, Data carries the pathval file = File(context.cacheDir, "upload-${UUID.randomUUID()}.jpg")file.writeBytes(jpegBytes)…How do you choose between a coroutine, WorkManager, a foreground service and
AlarmManagerfor a background task?This
Serviceruns the nightly sync and the Activity binds to it to read progress. What is wrong with it, and what should run the sync?class SyncService : Service() { private var percent = 0 inner class LocalBinder : Binder() { fun progress() = percent } override fun onBind(intent: Intent?): IBinder = LocalBinder() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { repo.syncBlocking { percent = it } return START_STICKY }} // Called when the app has no visible UI and no recent user interaction (e.g. from a sync callback):context.startService(Intent(context, SyncService::class.java))How does WorkManager work under the hood: where is work stored, what actually runs it, and how does it survive a reboot?
This
Workerkeeps copying files long after WorkManager stopped it. Why, and how doWorker,CoroutineWorkerandListenableWorkerhandle a stop?class CopyWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override fun doWork(): Result { for (file in files) { copy(file) } return Result.success() }}Find the bug: the upload Worker always ends as SUCCEEDED, yet some photos never reach the server.
class UploadWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { override fun doWork(): Result { val paths = inputData.getStringArray("paths") ?: return Result.failure() CoroutineScope(Dispatchers.IO).launch { for (path in paths) { api.upload(File(path)) File(path).delete() } } return Result.success() }}How do WorkManager constraints work, and what happens to a running Worker when a constraint stops being met?
val request = OneTimeWorkRequestBuilder<BackupWorker>() .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) // watched during the whole run .setRequiresBatteryNotLow(true) .setRequiresStorageNotLow(true)…This code runs in
Application.onCreate. Why do users get duplicate syncs, and why does the new two-hour interval never take effect?val wm = WorkManager.getInstance(this) wm.enqueue(OneTimeWorkRequestBuilder<SyncWorker>().build()) wm.enqueueUniquePeriodicWork( "sync", ExistingPeriodicWorkPolicy.KEEP, PeriodicWorkRequestBuilder<SyncWorker>(2, TimeUnit.HOURS).build(),)How do retries and backoff work in WorkManager, and how do you stop a Worker from retrying forever?
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result = try { api.sync() Result.success() } catch (e: HttpException) {…How do you chain Workers so that three uploads run in parallel and a publish step runs after all of them, and what happens if one upload fails?
val uploads = photos.map { photo -> OneTimeWorkRequestBuilder<UploadWorker>() .setInputData(workDataOf("path" to photo.path)) .build()}…How do you show progress from a long-running Worker, and when does it need
setForegroundand a foreground service type?class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result { setForeground(getForegroundInfo()) // long run: become a foreground service first val files = dao.pending() files.forEachIndexed { i, file ->…What does
setExpeditedgive you, and what doesOutOfQuotaPolicydecide?val sync = OneTimeWorkRequestBuilder<SyncWorker>() .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .setConstraints( Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build() ) // .setInitialDelay(...) here would throw: expedited work cannot be delayed…What changed for foreground services in Android 12, 14 and 15, and what does a foreground service type require?
// AndroidManifest.xml// <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />// <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />// <service android:name=".UploadService"// android:foregroundServiceType="dataSync" android:exported="false" />…What are Doze mode and App Standby buckets, and which background limits has Android added since 8.0?
Find the bug: run tracking works on office Wi-Fi, but on a weak mobile network the app crashes a few seconds after the user taps Start.
// Started from the Run screen: ContextCompat.startForegroundService(context, intent)class RunTrackingService : Service() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() = scope.cancel() override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { scope.launch { val route = routeRepo.loadActiveRoute() // HTTP call ServiceCompat.startForeground(this@RunTrackingService, NOTIFICATION_ID, buildNotification(route.name), ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION) tracker.track(route) } return START_NOT_STICKY }}How do you build an alarm clock that still rings at 6:00 after the phone spent the night in Doze and rebooted at 3:00?
class AlarmScheduler(private val ctx: Context, private val dao: AlarmDao) { private val am = ctx.getSystemService(AlarmManager::class.java) fun armNext(): Boolean { val next = dao.nextEnabled() ?: return true // false: the UI sends the user to Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM…Your nightly sync runs reliably on a Pixel but never on many Xiaomi and Samsung phones. Why, and what can you honestly do about it?
The client must pull new data as soon as the server changes it. How do you drive that with FCM without wasting the app's quota or battery?
class AppMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { // Did FCM honour our high priority, or downgrade it? if (message.priority != message.originalPriority) { analytics.log("fcm_priority_downgraded")…One user says the sync worker never runs and you cannot reproduce it. How do you find out what the scheduler actually did with the job?
# What did the scheduler decide about our job?adb shell dumpsys jobscheduler | grep -A 30 "com.example.app"# Required constraints: CONNECTIVITY BATTERY_NOT_LOW# Satisfied constraints: BATTERY_NOT_LOW <- no network: this is the answer# Standby bucket: RARE…