Skip to content

build: check API surface for Android suitability - #83

Open
soloturn wants to merge 2 commits into
preview/v4.x-SNAPSHOTfrom
android-suitability
Open

build: check API surface for Android suitability#83
soloturn wants to merge 2 commits into
preview/v4.x-SNAPSHOTfrom
android-suitability

Conversation

@soloturn

@soloturn soloturn commented Aug 23, 2026

Copy link
Copy Markdown

Same Android-suitability strategy as gestalt's gestalt-library-common.gradle.kts: applies the AnimalSniffer plugin plus the gummy-bears-api-24 signature set to every subproject, checking NUI's API surface against what's actually available on Android.

Why this matters concretely: DestinationSol's engine module already depends on nui, nui-libgdx, nui-gestalt, and nui-reflect directly, and DestinationSol targets Android via libGDX — so NUI's API surface needs to actually work there.

  • Root build.gradle: buildscript classpath for the Android Gradle Plugin and the AnimalSniffer Gradle plugin itself. common.gradle is a plain "apply from" script, so it can't declare plugins via a plugins {} block the way gestalt's precompiled build-logic convention plugin can — the classic apply plugin: 'ru.vyarus.animalsniffer' form needs the classpath supplied from the root project instead.
  • gradle/common.gradle: applies the plugin, adds the signature dependency.

Unlike gestalt, NUI wasn't Android-clean — the check initially surfaced 21 real violations across two modules:

  • ReflectionUtil (6) and ObjectLayoutBuilder (2): genuinely fixable by calling through the concrete Method/Constructor type instead of the unavailable Executable/Parameter types — no public API change, no behavior change.
  • UIText's clipboard handling (13): java.awt.Toolkit/Clipboard/DataFlavor/StringSelection genuinely don't exist on Android at all — not fixable by calling a different method. Isolated into a private nested class with LinkageError caught by the caller, so copy/paste degrades to a no-op with a logged warning instead of crashing. animalsniffer.ignore excludes java.awt.Toolkit/java.awt.datatransfer.* explicitly for this one real, permanent, documented exception.

animalsniffer.ignoreFailures is gone — the check is fully fatal, zero unignored violations.

Beyond the API-signature check: also adds a minimal, non-published com.android.application module (nui-android-verify, only included when a local Android SDK is configured) that actually depends on nui/nui-libgdx and references real classes from both — because the AnimalSniffer check only proves "no referenced java.* symbol is missing from Android," not that the compiled classes actually survive D8 desugaring/dexing/APK assembly. Running it found one more real, previously invisible problem: Android's checkDuplicateClasses task (Gradle's own resolution has no equivalent — it picks one version per coordinate, not per class) failed on two pre-existing conflicts in the transitive dependency graph:

  • com.google.code.findbugs:jsr305 vs com.google.code.findbugs:annotations (both declare javax.annotation.*) — excluded in nui-android-verify; any real Android app depending on nui-libgdx would hit the same conflict and need the same exclusion.
  • org.reflections:reflections (declared directly by nui/nui-reflect/nui-gestalt5/nui-gestalt7) vs the org.terasology:reflections fork gestalt-module already pulls in transitively — fixed at the source: all four now consistently depend on the fork. It's API-identical to upstream (confirmed by diffing its one commit against ronmamo/reflections — it only drops the javax.servlet/slf4j-simple optional deps upstream still carries), so this is a zero-behavior-change fix. Upgrading to current upstream 0.10.2 instead was considered and rejected — it removes Store.getAll() and changes SubTypesScanner's default java.lang.Object-exclusion behavior, which TypeRegistry explicitly relies on (new SubTypesScanner(false)); not a safe mechanical swap.

Test plan:

  • ./gradlew build (including tests) succeeds with the AnimalSniffer check active and fully fatal
  • Zero unignored AnimalSniffer violations (down from 21)
  • ./gradlew :nui-android-verify:assembleDebug succeeds end-to-end — real unsigned debug APK produced
  • Confirmed via dexdump/string search that UIText and LibGDXKeyboardDevice are genuinely present in the resulting classes*.dex, not just declared as an unused dependency
  • Confirmed the default ./gradlew build (no local.properties) is unaffected

