ScreenSync MCP guide

Testing Android In-App Updates with Claude Code and MCP

Test Google Play In-App Updates, Flexible background downloads, and Immediate update flows with Claude Code and ScreenSync MCP on real Android hardware.

Testing Android In-App Updates with Claude Code and MCP

Testing Google Play in-app updates on physical Android devices represents one of the most failure-prone, manually intensive phases of mobile release engineering. While unit tests and mock managers can assert that callback methods fire in isolation, they cannot verify whether a flexible download stalls under real cellular packet loss, whether an immediate update dialog properly blocks user navigation, or whether an activity cleanly resumes its update state machine after system-initiated process death. Coupling Claude Code with ScreenSync MCP provides mobile engineering teams with an autonomous, real-hardware verification pipeline that bridges local Dalvik bytecode execution, Google Play Core lifecycle states, and live accessibility tree validation without rooting the device or compromising proprietary code.

Key Takeaways

  • Google Play in-app updates require distinct architectural handling for Flexible background downloads versus Immediate full-screen blocking flows, each exposing unique lifecycle pitfalls.
  • Unit testing with FakeAppUpdateManager verifies state transitions programmatically but renders no visual UI dialogs on real devices, leaving production UI/UX integration untested.
  • ScreenSync MCP enables Claude Code to inspect the physical Android accessibility hierarchy via get_ui_hierarchy and capture frame buffers via native MediaProjection without requiring root access.
  • Device control actions—including tapping update dialog buttons, simulating home button backgrounding, and triggering network toggles—execute reliably through ADB over USB or wireless debugging.
  • Resuming flexible updates across activity recreation and Android process death requires continuous polling in onResume() to ensure the restart snackbar never disappears from the user.
  • Local streaming architecture ensures video frames remain isolated on your local developer network on port 3000, with only agent-requested screenshots passing to your model provider.

Google Play In-App Update Architecture: Flexible vs Immediate Workflows

Google Play In-App Updates enable Android applications to prompt active users for version updates directly within the app interface. The Google Play Core library provides two distinct interaction models—Flexible updates and Immediate updates—each governed by independent state machines, priority thresholds, and user experience constraints.

Direct Answer: How In-App Updates Function on Android

Google Play in-app updates operate through the Play Core library's AppUpdateManager, which queries Google Play services to compare the installed version code against available releases. Flexible updates download APK splits in the background while the user continues interacting, requiring an explicit reload once downloaded. Immediate updates display a blocking Google Play modal that prevents all user interaction until the update finishes downloading and installing.

The Flexible Update Lifecycle and Background Synchronization

Flexible updates are engineered for non-critical improvements, feature enhancements, and incremental bug fixes where interrupting the user session is undesirable. When an app initiates a flexible flow, Google Play services stream the update packages in the background while the foreground activity remains interactive.

To manage this asynchronous pipeline, your application must register an InstallStateUpdatedListener. This listener monitors state transitions through several discrete enumeration values:

  1. InstallStatus.PENDING: Google Play services have accepted the request and are queuing the download.
  2. InstallStatus.DOWNLOADING: The update payload is actively streaming. Progress can be monitored via bytesDownloaded() and totalBytesToDownload().
  3. InstallStatus.DOWNLOADED: All APK split artifacts have been verified and cached locally. The app is ready for installation.
  4. InstallStatus.INSTALLING: The Google Play installer daemon is applying the package delta.
  5. InstallStatus.INSTALLED: The package has successfully updated.
  6. InstallStatus.FAILED or InstallStatus.CANCELED: The network dropped, storage was insufficient, or the user aborted the flow.

The most critical engineering failure in flexible update implementations occurs between InstallStatus.DOWNLOADED and InstallStatus.INSTALLING. Unlike traditional app store installs that occur while the application is closed, a flexible update requires the host application to actively invoke appUpdateManager.completeUpdate(). If your application fails to display a persistent snackbar or dialog prompt when DOWNLOADED state is reached, the updated files sit idle in the app cache indefinitely, and the user continues running stale code.

