Two strands land together: the window draws pictures now, and the whole front end builds, installs and runs on the phone with the engine linked in-process. Images (the "make a picture actually show up" part) - Engine, additively: cst_agent_hooks_t.on_tool_images(call, list, n, ud) fires when a call left attachments behind, and cst_agent_pending_rel() / cst_agent_pending_is_image() read that list without the app touching the store. tests/test_attach.c::test_agent_pending pins the three cases (an image, a stored name that passes cst_attach_rel_ok, out-of-range NULL). - App: image://cstattach/<stored name> (app/src/ui/SessionImages.cpp) serves only the current session's attachment directory, checked with cst_attach_rel_ok, decodes down to the requested sourceSize and never upscales, and holds a mutex because requestImage() runs on the render thread while the directory can change underneath it. AgentCore gained attachmentDir(), pendingImages(), the toolImages()/promptImages() signals and hookToolImages(); history() gives replayed rows their images too. - QML: ImageStrip.qml (a thumb strip, <=150 px tall, shaped by the picture, tooltip with name/size/origin, the file's name when it went missing), wired into MessageCard and ToolCard. Qt 6's ListModel cannot hold a list-valued role (append() reads back undefined, setProperty drops it), so a row carries one image per line as <stored>|<name>|<origin>|<w>x<h> and ImageStrip decodes that. Android (the engine is already in the APK; this is the front end) - app/src/main.cpp: on Android the workspace, config, tmp and HOME go under the app's own data directory before the engine opens anything, because QDir::currentPath() is "/" there and /tmp is not writable. - app/android/: the manifest (QtApplication/QtActivity, INTERNET and POST_NOTIFICATIONS, adjustResize, no storage permission -- the picker will be the system one), strings, a vector icon; minSdk 24 and targetSdk 35 come from app/CMakeLists.txt through QT_ANDROID_MIN/TARGET_SDK_VERSION. - app/src/platform/android/AndroidTools.cpp: the Android file behind PlatformInit(), owner "app:android" (notification, clipboard, share sheet, file picker are stubs for now, so the tool list does not lie about them). - app/tools/selftest.c decides its scratch workspace with os_temp_dir(), so the C smoke test runs on the device too. - scripts/build_apk_android.sh: sources to installed APK in one command (environment checks, qt-cmake configure, --target apk, adb install), and the host-side plumbing the NixOS + qt.io combination needs -- the NDK sysroot include dirs for Qt's FindGLESv2/FindEGL (they only look under <sysroot>/include), -L$PREFIX/lib on the shared/module link lines as well as the executables', the Android debug keystore created on first use with QT_ANDROID_SIGN_APK + QT_ANDROID_KEYSTORE_* (androiddeployqt copies the unsigned gradle output and signs it itself), and the #!/bin/bash shebang of the build-tools wrappers, which NixOS does not have. - scripts/gradle-android-init.gradle (installed into ~/.gradle/init.d/): the release lint task is switched off -- 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. - Icons: app/icons/*.svg (Breeze's 24@3x files, the 19 names the window asks for) are compiled in as :/cst-icons and ThemeIcons.cpp falls back to them when the platform has no icon theme. On the phone the Qt theme lookup found nothing and the QML glyph fallback was an empty box in the phone's fonts. - docs/android.md: "The APK (Qt front end)" -- the SDK assembled without sdkmanager, the host Qt's missing libraries on NixOS, the Gradle mirrors, lint, signing -- and a Status that says what runs where. app/README.md gains the images and APK lines, and stops claiming Android platform tools that are still stubs. Verified: engine `make check` green; desktop `cst-app-selftest: OK` and --uitest all checks pass; phone (NX789J, Android 16, arm64-v8a) -- the APK installs, starts on the workspace under /data/user/0/xyz.whcself.cstagent/ files/, a turn reaches a configured provider over HTTPS from inside the ART process and renders the reply, rotation keeps the session and docks the sidebar, and the toolbar/card icons draw. Version 0.9.0. mode = coding
168 lines
8.5 KiB
Markdown
168 lines
8.5 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 (with no API key the provider answers
|
|
401 -- the engine, curl and TLS parts of the round trip are what that
|
|
shows). Rotation keeps the session; the sidebar appears in landscape.
|
|
* Still to come: the Android platform tools behind `app:android`
|
|
(`notification_send`, clipboard, share sheet, SAF file picker), 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.
|