Chapter 30: Background Task Scheduling¶
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 | dataSync foreground service 6 hour timeout |
| 16 | 36 | User-initiated job (UIJ) notifications centralized, UIJ notification dismissal restricted |
| 17 | 37 | New getPendingJobReasons*() diagnostics, abandoned-job detection, per-network connectivity batching, Perfetto job tracing, start-user-before-alarm |
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 default maximum number of jobs that we allow an app to schedule */
private static final int DEFAULT_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:
- DEFAULT_MAX_JOBS_PER_APP = 150: Each app can have at most 150 scheduled
jobs by default. The actual ceiling is passed into the constructor, so it is
configurable rather than a hard
MAX_JOBS_PER_APPconstant. - NUM_COMPLETED_JOB_HISTORY = 20: The system keeps track of the 20 most recently completed jobs for debugging
The constructor also wires in a JobPerfettoTracer (see §30.7) so that each
job's lifecycle can be emitted to a Perfetto trace:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// JobSchedulerService.java
this(context, DEFAULT_MAX_JOBS_PER_APP, null, JobPerfettoTracer.create());
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/
Some controllers (BatteryController, ConnectivityController,
IdleController) extend RestrictingController rather than StateController
directly. RestrictingController adds two hooks --
startTrackingRestrictedJobLocked() and stopTrackingRestrictedJobLocked() --
so that those controllers can also track jobs whose owning app is in the
RESTRICTED standby bucket, where the constraint must hold even more strictly.
The idle-detection plumbing for IdleController lives in the
controllers/idle/ subpackage (DeviceIdlenessTracker, CarIdlenessTracker,
and the IdlenessTracker/IdlenessListener interfaces), which lets the same
controller use a different definition of "idle" on a handheld versus an
automotive device.
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 {
// Explicit constraints share the JobInfo.CONSTRAINT_FLAG_* low bits
public static final int CONSTRAINT_CHARGING = JobInfo.CONSTRAINT_FLAG_CHARGING; // 1 << 0
public static final int CONSTRAINT_BATTERY_NOT_LOW =
JobInfo.CONSTRAINT_FLAG_BATTERY_NOT_LOW; // 1 << 1
public static final int CONSTRAINT_IDLE = JobInfo.CONSTRAINT_FLAG_DEVICE_IDLE; // 1 << 2
public static final int CONSTRAINT_STORAGE_NOT_LOW =
JobInfo.CONSTRAINT_FLAG_STORAGE_NOT_LOW; // 1 << 3
// The remaining explicit constraints occupy the high bits
public static final int CONSTRAINT_TIMING_DELAY = 1 << 31;
public static final int CONSTRAINT_DEADLINE = 1 << 30;
public static final int CONSTRAINT_CONNECTIVITY = 1 << 28;
public static final int CONSTRAINT_CONTENT_TRIGGER = 1 << 26;
// Implicit constraints the system adds on top of what the app requested
public static final int CONSTRAINT_DEVICE_NOT_DOZING = 1 << 25; // Implicit
public static final int CONSTRAINT_WITHIN_QUOTA = 1 << 24; // Implicit
public static final int CONSTRAINT_PREFETCH = 1 << 23;
public static final int CONSTRAINT_BACKGROUND_NOT_RESTRICTED = 1 << 22; // Implicit
public static final int CONSTRAINT_FLEXIBLE = 1 << 21; // Implicit
// The job is ready when all required constraints are satisfied
public boolean isReady() {
return isReady(mSatisfiedConstraintsOfInterest);
}
}
The low four bits (CONSTRAINT_CHARGING, CONSTRAINT_BATTERY_NOT_LOW,
CONSTRAINT_IDLE, CONSTRAINT_STORAGE_NOT_LOW) are shared with the
JobInfo.CONSTRAINT_FLAG_* values an app sets directly; the high bits hold the
timing, connectivity, and content-trigger constraints plus the implicit
constraints (CONSTRAINT_DEVICE_NOT_DOZING, CONSTRAINT_WITHIN_QUOTA,
CONSTRAINT_BACKGROUND_NOT_RESTRICTED, CONSTRAINT_FLEXIBLE) that the system
layers on regardless of what the app asked for. This is why a job that only set
a network constraint can still sit pending: doze, quota, and background
restrictions are constraints too, evaluated by their own controllers.
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
// Legacy 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 10 jobs
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.
These numbers are defaults, not constants. QuotaController.QcConstants reads
every quota value from a DeviceConfig namespace, so Google can tune them per
release without a framework change. The source carries three generations of
defaults side by side -- DEFAULT_LEGACY_*, DEFAULT_CURRENT_*, and
DEFAULT_LATEST_* -- and the active set is chosen at runtime. In the Android 17
tree the "current"/"latest" defaults relax the windows noticeably compared to
the legacy values:
| Bucket | Legacy allowed / window | Current/latest allowed / window |
|---|---|---|
| Active | 10 min / 10 min | 20 min / 60 min |
| Working | 10 min / 2 h | 10 min / 4 h |
| Frequent | 10 min / 8 h | 10 min / 12 h |
| Rare | 10 min / 24 h | 10 min / 24 h |
| Restricted | 10 min / 24 h, 10 jobs | 10 min / 24 h, 10 jobs |
Expedited jobs (EJs) get a separate budget tracked in the same controller, with
its own per-bucket limits inside a rolling 24 hour window (for example, 30 min
for ACTIVE, 15 min for WORKING, 10 min for FREQUENT/RARE, 5 min for
RESTRICTED), so a burst of expedited work does not consume the regular job
quota.
graph TD
subgraph "Quota Enforcement"
A["Job requests execution"] --> B{"Check standby<br/>bucket"}
B --> C["Active<br/>(largest window)"]
B --> D["Working"]
B --> E["Frequent"]
B --> F["Rare / Restricted<br/>(smallest budget)"]
C --> G{"Within bucket's<br/>allowed-time and<br/>job-count 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 manages the trade-off between job freshness and
system efficiency. It defines a set of FLEXIBLE_CONSTRAINTS -- the system-wide
ones are charging, battery-not-low, and device-idle, plus the job-specific
connectivity constraint -- and treats them as soft preferences early in a
job's life. A freshly scheduled job is initially asked to satisfy all of them
(so it runs at the most efficient moment, e.g. charging on Wi-Fi while idle),
but as the job approaches its deadline the controller progressively drops these
flexible constraints until, by the fallback deadline, none of them are required
and the job can run regardless of device state. Jobs with no explicit deadline
fall back to FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS. This is what
lets JobScheduler hold low-urgency work for an opportune moment without ever
letting it starve.
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 reserved slots across the WORK_TYPE_* categories
// Total concurrent slots depend on device memory and CPU
The manager categorizes running jobs into the WORK_TYPE_* bitset and reserves
a number of concurrent execution slots for each, so that lower-priority work
cannot starve higher-priority work:
| Work Type Constant | Meaning |
|---|---|
WORK_TYPE_TOP |
Job for an app in the TOP process state (active user) |
WORK_TYPE_FGS |
Job for an app at foreground-service state or higher |
WORK_TYPE_UI |
Job allowed to run as a user-initiated job (UIJ) |
WORK_TYPE_EJ |
Job allowed to run as an expedited job |
WORK_TYPE_BG |
Plain background job for the active user |
WORK_TYPE_BGUSER_IMPORTANT |
FGS/EJ/UIJ job for a fully backgrounded user |
WORK_TYPE_BGUSER |
Plain background job for a fully backgrounded user |
The two BGUSER types are how concurrency is split between the currently active
user and other (background) users on a multi-user device, so jobs for a
background user cannot crowd out the foreground user's jobs.
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
ThermalStatusRestriction extends the abstract JobRestriction. When the
device crosses thermal thresholds it stops affected jobs with
INTERNAL_STOP_REASON_DEVICE_THERMAL. In the Android 17 tree it reports a
specific pending reason -- PENDING_JOB_REASON_DEVICE_STATE_THERMAL -- instead
of the older generic PENDING_JOB_REASON_DEVICE_STATE, so apps querying why a
job is stuck can now tell thermal throttling apart from other device-state
blocks (see §30.7 on the new pending-reason APIs).
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 by a per-app quota
rather than a fixed minimum interval. In the Android 17 tree the defaults in
AlarmManagerService.Constants allow DEFAULT_ALLOW_WHILE_IDLE_QUOTA = 72
while-idle alarm deliveries inside a DEFAULT_ALLOW_WHILE_IDLE_WINDOW of one
hour for apps targeting modern API levels; apps still on the older "compat"
behavior get only DEFAULT_ALLOW_WHILE_IDLE_COMPAT_QUOTA = 7 per window. All of
these are DeviceConfig-tunable, so the exact ceiling can change per release.
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 Processing | mediaProcessing |
Video/photo editing and processing (6 hour limit) | FOREGROUND_SERVICE_MEDIA_PROCESSING |
| 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.
The same release added the mediaProcessing type (ServiceInfo.java value
1 << 13) for video and photo editing, which also carries a 6 hour limit and
the same onTimeout() contract. In ActiveServices, the active timeout is
driven by mDataSyncFgsTimeoutDuration and the matching media-processing
constant, so both long-running types share the same enforcement path:
// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
== ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING;
// ... timeout = mAm.mConstants.mMediaProcessingFgsTimeoutDuration
}
The shortService timeout is enforced through a separate
SERVICE_SHORT_FGS_TIMEOUT_MSG handler message with its own
OOM_ADJ_REASON_SHORT_FGS_TIMEOUT adjustment, reflecting that short services
are meant to be measured in minutes rather than hours.
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 Android 17 Background Execution Changes¶
The Android 17 source tree refines background scheduling rather than rebuilding
it: the JobScheduler controller architecture, the AlarmManager service, and the
foreground-service rules are all the same shapes described above. What changed is
mostly diagnostics, multi-user correctness, and a set of feature flags that
fine-tune batching and quotas. Most of these behaviors are gated by an aconfig
flag, the AOSP mechanism for shipping a change behind a runtime toggle. A few are
not: the start-user-before-alarm feature in §30.7.8, for instance, is guarded by a
config resource bool plus a multi-user check rather than an aconfig flag.
30.7.1 The aconfig Flags Behind the Scheduler¶
Background scheduling exposes its flags in four aconfig files inside the
jobscheduler APEX. The framework-side flags (in the backstage_power namespace)
gate new public APIs; the service-side flags gate internal behavior.
| aconfig file | Scope |
|---|---|
framework/aconfig/job.aconfig |
New JobScheduler/JobInfo APIs |
framework/aconfig/alarm.aconfig |
AlarmManager listener/quota behavior |
service/aconfig/job.aconfig |
Internal job batching and limits |
service/aconfig/device_idle.aconfig |
Doze (DeviceIdleController) tuning |
Source path: frameworks/base/apex/jobscheduler/framework/aconfig/ and
frameworks/base/apex/jobscheduler/service/aconfig/
30.7.2 New Pending-Reason Diagnostics APIs¶
Historically the only way to ask why a job had not run was
JobScheduler.getPendingJobReason(int jobId), which returns a single reason even
when several constraints are unmet. Android 17 deprecates it and adds three
richer APIs, declared in
frameworks/base/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java:
// Returns ALL the reasons a job is currently pending, not just one.
@NonNull @PendingJobReason
public int[] getPendingJobReasons(int jobId);
// A truncated time-stamped history of why the job was pending.
// Not persisted across reboots.
@NonNull
public List<PendingJobReasonsInfo> getPendingJobReasonsHistory(int jobId);
// An aggregated view: each PENDING_JOB_REASON_* mapped to the total
// time the job spent pending for that reason over its lifetime.
@NonNull
public Map<Integer, Duration> getPendingJobReasonStats(int jobId);
These map to the get_pending_job_reasons_api,
get_pending_job_reasons_history_api, and get_pending_job_reason_stats_api
flags. The companion enhanced_pending_and_stop_reasons_api flag adds more
specific reason codes: for example, the thermal JobRestriction now reports
PENDING_JOB_REASON_DEVICE_STATE_THERMAL instead of the generic
PENDING_JOB_REASON_DEVICE_STATE (see §30.2.14), so an app can distinguish
thermal throttling from doze or other device-state blocks. Because none of this
history is persisted across reboots, it is meant for live debugging, not
long-term telemetry.
flowchart TD
A["App: my job won't run"] --> B["getPendingJobReasons(jobId)"]
B --> C["int array of every<br/>unmet constraint right now"]
A --> D["getPendingJobReasonsHistory(jobId)"]
D --> E["timestamped snapshots<br/>(truncated, not persisted)"]
A --> F["getPendingJobReasonStats(jobId)"]
F --> G["reason to total-time-pending map<br/>(which constraint hurt most)"]
style C fill:#e8f5e9
style E fill:#e8f5e9
style G fill:#e8f5e9
30.7.3 Debug Tags and Trace Tags on Jobs¶
The job_debug_info_apis flag adds developer-attached metadata to jobs.
JobInfo.Builder.addDebugTag(String) attaches free-form debug tags (up to 32
per job, 127 characters each, no PII), and setTraceTag(String) attaches a
single tag that appears in system traces. These tags surface in
dumpsys jobscheduler and in the Perfetto job-tracing events (§30.7.4), making
it far easier to tell which of an app's many scheduled jobs is which when
diagnosing scheduling problems:
// frameworks/base/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
new JobInfo.Builder(JOB_ID, component)
.addDebugTag("nightly-photo-sync")
.setTraceTag("photo-sync")
.build();
30.7.4 Perfetto Job Tracing¶
Android 17 routes JobScheduler's tracing through the external Perfetto SDK. The
new JobPerfettoTracer (JobPerfettoTracer.java, Copyright 2025) is created in
the JobSchedulerService constructor and emits an instant trace event for each
job's lifecycle, tagged with the job's component and any trace tag. This replaces
the older ad hoc Trace calls and lets job execution show up as first-class
track events in a Perfetto capture, alongside the rest of the system trace.
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobPerfettoTracer.java
30.7.5 Abandoned-Job Detection¶
A long-standing failure mode is an app that returns true from onStartJob()
(promising async work) but never calls jobFinished() -- the job's execution
context is held until it times out, wasting a concurrency slot. The
handle_abandoned_jobs flag adds detection for this. When the
JobServiceContext times out a job, it checks whether the job was abandoned and,
if so, reports a distinct stop reason instead of a plain timeout:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/job/
// JobServiceContext.java
if (android.app.job.Flags.handleAbandonedJobs()
&& executing != null
&& executing.isAbandoned()) {
stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED;
internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED;
// " and maybe abandoned" appended to the stop message
}
The new STOP_REASON_TIMEOUT_ABANDONED lets the system count repeated abandoned
failures (JobStatus.getNumAbandonedFailures()) and treat a chronically
misbehaving job differently from one that merely ran long. A CompatChanges
gate (OVERRIDE_HANDLE_ABANDONED_JOBS) lets the stricter behavior be opted out
per UID during the transition.
30.7.6 Smarter Job Batching¶
Three service-side flags tune when ready jobs actually start, trading a little latency for fewer wakeups and radio activations:
batch_active_bucket_jobs: jobs in theACTIVEbucket are now included in the batching effort instead of being allowed to run as soon as they are ready.batch_connectivity_jobs_per_network: connectivity-constrained jobs are held until several are ready (or the network is already active), so a radio wakeup serves many jobs at once.do_not_force_rush_execution_at_boot: the scheduler no longer force-rushes job execution immediately after boot, smoothing the post-boot CPU and I/O spike.
Two more flags cap how much an app can demand of the scheduler:
enforce_proxied_jobs_limit bounds jobs scheduled indirectly (for example via
SyncManager), and limit_per_uid_cumulative_workitem_size bounds the total
memory an app's JobWorkItems can pin in the system. include_job_name_in_anr_message
is a small but practical debugging win: the offending job's component name now
appears in the slow-response ANR message.
30.7.7 User-Initiated Job Notifications¶
User-initiated jobs (UIJs) must show a notification while they run, similar to a
foreground service. This is not new in Android 17. The centralized
JobNotificationCoordinator (JobNotificationCoordinator.java, Copyright 2022)
shipped alongside UIJs themselves in Android 14 (API 34): it maps each running UIJ
to the app notification it is attached to, marks the notification with a
user-initiated-job flag through NotificationManagerInternal, and restricts the
app from silently dismissing a UIJ's notification while the job runs, so the user
always retains a visible, actionable indicator (and a way to stop the work).
Notifications are cleaned up when the owning user is stopped. Android 17 inherits
this coordinator unchanged; it is covered here because it underpins the UIJ
behavior the rest of this chapter relies on.
Source path: frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java
30.7.8 Starting a User Before Its Alarm Fires¶
On multi-user and private-space devices, an alarm scheduled by an app belonging
to a stopped user could be missed because the user (and thus the app) was not
running when the alarm time arrived. Android 17 closes this gap with the
UserWakeupStore (UserWakeupStore.java, Copyright 2024). The feature is not
behind an aconfig flag: AlarmManagerService sets mStartUserBeforeScheduledAlarms
only when UserManager.supportsMultipleUsers() is true and the config resource
bool config_allowAlarmsOnStoppedUsers is set (AlarmManagerService.java:1873-1875).
When it is enabled, AlarmManagerService records, per user, the earliest time that
user has an alarm due, persisting the set of user IDs with pending alarms to an XML
file under the system data directory:
// frameworks/base/apex/jobscheduler/service/java/com/android/server/alarm/
// AlarmManagerService.java
mUserWakeupStore.addUserWakeup(userId, convertToElapsed(...));
// ... at delivery time, the affected users are started first:
final int[] userIds = mUserWakeupStore.getUserIdsToWakeup(nowELAPSED);
Because the list is persisted, a user with a scheduled alarm can be started even after a device reboot, and the store deliberately staggers consecutive user starts (a fixed delay between them) so the system does not try to start several stopped users at the same instant. This is primarily what makes scheduled alarms reliable for private-space and secondary-profile apps.
sequenceDiagram
participant App as App (stopped user)
participant AMS as AlarmManagerService
participant Store as UserWakeupStore
participant UM as User lifecycle
App->>AMS: setExact(...) before user stops
AMS->>Store: addUserWakeup(userId, triggerElapsed)
Store->>Store: persist user IDs to XML
Note over AMS,Store: device reboots or user stops
AMS->>Store: getUserIdsToWakeup(now)
Store-->>AMS: userIds due now
AMS->>UM: start those users (staggered)
UM-->>AMS: user running
AMS->>App: deliver alarm
30.7.9 AlarmManager Listener and Quota Flags¶
The alarm.aconfig flags refine while-idle behavior:
allow_listeners_while_idle:OnAlarmListener-based alarms (the in-process variant from §30.3.8) can now be allowed to fire during doze under the while-idle quota, with their ownDEFAULT_ALLOW_WHILE_IDLE_LISTENER_QUOTA(72 per window), matching whatPendingIntentwhile-idle alarms already had.allow_alarms_with_relaxed_quota: certain allow-while-idle listener alarms are granted a relaxed quota path, checked inAlarmManagerServiceat delivery time viaFlags.allowAlarmsWithRelaxedQuota().
30.7.10 Doze Tuning Flags¶
The device_idle.aconfig flags adjust how Doze behaves without changing its
state machine (§30.1.5):
disable_wakelocks_in_light_idle: ignore wakelocks during light idle, not just deep idle, closing a battery-drain path.quick_doze_on_lid_close: enter Doze quickly when a foldable or laptop-style device's lid is closed, rather than waiting for the normal stationary timer.allow_non_wake_up_deep_alarms(Wear): allow non-wakeup alarms to be evaluated during deep idle on Wear devices.support_allow_while_idle_quota_zero: support configuring the while-idle quota all the way down to zero, fully blocking while-idle alarms when desired.
None of these change the public alarm or job APIs; they are knobs the platform (and OEMs, for Wear and foldables) use to tune the doze/standby battery trade-off.
30.8 Try It¶
This section provides hands-on exercises to explore Android's background task scheduling infrastructure.
30.8.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.8.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.8.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.8.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.8.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.8.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.8.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.8.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.8.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.8.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.8.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.8.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 |
| PrefetchController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/PrefetchController.java |
| RestrictingController | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/controllers/RestrictingController.java |
| JobNotificationCoordinator | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobNotificationCoordinator.java |
| JobPerfettoTracer | frameworks/base/apex/jobscheduler/service/java/com/android/server/job/JobPerfettoTracer.java |
| AlarmManagerService | frameworks/base/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java |
| UserWakeupStore | frameworks/base/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java |
| JobScheduler (public API) | frameworks/base/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java |
| Scheduler aconfig flags | frameworks/base/apex/jobscheduler/framework/aconfig/, frameworks/base/apex/jobscheduler/service/aconfig/ |
| 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.