The Immediate Update Lifecycle and Modal Blocking

Immediate updates are designed for critical security patches, breaking database schema migrations, and deprecated API protocol sunsets. When an immediate update triggers, Google Play services launch a full-screen, modal activity that superimposes itself directly over your task stack.

During an immediate update, user interaction with your application is entirely blocked. The modal displays a progress bar, release notes, and download metrics managed directly by the Google Play Store client. Once the download completes, Google Play immediately triggers the installation daemon, terminating your existing process and restarting the newly installed version automatically.

However, immediate updates introduce severe failure modes when users background the application during download or when low memory kills the backgrounded host process. If your activity does not verify AppUpdateType.IMMEDIATE status within its onResume() lifecycle callback, an interrupted download leaves the user trapped in an un-updated, half-migrated state upon relaunch. For comprehensive strategies on validating background lifecycle transitions, explore our guide on testing Android process death with Claude Code and MCP.

Update Characteristic Flexible In-App Update Immediate In-App Update
Primary Use Case Minor patches, UI enhancements, optional features Breaking API changes, security hotfixes, critical migrations
User Interruption Level Non-blocking; background download with optional banner Modal blocking; full-screen Google Play dialog covers app
Download Experience User continues browsing while assets stream App usage blocked until installation completes
Completion Trigger Manual invocation of completeUpdate() via UI snackbar Automated restart handled by Google Play services
Rollback / Cancel Behavior User can cancel at any prompt without penalty User cancellation aborts app access or closes task
Failure Vulnerability Dropped InstallStateUpdatedListener loses restart prompt Interrupted downloads fail to resume without onResume check
Network Data Consumption Graceful background retry on connection switch Aggressive foreground transfer; fails immediately on timeout

The Developer Dilemma: FakeAppUpdateManager vs Real Device Validation

Android developers traditionally validate in-app updates using unit tests backed by FakeAppUpdateManager. While this test double serves an essential role in CI unit test suites, relying exclusively on mocks creates dangerous blind spots for production deployments.

Direct Answer: Why Mocks Miss In-App Update Bugs

Mocking with FakeAppUpdateManager validates state machine listener logic programmatically, but it cannot render Google Play's native bottom sheets, cannot verify user touch targets on physical screens, and cannot test real Play Store throttling rules. Physical device testing with Claude Code and ScreenSync MCP validates the actual visual UI hierarchy, catches layout clipping across OEM skins, and verifies recovery under real hardware network constraints without manual tester intervention.

Limitations of FakeAppUpdateManager on Real Hardware

The official Google Play Core testing artifact provides FakeAppUpdateManager to simulate update availability, download progress, and user acceptance. However, executing FakeAppUpdateManager on a real physical device exposes severe architectural limitations:

  1. No Native System UI: Calling fakeAppUpdateManager.startUpdateFlowForResult() does not launch the Google Play bottom sheet or system modal. It simply flips internal boolean flags (isConfirmationDialogVisible(), isImmediateFlowVisible()).
  2. Missing System Dialog Overlay: Because no actual system window is created, you cannot verify whether your application properly pauses OpenGL renderers, cancels camera previews, or handles layout shifts when the system update sheet animates into view.
  3. No Process Termination Simulation: FakeAppUpdateManager cannot replicate the low-level SIGKILL signal that Google Play services dispatch to your application process when completeUpdate() executes on real hardware.
  4. Ignored Update Priorities: Google Play assigns integer priorities from 0 (lowest) to 5 (highest) to app releases. In production, apps use client-side logic to decide between flexible and immediate flows based on this priority. FakeAppUpdateManager requires manual programmatic mocking for each priority branch, frequently hiding misconfigured threshold comparisons.

Why Internal App Sharing Falls Short for Autonomous QA