Stacked on #82 (Gradle 9.7.1 + Java 17 bump) — AGP 9.3.1 doesn't work under the old 8.2.1 wrapper.

Partially addresses #84 — the real working clipboard for non-AWT platforms is #85, stacked on top of this one.

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 583eb3e1-33e4-424d-87c7-5bd037ad9d75

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@soloturn
soloturn force-pushed the android-suitability branch from 2ac1b3a to a64955a Compare August 23, 2026 09:59
@soloturn
soloturn force-pushed the android-suitability branch from a64955a to 2f339b1 Compare August 23, 2026 10:17
soloturn added a commit that referenced this pull request Aug 23, 2026
Closes #84.

UIText's clipboard access was hardwired to java.awt, which #83 could only
make crash-safe (LinkageError caught, degrades to a no-op) on platforms
without it - not actually give a working clipboard there. This makes the
implementation swappable instead:

- New ClipboardProvider interface in nui-input's org.terasology.input.device
  package, alongside the existing KeyboardDevice/MouseDevice device
  abstractions this mirrors.
- AwtClipboardProvider (nui, org.terasology.nui.util) - the extracted,
  unchanged java.awt implementation, now the default rather than hardcoded.
- UIText.setClipboardProvider(ClipboardProvider) lets an application swap
  the default at startup; getClipboardContents()/setClipboardContents()
  delegate to whatever's currently set, still catching LinkageError as a
  defensive fallback regardless of which implementation is active.
- LibGDXClipboardProvider (nui-libgdx) wraps libGDX's own cross-platform
  Gdx.app.getClipboard() - backed by java.awt on desktop, Android's
  ClipboardManager on Android, and the browser clipboard on GWT/HTML, so it
  actually works everywhere nui-libgdx runs (which is everywhere
  DestinationSol runs). Applications call
  UIText.setClipboardProvider(new LibGDXClipboardProvider()) during startup
  to opt in.

No behavior change for existing desktop consumers that don't call
setClipboardProvider - AwtClipboardProvider is still the default.

Verified: ./gradlew build succeeds, zero unignored AnimalSniffer violations.
soloturn added a commit that referenced this pull request Aug 23, 2026
Closes #84.

UIText's clipboard access was hardwired to java.awt, which #83 could only
make crash-safe (LinkageError caught, degrades to a no-op) on platforms
without it - not actually give a working clipboard there. This makes the
implementation swappable instead:

- New ClipboardProvider interface in nui-input's org.terasology.input.device
  package, alongside the existing KeyboardDevice/MouseDevice device
  abstractions this mirrors.
- AwtClipboardProvider (nui, org.terasology.nui.util) - the extracted,
  unchanged java.awt implementation, now the default rather than hardcoded.
- UIText.setClipboardProvider(ClipboardProvider) lets an application swap
  the default at startup; getClipboardContents()/setClipboardContents()
  delegate to whatever's currently set, still catching LinkageError as a
  defensive fallback regardless of which implementation is active.
- LibGDXClipboardProvider (nui-libgdx) wraps libGDX's own cross-platform
  Gdx.app.getClipboard() - backed by java.awt on desktop, Android's
  ClipboardManager on Android, and the browser clipboard on GWT/HTML, so it
  actually works everywhere nui-libgdx runs (which is everywhere
  DestinationSol runs). Applications call
  UIText.setClipboardProvider(new LibGDXClipboardProvider()) during startup
  to opt in.

No behavior change for existing desktop consumers that don't call
setClipboardProvider - AwtClipboardProvider is still the default.

Verified: ./gradlew build succeeds, zero unignored AnimalSniffer violations.
soloturn added a commit that referenced this pull request Aug 23, 2026
Closes #84.

UIText's clipboard access was hardwired to java.awt, which #83 could only
make crash-safe (LinkageError caught, degrades to a no-op) on platforms
without it - not actually give a working clipboard there. This makes the
implementation swappable instead:

- New ClipboardProvider interface in nui-input's org.terasology.input.device
  package, alongside the existing KeyboardDevice/MouseDevice device
  abstractions this mirrors.
- AwtClipboardProvider (nui, org.terasology.nui.util) - the extracted,
  unchanged java.awt implementation, now the default rather than hardcoded.
