ScreenSync MCP guide

Testing Android Scoped Storage with Claude Code and MCP

Automate Android Scoped Storage, MediaStore queries, and Storage Access Framework audits on physical devices with Claude Code and ScreenSync MCP.

Testing Android Scoped Storage with Claude Code and MCP

Managing local data persistence, media capture, and file sharing on Android has undergone a monumental architectural transformation over the past decade. In early releases of the Android operating system, applications enjoyed relatively unrestricted access to the shared external filesystem via the broad READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE permissions. Any application possessing these entitlements could freely traverse the /sdcard/ directory tree, inspect photos captured by other applications, read sensitive configuration files, and leave orphaned data artifacts scattered across physical disk partitions long after package uninstallation. To dismantle this severe privacy and security vulnerability, Google engineered a complete paradigm shift: Scoped Storage. Initiated in Android 10 (API level 29) and enforced as an uncompromising platform invariant in Android 11 (API level 30) through Android 15, Scoped Storage confines every mobile application to an isolated, private sandbox directory while mediating all interactions with shared media collections and document trees through structured system abstraction layers: the MediaStore API, the Storage Access Framework (SAF), and the modern system Photo Picker.

While Scoped Storage drastically enhances user data confidentiality and prevents malicious filesystem tampering, it introduces immense technical complexity into mobile application development and quality assurance pipelines. Direct Java File path references that functioned reliably for years now throw fatal java.io.FileNotFoundException: open failed: EACCES (Permission denied) runtime exceptions. Background workers attempting to cache media files must navigate asynchronous ContentResolver queries, handle RecoverableSecurityException intents when requesting write access to shared assets, and coordinate with dynamic runtime permission boundaries introduced in Android 13 (READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO) and Android 14 (READ_MEDIA_VISUAL_USER_SELECTED). Traditional mobile test automation frameworks like Appium and Espresso struggle immensely when evaluating these storage workflows. Espresso operates exclusively within the application's local process space and cannot interact with out-of-process system document pickers (com.google.android.documentsui), while Appium scripts frequently crash due to timing discrepancies between ADB file transfers and Android's asynchronous MediaScanner indexing daemon. By integrating Anthropic's Claude Code coding agent with the Model Context Protocol (MCP) using ScreenSync MCP, software engineering organizations can establish fully autonomous, closed-loop testing pipelines on physical Android hardware. ScreenSync MCP equips terminal-based AI agents with decoupled real-time display streaming and native Android Debug Bridge (ADB) control tools, enabling Claude Code to seed test fixtures, drive system storage pickers, audit ContentProvider URIs, triage disk exceptions, and repair storage bugs directly within your application repository.

Key Takeaways

  • Architectural Sandbox Enforcement: Android Scoped Storage divides persistent disk storage into app-specific isolated directories, system-managed MediaStore collections, and Storage Access Framework (SAF) document trees, eliminating raw /sdcard/ path access.
  • Overcoming Out-of-Process Test Barriers: ScreenSync MCP enables Claude Code to inspect and interact with system document pickers and permission dialogs that run outside the target application process, circumventing traditional Espresso testing limits.
  • Bridging ADB Seeding and MediaStore Indexing: AI agent automation bridges the gap between raw adb push file transfers and Android's MediaScanner daemon, ensuring mock test assets are immediately visible to application database cursors.
  • Low-Latency Hardware Streaming: ScreenSync MCP streams display frames from physical Android devices via the native MediaProjection API (Android 7.0+, zero root, zero ADB overhead) at approximately 94 milliseconds median latency over local Wi-Fi.
  • Automated Android 14 Granular Permission Auditing: Claude Code systematically evaluates partial photo access (READ_MEDIA_VISUAL_USER_SELECTED), verifying that applications handle limited asset access without throwing uncaught security exceptions.
  • Unified Multi-Surface Verification Fleet: Developers command 226 MCP tools—spanning 29 mobile and desktop tools (12 control, 3 OS, 14 capture and telemetry) alongside 197 browser automation tools—to validate full-stack mobile and cloud storage sync pipelines.

The Android Scoped Storage Architecture: Sandboxes, MediaStore, and SAF

Android Scoped Storage isolates file persistence into three distinct operational domains: private app-specific sandboxes, shared MediaStore collections for pictures and audio, and user-selected document trees accessed through the Storage Access Framework. Android restricts direct raw filesystem paths to prevent malicious snooping, enforcing granular URI permissions and strict ContentResolver access patterns across physical mobile hardware.