The alternative recommended by Google is uploading version-incremented packages to Google Play's Internal App Sharing or Internal Test tracks. While this tests real Google Play services, it introduces massive friction:

  • Uploading bundles, waiting for Google processing, and updating version codes takes 10 to 30 minutes per iteration.
  • Google Play aggressively caches update metadata on the device, often refusing to show an available update for several hours despite cache clearing.
  • Manual testers must repeatedly tap update prompts, background apps, cycle Wi-Fi, and inspect logcat buffers by hand.

By connecting Claude Code for Android app testing on a real device via ScreenSync MCP, developers achieve the best of both worlds: deterministic test triggers combined with live, physical hardware validation.

Verification Dimension Unit Tests (FakeAppUpdateManager) Internal App Sharing (Manual QA) Claude Code + ScreenSync MCP
Execution Environment JVM / Local Mock Physical Android Hardware Physical Android Hardware
Test Execution Speed Instant (< 2 seconds) Slow (15–45 min upload/sync loop) Rapid (1–3 min per automated scenario)
Visual UI Verification None (Headless state flags) Manual visual inspection by tester Deterministic node tree + visual capture
System Dialog Coverage Mocked; no real window drawn Real Google Play bottom sheets Real UI tree inspection & ADB control
Process Death Validation Theoretical state assertions Manual task manager swiping Automated ADB kill & launch verification
Logcat & Crash Analysis Unit test console logs Manual Android Studio logcat capture Direct get_logcat parsing in agent context
CI/CD Integration Local Gradle task High friction; requires human testers Headless CLI execution via Claude Code

Production Kotlin Architecture for Resilient In-App Updates

To ensure your Android application handles in-app updates reliably across all lifecycle transitions, you must encapsulate update logic in a dedicated, lifecycle-aware manager. The following production-grade implementation demonstrates clean separation of concerns, robust listener registration, and automatic state resumption.

package com.example.app.update

import android.app.Activity
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.InstallState
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

sealed class InAppUpdateUiState {
    object Idle : InAppUpdateUiState()
    object Checking : InAppUpdateUiState()
    data class UpdateAvailable(val updateInfo: AppUpdateInfo, val type: Int) : InAppUpdateUiState()
    data class Downloading(val bytesDownloaded: Long, val totalBytes: Long) : InAppUpdateUiState()
    object Downloaded : InAppUpdateUiState()
    data class Failed(val errorCode: Int) : InAppUpdateUiState()
}

class InAppUpdateController(
    private val appUpdateManager: AppUpdateManager
) : DefaultLifecycleObserver {

    private val _uiState = MutableStateFlow<InAppUpdateUiState>(InAppUpdateUiState.Idle)
    val uiState: StateFlow<InAppUpdateUiState> = _uiState.asStateFlow()

    private val installListener = InstallStateUpdatedListener { state: InstallState ->
        when (state.installStatus()) {
            InstallStatus.DOWNLOADING -> {
                _uiState.value = InAppUpdateUiState.Downloading(
                    bytesDownloaded = state.bytesDownloaded(),
                    totalBytes = state.totalBytesToDownload()
                )
            }
            InstallStatus.DOWNLOADED -> {
                _uiState.value = InAppUpdateUiState.Downloaded
            }
            InstallStatus.FAILED -> {
                _uiState.value = InAppUpdateUiState.Failed(state.installErrorCode())
            }
            InstallStatus.CANCELED -> {
                _uiState.value = InAppUpdateUiState.Idle
            }
            else -> Unit
        }
    }

    init {
        appUpdateManager.registerListener(installListener)
    }

    fun checkForUpdates(priorityThreshold: Int = 3) {
        _uiState.value = InAppUpdateUiState.Checking
        appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
            if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE) {
                val updateType = if (info.updatePriority() >= priorityThreshold &&
                    info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
                    AppUpdateType.IMMEDIATE
                } else if (info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
                    AppUpdateType.FLEXIBLE
                } else {
                    null
                }

                updateType?.let { type ->
                    _uiState.value = InAppUpdateUiState.UpdateAvailable(info, type)
                } ?: run {
                    _uiState.value = InAppUpdateUiState.Idle
                }
            } else {
                _uiState.value = InAppUpdateUiState.Idle
            }
        }.addOnFailureListener {
            _uiState.value = InAppUpdateUiState.Idle
        }
    }

    fun startUpdate(activity: Activity, updateInfo: AppUpdateInfo, type: Int, requestCode: Int) {
        appUpdateManager.startUpdateFlowForResult(updateInfo, type, activity, requestCode)
    }

    fun completeFlexibleUpdate() {
        appUpdateManager.completeUpdate()
    }

    override fun onResume(owner: LifecycleOwner) {
        // Crucial: Catch downloads completed while app was backgrounded
        appUpdateManager.appUpdateInfo.addOnSuccessListener { info ->
            if (info.installStatus() == InstallStatus.DOWNLOADED) {
                _uiState.value = InAppUpdateUiState.Downloaded
            } else if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
                // If an immediate update was in flight, resume it
                _uiState.value = InAppUpdateUiState.UpdateAvailable(info, AppUpdateType.IMMEDIATE)
            }
        }
    }

    override fun onDestroy(owner: LifecycleOwner) {
        appUpdateManager.unregisterListener(installListener)
    }
}

