| Status | Implemented |
| Modules | :app, :core:debug, :core:distro, and the Java Dev Pack's adapter/ |
| Primary sources | app/src/main/java/dev/blamspot/jcode/debug/AndroidAppProject.kt, app/src/main/java/dev/blamspot/jcode/debug/AndroidDebugAttach.kt (157 lines), app/src/main/java/dev/blamspot/jcode/debug/DebugController.kt (702 lines), core/distro/src/main/java/dev/blamspot/jcode/core/distro/adb/AdbHostClient.kt, the Java Dev Pack's adapter/ |
| Verified against | commit cea581c, 2026-08-09 |
Debugging an Android application built inside JCode, on the same device JCode is running on: how the application module is identified, and how the app's ART JDWP channel is exposed to the Java debug adapter.
It is always an attach, never a launch. ART speaks JDWP only for a debuggable process and only over adb's
jdwp:<pid>service — there is no-agentlib:jdwpleg to start. That single fact shapes the entire flow.
appModuleFor(hostPath, projectDir) returns the Android application module for a debug target:
the project directory when it is the Gradle root, otherwise the nearest Gradle root above the active
file's path. It mirrors ProjectRunner.androidAppModule, so Run and Debug agree on what an Android
app project is and which module the APK comes from.
- Require
root/gradlew. - Walk the tree with
maxDepth = SCAN_DEPTH (8), capped atSCAN_FILE_CAP (8,000)files, skippingnode_modules,bin,obj,build,dist,out,targetand any dot-directory. - Note whether
gradle/libs.versions.tomlnamescom.android.application. - For each
build.gradle/build.gradle.kts, look line by line forcom.android.applicationor a version-catalog alias matchingalias\s*\([^)]*[Aa]ndroid[^)]*[Aa]pplication[^)]*\), excluding any line containingapply false. - Fallback: an
AndroidManifest.xmlcontainingandroid.intent.category.LAUNCHER; the module is its great-grandparent directory (src/main/AndroidManifest.xml→ module).
The
apply falseexclusion is load-bearing. A root build script names the plugin asid("com.android.application") … apply falseto declare the version for subprojects without applying it; matching the file as a whole would pick the root as the app module.
| Member | Purpose |
|---|---|
gradleRoot(hostPath) |
Nearest directory at or above the path holding gradlew, up to ROOT_WALK_UP_LIMIT (8) levels |
debugApk(module) |
Newest .apk under build/outputs/apk/debug (APK_DIR) — what the Run flow installs |
sourceRoots(module) |
src/main/java, src/main/kotlin (those that exist) — for the adapter's stack-frame → file resolution |
sequenceDiagram
participant DC as DebugController
participant A as AndroidDebugAttach
participant ADB as AdbHostClient
participant D as Device
participant JD as java-debug (in proot)
DC->>A: attach(module)
A->>A: readBadging(module) → package, launcher activity
A->>ADB: onlineSerial()
A->>ADB: requireInstalled(serial, package)
A->>D: am set-debug-app -w <package>
A->>A: resolvePid(serial, package, launchActivity)
A->>D: adb forward tcp:<port> jdwp:<pid>
A-->>DC: local port
DC->>JD: DAP attach → 127.0.0.1:<port>
Steps in detail:
readBadging(module)runsaapt dump badgingover the built debug APK to read the package name and launcher activity.onlineSerial()resolves an online device from the ADB bridge, preferring the loopback relay serial.requireInstalled(serial, package)confirms the app is installed.am set-debug-app -w <package>— the-wholds the next launch at "Waiting For Debugger" until a debugger attaches, so breakpoints inApplication/Activitystart-up still bind in time. It has no effect on an already-running process, which is exactly why it is set before the pid is resolved.resolvePid(...)finds or launches the process.freeLocalPort()thenadb forward tcp:<port> jdwp:<pid>.- The java-debug adapter — running inside proot, which shares the app's network namespace — issues a
DAP
attachto127.0.0.1:<port>.
adb refused to forward tcp:<port> to jdwp:<pid> for <package> — only a debuggable
(debug-variant) build exposes a JDWP channel.
A release-variant APK has no JDWP channel at all; this is the most common attach failure and the error text says so directly.
suspend fun detach()Undoes everything attach changed:
am clear-debug-app— essential, becauseset-debug-app -woutlives the session. Left set, it would strand the app at "Waiting For Debugger" the next time the user launched it by hand.adb killforwardfor the forwarded port.
debugAppSerial and forwardedPort are tracked so a partial attach can still be undone.
DebugController.prepareJvm checks AndroidAppProject.appModuleFor(...). When it returns a module,
the flow routes to prepareAndroidAttach instead of a plain JVM launch. Everything downstream is a
normal DAP session — see
Debug Adapter Protocol.
The adapter is java-debug, wrapped by the Java Dev Pack's adapter/ with JCode-specific providers
(JCodeSourceLookUpProvider, JCodeEvaluationProvider, JCodeCompletionsProvider,
JCodeHotCodeReplaceProvider, JCodeVirtualMachineManagerProvider).
The virtual device is also an adb target: VirtualDeviceAdbService answers
shell:am start -n, shell:pm list packages and exec:cmd package 'install' -S <n> — so an app can
be built, installed and launched inside JCode's own sandbox, then debugged over the same ADB bridge.
See ADB bridge and
App sandbox architecture.
RUN_IN_VIRTUAL_DEVICE (default false) selects whether Run targets the sandbox or the real device.
- Android debugging is always an attach.
- Only a debuggable (debug-variant) build exposes a JDWP channel.
set-debug-app -wmust always be cleared on detach.- Detection excludes
apply falselines. - Run and Debug must agree on the app module — both go through the same detection.
- Tree scans are bounded by depth and file count so a monorepo cannot hang detection.
| Failure | Effect |
|---|---|
| Release-variant APK | adb forward refused; the error names the cause |
| No online device | Attach aborts at onlineSerial() |
| App not installed | Attach aborts at requireInstalled |
| Process never starts | resolvePid fails |
| Session killed without detach | The app is stranded at "Waiting For Debugger" until am clear-debug-app runs |
| Wireless debugging off | The ADB bridge is Degraded; no serial to attach to |
aapt not installed |
readBadging fails — the Android SDK catalog entry provides it |
- Hot code replace is wired through
JCodeHotCodeReplaceProviderbut not exercised in the documented device verification. - Source lookup depends on
sourceRoots(module)findingsrc/main/javaorsrc/main/kotlin; a non-standard layout will not resolve stack frames to files. - Only the application module is detected; a library module cannot be debugged directly.