ScreenSync MCP guide

Testing Android Process Death with Claude Code and MCP

Learn how to automate Android process death and state restoration testing with Claude Code and ScreenSync MCP. Verify SavedStateHandle and Compose persistence.

Testing Android Process Death with Claude Code and MCP

Android application developers frequently encounter hard-to-reproduce bugs where an end-user switches away from an app, answers a phone call, or navigates a heavy multi-app workflow, only to return to a blank screen, a reset checkout form, or a fatal NullPointerException. This silent failure mode stems from Android Operating System process death. When physical device RAM becomes constrained, the Android kernel's Low Memory Killer daemon (lmkd) assigns aggressive Out-Of-Memory adjustment (oom_adj) scores to cached background processes and abruptly evicts them from Linux memory space. While standard configuration changes (such as device rotation) recreate activities within an active process heap, genuine process death obliterates the entire Java/Kotlin Virtual Machine process space. Restoring an application gracefully demands that every critical UI state, navigation backstack entry, and active form input be serialized into Android OS state persistence mechanisms, specifically savedInstanceState, Jetpack ViewModel SavedStateHandle, or Jetpack Compose rememberSaveable.

Automating regression testing for lifecycle destruction has historically been manual, brittle, and difficult to orchestrate in continuous integration pipelines. Toggle options like "Don't keep activities" in Developer Options only simulate activity recreation while keeping singleton instances, static variables, and in-memory ViewModel caches alive, concealing the exact serialization crashes that plague real users. By pairing Anthropic's terminal agent Claude Code with ScreenSync MCP, mobile engineering teams can construct deterministic, unattended process death validation suites directly on real physical Android hardware over standard ADB connections.

Key Takeaways

  • Process Death vs Configuration Change: Process death evicts the entire JVM process space (kill -9), purging static variables, background thread states, and unpersisted ViewModel properties, whereas configuration changes retain singleton instances.
  • Dual-Plane Testing Architecture: ScreenSync MCP combines high-speed, consent-based screen capture (sub-100ms via MediaProjection, zero root, zero ADB) with 29 phone and desktop tools executed via ADB for deterministic input injection and OS lifecycle manipulation.
  • Automated Lifecycle Harness: Claude Code autonomously drives multi-step UI flows via control_tap and control_type, backgrounds the target application via control_key 3 (KEYCODE_HOME), terminates the background process with am kill, relaunches via control_launch_app, and validates UI fidelity via get_ui_hierarchy.
  • SavedStateHandle & rememberSaveable Verification: Structural node tree analysis ensures that dynamic Compose components and complex view trees maintain exact scroll offsets, input values, and selected states without crashing on TransactionTooLargeException.
  • Rapid CI/CD Deployment: Claude Code connects to the local ScreenSync MCP hub (claude mcp add screensync node "/path/to/hub/screensync-hub.js"), enabling unattended regression testing in local workflows and headless CI runners.

The Android Process Death Lifecycle: Architecture and State Eviction

Android process death occurs when the Linux kernel's Low Memory Killer daemon terminates background application processes with high out-of-memory adjustment scores to reclaim memory. Unlike configuration changes, process death destroys the entire virtual machine heap, requiring applications to rebuild view states, dependency injection graphs, and navigation stacks solely from serialized system bundles.

To understand why mobile applications fail in the wild, engineers must distinguish between the two primary ways Android destroys and recreates an Activity:

  1. Configuration Changes: Triggered by screen orientation changes, dark mode toggles, or locale changes. Here, Android destroys the Activity record and invokes onDestroy(), but the hosting Linux OS process (PID) remains running. Singleton dependency graphs managed by Dagger or Hilt, in-memory Kotlin StateFlow streams, and non-serialized ViewModel instances remain fully populated in JVM heap memory.
  2. Kernel Process Eviction (Process Death): Triggered when the user places the application into the background and the system encounters RAM pressure. The Linux kernel's Low Memory Killer daemon (lmkd) evaluates all active processes using Out-Of-Memory adjustment (oom_adj) scores ranging from 0 (foreground active) to 900+ (cached background apps). When available physical memory drops below critical thresholds, lmkd sends a SIGKILL to the target process. Every thread, static reference, in-memory repository cache, and ViewModel instance is instantly purged from RAM.