In your dependency injection graph (using Hilt or Koin), configure a provider that injects the real AppUpdateManagerFactory.create(context) for production builds, but easily substitutes FakeAppUpdateManager(context) when running debug automation suites. This architecture ensures complete testability across both automated unit loops and end-to-end device testing.

Setting Up Claude Code and ScreenSync MCP for Mobile Testing

To configure an autonomous AI agent capable of driving, inspecting, and asserting mobile app states on physical Android hardware, you connect Claude Code to the ScreenSync Model Context Protocol server.

Direct Answer: How ScreenSync MCP Connects Claude to Android

ScreenSync MCP runs as a local Node.js daemon that exposes 226 tools over standard input/output (stdio) to AI coding agents like Claude Code, Claude Desktop, and Cursor. Visual frames stream from the physical Android phone across your local network via Android's native MediaProjection API, while input controls execute through ADB, enabling the agent to inspect UI hierarchies and simulate touch events in real time.

Hub Architecture and Client Registration

ScreenSync MCP is engineered specifically for physical Android devices running Android 7.0 or later. The system operates on a dual-pathway architecture:

  1. Screen Capture Pathway: Uses Android's native MediaProjection API. Once the user taps "Start now" on the device screen prompt at the beginning of the session, the app streams display buffers directly across the local LAN to the desktop hub on HTTP port 3000. This capture pipeline requires no root access and zero ADB permissions, maintaining a median latency of approximately 94 ms on local Wi-Fi networks in our tests.
  2. Device Control Pathway: Action tools (control_tap, control_swipe, control_type, control_launch_app) execute through standard Android Debug Bridge (ADB) commands routed by the desktop hub over USB or wireless debugging.

To install and launch the local hub, ensure Node.js 18+ is available on your workstation (Windows, macOS, or Linux). Unpack the hub archive and run the platform startup script:

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

# On Windows PowerShell or Command Prompt
.\start-hub.bat

The hub initializes on local port 3000 and prints a connection QR code. Launch the free ScreenSync MCP app from Google Play (com.screensync.mcp), grant the MediaProjection screen capture prompt, and scan the QR code to pair your device over your local Wi-Fi network.

Next, register the hub as an active stdio MCP server inside Claude Code:

# Register ScreenSync MCP in Claude Code
claude mcp add screensync node "C:/tools/screensync/screensync-hub.js"

Verify that Claude Code recognizes the server by typing /mcp in the Claude Code CLI. You will see ScreenSync MCP listed with its 226 available tools: 29 phone and desktop tools (12 control_* action tools, 3 os_* host tools, and 14 capture and hub tools like get_latest_screenshot, get_ui_hierarchy, and get_logcat), plus 197 web_* browser tools powered by the Chrome/Edge extension. For complete step-by-step setup instructions, refer to the ScreenSync MCP setup guide.