To design resilient automated test suites, quality engineers must understand how modern Android releases partition physical storage into isolated security boundaries:

1. App-Specific Private Storage

Every installed application receives a dedicated private sandbox within the internal storage directory (/data/user/0/<package_name>/files) and an optional external storage directory (/storage/emulated/0/Android/data/<package_name>/files). These directories belong exclusively to the application process.

  • Permission Requirements: Zero permissions required. Applications can read and write files within their sandbox without requesting runtime storage permissions.
  • Lifecycle: When the user uninstalls the application, the operating system recursively deletes the entire directory structure.
  • Testing Challenge: Automated test runners cannot easily pre-populate these directories on production builds without using specialized ADB run-as commands or debugging hooks.

2. Shared Media Collections (MediaStore API)

When applications create or inspect media intended for user-wide visibility—such as camera snapshots, audio recordings, or downloaded invoices—they must interface with the system MediaStore ContentProvider. MediaStore partitions assets into structured database tables:

  • MediaStore.Images.Media.EXTERNAL_CONTENT_URI
  • MediaStore.Video.Media.EXTERNAL_CONTENT_URI
  • MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
  • MediaStore.Downloads.EXTERNAL_CONTENT_URI

Prior to Android 10, creating an image required writing raw bytes to /sdcard/DCIM/Camera/photo.jpg. Under Scoped Storage, applications insert metadata into ContentValues and obtain an OutputStream via contentResolver.openOutputStream(uri). While an application retains perpetual read and write access to files it created itself, reading media generated by third-party applications requires granular runtime permissions. In Android 13 (API 33), Google deprecated broad storage permissions in favor of granular entitlements: READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, and READ_MEDIA_AUDIO. In Android 14 (API 34), Google introduced READ_MEDIA_VISUAL_USER_SELECTED, enabling users to grant access to a selected subset of their media library.

3. Storage Access Framework (SAF)

