How to Test Offline Mode on Android (Complete Guide)
Android apps frequently lose connectivity: users enter elevators, board subways, or move between Wi‑Fi cells. When the network drops, the app must either gracefully degrade, queue work for later, or s
Why Offline Mode Matters on Android
Android apps frequently lose connectivity: users enter elevators, board subways, or move between Wi‑Fi cells. When the network drops, the app must either gracefully degrade, queue work for later, or surface a clear error. If it fails to do so, users experience crashes, ANRs, silent data loss, or confusing UI states that lead to bad reviews and churn. Offline‑mode bugs are especially costly because they often surface only after the app has been in the wild for weeks, when background sync, pending jobs, or cached state have accumulated. Detecting them early saves rework, protects brand reputation, and reduces support overhead.
Real‑world impact
- Crashes and ANRs – Attempting to read from a closed socket or accessing a null
ConnectivityManagercallback can throw unchecked exceptions that halt the UI thread. - Data inconsistency – Writes that succeed locally but never reach the server create divergent states; later sync may overwrite user edits or lose them entirely.
- UX friction – Spinners that never disappear, toast messages that flash and vanish, or buttons that stay enabled despite no network cause users to think the app is broken.
- Security gaps – Fallback to local storage may bypass encryption or token validation, exposing credentials if the device is compromised.
- Accessibility breaks – TalkBack may announce stale content when the UI fails to update, violating WCAG 2.1 Success Criterion 4.1.3 (Status Messages).
Common failure modes
| Failure mode | Typical symptom | Root cause |
|---|---|---|
| Network‑state check omitted | UI proceeds as if online, then throws IOException | Missing ConnectivityManager listener or reliance on stale isConnected() |
| No offline queue | User action disappears after reconnection | Background worker not persisted or not re‑scheduled |
| Stale cache served | Out‑of‑date data shown, no refresh indicator | Cache‑invalid‑on‑miss logic missing |
| Dialog not dismissed | Modal blocks interaction after network returns | Dialog dismissal tied only to success callback |
| Accessibility live region not updated | TalkBack reads old message | android:importantForAccessibility set to no on dynamic container |
| Token refresh loop | Repeated 401 → login screen → immediate retry | Auth interceptor does not check isNetworkAvailable() before retry |
---
Test Matrix for Offline Mode
A systematic matrix ensures you cover happy paths, error paths, edge cases, accessibility, security, performance, and cross‑session behavior. The table below lists test scenarios, the expected outcome, and the verification method.
| ID | Scenario | Preconditions | Steps | Expected result | Verification |
|---|---|---|---|---|---|
| O1 | Happy path – read‑only feed | App launched, network ON, feed cached | 1. Disable Wi‑Fi/mobile data 2. Scroll feed 3. Pull‑to‑refresh | Feed shows cached items, refresh shows “No connection” toast, no crash | UI screenshot, logcat for exceptions |
| O2 | Happy path – form submit | User on login screen, credentials entered | 1. Enable airplane mode 2. Tap “Sign in” 3. Observe feedback | Inline error “No network”, fields stay enabled, no progress spinner | Espresso idling resource, toast verification |
| O3 | Error path – POST retry | App has a pending POST request queued via WorkManager | 1. Disable network 2. Trigger POST 3. Re‑enable network after 10 s | Request retries automatically, success toast appears | WorkManager logs, network stub verification |
| O4 | Edge case – rapid toggle | Device supports fast Wi‑Fi ↔ mobile switch | 1. Toggle airplane mode on/off five times within 2 s 2. Perform a UI action each toggle | App never crashes, network state callbacks received for each change | Count CONNECTIVITY_CHANGE broadcasts in logcat |
| O5 | Accessibility – live region | Screen contains a TextView with android:accessibilityLiveRegion="polite" | 1. Disable network 2. Trigger action that updates live region (e.g., submit button) 3. Enable network | TalkBack announces updated message both offline and online | Accessibility Test Framework (ATF) assertions |
| O6 | Security – token storage | App stores auth token in SharedPreferences without encryption | 1. Go offline 2. Attempt to refresh token 3. Check storage | Token never written to plain file; if written, it is encrypted | File system inspection, adb shell run-as |
| O7 | Performance – ANR detection | UI thread performs a blocking network call wrapped in AsyncTask | 1. Disable network 2. Trigger the call 3. Wait 5 s | No ANR dialog; app shows fallback UI within 2 s | adb shell dumpsys activity services to check ANR count |
| O8 | Cross‑session – job persistence | App schedules a sync job with JobScheduler set to persist across reboots | 1. Disable network 2. Trigger job 3. Reboot device 4. Re‑enable network | Job executes after reboot, data uploaded | adb shell cmd jobscheduler list + server logs |
*Use this matrix as a checklist; add rows for features specific to your app (e.g., offline map tile download, media playback).*
---
Manual Testing Approach
Manual testing remains valuable for exploratory checks, especially when you need to verify subtle UI feedback or accessibility announcements. The following steps give you a repeatable routine that can be performed on a physical device or an emulator.
Setting up environment
- Install the latest Android Studio and create an AVD with Google Play APIs (to test Google‑location‑based offline behavior).
- Enable Developer options → USB debugging on the device.
- Grant the app
android.permission.ACCESS_NETWORK_STATEandandroid.permission.CHANGE_NETWORK_STATE(the latter only needed for rooted devices or emulator control). - Install a network‑simulation tool such as Clumsy (Windows) or Network Link Conditioner (macOS) if you prefer to shape traffic instead of toggling airplane mode.
Simulating network loss
| Method | Command | When to use |
|---|---|---|
| Airplane mode (quick) | adb shell svc wifi disable && adb shell svc data disable | Immediate total loss; works on non‑rooted devices |
| Selective Wi‑Fi off | adb shell svc wifi disable | Test Wi‑Fi‑only fallback |
| Selective mobile data off | adb shell svc data disable | Test cellular‑only fallback |
| Emulator cellular controls | Extended controls → Cellular → Signal strength = None | Precise, repeatable loss/gain |
tc traffic shaping (root) | adb shell su -c "tc qdisc add dev wlan0 root netem loss 100%" | Simulate packet loss without full disconnect |
Step‑by‑step checklist
- Baseline – Verify the feature works online (no errors, correct UI).
- Disable network – Use one of the methods above.
- Invoke the target flow – e.g., tap a button, scroll a list, open a drawer.
- Observe – Look for:
- Progress indicators that stop or show error.
- Toasts/snackbars with concise, actionable messages.
- No stack traces in logcat (
adb logcat | grep -E "AndroidRuntime|Exception"). - Accessibility feedback (enable TalkBack, verify announcements).
- Re‑enable network – Repeat the same flow; ensure:
- Pending actions are retried or reported as failed.
- UI updates to reflect fresh data.
- No duplicate work (e.g., two uploads of same payload).
- Check persistence – Force‑stop the app, relaunch, confirm that any queued work survived.
- Repeat with different personas – Simulate a novice user (slow taps, long presses), an impatient user (rapid repeated taps), and an accessibility user (TalkBack + switch access).
Record results in a spreadsheet mirroring the test matrix; mark each cell PASS/FAIL and attach a short log snippet for failures.
---
Automated Testing Approaches
Automation scales offline validation across configurations, API levels, and nightly CI pipelines. Below are concrete techniques you can adopt today.
Using Android Emulator network controls
The emulator exposes the gsm console to toggle voice/data and set network speed.
# Start emulator with a specific AVD
emulator -avd Pixel_5_API_34 -no-window -no-audio &
# Get its console port (usually 5554)
adb connect localhost:5554
# Disable data
adb -s emulator-5554 emu sms send 5554 "data off"
# Or via telnet
telnet localhost 5554
gsm data off
gsm data on
Integrate these commands into a Gradle task that runs before each UI test:
task disableNetwork(type: Exec) {
commandLine 'adb', '-s', 'emulator-5554', 'emu', 'sms', 'send', '5554', 'data off'
}
task enableNetwork(type: Exec) {
commandLine 'adb', '-s', 'emulator-5554', 'emu', 'sms', 'send', '5554', 'data on'
}
android.testOptions.unitTests.all {
dependsOn disableNetwork
finalizedBy enableNetwork
}
Using adb shell commands for fine‑grained control
You can query and modify the connectivity state directly:
# Get current state
adb shell dumpsys connectivity
# Disable all networks (requires root on user builds)
adb shell su -c "svc wifi disable && svc data disable"
# Enable only Wi‑Fi
adb shell svc wifi enable
adb shell svc data disable
Wrap these in a JUnit @Rule that toggles state before each test method:
public class NetworkRule implements TestRule {
private final boolean enableWifi;
public NetworkRule(boolean enableWifi) { this.enableWifi = enableWifi; }
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// disable
AdbHelper.run("svc wifi disable && svc data disable");
try {
base.evaluate();
} finally {
// restore
if (enableWifi) AdbHelper.run("svc wifi enable");
AdbHelper.run("svc data enable");
}
}
};
}
}
Espresso/UiAutomator with network mock
For unit‑level network calls, replace the real OkHttpClient with MockWebServer and simulate failures:
@Rule
public final MockWebServer server = new MockWebServer();
@Before
public void setUp() throws Exception {
server.enqueue(new MockResponse()
.setResponseCode(504) // Gateway Timeout simulates offline
.setBody(""));
MyApi.setBaseUrl(server.url("/"));
}
@Test
public void submitForm_showsOfflineError() {
onView(withId(R.id.email)).perform(typeText("test@example.com"), closeSoftKeyboard());
onView(withId(R.id.password)).perform(typeText("pwd"), closeSoftKeyboard());
onView(withId(R.id.loginBtn)).perform(click());
// Expect a Snackbar with offline message
onView(withText(R.string.error_no_network))
.check(matches(isDisplayed()));
}
When you need to test the actual Android networking stack (e.g., HttpURLConnection), use adb shell cmd connectivity to toggle airplane mode inside the test:
@Test
public void login_offline() {
AdbHelper.run("svc wifi disable && svc data disable");
onView(withId(R.id.loginBtn)).perform(click());
onView(withText(R.string.error_no_network))
.check(matches(isDisplayed()));
AdbHelper.run("svc wifi enable && svc data enable");
}
Autonomous, persona‑driven exploration with SUSA
SUSA’s agent can be pointed at an APK or a URL and will exercise the app using built‑in personas (curious, impatient, novice, adversarial, elderly, accessibility, power‑user). Each persona varies tap timing, scroll depth, input length, and error‑handling tolerance, which helps surface offline bugs that scripted tests miss because they follow a deterministic path.
To run an offline‑mode session:
# Install the agent
pip install susatest-agent
# Point at your built APK
susatest run --app ./app-debug.apk \
--mode offline \
--personas curious impatient elderly accessibility \
--output ./susareport.json
The agent automatically:
- Disables network via
adb shell svc wifi disable && svc data disableat random intervals. - Triggers UI events that each persona prefers (e.g., the impatient persona double‑taps buttons rapidly).
- Detects crashes, ANRs, unhandled exceptions, and accessibility live‑region failures.
- Generates regression scripts (Appium for Android, Playwright for Web) that you can commit to your repo.
Because the agent remembers explored screens and dead ends, subsequent runs become smarter: it will retry a previously failing offline flow with varied timing, increasing the chance to uncover flaky race conditions.
Third‑party tooling
| Tool | Primary use | How it helps offline testing |
|---|---|---|
| Charles Proxy / mitmproxy | TLS‑terminating proxy | Simulate latency, packet loss, or outright disconnect by configuring mapping rules; can rewrite responses to 504 on demand. |
| Stetho (Facebook) | Chrome DevTools bridge | Inspect IndexedDB/SharedPreferences while offline to verify correct queuing. |
| LeakCanary | Memory leak detection | Offline flows often retain references to ViewModels or WorkManager; LeakCanary warns if they linger after a network loss. |
| Android Profiler | CPU, memory, network | Confirms that no network threads are spinning when the device is offline (helps spot wasted wake locks). |
| Firebase Test Lab | Device farm | Run your Espresso/UIAutomator tests on dozens of real devices with automated airplane‑mode toggles via the gcloud CLI. |
---
Concrete Examples with Code/Commands
Below are ready‑to‑copy snippets you can drop into a project.
1. Toggle airplane mode via ADB (requires root on production builds)
# Disable all radios
adb shell su -c "settings put global airplane_mode_on 1 && am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true"
# Re‑enable after a delay
adb shell su -c "settings put global airplane_mode_on 0 && am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false"
2. Espresso IdlingResource that watches`
public class ConnectivityIdlingResource {
private final ConnectivityManager cm 0 && am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false"
3. MockWebServer scenario for a POST that should be queued
@Test
public void postIsQueuedWhenOffline() throws Exception {
// Server will not enqueue a response → client times out
server.enqueue(new MockResponse().setBodyDelay(5, TimeUnit.SECONDS));
// Disable network
AdbHelper.run("svc wifi disable && svc data disable");
// Trigger the repository method that posts
viewModel.submitPayload(testPayload);
// Fast‑forward IdlingResource to avoid waiting 5 s
IdlingRegistry.getInstance().register(new CountingIdlingResource("network"));
// Verify that a WorkRequest was enqueued
List<WorkInfo> workInfoList = WorkManager.getInstance(
ApplicationProvider.getApplicationContext())
.getWorkInfosByTagLiveData("UPLOAD_TAG")
.getOrAwait();
assertFalse(workInfoList.isEmpty());
assertEquals(WorkInfo.State.ENQUEUED, workInfoList.get(0).getState());
// Re‑enable network and confirm work runs
AdbHelper.run("svc wifi enable && svc data enable");
// Wait for completion (use a CountingIdlingResource tied to WorkManager)
// Assert success via a fake server response
server.enqueue(new MockResponse().setResponseCode(200));
// … assert UI shows success toast
}
4. SUSA CLI command for a nightly job
# In your CI pipeline (e.g., GitHub Actions)
- name: Run SUSA offline exploration
run: |
pip install susatest-agent
susatest run --app ./app-release.apk \
--mode offline \
--personas curious impatient elderly accessibility \
--max-depth 6 \
--output susa_report.json \
--format junit > susa_junit.xml
- name: Publish SUSA results
if: always()
uses: actions/upload-artifact@v3
with:
name: susa-report
path: susa_junit.xml
The generated JUnit XML can be consumed by your CI to mark the build unstable if any crash or ANR is detected.
5. Verifying encrypted token storage after offline refresh attempt
@Test
public void tokenIsEncryptedWhenWrittenOffline() throws Exception {
// Simulate offline
AdbHelper.run("svc wifi disable && svc data disable");
// Trigger a token refresh (e.g., via AuthInterceptor)
authRepository.refreshToken();
// Give the background worker a moment to run
Thread.sleep(1000);
// Pull the SharedPreferences file
File prefs = new File(
ApplicationProvider.getApplicationContext()
.getFilesDir(),
"auth_prefs.xml");
String content = new String(Files.readAllBytes(prefs.toPath()));
// Expect the token value to be base64‑encoded AES ciphertext, not plain JWT
assertFalse(content.contains("eyJ")); // typical JWT start
assertTrue(content.contains("c2VjcmV0")); // placeholder for encrypted blob
}
---
Edge Cases that Appear Only in Production
Even with exhaustive lab testing, certain offline‑mode bugs surface only when the app runs for days or weeks on real devices. Below are the most common production‑only patterns and how to guard against them.
Background services and JobScheduler
- Problem – A
JobServicescheduled withsetRequiresCharging(true)may never run if the device is unplugged while offline, causing stale data to persist indefinitely. - Fix – Use
setRequiresDeviceIdle(false)and provide a fallbackWorkManagerwithExistingWorkPolicy.REPLACEthat does not depend on charging. Write a unit test that verifies the job runs afteradb shell cmd jobscheduler schedulefollowed by a reboot.
Pending intents and notifications
- Problem – A
PendingIntentthat launches an activity to retry a failed request may be fired from a notification posted while offline. If the activity assumes network availability, it crashes. - Fix – In the activity’s
onCreate, checkConnectivityManager.getActiveNetworkInfo()(or the modernNetworkCallback) and show an inline error instead of proceeding. Add an Espresso test that posts a notification viaadb shell cmd notification postand then clicks it while offline.
Data persistence corruption
- Problem – Apps that write to a SQLite database using a
ContentProvidermay leave a transaction open when the process is killed due to low memory during an offline write. On restart, the database is locked, leading toSQLiteException: database is locked. - Fix – Always wrap DB writes in
try { db.beginTransaction(); … db.setTransactionSuccessful(); } finally { db.endTransaction(); }. Useandroidx.sqlite:sqlitelibrary’sSupportSQLiteDatabasewhich auto‑rolls back on exception. Verify withadb shell dumpsys meminfothat no open transactions remain after a simulated kill (adb shell am kill).
Battery optimizations and Doze mode
- Problem – On Android 6+, Doze can defer alarms and jobs, causing offline‑queued work to be delayed far beyond user expectations when the device is stationary and unplugged.
- Fix – Use
setAndAllowWhileIdle()for critical alarms, or request a temporary whitelist viaACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS. Test by enabling Doze manually:adb shell dumpsys deviceidle force-idlethenadb shell dumpsys deviceidle unforce.
Multi‑user / work profile
- Problem – In a work‑profile scenario, the personal profile may be offline while the work profile has VPN connectivity. An app that incorrectly reads the global network state may think it’s online and attempt to reach a corporate endpoint, failing silently.
- Fix – Query network capabilities per
Networkobject obtained fromConnectivityManager.getNetworkCapabilities(network)and checkNET_CAPABILITY_NOT_RESTRICTED. Write a test that creates a second user viaadb shell pm create-user testuserand switches to it withadb shell am switch-user.
Sudden network regain after prolonged loss
- Problem – Apps that keep a singleton
Retrofitinstance may retain a staleOkHttpClientwith a connection pool that still holds dead sockets, leading toSSLHandshakeExceptionon the first request after reconnection. - Fix – Call
client.dispatcher().executorService().shutdown()and rebuild theRetrofitobject, or useOkHttp’sconnectionPool().evictAll(). Add an IdlingResource that waits for the socket pool to be empty before asserting UI updates.
Logging and telemetry overload
- Problem – Some analytics libraries batch events and flush only on network change. If the app stays offline for a long time, the in‑memory queue can grow and cause an OOM kill.
- Fix – Implement a maximum batch size and drop oldest events when the limit is exceeded. Verify with
adb shell dumpsys meminfothat the heap stays below a safe threshold after sending thousands of events while offline.
---
Short Checklist for Offline Mode
| ✅ Item | How to verify | |
|---|---|---|
| Network state listener registered and unregistered correctly | Register a BroadcastReceiver for CONNECTIVITY_CHANGE in instrumentation test; assert onReceive called after each toggle. | |
| All user‑initiated actions show a clear offline message (toast, snackbar, inline) | Espresso onView(withText(R.string.error_no_network)).check(matches(isDisplayed())). | |
| No progress spinner remains visible after timeout | IdlingResource that waits for ProgressBar visibility to become gone within 5 s. | |
| Queued work persists across process kill and device reboot | Use WorkManager with setExpedited(false); after adb shell am force-stop and adb reboot, confirm work runs. | |
| Accessibility live region updates with offline status | Enable TalkBack, perform action, capture spoken feedback via uiautomator dump and check for offline string. | |
| No crash or ANR when network toggled rapidly | Run monkey test: adb shell monkey -p while enabling/disabling airplane mode every 2 s. | |
| Encrypted storage of any credentials written offline | Pull SharedPreferences or Room DB and verify ciphertext (e.g., AES‑GCM) rather than plain token. | |
| Battery‑friendly: no wake locks held when idle | `adb shell dumpsys power | grep WakeLocks` should show none after 30 s of offline inactivity. |
| No excessive memory growth from analytics/event queues | Capture heap before/after a 5‑minute offline idle with simulated event generation; assert delta < 5 MB. | |
| Regression scripts generated from exploratory runs pass on CI | Run the Appium/Playwright scripts produced by SUSA on a clean emulator; assert 0 failures. |
---
Closing Takeaways
Offline mode is not a niche edge case; it is a primary interaction mode for a significant fraction of Android users. The cost of missing an offline bug manifests as crashes, data loss, poor accessibility, and frustrated users who abandon the app. A disciplined approach combines:
- A comprehensive test matrix that covers happy paths, error paths, accessibility, security, performance, and cross‑session behavior.
- Manual exploratory steps that simulate real‑world network loss using ADB, airplane mode, and carrier‑specific controls, paired with persona‑driven observation.
- Automated checks leveraging Espresso/UiAutomator with MockWebServer, IdlingResources, and direct ADB toggles, augmented by third‑party proxies for fine‑grained latency and loss simulation.
- Autonomous, persona‑driven exploration (e.g., via SUSA) that discovers flows and timing combinations scripted tests never consider, producing regression assets you can lock into version control.
- Continuous validation of background jobs, persistence, battery optimizations, and multi‑user contexts, which are the typical sources of production‑only failures.
By integrating these practices into your CI pipeline—running the matrix on every PR, executing the SUSA exploration nightly, and treating any newly discovered offline failure as a blocker—you turn offline mode from a source of post‑release firefight into a reliably tested feature. The result is an app that behaves predictably whether the user is on a 5G tower or inside a subway tunnel, keeping trust high and support low.
---
*End of guide.*
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free