+-----------------------------------------------------------------------------------+
|                           Android OS Process Lifecycle                             |
+-----------------------------------------------------------------------------------+
|  [ Foreground App ]  --> User presses HOME / switches app                         |
|         |                                                                         |
|         v                                                                         |
|  [ onPause() -> onStop() ] --> Activity calls onSaveInstanceState(outState Bundle)|
|         |                                                                         |
|         v                                                                         |
|  [ System Memory Pressure ] --> lmkd evaluates oom_adj (Cached score: 700 - 900)   |
|         |                                                                         |
|         +---> SIGKILL (Process Death: Heap, Singletons, ViewModels wiped)         |
|         |                                                                         |
|  [ User Relaunches App ]                                                          |
|         |                                                                         |
|         v                                                                         |
|  [ Brand New Linux Process Created ]                                              |
|         |                                                                         |
|         v                                                                         |
|  [ onCreate(savedInstanceState) ] <-- OS passes serialized Bundle back to app     |
|         |                                                                         |
|         v                                                                         |
|  [ SavedStateHandle & rememberSaveable restored ]                                 |
+-----------------------------------------------------------------------------------+

When the user returns to the evicted application from the Android Recents overview, the Android Activity Manager Service (AMS) allocates a completely fresh Linux process, initializes a new Application class instance, and instantiates the top-level Activity. Crucially, the operating system injects the serialized Bundle that was captured during onSaveInstanceState() back into Activity.onCreate(savedInstanceState).

If the development team relied on static singletons or unpersisted ViewModel state without integrating SavedStateHandle or Compose rememberSaveable, the newly launched Activity attempts to read uninitialized properties, resulting in immediate crashes or silently blank screens.

Furthermore, state persistence is subject to strict architectural boundaries. The Android IPC mechanism relies on Linux Binder transactions, which share a strict 1 MB memory buffer per process. If an application attempts to write high-resolution bitmap byte arrays, massive JSON payloads, or long lists into outState: Bundle or SavedStateHandle, the system crashes immediately with a fatal android.os.TransactionTooLargeException.

The Limitations of Traditional Process Death Testing Tools

Developer Options toggles like 'Don't keep activities' fail to replicate real process death because the underlying application process, application class singletons, and memory caches stay intact. Manual execution of Android Debug Bridge commands remains cumbersome, requiring developers to manually navigate back to complex view states without automated assertion mechanisms or visual validation.

For over a decade, Android engineering teams have struggled to replicate lifecycle termination accurately during local feature development and automated testing:

  • The 'Don't Keep Activities' Trap: Located within Android Developer Options, this setting forces the OS to destroy every Activity as soon as the user navigates away from it. However, it specifically avoids killing the hosting application process. As a result, static variables, Dagger/Hilt component singletons, RxJava/Coroutine singleton event buses, and ViewModel stores remain intact. Testing with this toggle creates a false sense of security while letting fatal process death bugs reach production.
  • Manual ADB Command Line Scripting: Developers can simulate real process termination using adb shell am kill <package_name>. However, executing this command manually requires the engineer to:
    1. Manually tap through a multi-step form or checkout funnel on a connected handset.
    2. Press the home button to transition the application to the background (a running foreground process cannot be killed by am kill).
    3. Open a terminal workstation, query adb shell pidof <package_name>, and execute adb shell am kill <package_name>.
    4. Manually tap the app icon or recent apps list on the phone.
    5. Visually inspect whether form fields, drop-down menus, and scroll offsets were preserved.
      This manual loop is exhausting, impossible to scale across dozens of regression screens, and incapable of programmatic assertion.
  • Unit Testing Frameworks (Robolectric): While Robolectric allows developers to invoke activityController.recreate(), it runs inside a desktop JVM sandbox rather than a real Android ART runtime. It cannot simulate hardware graphics buffers, true IPC Binder size limits, or vendor-specific OEM memory manager quirks (such as aggressive background killing on Samsung One UI or Xiaomi MIUI).