For accessing arbitrary files outside media collections—such as EPUB books, PDF blueprints, or developer configuration bundles—applications must invoke the Storage Access Framework. SAF employs system intents (Intent.ACTION_OPEN_DOCUMENT, Intent.ACTION_CREATE_DOCUMENT, and Intent.ACTION_OPEN_DOCUMENT_TREE) to launch the system file picker (DocumentsUI). Once the user selects a file or directory tree, the application receives a persistent content URI (content://com.android.providers.media.documents/...). To retain access across application reboots, the application must invoke contentResolver.takePersistableUriPermission(uri, takeFlags).

Storage Domain File System Path Location Android Permission Required Lifecycle on App Uninstall Automation & Testing Friction Point
Internal App Sandbox /data/data/<package>/files/ None (Automatic) Completely wiped Inaccessible on release builds without root or run-as
External App Sandbox /Android/data/<package>/files/ None (Android 4.4+) Completely wiped Hidden from third-party file managers on Android 11+
MediaStore Images/Video /storage/emulated/0/Pictures/ READ_MEDIA_IMAGES (API 33+) Persists after uninstall adb push bypasses MediaScanner; files remain unindexed
MediaStore Downloads /storage/emulated/0/Download/ None for own files; SAF for others Persists after uninstall Direct file modification requires createWriteRequest()
Storage Access Framework (SAF) Arbitrary user-selected directories User selection via system picker Persists after uninstall Picker runs out-of-process; breaks in-app UI matchers
Photo Picker (Android 11+) Ephemeral system ContentProvider Zero permissions required N/A (Temporary URI) System modal requires cross-process coordinate control

Why Traditional Mobile Test Automation Fails on Scoped Storage

Traditional mobile test automation frameworks fail on Scoped Storage because system document pickers run inside isolated operating system processes outside the target application hierarchy. Furthermore, testing scripts relying on rigid Java File paths crash with permission exceptions, while test files pushed via ADB remain invisible to MediaStore queries until the system media scanner indexes newly added disk assets.

When engineering teams attempt to integrate storage validation into continuous integration pipelines, automated suites frequently suffer from brittle assertions and false-negative failures. These failures stem from four fundamental technical hurdles:

1. In-Process Test Isolation vs. System Pickers

Frameworks like Espresso, Compose Test, and Robolectric execute inside the application's runtime instrumentation sandbox. When an application launches ActivityResultContracts.PickVisualMedia() or ACTION_OPEN_DOCUMENT, the Android system displays an interactive window hosted by the com.google.android.documentsui or com.google.android.providers.media.module system processes. Because these interfaces reside outside the application's ViewRootImpl hierarchy, Espresso throws an immediate NoMatchingViewException. Testing teams are forced to either mock the intent launcher—leaving the actual file extraction unverified—or implement fragile UI Automator scripts that fail across differing OEM device skins.

2. MediaScanner Indexing Desynchronization

In automated CI environments, developers routinely seed test fixtures onto connected emulators or hardware using adb push mock_receipt.jpg /sdcard/Pictures/. However, Android's MediaProvider maintains a localized SQLite database tracking all indexed media. Pushing raw bytes through ADB writes data to the underlying Linux ext4 or F2FS filesystem but bypasses the MediaScannerConnection broadcast receiver. When the test script launches the application and queries contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, ...), the cursor returns zero rows. Without automated logic to trigger system media scan broadcasts, tests fail intermittently despite files existing on disk.

3. Android 14 Partial Photo Access Regressions

Android 14 introduced dynamic permission renegotiation. When prompted with media permissions, users can select "Select photos and videos" instead of "Allow all". Under this regime, the system grants READ_MEDIA_VISUAL_USER_SELECTED while withholding READ_MEDIA_IMAGES. If an application assumes full access upon receiving a successful permission callback, subsequent cursor queries for other images throw silent failures or empty view states. Simulating this multi-tier permission prompt requires interacting with complex system permission sheets that standard mobile test runners cannot orchestrate reliably. For a broader analysis of runtime permission triaging, explore testing Android runtime permissions with Claude Code and MCP.

4. Storage Access Latency and Cloud Relay Bottlenecks

Renting remote mobile devices from centralized cloud testing farms introduces severe network latency (often exceeding 800 to 1200 milliseconds per interaction) and security compliance risks. Testing file persistence and image uploads involves transmitting megabytes of binary test fixtures over public cloud relays. In contrast, running localized test fleets with ScreenSync MCP keeps all disk operations, display mirroring (~94 ms median latency), and ADB control commands strictly within the local developer environment.

Storage Testing Dimension Manual QA Testing Cloud Device Farms Custom ADB Bash Scripts ScreenSync MCP + Claude Code
System Document Picker Traversal High (Human clicks UI) Flaky (High video lag) Impossible (No UI control) Deterministic via get_ui_hierarchy + control_tap
MediaStore Scan Verification Manual gallery inspection Complex remote scripting Manual am broadcast calls Autonomous ADB broadcast + cursor assertion
Android 14 Partial Access Triage Subjective human testing Difficult to configure Script fails on modal change Dynamic system dialog detection & flow branch
Test Fixture Data Privacy Sensitive files on desks Assets uploaded to cloud Local files only 100% Local LAN execution; zero cloud relays
Root Cause Exception Diagnosis Tester files text ticket Raw logs downloaded manually Requires log grep skills Claude Code analyzes logcat & edits Kotlin repo
Multi-Surface Coordination Disconnected phone/desktop Isolated mobile device Fragmented shell scripts Unified 226 tools linking mobile & web backend

Production Kotlin Implementation: Jetpack Compose Storage Screen with MediaStore and Photo Picker

Production Android architectures encapsulate storage operations inside dedicated repositories, decoupling UI composables from raw ContentResolver queries. Developers utilize Jetpack Compose ActivityResultContracts to trigger the system Photo Picker without requiring legacy storage permissions, while inserting shared files through MediaStore ContentValues to ensure seamless operating system compliance across Android 10 through Android 15 releases.

The following Kotlin implementation demonstrates a production-grade, Scoped Storage compliant media management screen built with Jetpack Compose. It handles modern Photo Picker interactions, Storage Access Framework document generation, and MediaStore image insertions with proper error recovery:

package com.example.screensync.storage.ui

import android.app.Activity
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import java.io.IOException
import java.io.OutputStream

/**
 * Production Jetpack Compose screen demonstrating Scoped Storage compliance,
 * Photo Picker integration, and resilient MediaStore insertion.
 */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StorageVerificationScreen(
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
    var loadedBitmap by remember { mutableStateOf<Bitmap?>(null) }
    var statusMessage by remember { mutableStateOf("Ready for storage operations.") }
    var isError by remember { mutableStateOf(false) }

    // Modern Photo Picker launcher (Android 11+ backported, zero permissions required)
    val photoPickerLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) { uri: Uri? ->
        if (uri != null) {
            selectedImageUri = uri
            try {
                context.contentResolver.openInputStream(uri)?.use { stream ->
                    loadedBitmap = BitmapFactory.decodeStream(stream)
                }
                statusMessage = "Asset loaded successfully from Photo Picker."
                isError = false
            } catch (e: IOException) {
                statusMessage = "Failed to stream asset: ${e.localizedMessage}"
                isError = true
            }
        } else {
            statusMessage = "Photo selection cancelled by user."
        }
    }

    // Storage Access Framework launcher for creating export documents
    val createDocumentLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.CreateDocument("application/pdf")
    ) { uri: Uri? ->
        if (uri != null) {
            try {
                context.contentResolver.openOutputStream(uri)?.use { output: OutputStream ->
                    output.write("PDF Payload: Verification of SAF Write Stream".toByteArray())
                }
                statusMessage = "Document exported via Storage Access Framework."
                isError = false
            } catch (e: Exception) {
                statusMessage = "SAF Write Failed: ${e.localizedMessage}"
                isError = true
            }
        }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Scoped Storage Verification") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.surfaceVariant
                )
            )
        },
        modifier = modifier.fillMaxSize()
    ) { innerPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
                .padding(16.dp),
            verticalArrangement = Arrangement.spacedBy(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            // Diagnostic Status Banner
            Surface(
                color = if (isError) MaterialTheme.colorScheme.errorContainer 
                        else MaterialTheme.colorScheme.primaryContainer,
                shape = RoundedCornerShape(8.dp),
                modifier = Modifier.fillMaxWidth()
            ) {
                Row(
                    modifier = Modifier.padding(14.dp),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.spacedBy(10.dp)
                ) {
                    Icon(
                        imageVector = if (isError) Icons.Default.Warning else Icons.Default.Check,
                        contentDescription = null,
                        tint = if (isError) MaterialTheme.colorScheme.error 
                               else MaterialTheme.colorScheme.primary
                    )
                    Text(
                        text = statusMessage,
                        style = MaterialTheme.typography.bodyMedium
                    )
                }
            }

            // Image Preview Canvas
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(240.dp)
                    .clip(RoundedCornerShape(12.dp))
                    .background(MaterialTheme.colorScheme.surfaceVariant),
                contentAlignment = Alignment.Center
            ) {
                loadedBitmap?.let { bmp ->
                    Image(
                        bitmap = bmp.asImageBitmap(),
                        contentDescription = "Selected Media Preview",
                        modifier = Modifier.fillMaxSize()
                    )
                } ?: Text(
                    text = "No image selected. Trigger Photo Picker below.",
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }

            // Action Triggers
            Button(
                onClick = {
                    photoPickerLauncher.launch(
                        PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
                    )
                },
                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Launch System Photo Picker")
            }

            Button(
                onClick = {
                    createDocumentLauncher.launch("diagnostic_report.pdf")
                },
                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Export Document via SAF")
            }

            OutlinedButton(
                onClick = {
                    saveBitmapToMediaStore(context) { success, msg ->
                        statusMessage = msg
                        isError = !success
                    }
                },
                modifier = Modifier.fillMaxWidth()
            ) {
                Text("Save Image to MediaStore Collection")
            }
        }
    }
}

