Chapter 30: Background Task Scheduling¶
"The cheapest, most reliable, and most power-efficient computation is the one you never perform." -- Android Performance Team
Background work is the eternal tension in mobile operating systems. Users want their email synced, their photos backed up, their news feeds refreshed, and their notifications delivered promptly. But every background operation drains battery, consumes network bandwidth, and competes for CPU and memory with the foreground application. Multiply this by the hundreds of apps installed on a typical device, and you have a tragedy of the commons: each app's background work is individually reasonable but collectively devastating to battery life.
Android's answer has evolved over a decade of increasingly aggressive restrictions. This chapter traces the entire background execution infrastructure: the execution limits introduced in Android 8.0, the JobScheduler that replaced ad hoc background work with a constraint-aware scheduler, the AlarmManager that handles time-based wakeups, the WorkManager abstraction layer, foreground services and their evolving requirements, and broadcast restrictions that limit implicit wakeups.
30.1 Background Execution Limits¶
Android 8.0 (Oreo, API 26) introduced the most significant restrictions on background execution in the platform's history. Prior to Oreo, any application could start services, register broadcast receivers, and perform work in the background with minimal restrictions. The result was poor battery life and degraded system performance.
30.1.1 The Pre-Oreo Problem¶
Before Android 8.0, background abuse was rampant:
- Apps started long-running services that persisted indefinitely
- Dozens of apps registered for the same implicit broadcasts (e.g.,
CONNECTIVITY_CHANGED), causing broadcast storms that woke every registered app - Background services consumed CPU, memory, and network with no coordination
- Users had no visibility into which apps were consuming resources in the background
30.1.2 Background Service Limitations¶
Starting with Android 8.0, apps targeting API 26+ are subject to:
-
Background service restrictions: An app in the background cannot freely call
startService(). If the app is not in the foreground (no visible activities, no foreground service running), callingstartService()throws anIllegalStateException. -
Allowed alternatives:
startForegroundService()-- starts a service that must post a notification within 5 secondsJobScheduler.schedule()-- schedules constraint-aware workWorkManager.enqueue()-- schedules deferrable work (AndroidX)
flowchart TD
A["App wants to do<br/>background work"] --> B{"Is app in<br/>foreground?"}
B -->|Yes| C["startService() OK"]
B -->|No| D{"What kind<br/>of work?"}
D -->|"Time-sensitive,<br/>user-visible"| E["startForegroundService()<br/>+ notification within 5s"]
D -->|"Deferrable,<br/>constraint-dependent"| F["JobScheduler.schedule()"]
D -->|"Deferrable,<br/>needs guarantees"| G["WorkManager.enqueue()"]
D -->|"Exact time<br/>needed"| H["AlarmManager<br/>(with restrictions)"]
D -->|"Event-driven"| I["BroadcastReceiver<br/>(limited implicit)"]
style B fill:#fff3e0
style E fill:#e8f5e9
style F fill:#e8f5e9
style G fill:#e8f5e9
30.1.3 Foreground State Definition¶
The system defines an app as "in the foreground" if any of the following are true:
| Condition | Example |
|---|---|
| Has a visible Activity | App is open and on screen |
| Has a foreground Service | Music player, navigation, file upload |
| Connected via a foreground app | ContentProvider being used by a foreground app |
| Is in a temporary allowlist | Just received a high-priority FCM message |
The precise foreground/background state is tracked by ActivityManagerService
and its UidRecord for each application UID.
30.1.4 App Standby Buckets¶
Android 9.0 (Pie, API 28) introduced App Standby Buckets, which further tiered background restrictions based on how recently and frequently the user interacted with each app:
| Bucket | Criteria | Job Frequency | Alarm Frequency |
|---|---|---|---|
| Active | Currently in use or very recently used | No restrictions | No restrictions |
| Working Set | Used regularly, not currently active | Up to 2 hours deferred | Up to 6 minutes deferred |
| Frequent | Used often but not daily | Up to 8 hours deferred | Up to 30 minutes deferred |
| Rare | Rarely used | Up to 24 hours deferred | Up to 2 hours deferred |
| Restricted | Minimal user interaction, high battery use | Up to 24 hours, 1 job/day | Up to 24 hours deferred |
The bucket assignments are managed by UsageStatsManagerInternal and
AppStandbyInternal:
// Bucket indices defined in JobSchedulerService
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// JobSchedulerService.java
public static final int ACTIVE_INDEX = 0;
public static final int WORKING_INDEX = 1;
public static final int FREQUENT_INDEX = 2;
public static final int RARE_INDEX = 3;
public static final int NEVER_INDEX = 4;
public static final int RESTRICTED_INDEX = 5;
public static final int EXEMPTED_INDEX = 6;
30.1.5 Doze Mode and App Standby¶
Android 6.0 introduced Doze mode, which restricts background activity when the device is stationary, unplugged, and the screen is off for an extended period. Android 7.0 added a lighter "Doze on the go" that activates when the screen is off (even if the device is moving).
stateDiagram-v2
[*] --> Active: Screen on
Active --> Idle: Screen off +<br/>stationary +<br/>unplugged
Idle --> IdleMaintenance: Maintenance window
IdleMaintenance --> Idle: Window ends
Idle --> Active: Screen on /<br/>motion / charging
state Idle {
[*] --> LightDoze: Screen off
LightDoze --> DeepDoze: Extended idle
}
note right of Idle
In Doze:
- No network access
- No JobScheduler
- No syncs
- Alarms deferred
- No wakelocks
end note
During Doze, the system:
- Defers all alarms (except
setAlarmClock()and allowlisted exact alarms) - Blocks network access
- Suspends jobs and syncs
- Ignores wakelocks
- Periodically opens maintenance windows where deferred work can execute
30.1.6 Battery Saver¶
Battery Saver mode (user-activated or auto-triggered at low battery) adds further restrictions:
- Reduces background network access
- Defers jobs and alarms
- Reduces location accuracy
- Limits background CPU usage
- Restricts visual effects (animations, live wallpapers)
30.1.7 Historical Evolution of Background Restrictions¶
| Android Version | API | Key Restriction |
|---|---|---|
| 6.0 (Marshmallow) | 23 | Doze mode, App Standby |
| 7.0 (Nougat) | 24 | Doze on the go, limited implicit broadcasts |
| 8.0 (Oreo) | 26 | Background service limits, broadcast limits |
| 9.0 (Pie) | 28 | App Standby Buckets |
| 10 (Q) | 29 | Background activity launch restrictions |
| 11 (R) | 30 | Foreground service type requirements |
| 12 (S) | 31 | Exact alarm restrictions, foreground service launch from background |
| 12L | 32 | Further foreground service restrictions |
| 13 (T) | 33 | Per-app language, refined runtime permissions |
| 14 (U) | 34 | Foreground service types enforced, SCHEDULE_EXACT_ALARM restricted |
| 15 (V) | 35 | Further tightening of background task policies |
30.2 JobScheduler¶
JobScheduler is Android's primary mechanism for scheduling deferrable background work. Introduced in Android 5.0 (API 21), it allows apps to declare what work needs to be done and under what conditions, and the system decides when to run it. This enables the system to batch work, defer it to optimal times (e.g., when charging and on Wi-Fi), and enforce standby bucket quotas.
30.2.1 Architecture Overview¶
graph TD
subgraph "Application Process"
APP["App Code"] --> JS_CLIENT["JobScheduler<br/>(client proxy)"]
JOB_SERVICE["JobService<br/>(app-defined)"]
end
subgraph "System Server (JobScheduler APEX)"
JS_CLIENT -->|"Binder IPC<br/>schedule(JobInfo)"| JSS["JobSchedulerService"]
JSS --> JOB_STORE["JobStore<br/>(persistence)"]
JSS --> CONTROLLERS["StateControllers"]
JSS --> CONCURRENCY["JobConcurrencyManager"]
JSS --> PENDING["PendingJobQueue"]
CONTROLLERS --> CONN["ConnectivityController"]
CONTROLLERS --> TIME["TimeController"]
CONTROLLERS --> IDLE["IdleController"]
CONTROLLERS --> BATT["BatteryController"]
CONTROLLERS --> STORAGE["StorageController"]
CONTROLLERS --> QUOTA["QuotaController"]
CONTROLLERS --> FLEX["FlexibilityController"]
CONTROLLERS --> BG["BackgroundJobsController"]
CONTROLLERS --> DI["DeviceIdleJobsController"]
CONTROLLERS --> CONTENT["ContentObserverController"]
CONTROLLERS --> COMPONENT["ComponentController"]
CONTROLLERS --> PREFETCH["PrefetchController"]
CONCURRENCY -->|"Bind to service"| JOB_SERVICE
end
style JSS fill:#e1f5fe
style CONTROLLERS fill:#fff3e0
The key components all live in the JobScheduler APEX module:
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
30.2.2 JobSchedulerService¶
JobSchedulerService is the central coordinator. It extends SystemService
and implements two listener interfaces:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// JobSchedulerService.java
public class JobSchedulerService extends com.android.server.SystemService
implements StateChangedListener, JobCompletedListener {
public static final String TAG = "JobScheduler";
/** The maximum number of jobs that we allow an app to schedule */
private static final int MAX_JOBS_PER_APP = 150;
/** Master list of jobs. */
final JobStore mJobs;
/** List of controllers that will notify this service of updates to jobs. */
final List<StateController> mControllers;
/** Queue of pending jobs ready to execute. */
private final PendingJobQueue mPendingJobQueue = new PendingJobQueue();
/** Manages concurrent job execution slots. */
final JobConcurrencyManager mConcurrencyManager;
// ...
}
Key constants and limits:
- MAX_JOBS_PER_APP = 150: Each app can have at most 150 scheduled jobs
- NUM_COMPLETED_JOB_HISTORY = 20: The system keeps track of the 20 most recently completed jobs for debugging
30.2.3 JobInfo: Declaring Work and Constraints¶
Applications describe their jobs using JobInfo.Builder:
JobInfo jobInfo = new JobInfo.Builder(JOB_ID,
new ComponentName(context, MyJobService.class))
// Constraints
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
.setRequiresCharging(true)
.setRequiresDeviceIdle(true)
.setRequiresBatteryNotLow(true)
.setRequiresStorageNotLow(true)
// Timing
.setMinimumLatency(15 * 60 * 1000) // Don't run for at least 15 min
.setOverrideDeadline(60 * 60 * 1000) // Must run within 1 hour
.setPeriodic(24 * 60 * 60 * 1000) // Repeat every 24 hours
// Persistence
.setPersisted(true) // Survive reboots
// Content triggers
.addTriggerContentUri(
new JobInfo.TriggerContentUri(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
// Backoff policy
.setBackoffCriteria(30_000, JobInfo.BACKOFF_POLICY_EXPONENTIAL)
// Expedited job (API 31+)
.setExpedited(true)
// Estimated network usage
.setEstimatedNetworkBytes(
5 * 1024 * 1024, // 5 MB download
1024 * 1024) // 1 MB upload
.build();
// Schedule the job
JobScheduler scheduler = context.getSystemService(JobScheduler.class);
int result = scheduler.schedule(jobInfo);
// result == JobScheduler.RESULT_SUCCESS or RESULT_FAILURE
30.2.4 Constraint Types¶
The constraint system is the core of JobScheduler's value. Each constraint is
managed by a dedicated StateController:
| Constraint | Controller | Source File |
|---|---|---|
| Network type/connectivity | ConnectivityController |
controllers/ConnectivityController.java |
| Timing (delay, deadline, periodic) | TimeController |
controllers/TimeController.java |
| Device idle | IdleController |
controllers/IdleController.java |
| Charging / battery not low | BatteryController |
controllers/BatteryController.java |
| Storage not low | StorageController |
controllers/StorageController.java |
| Content URI changes | ContentObserverController |
controllers/ContentObserverController.java |
| Quota enforcement | QuotaController |
controllers/QuotaController.java |
| Flexibility | FlexibilityController |
controllers/FlexibilityController.java |
| Background restrictions | BackgroundJobsController |
controllers/BackgroundJobsController.java |
| Doze mode | DeviceIdleJobsController |
controllers/DeviceIdleJobsController.java |
| App component enabled | ComponentController |
controllers/ComponentController.java |
| Prefetch timing | PrefetchController |
controllers/PrefetchController.java |
All controller source files are at:
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/
30.2.5 StateController Architecture¶
All controllers extend the abstract StateController base class:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// controllers/StateController.java
public abstract class StateController {
protected final JobSchedulerService mService;
protected final StateChangedListener mStateChangedListener;
protected final Context mContext;
protected final Object mLock;
/**
* Implement the logic here to decide whether a job should be
* tracked by this controller.
*/
public abstract void maybeStartTrackingJobLocked(
JobStatus jobStatus, JobStatus lastJob);
/**
* Remove job from this controller's tracking.
*/
public abstract void maybeStopTrackingJobLocked(
JobStatus jobStatus, JobStatus lastJob);
/**
* Called when a controller's state changes to evaluate
* which jobs are now ready.
*/
public void evaluateStateLocked(JobStatus jobStatus) {}
}
Each controller tracks a set of jobs and maintains satisfaction bits on the
JobStatus object. When a controller's state changes (e.g., the device
connects to Wi-Fi), it notifies JobSchedulerService through the
StateChangedListener interface, which triggers a re-evaluation of pending
jobs.
30.2.6 JobStatus: Internal Job Representation¶
JobStatus is the internal representation of a scheduled job. It aggregates
the original JobInfo with runtime state tracked by the controllers:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// controllers/JobStatus.java
/**
* Uniquely identifies a job internally.
* Created from the public JobInfo object when it lands on the scheduler.
* Contains current state of the requirements of the job, as well as a
* function to evaluate whether it's ready to run.
*/
public final class JobStatus {
// Constraint satisfaction bits
static final int CONSTRAINT_CHARGING = 1 << 0;
static final int CONSTRAINT_BATTERY_NOT_LOW = 1 << 1;
static final int CONSTRAINT_STORAGE_NOT_LOW = 1 << 2;
static final int CONSTRAINT_TIMING_DELAY = 1 << 3;
static final int CONSTRAINT_DEADLINE = 1 << 4;
static final int CONSTRAINT_IDLE = 1 << 5;
static final int CONSTRAINT_CONNECTIVITY = 1 << 6;
static final int CONSTRAINT_CONTENT_TRIGGER = 1 << 7;
// ... more constraints
// The job is ready when all required constraints are satisfied
public boolean isReady() {
return isConstraintsSatisfied() && !isPending() && !isActive();
}
}
30.2.7 Job Scheduling Flow¶
sequenceDiagram
participant App as Application
participant JS as JobScheduler (client proxy)
participant JSS as JobSchedulerService
participant Store as JobStore
participant Controllers as StateControllers
participant Queue as PendingJobQueue
participant CM as ConcurrencyManager
participant Service as JobService (app process)
App->>JS: schedule(JobInfo)
JS->>JSS: schedule(JobInfo) [Binder IPC]
JSS->>JSS: Validate JobInfo<br/>(permissions, limits)
JSS->>Store: Add JobStatus
Store->>Store: Persist to XML<br/>(/data/system/job/jobs.xml)
JSS->>Controllers: maybeStartTrackingJobLocked()
Note over Controllers: Time passes...<br/>Constraints become satisfied
Controllers->>JSS: onControllerStateChanged()
JSS->>JSS: Evaluate ready jobs
JSS->>Queue: Add ready jobs
Queue->>CM: assignJobsToContexts()
CM->>Service: bindService() + onStartJob()
Service-->>CM: jobFinished(needsReschedule)
CM->>JSS: onJobCompleted()
JSS->>Store: Remove or reschedule
30.2.8 JobStore: Persistence¶
JobStore persists scheduled jobs to XML so they survive reboots:
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// JobStore.java
package com.android.server.job;
// Jobs are stored at /data/system/job/jobs.xml
// The format is XML with each job's constraints, timing, and metadata
The file is written atomically using AtomicFile to prevent corruption during
unexpected shutdowns. At boot, JobStore reads this file to restore all
persisted jobs (those created with setPersisted(true)).
30.2.9 ConnectivityController: Network Constraints¶
The ConnectivityController tracks network state and evaluates whether each
job's network constraint is satisfied:
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// controllers/ConnectivityController.java
package com.android.server.job.controllers;
// Tracks network types: NONE, ANY, UNMETERED, NOT_ROAMING, CELLULAR
// Uses ConnectivityManager.NetworkCallback to monitor changes
// Evaluates NetworkCapabilities for each job's requirements
The controller registers a NetworkCallback with ConnectivityManager and
re-evaluates all tracked jobs whenever the network state changes. It also
considers the app's data saver status and metered network policy.
30.2.10 QuotaController: Execution Quotas¶
The QuotaController enforces per-app execution time quotas based on standby
bucket assignments:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// controllers/QuotaController.java
// Quota windows and limits per standby bucket:
// Active: 10 min / 10 min window (effectively unlimited)
// Working: 10 min / 2 hour window
// Frequent: 10 min / 8 hour window
// Rare: 10 min / 24 hour window
// Restricted: 10 min / 24 hour window, max 1 job
The quota system tracks cumulative job execution time within rolling windows. When an app exceeds its quota, its jobs are deferred until the window rolls forward enough to make quota available again.
graph TD
subgraph "Quota Enforcement"
A["Job requests execution"] --> B{"Check standby<br/>bucket"}
B --> C["Active: 10min/10min"]
B --> D["Working: 10min/2h"]
B --> E["Frequent: 10min/8h"]
B --> F["Rare: 10min/24h"]
C --> G{Within quota?}
D --> G
E --> G
F --> G
G -->|Yes| H["Allow execution"]
G -->|No| I["Defer until quota<br/>replenishes"]
end
30.2.11 FlexibilityController¶
The FlexibilityController is a newer addition that manages the trade-off
between job freshness and system efficiency. It tracks which "flexible"
constraints (like connectivity type, charging state, idle state) a job could
optionally satisfy and adjusts satisfaction requirements based on how close the
job is to its deadline.
30.2.12 JobConcurrencyManager¶
The JobConcurrencyManager controls how many jobs can run simultaneously:
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// JobConcurrencyManager.java
package com.android.server.job;
// Manages a pool of JobServiceContext objects (execution slots)
// Balances between:
// - User-initiated jobs (highest priority)
// - Expedited jobs
// - Regular foreground jobs
// - Regular background jobs
// Total concurrent slots depend on device memory and CPU
The manager categorizes running jobs into work types:
| Work Type | Description | Priority |
|---|---|---|
| User-Initiated (UIJ) | Jobs triggered by direct user action | Highest |
| Expedited (EJ) | Time-sensitive jobs with quota limits | High |
| Foreground (FG) | Jobs from foreground apps | Medium |
| Background (BG) | Jobs from background apps | Lower |
| Background Restricted | Jobs from restricted-bucket apps | Lowest |
30.2.13 JobService: Application-Side Implementation¶
Applications implement JobService to receive and execute scheduled jobs:
public class MyJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
// Called on the main thread when the job should start
// Return true if work is ongoing (async)
// Return false if work is already complete
new Thread(() -> {
try {
doWork(params);
} finally {
// MUST call jobFinished when async work completes
jobFinished(params, false /* needsReschedule */);
}
}).start();
return true; // Work is asynchronous
}
@Override
public boolean onStopJob(JobParameters params) {
// Called when the system wants to stop the job
// (constraint no longer satisfied, or preempted)
// Return true to reschedule, false to drop
cancelOngoingWork();
return true; // Reschedule this job
}
}
The service must be declared in the manifest with the BIND_JOB_SERVICE
permission:
<service
android:name=".MyJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false" />
30.2.14 Restrictions and Thermal Throttling¶
The JobRestriction class allows the system to block job execution based on
device conditions beyond normal constraints. The current restriction is thermal
status:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// restrictions/ThermalStatusRestriction.java
// Blocks jobs when device is in thermal throttling state
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/restrictions/
30.2.15 Debugging JobScheduler¶
# Dump all scheduled jobs
adb shell dumpsys jobscheduler
# Dump jobs for a specific package
adb shell dumpsys jobscheduler | grep -A 20 "com.example.myapp"
# Force-run a specific job
adb shell cmd jobscheduler run -f com.example.myapp 42
# View job execution history
adb shell dumpsys jobscheduler | grep "completed"
# Check standby bucket for an app
adb shell am get-standby-bucket com.example.myapp
# Set standby bucket for testing
adb shell am set-standby-bucket com.example.myapp rare
30.3 AlarmManager¶
AlarmManager is the oldest background scheduling mechanism in Android,
predating JobScheduler by several years. It delivers time-based callbacks to
applications, either at a specific time or after a delay. Unlike JobScheduler,
AlarmManager is optimized for time accuracy rather than constraint
satisfaction.
30.3.1 Architecture¶
graph TD
subgraph "Application Process"
APP["App Code"] --> AM_CLIENT["AlarmManager<br/>(client proxy)"]
RECEIVER["BroadcastReceiver<br/>or AlarmListener"]
end
subgraph "System Server"
AM_CLIENT -->|"Binder IPC<br/>set(type, time, PI)"| AMS["AlarmManagerService"]
AMS --> ALARM_STORE["Alarm Store<br/>(in-memory)"]
AMS --> BATCHING["Alarm Batching<br/>Engine"]
AMS --> KERNEL["Kernel Timer<br/>(timerfd / RTC)"]
BATCHING --> POLICIES["Delivery Policies"]
POLICIES --> APP_STANDBY["App Standby Policy"]
POLICIES --> BATT_SAVER["Battery Saver Policy"]
POLICIES --> DOZE["Device Idle Policy"]
end
KERNEL -->|"Timer fires"| AMS
AMS -->|"Deliver alarm"| RECEIVER
style AMS fill:#e1f5fe
style BATCHING fill:#fff3e0
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
The AlarmManagerService is a large, complex service (5600+ lines) that has
evolved significantly over Android's history. Key imports reveal its
responsibilities:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/alarm/
// AlarmManagerService.java
package com.android.server.alarm;
// Key constants from the imports:
import static android.app.AlarmManager.ELAPSED_REALTIME;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
import static android.app.AlarmManager.RTC;
import static android.app.AlarmManager.RTC_WAKEUP;
import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
import static android.app.AlarmManager.FLAG_IDLE_UNTIL;
import static android.app.AlarmManager.FLAG_WAKE_FROM_IDLE;
import static android.app.AlarmManager.FLAG_PRIORITIZE;
30.3.2 Alarm Types¶
Android supports four alarm types based on two dimensions: time base and wake behavior:
| Type | Time Base | Wakes Device | Use Case |
|---|---|---|---|
RTC_WAKEUP |
Wall clock (UTC) | Yes | Calendar events, scheduled notifications |
RTC |
Wall clock (UTC) | No | Non-urgent time-based updates |
ELAPSED_REALTIME_WAKEUP |
Time since boot | Yes | Periodic checks, heartbeats |
ELAPSED_REALTIME |
Time since boot | No | UI timers, non-critical periodic work |
The _WAKEUP variants will wake the device from sleep (Doze) to deliver the
alarm. Non-wakeup alarms are deferred until the device is already awake.
30.3.3 Exact vs. Inexact Alarms¶
This is one of the most important distinctions in alarm scheduling:
Exact alarms fire at precisely the requested time. They are intended for user-facing commitments like calendar event notifications and medication reminders.
AlarmManager am = context.getSystemService(AlarmManager.class);
// Exact alarm -- fires at the precise time
am.setExact(AlarmManager.RTC_WAKEUP,
triggerTimeMillis,
pendingIntent);
// Exact alarm that can fire during Doze
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
triggerTimeMillis,
pendingIntent);
// Alarm clock (shown to user, always fires on time)
am.setAlarmClock(
new AlarmManager.AlarmClockInfo(triggerTimeMillis, showIntent),
pendingIntent);
Inexact alarms allow the system to batch the alarm with nearby alarms, reducing the number of device wake-ups:
// Inexact alarm -- system may batch with nearby alarms
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 30 * 60 * 1000,
pendingIntent);
// Inexact repeating alarm
am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 60 * 1000,
AlarmManager.INTERVAL_HALF_HOUR,
pendingIntent);
// Window alarm -- fires within a time window
am.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP,
triggerTime,
windowLength,
pendingIntent);
30.3.4 Exact Alarm Restrictions (API 31+)¶
Starting with Android 12 (API 31), exact alarms require the
SCHEDULE_EXACT_ALARM permission, which is a special permission the user can
revoke:
Starting with Android 14 (API 34), the permission is not granted by default for new installs. Apps must direct users to the special app access screen:
// Check if exact alarms are allowed
if (!alarmManager.canScheduleExactAlarms()) {
// Direct user to settings
Intent intent = new Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM);
startActivity(intent);
}
Exempt from the permission:
- Alarm clock apps (hold
SET_ALARMpermission + alarm clock role) - System apps
- Apps using
setAlarmClock()(always exempt, as they are user-visible)
The AlarmManagerService tracks exact alarm permission status:
// From AlarmManagerService.java imports
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_ALLOW_LIST;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_COMPAT;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_LISTENER;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_NOT_APPLICABLE;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_PERMISSION;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_POLICY_PERMISSION;
import static com.android.server.alarm.Alarm.EXACT_ALLOW_REASON_PRIORITIZED;
30.3.5 Alarm Batching¶
For inexact alarms, the system batches nearby alarms together to minimize wake-ups:
gantt
title Alarm Batching
dateFormat HH:mm
axisFormat %H:%M
section Without Batching
App A alarm :a1, 09:00, 1m
App B alarm :a2, 09:05, 1m
App C alarm :a3, 09:12, 1m
App D alarm :a4, 09:18, 1m
App E alarm :a5, 09:22, 1m
section With Batching
Batch 1 (A+B+C) :b1, 09:12, 3m
Batch 2 (D+E) :b2, 09:22, 2m
Instead of waking the device 5 times, batching reduces it to 2 wake-ups.
The system creates time windows and aligns inexact alarms to the same window
boundary. setWindow() gives the developer some control over the acceptable
window size.
30.3.6 Idle Dispatch¶
The AlarmManagerService supports idle dispatch alarms that only fire during
Doze maintenance windows:
// Flags from AlarmManagerService.java
FLAG_ALLOW_WHILE_IDLE // Can fire during Doze (with quotas)
FLAG_ALLOW_WHILE_IDLE_COMPAT // Compat mode for older targeting
FLAG_IDLE_UNTIL // Schedules the next Doze state transition
FLAG_WAKE_FROM_IDLE // Can wake device from Doze
FLAG_PRIORITIZE // Gets priority delivery
Apps that need to fire alarms during Doze can use setAndAllowWhileIdle() or
setExactAndAllowWhileIdle(), but these are rate-limited. The system limits
while-idle alarms to approximately once per 9 minutes per app to prevent abuse.
30.3.7 Alarm Delivery Policies¶
Multiple policy layers can defer alarm delivery:
flowchart TD
A["Alarm fires at<br/>requested time"] --> B{"Device Idle<br/>(Doze) Policy"}
B -->|In Doze, not exempt| C["Defer to next<br/>maintenance window"]
B -->|Not in Doze, or exempt| D{"Battery Saver<br/>Policy"}
D -->|"Battery Saver on,<br/>not exempt"| E["Defer alarm"]
D -->|OK| F{"App Standby<br/>Policy"}
F -->|Bucket limit exceeded| G["Defer alarm"]
F -->|Within quota| H{"Requester<br/>Policy"}
H -->|App-specific block| I["Defer alarm"]
H -->|OK| J["Deliver alarm<br/>to app"]
30.3.8 Listener-Based Alarms¶
Android 7.0 introduced AlarmManager.OnAlarmListener as an alternative to
PendingIntent for alarm delivery within the same process:
AlarmManager am = getSystemService(AlarmManager.class);
// Listener-based alarm (no PendingIntent needed)
am.set(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 5000,
"my-alarm-tag",
() -> {
// This runs on the specified handler's thread
Log.d("Alarm", "Alarm fired!");
},
handler);
Listener alarms are more efficient than PendingIntent alarms because they
avoid the IPC overhead of constructing and delivering a PendingIntent. However,
they only work while the app's process is alive.
30.3.9 AlarmManager vs. JobScheduler¶
| Feature | AlarmManager | JobScheduler |
|---|---|---|
| Time precision | High (exact/window) | Low (deferred to optimal time) |
| Constraint support | None (time only) | Rich (network, charging, idle, ...) |
| Batching | System-managed for inexact | System-managed always |
| Persistence | Not across reboots (except setAlarmClock) |
setPersisted(true) |
| Power efficiency | Lower (wake-ups) | Higher (batched, deferred) |
| Use case | Calendar events, alarms | Sync, upload, maintenance |
| API level | 1+ | 21+ |
Rule of thumb: Use AlarmManager only when you need to fire at a specific
time. For all other background work, use JobScheduler or WorkManager.
30.3.10 Debugging AlarmManager¶
# Dump all pending alarms
adb shell dumpsys alarm
# Dump alarms for a specific package
adb shell dumpsys alarm | grep -A 5 "com.example.myapp"
# Check exact alarm permission
adb shell appops get com.example.myapp SCHEDULE_EXACT_ALARM
# View alarm statistics
adb shell dumpsys alarm | grep -A 20 "Alarm Stats"
# Force all pending alarms to fire (testing only)
adb shell cmd alarm set-time <epoch_millis>
30.4 WorkManager¶
WorkManager is an AndroidX library (not part of AOSP proper) that provides a
high-level API for deferrable, guaranteed background work. It is the
recommended API for most background work in modern Android applications. It
abstracts over JobScheduler, AlarmManager, and other mechanisms to provide
a unified, backward-compatible solution.
30.4.1 Why WorkManager Exists¶
The fragmentation of background execution APIs creates a problem for app developers:
| API Level | Available Mechanism | Limitation |
|---|---|---|
| 1-13 | AlarmManager only |
No constraints, no batching |
| 14-20 | AlarmManager + custom services |
No constraint API |
| 21-22 | JobScheduler introduced |
Limited constraints |
| 23+ | JobScheduler + Doze |
Complex interaction |
| 26+ | Background service limits | Legacy code breaks |
WorkManager abstracts all of this:
flowchart TD
A["App: WorkManager.enqueue(WorkRequest)"] --> B{API Level?}
B -->|"API 23+"| C["Use JobScheduler"]
B -->|"API 14-22"| D["Use AlarmManager +<br/>BroadcastReceiver"]
C --> E["Work executes when<br/>constraints are met"]
D --> E
E --> F{Work succeeded?}
F -->|Yes| G["Mark complete"]
F -->|No, retryable| H["Reschedule with<br/>backoff policy"]
F -->|No, failed| I["Mark failed,<br/>notify observers"]
30.4.2 Core Concepts¶
WorkManager introduces several key abstractions:
WorkRequest: Defines a unit of work with optional constraints and scheduling:
// One-time work
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(UploadWorker.class)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.addTag("upload")
.setInputData(new Data.Builder()
.putString("file_path", "/sdcard/photo.jpg")
.build())
.build();
// Periodic work
PeriodicWorkRequest syncWork = new PeriodicWorkRequest.Builder(
SyncWorker.class, 1, TimeUnit.HOURS)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build();
Worker: The actual work implementation:
public class UploadWorker extends Worker {
public UploadWorker(Context context, WorkerParameters params) {
super(context, params);
}
@Override
public Result doWork() {
String filePath = getInputData().getString("file_path");
try {
uploadFile(filePath);
return Result.success();
} catch (IOException e) {
return Result.retry(); // Will be retried with backoff
}
}
}
Work chaining: Composing complex work graphs:
WorkManager.getInstance(context)
// First: download data (can run in parallel)
.beginWith(Arrays.asList(downloadWork1, downloadWork2))
// Then: process the downloaded data
.then(processWork)
// Finally: upload the result
.then(uploadWork)
.enqueue();
30.4.3 Architecture¶
graph TD
subgraph "Application Code"
APP["App"] --> WM["WorkManager.getInstance()"]
end
subgraph "WorkManager Library"
WM --> IMPL["WorkManagerImpl"]
IMPL --> PROCESSOR["Processor<br/>(runs Workers)"]
IMPL --> SCHEDULER["Schedulers"]
IMPL --> DB["WorkDatabase<br/>(Room / SQLite)"]
SCHEDULER --> GREEDY["GreedyScheduler<br/>(immediate work)"]
SCHEDULER --> SYSTEM["SystemJobScheduler<br/>(deferred work)"]
SCHEDULER --> ALARM["SystemAlarmScheduler<br/>(legacy fallback)"]
SYSTEM --> JS["JobScheduler<br/>(system service)"]
ALARM --> AM["AlarmManager<br/>(system service)"]
end
subgraph "System Services"
JS --> JSS["JobSchedulerService"]
AM --> AMS["AlarmManagerService"]
end
style IMPL fill:#e8f5e9
style DB fill:#fff3e0
Key architectural decisions:
-
Room database for persistence: WorkManager stores all work requests in a Room (SQLite) database. This ensures work survives process death and device reboots.
-
Scheduler delegation: On API 23+, WorkManager creates a
JobInfofor each deferredWorkRequestand delegates scheduling toJobScheduler. On older APIs, it usesAlarmManagerwithBroadcastReceiver. -
Greedy scheduler: For work with no constraints, WorkManager's
GreedySchedulerruns the work immediately without involvingJobScheduler. -
Observable work: WorkManager returns
LiveData<WorkInfo>orFlow<WorkInfo>that applications can observe to track work progress and completion.
30.4.4 WorkManager + JobScheduler Integration¶
When WorkManager delegates to JobScheduler, it creates a JobInfo that maps the WorkRequest's constraints:
| WorkManager Constraint | JobInfo Equivalent |
|---|---|
NetworkType.CONNECTED |
setRequiredNetworkType(NETWORK_TYPE_ANY) |
NetworkType.UNMETERED |
setRequiredNetworkType(NETWORK_TYPE_UNMETERED) |
requiresCharging() |
setRequiresCharging(true) |
requiresDeviceIdle() |
setRequiresDeviceIdle(true) |
requiresBatteryNotLow() |
setRequiresBatteryNotLow(true) |
requiresStorageNotLow() |
setRequiresStorageNotLow(true) |
WorkManager's SystemJobService (which extends JobService) receives the
callback from JobSchedulerService and dispatches it to the appropriate
Worker.
30.4.5 Work States¶
stateDiagram-v2
[*] --> ENQUEUED: enqueue
ENQUEUED --> RUNNING: Constraints met
RUNNING --> SUCCEEDED: Result.success
RUNNING --> FAILED: Result.failure
RUNNING --> ENQUEUED: Result.retry
ENQUEUED --> BLOCKED: depends on<br/>unfinished work
BLOCKED --> ENQUEUED: dependencies<br/>complete
ENQUEUED --> CANCELLED: cancel
RUNNING --> CANCELLED: cancel
BLOCKED --> CANCELLED: cancel
SUCCEEDED --> [*]
FAILED --> [*]
CANCELLED --> [*]
30.4.6 Unique Work¶
WorkManager supports unique work to prevent duplicate scheduling:
// Unique one-time work
WorkManager.getInstance(context).enqueueUniqueWork(
"sync-data", // Unique name
ExistingWorkPolicy.REPLACE, // KEEP, REPLACE, or APPEND
oneTimeWorkRequest);
// Unique periodic work
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic-sync",
ExistingPeriodicWorkPolicy.KEEP, // KEEP or UPDATE
periodicWorkRequest);
This prevents the common bug of scheduling duplicate periodic work every time the app launches.
30.4.7 Expedited Work¶
For time-sensitive work on API 31+, WorkManager supports expedited work that maps to JobScheduler's expedited jobs:
OneTimeWorkRequest urgentWork = new OneTimeWorkRequest.Builder(UrgentWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build();
WorkManager.getInstance(context).enqueue(urgentWork);
On API 31+, this uses JobInfo.Builder.setExpedited(true). On older APIs,
WorkManager falls back to a foreground service.
30.4.8 Long-Running Work¶
For work that may exceed the standard 10-minute execution limit:
public class LongUploadWorker extends CoroutineWorker {
@Override
public ForegroundInfo getForegroundInfo() {
return new ForegroundInfo(
NOTIFICATION_ID,
createNotification("Uploading..."),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
}
@Override
public Result doWork() {
setForeground(getForegroundInfo());
// This work can now run for an extended time
performLongUpload();
return Result.success();
}
}
By calling setForeground(), the worker gets promoted to a foreground service,
which is exempt from the standard execution time limit.
30.5 Foreground Services¶
Foreground services are services that perform work the user is actively aware of, indicated by a persistent notification in the notification shade. They are exempt from background execution limits and can run indefinitely, but they come with increasing restrictions in modern Android versions.
30.5.1 Creating a Foreground Service¶
public class MusicService extends Service {
private static final int NOTIFICATION_ID = 1;
private static final String CHANNEL_ID = "music_playback";
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Playing Music")
.setContentText("Artist - Song Title")
.setSmallIcon(R.drawable.ic_music_note)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build();
// Must call within 5 seconds of startForegroundService()
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
return START_STICKY;
}
}
Starting a foreground service:
// From Android 8.0+, use startForegroundService()
Intent serviceIntent = new Intent(context, MusicService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
30.5.2 Foreground Service Types (API 29+)¶
Android 10 introduced mandatory foreground service types. Android 14 (API 34)
made the foregroundServiceType attribute required in the manifest.
| Type | Constant | Use Case | Required Permission |
|---|---|---|---|
| Camera | camera |
Camera preview, video recording | FOREGROUND_SERVICE_CAMERA |
| Connected Device | connectedDevice |
Bluetooth, USB, companion device | FOREGROUND_SERVICE_CONNECTED_DEVICE |
| Data Sync | dataSync |
Cloud sync, backup | FOREGROUND_SERVICE_DATA_SYNC |
| Health | health |
Fitness tracking, heart rate | FOREGROUND_SERVICE_HEALTH |
| Location | location |
Navigation, location tracking | FOREGROUND_SERVICE_LOCATION + ACCESS_*_LOCATION |
| Media Playback | mediaPlayback |
Music, podcast, audio book | FOREGROUND_SERVICE_MEDIA_PLAYBACK |
| Media Projection | mediaProjection |
Screen capture, casting | FOREGROUND_SERVICE_MEDIA_PROJECTION |
| Microphone | microphone |
Voice recording, VoIP | FOREGROUND_SERVICE_MICROPHONE |
| Phone Call | phoneCall |
VoIP, calling apps | FOREGROUND_SERVICE_PHONE_CALL |
| Remote Messaging | remoteMessaging |
Messaging on companion device | FOREGROUND_SERVICE_REMOTE_MESSAGING |
| Short Service | shortService |
Brief user-initiated work (< 3 min) | None |
| Special Use | specialUse |
Cases not covered above | FOREGROUND_SERVICE_SPECIAL_USE |
| System Exempted | systemExempted |
System apps only | (System only) |
<!-- In AndroidManifest.xml -->
<service
android:name=".MusicService"
android:foregroundServiceType="mediaPlayback"
android:exported="false">
</service>
<!-- Required permission -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
30.5.3 Foreground Service Lifecycle¶
sequenceDiagram
participant App as Application
participant AMS as ActivityManagerService
participant NMS as NotificationManagerService
participant FGS as Foreground Service
App->>AMS: startForegroundService(intent)
AMS->>AMS: Create/start service
AMS->>AMS: Start 5-second timeout
AMS->>FGS: onStartCommand()
FGS->>AMS: startForeground(id, notification, type)
AMS->>AMS: Cancel 5-second timeout
AMS->>NMS: Post foreground notification
NMS->>NMS: Show persistent notification
Note over FGS: Service runs indefinitely<br/>with notification visible
FGS->>AMS: stopForeground(STOP_FOREGROUND_REMOVE)
AMS->>NMS: Remove notification
FGS->>AMS: stopSelf()
AMS->>FGS: onDestroy()
30.5.4 Background Start Restrictions (API 31+)¶
Android 12 introduced restrictions on starting foreground services from the
background. Apps cannot call startForegroundService() when they are in the
background unless they have a valid exemption:
| Exemption | Description |
|---|---|
| High-priority FCM | App received a high-priority push message |
| Alarm/AlarmClock | App's alarm just fired |
| Geofence/Activity transition | Location-related trigger |
| System broadcast | Certain system broadcasts (boot, locale change) |
SYSTEM_ALERT_WINDOW |
App has overlay permission |
| Companion device | App manages a companion device |
START_ACTIVITIES_FROM_BACKGROUND |
Privileged permission |
Apps without an exemption should use WorkManager with setExpedited() or
setForeground() instead.
30.5.5 Short Service (API 34+)¶
Android 14 introduced the shortService foreground service type for brief
user-initiated operations that need to run in the foreground but only for a
short time (under 3 minutes):
// Start a short foreground service
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
// System will call onTimeout() after approximately 3 minutes
@Override
public void onTimeout(int startId, int fgsType) {
// Must stop the service or upgrade to a longer type
stopSelf();
}
Short services have fewer permission requirements than other types, making them suitable for one-off operations like sending a message or processing a payment.
30.5.6 Data Sync Foreground Service Timeout (API 35+)¶
Starting with Android 15, dataSync foreground services have a timeout of
approximately 6 hours. After the timeout, the system calls onTimeout() and
the service must stop or convert to a different type. This prevents indefinite
data sync services that may have been abandoned by buggy code.
30.5.7 Foreground Service ANR¶
If a foreground service does not call startForeground() within 5 seconds of
startForegroundService(), the system generates a
ForegroundServiceDidNotStartInTimeException crash. On API 31+, this is a
ForegroundServiceStartNotAllowedException if the app attempts to start from
the background without an exemption.
flowchart TD
A["startForegroundService()"] --> B["5-second timer starts"]
B --> C{"startForeground()<br/>called in time?"}
C -->|Yes| D["Service runs normally"]
C -->|No| E["ANR / crash:<br/>ForegroundServiceDidNotStartInTimeException"]
30.5.8 Notification Requirements¶
Every foreground service must have a visible notification:
// Create notification channel (required API 26+)
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"Background Service",
NotificationManager.IMPORTANCE_LOW); // LOW = no sound
channel.setDescription("Shows when the service is active");
notificationManager.createNotificationChannel(channel);
// Build the notification
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Syncing Data")
.setContentText("Uploading 42 files...")
.setSmallIcon(R.drawable.ic_sync)
.setOngoing(true) // Cannot be swiped away
.setProgress(100, 42, false) // Show progress bar
.addAction(R.drawable.ic_cancel, "Cancel",
cancelPendingIntent) // User can cancel
.build();
The notification serves a dual purpose: informing the user about ongoing work, and providing accountability -- if a user sees an unexpected notification, they know which app is consuming resources and can stop it.
30.5.9 User-Visible Foreground Service Notifications¶
Starting with Android 13 (API 33), users can long-press the foreground service notification to stop the service directly. This gives users control over misbehaving apps without needing to navigate to Settings.
30.6 Broadcast Restrictions¶
Broadcasts are the event system of Android: they notify apps about system events (network changes, battery state, screen on/off) and enable inter-app communication. However, implicit broadcasts -- those sent to any registered receiver rather than a specific component -- are a major source of background wake-ups.
30.6.1 The Implicit Broadcast Problem¶
Consider ACTION_CONNECTIVITY_CHANGE. Before Android 7.0, every app that
registered a manifest receiver for this broadcast was woken up every time the
network changed. With hundreds of apps installed, a single Wi-Fi toggle could
launch 50+ application processes simultaneously.
graph TD
EVENT["Wi-Fi Connected"] --> SYSTEM["System sends<br/>ACTION_CONNECTIVITY_CHANGE"]
SYSTEM --> APP1["App 1 wakes up"]
SYSTEM --> APP2["App 2 wakes up"]
SYSTEM --> APP3["App 3 wakes up"]
SYSTEM --> APP4["App 4 wakes up"]
SYSTEM --> DOTS["... 46 more apps ..."]
SYSTEM --> APP50["App 50 wakes up"]
style EVENT fill:#ffcdd2
style SYSTEM fill:#ffcdd2
30.6.2 Broadcast Restrictions Timeline¶
| Version | API | Restriction |
|---|---|---|
| 7.0 | 24 | CONNECTIVITY_ACTION no longer delivered to manifest receivers |
| 7.0 | 24 | ACTION_NEW_PICTURE, ACTION_NEW_VIDEO restricted |
| 8.0 | 26 | Most implicit broadcasts blocked for manifest receivers |
| 8.0 | 26 | Explicit exceptions list for essential broadcasts |
30.6.3 Explicit vs. Implicit Broadcasts¶
Implicit broadcasts target any receiver that matches the intent filter:
<!-- This manifest receiver NO LONGER WORKS for most broadcasts (API 26+) -->
<receiver android:name=".NetworkChangeReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
Explicit broadcasts target a specific component:
// This still works -- explicit broadcast to a specific receiver
Intent intent = new Intent(context, MyReceiver.class);
intent.setAction("com.example.MY_ACTION");
context.sendBroadcast(intent);
30.6.4 Exempt Broadcasts¶
Certain system broadcasts are exempt from the implicit broadcast restriction because they are essential for correct app behavior. The exempt list includes:
| Broadcast | Reason for Exemption |
|---|---|
ACTION_BOOT_COMPLETED |
Apps need to restore alarms/state after boot |
ACTION_LOCALE_CHANGED |
Apps need to refresh locale-dependent caches |
ACTION_MY_PACKAGE_REPLACED |
App needs to know when it was updated |
ACTION_PACKAGE_ADDED/REMOVED |
Package management |
ACTION_TIMEZONE_CHANGED |
Time-sensitive apps need immediate notification |
ACTION_TIME_SET |
Similar to timezone changes |
USB_DEVICE_ATTACHED |
USB accessory apps |
ACTION_HEADSET_PLUG |
Audio apps need to know about audio routing |
ACTION_CONNECTION_STATE_CHANGED (Bluetooth) |
Companion device apps |
30.6.5 Alternatives to Implicit Broadcasts¶
For each restricted implicit broadcast, Android provides a recommended alternative:
| Restricted Broadcast | Alternative |
|---|---|
CONNECTIVITY_CHANGE |
JobScheduler with setRequiredNetworkType() or ConnectivityManager.registerNetworkCallback() |
ACTION_POWER_CONNECTED/DISCONNECTED |
JobScheduler with setRequiresCharging() |
ACTION_NEW_PICTURE/VIDEO |
JobScheduler with addTriggerContentUri() for MediaStore |
| Custom periodic wake-ups | WorkManager with PeriodicWorkRequest |
30.6.6 Context-Registered Receivers¶
While manifest-registered receivers are restricted for implicit broadcasts,
context-registered receivers (registered with registerReceiver()) are not:
// Context-registered receivers still work for all broadcasts
// But only while the app process is alive
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Handle connectivity change
}
};
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
registerReceiver(receiver, filter);
// Must unregister to avoid leaks
unregisterReceiver(receiver);
The key difference: context-registered receivers only work while the app is already running. They do not cause the app to be launched from a stopped state. This is the core of the restriction's effectiveness -- it prevents broadcast storms from launching dozens of dormant apps.
30.6.7 Ordered and Sticky Broadcasts¶
Android also supports ordered broadcasts (where receivers process the broadcast in priority order and can abort it) and sticky broadcasts (deprecated). The restriction rules apply equally to all broadcast types -- the restriction is about the receiver registration mechanism (manifest vs. context), not the broadcast type.
30.6.8 Protected Broadcasts¶
System services can declare broadcasts as "protected", meaning only the system can send them:
<!-- In the framework's AndroidManifest.xml -->
<protected-broadcast android:name="android.intent.action.BOOT_COMPLETED" />
<protected-broadcast android:name="android.intent.action.LOCALE_CHANGED" />
If a non-system app tries to send a protected broadcast, the system logs a warning and may reject it.
30.7 Try It¶
This section provides hands-on exercises to explore Android's background task scheduling infrastructure.
30.7.1 Exercise: Inspect JobScheduler State¶
# Dump all scheduled jobs
adb shell dumpsys jobscheduler
# The output includes:
# - Registered jobs (per-app, with constraints)
# - Currently running jobs
# - Pending job queue
# - Controller states
# - Quota usage per standby bucket
# - Recent execution history
# Look at registered jobs for a specific package
adb shell dumpsys jobscheduler | grep -B 2 -A 20 "com.google.android.gms"
# Check standby buckets for all apps
adb shell dumpsys usagestats | grep -A 2 "bucket"
# View job execution timeline
adb shell dumpsys jobscheduler | grep "Job history"
30.7.2 Exercise: Schedule and Monitor a Job¶
Create a simple job:
public class DemoJobService extends JobService {
private static final String TAG = "DemoJob";
@Override
public boolean onStartJob(JobParameters params) {
Log.d(TAG, "Job started: id=" + params.getJobId()
+ " network=" + params.getNetwork());
// Simulate async work
new Thread(() -> {
try {
Thread.sleep(5000); // 5 seconds of work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Log.d(TAG, "Job completed: id=" + params.getJobId());
jobFinished(params, false);
}).start();
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
Log.d(TAG, "Job stopped: id=" + params.getJobId()
+ " reason=" + params.getStopReason());
return true; // Reschedule
}
}
Schedule it:
ComponentName service = new ComponentName(this, DemoJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(1001, service)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setRequiresCharging(false)
.setMinimumLatency(5000) // 5 seconds minimum delay
.setOverrideDeadline(30000); // 30 seconds maximum delay
JobScheduler scheduler = getSystemService(JobScheduler.class);
int result = scheduler.schedule(builder.build());
Log.d("Schedule", "Result: " + (result == JobScheduler.RESULT_SUCCESS
? "SUCCESS" : "FAILURE"));
Force-run it from adb:
# Force run the job immediately (bypass constraints)
adb shell cmd jobscheduler run -f com.example.myapp 1001
# Watch logcat for the job output
adb logcat -s DemoJob:D
# Check the job's current state
adb shell dumpsys jobscheduler | grep -A 15 "com.example.myapp"
30.7.3 Exercise: Test Alarm Scheduling¶
public class AlarmDemoActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AlarmManager am = getSystemService(AlarmManager.class);
// 1. Inexact alarm (batched by system)
PendingIntent inexactPi = PendingIntent.getBroadcast(this, 0,
new Intent(this, AlarmReceiver.class).setAction("INEXACT"),
PendingIntent.FLAG_IMMUTABLE);
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + 60_000, inexactPi);
// 2. Exact alarm (requires SCHEDULE_EXACT_ALARM on API 31+)
if (am.canScheduleExactAlarms()) {
PendingIntent exactPi = PendingIntent.getBroadcast(this, 1,
new Intent(this, AlarmReceiver.class).setAction("EXACT"),
PendingIntent.FLAG_IMMUTABLE);
am.setExact(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis() + 30_000, exactPi);
}
// 3. Alarm clock (always fires on time, shows in status bar)
PendingIntent clockPi = PendingIntent.getActivity(this, 2,
new Intent(this, AlarmDemoActivity.class),
PendingIntent.FLAG_IMMUTABLE);
PendingIntent alarmPi = PendingIntent.getBroadcast(this, 3,
new Intent(this, AlarmReceiver.class).setAction("ALARM_CLOCK"),
PendingIntent.FLAG_IMMUTABLE);
am.setAlarmClock(
new AlarmManager.AlarmClockInfo(
System.currentTimeMillis() + 120_000, clockPi),
alarmPi);
Log.d("AlarmDemo", "Alarms scheduled");
}
}
Monitor from adb:
# View all pending alarms
adb shell dumpsys alarm | head -100
# View alarms for your package
adb shell dumpsys alarm | grep -A 5 "com.example.myapp"
# View alarm statistics
adb shell dumpsys alarm | grep -A 30 "Alarm Stats"
# View alarm clock (shown to user)
adb shell dumpsys alarm | grep "AlarmClock"
30.7.4 Exercise: WorkManager Chain¶
// Step 1: Create workers
public class DownloadWorker extends Worker {
@Override public Result doWork() {
String url = getInputData().getString("url");
Log.d("WM", "Downloading: " + url);
// Simulate download
try { Thread.sleep(2000); } catch (Exception e) {}
return Result.success(new Data.Builder()
.putString("local_path", "/data/downloaded_file")
.build());
}
}
public class ProcessWorker extends Worker {
@Override public Result doWork() {
String path = getInputData().getString("local_path");
Log.d("WM", "Processing: " + path);
try { Thread.sleep(3000); } catch (Exception e) {}
return Result.success(new Data.Builder()
.putString("processed_path", "/data/processed_file")
.build());
}
}
public class UploadWorker extends Worker {
@Override public Result doWork() {
String path = getInputData().getString("processed_path");
Log.d("WM", "Uploading: " + path);
try { Thread.sleep(2000); } catch (Exception e) {}
return Result.success();
}
}
// Step 2: Build and enqueue the chain
OneTimeWorkRequest download = new OneTimeWorkRequest.Builder(DownloadWorker.class)
.setInputData(new Data.Builder().putString("url", "https://example.com/data").build())
.addTag("pipeline")
.build();
OneTimeWorkRequest process = new OneTimeWorkRequest.Builder(ProcessWorker.class)
.addTag("pipeline")
.build();
OneTimeWorkRequest upload = new OneTimeWorkRequest.Builder(UploadWorker.class)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.addTag("pipeline")
.build();
WorkManager.getInstance(context)
.beginWith(download)
.then(process)
.then(upload)
.enqueue();
// Step 3: Observe the chain
WorkManager.getInstance(context)
.getWorkInfosByTagLiveData("pipeline")
.observe(this, workInfos -> {
for (WorkInfo info : workInfos) {
Log.d("WM", "Work " + info.getId()
+ " state=" + info.getState()
+ " progress=" + info.getProgress());
}
});
30.7.5 Exercise: Test Doze Mode¶
# Put device into Doze mode (screen must be off)
adb shell dumpsys deviceidle enable
# Force device into light Doze
adb shell dumpsys deviceidle force-idle light
# Force device into deep Doze
adb shell dumpsys deviceidle force-idle deep
# Check Doze state
adb shell dumpsys deviceidle get deep
adb shell dumpsys deviceidle get light
# Step through Doze states
adb shell dumpsys deviceidle step light
adb shell dumpsys deviceidle step deep
# Observe: your scheduled jobs and alarms will be deferred
# Exit Doze
adb shell dumpsys deviceidle unforce
adb shell dumpsys deviceidle disable
# Check which apps are whitelisted from Doze
adb shell dumpsys deviceidle whitelist
30.7.6 Exercise: Test App Standby Buckets¶
# Check current bucket for an app
adb shell am get-standby-bucket com.example.myapp
# Set bucket for testing
adb shell am set-standby-bucket com.example.myapp active
adb shell am set-standby-bucket com.example.myapp working_set
adb shell am set-standby-bucket com.example.myapp frequent
adb shell am set-standby-bucket com.example.myapp rare
adb shell am set-standby-bucket com.example.myapp restricted
# Observe the effect on your scheduled jobs
adb shell dumpsys jobscheduler | grep -A 10 "com.example.myapp"
# Reset to automatic bucket assignment
adb shell am reset-standby-bucket com.example.myapp
30.7.7 Exercise: Test Background Restrictions¶
# Restrict background for an app (simulates Battery Saver per-app restriction)
adb shell cmd appops set com.example.myapp RUN_IN_BACKGROUND deny
adb shell cmd appops set com.example.myapp RUN_ANY_IN_BACKGROUND deny
# Observe: background services will be stopped, jobs deferred
# Reset
adb shell cmd appops set com.example.myapp RUN_IN_BACKGROUND allow
adb shell cmd appops set com.example.myapp RUN_ANY_IN_BACKGROUND allow
# Simulate Battery Saver mode
adb shell settings put global low_power 1
# Observe the effect on pending jobs
adb shell dumpsys jobscheduler | grep "Ready"
# Disable Battery Saver
adb shell settings put global low_power 0
30.7.8 Exercise: Foreground Service with Type¶
public class LocationTrackingService extends Service {
private static final int NOTIFICATION_ID = 100;
private LocationManager locationManager;
private LocationListener locationListener;
@Override
public void onCreate() {
super.onCreate();
locationManager = getSystemService(LocationManager.class);
locationListener = location -> {
Log.d("LocationFGS", "Location: " + location.getLatitude()
+ ", " + location.getLongitude());
};
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = new NotificationCompat.Builder(
this, "location_channel")
.setContentTitle("Tracking Location")
.setContentText("Your location is being tracked")
.setSmallIcon(R.drawable.ic_location)
.setOngoing(true)
.build();
// Specify the foreground service type
startForeground(NOTIFICATION_ID, notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION);
// Now we can request location updates in the background
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 5000, 10, locationListener);
return START_STICKY;
}
@Override
public void onDestroy() {
locationManager.removeUpdates(locationListener);
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
Required manifest entries:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<service
android:name=".LocationTrackingService"
android:foregroundServiceType="location"
android:exported="false" />
Monitor foreground services:
# List running foreground services
adb shell dumpsys activity services | grep "isForeground=true"
# Detailed service info
adb shell dumpsys activity services com.example.myapp
# Check foreground service types in use
adb shell dumpsys activity services | grep "foregroundServiceType"
30.7.9 Exercise: Observe Broadcast Restrictions¶
// This manifest receiver will NOT work on API 26+ for most implicit broadcasts
// <receiver android:name=".ConnectivityReceiver">
// <intent-filter>
// <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
// </intent-filter>
// </receiver>
// Instead, register at runtime:
public class MainActivity extends Activity {
private BroadcastReceiver connectivityReceiver;
@Override
protected void onResume() {
super.onResume();
connectivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("Broadcast", "Network changed: " + intent.getAction());
}
};
registerReceiver(connectivityReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
@Override
protected void onPause() {
super.onPause();
if (connectivityReceiver != null) {
unregisterReceiver(connectivityReceiver);
connectivityReceiver = null;
}
}
}
// Or better yet, use the modern approach:
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
cm.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
Log.d("Network", "Connected: " + network);
}
@Override
public void onLost(Network network) {
Log.d("Network", "Disconnected: " + network);
}
});
30.7.10 Exercise: Build a Complete Background Task Solution¶
Combine all the concepts into a robust background sync solution:
// 1. Define the worker
public class DataSyncWorker extends CoroutineWorker {
@Override
public ForegroundInfo getForegroundInfo() {
return createForegroundInfo("Syncing data...");
}
@Override
public Result doWork() {
// Report progress
setProgress(new Data.Builder()
.putInt("progress", 0)
.build());
try {
// Step 1: Download updates
List<Update> updates = downloadUpdates();
setProgress(new Data.Builder().putInt("progress", 33).build());
// Step 2: Apply updates locally
applyUpdates(updates);
setProgress(new Data.Builder().putInt("progress", 66).build());
// Step 3: Upload local changes
uploadChanges();
setProgress(new Data.Builder().putInt("progress", 100).build());
return Result.success();
} catch (IOException e) {
Log.e("Sync", "Sync failed", e);
if (getRunAttemptCount() < 3) {
return Result.retry();
} else {
return Result.failure(new Data.Builder()
.putString("error", e.getMessage())
.build());
}
}
}
}
// 2. Schedule periodic sync
public class SyncScheduler {
public static void schedulePeriodicSync(Context context) {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build();
PeriodicWorkRequest syncRequest =
new PeriodicWorkRequest.Builder(
DataSyncWorker.class,
1, TimeUnit.HOURS, // Repeat every hour
15, TimeUnit.MINUTES) // Flex window: 15 min
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.addTag("periodic-sync")
.build();
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"data-sync",
ExistingPeriodicWorkPolicy.KEEP,
syncRequest);
}
// One-time immediate sync (user-initiated)
public static void syncNow(Context context) {
OneTimeWorkRequest syncRequest =
new OneTimeWorkRequest.Builder(DataSyncWorker.class)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.addTag("immediate-sync")
.build();
WorkManager.getInstance(context).enqueueUniqueWork(
"immediate-sync",
ExistingWorkPolicy.REPLACE,
syncRequest);
}
}
// 3. Observe sync status in UI
public class SyncStatusFragment extends Fragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
WorkManager.getInstance(requireContext())
.getWorkInfosForUniqueWorkLiveData("data-sync")
.observe(getViewLifecycleOwner(), workInfos -> {
if (workInfos == null || workInfos.isEmpty()) return;
WorkInfo info = workInfos.get(0);
switch (info.getState()) {
case RUNNING:
int progress = info.getProgress().getInt("progress", 0);
showProgress(progress);
break;
case SUCCEEDED:
showSuccess();
break;
case FAILED:
String error = info.getOutputData().getString("error");
showError(error);
break;
case ENQUEUED:
showWaiting();
break;
}
});
}
}
30.7.11 Summary: Choosing the Right API¶
flowchart TD
A["I need to do<br/>background work"] --> B{"Is it time-critical<br/>and user-facing?"}
B -->|"Yes (alarm clock,<br/>calendar reminder)"| C["AlarmManager<br/>setAlarmClock()"]
B -->|No| D{"Must it run at<br/>a specific time?"}
D -->|Yes| E["AlarmManager<br/>setExact()"]
D -->|No| F{"Is it deferrable<br/>and constraint-dependent?"}
F -->|Yes| G{Using AndroidX?}
G -->|Yes| H["WorkManager<br/>(recommended)"]
G -->|No| I["JobScheduler<br/>(direct)"]
F -->|No| J{"Is the user<br/>actively aware?"}
J -->|"Yes (music, nav,<br/>call, upload)"| K["Foreground Service<br/>with notification"]
J -->|No| L{"Is it brief<br/>(< 10 min)?"}
L -->|Yes| H
L -->|No| M["Foreground Service<br/>or split into<br/>shorter work units"]
style H fill:#c8e6c9
style C fill:#fff9c4
style K fill:#bbdefb
30.7.12 Summary of Key Source Paths¶
| Component | Source Path |
|---|---|
| JobSchedulerService | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java |
| JobStore | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobStore.java |
| JobConcurrencyManager | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java |
| JobServiceContext | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java |
| StateController (base) | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java |
| JobStatus | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java |
| ConnectivityController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java |
| TimeController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/TimeController.java |
| QuotaController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java |
| FlexibilityController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java |
| BatteryController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java |
| IdleController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java |
| BackgroundJobsController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java |
| DeviceIdleJobsController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java |
| ContentObserverController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/ContentObserverController.java |
| ThermalStatusRestriction | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java |
| AlarmManagerService | frameworks/base/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java |
| JobSchedulerInternal | frameworks/base/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java |
Key takeaways from this chapter:
-
Background limits are pervasive: Starting with Android 8.0, apps cannot freely run background services. Every version since has added further restrictions. Apps must design around these limits from the start.
-
JobScheduler is the platform primitive: All constraint-based background work should go through JobScheduler (directly or via WorkManager). The controller architecture with its 12+ StateControllers provides a flexible, extensible constraint evaluation system.
-
AlarmManager is for time-specific work only: Use it for alarm clocks, calendar reminders, and other user-facing timed events. Exact alarm permissions restrict access to prevent abuse.
-
WorkManager is the recommended abstraction: For AndroidX apps, WorkManager provides persistence, chaining, observability, and backward compatibility on top of JobScheduler and AlarmManager.
-
Foreground services require types and notifications: Modern Android enforces typed foreground services with corresponding permissions. The notification serves as both user information and accountability.
-
Broadcast restrictions prevent wake-up storms: Manifest-registered receivers for implicit broadcasts are mostly blocked. Apps must use context-registered receivers, JobScheduler content triggers, or ConnectivityManager callbacks instead.
-
Standby buckets tier everything: An app's background execution budget depends on its standby bucket, which reflects user engagement. Active apps get more budget; rarely-used apps are heavily restricted.