Testing Methodology Process Termination Fidelity State Verification Depth Multi-Step Navigation Automation Execution Overhead Regression Flakiness
Developer Options ("Don't Keep Activities") Low (Process heap & singletons stay alive) Low (Manual visual check only) None (Manual human interaction) Low (Single device setting) Medium (Fails to catch singleton bugs)
Manual Terminal ADB (am kill) High (Genuine Linux process termination) Low (Subjective human inspection) None (Manual terminal typing) High (Manual repetitive cycle) High (Prone to human oversight)
Robolectric Recreate (buildActivity()) Medium (Synthetic JVM simulation) High (Unit test assertions) High (Programmatic code flow) Low (Fast local JVM run) High (Differs from real device ART)
Standard UI Automator / Espresso Medium (Complex to script process death) High (View assertions) Medium (Rigid, brittle selectors) High (Heavy test APK compilation) High (Flaky on async timing)
Claude Code + ScreenSync MCP Complete (Genuine OS process death & restart) Complete (Structural UI tree + Visual OCR) Autonomous (Adaptive AI agent navigation) Minimal (Zero test APK, stdio MCP) Very Low (Self-healing AI retry loop)

By moving to an autonomous agent architecture powered by ScreenSync MCP and Claude Code, mobile developers can automate the entire lifecycle verification pipeline across real physical hardware.

ScreenSync MCP Architecture for Autonomous Lifecycle Testing

ScreenSync MCP implements a dual-plane architecture separating high-speed screen inspection from device control commands. Screen capture streams directly through Android's MediaProjection API requiring no root privileges and no ADB overhead, while 29 phone and desktop tools operate over ADB to inject touches, navigate systems, terminate processes, and extract comprehensive logcat crash dumps.

Understanding the mechanics of ScreenSync MCP explains why it excels at mobile lifecycle automation. ScreenSync MCP is an Android and browser Model Context Protocol server developed by EpsolDev, comprising 226 MCP tools in total:

  • 29 Phone and Desktop Tools: 12 control_* tools (tap, swipe, type, long-press, key events, launch app), 3 os_* desktop control tools, and 14 capture and telemetry tools (such as get_latest_screenshot, get_ui_hierarchy, get_logcat, record_screen).
  • 197 Browser Tools: Comprehensive web_* tools for testing web applications and hybrid views.
+------------------------------------------------------------------------------------+
|                         ScreenSync MCP Testing Architecture                         |
+------------------------------------------------------------------------------------+
|                                                                                    |
|  +-----------------------+                    +---------------------------------+  |
|  |      Claude Code      | <--( stdio )-----> |   ScreenSync MCP Desktop Hub    |  |
|  | (Anthropic AI Agent)  |                    |      (Node.js 18+ on port 3000) |  |
|  +-----------------------+                    +---------------------------------+  |
|                                                          |             |           |
|              +-------------------------------------------+             |           |
|              | Control Plane (ADB USB / Wi-Fi)                         | Data      |
|              v                                                         | Plane     |
|  +------------------------------------------------------+              | (LAN)     |
|  |               Physical Android Device                |              v           |
|  |  +-----------------------+   +--------------------+  |    +------------------+  |
|  |  | ScreenSync Android App|   | Target Application |  |    | MediaProjection  |  |
|  |  | (com.screensync.mcp)  |   | (App under test)   |  |    | High-Speed Frame |  |
|  |  +-----------------------+   +--------------------+  |    | Capture (No ADB) |  |
|  |              |                          |            |    +------------------+  |
|  |              +--- Accessibility Tree ---+            |              |           |
|  +------------------------------------------------------+              |           |
|                                                                        v           |
|  Sub-100ms Mirroring Stream (94ms median on LAN) ----------------------+           |
+------------------------------------------------------------------------------------+

