An on-device developer console for Android. Open a full inspector — network traffic, crashes, storage, feature flags, exports — right inside your debug app, by shake, floating button, or one line of code. When you want a bigger screen, the same data streams to any browser through an embedded web dashboard. Release builds compile against a no-op twin artifact, so none of this code ever ships to production, and the Gradle plugin fails the build if it would.
1. Add the plugin and two dependencies to your app's build.gradle.kts:
plugins {
id("com.android.application")
id("io.github.devconsole-android") version "0.3.0"
}
dependencies {
debugImplementation("io.github.devconsole-android:devconsole:0.3.0")
releaseImplementation("io.github.devconsole-android:devconsole-noop:0.3.0")
}2. Open the inspector on the device. The SDK auto-initializes on debuggable builds, so this works immediately — from any button in your debug UI:
DevConsole.open(context)or hands-free, by opting into the built-in triggers — shake the device (intensity adjustable) or tap a draggable floating button:
DevConsole.initialize(
application,
DevConsoleConfig.default()
.withOpenTriggers(OpenTriggers(shakeToOpen = true, floatingButton = true)),
)That's the setup. The inspector shows crash/ANR reports, feature flags, and inspectors for SharedPreferences, SQLite, and files (read-only by default); wire your HTTP client with one line ("Wire up your network stack" below) and network, WebSocket, and MQTT traffic appears too.
3. Want a bigger screen? Start the browser dashboard — the same data live-tailing in any
browser, plus mock-rule editing and one-click HAR / Postman / bug-report exports. The easiest way:
tap Start server on the inspector's More screen, which then shows the connect URL and a QR
code. Or from code (either way, make sure your manifest has INTERNET — most apps already do):
lifecycleScope.launch { // startBrowser is a suspend function; the server never starts on its own
val result = DevConsole.startBrowser(StartRequest(bindingMode = BindingMode.LOOPBACK))
val connectUrl = (result as? StartResult.Started)?.access?.connectUrl
// e.g. http://127.0.0.1:8080/#code=B7KQ2XWZ — surface this in your debug UI
}adb forward tcp:8080 tcp:8080 # use the port from the DevConsole log linethen open the whole URL — the #code= fragment is the credential.
| Area | What it does |
|---|---|
| In-app inspector | DevConsole.open(context) shows every inspector below as an on-device screen (included with devconsole), plus a QR code for pairing the browser. Opens by shake (adjustable intensity) or draggable floating button via the opt-in DevConsoleConfig.openTriggers flags. Its More screen can also start and stop the dashboard server. |
| Network inspector | Every HTTP call with headers, bodies, and a DNS/TCP/TLS/send/wait/receive timing bar. Live-tails as traffic happens. |
| WebSocket & MQTT inspectors | Connection lifecycles and every frame, inbound and outbound. MQTT rides the Eclipse Paho adapter. |
| Mock rules | Serve canned responses for matching requests (OkHttp), toggled from the dashboard, with deterministic priority matching. |
| Request composer | Make the device issue ad-hoc HTTP requests from the dashboard. Off by default, host-allowlist confinable. |
| Crash & ANR capture | Uncaught exceptions and a main-looper watchdog with bounded all-thread dumps and breadcrumbs. Always delegates to any crash reporter you already have. |
| Push timeline | Record FCM (or any) push messages and their lifecycle: received → displayed → opened. The Firebase adapter uses reflection — no compile-time Firebase dependency. |
| State & feature flags | Snapshot host-registered state and override feature flags from the browser. |
| Data inspectors | Browse SharedPreferences, SQLite (incl. a SQL console), and app files. Read-only by default; every edit surface is opt-in. |
| Evidence tray & exports | Flag anything, attach it to a bug report bundle or Markdown/Jira/GitHub clipboard text. Export HAR, Postman Collection, or a full session ZIP. |
| Background keep-alive | Opt-in foreground service that keeps the server alive while your app is backgrounded. Manifest-only opt-in, zero SDK-declared permissions. |
Capture is category-scoped: DevConsoleConfig.withCaptureCategories(...) narrows what's recorded
(NETWORK, SOCKET, MQTT, PUSH, LOGS, CRASHES, STATE, INSPECTION, MOCKS — default
is all). Events persist in a Room database bounded by a retention policy (7 days / 100 MB by
default).
devconsole (debug) and devconsole-noop (release) expose the same public API. The no-op
twin records nothing, serves nothing, and links no server code — production safety comes from
dependency selection, not a runtime flag. The Gradle plugin
(io.github.devconsole-android) auto-wires the debug/release split if you omit the
dependencies, and verifies — declared dependencies, resolved runtime classpath, and final
APK/AAB bytes — that the full runtime never reaches a protected variant.
On debuggable builds the SDK auto-initializes from its own ContentProvider; no
Application.onCreate boilerplate needed. The server itself never auto-starts — you always
call startBrowser(...) explicitly.
DevConsole.open(context) opens the inspector from any trigger you like and returns an
InspectorOpenResult. To let the SDK open it without host code, opt into the triggers — both are
off by default, and neither ever starts the server:
DevConsoleConfig.default().withOpenTriggers(
OpenTriggers(
shakeToOpen = true,
shakeIntensity = ShakeIntensity.MEDIUM, // LIGHT | MEDIUM | FIRM
floatingButton = true,
),
)Java: DevConsoleConfig.builder().openTriggers(OpenTriggers.builder().shakeToOpen(true).build()).
No code required: the inspector's More screen has Start/Stop buttons, and once running it shows the live connect URL — as text, a copy button, and a QR code to scan from another machine.
From code:
// Optional — auto-init already ran on debuggable builds. Call it yourself to customize:
DevConsole.initialize(application, DevConsoleConfig.default())
// startBrowser and stop are suspend functions — call them from a coroutine:
val result = DevConsole.startBrowser(StartRequest(bindingMode = BindingMode.LOOPBACK))
when (result) {
is StartResult.Started -> {
result.endpoint // host + port actually bound
result.access.connectUrl // the full credential URL — treat as a secret
}
is StartResult.PermissionRequired -> { /* LAN only: request result.permission */ }
else -> { /* NoEligibleNetwork, ServerUnavailable, ... */ }
}
DevConsole.stop(StopReason.UserRequested)Java is fully supported via async variants and builders:
DevConsole.initialize(getApplication(), DevConsoleConfig.builder().build());
StartRequest request = new StartRequest(BindingMode.LAN, new kotlin.ranges.IntRange(8080, 8099));
DevConsole.startBrowserAsync(request, result -> runOnUiThread(() -> {
if (result instanceof StartResult.Started) {
StartResult.Started started = (StartResult.Started) result;
String url = started.getAccess().getConnectUrl();
}
}));After startBrowser, filter Logcat on the DevConsole tag:
I/DevConsole: Dashboard available at: http://127.0.0.1:8080/ (access link available through the DevConsole API/launcher; binding: LOOPBACK)
The session code is deliberately absent from Logcat. The full URL — with its
#code=<session code> credential fragment — is available only from
StartResult.Started.access.connectUrl, DevConsole.accessInfo(), and the device's More screen
(as text and QR code). The port is the first free one in 8080–8099, so read it from the log
rather than assuming 8080.
- Loopback (default): run
adb forward tcp:<port> tcp:<port>, then open the connect URL. - LAN: pass
BindingMode.LANand open the URL from any device on the same network — read the threat model first; the dashboard speaks plaintext HTTP. A LAN start that finds no eligible network interface returnsStartResult.NoEligibleNetwork— it never silently falls back to loopback (or vice versa).
Open the whole URL. The #code= fragment is the credential: single-use, expires in five
minutes, creates a session immediately with no approval step. Bare http://host:port/ sits
unauthenticated forever. Issue a fresh code from the device if one lapses.
// OkHttp / Retrofit — capture + timing in one call:
val client = OkHttpClient.Builder()
.installDevConsole(DevConsole.networkRecorder())
.addInterceptor(DevConsoleMockInterceptor(DevConsole.mockEngine())) // optional, mocks
.build()
// Ktor client (captures metadata + request bodies; response bodies are never read):
val ktor = HttpClient { install(DevConsoleKtorClientPlugin) { recorder = DevConsole.networkRecorder() } }
// WebSocket (OkHttp): inbound + outbound
val socket = client.newWebSocket(request, DevConsoleOkHttpWebSocketListener(DevConsole.socketRecorder()))
val recording = DevConsoleRecordingWebSocket.wrap(socket, DevConsole.socketRecorder())
// MQTT (Eclipse Paho):
val publisher = DevConsolePahoMqtt.install(mqttClient, DevConsole.socketRecorder())
// Push (from FirebaseMessagingService.onMessageReceived):
DevConsole.recordPush(FirebaseRemoteMessageAdapter().toPushInput(remoteMessage))
// Anything else (Cronet, Volley, custom): record directly — never throws, never blocks:
DevConsole.networkRecorder().record(requestInput, responseInput, startedAtMs, completedAtMs)Java equivalents exist throughout (e.g. DevConsoleOkHttp.install(builder, recorder)). Details:
docs/NETWORK_ADAPTERS.md, docs/MQTT_CAPTURE.md,
docs/PUSH.md.
Group io.github.devconsole-android, one version for everything. Two coordinates cover a normal
integration (there is deliberately no BOM):
| Coordinate | Scope | What it is |
|---|---|---|
devconsole |
debugImplementation |
The full debug runtime: server, dashboard, capture, storage, exports, OkHttp adapters. |
devconsole-noop |
releaseImplementation |
Same API, does nothing, links nothing. |
Opt-in add-ons (each -noop twin is the matching releaseImplementation):
| Coordinate | Release twin | Adds |
|---|---|---|
devconsole-ui-compose |
(none — debugImplementation only) |
Compose launcher-panel API (the on-device inspector itself already ships inside devconsole; name this coordinate only to call the panel composables) |
devconsole-ui-views |
(none — debugImplementation only) |
DevConsolePanelView launcher for XML/Views hosts |
devconsole-network-ktor |
(none) | Ktor HttpClient capture plugin |
devconsole-socket-paho |
devconsole-socket-paho-noop |
MQTT capture via Eclipse Paho |
devconsole-push-firebase |
devconsole-push-firebase-noop |
FCM push adapter (reflection-based) |
Every other module (devconsole-core, devconsole-storage-room, …) arrives transitively — you
never name it. All modules publish sources, javadoc, and signed POMs.
DevConsole is a debugging tool that intentionally exposes your app's internals to a browser. The design keeps that safe by default, but you should know exactly where the edges are:
- The dashboard speaks plaintext HTTP. There is no TLS. In LAN mode, anyone who can observe
your network packets can read everything the dashboard shows — headers, tokens, bodies, exports.
Loopback +
adb forwardis the default for this reason; LAN is always an explicit opt-in. - The connect URL is a credential. Possession of a live
#code=fragment creates a session — no on-device approval step. Codes are single-use with a five-minute TTL; sessions last 30 minutes. Treat the URL like a password; the device's More screen can revoke sessions. - Redaction is an allowlist. ~25 well-known field names (plus
Bearertokens) are masked. Custom header names, signed-URL query params, and PII inside bodies pass through verbatim. See docs/SECURITY_AND_REDACTION.md. - Screenshots cannot be redacted — pixels don't have field names. Screenshot capture is off
by default and everything it produces is marked
UNREDACTED. - Editing is off by default. Preferences/database/file writes, mock editing, and capture-rule
editing are each gated per surface via
EditingCapabilities; the composer and state mutation have their ownDevConsoleConfigflags. Everything defaults to off/read-only.
The full analysis, including what a malicious network peer or co-installed app can and cannot do: docs/THREAT_MODEL.md.
Three runnable samples under samples/ cover every integration style:
| Sample | Stack | Posture |
|---|---|---|
compose-app |
Jetpack Compose | Everything unlocked: all editing capabilities, composer, screenshots, MQTT + WebSocket demos, in-app inspector, shake + floating-button open triggers |
foundation-app |
Stock widgets, no UI framework | Everything at its locked-down default — the read-only contrast |
views-java-app |
Java + XML, ui-views panel |
Middle ground: mocks and capture rules editable, data read-only, async Java APIs, shake-to-open (LIGHT) |
./gradlew :samples:compose-app:assembleDebugEach sample seeds preferences, a SQLite table, and files so the inspectors have real content, and includes a hazard section that triggers a real crash and a real ANR.
minSdk |
23 |
compileSdk / targetSdk |
35 |
| Android Gradle Plugin | 8.13.0 (built against; the Gradle plugin supports AGP 8.x–9.x hosts) |
| Gradle | 8.9+ (including 9.x) |
| Kotlin | 2.2.20 |
Full index: docs/README.md. Highlights:
- Threat model and safe operation — start here before LAN mode
- Getting started: Compose · XML/Kotlin · XML/Java
- Build variants and production safety
- Network adapters · MQTT capture · Push
- Data inspectors and exports · Evidence and bug reports
- Crash and ANR capture · Background keep-alive
- FAQ / troubleshooting
See CONTRIBUTING.md for build prerequisites and the test/lint/API-check
commands. CI runs the same ./gradlew tasks on every PR.