{
  "mcpServers": {
    "screensync": {
      "command": "node",
      "args": ["C:/tools/screensync/screensync-hub.js"],
      "env": {
        "SCREEN_SYNC_TOKEN": "your_secure_bearer_token_here"
      }
    }
  }
}

This configuration also integrates seamlessly with Claude Desktop by placing the block above in claude_desktop_config.json via Settings > Developer.

Step-by-Step Production Walkthrough: Automated In-App Update QA Cycle

With Claude Code connected to your physical Android device, you can orchestrate comprehensive, multi-step in-app update test routines using conversational prompts. The agent coordinates UI inspection, gesture simulation, and log monitoring across real device hardware.

Step 1: Launch the Application and Inspect Initial State

Direct Claude Code to launch the target package on the connected phone and verify that the application cold starts into a stable state:

Launch com.example.app on the attached Android device. Confirm that the home activity is rendered, inspect the UI hierarchy to verify that no update prompts are currently active, and capture the current screenshot.

Claude Code calls control_launch_app followed by get_ui_hierarchy:

{
  "name": "control_launch_app",
  "arguments": {
    "package": "com.example.app",
    "activity": ".MainActivity"
  }
}

The agent parses the structural accessibility node tree returned by the device. Unlike pixel-based vision models that guess element positions, ScreenSync MCP returns exact view IDs, resource identifiers, and bounding boxes. For a detailed breakdown of why node trees outperform computer vision, read how AI agents use Android UI trees not screenshots.

Step 2: Trigger Flexible Update and Assert UI State

Instruct Claude Code to trigger an in-app update check in your debug test build:

Trigger the in-app update check in the app. Use get_ui_hierarchy to locate the update confirmation prompt. Verify that the dialog displays the update title, tap the 'Update' button, and assert that the download progress indicator begins tracking.

Claude Code retrieves the node tree, locates the target confirmation button, and dispatches a precise tap event via ADB:

{
  "name": "control_tap",
  "arguments": {
    "x": 540,
    "y": 1820
  }
}

The agent then monitors the accessibility tree to verify that a ProgressBar or download percentage TextView appears in the hierarchy, confirming that InstallStatus.DOWNLOADING is active.

Step 3: Verify the Completion Snackbar and Restart Execution

Once the download finishes, the application must display a persistent snackbar informing the user that the update is ready to install:

Wait for the flexible update download to complete. Call get_ui_hierarchy to assert the presence of the snackbar with text 'An update has just been downloaded'. Tap the 'RESTART' action button and verify via get_logcat that appUpdateManager.completeUpdate() was called without throwing exceptions.

Claude Code queries get_ui_hierarchy, confirms that the restart button is visible and clickable, and triggers control_tap. Immediately following the click, the agent calls get_logcat with a log filter to observe the Google Play Core runtime output:

{
  "name": "get_logcat",
  "arguments": {
    "filter": "PlayCore",
    "lines": 50
  }
}

The agent verifies that Google Play Core log entries report completeUpdate called successfully, confirming that the application restarted into the updated binary.

Deep Dive: Testing Lifecycle Edge Cases and Failure Modes

The primary reason in-app updates fail in production is unhandled Android lifecycle interruptions. Below are the three most critical edge cases that engineering teams must validate using Claude Code on physical hardware.

1. Simulating Background Process Death During Flexible Downloads

When users minimize an app during a 50MB flexible update, Android's low-memory killer (LMK) may terminate the host activity while Google Play services continue streaming the APK in the background.

To test this edge case with Claude Code:

  1. Start a flexible download.
  2. Instruct Claude to background the app using control_key with keycode 3 (KEYCODE_HOME).
  3. Direct Claude to kill the app process via ADB shell: am kill com.example.app.
  4. Launch the app again using control_launch_app.
  5. Call get_ui_hierarchy to assert that InAppUpdateController.onResume() immediately detects InstallStatus.DOWNLOADED and restores the restart snackbar.

If your code forgot to query appUpdateManager.appUpdateInfo in onResume(), the snackbar will not appear, and Claude Code will flag the defect immediately. To explore advanced memory and crash simulation patterns, see our guide on testing Android memory leaks with Claude Code and MCP.