The system operates via two dedicated communication channels:

  1. High-Speed Vision Plane (MediaProjection): The ScreenSync MCP Android client (com.screensync.mcp, available for Android 7.0 and later on Google Play) establishes an efficient frame capture pipeline utilizing Android's native MediaProjection API. This capture pipeline requires zero root privileges and zero ADB bandwidth, delivering high-definition frames to the desktop hub with a tested median latency of 94 ms on local LAN connections. Privacy is preserved by design: video frames transmit strictly between the phone and the local hub on the local area network, never routing through EpsolDev cloud servers.
  2. Command & Telemetry Plane (ADB): Control tools (control_tap, control_type, control_swipe, control_key, control_launch_app) and diagnostic tools (get_logcat, get_ui_hierarchy) operate through the Android Debug Bridge over USB or wireless debugging.

This separation of concerns is vital during process death testing. While ADB handles the aggressive operational commands—such as issuing KEYCODE_HOME (key 3) and triggering am kill—the MediaProjection and Accessibility pipelines deliver real-time visual and semantic node feedback, allowing Claude Code to observe exactly how the target application behaves upon rebirth.

Configuring the Environment for Process Death Verification

Setting up Claude Code with ScreenSync MCP requires installing Node.js 18 or later, pairing an Android 7.0 or higher device via USB or wireless debugging, and registering the hub command in Claude Code using stdio transport. Once registered, Claude Code gains direct terminal access to mobile inspection, gesture injection, and process lifecycle tools.

Getting started takes less than five minutes. Ensure your development machine has Node.js 18+ installed and that ADB is available on your system path.

1. Launch the Desktop Hub

Download the desktop hub from the official ScreenSync MCP setup guide and launch it using the startup script:

# On Windows
start-hub.bat

# On macOS or Linux
./start-hub.sh

The hub initializes its local HTTP server on port 3000 and prepares its Model Context Protocol stdio transport interfaces.

2. Pair the Physical Device

Install the free client application from Google Play. Open the app on your Android device and pair it with the hub using either:

  • QR Code Scanning: Scan the QR code displayed in the hub console window.
  • mDNS Auto-Discovery: Let the app automatically discover the desktop hub over your local Wi-Fi network.

Grant the single-session MediaProjection screen recording permission when prompted on the handset screen.

3. Register ScreenSync MCP with Claude Code

In your workstation terminal, register the ScreenSync MCP server with Claude Code by executing:

claude mcp add screensync node "/path/to/hub/screensync-hub.js"

To configure Claude Desktop or other desktop IDEs (such as Cursor or VS Code agent mode), add the server entry to your configuration JSON file:

{
  "mcpServers": {
    "screensync": {
      "command": "node",
      "args": ["/path/to/hub/screensync-hub.js"],
      "env": {
        "SCREEN_SYNC_TOKEN": "your-hub-token"
      }
    }
  }
}

Verify the connection by launching Claude Code and executing /mcp. Claude will list the 226 connected tools, including mobile inspection and control commands. For a complete deep dive into all available functions, refer to our complete Android MCP tool reference guide.

Constructing the Process Death Verification Pipeline

A robust process death verification pipeline follows a five-stage test cycle: form population, backgrounding via home key, process termination via Android Debug Bridge shell, cold activity restart, and structural assertion. Claude Code coordinates these steps sequentially, comparing accessibility hierarchy trees before and after process termination to verify uncompromised state restoration.

When Claude Code evaluates an Android screen for process death resilience, it executes a rigorous five-phase operational loop:

+-----------------------------------------------------------------------------------+
|               Five-Phase Automated Process Death Verification Cycle                |
+-----------------------------------------------------------------------------------+
|  Phase 1: Input & Navigation     | Claude Code uses control_tap & control_type    |
|                                  | to populate form fields, toggles & lists.      |
|                                  v                                                |
|  Phase 2: Baseline Snapshot      | Claude Code calls get_ui_hierarchy to snapshot |
|                                  | exact node properties, text values & states.   |
|                                  v                                                |
|  Phase 3: Background Transition  | Claude Code sends control_key 3 (KEYCODE_HOME) |
|                                  | to trigger onSaveInstanceState() lifecycle.    |
|                                  v                                                |
|  Phase 4: Kernel Process Kill    | Claude Code executes `am kill <package_name>`   |
|                                  | via ADB shell to destroy process heap.         |
|                                  v                                                |
|  Phase 5: Relaunch & Tree Diff   | Claude Code invokes control_launch_app, takes  |
|                                  | get_ui_hierarchy & get_latest_screenshot,     |
|                                  | and programmatically diffs restored state.     |
+-----------------------------------------------------------------------------------+

