Skip to content

Commit

Permalink
Android: rework backend to use android_activity crate
Browse files Browse the repository at this point in the history
This updates the Android backend to use the android_activity crate instead
of ndk-glue. This solves a few issues:
1. The backend is agnostic of the application's choice of Activity base
   class
2. Winit is no longer responsible for handling any Java synchronization
   details, since these are encapsulated by the design of
   android_activity
3. The backend no longer depends on global / static getters for state
   such as the native_window() which puts it in a better position to
   support running multiple activities within a single Android process.
4. Redraw requests are flagged, not queued, in a way that avoids taking
   priority over user events (resolves rust-windowing#2299)

Addresses: PR rust-windowing#1892
Addresses: PR rust-windowing#2307
Addresses: PR rust-windowing#2343

Addresses: rust-windowing#2293
Resolves: rust-windowing#2299
  • Loading branch information
rib committed Nov 29, 2022
1 parent 2e4338b commit 2c050f3
Show file tree
Hide file tree
Showing 7 changed files with 723 additions and 416 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
- { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, }
- { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: x11 }
- { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: "wayland,wayland-dlopen" }
- { target: aarch64-linux-android, os: ubuntu-latest, cmd: 'apk --' }
- { target: aarch64-linux-android, os: ubuntu-latest, cmd: 'apk --', features: "android-native-activity" }
- { target: x86_64-apple-darwin, os: macos-latest, }
- { target: x86_64-apple-ios, os: macos-latest, }
- { target: aarch64-apple-ios, os: macos-latest, }
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ Please keep one empty line before and after all headers. (This is required for `
And please only add new entries to the top of this list, right below the `# Unreleased` header.

# Unreleased
- **Breaking:** On Android, switched to using [`android-activity`](https://github.com/rib/android-activity) crate as a glue layer instead of [`ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue). See [README.md#Android](https://github.com/rust-windowing/winit#Android) for more details. ([#2444](https://github.com/rust-windowing/winit/pull/2444))

# 0.27.3

- On Windows, added `WindowExtWindows::set_undecorated_shadow` and `WindowBuilderExtWindows::with_undecorated_shadow` to draw the drop shadow behind a borderless window.
- On Windows, fixed default window features (ie snap, animations, shake, etc.) when decorations are disabled.
- On Windows, fixed ALT+Space shortcut to open window menu.
- On Wayland, fixed `Ime::Preedit` not being sent on IME reset.
- Fixed unbound version specified for `raw-window-handle` leading to compilation failures.
- Empty `Ime::Preedit` event will be sent before `Ime::Commit` to help clearing preedit.
- On X11, fixed IME context picking by querying for supported styles beforehand.

# 0.27.2 (2022-8-12)

Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ wayland = ["wayland-client", "wayland-protocols", "sctk"]
wayland-dlopen = ["sctk/dlopen", "wayland-client/dlopen"]
wayland-csd-adwaita = ["sctk-adwaita", "sctk-adwaita/title"]
wayland-csd-adwaita-notitle = ["sctk-adwaita"]
android-native-activity = [ "android-activity/native-activity" ]
android-game-activity = [ "android-activity/game-activity" ]

[dependencies]
instant = { version = "0.1", features = ["wasm-bindgen"] }
Expand All @@ -58,7 +60,7 @@ simple_logger = "2.1.0"
[target.'cfg(target_os = "android")'.dependencies]
# Coordinate the next winit release with android-ndk-rs: https://github.com/rust-windowing/winit/issues/1995
ndk = "0.7.0"
ndk-glue = "0.7.0"
android-activity = "0.4.0"

[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
objc = "0.2.7"
Expand Down
77 changes: 61 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,36 +99,81 @@ book].

#### Android

This library makes use of the [ndk-rs](https://github.com/rust-windowing/android-ndk-rs) crates, refer to that repo for more documentation.
The Android backend builds on (and exposes types from) the [`ndk`](https://docs.rs/ndk/0.7.0/ndk/) crate.

The `ndk-glue` version needs to match the version used by `winit`. Otherwise, the application will not start correctly as `ndk-glue`'s internal `NativeActivity` static is not the same due to version mismatch.
Native Android applications need some form of "glue" crate that is responsible for defining the main entry point for your Rust application as well as tracking various life-cycle events and synchronizing with the main JVM thread.

`winit` compatibility table with `ndk-glue`:
Winit uses the [android-activity](https://github.com/rib/android-activity) as a glue crate (prior to `0.28` it used [ndk-glue](https://github.com/rust-windowing/android-ndk-rs/tree/master/ndk-glue).

| winit | ndk-glue |
| :---: | :------------------: |
| 0.24 | `ndk-glue = "0.2.0"` |
| 0.25 | `ndk-glue = "0.3.0"` |
| 0.26 | `ndk-glue = "0.5.0"` |
| 0.27 | `ndk-glue = "0.7.0"` |
The version of the glue crate that your application depends on _must_ match the version that Winit depends on because the glue crate is responsible for your application's main entrypoint. If Cargo resolves multiple versions they will clash.

`winit` glue compatibility table:

| winit | ndk-glue |
| :---: | :--------------------------: |
| 0.28 | `android-activity = "0.4"` |
| 0.27 | `ndk-glue = "0.7"` |
| 0.26 | `ndk-glue = "0.5"` |
| 0.25 | `ndk-glue = "0.3"` |
| 0.24 | `ndk-glue = "0.2"` |

_Note: There is unfortunately no ergonomic way to configure Cargo so that it understands that the glue crate is special on Android and to give a clear error if multiple versions are resolved. With `ndk-glue` you will get a runtime failure caused by clashing, global static variables. With `android-activity` you will see a compile-time error due to missing feature flags._

Running on an Android device needs a dynamic system library, add this to Cargo.toml:

```toml
[[example]]
name = "request_redraw_threaded"
[lib]
name = "main"
crate-type = ["cdylib"]
```

And add this to the example file to add the native activity glue:
All Android applications are based on an `Activity` subclass and the `android-activity` crate is designed to support different choices for this base class. You application must specify the base class it needs via a feature flag:

| Base Class | Feature Flag | Notes |
| :--------------: | :---------------: | :-----: |
| `NativeActivity` | `android-native-activity` | Built-in to Android - making it possible to build some tests/demos without needing to compile any JVM code. Can give a false sense of convenience because it's often not really possible to avoid needing a build system that can compile some JVM code, to at least subclass `NativeActivity` |
| [`GameActivity`] | `android-game-activity` | Derives from [`AndroidAppCompat`] which is a defacto standard `Activity` base class that helps support a wider range of Android versions. Will offer integration with [`GameTextInput`] library for soft keyboard support. Requires a build system that can compile Java and fetch Android dependencies from a Maven repository (with [Gradle] being the defacto standard build system for Android applications) |

[`GameActivity`]: https://developer.android.com/games/agdk/game-activity
[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input
[`AndroidAppCompat`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
[Gradle]: https://developer.android.com/studio/build

For example, add this to Cargo.toml:
```toml
winit = { version = "0.28", features = [ "android-game-activity" ] }

[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.0"
```

And, for example, define an entry point for your library like this:
```rust
#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))]
fn main() {
...
#[cfg(target_os = "android")]
use winit::platform::android::activity::AndroidApp;

#[cfg(target_os = "android")]
#[no_mangle]
fn android_main(app: AndroidApp) {
use winit::platform::android::EventLoopBuilderExtAndroid;

android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Trace));

let event_loop = EventLoopBuilder::with_user_event()
.with_android_app(app)
.build();
_main(event_loop);
}
```

And run the application with `cargo apk run --example request_redraw_threaded`
For more details, refer to these `android-activity` [example applications](https://github.com/rib/android-activity/tree/main/examples).

##### Converting from `ndk-glue` to `android-activity`

If your application is currently based on `NativeActivity` via the `ndk-glue` crate and building with `cargo apk` then the minimal changes would be:
1. Remove `ndk-glue` from your `Cargo.toml` and add dependency on `android-activity` that specifies the `"native-activity"` feature like: `android-activity = { version = "0.3", features = [ "native-activity" ] }`
2. Add an `android_main` entrypoint (as above), instead of using the '`[ndk_glue::main]` proc macro from `ndk-macros` (optionally add a dependency on `android_logger` and initialize logging as above).
3. Pass a clone of the `AndroidApp` that your application receives to Winit when building your event loop (as shown above).

#### MacOS

Expand Down
10 changes: 10 additions & 0 deletions src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,18 @@ impl<T> EventLoopBuilder<T> {
/// `WINIT_UNIX_BACKEND`. Legal values are `x11` and `wayland`.
/// If it is not set, winit will try to connect to a Wayland connection, and if that fails,
/// will fall back on X11. If this variable is set with any other value, winit will panic.
/// - **Android:** Must be configured with an `AndroidApp` from `android_main()` by calling
/// [`.with_android_app(app)`] before calling `.build()`.
///
/// [`platform`]: crate::platform
#[cfg_attr(
target_os = "android",
doc = "[`.with_android_app(app)`]: crate::platform::android::EventLoopBuilderExtAndroid::with_android_app"
)]
#[cfg_attr(
not(target_os = "android"),
doc = "[`.with_android_app(app)`]: #only-available-on-android"
)]
#[inline]
pub fn build(&mut self) -> EventLoop<T> {
static EVENT_LOOP_CREATED: OnceCell<()> = OnceCell::new();
Expand Down
38 changes: 33 additions & 5 deletions src/platform/android.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#![cfg(any(target_os = "android"))]

use crate::{
event_loop::{EventLoop, EventLoopWindowTarget},
event_loop::{EventLoop, EventLoopBuilder, EventLoopWindowTarget},
window::{Window, WindowBuilder},
};
use ndk::configuration::Configuration;
use ndk_glue::Rect;

use android_activity::{AndroidApp, ConfigurationRef, Rect};

/// Additional methods on [`EventLoop`] that are specific to Android.
pub trait EventLoopExtAndroid {}
Expand All @@ -19,15 +19,15 @@ pub trait EventLoopWindowTargetExtAndroid {}
pub trait WindowExtAndroid {
fn content_rect(&self) -> Rect;

fn config(&self) -> Configuration;
fn config(&self) -> ConfigurationRef;
}

impl WindowExtAndroid for Window {
fn content_rect(&self) -> Rect {
self.window.content_rect()
}

fn config(&self) -> Configuration {
fn config(&self) -> ConfigurationRef {
self.window.config()
}
}
Expand All @@ -38,3 +38,31 @@ impl<T> EventLoopWindowTargetExtAndroid for EventLoopWindowTarget<T> {}
pub trait WindowBuilderExtAndroid {}

impl WindowBuilderExtAndroid for WindowBuilder {}

pub trait EventLoopBuilderExtAndroid {
/// Associates the `AndroidApp` that was passed to `android_main()` with the event loop
///
/// This must be called on Android since the `AndroidApp` is not global state.
fn with_android_app(&mut self, app: AndroidApp) -> &mut Self;
}

impl<T> EventLoopBuilderExtAndroid for EventLoopBuilder<T> {
fn with_android_app(&mut self, app: AndroidApp) -> &mut Self {
self.platform_specific.android_app = Some(app);
self
}
}

/// We re-export the `android_activity` API via Winit so that most applications
/// can rely on the Winit crate to resolve the required version and avoid any
/// chance of a conflict between Winit and the application crate.
///
/// For compatibility applications should then import the `AndroidApp` type for
/// their `android_main(app: AndroidApp)` function like:
/// ```rust
/// #[cfg(target_os="android")]
/// use winit::platform::android::activity::AndroidApp;
/// ```
pub mod activity {
pub use android_activity::*;
}
Loading

0 comments on commit 2c050f3

Please sign in to comment.