Logo
From Foreground Services to WorkManager: Adapting Auto App Backup for Modern Android

From Foreground Services to WorkManager: Adapting Auto App Backup for Modern Android

By Sagar Maiyad  Oct 24, 2025

We migrated our auto app backup feature from a 24/7 foreground service to WorkManager periodic workers, significantly improving battery life while maintaining reliable backups. This post shares our journey adapting to Android's increasingly strict background execution policies.

The Challenge: Android's War on Background Services

If you've been developing Android apps for a while, you've witnessed the platform's progressive tightening of background execution. What started as gentle warnings in Android 8.0 (Oreo) has evolved into strict enforcement by Android 14+.

For our All Backup & Restore app, this created a critical problem: How do you automatically backup newly installed or updated apps when Android won't let you run in the background?

The Old Way: Foreground Service (2020-2024)

How It Worked

Our initial implementation was straightforward:

// Broadcast Receiver listening for boot
class BootCompleteReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == "android.intent.action.BOOT_COMPLETED") {
            // Start foreground service
            context?.startForegroundService(serviceIntent)
        }
    }
}

The Service would:

  1. Start as a foreground service with persistent notification
  2. Register broadcast receivers for PACKAGE_ADDED and PACKAGE_REPLACED
  3. Immediately backup any newly installed/updated app
  4. Run 24/7 until user disables the feature

Architecture Diagram (Old):

User Enables Auto Backup
    ↓
Start Foreground Service
    ↓
Show Persistent Notification
    ↓
Register Broadcast Receivers
    ↓
Listen for PACKAGE_ADDED/REPLACED
    ↓
Immediate Backup Triggered

The Problems Started Rolling In

1. Android 14 Crashes (Late 2024)

Fatal Exception: ForegroundServiceStartNotAllowedException
Unable to start service - not allowed to start foreground service

Android 14 introduced FOREGROUND_SERVICE_TYPE_DATA_SYNC requirements. We had to add specific permissions and service type declarations in the manifest.

But the crashes kept coming.

2. Battery Drain Complaints

User reviews started mentioning:

  • "App drains battery even when I'm not using it"
  • "Why is there always a notification?"
  • "Can't you just run when needed?"

Comparison between old foreground service approach with persistent notifications vs new WorkManager approach with no persistent notifications

3. System Killing the Service

Despite being a foreground service, Android would still kill it on:

  • Low memory situations
  • Battery saver mode
  • Manufacturer-specific optimizations (Samsung, Xiaomi, OnePlus)

We added restart logic, but it felt like fighting the platform.

The Turning Point: Mid-2025

After analyzing production data showing:

  • Significant portion of Android 14 users experiencing service crashes
  • Noticeable battery consumption from our background service
  • User retention dropping due to service-related issues

We decided: Stop fighting the system. Work with it.

The New Way: WorkManager Periodic Polling

Architecture Redesign

Instead of listening for app changes in real-time, we poll for changes periodically.

Battery usage comparison showing dramatic improvement from foreground service to WorkManager approach

Core Components

1. MonitorWorker - The Detector

This worker runs periodically and:

  • Loads the previously known list of installed apps (from persistent storage)
  • Gets the current list of installed apps from PackageManager
  • Compares both lists to detect new installations or updates
  • Schedules individual backup workers for changed apps
  • Saves the current app list for the next comparison

Key Logic:

// Pseudo-code for clarity
fun detectChanges(lastKnown: List<AppInfo>, current: List<AppInfo>): List<AppInfo> {
    val changes = mutableListOf<AppInfo>()

    for (app in current) {
        val oldApp = lastKnown.find { it.packageName == app.packageName }
        when {
            oldApp == null -> changes.add(app.copy(isNew = true))
            app.versionCode > oldApp.versionCode -> changes.add(app.copy(isUpdate = true))
        }
    }
    return changes
}

2. BackupWorker - The Executor