Here is a verified Node.js test harness script demonstrating how Claude Code coordinates the ScreenSync MCP tools programmatically:

/**
 * Automated Android Process Death and State Restoration Test Harness
 * Demonstrating deterministic lifecycle verification using ScreenSync MCP tools
 */

const targetPackage = "com.example.checkoutapp";
const targetActivity = "com.example.checkoutapp.ui.CheckoutActivity";

async function runProcessDeathVerification(mcp) {
  console.log("=== Phase 1: Navigating to Multi-Step Checkout ===");
  // Launch target checkout activity
  await mcp.callTool("control_launch_app", {
    package: targetPackage,
    activity: targetActivity
  });

  // Tap billing input and enter user data
  await mcp.callTool("control_tap", { x: 350, y: 720 });
  await mcp.callTool("control_type", { text: "Jane Developer" });

  // Select shipping method and toggle express delivery
  await mcp.callTool("control_tap", { x: 500, y: 1140 });

  console.log("=== Phase 2: Capturing Baseline UI Hierarchy ===");
  const baselineHierarchy = await mcp.callTool("get_ui_hierarchy", {});
  const baselineNodes = JSON.parse(baselineHierarchy.content[0].text);

  // Extract baseline form field values
  const baselineName = findNodeTextById(baselineNodes, "input_customer_name");
  const baselineExpressToggle = findNodeCheckedById(baselineNodes, "switch_express");

  console.log(`Baseline Recorded: Name='${baselineName}', Express=${baselineExpressToggle}`);

  console.log("=== Phase 3: Moving Application to Background ===");
  // Trigger onPause() and onSaveInstanceState() by pressing Android HOME key (Keycode 3)
  await mcp.callTool("control_key", { keycode: 3 });

  // Allow OS lifecycle serialization to complete
  await new Promise((resolve) => setTimeout(resolve, 1500));

  console.log("=== Phase 4: Simulating Kernel Process Death ===");
  // Terminate background cached process using ADB am kill
  // Note: Target package must be in background for am kill to succeed
  await mcp.callTool("run_shell", {
    command: `adb shell am kill ${targetPackage}`
  });

  console.log("=== Phase 5: Relaunching Application from Recents/Launcher ===");
  await mcp.callTool("control_launch_app", {
    package: targetPackage,
    activity: targetActivity
  });

  // Wait for activity recreation and state bundle inflation
  await new Promise((resolve) => setTimeout(resolve, 2000));

  // Capture post-restoration hierarchy and visual screenshot
  const restoredHierarchy = await mcp.callTool("get_ui_hierarchy", {});
  const restoredNodes = JSON.parse(restoredHierarchy.content[0].text);
  const screenshot = await mcp.callTool("get_latest_screenshot", {});

  const restoredName = findNodeTextById(restoredNodes, "input_customer_name");
  const restoredExpressToggle = findNodeCheckedById(restoredNodes, "switch_express");

  console.log(`Restored State: Name='${restoredName}', Express=${restoredExpressToggle}`);

  // Assertion check
  if (restoredName === baselineName && restoredExpressToggle === baselineExpressToggle) {
    console.log(">>> SUCCESS: Application successfully restored state after process death!");
    return { status: "PASSED", screenshotUrl: screenshot.url };
  } else {
    console.error(">>> FAILURE: State regression detected! Form data lost after process death.");
    // Pull logcat buffer to inspect potential lifecycle or serialization crashes
    const crashLogs = await mcp.callTool("get_logcat", { lines: 100, filter: "AndroidRuntime" });
    return { status: "FAILED", errors: crashLogs };
  }
}

