Skip to content

Commit

Permalink
Android support for eframe (#5318)
Browse files Browse the repository at this point in the history
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

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!)

<details><summary><code>Dockerfile</code></summary>

```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" ]
```
</details>

<details><summary><code>.dockerignore</code></summary>

```ignore
# Ignore everything, only the Dockerfile is needed to build the container
*
```
</details>

```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
  • Loading branch information
parasyte authored Dec 12, 2024
1 parent 6c1d695 commit ea89c29
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 0 deletions.
38 changes: 38 additions & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
16 changes: 16 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<winit::platform::android::activity::AndroidApp>,
}

#[cfg(not(target_arch = "wasm32"))]
Expand All @@ -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
}
}
Expand Down Expand Up @@ -424,6 +437,9 @@ impl Default for NativeOptions {
persistence_path: None,

dithering: true,

#[cfg(target_os = "android")]
android_app: None,
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/eframe/src/native/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,20 @@ use crate::{

// ----------------------------------------------------------------------------
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
#[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);
}
Expand Down
32 changes: 32 additions & 0 deletions examples/hello_android/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "hello_android"
version = "0.1.0"
authors = ["Emil Ernerfeldt <[email protected]>"]
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" ]
20 changes: 20 additions & 0 deletions examples/hello_android/README.md
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions examples/hello_android/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions examples/hello_android/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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::<MyApp>::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"
));
});
}
}

0 comments on commit ea89c29

Please sign in to comment.