On this page
Testing Android broadcast receivers on authentic physical hardware represents one of the most critical yet structurally elusive disciplines in modern mobile quality assurance. In the Android operating system, inter-process communication (IPC) relies heavily on the Intent messaging system. Broadcast receivers function as asynchronous message listeners, waiting dormant until either the Android framework or another application broadcasts a matching action event—ranging from critical system triggers like network state alterations, battery drops, and device reboots, to bespoke in-app synchronization signals. Historically, developers treated broadcast receivers as open event buses, registering them globally in application manifests without strict boundary enforcement. However, over successive platform versions from Android 8.0 Oreo's implicit broadcast restrictions to Android 14's mandatory runtime export flags (RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED), Google has transformed the broadcast subsystem into a tightly controlled security boundary. For software testing teams, verifying that an application correctly receives, validates, and processes broadcasts without crashing, leaking private user state, or freezing the main UI looper into an Application Not Responding (ANR) lockup has proven exceptionally painful. Synthetic test runners like Robolectric mock Android framework internals in host Java Virtual Machines, completely masking OEM-specific broadcast throttling and native process lifecycle kills. In-process test runners like Espresso remain sandboxed within application UI threads, rendering them incapable of dispatching system-level broadcasts or observing background component state transitions. By bridging terminal-based AI coding agents like Claude Code with physical Android hardware through the Model Context Protocol (MCP) using ScreenSync MCP, engineering teams can now orchestrate deterministic, end-to-end broadcast testing harnesses that validate IPC security, system event handling, and real-time UI synchronization across real physical devices.
Key Takeaways
- System-level intent injection: ScreenSync MCP empowers Claude Code to dispatch deterministic system and custom broadcasts directly across physical Android hardware via Android Debug Bridge shell execution (
am broadcast), validating reactive state transitions without manual physical intervention. - Android 14 export compliance verification: Claude Code audits application manifests and runtime registration pipelines to detect missing
RECEIVER_EXPORTEDorRECEIVER_NOT_EXPORTEDflags, preventing fatal startupSecurityExceptioncrashes before production deployment. - Continuous local MediaProjection verification: Real-time display frames stream from the physical Android phone to the workstation desktop hub via native MediaProjection at approximately 94 milliseconds median latency without consuming ADB bandwidth or requiring root privileges.
- Zero-root main-thread ANR diagnostics: Claude Code monitors real-time
get_logcattelemetry during broadcast reception, triaging execution duration against the 10-second system threshold and asserting proper asynchronous offloading viagoAsync(). - State and UI hierarchy synchronization: Following broadcast dispatch, Claude Code invokes
get_ui_hierarchyto assert that in-app status indicators, badges, and Jetpack Compose state holders update immediately without visual tearing or UI freezes. - Unified 226-tool automation ecosystem: ScreenSync MCP unites 29 mobile and desktop tools (12
control_*, 3os_*, and 14 capture tools) with 197 browser tools to verify cross-surface workflows such as remote web dashboard updates triggered by device broadcasts.
The Modern Broadcast Architecture: Manifest Declared vs Context Registered
The modern Android broadcast architecture separates manifest-declared static receivers from context-registered dynamic receivers to enforce strict battery conservation and IPC security. Static receivers remain restricted to explicit intents and specific system allowlist actions, whereas dynamic receivers handle active runtime events and require explicit export scoping.
To understand why automated broadcast testing on physical devices is indispensable in modern Android engineering, one must trace the evolution of Android's component model. In early Android releases, developers frequently declared broadcast receivers inside AndroidManifest.xml with intent filters tuned to implicit system actions like android.net.conn.CONNECTIVITY_CHANGE or android.intent.action.BATTERY_CHANGED. Whenever that event fired across the device, the Android framework woke up every registered application process, loaded its classes into memory, and executed BroadcastReceiver.onReceive(). When dozens of installed applications woke simultaneously to process a single network switch, the device experienced severe CPU contention, memory thrashing, and rapid battery depletion.
Beginning with Android 8.0 (API level 26), Google enforced strict background execution limits that prohibited manifest-declared receivers from listening to implicit broadcasts. An application cannot register in its manifest for general system broadcasts unless the broadcast action is explicitly exempt (such as ACTION_BOOT_COMPLETED, ACTION_MY_PACKAGE_REPLACED, or ACTION_LOCALE_CHANGED). Consequently, modern Android architecture bifurcates broadcast handling into two distinct implementation models:
- Manifest-Declared (Static) Receivers: Defined in XML as
<receiver android:name=".BootReceiver">. The operating system can instantiate and invoke these components even if the host application process is not running. They are reserved for critical, explicit lifecycle transitions and exempt system actions. Misconfigurations here result in silent failures, where the operating system quietly ignores the intent filter without throwing an explicit developer error. - Context-Registered (Dynamic) Receivers: Registered programmatically at runtime via
Context.registerReceiver()within an Activity, Service, or custom dependency injection lifecycle scope. These receivers listen for events only while their enclosing context remains active and must be unregistered viaContext.unregisterReceiver()to prevent severe memory leaks. Dynamic receivers are the primary mechanism for updating active UI components in response to background updates, hardware state alterations, or inter-module communication.
Testing both patterns in continuous integration using legacy toolchains has historically produced severe blind spots. The following table contrasts traditional testing methodologies against physical device orchestration using Claude Code and ScreenSync MCP:
| Broadcast Testing Dimension | Mocked JVM Suites (Robolectric) | In-Process UI Runners (Espresso) | Standalone Bash / ADB Scripts | ScreenSync MCP + Claude Code (Physical Device) |
|---|---|---|---|---|
| Execution Environment | Simulated Android runtime on desktop JVM | Sandboxed application process on device or emulator | Uncoordinated workstation command-line scripts | Authentic physical Android device running production OS builds |
| System Broadcast Simulation | Mocked via ShadowApplication.sendBroadcast() |
Unsupported; cannot cross process boundary into system daemons | Possible via adb shell am broadcast, but disconnected from UI asserts |
Native ADB execution coordinated with real-time UI hierarchy and frame assertion |
| Android 14 Export Enforcement | Often bypasses native OS SecurityException checks | Cannot assert export boundary security with external apps | Script cannot verify if runtime exception occurred without parsing logs | Directly triages SecurityException in real-time Logcat streams via get_logcat |
| Main-Thread ANR Detection | Timers execute synchronously in simulated looper | Freezes test runner process; generates uninformative timeouts | Requires manual tombstone scraping from /data/anr/ |
Real-time monitoring of ActivityManager looper warnings and logcat dumps |
| UI State Reactivity Assertion | Inspects in-memory view objects; misses composition lag | In-process matchers struggle if broadcast triggers navigation | Requires custom ADB screencap and image diffing pipelines | Extracts authoritative system accessibility tree via get_ui_hierarchy |
| Visual Verification Fidelity | Zero visual render; does not evaluate pixel fidelity | Dependent on custom in-memory screenshot libraries | High-latency screen capture prone to ADB bridge saturation | Native MediaProjection mirror (~94 ms latency) via get_latest_screenshot |
Android 14 Export Enforcement: RECEIVER_EXPORTED vs RECEIVER_NOT_EXPORTED
Android 14 enforces mandatory explicit export scoping for context-registered broadcast receivers to prevent inter-application spoofing and malicious intent injection. Omitting either the RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED flag when targeting API level 34 causes the operating system to throw an immediate SecurityException during execution.
Prior to Android 14 (API level 34), registering a dynamic broadcast receiver without declaring an export status left the receiver accessible to broadcasts originating from any application installed on the device, unless a custom signature permission was explicitly enforced. Attackers could reverse-engineer an application's public intent filters and broadcast malicious or malformed intents, triggering unauthorized state updates, bypassing authentication screens, or forcing denial-of-service conditions.
To eliminate this vulnerability, Android 14 introduced strict export enforcement. When an application targeting API level 34 or higher invokes registerReceiver() for non-system broadcasts without specifying an export flag, the Android framework halts execution immediately:
E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.example.screensync.broadcasts, PID: 14820
E AndroidRuntime: java.lang.SecurityException: com.example.screensync.broadcasts: One of RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED should be specified when a receiver isn't being registered exclusively for system broadcasts
E AndroidRuntime: at android.app.LoadedApk.registerReceiverInternal(LoadedApk.java:1852)
E AndroidRuntime: at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1882)
E AndroidRuntime: at android.app.ContextImpl.registerReceiver(ContextImpl.java:1850)
E AndroidRuntime: at com.example.screensync.broadcasts.NetworkSyncActivity.onStart(NetworkSyncActivity.kt:42)
Implementing Defensive Kotlin Registration with Jetpack Compose
To maintain complete compatibility across legacy and modern Android releases, production applications must utilize the ContextCompat.registerReceiver API, passing explicit export flags based on the intended scope of communication. If the receiver only listens to broadcasts dispatched by your own application or trusted system services, it must be designated RECEIVER_NOT_EXPORTED. If the receiver must accept intents from third-party applications, it must be declared RECEIVER_EXPORTED, preferably guarded by a custom signature-level permission.
The following Kotlin implementation demonstrates a production-grade context-registered broadcast receiver integrated into an Android Activity running Jetpack Compose. The component listens for network connectivity state transitions, updates reactive UI state holders, and executes long-running data sync operations safely off the main looper using goAsync():
package com.example.screensync.broadcasts
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class NetworkSyncActivity : ComponentActivity() {
private val isConnectedState = mutableStateOf(false)
private val syncStatusMessage = mutableStateOf("Awaiting Network State")
// Context-registered dynamic receiver for real-time network evaluation
private val networkStateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (ConnectivityManager.CONNECTIVITY_ACTION == action ||
"com.example.screensync.ACTION_TRIGGER_SYNC" == action) {
// Use goAsync() for background processing exceeding a few milliseconds
val pendingResult = goAsync()
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
val isOnline = capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
isConnectedState.value = isOnline
syncStatusMessage.value = if (isOnline) "Network Active - Syncing" else "Offline - Queueing"
// Execute heavy background serialization without blocking main UI looper
CoroutineScope(Dispatchers.IO).launch {
try {
if (isOnline) {
Thread.sleep(800) // Simulate database synchronization batch
}
} finally {
pendingResult.finish() // Crucial: Inform OS that processing completed
}
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
BroadcastDemoScreen(
isOnline = isConnectedState.value,
statusMessage = syncStatusMessage.value
)
}
}
}
}
override fun onStart() {
super.onStart()
val filter = IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction("com.example.screensync.ACTION_TRIGGER_SYNC")
}
// Android 14 (API 34) Compliance: Explicit RECEIVER_NOT_EXPORTED flag
ContextCompat.registerReceiver(
this,
networkStateReceiver,
filter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
override fun onStop() {
super.onStop()
// Defensive Lifecycle Management: Prevent memory leaks
try {
unregisterReceiver(networkStateReceiver)
} catch (e: IllegalArgumentException) {
// Receiver was already unregistered
}
}
}
@Composable
fun BroadcastDemoScreen(isOnline: Boolean, statusMessage: String) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.size(16.dp)
.background(if (isOnline) Color(0xFF4CAF50) else Color(0xFFF44336))
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = if (isOnline) "ONLINE" else "DISCONNECTED",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = statusMessage,
style = MaterialTheme.typography.bodyMedium
)
}
}
When building apps with modern reactive UI toolchains, developers can apply testing Jetpack Compose apps with Claude Code and MCP to verify that composable state holders respond instantaneously to broadcast state updates without visual tearing or UI freezes.
Hardening Manifest-Declared Receivers with Custom Permissions
For static receivers declared in AndroidManifest.xml that must communicate across package boundaries, developers should enforce custom signature permissions. This architecture guarantees that only applications signed with the identical cryptographic developer key can trigger the receiver:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.screensync.broadcasts">
<!-- Define proprietary signature-level permission -->
<permission
android:name="com.example.screensync.permission.AUTHENTICATED_SYNC"
android:protectionLevel="signature" />
<uses-permission android:name="com.example.screensync.permission.AUTHENTICATED_SYNC" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="Broadcast Demo"
android:theme="@style/Theme.BroadcastDemo">
<!-- Manifest receiver protected by custom permission -->
<receiver
android:name=".StartupSyncReceiver"
android:enabled="true"
android:exported="true"
android:permission="com.example.screensync.permission.AUTHENTICATED_SYNC">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="com.example.screensync.ACTION_REMOTE_FLUSH" />
</intent-filter>
</receiver>
<activity
android:name=".NetworkSyncActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
| Android Broadcast Type | Registration Location | Android 14 Export Enforcement | Primary Use Case | ADB Shell Simulation Command |
|---|---|---|---|---|
| Boot Completed | Manifest (<receiver>) |
Exempt system action (android:exported="true") |
Restoring background alarms, WorkManager jobs, and sync queues | adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n <pkg>/.StartupSyncReceiver |
| Package Replaced | Manifest (<receiver>) |
Exempt system action (android:exported="true") |
Database schema migrations and cache invalidation after app update | adb shell am broadcast -a android.intent.action.MY_PACKAGE_REPLACED -n <pkg>/.StartupSyncReceiver |
| Internal App Sync | Context (registerReceiver) |
Mandatory RECEIVER_NOT_EXPORTED |
Inter-module messaging, fragment-activity sync, local data refreshes | adb shell am broadcast -a com.example.screensync.ACTION_TRIGGER_SYNC |
| Cross-App Shared Event | Context (registerReceiver) |
Mandatory RECEIVER_EXPORTED |
External plugin events, third-party scanner broadcasts, enterprise MDM signals | adb shell am broadcast -a com.example.screensync.ACTION_PUBLIC_EVENT |
| Locale / Timezone Changed | Context or Manifest | Context requires export flag; Manifest restricted | Updating localized UI text, formatting currencies, refreshing date pickers | adb shell am broadcast -a android.intent.action.LOCALE_CHANGED |
| Low Battery Alert | Context (registerReceiver) |
Explicit system action handling | Throttling background polling frequencies to conserve energy | adb shell am broadcast -a android.intent.action.BATTERY_LOW |
How ScreenSync MCP Connects Claude Code to Android Inter-Process Communication
ScreenSync MCP connects Claude Code to Android inter-process communication by decoupling terminal agent cognition, real-time MediaProjection visual confirmation, and hardware ADB command execution. Claude Code dispatches broadcast intents through ADB shell sockets while simultaneously inspecting active UI node trees and live video streams.
The core engineering breakthrough of testing mobile applications with ScreenSync MCP is its tripartite architecture. Rather than forcing an artificial in-memory test runner into the target Android process, ScreenSync MCP separates testing responsibilities across three dedicated operational planes:
| Architectural Plane | Underlying Transport | Latency Profile | Role in Broadcast Receiver Testing |
|---|---|---|---|
| Cognitive Agent Plane | Local workstation stdio process pipe |
Sub-millisecond JSON-RPC | Claude Code parses developer test briefs, issues ADB broadcast payloads, and evaluates structural assertions |
| Visual Verification Plane | Local Wi-Fi / LAN / USB reverse socket | Native MediaProjection (~94 ms median) | Streams live 1080p display frames (Android 7.0+, zero root, zero ADB overhead) to verify UI badge updates and screen changes |
| Hardware & OS Control Plane | Physical USB or Wireless Debugging | ADB Daemon Socket | Injects am broadcast commands, triggers UI gestures (control_tap), and extracts accessibility trees via get_ui_hierarchy |
1. Infallible Intent Dispatch and Execution Validation
When Claude Code tests a broadcast receiver, it issues precise ADB shell commands directly through the host workstation shell. Unlike manual testing, where triggering events like BATTERY_LOW or ACTION_LOCALE_CHANGED requires complex physical manipulation or modifying global system settings, Claude Code targets the receiver deterministically:
adb shell am broadcast -a com.example.screensync.ACTION_TRIGGER_SYNC --es "sync_source" "automated_eval" --ez "force_full_refresh" true
The Android ActivityManager returns an immediate status code in the terminal:
Broadcasting: Intent { act=com.example.screensync.ACTION_TRIGGER_SYNC (has extras) }
Broadcast completed: result=0
2. Immediate Structural Assertion via get_ui_hierarchy
Following the broadcast dispatch, Claude Code does not rely on fragile pixel guessing. It invokes ScreenSync MCP's get_ui_hierarchy tool, querying the Android Accessibility Service running on the device. ScreenSync MCP extracts the authoritative node hierarchy of the active window in sub-50 milliseconds:
<node index="0" package="com.example.screensync.broadcasts" class="android.widget.FrameLayout">
<node index="0" class="android.widget.TextView" text="ONLINE" resource-id="com.example.screensync.broadcasts:id/status_header" />
<node index="1" class="android.widget.TextView" text="Network Active - Syncing" resource-id="com.example.screensync.broadcasts:id/status_message" />
</node>
Claude Code parses the XML hierarchy directly, asserting that the composable or view received the event, updated its text attributes, and toggled its layout state without throwing an uncaught exception.
3. High-Speed Visual Capture via Native MediaProjection
While structural XML trees provide deterministic data assertions, real-world quality assurance requires proving that the visual screen actually rendered the updated state. ScreenSync MCP captures real-time video frames using Android's native MediaProjection API (Android 7.0 or higher). Display capture requires zero device rooting and consumes zero ADB bandwidth, maintaining a median latency of approximately 94 milliseconds over a local area network (LAN). Claude Code calls get_latest_screenshot to confirm that status indicator colors transitioned cleanly from red to green without graphical artifacts.
4. The 226-Tool Ecosystem Advantage
ScreenSync MCP exposes 226 MCP tools to developer agents:
- 29 Phone & Desktop Tools: Comprising 12 device control tools (
control_tap,control_swipe,control_type,control_key,control_open_url,control_launch_app), 3 operating system automation tools (os_mouse_click,os_type,os_hotkey), and 14 capture and telemetry tools (get_latest_screenshot,get_ui_hierarchy,get_logcat,record_screen,get_device_status). - 197 Browser Tools: The
web_*suite operates through an authenticated Chrome/Edge browser extension. In enterprise multi-surface architectures where mobile broadcast receivers trigger cloud synchronization with administrative web portals, Claude Code can seamlessly jump between native mobile screen triage and web browser configuration within a single automated run. For an exhaustive breakdown of every available capability, review our complete Android MCP tool reference deep dive.
Workstation & Physical Device Environment Setup
Configuring ScreenSync MCP for broadcast receiver testing requires launching the desktop hub on Node.js 18, registering the stdio MCP server in Claude Code, and connecting an Android 7.0+ device via USB or wireless debugging. Reverse port forwarding guarantees continuous visual verification during automated intent sweeps.
Engineering teams can deploy this automated testing harness on Windows, macOS, or Linux through five sequential steps:
Step 1: Workstation Baseline Prerequisites
Ensure your testing workstation meets the following operational baseline:
- Workstation Environment: Windows 10/11, macOS, or Linux running Node.js 18 or later with
adb(Android Platform Tools) installed and accessible in your systemPATH. - Physical Smartphone: An authentic Android device running Android 7.0 (API level 24) or higher connected via USB cable or local Wi-Fi.
- Application Build: Debug build of your application installed with broadcast receivers and intent filters configured.
Step 2: Launch the Desktop Hub
Download the ScreenSync MCP desktop hub distribution from screensyncmcp.tech and extract the archive to a permanent workstation directory. Execute the platform-specific launcher:
# On Windows (PowerShell or Command Prompt)
cd C:\tools\screensync-hub
.\start-hub.bat
# On macOS or Linux (Terminal)
cd ~/tools/screensync-hub
chmod +x start-hub.sh
./start-hub.sh
The desktop hub initializes an HTTP server on port 3000 to ingest local MediaProjection video frames and prepares a JSON-RPC Model Context Protocol server over stdio. The console output displays your unique authentication token (SCREEN_SYNC_TOKEN). For comprehensive documentation, visit the official ScreenSync MCP setup guide.
Step 3: Register ScreenSync MCP with Claude Code
Register the desktop hub with the Claude Code CLI in your terminal:
claude mcp add screensync node "/path/to/screensync-hub/screensync-hub.js"
If you are developing inside Claude Desktop, edit your claude_desktop_config.json file (Settings > Developer > Edit Config):
{
"mcpServers": {
"screensync": {
"command": "node",
"args": ["/path/to/screensync-hub/screensync-hub.js"],
"env": {
"SCREEN_SYNC_TOKEN": "your_authenticated_token_here"
}
}
}
}
(Note: ScreenSync MCP integrates with local stdio MCP clients including Claude Code, Claude Desktop, Cursor, and VS Code agent mode. ChatGPT's web application requires remote HTTPS endpoints and does not support local stdio processes).
Step 4: Install the Android Application and Authorize MediaProjection
- Download the free ScreenSync MCP application (package
com.screensync.mcp) from Google Play. - Open the application. Allow local network mDNS discovery to automatically detect your desktop hub on the local Wi-Fi subnet, or scan the pairing QR code displayed in the hub console.
- Tap Start Projection and confirm Android's native MediaProjection screen capture consent prompt.
- Unlock Developer Options on your smartphone (Settings > About Phone > tap Build Number 7 times) and enable USB Debugging (or Wireless Debugging). If you prefer wireless operation, review our guide on pairing Android wireless debugging to an MCP server without USB.
- Verify ADB connectivity by running
adb devicesin your workstation terminal.
Step 5: Establish USB Reverse Port Forwarding
To ensure uninterrupted visual streaming even when toggling airplane mode or isolating network radios during broadcast receiver testing, configure USB reverse port forwarding:
adb reverse tcp:3000 tcp:3000
With this reverse socket configured, the ScreenSync MCP mobile application delivers MediaProjection frames directly across the USB physical link to the hub, even when the phone has airplane mode enabled and Wi-Fi disabled.
Automated QA Workflows: System Intents, Custom Payloads, and ANR Diagnostics
Practical broadcast receiver QA workflows validate three essential execution pathways: simulating system-wide hardware triggers, testing parameterized custom intent payloads, and detecting main-thread ANR lockups. Claude Code coordinates these multi-step sequences by synthesizing ADB intent execution with real-time accessibility hierarchy audits.
Workflow 1: Validating System Event Triage (BOOT_COMPLETED and Connectivity)
Validating how an application initializes after a cold device boot or recovers during connectivity changes represents a foundational test requirement.
Developer Prompt:
"Claude, launch our app com.example.screensync.broadcasts. Use ADB to broadcast android.intent.action.BOOT_COMPLETED directly to our StartupSyncReceiver. Read get_logcat to confirm that our background synchronization worker was scheduled, then query get_ui_hierarchy to assert that the app displays 'Sync Scheduled on Boot'."
Step 1: Dispatch Boot Completed via ADB
Claude Code executes the targeted broadcast through the terminal:
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED -n com.example.screensync.broadcasts/.StartupSyncReceiver
Step 2: Inspect Logcat Telemetry
Claude Code calls get_logcat with filter StartupSyncReceiver:
I StartupSyncReceiver: Received BOOT_COMPLETED broadcast
I StartupSyncReceiver: Enqueuing WorkManager periodic synchronization constraint
I WorkManager: Enqueued WorkRequest: 8a4c21df-6b8a-4d2b-9281-91a5ef870bc1
Claude Code parses the logcat output, confirming that the static receiver caught the broadcast without raising security rejections. When verifying persistent background workers and constraints, teams can consult our guide on testing Android WorkManager with Claude Code and MCP.
Step 3: Assert Visual and Structural State
Claude Code invokes control_launch_app to bring the Activity into the foreground, calls get_ui_hierarchy to assert that the sync status banner confirms successful scheduling, and captures get_latest_screenshot to verify visual layout integrity.
Workflow 2: Testing Custom Data Payloads and Intent Extras
Dynamic receivers frequently expect strongly typed intent extras (strings, booleans, integer flags, or serialized parcelables). Testing how the receiver behaves when parameters are missing or corrupted prevents fatal unhandled exceptions.
Developer Prompt:
"Claude, test our custom synchronization receiver. Dispatch com.example.screensync.ACTION_TRIGGER_SYNC with string extra 'sync_source' set to 'cli_eval' and boolean 'force_full_refresh' set to true. Verify that the UI updates to show 'Full Refresh in Progress'. Then send a second broadcast omitting 'force_full_refresh' to assert fallback default behavior."
Step 1: Inject Parameterized Broadcast
Claude Code executes the parameterized broadcast:
adb shell am broadcast -a com.example.screensync.ACTION_TRIGGER_SYNC -n com.example.screensync.broadcasts/.NetworkSyncActivity --es "sync_source" "cli_eval" --ez "force_full_refresh" true
Step 2: Assert UI Reaction via Structural Hierarchy
Claude Code immediately calls get_ui_hierarchy:
<node text="Full Refresh in Progress" resource-id="com.example.screensync.broadcasts:id/status_message" />
Claude Code confirms the presence of the updated text node. It then dispatches the second broadcast omitting the extra flag, calling get_ui_hierarchy once more to verify that the UI falls back gracefully to standard delta syncing rather than crashing with a NullPointerException. For an architectural view of terminal agent orchestration, explore our autonomous mobile app testing Claude Code guide.
Workflow 3: Diagnosing 10-Second Main Thread ANRs and Verifying goAsync()
In Android, BroadcastReceiver.onReceive() runs synchronously on the main UI looper. If processing exceeds 10 seconds (or 5 seconds for foreground receivers in certain configurations), the system displays the dreaded Application Not Responding (ANR) fatal dialog.
Developer Prompt:
"Claude, audit our broadcast receiver for ANR vulnerabilities. Send com.example.screensync.ACTION_HEAVY_CALCULATION. Monitor get_logcat for Looper warnings or ActivityManager ANR messages. Verify whether the receiver uses goAsync() to offload tasks to Dispatchers.IO."
Step 1: Dispatch Broadcast and Stream Telemetry
Claude Code injects the broadcast action while streaming get_logcat logs:
adb shell am broadcast -a com.example.screensync.ACTION_HEAVY_CALCULATION -n com.example.screensync.broadcasts/.NetworkSyncActivity
Step 2: Triage Main Looper Execution Time
If the developer made the mistake of executing network requests or heavy database parsing directly inside onReceive(), Claude Code identifies the warning emitted by Android's ActivityManager:
W ActivityManager: Broadcast of Intent { act=com.example.screensync.ACTION_HEAVY_CALCULATION cmp=com.example.screensync.broadcasts/.NetworkSyncActivity } took 8412ms
If the duration exceeds 10,000 milliseconds, Android kills the application:
E ActivityManager: ANR in com.example.screensync.broadcasts (com.example.screensync.broadcasts/.NetworkSyncActivity)
E ActivityManager: Reason: Broadcast of Intent { act=com.example.screensync.ACTION_HEAVY_CALCULATION cmp=com.example.screensync.broadcasts/.NetworkSyncActivity }
E ActivityManager: Load: 12.4 / 10.2 / 8.1
Claude Code detects this ANR tombstone, flags the offending code path, and refactors onReceive() to wrap the calculation in val pendingResult = goAsync() and CoroutineScope(Dispatchers.IO).launch, ensuring that pendingResult.finish() is strictly called in a finally block once background processing concludes. When testing long-running tasks that must survive beyond the immediate broadcast lifecycle, developers can also implement foreground services as detailed in our guide on testing Android foreground services with Claude Code and MCP.
| Broadcast QA Scenario | ScreenSync MCP Tools Used | Primary Assertion Mechanism | Critical Defect Caught |
|---|---|---|---|
| Android 14 Export Enforcement | get_logcat, get_ui_hierarchy |
Logcat SecurityException: One of RECEIVER_EXPORTED... |
Missing export flag crashes app immediately upon activity launch |
| System Boot Recovery | get_logcat, get_ui_hierarchy |
Component instantiation in logcat + UI badge | Receiver disabled in manifest or missing RECEIVE_BOOT_COMPLETED |
| Main Thread ANR Triage | get_logcat, record_screen |
ActivityManager took > 5000ms warning or ANR dialog |
Heavy database operations executed synchronously inside onReceive() |
| Missing Intent Extras | get_ui_hierarchy, control_tap |
Composable state fallback in UI hierarchy | App throws unhandled NullPointerException on missing bundle keys |
| Memory Leak on Destroy | get_logcat, control_key |
Activity recreation leak dump via logcat | Activity destroyed without calling unregisterReceiver() |
| Broadcast Spoofing Defense | get_logcat |
Permission rejection by system package manager | Public receiver unprotected by signature permission |
Common Broadcast Implementation Pitfalls and Logcat Diagnostics
Common broadcast implementation pitfalls include leaking dynamic receivers across Activity lifecycles, exposing internal application components to external intent spoofing, and failing to invoke PendingResult.finish() when using asynchronous workers. Claude Code triages these defects by correlating Logcat stack traces with real-time ADB package manager dumps.
1. Leaked Dynamic Receivers Across Activity Lifecycles
A notorious defect in Android application architecture occurs when a developer registers a broadcast receiver in onStart() or onResume(), but forgets to unregister it in the corresponding onStop() or onPause() method. When the user rotates the device or navigates away, the Android framework recreates the Activity, leaving the old receiver instance referenced inside the system LoadedApk receiver dispatcher.
When the Activity is garbage collected, Android's StrictMode logs a severe memory leak warning:
E ActivityThread: Activity com.example.screensync.broadcasts.NetworkSyncActivity has leaked IntentReceiver com.example.screensync.broadcasts.NetworkSyncActivity$networkStateReceiver@4a2c1f that was originally registered here. Are you missing a call to unregisterReceiver()?
E ActivityThread: android.app.IntentReceiverLeaked: Activity com.example.screensync.broadcasts.NetworkSyncActivity has leaked IntentReceiver
Claude Code detects this leak during automated rotation sweeps (triggered via control_key or adb shell settings put system user_rotation), alerts the engineering team, and inserts defensive unregistration logic inside onStop(). To evaluate runtime permission dialogs and rationale cards that might intercept broadcast flows, explore our deep dive on testing Android runtime permissions with Claude Code and MCP.
2. Intent Spoofing Vulnerabilities on Public Receivers
If an application declares a receiver in AndroidManifest.xml with android:exported="true" but fails to guard it with android:permission, any rogue application installed on the device can broadcast intents to that component, triggering unauthorized state transitions or resetting local application caches.
Claude Code audits the application's exported components via ADB dumpsys:
adb shell dumpsys package com.example.screensync.broadcasts | grep -A 15 "Receiver Resolver Table:"
Claude Code parses the resolver output:
Non-Data Actions:
com.example.screensync.ACTION_REMOTE_FLUSH:
4a18b2 com.example.screensync.broadcasts/.StartupSyncReceiver filter 7b29a1
Action: "com.example.screensync.ACTION_REMOTE_FLUSH"
Required Permission: "NULL" (SECURITY HAZARD: EXPORTED WITHOUT PERMISSION)
Claude Code flags the missing permission requirement and refactors the manifest to attach a custom signature-level permission.
3. Hanging Asynchronous Work with Missing finish() Calls
When developers utilize val pendingResult = goAsync() to execute asynchronous coroutines inside BroadcastReceiver.onReceive(), Android allocates a temporary wake lock and grants the process an extended grace period to complete execution. However, if an unhandled exception occurs inside the coroutine before pendingResult.finish() is executed, the pending result token remains open indefinitely.
The Android ActivityManager eventually times out the process, terminating it abruptly:
W ActivityManager: Timeout of broadcast BroadcastRecord{7d10a2 u0 com.example.screensync.ACTION_HEAVY_CALCULATION}
E ActivityManager: Receiver did not finish broadcast within timeout: killing process
Claude Code inspects coroutine catch blocks, ensuring that pendingResult.finish() resides inside a mandatory finally block or is managed via a defensive Kotlin inline extension.
Implementation Checklist for Automated Broadcast Testing
- [ ] Ensure host development workstation runs Node.js 18+ with Android Platform Tools
adbconfigured in systemPATH. - [ ] Unpack the ScreenSync MCP desktop hub and execute
start-hub.bat(Windows) orstart-hub.sh(macOS/Linux). - [ ] Register ScreenSync MCP with the Claude Code CLI via
claude mcp add screensync node "/path/to/screensync-hub.js". - [ ] Install the ScreenSync MCP client (version 2.5.7+) on physical Android 7.0+ hardware from Google Play.
- [ ] Connect smartphone and workstation to the same local Wi-Fi subnet; complete mDNS pairing or QR code authentication.
- [ ] Authorize native MediaProjection display recording on the Android device for low-latency (~94 ms) local screen mirroring.
- [ ] Execute
adb reverse tcp:3000 tcp:3000over USB to guarantee uninterrupted visual streaming during airplane mode tests. - [ ] Verify that every context-registered broadcast receiver targeting Android 14+ explicitly declares
RECEIVER_EXPORTEDorRECEIVER_NOT_EXPORTED. - [ ] Audit
AndroidManifest.xmlto confirm that manifest-declared receivers enforceandroid:protectionLevel="signature"for private inter-app intents. - [ ] Validate that all long-running operations inside
onReceive()utilizegoAsync()with guaranteedfinish()execution insidefinallyblocks. - [ ] Dispatch deterministic system and custom broadcasts via
adb shell am broadcastto assert UI hierarchy reactivity and visual integrity on real hardware.
Next steps
Questions, answered
Why does an Android app crash with SecurityException when registering a broadcast receiver on Android 14?
Starting with Android 14 (API level 34), the operating system requires all context-registered dynamic broadcast receivers to explicitly specify either Context.RECEIVER_EXPORTED or Context.RECEIVER_NOT_EXPORTED. Omitting this parameter throws an immediate runtime SecurityException unless the receiver is registered exclusively for system broadcasts.
Can Claude Code trigger Android system broadcasts like BOOT_COMPLETED on physical devices?
Yes. Claude Code uses ADB shell execution to dispatch system broadcasts directly to connected physical devices using commands like adb shell am broadcast -a android.intent.action.BOOT_COMPLETED. This triggers registered receivers deterministically without requiring a manual physical reboot of the smartphone.
How does ScreenSync MCP verify that a broadcast receiver updated the user interface?
ScreenSync MCP combines get_ui_hierarchy and get_latest_screenshot. The get_ui_hierarchy tool inspects the active accessibility node tree in sub-50 milliseconds to verify text and layout state, while get_latest_screenshot streams low-latency (~94 ms) MediaProjection frames to verify visual rendering.
What is the difference between RECEIVER_EXPORTED and RECEIVER_NOT_EXPORTED?
RECEIVER_EXPORTED allows external third-party applications installed on the device to send broadcast intents to your receiver. RECEIVER_NOT_EXPORTED restricts intent reception exclusively to the Android operating system and your own application package, protecting private components from unauthorized spoofing.
Why do long-running tasks inside BroadcastReceiver.onReceive() trigger ANR errors?
The onReceive() method executes synchronously on the application main UI thread. If execution blocks the looper for more than 10 seconds (or 5 seconds for foreground broadcasts), the Android framework flags an Application Not Responding (ANR) lockup. Long tasks must use goAsync() or WorkManager.
Does ScreenSync MCP require root privileges to test Android broadcast receivers?
No. ScreenSync MCP operates without device root privileges. Screen capture utilizes Android's native MediaProjection API following user consent, while hardware control and broadcast injection execute through standard Android Debug Bridge (ADB) permissions over physical USB or local wireless debugging.
How does Claude Code detect memory leaks caused by unregistered broadcast receivers?
Claude Code monitors real-time get_logcat telemetry during activity recreation and screen rotation sweeps. If an activity is destroyed while a dynamic receiver remains registered, Android's StrictMode logs an IntentReceiverLeaked error, allowing Claude Code to identify the unclosed listener immediately.
Can broadcast receivers be tested in continuous integration without an emulator?
Yes. By connecting an authentic physical Android phone running ScreenSync MCP to your CI runner or developer workstation via USB, Claude Code can orchestrate full automated broadcast sweeps across real hardware, eliminating emulator rendering discrepancies and synthetic framework mocking flaws.