function findNodeTextById(node, resourceId) {
  if (node.resourceId && node.resourceId.includes(resourceId)) return node.text;
  if (node.children) {
    for (const child of node.children) {
      const match = findNodeTextById(child, resourceId);
      if (match !== undefined) return match;
    }
  }
  return null;
}

function findNodeCheckedById(node, resourceId) {
  if (node.resourceId && node.resourceId.includes(resourceId)) return node.checked;
  if (node.children) {
    for (const child of node.children) {
      const match = findNodeCheckedById(child, resourceId);
      if (match !== undefined) return match;
    }
  }
  return null;
}

This automated sequence verifies state survival without requiring test engineers to manually re-enter data. For further insights on how structural accessibility node trees eliminate visual ambiguity during agent testing, see our comprehensive guide on how AI agents use Android UI trees not screenshots.

Verifying Jetpack Compose and ViewModel State Persistence

Jetpack Compose applications frequently suffer silent state loss when developers use remember instead of rememberSaveable for interactive UI states. Claude Code navigates deeply nested Compose component hierarchies using ScreenSync MCP's get_ui_hierarchy tool, inspecting text attributes, toggle states, and scroll positions across process eviction boundaries to detect lost state.

In modern Android development using Jetpack Compose and Kotlin Coroutines, state management is split across two primary layers:

1. UI Tree State (remember vs rememberSaveable)

A common source of regressions in Compose applications is the misuse of remember:

// BAD: State is lost on process death AND configuration change
var searchQuery by remember { mutableStateOf("") }

// GOOD: State is persisted into SavedStateRegistry automatically
var searchQuery by rememberSaveable { mutableStateOf("") }

When custom complex data types are stored in Compose state, standard rememberSaveable fails unless developers provide a custom Saver:

data class FilterSelection(val categoryId: String, val minRating: Float, val isDiscounted: Boolean)

val FilterSelectionSaver = run {
    val categoryKey = "category_id"
    val ratingKey = "min_rating"
    val discountKey = "is_discounted"
    mapSaver(
        save = { mapOf(categoryKey to it.categoryId, ratingKey to it.minRating, discountKey to it.isDiscounted) },
        restore = {
            FilterSelection(
                categoryId = it[categoryKey] as String,
                minRating = (it[ratingKey] as Double).toFloat(),
                isDiscounted = it[discountKey] as Boolean
            )
        }
    )
}

// Persisting complex domain state across process eviction
@Composable
fun ProductSearchScreen() {
    var filters by rememberSaveable(stateSaver = FilterSelectionSaver) {
        mutableStateOf(FilterSelection("electronics", 4.0f, true))
    }
    // UI rendering...
}

Claude Code identifies whether rememberSaveable was correctly implemented by checking semantic properties inside the Compose accessibility tree. For an in-depth breakdown of testing Compose UI elements, review our article on testing Jetpack Compose apps with Claude Code and MCP.

2. ViewModel State (SavedStateHandle)

In production MVVM or MVI architectures, state lives inside the ViewModel. Jetpack provides SavedStateHandle, which receives serialized data from the Activity's savedInstanceState automatically:

class CheckoutViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val paymentRepository: PaymentRepository
) : ViewModel() {

    // Reading persisted state on process recreation
    val customerName: StateFlow<String> = savedStateHandle.getStateFlow(KEY_NAME, "")

    fun updateCustomerName(name: String) {
        savedStateHandle[KEY_NAME] = name
    }

    companion object {
        private const val KEY_NAME = "checkout_customer_name"
    }
}

If an engineer initializes ViewModel properties strictly from in-memory constructor parameters or network responses without backing them with SavedStateHandle, the ViewModel will instantiate with blank default values post-eviction. ScreenSync MCP's get_ui_hierarchy immediately flags this discrepancy by comparing pre-eviction node text with post-eviction node text.

Automating Crash Forensics with Logcat and Structural Node Trees

Post-eviction relaunch crashes typically occur when activities attempt to access uninitialized lateinit variables, null view bindings, or deserialization failures. Claude Code automatically queries ScreenSync MCP's get_logcat tool immediately following an app launch failure, parsing stack traces to isolate lifecycle ordering bugs and identify missing SavedStateHandle entries.