/**
 * Helper function to insert a sample bitmap into MediaStore.Images collection.
 */
fun saveBitmapToMediaStore(
    context: Context,
    onResult: (Boolean, String) -> Unit
) {
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "screensync_test_${System.currentTimeMillis()}.png")
        put(MediaStore.Images.Media.MIME_TYPE, "image/png")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/ScreenSyncTests")
            put(MediaStore.Images.Media.IS_PENDING, 1)
        }
    }

    val resolver = context.contentResolver
    val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

    val itemUri = resolver.insert(collectionUri, values)
    if (itemUri == null) {
        onResult(false, "Failed to create MediaStore entry.")
        return
    }

    try {
        resolver.openOutputStream(itemUri)?.use { stream ->
            val sampleBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)
            sampleBitmap.eraseColor(android.graphics.Color.rgb(101, 65, 214))
            sampleBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.clear()
            values.put(MediaStore.Images.Media.IS_PENDING, 0)
            resolver.update(itemUri, values, null, null)
        }
        onResult(true, "Successfully inserted media asset: $itemUri")
    } catch (e: Exception) {
        resolver.delete(itemUri, null, null)
        onResult(false, "MediaStore write failure: ${e.localizedMessage}")
    }
}

How ScreenSync MCP Connects Claude Code to Android Storage Layers