This worker handles the actual backup:

  • Receives package name as input
  • Performs the backup operation
  • Returns success/failure result

3. Scheduling Logic

// Scheduling periodic monitoring
val workRequest = PeriodicWorkRequestBuilder<MonitorWorker>(
    1, TimeUnit.HOURS // Check every hour
)
.setConstraints(
    Constraints.Builder()
        .setRequiresBatteryNotLow(true)
        .setRequiresStorageNotLow(true)
        .build()
)
.build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "app_monitor_work",
        ExistingPeriodicWorkPolicy.KEEP,
        workRequest
    )

Architecture Diagram (New):

User Enables Auto Backup
    ↓
Schedule MonitorWorker (Every 1 hour)
    ↓
Worker Wakes Up
    ↓
Load Last Known Apps from Storage
    ↓
Get Current Installed Apps
    ↓
Compare & Detect Changes
    ↓
For Each Changed App:
    Schedule BackupWorker (Staggered delays)
    ↓
Save Current Apps List
    ↓
Worker Sleeps (No notification, no battery drain)

WorkManager scheduling visualization showing periodic app monitoring and automated backup triggering

The Evolution: From Real-Time to Periodic

Timeline of Changes

Period Change Reason
Late 2024 Added Android 14 service type Crash fixes
Mid 2024 Started investigating alternatives Battery complaints
Mid 2025 Switched to WorkManager Complete redesign
Late 2025 Optimized interval (15min → 1 hour) Reduce overhead
Recent Fixed coroutine scope issues Stability improvements

Key Optimizations We Made

1. Staggered Worker Execution

Problem: When 20 apps update at once, starting 20 backup workers simultaneously caused ANR (Application Not Responding).

Solution: Stagger delays by 20 seconds per app.

// Pseudo-code
changedApps.forEachIndexed { index, app ->
    val delaySeconds = index * 20L
    // App 0: immediate, App 1: 20s, App 2: 40s, etc.
    scheduleBackup(app, delaySeconds)
}

Result: Maximum 1-2 concurrent workers, no ANR.

2. Structured Concurrency Fix

Problem: Certain backup operations used unstructured coroutine scopes which caused cancellation exceptions.

Solution: Properly scope coroutines within parent job hierarchy.

// Before (Problematic)
fun saveBundleApp() = CoroutineScope(Dispatchers.IO).async {
    // Orphaned scope - not connected to parent lifecycle
}

// After (Fixed)
suspend fun saveBundleApp(): Result = coroutineScope {
    try {
        val result = async { /* backup logic */ }.await()
        result
    } catch (e: Exception) {
        // Proper error handling
        Result.failure(e)
    }
}

Result: Eliminated cancellation-related crashes in production.

3. Smart Constraints

val constraints = Constraints.Builder()
    .setRequiresBatteryNotLow(true)
    .setRequiresStorageNotLow(true)
    .build()

Workers don't run when:

  • Battery < 15%
  • Storage critically low

Before (Foreground Service)

  • ⚡ Battery impact: Significant daily drain
  • 📱 Persistent notification: Always visible
  • ⏱️ Backup delay: Immediate (0-5 seconds)
  • 💥 Crash rate: Notable on Android 14+
  • 👥 User satisfaction: Lower ratings mentioning battery

After (WorkManager)

  • ⚡ Battery impact: ~70% reduction
  • 📱 Persistent notification: None (only during active backup)
  • ⏱️ Backup delay: Up to 1 hour
  • 💥 Crash rate: 95% reduction
  • 👥 User satisfaction: Improved ratings, fewer battery complaints

Visual representation of migration journey from complex foreground service to clean WorkManager solution

Trade-offs and Learnings

What We Gained

Battery Efficiency - Dramatic reduction in battery usage
Better UX - No annoying persistent notifications
Platform Compliance - Works with Android, not against it
Stability - Significant reduction in crashes
Future-Proof - Compatible with future Android versions

What We Sacrificed