2. Testing Network Flapping and Stalled Downloads

Mobile devices frequently transition between Wi-Fi and cellular networks, causing momentary socket resets. While Google Play services automatically pause and resume downloads, applications that implement custom download progress UI often get stuck in infinite progress loops.

Using ScreenSync MCP, Claude Code can toggle airplane mode or disconnect Wi-Fi via ADB commands:

adb shell svc wifi disable

The agent observes whether your InstallStateUpdatedListener transitions cleanly to InstallStatus.FAILED or surfaces a retry CTA. After re-enabling Wi-Fi (adb shell svc wifi enable), Claude Code validates whether the download resumes without duplicating background threads.

3. Immediate Update Cancellation and Forced Exit Enforcement

Immediate updates must be non-negotiable: if a user cancels the Google Play immediate update dialog (by tapping the system back button), the application must not allow them to continue using obsolete code.

To verify this constraint:

  1. Trigger an immediate update flow.
  2. When the full-screen Google Play modal appears, instruct Claude to press the system back key (control_key keycode 4).
  3. In onActivityResult or your ActivityResultLauncher, your activity must inspect resultCode == Activity.RESULT_CANCELED. If canceled, the app should finish the activity or re-launch the update dialog.
  4. Claude Code checks get_ui_hierarchy to ensure the home dashboard is never accessible. If the app failed to terminate or re-prompt, Claude logs a critical compliance bug.

For complementary validation of release-time code shrinking, resource stripping, and ProGuard obfuscation rules that might inadvertently mangle Google Play Core interface signatures, consult our guide on testing Android R8 and ProGuard with Claude Code and MCP.

Security, Token Authentication, and Local Privacy Architecture

Automating real mobile devices in enterprise development environments requires strict adherence to security boundaries and data governance policies.

Local LAN Stream Isolation

The ScreenSync MCP architecture strictly isolates visual frame streaming within your local developer network. When the Android application transmits display frames via MediaProjection, packets travel directly over your local Wi-Fi or USB tether to the desktop hub running on HTTP port 3000. Display frames and video buffers are never uploaded to EpsolDev cloud servers. An optional Google Drive upload fallback exists within the app settings, but it remains entirely optional and disabled by default.

Model Context Protocol Token Security

Communication between the Android app, desktop hub, and AI coding agents is guarded by an ephemeral bearer token (SCREEN_SYNC_TOKEN). This token prevents unauthorized devices on shared office Wi-Fi networks from intercepting device streams or dispatching malicious ADB touch events.

AI Model Provider Data Transmission

Developers must understand the exact boundary where visual data leaves the local machine. Video streams processed by the ScreenSync MCP hub stay strictly local. However, when Claude Code or any connected AI agent explicitly calls get_latest_screenshot, that specific JPEG frame is embedded in the agent's context payload and transmitted to Anthropic's API endpoints for multimodal visual inference.

When conducting automated QA on production builds, developers should avoid displaying sensitive user personal identifiable information (PII), real credit card numbers, or live authentication secrets on screen. For end-to-end billing and digital goods validation, review our security practices in testing Android in-app purchases with Claude Code and MCP.

Pre-Testing Checklist for Android In-App Updates

Before initiating an automated in-app update test session with Claude Code and ScreenSync MCP, verify the following prerequisites:

  1. Physical Device OS Version: Ensure the target Android device runs Android 7.0 (API Level 24) or later.
  2. ScreenSync Android App: Install version 2.5.7 from Google Play (com.screensync.mcp) and grant the MediaProjection consent prompt.
  3. Desktop Hub Environment: Verify Node.js 18+ is installed on the host computer and launch hub version 1.14.4 via start-hub.bat or start-hub.sh on port 3000.
  4. ADB Connection: Confirm that the device is authorized for USB debugging or wireless debugging (adb devices lists the device as device, not unauthorized).
  5. Claude Code MCP Registration: Verify that claude mcp add screensync node "/path/to/hub/screensync-hub.js" is configured and active.
  6. Play Core Dependency: Confirm that com.google.android.play:app-update:2.1.0 (or app-update-ktx) is properly compiled into the target APK.
  7. Lifecycle Resumption Hook: Check that appUpdateManager.appUpdateInfo is queried inside onResume() to catch updates completed during process death.
  8. Network Discovery: Ensure the mobile device and development workstation share the same local subnet for mDNS pairing.