When an application fails to survive process death, the symptom is rarely a clean exit. More frequently, the app crashes immediately during onCreate() or onStart(). Claude Code leverages ScreenSync MCP's get_logcat tool to capture real-time system logs, filtering stack traces by PID and log level.

Failure Mode Root Cause Mechanism Diagnostic Logcat Signature ScreenSync MCP Tool & Assertion Remediation Architecture Pattern
Uninitialized Lateinit Property Dependency or ViewBinding initialized in onCreate() assumes non-null user session kotlin.UninitializedPropertyAccessException: lateinit property userSession has not been initialized get_logcat {filter: "FATAL EXCEPTION"} checks for immediate crash on launch Guard access with session validation; route to login if auth token is expired
TransactionTooLargeException Application attempts to serialize over 1MB of bitmap data or long item lists into Bundle android.os.TransactionTooLargeException: data parcel size 1248512 bytes get_logcat {lines: 50, filter: "TransactionTooLarge"} catches Binder overflow Store large payloads in local SQLite/Room database; store only IDs in SavedStateHandle
Silent State Wipe Screen UI relies on remember instead of rememberSaveable Clean relaunch logs, but input fields revert to empty default strings get_ui_hierarchy diff reveals node text property reset to "" Replace remember with rememberSaveable; implement custom mapSaver for models
Fragment ClassNotFoundException Non-default Fragment constructor used without custom FragmentFactory androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment... could not find FragmentFactory get_logcat highlights reflection failure during Fragment manager restore Register custom FragmentFactory in Application class or pass arguments via Bundle
Null Bundle Extraction Activity reads intent.extras or savedInstanceState without null checks java.lang.NullPointerException: Attempt to invoke virtual method on a null object reference get_ui_hierarchy fails to render; logcat records NPE in onCreate Use Kotlin safe calls (?.) and validate state presence with SavedStateHandle defaults

By coupling automated stack trace triage with visual frame validation, development teams can eliminate recurring crash reports before pushing builds to the Play Store. To diagnose memory leaks that exacerbate kernel process eviction, explore our guide on testing Android memory leaks with Claude Code. Furthermore, optimizing cold activity start times post-eviction is covered in our tutorial on testing Android app startup with Claude Code.

Deploying Autonomous Lifecycle Checks into Continuous Integration

Integrating autonomous process death testing into continuous integration workflows involves running Claude Code scripts against connected physical test devices or headless emulators. Automated runners invoke ScreenSync MCP in headless mode, executing deterministic test suites on pull requests and generating structured pass/fail reports with visual screenshot artifacts and logcat dumps.

To ensure your mobile application remains permanently protected against process death regressions, integrate the ScreenSync MCP test harness into your GitHub Actions or self-hosted CI/CD pipeline:

#!/usr/bin/env bash
# Unattended CI Pipeline for Android Process Death Testing
set -euo pipefail

echo ">>> Starting ScreenSync MCP Hub in Headless CI Mode..."
node /opt/screensync-hub/screensync-hub.js --headless --port 3000 &
HUB_PID=$!

# Ensure hub shuts down on exit
trap "kill -9 $HUB_PID" EXIT

echo ">>> Checking Connected ADB Devices..."
adb devices -l

# Wait for Android emulator or device boot completion
adb wait-for-device
boot_completed=""
while [ "$boot_completed" != "1" ]; do
  sleep 2
  boot_completed=$(adb shell getprop sys.boot_completed 2>/dev/null || true)
done
echo ">>> Device Boot Complete."

# Install latest release APK
echo ">>> Installing Target Application APK..."
adb install -r -d ./app/build/outputs/apk/debug/app-debug.apk

# Execute Claude Code test runner
echo ">>> Executing Autonomous Process Death Test Suite via Claude Code..."
claude code run "Run the process death test suite on com.example.checkoutapp. Verify that all shipping form fields, selected items, and coupon codes survive kernel process eviction using ScreenSync MCP. Output a Markdown test report."