ScreenSync MCP equips Claude Code with decoupled screen perception and hardware control planes across physical Android devices. High-speed screen frames stream locally via Android MediaProjection without root requirements, while the desktop hub routes Model Context Protocol commands through ADB to inspect global accessibility node bounds, dismiss out-of-process system storage dialogs, and stream real-time logcat diagnostics.

Automating Scoped Storage quality engineering requires an AI agent to execute two synchronized operations: command-line shell orchestration and visual/structural verification. ScreenSync MCP accomplishes this through a bifurcated architecture:

1. High-Speed Visual Perception (Zero-Root, Zero-ADB)

The ScreenSync MCP Android client (com.screensync.mcp, available for Android 7.0 and newer) runs directly on the test device. Upon user consent during initial pairing, the application captures screen frames directly from Android's hardware surface compositor using the native MediaProjection API. Frames stream across the local Wi-Fi network directly to the desktop hub at approximately 94 milliseconds median latency. Because display streaming requires no root privileges and zero ADB overhead, the agent observes real-time screen transitions, picker animations, and storage progress bars with minimal latency.

2. Deep Control & System Inspection Plane (ADB Stdio Transport)

While display capture operates independently, control tools (control_tap, control_swipe, control_type, launch_app, get_ui_hierarchy, get_logcat) execute through ADB managed by the ScreenSync desktop hub. The hub runs on Node.js 18+ (Windows, macOS, or Linux) and exposes 226 MCP tools over standard input/output (stdio):

  • 29 mobile and desktop tools: 12 control tools, 3 OS tools, and 14 capture and telemetry tools such as get_latest_screenshot, get_ui_hierarchy, and get_logcat.
  • 197 browser automation tools (web_*) for validating cloud storage admin panels and hybrid WebViews.

To configure Claude Code with ScreenSync MCP, quality engineers execute a single terminal command:

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

For comprehensive configuration flags and environment settings, consult the ScreenSync MCP desktop hub setup guide and inspect the screen MCP server architecture overview. For comparative evaluations against other mobile tooling, review the Android MCP servers directory.

3. Inspecting Out-of-Process UI Trees with get_ui_hierarchy

When an application triggers the system Photo Picker, the active view is hosted outside the target app. Claude Code invokes get_ui_hierarchy, which queries Android's system-wide AccessibilityNodeInfo tree. The output reveals the exact coordinate bounds and semantic properties of the system picker:

{
  "className": "android.widget.ImageView",
  "packageName": "com.google.android.providers.media.module",
  "contentDescription": "Photo taken on Nov 5, 2026, 8:14 PM",
  "bounds": {
    "left": 48,
    "top": 360,
    "right": 384,
    "bottom": 696
  },
  "clickable": true
}

Claude Code parses these bounds and executes control_tap at coordinates (216, 528), selecting the photo deterministically without fragile image-matching algorithms. For a detailed exploration of accessibility node trees, read how AI agents use Android UI trees instead of screenshots.

Step-by-Step Autonomous Scoped Storage QA Pipeline with Claude Code

The autonomous Scoped Storage testing pipeline operates in an automated four-stage verification cycle. Claude Code seeds designated test media into private app sandboxes using ADB commands, launches user journeys to trigger storage prompts, navigates system picker interfaces via coordinate taps, and verifies persistent URI access and database cursor integrity across application lifecycle events.

Engineering teams can command Claude Code to execute end-to-end Scoped Storage test cycles using natural language prompts. Below is the step-by-step workflow:

Step 1: Pre-Seeding Test Assets and Forcing MediaScanner Indexing

Before running storage tests, Claude Code copies mock binary files to the test handset and forces immediate indexing:

# Push mock image to shared pictures directory
adb push test_receipt.png /storage/emulated/0/Pictures/test_receipt.png

# Force Android MediaScanner to index the file into MediaStore immediately
adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///storage/emulated/0/Pictures/test_receipt.png

# Seed mock database into the app private sandbox
adb push mock_cache.bin /storage/emulated/0/Android/data/com.example.screensync/files/cache.bin