Conclusion

Testing Google Play in-app updates on real Android hardware is essential for delivering flawless mobile release experiences. By moving beyond synthetic mocks and bridging Claude Code with physical devices through ScreenSync MCP, engineering teams can autonomously validate flexible background downloads, immediate modal enforcement, and complex lifecycle recoveries with unmatched fidelity.

ScreenSync MCP provides the ScreenSync MCP Android app, ScreenSync MCP desktop hub, and ScreenSync MCP browser extension to mobile engineers worldwide. To explore comprehensive device control capabilities, consult the complete Android MCP tool reference, learn more about the overall ScreenSync MCP architecture, or explore our complete Android MCP server guide.

Ready to automate your Android testing workflows on real hardware? Download the free app on ScreenSync MCP on Google Play and follow the ScreenSync MCP setup guide to connect your AI agent today.

Next steps

FAQ

Questions, answered

Can Claude Code trigger and test Google Play in-app updates on real Android hardware?

Yes, Claude Code connects to the local ScreenSync MCP hub over stdio to trigger in-app updates, monitor download states, and inspect installation snackbars on physical Android hardware running Android 7.0 or later. Input actions like tapping update confirmations use ADB, while real-time visual verification uses Android's native MediaProjection API.

What is the difference between Flexible and Immediate in-app updates in Android?

Flexible in-app updates download APK bytes in the background while users continue interacting with the app, prompting a snackbar restart when downloaded. Immediate in-app updates display a full-screen Google Play blocking UI that forces users to download and restart before regaining access to the application.

Why does FakeAppUpdateManager fail to display real update UI dialogs on physical devices?

Google Play's FakeAppUpdateManager is a pure mock implementation bundled inside the Play Core library designed for unit and logic testing. It tracks state machines programmatically without contacting Google Play Store services, so no native bottom sheet, confirmation dialog, or installation banner appears on screen without custom UI bindings.

Does testing Android in-app updates with ScreenSync MCP require root access?

No, ScreenSync MCP requires no root access on your Android device. Screen capture runs through Android's native MediaProjection API following standard user consent per session, and input controls like tapping update buttons or dismissing dialogs execute through Android Debug Bridge (ADB) over USB or wireless debugging.

How do AI agents verify that an in-app update download completed without pixel guessing?

Instead of relying on brittle computer vision coordinates, Claude Code calls get_ui_hierarchy via ScreenSync MCP to inspect the live accessibility node tree. The agent deterministically asserts the presence of the completion snackbar, its action button text, and view IDs before calling control_tap to restart the app.

How can engineering teams automate in-app update testing before pushing builds to production?

Teams configure dependency injection to supply FakeAppUpdateManager in debug builds or publish version-incremented packages to Google Play Internal App Sharing. Claude Code then launches the app via ADB, verifies update prompts, simulates download completions, and monitors get_logcat to catch state leaks or dropped listeners.

What happens if an Android app undergoes process death during a flexible update download?

If Android terminates the hosting activity while an update downloads in the background, the download continues in Google Play Core services. Upon relaunch, the application's onResume() lifecycle callback must query appUpdateManager.appUpdateInfo and re-register InstallStateUpdatedListener to show the restart prompt if already downloaded.

Are application source code or screen recordings sent to EpsolDev servers during MCP testing?

No, video frames stream strictly across your local Wi-Fi network between the Android phone and your local desktop hub on port 3000. No frames or source files reach EpsolDev servers. When Claude Code executes get_latest_screenshot, only that specific captured frame is transmitted to Anthropic's model API.

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.