vN^0CcDyTHpQP3u6u5
z6!nzS+)3Mgi9WY>V>`nN&-i+v9&85_^Eb}k*x>%axu;b^@Hr(sanx3s1{wv{{1u}g
zm7C8Qm@aA`jJTa(lq0wOSq5O6Y{f&No*Xn-zdC?Fy<5865cS+<{B{o&m&nSj9_d%(
zvY?J%dvV<@ksW@Oa~pLnnFMCDNMU7mJe$T#EFTd|aiqCa_maNd%d6+?@@gB@&nx!Y
zJ$Ac26r0LAxSg$g%bm6%*=+vftTv0Gx73KfOq}DtC~AJ7s24l03Zb#4-|2u68IKHT(p;Ew~p79j5v^`~WU+cPlzw9jIu^II{zq$O8{+
z;BlC#Nd0)Vaa_dP^q)E(TdzpnX8PU(##=Fb#4iUp6+gR$nz+R+|N7Ei>JEVAQia
zU5H`<;>SHx3>Cc{KJQlKtp`cNK$~O{qO&L=N}P5+U5S9q4C{)_c81}*j6SN2L@qkm
z$QL6_wp9EC#E=!wEcAue<&r_Xl9e&YES!-(@v+8t86&5LeI%GqY3P2gAlklgo3J(k
zAocA7*Jnp3
z5xJk-iF|rZjf_bj4_pr>&<@fQeO<&1-G?<{G==&(_G5YY54p)=Z$YJ%A>JaCQn{?PYN(2TyNlI}rI-unD8r{*_ooaSO3U{!cw;eJDZ4I|21i5K)Ih!9V8lfp8CSkAVJ
zXwPrE$SnKSR~)ZP!K0-gKNZCB8LPj$lU!$BFD%MjG|HjHB*8#k$Q%wUh5^xWBii^Q
z@uZca4Q%6?_>!t(@de95BrM2W
zvLn^^@kv3--hb-|SQ*|H`C6_x#zlV#?C2!VVcDtcs`yXZaL;0_qF71FQWbf$dYZf
literal 0
HcmV?d00001
diff --git a/android/icon/mipmap-xxxhdpi/ic_launcher.png b/android/icon/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000000000000000000000000000000000..4848f02f7bd131cdaa6195fb860288a182d3aee1
GIT binary patch
literal 4202
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
literal 0
HcmV?d00001
diff --git a/android/scripts/build.sh b/android/scripts/build.sh
new file mode 100755
index 0000000..c53081e
--- /dev/null
+++ b/android/scripts/build.sh
@@ -0,0 +1,76 @@
+#!/usr/bin/env bash
+# One-command Android build: cross-compiles the Rust cdylib and assembles the APK.
+#
+# ./android/scripts/build.sh # debug APK (faster)
+# ./android/scripts/build.sh release # release APK
+#
+# Prereqs (one-time):
+# rustup target add aarch64-linux-android
+# cargo install cargo-ndk --locked
+# Android SDK + an NDK + CMake (Android Studio installs these).
+#
+# Auto-detects the SDK and the newest installed NDK; override with
+# ANDROID_SDK_ROOT / ANDROID_NDK_HOME.
+set -euo pipefail
+
+profile="${1:-debug}"
+abi="arm64-v8a"
+api="24"
+here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # android/
+repo="$(cd "$here/.." && pwd)"
+
+# --- locate SDK + NDK ---------------------------------------------------------
+sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}"
+[ -d "$sdk" ] || { echo "Android SDK not found (set ANDROID_SDK_ROOT)"; exit 1; }
+export ANDROID_SDK_ROOT="$sdk"
+
+ndk="${ANDROID_NDK_HOME:-${ANDROID_NDK:-}}"
+if [ -z "$ndk" ]; then
+ ndk="$(ls -d "$sdk"/ndk/* 2>/dev/null | sort -V | tail -1 || true)"
+fi
+[ -n "$ndk" ] && [ -d "$ndk" ] || { echo "NDK not found (set ANDROID_NDK_HOME)"; exit 1; }
+export ANDROID_NDK_HOME="$ndk"
+echo "SDK: $sdk"
+echo "NDK: $ndk"
+
+# --- toolchain checks ---------------------------------------------------------
+rustup target list --installed | grep -q aarch64-linux-android \
+ || { echo "run: rustup target add aarch64-linux-android"; exit 1; }
+command -v cargo-ndk >/dev/null \
+ || { echo "run: cargo install cargo-ndk --locked"; exit 1; }
+
+# --- SDL libs + glue (only if missing) ----------------------------------------
+if [ ! -f "$here/app/src/main/jniLibs/$abi/libSDL2.so" ]; then
+ echo "==> building libSDL2.so + syncing SDL glue"
+ bash "$here/scripts/sync-sdl.sh"
+fi
+
+# --- link workaround ----------------------------------------------------------
+# NDK r23+ dropped libgcc, but rustc's aarch64-linux-android target spec still
+# emits `-lgcc`. Redirect it to libunwind; the `-L` for this stub and for the
+# bundled libSDL2.so live in .cargo/config.toml.
+mkdir -p "$repo/target/ndk-libgcc-stub"
+echo "INPUT(-lunwind)" > "$repo/target/ndk-libgcc-stub/libgcc.a"
+
+# --- build cdylib -------------------------------------------------------------
+echo "==> cross-compiling libretsend.so ($profile)"
+cd "$repo"
+ndk_flags=(-t "$abi" -P "$api" -o android/app/src/main/jniLibs build)
+[ "$profile" = "release" ] && ndk_flags+=(--release)
+cargo ndk "${ndk_flags[@]}"
+
+# --- assemble APK -------------------------------------------------------------
+echo "==> assembling APK ($profile)"
+cd "$here"
+# Both build types sign with this one keystore (see app/build.gradle) so
+# reinstalls update in place instead of failing on a signature mismatch.
+[ -f app/debug.keystore ] || keytool -genkeypair -v -keystore app/debug.keystore \
+ -storepass android -keypass android -alias androiddebugkey \
+ -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=retsend,O=retsend,C=US"
+if [ "$profile" = "release" ]; then
+ ./gradlew --no-daemon assembleRelease
+ echo "APK: android/app/build/outputs/apk/release/app-release.apk"
+else
+ ./gradlew --no-daemon assembleDebug
+ echo "APK: android/app/build/outputs/apk/debug/app-debug.apk"
+fi
diff --git a/android/scripts/sync-sdl.sh b/android/scripts/sync-sdl.sh
new file mode 100755
index 0000000..1771a52
--- /dev/null
+++ b/android/scripts/sync-sdl.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+# Pull the SDL pieces that must match the linked sdl2-sys version out of the
+# Cargo registry into this Gradle project, and build the matching libSDL2.so:
+#
+# 1. Java glue -> app/src/main/java/org/libsdl/app/*.java
+# 2. Gradle wrapper jar + gradlew (so ./gradlew works without a system Gradle)
+# 3. libSDL2.so, built from that same SDL source for arm64-v8a
+# 4. libc++_shared.so, from the NDK sysroot
+# 5. Launcher icons, into the generated res/mipmap-* dirs
+#
+# Run `cargo fetch` first so the sdl2-sys source is present. Requires
+# ANDROID_NDK_HOME (or ANDROID_NDK) and cmake on PATH.
+set -euo pipefail
+
+here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # android/
+repo="$(cd "$here/.." && pwd)"
+abi="arm64-v8a"
+api="24"
+
+ndk="${ANDROID_NDK_HOME:-${ANDROID_NDK:-}}"
+[ -n "$ndk" ] || { echo "set ANDROID_NDK_HOME (or ANDROID_NDK)"; exit 1; }
+
+# SDL 2.26 declares cmake_minimum_required 3.0.0, which CMake 4.x rejects.
+# Prefer an Android-SDK-bundled CMake (3.x) over a too-new system one.
+cmake_bin="${CMAKE:-}"
+if [ -z "$cmake_bin" ]; then
+ sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Android/Sdk}}"
+ bundled="$(ls -d "$sdk"/cmake/*/bin/cmake 2>/dev/null | sort -V | tail -1 || true)"
+ cmake_bin="${bundled:-cmake}"
+fi
+echo "using cmake: $cmake_bin ($("$cmake_bin" --version | head -1))"
+
+cargo_home="${CARGO_HOME:-$HOME/.cargo}"
+sdl_crate="$(find "$cargo_home/registry/src" -maxdepth 2 -type d -name 'sdl2-sys-*' | sort -V | tail -1)"
+[ -n "$sdl_crate" ] || { echo "sdl2-sys not found; run 'cargo fetch' first"; exit 1; }
+sdl_src="$sdl_crate/SDL"
+sdl_proj="$sdl_src/android-project"
+echo "using SDL from: $sdl_crate"
+
+# 1. Java glue
+glue_dst="$here/app/src/main/java/org/libsdl/app"
+mkdir -p "$glue_dst"
+cp "$sdl_proj"/app/src/main/java/org/libsdl/app/*.java "$glue_dst/"
+
+# 2. Gradle wrapper (the jar is forward-compatible; gradle-wrapper.properties pins 8.7)
+mkdir -p "$here/gradle/wrapper"
+cp "$sdl_proj/gradle/wrapper/gradle-wrapper.jar" "$here/gradle/wrapper/"
+cp "$sdl_proj/gradlew" "$sdl_proj/gradlew.bat" "$here/"
+chmod +x "$here/gradlew"
+
+# 3. libSDL2.so
+jnilibs="$here/app/src/main/jniLibs/$abi"
+mkdir -p "$jnilibs"
+build="$repo/target/sdl-android-$abi"
+"$cmake_bin" -S "$sdl_src" -B "$build" \
+ -DCMAKE_TOOLCHAIN_FILE="$ndk/build/cmake/android.toolchain.cmake" \
+ -DANDROID_ABI="$abi" -DANDROID_PLATFORM="android-$api" \
+ -DSDL_STATIC=OFF -DSDL_SHARED=ON \
+ -DSDL_SENSOR=OFF >/dev/null # SDL 2.26's Android sensor calls ALooper_pollAll,
+ # gone from the NDK 27 headers and unused here.
+"$cmake_bin" --build "$build" --target SDL2 -j"$(nproc 2>/dev/null || echo 4)" >/dev/null
+cp "$build"/libSDL2.so "$jnilibs/"
+
+# 4. libc++_shared.so
+host="linux-x86_64"; [ "$(uname)" = "Darwin" ] && host="darwin-x86_64"
+cp "$ndk/toolchains/llvm/prebuilt/$host/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so" \
+ "$jnilibs/"
+
+# 5. Icons. res/mipmap-* is generated, so lay down SDL's placeholder for every
+# density first (keeping @mipmap/ic_launcher resolvable) and overlay ours.
+for dir in "$sdl_proj"/app/src/main/res/mipmap-*; do
+ [ -d "$dir" ] || continue
+ dst="$here/app/src/main/res/$(basename "$dir")"
+ mkdir -p "$dst"
+ cp "$dir"/ic_launcher.png "$dst/" 2>/dev/null || true
+done
+for dir in "$here"/icon/mipmap-*; do
+ [ -d "$dir" ] || continue
+ dst="$here/app/src/main/res/$(basename "$dir")"
+ mkdir -p "$dst"
+ cp "$dir"/ic_launcher.png "$dst/"
+done
+
+echo "synced SDL glue + libs into $here"
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..46aa1e3
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "retsend"
+include ':app'
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 05c3ae1..34d53f5 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -325,10 +325,15 @@ impl App {
.osk
.open(OskTarget::PeerAddress, &local_subnet_prefix());
}
- (
- Focus::Tabs,
- AppCommand::Start | AppCommand::Back | AppCommand::TogglePin | AppCommand::Alt,
- ) => {}
+ // Nothing left to leave. Android's Back is a system button that has
+ // to lead somewhere, so there it quits; on the handhelds the
+ // launcher owns quitting and this stays inert.
+ (Focus::Tabs, AppCommand::Back) => {
+ if cfg!(target_os = "android") {
+ self.running = false;
+ }
+ }
+ (Focus::Tabs, AppCommand::Start | AppCommand::TogglePin | AppCommand::Alt) => {}
(_, AppCommand::PickRow(_) | AppCommand::PickKey { .. } | AppCommand::PickTab(_)) => {
unreachable!("taps are routed by their target above, before the focus match")
}
diff --git a/src/config/device.rs b/src/config/device.rs
index e0138db..8a939bf 100644
--- a/src/config/device.rs
+++ b/src/config/device.rs
@@ -18,16 +18,33 @@ impl Default for DeviceConfig {
Self {
alias: default_alias(),
device_model: "Retro Handheld".to_string(),
- device_type: "desktop".to_string(),
+ device_type: default_device_type().to_string(),
}
}
}
-/// Hostname when readable (Linux-only targets), else a recognizable fallback.
+/// `RETSEND_ALIAS` (Android has no hostname; the activity passes the device
+/// model), else the hostname, else a recognizable fallback.
fn default_alias() -> String {
+ if let Ok(alias) = std::env::var("RETSEND_ALIAS") {
+ let alias = alias.trim();
+ if !alias.is_empty() {
+ return alias.to_string();
+ }
+ }
std::fs::read_to_string("/etc/hostname")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "retsend".to_string())
}
+
+/// The protocol has no handheld type, so they ride as desktops; Android is a
+/// mobile.
+fn default_device_type() -> &'static str {
+ if cfg!(target_os = "android") {
+ "mobile"
+ } else {
+ "desktop"
+ }
+}
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 1e5513e..09f8da5 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -20,7 +20,7 @@ pub use device::DeviceConfig;
pub use display::DisplayConfig;
pub use input::InputConfig;
pub use network::NetworkConfig;
-pub use paths::{data_dir, device_scale};
+pub use paths::{data_dir, device_scale, env_browser_roots};
pub use transfer::TransferConfig;
#[derive(Clone, Default, Serialize, Deserialize)]
diff --git a/src/config/paths.rs b/src/config/paths.rs
index 3711c03..ea549a2 100644
--- a/src/config/paths.rs
+++ b/src/config/paths.rs
@@ -46,6 +46,20 @@ pub(super) fn config_path() -> String {
format!("{}config.toml", data_dir())
}
+/// Extra file-browser roots from `RETSEND_BROWSER_ROOTS`, `:`-separated, joining
+/// the detected mount points and `[transfer] browser_roots`. For launchers that
+/// know paths no built-in candidate could name — the Android activity passes the
+/// storage volumes it can reach, which are per-install.
+pub fn env_browser_roots() -> Vec {
+ std::env::var("RETSEND_BROWSER_ROOTS")
+ .unwrap_or_default()
+ .split(':')
+ .map(str::trim)
+ .filter(|s| !s.is_empty())
+ .map(str::to_string)
+ .collect()
+}
+
/// Default directory for received files: `RETSEND_SAVE_DIR` (the PortMaster
/// launcher points it at the device's ROMs root), else `~/Downloads` when it
/// exists, else `received/` inside [`data_dir`].
diff --git a/src/event/keyboard.rs b/src/event/keyboard.rs
index 8eb7260..d46c213 100644
--- a/src/event/keyboard.rs
+++ b/src/event/keyboard.rs
@@ -58,7 +58,9 @@ pub fn on_key_down(keymap: Keymap, kc: Keycode, repeat: bool, commands: &mut Vec
fn desktop(kc: Keycode) -> Option {
Some(match kc {
Keycode::Return | Keycode::KpEnter => AppCommand::Confirm,
- Keycode::Escape => AppCommand::Back,
+ // AcBack is Android's hardware/gesture Back, trapped into a key event by
+ // the hint `run_app` sets there.
+ Keycode::Escape | Keycode::AcBack => AppCommand::Back,
// Both the pad's label and the key a desktop hand reaches for.
Keycode::X | Keycode::Backspace => AppCommand::Alt,
Keycode::F1 => AppCommand::Start,
diff --git a/src/lib.rs b/src/lib.rs
index 693f0b7..bf0867f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,8 +11,8 @@ mod ui;
use crate::app::App;
-/// Shared startup for the desktop binary (and later the handheld launcher —
-/// same binary, different env). Mirrors retsurf's `run_app`.
+/// Shared startup for the desktop binary, the handheld launcher (same binary,
+/// different env), and Android's `SDL_main`. Mirrors retsurf's `run_app`.
pub fn run_app() {
// Capture panics before anything else can panic. On the handheld the
// launcher usually discards stderr, so a bare panic leaves no trace beyond
@@ -29,6 +29,8 @@ pub fn run_app() {
if let Ok(v) = std::env::var("RETSEND_GLES") {
app_config.display.use_gles = v != "0";
}
+ #[cfg(target_os = "android")]
+ android_startup(&mut app_config);
transfer::files::sweep_stale_parts(std::path::Path::new(&app_config.transfer.save_dir));
@@ -42,6 +44,8 @@ pub fn run_app() {
// On a Wayland desktop SDL still often defaults to x11; align it to
// Wayland. On the handheld (no WAYLAND_DISPLAY) this is skipped and SDL
// falls back to its kmsdrm driver. An explicit SDL_VIDEODRIVER wins.
+ // Android has its own video driver and no WAYLAND_DISPLAY, so skip it.
+ #[cfg(not(target_os = "android"))]
if std::env::var_os("SDL_VIDEODRIVER").is_none()
&& std::env::var_os("WAYLAND_DISPLAY").is_some()
{
@@ -54,6 +58,46 @@ pub fn run_app() {
app.run();
}
+/// SDL's Android shell (`SDLActivity`) `dlopen`s our cdylib and calls this on
+/// its own thread — Android's `src/main.rs`.
+#[cfg(target_os = "android")]
+#[no_mangle]
+pub extern "C" fn SDL_main(
+ _argc: std::os::raw::c_int,
+ _argv: *const *const std::os::raw::c_char,
+) -> std::os::raw::c_int {
+ run_app();
+ 0
+}
+
+/// What only Android decides. Its paths, alias and UI scale arrive as the same
+/// `RETSEND_*` env vars the handheld launchers set, written by `RetsendActivity`
+/// before SDL starts.
+#[cfg(target_os = "android")]
+fn android_startup(config: &mut config::AppConfig) {
+ // Mali/Adreno/PowerVR expose only GLES, so desktop GL is never an option.
+ config.display.use_gles = true;
+
+ // egui-sdl2 synthesizes a pointer stream from finger events itself; SDL's own
+ // synthesis would deliver every tap twice.
+ std::env::set_var("SDL_TOUCH_MOUSE_EVENTS", "0");
+
+ // Untrapped, Back backgrounds the activity and the app never sees it. Trapped,
+ // it arrives as an AC_BACK key — the only way out of a screen without a pad.
+ std::env::set_var("SDL_ANDROID_TRAP_BACK_BUTTON", "1");
+}
+
+#[cfg(target_os = "android")]
+fn init_logging() {
+ // No stderr on Android; route `log` to logcat (`adb logcat -s retsend`).
+ android_logger::init_once(
+ android_logger::Config::default()
+ .with_max_level(log::LevelFilter::Info)
+ .with_tag("retsend"),
+ );
+}
+
+#[cfg(not(target_os = "android"))]
fn init_logging() {
let env = env_logger::Env::default()
.filter_or("RETSEND_LOG_LEVEL", "info")
diff --git a/src/net/mod.rs b/src/net/mod.rs
index 44c6e13..6c7daa7 100644
--- a/src/net/mod.rs
+++ b/src/net/mod.rs
@@ -244,9 +244,17 @@ pub fn sample_status() -> NetStatus {
}
}
+/// Android has neither wireless tool, and the framework's SSID needs the
+/// location permission — the Receive screen shows the address alone.
+#[cfg(target_os = "android")]
+pub fn wifi_ssid() -> Option {
+ None
+}
+
/// Best-effort connected Wi-Fi SSID. Tries `iwgetid -r`, then `iw dev
/// link` for each wireless interface under `/sys/class/net`. None if no
/// wireless link is up or neither tool exists (e.g. desktop on ethernet).
+#[cfg(not(target_os = "android"))]
pub fn wifi_ssid() -> Option {
if let Some(out) = run_stdout("iwgetid", &["-r"]) {
let ssid = out.trim();
@@ -272,6 +280,7 @@ pub fn wifi_ssid() -> Option {
/// Run a command, returning its stdout on a zero exit (None if it can't spawn
/// or fails). Used only for the optional Wi-Fi probes above.
+#[cfg(not(target_os = "android"))]
fn run_stdout(cmd: &str, args: &[&str]) -> Option {
let out = std::process::Command::new(cmd).args(args).output().ok()?;
out.status
@@ -281,6 +290,7 @@ fn run_stdout(cmd: &str, args: &[&str]) -> Option {
/// Wireless interface names: `/sys/class/net` entries exposing a `wireless`
/// subdirectory.
+#[cfg(not(target_os = "android"))]
fn wireless_interfaces() -> Vec {
let Ok(entries) = std::fs::read_dir("/sys/class/net") else {
return Vec::new();
diff --git a/src/overlay/browser.rs b/src/overlay/browser.rs
index 8d0b18f..dfd9751 100644
--- a/src/overlay/browser.rs
+++ b/src/overlay/browser.rs
@@ -396,7 +396,11 @@ fn build_roots(extra: &[String]) -> Vec {
.map(PathBuf::from)
.filter(|p| p.is_dir())
.collect();
- for path in extra {
+ for path in extra
+ .iter()
+ .cloned()
+ .chain(crate::config::env_browser_roots())
+ {
let path = PathBuf::from(path);
if path.is_dir() && !roots.contains(&path) {
roots.push(path);