echo ">>> Test Run Complete. Evaluating Exit Codes..."

Because ScreenSync MCP communicates over standard Model Context Protocol transports, it integrates natively into any modern agent orchestration platform. Whether running on a developer laptop or within a dedicated on-premise hardware rack, the suite runs autonomously, capturing full-fidelity regression evidence without human supervision.

Engineering Summary and Next Steps

Testing for Android process death has long been one of mobile engineering's most neglected disciplines, largely due to the frustrating limitations of Developer Options and manual terminal scripting. By combining Anthropic's Claude Code agent with the dual-plane capabilities of ScreenSync MCP, developers can finally automate genuine process eviction, UI tree state verification, and crash triage in a continuous, deterministic workflow.

To get started with autonomous Android lifecycle testing in your own development environment:

  1. Install the Mobile Client: Download the free ScreenSync MCP client app from Google Play.
  2. Download the Desktop Hub: Follow the step-by-step setup instructions on our official official ScreenSync MCP setup documentation.
  3. Explore Architectural Capabilities: Compare ScreenSync MCP against legacy mobile automation frameworks on our Android MCP servers comparison page and discover how browser workflows unify with mobile testing via the Chrome and Edge extension.
FAQ

Questions, answered

What is the difference between Android configuration change and process death?

A configuration change destroys and recreates the visible Activity while keeping the underlying Linux application process alive in memory. Kernel process death abruptly terminates the entire virtual machine heap, destroying all singleton classes, in-memory caches, and ViewModels, requiring the application to reconstruct state solely from serialized system bundles.

Why doesn't 'Don't keep activities' catch all process death bugs?

The 'Don't keep activities' developer toggle only recreates activities upon navigation, leaving application singletons, dependency injection graphs, and ViewModel instances intact in heap memory. Because the process is never actually killed, it cannot detect crashes caused by uninitialized static variables or missing SavedStateHandle entries.

Does ScreenSync MCP require root access or ADB for screen capture?

ScreenSync MCP captures high-speed screen frames using Android's native MediaProjection API after user consent, requiring no root access and no ADB connection. However, control tools like tap, swipe, and shell process management run via ADB over USB or wireless debugging.

How does Claude Code simulate Android process death during an automated test run?

Claude Code backgrounds the target application by sending KEYCODE_HOME via control_key 3, allowing Android to serialize state into onSaveInstanceState. It then executes am kill via the command line to terminate the background process before issuing control_launch_app to trigger state restoration.

How does ScreenSync MCP verify Jetpack Compose state restoration?

ScreenSync MCP extracts the structural accessibility node tree via get_ui_hierarchy. Claude Code compares pre-eviction node properties such as text fields, toggle switches, and list scroll offsets against post-relaunch nodes, immediately detecting state loss caused by using remember instead of rememberSaveable.

Can Claude Code test process death on a real Android device without a USB cable?

Yes. ScreenSync MCP supports wireless ADB debugging alongside local network mDNS discovery. Once paired over Wi-Fi, Claude Code communicates with the local desktop hub over stdio, streaming frames and executing control commands wirelessly without physical USB tethering. Developers can trigger remote shell kills and monitor state restoration across untethered handsets effortlessly.

What happens if an application exceeds the 1MB Android binder transaction limit?

Android allocates a shared 1MB buffer across all active binder transactions per process. If an application serializes large bitmaps, raw images, or extensive data lists into outState or SavedStateHandle, the Android OS throws a TransactionTooLargeException crash immediately upon backgrounding. Teams should persist large datasets into local databases like Room and store only identifier keys in the saved state bundle.

Which MCP clients support ScreenSync MCP for automated Android testing?

ScreenSync MCP works with any Model Context Protocol client supporting local stdio servers, including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. Web-based chat clients requiring remote HTTPS endpoints are not supported due to local network requirements.

Written by

ScreenSync MCP team

The ScreenSync MCP team at EpsolDev builds the Android app, the desktop MCP hub and the browser extension that let AI agents see and control a real phone. These guides come out of that work.