- UIText.setClipboardProvider(ClipboardProvider) lets an application swap
  the default at startup; getClipboardContents()/setClipboardContents()
  delegate to whatever's currently set, still catching LinkageError as a
  defensive fallback regardless of which implementation is active.
- LibGDXClipboardProvider (nui-libgdx) wraps libGDX's own cross-platform
  Gdx.app.getClipboard() - backed by java.awt on desktop, Android's
  ClipboardManager on Android, and the browser clipboard on GWT/HTML, so it
  actually works everywhere nui-libgdx runs (which is everywhere
  DestinationSol runs). Applications call
  UIText.setClipboardProvider(new LibGDXClipboardProvider()) during startup
  to opt in.

No behavior change for existing desktop consumers that don't call
setClipboardProvider - AwtClipboardProvider is still the default.

Verified: ./gradlew build succeeds, zero unignored AnimalSniffer violations.
@BenjaminAmos
BenjaminAmos changed the base branch from gradle-9.7.1 to preview/v4.x-SNAPSHOT August 24, 2026 10:52
@BenjaminAmos

Copy link
Copy Markdown
Contributor

Since NUI does not explicitly target Android, even though it is used on that platform by Destination Sol, I am not sure if this is an appropriate change. Depending on the Android Gradle Plugin for validation seems excessive and undermines the perspective of NUI as a platform-independent library.

@soloturn
soloturn force-pushed the android-suitability branch from 2d1f50c to 006dfd7 Compare August 25, 2026 16:48
@soloturn

Copy link
Copy Markdown
Author

Since NUI does not explicitly target Android, even though it is used on that platform by Destination Sol, I am not sure if this is an appropriate change. Depending on the Android Gradle Plugin for validation seems excessive and undermines the perspective of NUI as a platform-independent library.

thank you very much for the feedback @BenjaminAmos ! corrected now, plugin only switched on if android sdk installed and desired.

@BenjaminAmos

Copy link
Copy Markdown
Contributor

I would prefer that the nui-android-verify project is removed entirely from these changes. There is no reason to compile NUI against Android - just maintaining API compatibility is enough.

@soloturn
soloturn force-pushed the android-suitability branch from 006dfd7 to f7e546b Compare August 26, 2026 00:22
@soloturn
soloturn force-pushed the android-suitability branch from f7e546b to c7ea812 Compare August 26, 2026 01:06
@soloturn

Copy link
Copy Markdown
Author

@BenjaminAmos agreed, done - dropped nui-android-verify entirely. This is now just the AnimalSniffer API-surface check (via gradle/common.gradle) plus the fixes it flagged (ReflectionUtil/UIText/ObjectLayoutBuilder avoiding pre-API-26 classes and java.awt). No AGP, no google() repo, no real Android compile anywhere in the build.

soloturn added a commit that referenced this pull request Aug 26, 2026
Closes #84.

UIText's clipboard access was hardwired to java.awt, which #83 could only
make crash-safe (LinkageError caught, degrades to a no-op) on platforms
without it - not actually give a working clipboard there. This makes the
implementation swappable instead:

- New ClipboardProvider interface in nui-input's org.terasology.input.device
  package, alongside the existing KeyboardDevice/MouseDevice device
  abstractions this mirrors.
- AwtClipboardProvider (nui, org.terasology.nui.util) - the extracted,
  unchanged java.awt implementation, now the default rather than hardcoded.
- UIText.setClipboardProvider(ClipboardProvider) lets an application swap
  the default at startup; getClipboardContents()/setClipboardContents()
  delegate to whatever's currently set, still catching LinkageError as a
  defensive fallback regardless of which implementation is active.
- LibGDXClipboardProvider (nui-libgdx) wraps libGDX's own cross-platform
  Gdx.app.getClipboard() - backed by java.awt on desktop, Android's
  ClipboardManager on Android, and the browser clipboard on GWT/HTML, so it
  actually works everywhere nui-libgdx runs (which is everywhere
  DestinationSol runs). Applications call
  UIText.setClipboardProvider(new LibGDXClipboardProvider()) during startup
  to opt in.

No behavior change for existing desktop consumers that don't call
setClipboardProvider - AwtClipboardProvider is still the default.

Verified: ./gradlew build succeeds, zero unignored AnimalSniffer violations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants