Skip to content

Latest commit

 

History

History
138 lines (108 loc) · 6.19 KB

File metadata and controls

138 lines (108 loc) · 6.19 KB

odr-core-android — the AAR

The JNI bindings (../jni) packaged for android: the java API, the native library for every ABI, and the runtime data the renderer needs, in one artifact.

odr-core-android.aar
├── classes.jar                        app.opendocument.core (../jni/java) + OdrAndroid
├── jni/<abi>/libodr_jni.so            the bindings with the core linked in
├── jni/<abi>/libc++_shared.so         the c++ runtime they were built against
└── proguard.txt                       keeps the classes JNI resolves by name

ABIs: arm64-v8a, armeabi-v7a, x86, x86_64. minSdk 26.

Using it

repositories {
    mavenCentral()
}

dependencies {
    implementation "app.opendocument:odr-core-android:<version>"
}

The same artifact is also on GitHub Packages, but prefer Central: GitHub Packages needs a token with read:packages even for public packages — see Publishing for what that rules out. Do not depend on odr-core-java as well: the AAR carries the same classes.

import app.opendocument.core.*

val file = Odr.open(path)
val service = Html.translate(file, cacheDir.path, HtmlConfig())
val html = service.bringOffline(outputDir.path)

Nothing needs initialising: the renderer's css and js are part of the native library. OdrAndroid.init(context) is a deprecated no-op — it used to unpack the assets that carried them — and stays callable, checked IOException and all, so apps written against the older AAR keep compiling.

Serving the rendered HTML through HttpServer needs two things from the app, neither of which a library may decide on its own: android.permission.INTERNET, and permission for plain HTTP on loopback (android blocks cleartext from API 28 on) — a networkSecurityConfig with a domain-config for 127.0.0.1 is the narrow way to grant it.

HttpServer.listen() blocks, so it runs on a thread of the app's. stop() and close() return only once that thread is back out of it, so tearing the server down needs no join or timeout of its own — and close() on its own is enough, it stops the server before freeing it. A server that outlives one document is best left listening with clear() between workloads: rebinding costs a port.

Building

The AAR needs the native libraries first; build_native.py cross compiles them through conan and cmake and lays them out where the gradle build reads them:

export ANDROID_HOME=~/Library/Android/sdk
./gradlew assembleRelease                     # builds all four ABIs on the way
./gradlew assembleRelease -Podr.abis=x86_64   # just one, for the emulator
./gradlew assembleRelease -Podr.abis=         # none: use what is already there
python build_native.py --abi x86_64           # or drive it directly

Anything the script needs but cannot guess is a gradle property: -Podr.conan=<path> (a conan outside PATH, e.g. in a virtualenv), -Podr.buildProfile=<profile> (the conan profile of this machine), -Podr.python=<path>.

The odrcore build is a normal one — ODR_JNI=ON, static core linked into libodr_jni.so — driven by the android-<arch> conan profiles in .github/config/conan/profiles, which pin the NDK and API 26.

The libraries ship unstripped — 60-72 MB per ABI, most of the AAR — so that a consuming app's ndk.debugSymbolLevel can hand play what it needs to symbolicate a crash inside the core. Devices never see it: play serves APKs built from the stripped copies, so the weight is on maven central and developer builds only.

It takes both build_native.py not stripping and the packaging.jniLibs.keepDebugSymbols rule in build.gradle.kts. Without the second the first is invisible.

Testing

./gradlew lint                            # NewApi against minSdk 26, over ../jni/java too
./gradlew connectedDebugAndroidTest       # on a running emulator or device
./gradlew spotlessApply                   # ktfmt, kotlinlang style

The instrumented suite (src/androidTest) is the part that sees what a device sees: it loads the native library, decodes and renders documents, drives a java log sink from native code, and serves a document over HTTP. Its inputs come from ../jni/testfixtures, the same ones the host junit suite uses.

CI (.github/workflows/android.yml) cross compiles each ABI, assembles and lints the AAR, and runs the suite on API 26 — the floor OpenDocument.droid ships to — and on a current API level.

Publishing

Releases go to Maven Central and GitHub Packages, and only once the instrumented suite has passed on every API level — an AAR that assembles and lints is no evidence that it works on a device.

Maven Central is the one that matters. GitHub Packages demands read:packages even to read a public artifact, which rules out the consumer this is ultimately built for: f-droid builds from source with no credentials at all. It stays published there only because the maven jar is, and dropping it would break whoever already reads it.

app.opendocument is an established namespace on Central — pdf2htmlex-android and wvware-android have been there since 2024 — so this is a new artifact under an old name, not a new namespace. Central asks for more than GitHub Packages does, and the module supplies it: sources and javadoc jars, a POM carrying developers, and a PGP signature over every file. Signing is conditional on a key being configured, so publishToMavenLocal and the GitHub Packages publish still work without one; an unsigned upload is rejected by the portal, so the release path stays guarded either way.

Publishing uploads a deployment to the portal and stops. Releasing it is a deliberate click in the portal UI, because Central never forgets a version — that click is the last point at which a bad artifact can be dropped rather than lived with.

For now OpenDocument.droid keeps building odrcore from the conan package (with_jni=True), deploying libodr_jni.so and odr-core-java.jar out of it. That path is unaffected by anything here, and it is the reason the two halves cannot drift: they come out of one build.

So the AAR is, for now, a second packaging of that same build — for consumers that just want a dependency, and the reason android gets compiled and exercised on every push at all.