The tool was the last Android one that never worked on the NX789J: it would fail with "Notification.Builder.build() failed: no exception detail" and the shade stayed empty. The detail was the whole story -- Qt clears a Java exception before C++ gets to look at it, so a build() that throws and a build() that returns null look exactly the same from here, and the only way to catch the difference is to let Java catch it. - app/android/src/xyz/whcself/cstagent/Notifier.java: the builder, the PendingIntent, build() and notify() in one try/catch that returns "" or the throwable's class and message. androiddeployqt copies app/android/ with the rest of the package source and Qt's Gradle template puts its src/ on java.srcDirs, so a class dropped there needs no build-script change (the first one is this; the foreground service of the next step goes there too). - AndroidTools.cpp keeps what it can see from C++ (the context, the channel on API 26+, the small icon looked up by name, the launch intent) and calls Notifier.post(); the result line now carries the Java failure when there is one. The javaErrorText() helper of the previous attempt is gone: reading a pending exception from C++ is exactly what does not work here. - On the device: the tool answers "notification sent", the system holds the record (channel cst-agent, importance 3, the framework small icon, the tap intent back into the app). Earlier attempts in the same session are in the transcript next to it: four failures from the old path, then this one. Docs: docs/android.md gets the Java-helper mechanism (and why the tool has to be wrapped in Java at all) and no longer lists notification_send as missing; app/README.md's platform table and the Android note say where a Java class goes. Version 0.9.3.
193 lines
10 KiB
Markdown
193 lines
10 KiB
Markdown
# CST-agent on Android
|
|
|
|
The engine runs on the phone as a plain bionic binary -- no Termux, no proot,
|
|
no second process for the UI to talk to. It is the same C core the CLI, the
|
|
TUI and the WebUI use, cross-compiled with the NDK; on the phone the Qt front
|
|
end (app/) will link `libcstcore.a` in-process and register its own platform
|
|
tools under the owner `app:android`.
|
|
|
|
Two scripts cover the engine side:
|
|
|
|
| script | what it does |
|
|
| --- | --- |
|
|
| `scripts/build_third_party_android.sh ABI [all\|mbedtls\|curl]` | static mbedTLS + libcurl into `third_party/prefix-android-ABI` |
|
|
| `scripts/build_android.sh [ABI] [--push] [--run] [--run-tests]` | `libcstcore.a`, `cst-agent`, the test binaries; optional push and on-device run |
|
|
|
|
## Toolchain
|
|
|
|
NixOS: nixpkgs carries the NDK (the Android SDK license makes it unfree, hence
|
|
the flag; the packaged NDK has a patched interpreter, so its clang runs here):
|
|
|
|
```sh
|
|
export ANDROID_NDK_HOME=$(NIXPKGS_ALLOW_UNFREE=1 nix build --impure --no-link \
|
|
--print-out-paths 'nixpkgs#androidenv.androidPkgs.ndk-bundle')/libexec/android-sdk/ndk-bundle
|
|
```
|
|
|
|
`adb` comes from `nix-shell -p android-tools`; the builds need cmake, ninja and
|
|
curl. End to end:
|
|
|
|
```sh
|
|
nix-shell -p cmake ninja curl android-tools --run '
|
|
JOBS=16 ./scripts/build_third_party_android.sh arm64-v8a all'
|
|
nix-shell -p cmake ninja curl android-tools --run '
|
|
JOBS=16 ./scripts/build_android.sh arm64-v8a --push --run'
|
|
```
|
|
|
|
`--run` finishes with the two smoke checks on the device (`cst-agent
|
|
--version` and `tests/test_embed`, the in-process embedding contract);
|
|
`--run-tests` builds the whole `tests/` suite, pushes it and runs every binary
|
|
on the phone, printing a pass/fail line per test.
|
|
|
|
## The APK (Qt front end)
|
|
|
|
`scripts/build_apk_android.sh [--install] [--logs] [--clean]` configures with
|
|
the Qt for Android kit, builds the `apk` target (Gradle, no port opened) and
|
|
optionally installs it. Bits it needs, all overridable through the
|
|
environment:
|
|
|
|
| variable | what it points at |
|
|
| --- | --- |
|
|
| `QT_ANDROID` | Qt for Android 6.11.2 (`aqt install-qt all_os android 6.11.2 android_arm64_v8a -O ~/Qt --archives qtbase qtdeclarative qtquickcontrols2`) |
|
|
| `QT_HOST` | the host Qt of the same version (`aqt install-qt linux desktop 6.11.2 linux_gcc_64 -O ~/Qt --archives qtbase qtdeclarative qtsvg`) |
|
|
| `ANDROID_SDK_ROOT` | SDK with `platforms/android-<n>` and `build-tools/<v>` |
|
|
| `ANDROID_NDK_HOME` | the NDK the engine was built with |
|
|
| `JAVA_HOME` | JDK 17+ for Gradle |
|
|
|
|
### The SDK without sdkmanager
|
|
|
|
`nixpkgs`'s full `androidsdk` pulls the emulator and system images (gigabytes);
|
|
the two components actually needed are cheap. The platform comes from the nix
|
|
store (unfree, so the license flag is required) and build-tools from Google's
|
|
own zip, laid out as the SDK `sdkmanager` would:
|
|
|
|
```sh
|
|
PLATFORM=$(NIXPKGS_ALLOW_UNFREE=1 NIXPKGS_ACCEPT_ANDROID_SDK_LICENSE=1 nix build \
|
|
--impure --no-link --print-out-paths --expr \
|
|
'builtins.elemAt (import <nixpkgs> {}).androidenv.androidPkgs.platforms 2') # android-35
|
|
mkdir -p ~/android-sdk/{build-tools,platforms}
|
|
ln -s $PLATFORM/libexec/android-sdk/platforms/android-35 ~/android-sdk/platforms/
|
|
```
|
|
|
|
`androiddeployqt` then picks `build-tools` and `platform-tools` from there. For
|
|
build-tools, `dl.google.com` is the only source: from here it hands out
|
|
~100 KB/s per connection, so the script downloads the zip in 16 ranged
|
|
requests (≈25x faster) and fixes the exec bits Python's `zipfile` drops.
|
|
|
|
### Host Qt on NixOS
|
|
|
|
The host Qt is built for a regular distribution: its tools (`qmlimportscanner`
|
|
first of all) look for `libgssapi_krb5.so.2`, `libglib-2.0.so.0`, `libz.so.1`,
|
|
`libfontconfig`, `libX11`, `libEGL`/`libGL`, `libzstd`, `libbrotlidec`,
|
|
`libdbus-1`, `libfreetype`, `libstdc++`, none of which exist in NixOS's search
|
|
paths. The tools carry `RUNPATH=$ORIGIN/../lib`, so symlinking those libraries
|
|
from the nix store into `<QT_HOST>/lib/` is enough:
|
|
|
|
```sh
|
|
nix build --no-link --print-out-paths nixpkgs#krb5.lib nixpkgs#dbus.lib \
|
|
nixpkgs#libglvnd nixpkgs#fontconfig.lib nixpkgs#freetype nixpkgs#glib.lib \
|
|
nixpkgs#libx11 nixpkgs#libxkbcommon nixpkgs#zlib nixpkgs#zstd \
|
|
nixpkgs#brotli.lib nixpkgs#stdenv.cc.cc.lib # then ln -s <lib> ~/Qt/6.11.2/gcc_64/lib/
|
|
```
|
|
|
|
Two more things the script does on the way:
|
|
|
|
* **GLESv2/EGL**: Qt's `FindGLESv2.cmake` only searches `CMAKE_FIND_ROOT_PATH`
|
|
prefixes (`<sysroot>/include`), but the NDK keeps the headers in
|
|
`<sysroot>/usr/include` -- without `-DGLESv2_INCLUDE_DIR`/`-DEGL_INCLUDE_DIR`
|
|
`find_package(Qt6 Gui)` fails with "dependency GLESv2 could not be found".
|
|
* **`-L$PREFIX/lib` on every link type**: the APK's payload is a *shared
|
|
module*, so `CMAKE_SHARED_LINKER_FLAGS`/`CMAKE_MODULE_LINKER_FLAGS` need the
|
|
prefix too, not just the EXE flags.
|
|
|
|
### Gradle
|
|
|
|
The generated project resolves plugins and dependencies from `google()` and
|
|
`mavenCentral()`, plus a Gradle distribution from `services.gradle.org`. From
|
|
here all three are slow (dl.google.com ~100 KB/s), so two local tweaks:
|
|
|
|
* `~/.gradle/init.d/*.gradle` (machine-local, not in the repo) prepends the
|
|
Aliyun/Tencent mirrors for `pluginManagement`, `dependencyResolutionManagement`
|
|
and every `buildscript`.
|
|
* the wrapper in `<QT_ANDROID>/src/3rdparty/gradle/gradle/wrapper/` points at
|
|
the Tencent Gradle mirror (51 MB/s vs 100 KB/s).
|
|
|
|
`scripts/gradle-android-init.gradle` (installed into `~/.gradle/init.d/` by the
|
|
build script) switches off release lint: `lintVitalAnalyzeRelease` resolves its
|
|
own tool jars straight from `dl.google.com`, bypassing every repository the
|
|
build configures, and fails the build after all the real work is done.
|
|
|
|
### Signing
|
|
|
|
`androiddeployqt` copies the **unsigned** Gradle output and signs it itself
|
|
(`QT_ANDROID_SIGN_APK` + the `QT_ANDROID_KEYSTORE_*` variables; a signed Gradle
|
|
output makes it fail with "Could not copy unsigned APK"). The script exports
|
|
those variables and creates the standard Android debug keystore
|
|
(`~/.android/debug.keystore`, alias `androiddebugkey`) on first use;
|
|
`CST_ANDROID_KEYSTORE` and `CST_ANDROID_KEY_ALIAS`/`_PASS` override it.
|
|
`apksigner` is a bash script with a `#!/bin/bash` shebang, which NixOS does not
|
|
have, so the script rewrites the shebang of the build-tools wrappers.
|
|
|
|
## What is Android-specific
|
|
|
|
* **API level 24** (`API_LEVEL=24`): bionic provides threads, `exec` and
|
|
`/system/etc/security/cacerts`, which is where libcurl is pointed
|
|
(`CURL_CA_PATH`). Bump the level if a newer syscall or CA layout is needed.
|
|
* **No TUI, no WebUI**: `-DCST_BUILD_TUI=OFF -DCST_BUILD_WEBUI=OFF` -- there is
|
|
no ncurses on Android, and the phone front end talks to the engine in-process
|
|
instead of over HTTP. `src/webui_stub.c` compiles in place of the server.
|
|
* **Sandbox**: `src/sandbox_exec.c` only enables the seccomp/Landlock paths
|
|
under `defined(__linux__) && !defined(__ANDROID__)`, so the phone build gets
|
|
the plain exec path -- the same rule the Termux build follows.
|
|
The seccomp supervisor used to compile in and the probe reported "seccomp",
|
|
but on the device a write outside the workspace was not denied
|
|
(`tests/test_sandbox.c:308`), so the phone build now reports no sandbox
|
|
instead of one that does not hold.
|
|
* **sashidown** is built per ABI (`vendor/sashidown/libsashidown-<abi>.a`) with
|
|
the NDK's `aarch64-linux-android24-clang` wrapper: CMake's compiler is plain
|
|
clang and keeps `--target`/`--sysroot` in its own variables, which the
|
|
vendored makefile cannot see. The host archive stays host-built.
|
|
* **Static libcurl** needs `-L$PREFIX/lib` on the link line, because curl's own
|
|
CMake config lists its mbedTLS dependencies as `-l` flags rather than paths.
|
|
|
|
## Status
|
|
|
|
* arm64-v8a, Android 16 (NX789J): `cst-agent` runs, `tests/test_embed` passes.
|
|
* The whole suite runs on the device (`scripts/build_android.sh arm64-v8a
|
|
--run-tests`): 36/36 after the diff test learned to tell toybox's `diff` from
|
|
GNU diffutils.
|
|
* The APK builds, installs and runs: the Qt front end links the engine
|
|
in-process (no port), `main.cpp` puts the workspace, config and tmp under
|
|
`/data/user/0/xyz.whcself.cstagent/files/`, and a turn does a real HTTPS
|
|
request from inside the ART process. The saved provider key is resolved into
|
|
the engine's config on every start (`AgentCore::open()`: saved key, the
|
|
provider's environment variable, `CST_API_KEY`), so the phone answers without
|
|
being asked for the key again. Rotation keeps the session; the sidebar
|
|
appears in landscape.
|
|
* Platform tools behind `app:android` (`app/src/platform/android/AndroidTools.cpp`):
|
|
`notification_send`, `clipboard_get` / `clipboard_set` under the engine's
|
|
own tool names and `share` (`ACTION_SEND` + chooser) all work on the device.
|
|
The file
|
|
card's **Open** builds a `content://` URI through the app's FileProvider
|
|
(`<package>.qtprovider`, Qt's `@xml/qtprovider_paths`), checks
|
|
`resolveActivity()` before starting `ACTION_VIEW` and grants the reader
|
|
permission; **Share** reuses the share sheet.
|
|
* **Java helpers**: a notification built entirely through `QJniObject` posted
|
|
nothing on the NX789J -- `Notification.Builder.build()` came back null and
|
|
the tool could only say "failed: no exception detail", because Qt clears a
|
|
Java exception before C++ gets to look at it. The one step that has to fail
|
|
visibly (a builder, a `PendingIntent`, `build()`, `notify()`) now lives in
|
|
`app/android/src/xyz/whcself/cstagent/Notifier.java`, which catches the
|
|
throwable and hands the text back for the tool result; the device then
|
|
reported `notification sent` and the shade had it. androiddeployqt copies
|
|
`app/android/` with the rest of the package source and Qt's Gradle template
|
|
puts its `src/` on `java.srcDirs`, so **a class dropped in that directory
|
|
needs no build-script change** -- the front service of the next step goes
|
|
there too.
|
|
* A session's transcript replays a delivered file (`deliver_file`) as a card
|
|
with Open and Share -- the copy lives next to the session, marked `for_user`
|
|
so no request ever contains it (see `app/README.md`, "Files the agent
|
|
delivers").
|
|
* Still to come: the SAF picker for attaching a picture from another app, the
|
|
back gesture, and the icon glyphs -- `ThemeIcon` finds no icon files on the
|
|
phone and draws empty boxes where the desktop shows Breeze icons.
|