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

March 06, 2026 · 15 min read · How-To Guides

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

Common failure modes

Failure modeTypical symptomRoot cause
Network‑state check omittedUI proceeds as if online, then throws IOExceptionMissing ConnectivityManager listener or reliance on stale isConnected()
No offline queueUser action disappears after reconnectionBackground worker not persisted or not re‑scheduled
Stale cache servedOut‑of‑date data shown, no refresh indicatorCache‑invalid‑on‑miss logic missing
Dialog not dismissedModal blocks interaction after network returnsDialog dismissal tied only to success callback
Accessibility live region not updatedTalkBack reads old messageandroid:importantForAccessibility set to no on dynamic container
Token refresh loopRepeated 401 → login screen → immediate retryAuth 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.

IDScenarioPreconditionsStepsExpected resultVerification
O1Happy path – read‑only feedApp launched, network ON, feed cached1. Disable Wi‑Fi/mobile data 2. Scroll feed 3. Pull‑to‑refreshFeed shows cached items, refresh shows “No connection” toast, no crashUI screenshot, logcat for exceptions
O2Happy path – form submitUser on login screen, credentials entered1. Enable airplane mode 2. Tap “Sign in” 3. Observe feedbackInline error “No network”, fields stay enabled, no progress spinnerEspresso idling resource, toast verification
O3Error path – POST retryApp has a pending POST request queued via WorkManager1. Disable network 2. Trigger POST 3. Re‑enable network after 10 sRequest retries automatically, success toast appearsWorkManager logs, network stub verification
O4Edge case – rapid toggleDevice supports fast Wi‑Fi ↔ mobile switch1. Toggle airplane mode on/off five times within 2 s 2. Perform a UI action each toggleApp never crashes, network state callbacks received for each changeCount CONNECTIVITY_CHANGE broadcasts in logcat
O5Accessibility – live regionScreen contains a TextView with android:accessibilityLiveRegion="polite"1. Disable network 2. Trigger action that updates live region (e.g., submit button) 3. Enable networkTalkBack announces updated message both offline and onlineAccessibility Test Framework (ATF) assertions
O6Security – token storageApp stores auth token in SharedPreferences without encryption1. Go offline 2. Attempt to refresh token 3. Check storageToken never written to plain file; if written, it is encryptedFile system inspection, adb shell run-as
O7Performance – ANR detectionUI thread performs a blocking network call wrapped in AsyncTask1. Disable network 2. Trigger the call 3. Wait 5 sNo ANR dialog; app shows fallback UI within 2 sadb shell dumpsys activity services to check ANR count
O8Cross‑session – job persistenceApp schedules a sync job with JobScheduler set to persist across reboots1. Disable network 2. Trigger job 3. Reboot device 4. Re‑enable networkJob executes after reboot, data uploadedadb 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

  1. Install the latest Android Studio and create an AVD with Google Play APIs (to test Google‑location‑based offline behavior).
  2. Enable Developer optionsUSB debugging on the device.
  3. Grant the app android.permission.ACCESS_NETWORK_STATE and android.permission.CHANGE_NETWORK_STATE (the latter only needed for rooted devices or emulator control).
  4. 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

MethodCommandWhen to use
Airplane mode (quick)adb shell svc wifi disable && adb shell svc data disableImmediate total loss; works on non‑rooted devices
Selective Wi‑Fi offadb shell svc wifi disableTest Wi‑Fi‑only fallback
Selective mobile data offadb shell svc data disableTest cellular‑only fallback
Emulator cellular controlsExtended controls → Cellular → Signal strength = NonePrecise, 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

  1. Baseline – Verify the feature works online (no errors, correct UI).
  2. Disable network – Use one of the methods above.
  3. Invoke the target flow – e.g., tap a button, scroll a list, open a drawer.
  4. Observe – Look for:
  1. Re‑enable network – Repeat the same flow; ensure:
  1. Check persistence – Force‑stop the app, relaunch, confirm that any queued work survived.
  2. 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:

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

ToolPrimary useHow it helps offline testing
Charles Proxy / mitmproxyTLS‑terminating proxySimulate latency, packet loss, or outright disconnect by configuring mapping rules; can rewrite responses to 504 on demand.
Stetho (Facebook)Chrome DevTools bridgeInspect IndexedDB/SharedPreferences while offline to verify correct queuing.
LeakCanaryMemory leak detectionOffline flows often retain references to ViewModels or WorkManager; LeakCanary warns if they linger after a network loss.
Android ProfilerCPU, memory, networkConfirms that no network threads are spinning when the device is offline (helps spot wasted wake locks).
Firebase Test LabDevice farmRun 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

Pending intents and notifications

Data persistence corruption

Battery optimizations and Doze mode

Multi‑user / work profile

Sudden network regain after prolonged loss

Logging and telemetry overload

---

Short Checklist for Offline Mode

✅ ItemHow to verify
Network state listener registered and unregistered correctlyRegister 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 timeoutIdlingResource that waits for ProgressBar visibility to become gone within 5 s.
Queued work persists across process kill and device rebootUse WorkManager with setExpedited(false); after adb shell am force-stop and adb reboot, confirm work runs.
Accessibility live region updates with offline statusEnable TalkBack, perform action, capture spoken feedback via uiautomator dump and check for offline string.
No crash or ANR when network toggled rapidlyRun monkey test: adb shell monkey -p -c android.intent.category.LAUNCHER 500 --throttle 100 --pct-syskeys 0 --pct-nav 0 --pct-majornav 0 --pct-appswitch 0 --pct-anyevent 100 while enabling/disabling airplane mode every 2 s.
Encrypted storage of any credentials written offlinePull 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 powergrep WakeLocks` should show none after 30 s of offline inactivity.
No excessive memory growth from analytics/event queuesCapture heap before/after a 5‑minute offline idle with simulated event generation; assert delta < 5 MB.
Regression scripts generated from exploratory runs pass on CIRun 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:

  1. A comprehensive test matrix that covers happy paths, error paths, accessibility, security, performance, and cross‑session behavior.
  2. Manual exploratory steps that simulate real‑world network loss using ADB, airplane mode, and carrier‑specific controls, paired with persona‑driven observation.
  3. Automated checks leveraging Espresso/UiAutomator with MockWebServer, IdlingResources, and direct ADB toggles, augmented by third‑party proxies for fine‑grained latency and loss simulation.
  4. 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.
  5. 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