Step 2: Orchestrating the System Photo Picker

Claude Code launches the test application and triggers the Photo Picker:

claude "Launch com.example.screensync, tap 'Launch System Photo Picker', wait for the Photo Picker to render, select the first photo thumbnail in the grid, and verify that the image preview canvas renders the selected bitmap."
  1. Claude Code calls launch_app {package_name: "com.example.screensync"}.
  2. Claude Code inspects get_ui_hierarchy, locates the button with text "Launch System Photo Picker", and calls control_tap.
  3. Claude Code calls get_ui_hierarchy again to confirm that com.google.android.providers.media.module has become the foreground package.
  4. Claude Code taps the target photo node and inspects the status banner to confirm: "Asset loaded successfully from Photo Picker."
  5. Claude Code verifies visual fidelity by calling get_latest_screenshot, ensuring no broken image placeholders render. For more details on verifying real handset behavior, check testing Android apps on real mobile devices with Claude Code.

Step 3: Validating Persistent Storage and Offline Cache Sync

Storage validation frequently intersects with network resilience and offline databases. Claude Code asserts that cached files remain readable when network connections drop:

# Simulate airplane mode to verify offline file caching
adb shell cmd connectivity airplane-mode enable

Claude Code verifies that previously downloaded documents load instantly from local app storage without throwing network socket exceptions. When testing Flutter or multiplatform applications, engineers can reference debugging Flutter mobile applications with AI agents on Android and testing Android offline mode and data synchronization with Claude Code.

Step 4: Triaging Storage Exceptions and Auto-Patching Kotlin Code

If the application crashes during file export, Claude Code invokes get_logcat to capture the stack trace:

adb logcat -d -s StorageVerification:E AndroidRuntime:E

Claude detects an unhandled SecurityException:

java.lang.SecurityException: UID 10245 does not have permission to content://media/external/images/media/892

Claude Code immediately opens the repository's StorageRepository.kt file, locates the missing takePersistableUriPermission call, applies the patch, recompiles the debug APK via Gradle, pushes the updated build to the device, and re-executes the test suite until green. For production crash triaging workflows, review enterprise Android crash triaging with logcat and touch telemetry.

Advanced Scoped Storage Edge Cases Triaged by AI Agents

Edge case triaging requires validating complex mobile failure modes that traditional unit tests cannot replicate. Claude Code simulates extreme storage constraints by creating zero-byte mock files, executing runtime permission revocations, handling OEM document picker variations across Samsung and Xiaomi devices, and validating that room database backups exclude sensitive external files per security standards. To evaluate how internal SQLite schemas evolve without data corruption during application updates, explore our guide on testing Android Room database migrations with Claude Code and MCP.

Autonomous testing with ScreenSync MCP excels at probing edge-case conditions that manual QA engineers rarely have the time or tooling to test:

1. Simulating Disk Full and Quota Exhaustion (ENOSPC)

Applications often crash unexpectedly when a mobile handset runs out of internal storage. Claude Code can simulate near-zero disk capacity by generating a large sparse dummy file in the device's temporary directory:

# Allocate 95% of remaining storage capacity
adb shell fallocate -l 10G /data/local/tmp/fill_storage.tmp

Claude Code then triggers the application's file download or backup flow, asserting that the app catches IOException or SQLiteFullException and displays a user-friendly error dialog rather than terminating abruptly. After test completion, Claude removes the dummy file:

adb shell rm /data/local/tmp/fill_storage.tmp

2. Validating Android 14 READ_MEDIA_VISUAL_USER_SELECTED Flow

When a user updates permission from partial access to full access, Android does not necessarily recreate the running Activity. Claude Code tests this transition by dynamically altering permissions via ADB:

# Grant partial photo access
adb shell pm grant com.example.screensync android.permission.READ_MEDIA_VISUAL_USER_SELECTED
adb shell pm revoke com.example.screensync android.permission.READ_MEDIA_IMAGES

# Verify app UI displays partial access warning banner
# Next, grant full access and verify app updates without restart
adb shell pm grant com.example.screensync android.permission.READ_MEDIA_IMAGES

3. Triage OEM Document Picker Variations (Samsung One UI vs Stock AOSP)

Original Equipment Manufacturers (OEMs) heavily customize the system DocumentsUI package. Samsung One UI features a custom drawer layout with customized resource IDs, whereas Google Pixel devices feature standard Material 3 layouts. Claude Code's get_ui_hierarchy dynamically navigates these interface variations by querying semantic accessibility properties (such as contentDescription and node text) rather than relying on hardcoded resource IDs.