⚠️ Real-Time Detection - Up to 1 hour delay (acceptable for backup use case)
⚠️ Polling Overhead - Checks every hour even if no changes (minimal CPU)

Key Learnings

  1. Stop Fighting the Platform
    Android's restrictions exist for good reasons (battery, security). Work with them.

  2. Polling > Listening in Modern Android
    For non-critical tasks, periodic polling is more reliable than broadcast receivers.

  3. WorkManager is Production-Ready
    Despite initial skepticism, WorkManager handles edge cases better than custom solutions.

  4. Structured Concurrency Matters
    Orphaned coroutine scopes will cause production issues. Always use structured concurrency.

  5. Test on Real Devices
    Manufacturer-specific optimizations behave differently than emulators.

Migration Guide for Other Apps

If you're facing similar issues, here's our recommended approach:

Step 1: Audit Current Background Usage

Analyze:

  • Battery consumption patterns
  • Crash reports related to background services
  • User feedback about battery/notifications

Step 2: Identify Real-Time vs Delayed Requirements

Ask: "Does this NEED to happen immediately, or can it wait 15-60 minutes?"

For most background tasks, delays are acceptable.

Step 3: Implement WorkManager

// Periodic task example
val workRequest = PeriodicWorkRequestBuilder<YourWorker>(
    15, TimeUnit.MINUTES // Minimum interval
).build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "unique_work_name",
        ExistingPeriodicWorkPolicy.KEEP,
        workRequest
    )

Step 4: Add Constraints

.setConstraints(
    Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .setRequiresStorageNotLow(true)
        .build()
)

Step 5: Monitor and Optimize

  • Track worker success/failure rates
  • Monitor battery impact
  • A/B test different intervals
  • Adjust based on user feedback

Technical Considerations

Data Persistence

Store the app list using:

  • SharedPreferences for small datasets
  • Room Database for larger datasets
  • Kotlin Serialization for easy JSON conversion

Comparison Algorithm

Efficient comparison is key:

// Use maps for O(1) lookup
val lastKnownMap = lastKnown.associateBy { it.packageName }

// Single pass comparison
current.forEach { app ->
    val oldVersion = lastKnownMap[app.packageName]?.versionCode ?: 0
    if (app.versionCode > oldVersion) {
        // App is new or updated
    }
}

Worker Chaining

For complex operations:

val monitor = OneTimeWorkRequestBuilder<MonitorWorker>().build()
val backup = OneTimeWorkRequestBuilder<BackupWorker>().build()

WorkManager.getInstance(context)
    .beginWith(monitor)
    .then(backup)
    .enqueue()

Common Pitfalls to Avoid

  1. Too Frequent Polling
    Checking every 1-5 minutes defeats the purpose. Stick to 15min minimum.

  2. Not Using Constraints
    Always add battery and storage constraints.

  3. Ignoring WorkManager Guarantees
    WorkManager will execute your work eventually, but timing isn't guaranteed.

  4. Forgetting to Handle Cancellation
    Always implement proper cancellation handling in workers.

  5. Not Testing Edge Cases
    Test with airplane mode, battery saver, and doze mode enabled.

Conclusion

Migrating from foreground services to WorkManager wasn't just about compliance—it made our app genuinely better. Users are happier, battery life improved, and we eliminated an entire class of crashes.

If you're still using foreground services for periodic background tasks, it's time to migrate. The platform has evolved, and so should we.

The key insight: Modern Android rewards apps that respect system resources. WorkManager is the platform's way of saying "we'll help you do background work efficiently—just follow our guidelines."


Android WorkManager Technical Deep Dive Background Services

Author

Sagar Maiyad
Written By
Sagar Maiyad

Sagar Maiyad - Android Team Lead specializing in Kotlin, Jetpack Compose, Flutter, and Node.js development. Building practical Android apps with 2M+ downloads and sharing real-world development insights.

View All Posts →

Latest Post

Latest Tags