From ea89c2935ef6bf1b9c4d2feed37f75101e03e726 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Thu, 12 Dec 2024 10:24:26 -0800 Subject: [PATCH] Android support for eframe (#5318) Android support is "almost there". This PR pushes it just a bit further by allowing `eframe` to be used on Android. It works by smuggling the `AndroidApp` required by `winit` through `NativeOptions`. The example isn't great because it doesn't leave space on the display for Android's top status bar or the lower navigation bar. I don't know what to do about that, yet. This is as far as I've managed to get it working. Another problem is that the development environment setup is completely awful for Android unless you happen to already be a full-time Android developer with everything configured on your build host. As a Rustacean, this makes me very sad. I've had some luck moving all of that mess to a container, adapted from https://github.com/SergioRibera/docker-rust-android. It takes care of all of the build dependencies, Android SDK, and the `cargo-apk` patches for bugs that I hit while getting the example to work on my device. (I also had to install an adb driver on my host and downloaded the Android platform-tools to get access to `adb`. An alternative is exposing the USB device to Docker. On Windows hosts, that means [installing `usbipd`](https://learn.microsoft.com/en-us/windows/wsl/connect-usb). A second alternative is using an `mtp` client to upload the APK as a file with USB file transfer enabled, then manually install it through the device's file manager.) I'm not including the docker stuff in this PR, but here are the files and instructions for future reference (and it will probably simplify manual testing and CI, FWIW!)
Dockerfile ```dockerfile FROM rust:1.76.0-slim # Variable arguments ARG JAVA_VERSION=17 ARG NDK_VERSION=25.1.8937393 ARG BUILDTOOLS_VERSION=30.0.0 ARG PLATFORM_VERSION=android-30 ARG CLITOOLS_VERSION=8512546_latest # Install Android requirements RUN apt-get update -yqq && \ apt-get install -y --no-install-recommends \ libcurl4-openssl-dev libssl-dev pkg-config build-essential git python3 wget zip unzip openjdk-${JAVA_VERSION}-jdk && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Install android targets RUN rustup target add armv7-linux-androideabi aarch64-linux-android # Install cargo-apk RUN git clone -b fix/bin-targets-workspace-members https://github.com/parasyte/cargo-apk.git /tmp/cargo-apk && \ cargo install --path /tmp/cargo-apk/cargo-apk # Generate Environment Variables ENV JAVA_VERSION=${JAVA_VERSION} ENV ANDROID_HOME=/opt/Android ENV NDK_HOME=/opt/Android/ndk/${NDK_VERSION} ENV ANDROID_NDK_ROOT=${NDK_HOME} ENV PATH=$PATH:${ANDROID_HOME}:${ANDROID_NDK_ROOT}:${ANDROID_HOME}/build-tools/${BUILDTOOLS_VERSION}:${ANDROID_HOME}/cmdline-tools/bin # Install command line tools RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \ wget -qc "https://dl.google.com/android/repository/commandlinetools-linux-${CLITOOLS_VERSION}.zip" -P /tmp && \ unzip -d ${ANDROID_HOME} /tmp/commandlinetools-linux-${CLITOOLS_VERSION}.zip && \ rm -fr /tmp/commandlinetools-linux-${CLITOOLS_VERSION}.zip # Install sdk requirements RUN echo y | sdkmanager --sdk_root=${ANDROID_HOME} --install \ "build-tools;${BUILDTOOLS_VERSION}" "ndk;${NDK_VERSION}" "platforms;${PLATFORM_VERSION}" # Create APK keystore for debug profile # Adapted from https://github.com/rust-mobile/cargo-apk/blob/caa806283dc26733ad8232dce1fa4896c566f7b8/ndk-build/src/ndk.rs#L393-L423 RUN keytool -genkey -v -keystore ${HOME}/.android/debug.keystore -storepass android -alias androiddebugkey \ -keypass android -dname 'CN=Android Debug,O=Android,C=US' -keyalg RSA -keysize 2048 -validity 10000 # Cleanup RUN rm -rf /tmp/* WORKDIR /src ENTRYPOINT [ "cargo", "apk", "build" ] ```
.dockerignore ```ignore # Ignore everything, only the Dockerfile is needed to build the container * ```
```sh docker build -t rust-android:latest . docker run --rm -it -v "$PWD:/src" rust-android:latest -p hello_android adb install target/debug/apk/hello_android.apk ``` * Part of #2066 * [x] I have followed the instructions in the PR template --- Cargo.lock | 38 ++++++++++++++++ crates/eframe/src/epi.rs | 16 +++++++ crates/eframe/src/native/run.rs | 11 +++++ examples/hello_android/Cargo.toml | 32 +++++++++++++ examples/hello_android/README.md | 20 +++++++++ examples/hello_android/screenshot.png | 3 ++ examples/hello_android/src/lib.rs | 65 +++++++++++++++++++++++++++ 7 files changed, 185 insertions(+) create mode 100644 examples/hello_android/Cargo.toml create mode 100644 examples/hello_android/README.md create mode 100644 examples/hello_android/screenshot.png create mode 100644 examples/hello_android/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3837d7ec4c0..ebe0aa08f70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1473,6 +1490,16 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -1935,6 +1962,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hello_android" +version = "0.1.0" +dependencies = [ + "android_logger", + "eframe", + "egui_extras", + "log", + "winit", +] + [[package]] name = "hello_world" version = "0.1.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 9f4f6dde8b1..53051f398e4 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -364,6 +364,16 @@ pub struct NativeOptions { /// /// Defaults to true. pub dithering: bool, + + /// Android application for `winit`'s event loop. + /// + /// This value is required on Android to correctly create the event loop. See + /// [`EventLoopBuilder::build`] and [`with_android_app`] for details. + /// + /// [`EventLoopBuilder::build`]: winit::event_loop::EventLoopBuilder::build + /// [`with_android_app`]: winit::platform::android::EventLoopBuilderExtAndroid::with_android_app + #[cfg(target_os = "android")] + pub android_app: Option, } #[cfg(not(target_arch = "wasm32"))] @@ -383,6 +393,9 @@ impl Clone for NativeOptions { persistence_path: self.persistence_path.clone(), + #[cfg(target_os = "android")] + android_app: self.android_app.clone(), + ..*self } } @@ -424,6 +437,9 @@ impl Default for NativeOptions { persistence_path: None, dithering: true, + + #[cfg(target_os = "android")] + android_app: None, } } } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 43dd07ee0da..219edd56625 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -17,9 +17,20 @@ use crate::{ // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { + #[cfg(target_os = "android")] + use winit::platform::android::EventLoopBuilderExtAndroid as _; + crate::profile_function!(); let mut builder = winit::event_loop::EventLoop::with_user_event(); + #[cfg(target_os = "android")] + let mut builder = + builder.with_android_app(native_options.android_app.take().ok_or_else(|| { + crate::Error::AppCreation(Box::from( + "`NativeOptions` is missing required `android_app`", + )) + })?); + if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { hook(&mut builder); } diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml new file mode 100644 index 00000000000..dcb0a5a5c01 --- /dev/null +++ b/examples/hello_android/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "hello_android" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. +# [lints] +# workspace = true + +[lib] +crate-type = ["cdylib"] + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "android-native-activity", +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +log = { workspace = true } +winit = { workspace = true } +android_logger = "0.14" + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android" ] diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md new file mode 100644 index 00000000000..fe14eb9face --- /dev/null +++ b/examples/hello_android/README.md @@ -0,0 +1,20 @@ +Hello world example for Android. + +Use `cargo-apk` to build and run. Requires a patch to workaround [an upstream bug](https://github.com/rust-mobile/cargo-subcommand/issues/29). + +One-time setup: + +```sh +cargo install \ + --git https://github.com/parasyte/cargo-apk.git \ + --rev 282639508eeed7d73f2e1eaeea042da2716436d5 \ + cargo-apk +``` + +Build and run: + +```sh +cargo apk run -p hello_android +``` + +![](screenshot.png) diff --git a/examples/hello_android/screenshot.png b/examples/hello_android/screenshot.png new file mode 100644 index 00000000000..91179fa2f41 --- /dev/null +++ b/examples/hello_android/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add91d7d6b73f48e98f20d84cba3bd3a950cf97aa31f5e9fa93da9af98e876c +size 120019 diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs new file mode 100644 index 00000000000..adda66ca5f7 --- /dev/null +++ b/examples/hello_android/src/lib.rs @@ -0,0 +1,65 @@ +#![cfg(target_os = "android")] +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use android_logger::Config; +use eframe::egui; +use log::LevelFilter; +use winit::platform::android::activity::AndroidApp; + +#[no_mangle] +fn android_main(app: AndroidApp) { + // Log to android output + android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); + + let options = eframe::NativeOptions { + android_app: Some(app), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) + .unwrap() +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); + }); + } +}