Conclusion and Next Steps

Automating Android Scoped Storage quality assurance with Claude Code and ScreenSync MCP eliminates fragile UI scripts and manual tester bottlenecks. Engineering organizations achieve deterministic, end-to-end storage validation on physical hardware across dynamic permission boundaries, MediaStore indexing cycles, and system document pickers, ensuring rock-solid data persistence and compliance before reaching production users.

Modern Android development demands rigorous verification across increasingly strict security and storage layers. Traditional test frameworks that were built for legacy Android versions fail to bridge the gap between private sandboxes, system pickers, and asynchronous ContentProvider pipelines. By uniting Claude Code's autonomous reasoning with ScreenSync MCP's 226-tool verification surface, mobile teams can shift storage testing from reactive bug triage to continuous automated assurance.

Deploy autonomous storage testing on your Android devices today:

  1. Install the free ScreenSync MCP Android client on ScreenSync MCP on Google Play.
  2. Download the desktop hub and follow the ScreenSync MCP desktop hub setup guide.
  3. Connect Claude Code and start validating your mobile storage, permission, and persistence flows autonomously on ScreenSync MCP official site.
FAQ

Questions, answered

How does Claude Code interact with system document pickers that run outside the target application process?

Claude Code interacts with out-of-process system document pickers by issuing get_ui_hierarchy tool calls through ScreenSync MCP. Because the Model Context Protocol server queries Android's global accessibility tree, Claude Code identifies node bounds and resource identifiers inside com.google.android.documentsui, dispatching control_tap coordinates directly without requiring in-process instrumentation hooks.

Why do files pushed via ADB fail to appear immediately in Android MediaStore queries?

Files pushed directly to shared storage via adb push bypass Android's MediaScanner indexing daemon. Consequently, the system MediaStore ContentProvider contains no database records for newly copied files. AI testing workflows resolve this discrepancy by triggering adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE or invoking MediaScannerConnection.scanFile() to force immediate database indexing.

Does ScreenSync MCP require root privileges or ADB to capture Android storage dialogs?

ScreenSync MCP requires zero root privileges and zero ADB commands for display capture. The Android client captures screens using Android's native MediaProjection API following user consent. However, executing control operations—such as tapping system storage buttons, pushing mock test media, or inspecting background logcat streams—operates through ADB commands orchestrated by the desktop hub.

How does an AI agent test Android 14 partial photo access with Selected Photos permissions?

In Android 14, granting READ_MEDIA_VISUAL_USER_SELECTED restricts an application to explicit user-picked assets. Claude Code tests this permission boundary by automating the system Photo Picker, selecting specific test bitmaps, asserting that unselected media queries return empty cursor results, and subsequently triggering the system re-prompt dialog to grant expanded media access.

How can automated tests simulate low disk space or storage quota exhaustion on physical Android devices?

Automated tests simulate storage exhaustion by instructing Claude Code to execute adb shell fallocate -l <size> /data/local/tmp/dummy_fill or writing large temporary byte arrays into the application cache directory. Claude Code then verifies that the application gracefully surfaces user-facing storage warning banners without crashing on uncaught IOException or SQLiteFullException errors.

Can Claude Code test persistent URI permissions across Android application process death?

Yes. Claude Code tests persistent URI permissions by automating Storage Access Framework document selection, verifying takePersistableUriPermission calls, and subsequently terminating the application process via adb shell am kill <package_name>. Upon relaunching the application, Claude Code confirms that the persisted content URI successfully resolves without throwing security exceptions.

What is the latency overhead when streaming real-time Android frames to Claude Code during storage testing?

ScreenSync MCP streams display frames locally from physical Android hardware to the desktop hub with approximately 94 milliseconds median latency over local Wi-Fi. Because video frames transmit over high-speed local network sockets without routing through external cloud relays, Claude Code perceives dynamic storage dialog transitions and progress indicators in near real-time.

How does ScreenSync MCP differ from cloud device farms when testing private local storage?

Unlike cloud device farms that stream compressed video over high-latency public relays and store artifacts on third-party servers, ScreenSync MCP keeps all screen frames and storage data strictly within the developer's local area network. Test fixtures and proprietary database records remain private, while local execution eliminates cloud queue times.

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.