Skip to content

Commit 2ca4ab3

Browse files
authored
Add Android on-device debugging support (#5012)
* Add Android on-device debugging support Mirrors the iOS on-device-debug flow from #4999 for Android, taking advantage of the fact that Dalvik/ART already speaks JDWP — no desktop proxy needed. The Codename One Maven plugin orchestrates adb: install the APK, mark it as the debug-app, launch the Activity, poll for the PID, forward JDWP onto localhost, and tail logcat. The existing IntelliJ workflow extends: there is now a CN1 Android On-Device Debug + CN1 Attach Android pair under the same On-Device Debug folder as the iOS configs. Pieces: - AndroidGradleBuilder parses a new android.onDeviceDebug build hint that flips the generated manifest to debuggable=true and disables R8/proguard so symbols and locals survive the build. Release builds (anything without the hint) are unaffected. - cn1:android-on-device-debugging — new Mojo that drives the adb session end-to-end, with autodetection of adb from ANDROID_HOME / ANDROID_SDK_ROOT / standard Studio SDK paths / $PATH, optional adb connect <ip:port> for wireless devices (Android 11+ adb pair / adb connect and legacy adb tcpip both covered), APK autodetection from target/, am set-debug-app -w for wait-for- attach, and a logcat stream prefixed [device] for the lifetime of the session. Cleans up adb forward on shutdown. - cn1:buildAndroidOnDeviceDebug — wrapper that force-sets the hint and invokes the existing android-device cloud build, so the IDE menu has a one-click entry that doesn't depend on the project's codenameone_settings.properties. Archetype: - common/codenameone_settings.properties carries the new hint commented out, next to the existing ios.onDeviceDebug.* lines. - .idea/runConfigurations/CN1_Android_OnDeviceDebug.xml — Maven run config that invokes the new Mojo from the project root. - .idea/runConfigurations/CN1_Attach_Android.xml — Remote JVM Debug to localhost:5005, scoped to the -common module. Developer guide: - New On-Device-Debugging-Android.asciidoc chapter: IntelliJ quick start, wireless debugging (both Android 11+ and legacy paths), Maven CLI flow, flag table, source-resolution for both codenameone-core and codenameone-android sources jars, and troubleshooting. Explicitly calls out that JNI C/C++ is out of scope (use Android Studio + LLDB for that, can run alongside). - Advanced-Topics-Under-The-Hood gets an android.onDeviceDebug entry next to the existing android.debug / android.release rows. * Address CI feedback on Android on-device debugging Three gates failed on the original push: - SpotBugs DE_MIGHT_IGNORE in AndroidOnDeviceDebuggingMojo's shutdown hook (catch-all swallowed every Exception silently). Narrow the catch to MojoFailureException — the only checked exception runAdb can throw — and surface the message to stderr so a stuck adb forward is at least visible at shutdown. Process.destroy() doesn't declare a checked exception, so the surrounding try/catch is no longer needed for that call. - Vale (Microsoft.Contractions + Microsoft.Adverbs) on On-Device-Debugging-Android.adoc: "it is" → "it's"; drop "rarely" by rewriting the command-line-flags lead sentence; drop "silently" from the breakpoint-not-firing troubleshooter. - LanguageTool typo flags: "codepath" → "code path" in the waitForAttach=false note; allowlist "adb", "logcat", and "pidof" in languagetool-accept.txt under a new "Android tooling" section so future Android docs don't re-trip the same rule. * android.onDeviceDebug: also disable ProGuard and pin to debug-only Two follow-ups to the initial hint binding: - Force off android.enableProguard alongside disableR8. ProGuard runs on the non-Gradle-8 legacy path, and even on Gradle 8 it can layer on top of R8 when android.enableProguard=true; either way the user loses method names and locals. The hint now disables both obfuscators so symbols survive the build regardless of which Gradle line the project is on. - Force android.release=false and android.debug=true. Android's manifest is shared between the release and debug variants, so android:debuggable="true" propagates to both. The cloud build also produces both APKs by default (android.release defaults to true). Without this pin, a stray hint left in codenameone_settings.properties would silently ship a release-signed APK marked debuggable=true — a serious security problem since any device that side-loads it would be exposing a JDWP socket. Pinning to debug-only means the release APK is simply not produced when the hint is on, and the developer guide row for the hint now spells that out. Companion change in the BuildDaemon's AndroidGradleBuilder (separate repo) applies the same guard for the cloud build path. * docs: contraction fixes in android.onDeviceDebug hint row Vale Microsoft.Contractions flagged the expanded row on the second CI run: "cannot" → "can't", "that is" → "that's". Same fix pattern as the earlier batch of contraction tweaks in the Android chapter.
1 parent 18502ed commit 2ca4ab3

10 files changed

Lines changed: 909 additions & 0 deletions

File tree

docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Here is the current list of supported arguments. Build hints change over time, s
2525
|android.release
2626
|true/false defaults to true - indicates whether to include the release version in the build
2727

28+
|android.onDeviceDebug
29+
|Boolean true/false defaults to false. When `true`, the generated `AndroidManifest.xml` is marked `android:debuggable="true"`, R8/proguard is disabled, and the build is pinned to debug-only (`android.release` is forced off and `android.debug` is forced on) so a stray hint can't ship a release-signed APK that's `debuggable="true"`. Pair with the `cn1:android-on-device-debugging` Maven goal (or the bundled IntelliJ run configs) to install, launch, forward JDWP, and stream logcat through adb. Has no effect on builds that don't carry it — release builds are unaffected. See the link:#_ondevice_debugging_android[On-Device Debugging (Android) chapter] for the full flow.
30+
2831
|android.installLocation
2932
|Maps to android:installLocation manifest entry defaults to auto. Can also be set to internalOnly or preferExternal.
3033

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
== On-Device Debugging (Android)
2+
3+
The companion to the iOS chapter, for the Android side. The motivation
4+
is the same — bugs that only reproduce on a real device or on the
5+
Android emulator — but the implementation is much simpler. The
6+
Dalvik/ART runtime already exposes a JDWP socket per debuggable
7+
process, so there is no desktop proxy and no custom protocol. The
8+
Codename One Maven plugin just orchestrates `adb`: install the APK,
9+
mark it as the debug-app, launch the activity, forward the JDWP
10+
socket onto `localhost`, and tail logcat.
11+
12+
Everything below assumes you have the Android platform-tools on your
13+
machine (Android Studio installs them, or
14+
https://developer.android.com/tools/releases/platform-tools[the
15+
standalone package]) and `adb` is reachable through `ANDROID_HOME`,
16+
`ANDROID_SDK_ROOT`, or `$PATH`.
17+
18+
=== When to use it
19+
20+
* A bug only reproduces on a physical Android device or in the Android
21+
emulator and not in the Codename One simulator.
22+
* You want to single-step through your code while it runs on the
23+
device, with full access to locals, fields, and the
24+
`Display.getInstance()...` accessor chains.
25+
* You want to attach without a USB cable — the wireless flow below
26+
covers both the legacy `adb tcpip` path and the Android 11+
27+
`adb pair` path.
28+
29+
If the bug reproduces in the simulator, stay in the simulator — its
30+
debugger is faster and has zero device-side moving parts.
31+
32+
=== Quick start (IntelliJ IDEA)
33+
34+
Projects generated from the `cn1app-archetype` ship with two run
35+
configurations under the *On-Device Debug* folder: *CN1 Android
36+
On-Device Debug* and *CN1 Attach Android*. The flow is:
37+
38+
==== 1. Enable the build hint
39+
40+
In `common/codenameone_settings.properties`, uncomment the line the
41+
archetype generated:
42+
43+
[source,properties]
44+
----
45+
codename1.arg.android.onDeviceDebug=true
46+
----
47+
48+
This flips the generated `AndroidManifest.xml` to
49+
`debuggable="true"` and disables R8/proguard so symbols and locals
50+
survive the build. Release builds (anything without this hint) are
51+
unaffected.
52+
53+
==== 2. Build the APK
54+
55+
Either the cloud build (`cn1:buildAndroidOnDeviceDebug` — wraps
56+
`cn1:buildAndroid` and force-sets the hint above) or your usual
57+
`cn1:buildAndroid` once the hint is set. Both produce a signed,
58+
debuggable APK in the project's `target/` directory.
59+
60+
For a fully local build, use `cn1:buildAndroidGradleProject` to
61+
generate the Gradle project under `target/...-android-source/` and
62+
run `./gradlew assembleDebug` from there; the Mojo will autodetect
63+
the resulting APK under `build/outputs/apk/`.
64+
65+
==== 3. Connect the device
66+
67+
Plug a device in over USB with USB debugging enabled, *or* connect
68+
wirelessly (see <<on-device-debug-android-wireless,Wireless debugging>>
69+
below).
70+
71+
==== 4. Run the debug session from IntelliJ
72+
73+
Select *CN1 Android On-Device Debug* from the Run-config dropdown
74+
and click ▶ *Run* (the green play icon — not the bug icon). The Run
75+
tool window prints:
76+
77+
----
78+
Using adb: /Users/you/Library/Android/sdk/platform-tools/adb
79+
Target device: emulator-5554
80+
Installing my-app-1.0.apk on emulator-5554
81+
Marking com.example.myapp as the debug app (waits for debugger).
82+
Launching com.example.myapp.MyAppStub
83+
App PID on device: 12345
84+
85+
==================================================================
86+
JDWP forwarded: localhost:5005 -> device pid 12345
87+
Attach IntelliJ: Run -> 'CN1 Attach Android' (Remote JVM Debug)
88+
==================================================================
89+
----
90+
91+
After that banner, logcat output (filtered to the app's PID) streams
92+
through the Run window prefixed with `[device]`.
93+
94+
==== 5. Attach the debugger
95+
96+
Switch the Run-config dropdown to *CN1 Attach Android* and click 🐞
97+
*Debug*. IntelliJ connects to `localhost:5005` and opens a Debug tool
98+
window. The app is paused inside `Debug.waitForDebugger()` at this
99+
point — set your breakpoints, then resume from the IDE to let it
100+
proceed past boot.
101+
102+
Set breakpoints, step, inspect, evaluate expressions — all the
103+
normal remote-attach features work. Unlike the iOS path, there is
104+
no method-invocation or static-field limitation here; the JVM is
105+
the real Android runtime.
106+
107+
[#on-device-debug-android-wireless]
108+
=== Wireless debugging
109+
110+
==== Android 11 and newer (recommended)
111+
112+
. On the device: *Settings → Developer options → Wireless debugging →
113+
Pair device with pairing code*. Note the *IP & port* (for example
114+
`192.168.1.42:37051`) and the *six-digit pairing code*.
115+
. Pair from your laptop (this only has to be done once per network):
116+
+
117+
[source,bash]
118+
----
119+
adb pair 192.168.1.42:37051
120+
# When prompted, enter the 6-digit code shown on the device.
121+
----
122+
. Connect:
123+
+
124+
[source,bash]
125+
----
126+
adb connect 192.168.1.42:5555
127+
----
128+
+
129+
The connect port is *not* the pairing port — it's the one shown on
130+
the *Wireless debugging* screen above the *Pair device* button.
131+
. Run the debug session as normal. Pass the IP and port through the
132+
Mojo if you prefer to do the `adb connect` in one step:
133+
+
134+
[source,bash]
135+
----
136+
mvn cn1:android-on-device-debugging \
137+
-Dcn1.android.onDeviceDebug.wireless=192.168.1.42:5555
138+
----
139+
140+
==== Android 10 and older
141+
142+
. Plug the device in over USB.
143+
. Switch adb to TCP/IP mode and grab the device's IP from
144+
*Settings → About phone → Status*:
145+
+
146+
[source,bash]
147+
----
148+
adb tcpip 5555
149+
adb connect 192.168.1.42:5555
150+
----
151+
. Unplug the cable, then run the debug session as normal.
152+
153+
In either flow, the JDWP forward, app launch, and logcat stream all
154+
happen over the same Wi-Fi link.
155+
156+
=== Quick start (Maven from the command line)
157+
158+
Without the IntelliJ run configs, the same flow is two terminals:
159+
160+
[source,bash]
161+
----
162+
# Terminal 1 — build the APK once
163+
mvn cn1:buildAndroidOnDeviceDebug
164+
165+
# Terminal 2 — install, launch, forward JDWP, tail logcat
166+
mvn cn1:android-on-device-debugging
167+
----
168+
169+
Attach jdb (or another JDWP client) to the forwarded port:
170+
171+
[source,bash]
172+
----
173+
jdb -attach localhost:5005 \
174+
-sourcepath src/main/java:$HOME/.m2/repository/com/codenameone/codenameone-core/8.0-SNAPSHOT/codenameone-core-8.0-SNAPSHOT-sources.jar
175+
----
176+
177+
For VS Code (Debugger for Java extension), add a launch
178+
configuration of type `java` with `"request": "attach"`,
179+
`"hostName": "localhost"`, `"port": 5005`.
180+
181+
=== Useful command-line flags
182+
183+
The Mojo's defaults match what the archetype's run config does. The
184+
flags below exist for unusual setups:
185+
186+
[cols="1,2", options="header"]
187+
|===
188+
|Property
189+
|Meaning
190+
191+
|`-Dcn1.android.onDeviceDebug.adb=<path>`
192+
|Use a specific adb executable (default: search `ANDROID_HOME`,
193+
`ANDROID_SDK_ROOT`, the standard Android Studio SDK locations, and
194+
`$PATH`).
195+
196+
|`-Dcn1.android.onDeviceDebug.deviceSerial=<serial>`
197+
|Force a target device when more than one is online
198+
(`adb devices`).
199+
200+
|`-Dcn1.android.onDeviceDebug.wireless=<ip:port>`
201+
|Run `adb connect <ip:port>` before everything else (covers both the
202+
Android 11 wireless-debug path and the legacy `adb tcpip` path).
203+
204+
|`-Dcn1.android.onDeviceDebug.jdwpPort=<port>`
205+
|Local TCP port for `adb forward`. Default `5005`. Match this with
206+
the *CN1 Attach Android* run config if you change it.
207+
208+
|`-Dcn1.android.onDeviceDebug.apk=<path>`
209+
|Skip APK autodetection and install this APK instead.
210+
211+
|`-Dcn1.android.onDeviceDebug.skipInstall=true`
212+
|Skip the install step — the app is already on the device.
213+
214+
|`-Dcn1.android.onDeviceDebug.waitForAttach=false`
215+
|Don't run `am set-debug-app -w`. The app launches normally and you
216+
attach the debugger afterwards. Default is `true`.
217+
|===
218+
219+
=== What you can step through
220+
221+
Everything runs in one Dalvik/ART process, so the JDWP attach sees
222+
every class loaded by the app. In practical terms:
223+
224+
* *Your common-module Java* (`com.<yourcompany>.<app>.*`, anything
225+
under the `-common` module's `src/main/java`). Breakpoints work
226+
out of the box — this is the module the *CN1 Attach Android* run
227+
config is scoped to, so IntelliJ resolves the source pane and the
228+
variables view from your common-module classpath.
229+
* *Your android-module Java or Kotlin* (`src/main/java` of the
230+
`-android` module — native interface implementations, custom
231+
Activities, Android-only helpers). Open the file and set a
232+
breakpoint; the IDE's *Remote JVM Debug* config doesn't restrict
233+
which module breakpoints live in, the `<module>` setting only
234+
decides which module's classpath is used to *display* state. If
235+
the variables view ever looks empty when you stop inside
236+
`-android` code, switch the run config's *Use classpath of module*
237+
to the `-android` module and reattach.
238+
* *Codename One framework code* in `codenameone-core` and the
239+
Android port (`com.codename1.impl.android.*` such as
240+
`AndroidImplementation` and `CodenameOneActivity`). Both need
241+
source resolution — see the next section.
242+
* *Native C/C++ via the NDK* is *not* debuggable through this path.
243+
JDWP only speaks JVM. If you've added native sources through the
244+
Android NDK, attach Android Studio's LLDB to the same process for
245+
C/C++ debugging — the two attaches are independent and can run
246+
side-by-side against one device.
247+
248+
=== Pointing the IDE at the Codename One sources
249+
250+
The Android runtime serves real `.class` files, so the IDE only needs
251+
the matching `.java` files to render the source pane. The same two
252+
options cover both `codenameone-core` and the Android port
253+
(`codenameone-android`):
254+
255+
* *Maven sources jars (recommended).* IntelliJ resolves
256+
`codenameone-core-<version>-sources.jar` *and*
257+
`codenameone-android-<version>-sources.jar` automatically once you
258+
run *Maven → Reimport* with "Sources" enabled in the Maven
259+
settings.
260+
* *Local clone of the
261+
https://github.com/codenameone/CodenameOne[Codename One GitHub
262+
repository].* In IntelliJ: *Run → Edit Configurations… → CN1
263+
Attach Android → Configuration → Source roots → +*. Add two
264+
entries: the clone's `CodenameOne/src` directory for the
265+
framework core, and `Ports/Android/src` for `AndroidImplementation`
266+
and the rest of the Android port.
267+
268+
Without a source path, breakpoints in framework classes still trigger
269+
and locals / fields still read — you'll just see "Sources not found"
270+
in the editor when stepping into framework code.
271+
272+
=== Troubleshooting
273+
274+
==== "No Android device is online"
275+
276+
`adb devices` returns nothing useful. Common causes:
277+
278+
* The device's *USB debugging* toggle is off, or the per-laptop
279+
RSA fingerprint prompt is still pending on-device.
280+
* The cable is power-only (some short USB-C cables are charge-only).
281+
* For wireless: pairing expired (Android 11+) or the laptop is on a
282+
different Wi-Fi network.
283+
284+
==== "Multiple devices online"
285+
286+
Pass `-Dcn1.android.onDeviceDebug.deviceSerial=<serial>` (run
287+
`adb devices` to see serials). The emulator's serial looks like
288+
`emulator-5554`; a USB-attached phone is its hardware ID; a wireless
289+
device is `<ip>:<port>`.
290+
291+
==== App installs but won't pause for the debugger
292+
293+
`waitForAttach` only takes effect when the APK is built with
294+
`android.onDeviceDebug=true` (or `android.xapplication_attr`
295+
containing `android:debuggable="true"`). Verify by running
296+
`adb shell dumpsys package <your.package> | grep flags` — `DEBUGGABLE`
297+
must be in the list. If it isn't, rebuild with
298+
`mvn cn1:buildAndroidOnDeviceDebug`.
299+
300+
==== Breakpoint never fires
301+
302+
Check the *CN1 Attach Android* run config:
303+
304+
* *Host* is `localhost`, *Port* matches the `jdwpPort` printed by the
305+
debug session.
306+
* The *Use module classpath* dropdown is the `-common` module, so
307+
IntelliJ can resolve your `.java` files.
308+
309+
If a class loaded on the device doesn't match the class IntelliJ
310+
thinks is current, breakpoints stop firing with no error message.
311+
Rebuild and reinstall (`cn1:buildAndroidOnDeviceDebug` + re-run
312+
*CN1 Android On-Device Debug*).
313+
314+
==== logcat shows the app exiting with `Debug.waitForDebugger`-like noise
315+
316+
The system killed the process for taking too long to attach. Set
317+
`-Dcn1.android.onDeviceDebug.waitForAttach=false` and trigger the
318+
code path you want to debug manually after attach — Android's debug-app
319+
wait isn't bound to the breakpoint, only to process start.
320+
321+
==== Wireless connection drops mid-session
322+
323+
Wi-Fi connections to debug-mode devices are sensitive to power-saving.
324+
On the device, keep the screen on (or set *Settings → Developer
325+
options → Stay awake* while charging). For long sessions, plug into
326+
USB and use `adb -s <wireless-serial>` to keep the wireless TCP socket
327+
alive without depending on the radio.

docs/developer-guide/developer-guide.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ include::Working-With-iOS.asciidoc[]
9292

9393
include::On-Device-Debugging.asciidoc[]
9494

95+
include::On-Device-Debugging-Android.asciidoc[]
96+
9597
include::Working-With-Javascript.asciidoc[]
9698

9799
include::Working-with-Mac-OS-X.asciidoc[]

docs/developer-guide/languagetool-accept.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,11 @@ jdb
491491
loopback
492492
rethrow
493493
rethrows
494+
495+
# -----------------------------------------------------------------------------
496+
# Android tooling — names from the Android SDK / platform-tools that the
497+
# English dictionary doesn't recognise.
498+
# -----------------------------------------------------------------------------
499+
adb
500+
logcat
501+
pidof

maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Android_OnDeviceDebug.xml

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)