From a829578fb80f7399127109e0f7172e03429ad4b1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 13:36:50 +0300 Subject: [PATCH 1/4] Add Android on-device debugging support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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. --- .../Advanced-Topics-Under-The-Hood.asciidoc | 3 + .../On-Device-Debugging-Android.asciidoc | 327 ++++++++++++++ docs/developer-guide/developer-guide.asciidoc | 2 + .../CN1_Android_OnDeviceDebug.xml | 28 ++ .../runConfigurations/CN1_Attach_Android.xml | 17 + .../common/codenameone_settings.properties | 9 + .../builders/AndroidGradleBuilder.java | 19 + .../maven/AndroidOnDeviceDebuggingMojo.java | 412 ++++++++++++++++++ .../BuildAndroidOnDeviceDebugMojo.java | 75 ++++ 9 files changed, 892 insertions(+) create mode 100644 docs/developer-guide/On-Device-Debugging-Android.asciidoc create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Android_OnDeviceDebug.xml create mode 100644 maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Attach_Android.xml create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidOnDeviceDebuggingMojo.java create mode 100644 maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildAndroidOnDeviceDebugMojo.java diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index c1edb9f64a..81a0846e78 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -25,6 +25,9 @@ Here is the current list of supported arguments. Build hints change over time, s |android.release |true/false defaults to true - indicates whether to include the release version in the build +|android.onDeviceDebug +|Boolean true/false defaults to false. When `true`, the generated `AndroidManifest.xml` is marked `android:debuggable="true"` and R8/proguard is disabled so the Dalvik/ART JDWP socket is reachable for the resulting APK. 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. + |android.installLocation |Maps to android:installLocation manifest entry defaults to auto. Can also be set to internalOnly or preferExternal. diff --git a/docs/developer-guide/On-Device-Debugging-Android.asciidoc b/docs/developer-guide/On-Device-Debugging-Android.asciidoc new file mode 100644 index 0000000000..a04442840a --- /dev/null +++ b/docs/developer-guide/On-Device-Debugging-Android.asciidoc @@ -0,0 +1,327 @@ +== On-Device Debugging (Android) + +The companion to the iOS chapter, for the Android side. The motivation +is the same — bugs that only reproduce on a real device or on the +Android emulator — but the implementation is much simpler. The +Dalvik/ART runtime already exposes a JDWP socket per debuggable +process, so there is no desktop proxy and no custom protocol. The +Codename One Maven plugin just orchestrates `adb`: install the APK, +mark it as the debug-app, launch the activity, forward the JDWP +socket onto `localhost`, and tail logcat. + +Everything below assumes you have the Android platform-tools on your +machine (Android Studio installs them, or +https://developer.android.com/tools/releases/platform-tools[the +standalone package]) and `adb` is reachable through `ANDROID_HOME`, +`ANDROID_SDK_ROOT`, or `$PATH`. + +=== When to use it + +* A bug only reproduces on a physical Android device or in the Android + emulator and not in the Codename One simulator. +* You want to single-step through your code while it runs on the + device, with full access to locals, fields, and the + `Display.getInstance()...` accessor chains. +* You want to attach without a USB cable — the wireless flow below + covers both the legacy `adb tcpip` path and the Android 11+ + `adb pair` path. + +If the bug reproduces in the simulator, stay in the simulator — its +debugger is faster and has zero device-side moving parts. + +=== Quick start (IntelliJ IDEA) + +Projects generated from the `cn1app-archetype` ship with two run +configurations under the *On-Device Debug* folder: *CN1 Android +On-Device Debug* and *CN1 Attach Android*. The flow is: + +==== 1. Enable the build hint + +In `common/codenameone_settings.properties`, uncomment the line the +archetype generated: + +[source,properties] +---- +codename1.arg.android.onDeviceDebug=true +---- + +This flips the generated `AndroidManifest.xml` to +`debuggable="true"` and disables R8/proguard so symbols and locals +survive the build. Release builds (anything without this hint) are +unaffected. + +==== 2. Build the APK + +Either the cloud build (`cn1:buildAndroidOnDeviceDebug` — wraps +`cn1:buildAndroid` and force-sets the hint above) or your usual +`cn1:buildAndroid` once the hint is set. Both produce a signed, +debuggable APK in the project's `target/` directory. + +For a fully local build, use `cn1:buildAndroidGradleProject` to +generate the Gradle project under `target/...-android-source/` and +run `./gradlew assembleDebug` from there; the Mojo will autodetect +the resulting APK under `build/outputs/apk/`. + +==== 3. Connect the device + +Plug a device in over USB with USB debugging enabled, *or* connect +wirelessly (see <> +below). + +==== 4. Run the debug session from IntelliJ + +Select *CN1 Android On-Device Debug* from the Run-config dropdown +and click ▶ *Run* (the green play icon — not the bug icon). The Run +tool window prints: + +---- +Using adb: /Users/you/Library/Android/sdk/platform-tools/adb +Target device: emulator-5554 +Installing my-app-1.0.apk on emulator-5554 +Marking com.example.myapp as the debug app (waits for debugger). +Launching com.example.myapp.MyAppStub +App PID on device: 12345 + +================================================================== + JDWP forwarded: localhost:5005 -> device pid 12345 + Attach IntelliJ: Run -> 'CN1 Attach Android' (Remote JVM Debug) +================================================================== +---- + +After that banner, logcat output (filtered to the app's PID) streams +through the Run window prefixed with `[device]`. + +==== 5. Attach the debugger + +Switch the Run-config dropdown to *CN1 Attach Android* and click 🐞 +*Debug*. IntelliJ connects to `localhost:5005` and opens a Debug tool +window. The app is paused inside `Debug.waitForDebugger()` at this +point — set your breakpoints, then resume from the IDE to let it +proceed past boot. + +Set breakpoints, step, inspect, evaluate expressions — all the +normal remote-attach features work. Unlike the iOS path, there is +no method-invocation or static-field limitation here; the JVM is +the real Android runtime. + +[#on-device-debug-android-wireless] +=== Wireless debugging + +==== Android 11 and newer (recommended) + +. On the device: *Settings → Developer options → Wireless debugging → + Pair device with pairing code*. Note the *IP & port* (for example + `192.168.1.42:37051`) and the *six-digit pairing code*. +. Pair from your laptop (this only has to be done once per network): ++ +[source,bash] +---- +adb pair 192.168.1.42:37051 +# When prompted, enter the 6-digit code shown on the device. +---- +. Connect: ++ +[source,bash] +---- +adb connect 192.168.1.42:5555 +---- ++ +The connect port is *not* the pairing port — it is the one shown on +the *Wireless debugging* screen above the *Pair device* button. +. Run the debug session as normal. Pass the IP and port through the + Mojo if you prefer to do the `adb connect` in one step: ++ +[source,bash] +---- +mvn cn1:android-on-device-debugging \ + -Dcn1.android.onDeviceDebug.wireless=192.168.1.42:5555 +---- + +==== Android 10 and older + +. Plug the device in over USB. +. Switch adb to TCP/IP mode and grab the device's IP from + *Settings → About phone → Status*: ++ +[source,bash] +---- +adb tcpip 5555 +adb connect 192.168.1.42:5555 +---- +. Unplug the cable, then run the debug session as normal. + +In either flow, the JDWP forward, app launch, and logcat stream all +happen over the same Wi-Fi link. + +=== Quick start (Maven from the command line) + +Without the IntelliJ run configs, the same flow is two terminals: + +[source,bash] +---- +# Terminal 1 — build the APK once +mvn cn1:buildAndroidOnDeviceDebug + +# Terminal 2 — install, launch, forward JDWP, tail logcat +mvn cn1:android-on-device-debugging +---- + +Attach jdb (or another JDWP client) to the forwarded port: + +[source,bash] +---- +jdb -attach localhost:5005 \ + -sourcepath src/main/java:$HOME/.m2/repository/com/codenameone/codenameone-core/8.0-SNAPSHOT/codenameone-core-8.0-SNAPSHOT-sources.jar +---- + +For VS Code (Debugger for Java extension), add a launch +configuration of type `java` with `"request": "attach"`, +`"hostName": "localhost"`, `"port": 5005`. + +=== Useful command-line flags + +The Mojo's defaults match what the archetype's run config does — you +rarely need any of these — but they exist for unusual setups: + +[cols="1,2", options="header"] +|=== +|Property +|Meaning + +|`-Dcn1.android.onDeviceDebug.adb=` +|Use a specific adb executable (default: search `ANDROID_HOME`, +`ANDROID_SDK_ROOT`, the standard Android Studio SDK locations, and +`$PATH`). + +|`-Dcn1.android.onDeviceDebug.deviceSerial=` +|Force a target device when more than one is online +(`adb devices`). + +|`-Dcn1.android.onDeviceDebug.wireless=` +|Run `adb connect ` before everything else (covers both the +Android 11 wireless-debug path and the legacy `adb tcpip` path). + +|`-Dcn1.android.onDeviceDebug.jdwpPort=` +|Local TCP port for `adb forward`. Default `5005`. Match this with +the *CN1 Attach Android* run config if you change it. + +|`-Dcn1.android.onDeviceDebug.apk=` +|Skip APK autodetection and install this APK instead. + +|`-Dcn1.android.onDeviceDebug.skipInstall=true` +|Skip the install step — the app is already on the device. + +|`-Dcn1.android.onDeviceDebug.waitForAttach=false` +|Don't run `am set-debug-app -w`. The app launches normally and you +attach the debugger afterwards. Default is `true`. +|=== + +=== What you can step through + +Everything runs in one Dalvik/ART process, so the JDWP attach sees +every class loaded by the app. In practical terms: + +* *Your common-module Java* (`com...*`, anything + under the `-common` module's `src/main/java`). Breakpoints work + out of the box — this is the module the *CN1 Attach Android* run + config is scoped to, so IntelliJ resolves the source pane and the + variables view from your common-module classpath. +* *Your android-module Java or Kotlin* (`src/main/java` of the + `-android` module — native interface implementations, custom + Activities, Android-only helpers). Open the file and set a + breakpoint; the IDE's *Remote JVM Debug* config doesn't restrict + which module breakpoints live in, the `` setting only + decides which module's classpath is used to *display* state. If + the variables view ever looks empty when you stop inside + `-android` code, switch the run config's *Use classpath of module* + to the `-android` module and reattach. +* *Codename One framework code* in `codenameone-core` and the + Android port (`com.codename1.impl.android.*` such as + `AndroidImplementation` and `CodenameOneActivity`). Both need + source resolution — see the next section. +* *Native C/C++ via the NDK* is *not* debuggable through this path. + JDWP only speaks JVM. If you've added native sources through the + Android NDK, attach Android Studio's LLDB to the same process for + C/C++ debugging — the two attaches are independent and can run + side-by-side against one device. + +=== Pointing the IDE at the Codename One sources + +The Android runtime serves real `.class` files, so the IDE only needs +the matching `.java` files to render the source pane. The same two +options cover both `codenameone-core` and the Android port +(`codenameone-android`): + +* *Maven sources jars (recommended).* IntelliJ resolves + `codenameone-core--sources.jar` *and* + `codenameone-android--sources.jar` automatically once you + run *Maven → Reimport* with "Sources" enabled in the Maven + settings. +* *Local clone of the + https://github.com/codenameone/CodenameOne[Codename One GitHub + repository].* In IntelliJ: *Run → Edit Configurations… → CN1 + Attach Android → Configuration → Source roots → +*. Add two + entries: the clone's `CodenameOne/src` directory for the + framework core, and `Ports/Android/src` for `AndroidImplementation` + and the rest of the Android port. + +Without a source path, breakpoints in framework classes still trigger +and locals / fields still read — you'll just see "Sources not found" +in the editor when stepping into framework code. + +=== Troubleshooting + +==== "No Android device is online" + +`adb devices` returns nothing useful. Common causes: + +* The device's *USB debugging* toggle is off, or the per-laptop + RSA fingerprint prompt is still pending on-device. +* The cable is power-only (some short USB-C cables are charge-only). +* For wireless: pairing expired (Android 11+) or the laptop is on a + different Wi-Fi network. + +==== "Multiple devices online" + +Pass `-Dcn1.android.onDeviceDebug.deviceSerial=` (run +`adb devices` to see serials). The emulator's serial looks like +`emulator-5554`; a USB-attached phone is its hardware ID; a wireless +device is `:`. + +==== App installs but won't pause for the debugger + +`waitForAttach` only takes effect when the APK is built with +`android.onDeviceDebug=true` (or `android.xapplication_attr` +containing `android:debuggable="true"`). Verify by running +`adb shell dumpsys package | grep flags` — `DEBUGGABLE` +must be in the list. If it isn't, rebuild with +`mvn cn1:buildAndroidOnDeviceDebug`. + +==== Breakpoint never fires + +Check the *CN1 Attach Android* run config: + +* *Host* is `localhost`, *Port* matches the `jdwpPort` printed by the + debug session. +* The *Use module classpath* dropdown is the `-common` module, so + IntelliJ can resolve your `.java` files. + +If a class loaded on the device doesn't match the class IntelliJ +thinks is current, breakpoints silently drop. Rebuild and reinstall +(`cn1:buildAndroidOnDeviceDebug` + re-run *CN1 Android On-Device +Debug*). + +==== logcat shows the app exiting with `Debug.waitForDebugger`-like noise + +The system killed the process for taking too long to attach. Set +`-Dcn1.android.onDeviceDebug.waitForAttach=false` and trigger the +codepath you want to debug manually after attach — Android's debug-app +wait isn't bound to the breakpoint, only to process start. + +==== Wireless connection drops mid-session + +Wi-Fi connections to debug-mode devices are sensitive to power-saving. +On the device, keep the screen on (or set *Settings → Developer +options → Stay awake* while charging). For long sessions, plug into +USB and use `adb -s ` to keep the wireless TCP socket +alive without depending on the radio. diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 7a2d34b5e9..6b87b1785a 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -92,6 +92,8 @@ include::Working-With-iOS.asciidoc[] include::On-Device-Debugging.asciidoc[] +include::On-Device-Debugging-Android.asciidoc[] + include::Working-With-Javascript.asciidoc[] include::Working-with-Mac-OS-X.asciidoc[] diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Android_OnDeviceDebug.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Android_OnDeviceDebug.xml new file mode 100644 index 0000000000..c60769443d --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Android_OnDeviceDebug.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Attach_Android.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Attach_Android.xml new file mode 100644 index 0000000000..42cc5dca33 --- /dev/null +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/.idea/runConfigurations/CN1_Attach_Android.xml @@ -0,0 +1,17 @@ + + + + diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/codenameone_settings.properties b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/codenameone_settings.properties index 5caf691923..f75b2331d1 100644 --- a/maven/cn1app-archetype/src/main/resources/archetype-resources/common/codenameone_settings.properties +++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/common/codenameone_settings.properties @@ -34,6 +34,15 @@ codename1.ios.release.provision= #codename1.arg.ios.onDeviceDebug.proxyPort=55333 # Block the app at boot until the debugger attaches (default: false). #codename1.arg.ios.onDeviceDebug.waitForAttach=true +# On-device debugging for Android apps. When set, the Android build +# marks the APK debuggable and skips R8/proguard so the Dalvik/ART +# JDWP socket is reachable through adb. Pair with the IntelliJ +# "CN1 Android On-Device Debug" and "CN1 Attach Android" run configs +# bundled with this project, or with the cn1:android-on-device-debugging +# Maven goal. See the "On-Device Debugging (Android)" chapter of the +# developer guide for the wireless-debugging instructions and the full +# adb flow. +#codename1.arg.android.onDeviceDebug=true codename1.j2me.nativeTheme=nbproject/nativej2me.res codename1.kotlin=false codename1.languageLevel=5 diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index 69c278d3f2..f19983cb04 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -538,6 +538,16 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc // R8 configuration - disable full mode by default to prevent issues with reflection disableR8 = request.getArg("android.disableR8", "false").equals("true"); disableR8FullMode = request.getArg("android.disableR8FullMode", "true").equals("true"); + + // On-device debugging: when set, mark the APK debuggable so Dalvik/ART exposes + // a JDWP socket the cn1:android-on-device-debugging Mojo can forward through adb. + // Forcing debuggable also flips off R8/proguard (debug builds skip shrinking + // anyway, but we may be invoked from the android-device cloud target which + // would otherwise run full optimisation). Release builds are unaffected. + boolean onDeviceDebug = request.getArg("android.onDeviceDebug", "false").equals("true"); + if (onDeviceDebug) { + disableR8 = true; + } if (useGradle8) { getGradleJavaHome(); // will throw build exception if JAVA17_HOME is not set MIN_GRADLE_VERSION = 8; @@ -2483,6 +2493,14 @@ public void usesClassMethod(String cls, String method) { allowBackup = ""; } + // On-device debugging needs the application to be marked debuggable so the + // runtime hands out a JDWP socket per-process. Idempotent: skip if the user + // already declared android:debuggable via android.xapplication_attr. + String debuggableAttr = ""; + if (onDeviceDebug && !applicationAttr.contains("android:debuggable")) { + debuggableAttr = " android:debuggable=\"true\" "; + } + String applicationNode = " } for wireless devices. + * 2. (optional) {@code adb install -r } when an APK is found in + * the build output or passed via -Dapk. + * 3. {@code adb shell am set-debug-app -w --persistent } when + * waitForAttach is true, so the app blocks at boot for the IDE. + * 4. {@code adb shell am start -n /Stub} to launch. + * 5. Polls {@code adb shell pidof } for the running PID. + * 6. {@code adb forward tcp: jdwp:} exposes the device + * JDWP socket on localhost. + * 7. Streams {@code adb logcat --pid=} to this console with a + * {@code [device]} prefix. + * + * The Mojo blocks until interrupted (Ctrl-C) or the device process exits. + * + * Properties: + * -Dcn1.android.onDeviceDebug.adb=path/to/adb adb override + * -Dcn1.android.onDeviceDebug.deviceSerial= target one device + * -Dcn1.android.onDeviceDebug.wireless=192.168.1.5:5555 adb connect first + * -Dcn1.android.onDeviceDebug.jdwpPort=5005 local forward port + * -Dcn1.android.onDeviceDebug.apk=path/to/app.apk APK override + * -Dcn1.android.onDeviceDebug.packageName=... override package + * -Dcn1.android.onDeviceDebug.mainClass=... override main class + * -Dcn1.android.onDeviceDebug.waitForAttach=true|false default true + * -Dcn1.android.onDeviceDebug.skipInstall=true don't reinstall APK + */ +@Mojo(name = "android-on-device-debugging") +public class AndroidOnDeviceDebuggingMojo extends AbstractCN1Mojo { + + @Parameter(property = "cn1.android.onDeviceDebug.adb") + private String adbPath; + + @Parameter(property = "cn1.android.onDeviceDebug.deviceSerial") + private String deviceSerial; + + @Parameter(property = "cn1.android.onDeviceDebug.wireless") + private String wireless; + + @Parameter(property = "cn1.android.onDeviceDebug.jdwpPort", defaultValue = "5005") + private int jdwpPort; + + @Parameter(property = "cn1.android.onDeviceDebug.apk") + private String apkPath; + + @Parameter(property = "cn1.android.onDeviceDebug.packageName") + private String packageNameOverride; + + @Parameter(property = "cn1.android.onDeviceDebug.mainClass") + private String mainClassOverride; + + @Parameter(property = "cn1.android.onDeviceDebug.waitForAttach", defaultValue = "true") + private boolean waitForAttach; + + @Parameter(property = "cn1.android.onDeviceDebug.skipInstall", defaultValue = "false") + private boolean skipInstall; + + @Override + protected void executeImpl() throws MojoExecutionException, MojoFailureException { + File commonDir = getCN1ProjectDir(); + if (commonDir == null) { + throw new MojoFailureException("Could not locate Codename One project root"); + } + File rootProjectDir = commonDir.getParentFile(); + if (rootProjectDir == null) rootProjectDir = commonDir; + + Properties cn1Settings = readCn1Settings(commonDir); + String packageName = firstNonEmpty(packageNameOverride, + cn1Settings.getProperty("codename1.packageName")); + String mainName = firstNonEmpty(mainClassOverride, + cn1Settings.getProperty("codename1.mainName")); + if (packageName == null || mainName == null) { + throw new MojoFailureException("Could not resolve package / main class from " + + new File(commonDir, "codenameone_settings.properties")); + } + String launcherActivity = packageName + "." + mainName + "Stub"; + + File adb = resolveAdb(); + getLog().info("Using adb: " + adb); + + if (wireless != null && !wireless.isEmpty()) { + getLog().info("adb connect " + wireless); + runAdb(adb, null, "connect", wireless); + } + + String serial = resolveDevice(adb); + getLog().info("Target device: " + serial); + + if (!skipInstall) { + File apk = resolveApk(rootProjectDir); + if (apk != null) { + getLog().info("Installing " + apk.getName() + " on " + serial); + CommandResult install = runAdb(adb, serial, "install", "-r", "-t", apk.getAbsolutePath()); + if (install.exitCode != 0) { + throw new MojoFailureException("adb install failed:\n" + install.stdout); + } + } else { + getLog().warn("No APK found under target/ — skipping install. " + + "Pass -Dcn1.android.onDeviceDebug.apk= or run " + + "'mvn cn1:buildAndroidOnDeviceDebug' first."); + } + } + + if (waitForAttach) { + getLog().info("Marking " + packageName + " as the debug app (waits for debugger)."); + runAdb(adb, serial, "shell", "am", "set-debug-app", "-w", "--persistent", packageName); + } else { + // Clear any stale debug-app flag from a previous run. + runAdb(adb, serial, "shell", "am", "clear-debug-app"); + } + + getLog().info("Launching " + launcherActivity); + CommandResult launch = runAdb(adb, serial, "shell", "am", "start", "-n", + packageName + "/" + launcherActivity); + if (launch.exitCode != 0) { + throw new MojoFailureException("adb am start failed:\n" + launch.stdout); + } + + String pid = waitForPid(adb, serial, packageName); + if (pid == null) { + throw new MojoFailureException("Timed out waiting for process " + packageName + + " on device " + serial + ". Did the app crash on launch?"); + } + getLog().info("App PID on device: " + pid); + + // Map the device JDWP socket onto a local TCP port. + runAdb(adb, serial, "forward", "tcp:" + jdwpPort, "jdwp:" + pid); + + getLog().info(""); + getLog().info("=================================================================="); + getLog().info(" JDWP forwarded: localhost:" + jdwpPort + " -> device pid " + pid); + getLog().info(" Attach IntelliJ: Run -> 'CN1 Attach Android' (Remote JVM Debug)"); + getLog().info(" Or from a shell: jdb -attach localhost:" + jdwpPort); + if (waitForAttach) { + getLog().info(""); + getLog().info(" The app is paused in waitForDebugger() — attach now to resume."); + } + getLog().info("=================================================================="); + getLog().info(""); + + // Stream logcat for the lifetime of the session. Logcat tee'd to our + // own stdout so it lands in the IDE's Run tool window alongside the + // [adb] lines above. --pid filters out unrelated chatter. + ProcessBuilder logcat = new ProcessBuilder( + buildAdbCommand(adb, serial, "logcat", "-v", "threadtime", "--pid=" + pid)); + logcat.redirectErrorStream(true); + Process logcatProc; + try { + logcatProc = logcat.start(); + } catch (IOException e) { + throw new MojoExecutionException("Failed to start adb logcat: " + e.getMessage(), e); + } + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + logcatProc.destroy(); + // Best-effort cleanup so a re-run doesn't trip over a stale forward. + runAdb(adb, serial, "forward", "--remove", "tcp:" + jdwpPort); + } catch (Exception ignored) { + } + })); + + try (BufferedReader r = new BufferedReader( + new InputStreamReader(logcatProc.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = r.readLine()) != null) { + getLog().info("[device] " + line); + } + } catch (IOException e) { + getLog().warn("logcat stream interrupted: " + e.getMessage()); + } + + try { + int rc = logcatProc.waitFor(); + if (rc != 0) { + getLog().warn("logcat exited with code " + rc); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private Properties readCn1Settings(File commonDir) throws MojoFailureException { + File f = new File(commonDir, "codenameone_settings.properties"); + if (!f.isFile()) { + throw new MojoFailureException("Missing " + f); + } + Properties p = new Properties(); + try (FileInputStream in = new FileInputStream(f)) { + p.load(in); + } catch (IOException e) { + throw new MojoFailureException("Failed to read " + f + ": " + e.getMessage()); + } + return p; + } + + private File resolveAdb() throws MojoFailureException { + if (adbPath != null && !adbPath.isEmpty()) { + File f = new File(adbPath); + if (!f.isFile()) throw new MojoFailureException("Configured adb not found: " + f); + return f; + } + List candidates = new ArrayList<>(); + String androidHome = System.getenv("ANDROID_HOME"); + String androidSdkRoot = System.getenv("ANDROID_SDK_ROOT"); + if (androidHome != null) candidates.add(androidHome + "/platform-tools/adb"); + if (androidSdkRoot != null) candidates.add(androidSdkRoot + "/platform-tools/adb"); + String home = System.getProperty("user.home"); + if (home != null) { + // macOS, Linux, and Windows default Android Studio SDK locations. + candidates.add(home + "/Library/Android/sdk/platform-tools/adb"); + candidates.add(home + "/Android/Sdk/platform-tools/adb"); + candidates.add(home + "/AppData/Local/Android/Sdk/platform-tools/adb.exe"); + } + for (String c : candidates) { + File f = new File(c); + if (f.isFile()) return f; + } + // PATH lookup as a last resort. + String pathEnv = System.getenv("PATH"); + if (pathEnv != null) { + String exe = isWindows() ? "adb.exe" : "adb"; + for (String dir : pathEnv.split(File.pathSeparator)) { + File f = new File(dir, exe); + if (f.isFile()) return f; + } + } + throw new MojoFailureException( + "Could not locate the Android adb executable. Install the Android SDK " + + "platform-tools (or Android Studio) and set ANDROID_HOME, or pass " + + "-Dcn1.android.onDeviceDebug.adb=."); + } + + private String resolveDevice(File adb) throws MojoFailureException { + if (deviceSerial != null && !deviceSerial.isEmpty()) return deviceSerial; + CommandResult res = runAdb(adb, null, "devices"); + // adb devices output: header line then `\t` per device. + List online = new ArrayList<>(); + for (String line : res.stdout.split("\\r?\\n")) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("List of devices")) continue; + String[] parts = line.split("\\s+"); + if (parts.length >= 2 && "device".equals(parts[1])) { + online.add(parts[0]); + } + } + if (online.isEmpty()) { + throw new MojoFailureException( + "No Android device is online. Plug a device in with USB debugging enabled, " + + "or pass -Dcn1.android.onDeviceDebug.wireless= to dial a " + + "wireless device first. (adb devices output: " + res.stdout.trim() + ")"); + } + if (online.size() > 1) { + throw new MojoFailureException( + "Multiple devices online (" + String.join(", ", online) + "); pick one with " + + "-Dcn1.android.onDeviceDebug.deviceSerial=."); + } + return online.get(0); + } + + private File resolveApk(File rootProjectDir) { + if (apkPath != null && !apkPath.isEmpty()) { + File f = new File(apkPath); + return f.isFile() ? f : null; + } + // Locally-built Gradle source path (from `cn1:buildAndroidGradleProject`). + // Cloud-built APK lands directly in /target/*.apk. + List roots = new ArrayList<>(); + File androidModule = new File(rootProjectDir, "android"); + if (androidModule.isDirectory()) roots.add(new File(androidModule, "target")); + roots.add(new File(rootProjectDir, "target")); + roots.add(rootProjectDir); + + List hits = new ArrayList<>(); + for (File root : roots) { + if (!root.isDirectory()) continue; + try (Stream walk = Files.walk(root.toPath())) { + walk.filter(p -> { + String n = p.getFileName().toString().toLowerCase(Locale.ROOT); + return n.endsWith(".apk") + && !p.toString().contains("/.gradle/") + && !p.toString().contains("/intermediates/"); + }).forEach(p -> hits.add(p.toFile())); + } catch (IOException ignored) { + } + } + if (hits.isEmpty()) return null; + hits.sort(Comparator.comparingLong(File::lastModified).reversed()); + return hits.get(0); + } + + private String waitForPid(File adb, String serial, String packageName) throws MojoFailureException { + long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(20); + while (System.currentTimeMillis() < deadline) { + CommandResult r = runAdbQuiet(adb, serial, "shell", "pidof", packageName); + String pid = r.stdout.trim(); + // pidof prints space-separated pids when multiple matches exist; take the first. + int sp = pid.indexOf(' '); + if (sp > 0) pid = pid.substring(0, sp); + if (!pid.isEmpty() && pid.chars().allMatch(Character::isDigit)) { + return pid; + } + try { + Thread.sleep(250); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } + return null; + } + + private List buildAdbCommand(File adb, String serial, String... args) { + List cmd = new ArrayList<>(); + cmd.add(adb.getAbsolutePath()); + if (serial != null) { + cmd.add("-s"); + cmd.add(serial); + } + cmd.addAll(Arrays.asList(args)); + return cmd; + } + + private CommandResult runAdb(File adb, String serial, String... args) throws MojoFailureException { + CommandResult r = runAdbQuiet(adb, serial, args); + if (r.exitCode != 0 && getLog().isDebugEnabled()) { + getLog().debug("adb " + String.join(" ", args) + " exited " + r.exitCode + ": " + r.stdout); + } + return r; + } + + private CommandResult runAdbQuiet(File adb, String serial, String... args) throws MojoFailureException { + ProcessBuilder pb = new ProcessBuilder(buildAdbCommand(adb, serial, args)); + pb.redirectErrorStream(true); + try { + Process p = pb.start(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream in = p.getInputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + int rc = p.waitFor(); + return new CommandResult(rc, out.toString(StandardCharsets.UTF_8.name())); + } catch (IOException | InterruptedException e) { + throw new MojoFailureException("adb invocation failed: " + e.getMessage()); + } + } + + private static String firstNonEmpty(String... values) { + for (String v : values) { + if (v != null && !v.isEmpty()) return v; + } + return null; + } + + private static boolean isWindows() { + String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + return os.contains("win"); + } + + private static final class CommandResult { + final int exitCode; + final String stdout; + + CommandResult(int exitCode, String stdout) { + this.exitCode = exitCode; + this.stdout = stdout; + } + } +} diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildAndroidOnDeviceDebugMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildAndroidOnDeviceDebugMojo.java new file mode 100644 index 0000000000..85de0f79f0 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/buildWrappers/BuildAndroidOnDeviceDebugMojo.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + */ +package com.codename1.maven.buildWrappers; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.invoker.*; + +import java.io.File; +import java.util.Collections; +import java.util.Properties; + +/** + * Builds an Android APK with the on-device-debug flag flipped on and + * submits it through the normal Codename One Android build flow. + * + * Pair with {@code mvn cn1:android-on-device-debugging} to install the + * APK on a connected device, forward JDWP, and stream logcat — see + * {@code docs/developer-guide/On-Device-Debugging.asciidoc}. + * + * Forces {@code codename1.arg.android.onDeviceDebug=true} so the + * generated manifest is {@code debuggable="true"} and R8/proguard + * is disabled, regardless of what + * {@code codenameone_settings.properties} contains. + */ +@Mojo(name = "buildAndroidOnDeviceDebug", + requiresDependencyResolution = ResolutionScope.NONE, + requiresDependencyCollection = ResolutionScope.NONE) +public class BuildAndroidOnDeviceDebugMojo extends AbstractMojo { + + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (!project.isExecutionRoot()) { + getLog().info("Skipping execution for non-root project"); + return; + } + InvocationRequest request = new DefaultInvocationRequest(); + request.setPomFile(new File("pom.xml")); + request.setGoals(Collections.singletonList("package")); + + Properties properties = new Properties(); + properties.setProperty("skipTests", "true"); + properties.setProperty("codename1.platform", "android"); + properties.setProperty("codename1.buildTarget", "android-device"); + // Self-contained from the IDE menu: do not depend on what the + // project's codenameone_settings.properties currently has. + properties.setProperty("codename1.arg.android.onDeviceDebug", "true"); + request.setProperties(properties); + + Invoker invoker = new DefaultInvoker(); + try { + InvocationResult result = invoker.execute(request); + if (result.getExitCode() != 0) { + throw new MojoFailureException("Failed to build project with exit code " + result.getExitCode()); + } + } catch (MavenInvocationException e) { + throw new MojoExecutionException("Failed to invoke Maven", e); + } + } +} From d2ed283001ff6b58078220d2c68579343f260738 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 13:56:07 +0300 Subject: [PATCH 2/4] Address CI feedback on Android on-device debugging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../On-Device-Debugging-Android.asciidoc | 14 +++++++------- docs/developer-guide/languagetool-accept.txt | 8 ++++++++ .../maven/AndroidOnDeviceDebuggingMojo.java | 6 ++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/developer-guide/On-Device-Debugging-Android.asciidoc b/docs/developer-guide/On-Device-Debugging-Android.asciidoc index a04442840a..ea933b9c2f 100644 --- a/docs/developer-guide/On-Device-Debugging-Android.asciidoc +++ b/docs/developer-guide/On-Device-Debugging-Android.asciidoc @@ -126,7 +126,7 @@ adb pair 192.168.1.42:37051 adb connect 192.168.1.42:5555 ---- + -The connect port is *not* the pairing port — it is the one shown on +The connect port is *not* the pairing port — it's the one shown on the *Wireless debugging* screen above the *Pair device* button. . Run the debug session as normal. Pass the IP and port through the Mojo if you prefer to do the `adb connect` in one step: @@ -180,8 +180,8 @@ configuration of type `java` with `"request": "attach"`, === Useful command-line flags -The Mojo's defaults match what the archetype's run config does — you -rarely need any of these — but they exist for unusual setups: +The Mojo's defaults match what the archetype's run config does. The +flags below exist for unusual setups: [cols="1,2", options="header"] |=== @@ -307,15 +307,15 @@ Check the *CN1 Attach Android* run config: IntelliJ can resolve your `.java` files. If a class loaded on the device doesn't match the class IntelliJ -thinks is current, breakpoints silently drop. Rebuild and reinstall -(`cn1:buildAndroidOnDeviceDebug` + re-run *CN1 Android On-Device -Debug*). +thinks is current, breakpoints stop firing with no error message. +Rebuild and reinstall (`cn1:buildAndroidOnDeviceDebug` + re-run +*CN1 Android On-Device Debug*). ==== logcat shows the app exiting with `Debug.waitForDebugger`-like noise The system killed the process for taking too long to attach. Set `-Dcn1.android.onDeviceDebug.waitForAttach=false` and trigger the -codepath you want to debug manually after attach — Android's debug-app +code path you want to debug manually after attach — Android's debug-app wait isn't bound to the breakpoint, only to process start. ==== Wireless connection drops mid-session diff --git a/docs/developer-guide/languagetool-accept.txt b/docs/developer-guide/languagetool-accept.txt index 86b6ee1870..d987416484 100644 --- a/docs/developer-guide/languagetool-accept.txt +++ b/docs/developer-guide/languagetool-accept.txt @@ -491,3 +491,11 @@ jdb loopback rethrow rethrows + +# ----------------------------------------------------------------------------- +# Android tooling — names from the Android SDK / platform-tools that the +# English dictionary doesn't recognise. +# ----------------------------------------------------------------------------- +adb +logcat +pidof diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidOnDeviceDebuggingMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidOnDeviceDebuggingMojo.java index 7410a6021e..e0f3d531ca 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidOnDeviceDebuggingMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/AndroidOnDeviceDebuggingMojo.java @@ -192,11 +192,13 @@ protected void executeImpl() throws MojoExecutionException, MojoFailureException } Runtime.getRuntime().addShutdownHook(new Thread(() -> { + logcatProc.destroy(); try { - logcatProc.destroy(); // Best-effort cleanup so a re-run doesn't trip over a stale forward. runAdb(adb, serial, "forward", "--remove", "tcp:" + jdwpPort); - } catch (Exception ignored) { + } catch (MojoFailureException e) { + System.err.println("[adb] failed to remove port forward on shutdown: " + + e.getMessage()); } })); From be203b956985429eb49d1aad8f7c9882d16c8956 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 14:02:19 +0300 Subject: [PATCH 3/4] android.onDeviceDebug: also disable ProGuard and pin to debug-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Advanced-Topics-Under-The-Hood.asciidoc | 2 +- .../codename1/builders/AndroidGradleBuilder.java | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index 81a0846e78..2b536f1601 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -26,7 +26,7 @@ Here is the current list of supported arguments. Build hints change over time, s |true/false defaults to true - indicates whether to include the release version in the build |android.onDeviceDebug -|Boolean true/false defaults to false. When `true`, the generated `AndroidManifest.xml` is marked `android:debuggable="true"` and R8/proguard is disabled so the Dalvik/ART JDWP socket is reachable for the resulting APK. 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. +|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 cannot ship a release-signed APK that is `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. |android.installLocation |Maps to android:installLocation manifest entry defaults to auto. Can also be set to internalOnly or preferExternal. diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java index f19983cb04..6891e7ab95 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java @@ -541,12 +541,19 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc // On-device debugging: when set, mark the APK debuggable so Dalvik/ART exposes // a JDWP socket the cn1:android-on-device-debugging Mojo can forward through adb. - // Forcing debuggable also flips off R8/proguard (debug builds skip shrinking - // anyway, but we may be invoked from the android-device cloud target which - // would otherwise run full optimisation). Release builds are unaffected. + // Forcing debuggable also flips off R8 and ProGuard so symbols, method names, + // and line numbers survive the build — we may be invoked from the android-device + // cloud target which would otherwise run full optimisation. Also force the build + // down to debug-only — Android otherwise produces both a release and a debug + // APK from the same manifest, so without this a stray hint could ship a + // release-signed, debuggable APK. Release builds and projects that don't opt + // in see no change. boolean onDeviceDebug = request.getArg("android.onDeviceDebug", "false").equals("true"); if (onDeviceDebug) { disableR8 = true; + request.putArgument("android.enableProguard", "false"); + request.putArgument("android.release", "false"); + request.putArgument("android.debug", "true"); } if (useGradle8) { getGradleJavaHome(); // will throw build exception if JAVA17_HOME is not set From 81d4f711b8d7128e7001d3f673ece74e5ded7d00 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 14:10:08 +0300 Subject: [PATCH 4/4] docs: contraction fixes in android.onDeviceDebug hint row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc index 2b536f1601..dd97842cd5 100644 --- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc +++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc @@ -26,7 +26,7 @@ Here is the current list of supported arguments. Build hints change over time, s |true/false defaults to true - indicates whether to include the release version in the build |android.onDeviceDebug -|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 cannot ship a release-signed APK that is `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. +|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. |android.installLocation |Maps to android:installLocation manifest entry defaults to auto. Can also be set to internalOnly or preferExternal.