Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02021c912c | |||
| 10d95187c7 | |||
| 5fbc26c61f |
@@ -1,10 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -16,10 +16,12 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# See top README for MSRV policy
|
||||
rust_version: [1.64.0, stable]
|
||||
# We need to support the same MSRV as Winit which we are currently
|
||||
# assuming will be bumped to 1.60.0 soon:
|
||||
# https://github.com/rust-windowing/winit/pull/2453
|
||||
rust_version: [1.60.0, stable]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: hecrj/setup-rust-action@v1
|
||||
with:
|
||||
@@ -34,9 +36,7 @@ jobs:
|
||||
i686-linux-android
|
||||
|
||||
- name: Install cargo-ndk
|
||||
# We're currently sticking with cargo-ndk 2, until we bump our
|
||||
# MSRV to 1.68+
|
||||
run: cargo install cargo-ndk --version "^2"
|
||||
run: cargo install cargo-ndk
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
@@ -89,6 +89,28 @@ jobs:
|
||||
-t x86
|
||||
-o app/src/main/jniLibs/ -- build
|
||||
|
||||
- name: Build agdk-egui example
|
||||
if: matrix.rust_version == 'stable'
|
||||
working-directory: examples/agdk-egui
|
||||
run: >
|
||||
cargo ndk
|
||||
-t arm64-v8a
|
||||
-t armeabi-v7a
|
||||
-t x86_64
|
||||
-t x86
|
||||
-o app/src/main/jniLibs/ -- build
|
||||
|
||||
- name: Build agdk-eframe example
|
||||
if: matrix.rust_version == 'stable'
|
||||
working-directory: examples/agdk-eframe
|
||||
run: >
|
||||
cargo ndk
|
||||
-t arm64-v8a
|
||||
-t armeabi-v7a
|
||||
-t x86_64
|
||||
-t x86
|
||||
-o app/src/main/jniLibs/ -- build
|
||||
|
||||
- name: Documentation
|
||||
run: >
|
||||
cargo ndk -t arm64-v8a doc --no-deps
|
||||
@@ -96,7 +118,7 @@ jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -109,10 +131,6 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: android-activity
|
||||
|
||||
- name: Format na-mainloop example
|
||||
- name: Format agdk-egui example
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: examples/na-mainloop
|
||||
|
||||
- name: Format agdk-mainloop example
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: examples/agdk-mainloop
|
||||
working-directory: examples/agdk-egui
|
||||
|
||||
@@ -4,5 +4,15 @@ members = [
|
||||
]
|
||||
|
||||
exclude = [
|
||||
"examples",
|
||||
"examples/agdk-mainloop",
|
||||
"examples/agdk-winit-wgpu",
|
||||
"examples/agdk-eframe",
|
||||
"examples/agdk-egui",
|
||||
"examples/agdk-oboe",
|
||||
"examples/agdk-cpal",
|
||||
"examples/na-mainloop",
|
||||
"examples/na-winit-wgpu",
|
||||
"examples/na-subclass-jni",
|
||||
"examples/na-openxr-info",
|
||||
"examples/na-openxr-wgpu"
|
||||
]
|
||||
|
||||
@@ -1,66 +1,59 @@
|
||||
# `android-activity`
|
||||
|
||||
[](https://github.com/rust-mobile/android-activity/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/android-activity)
|
||||
[](https://docs.rs/android-activity)
|
||||
[](https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html)
|
||||
|
||||
## Overview
|
||||
# Overview
|
||||
|
||||
`android-activity` provides a "glue" layer for building native Rust
|
||||
applications on Android, supporting multiple [`Activity`] base classes.
|
||||
It's comparable to [`android_native_app_glue.c`][ndk_concepts]
|
||||
for C/C++ applications and is an alternative to the [ndk-glue] crate.
|
||||
for C/C++ applications.
|
||||
|
||||
`android-activity` provides a way to load your crate as a `cdylib` library via
|
||||
the `onCreate` method of your Android `Activity` class; run an `android_main()`
|
||||
function in a separate thread from the Java main thread and marshal events (such
|
||||
as lifecycle events and input events) between Java and your native thread.
|
||||
`android-activity` supports [`NativeActivity`] or [`GameActivity`] from the
|
||||
Android Game Development Kit and can be extended to support additional base
|
||||
classes.
|
||||
|
||||
So far it supports [`NativeActivity`] or [`GameActivity`] (from the
|
||||
[Android Game Development Kit][agdk]) and there's also interest in supporting a first-party
|
||||
`RustActivity` base class that could be better tailored to the needs of Rust
|
||||
applications.
|
||||
`android-activity` provides a way to load a `cdylib` via the `onCreate` method of
|
||||
your `Activity` class; run an `android_main()` function in a separate thread from the Java
|
||||
main thread and marshal events (such as lifecycle events and input events) between
|
||||
Java and your native thread.
|
||||
|
||||
[`Activity`]: https://developer.android.com/reference/android/app/Activity
|
||||
[`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
|
||||
[ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa
|
||||
[`GameActivity`]: https://developer.android.com/games/agdk/integrate-game-activity
|
||||
[ndk-glue]: https://crates.io/crates/ndk-glue
|
||||
[agdk]: https://developer.android.com/games/agdk
|
||||
|
||||
## Example
|
||||
### Example
|
||||
|
||||
```
|
||||
cargo init --lib --name=example
|
||||
```
|
||||
|
||||
Cargo.toml
|
||||
|
||||
```toml
|
||||
```
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
android_logger = "0.11"
|
||||
android-activity = { version = "0.4", features = [ "native-activity" ] }
|
||||
android-activity = { git = "https://github.com/rib/android-activity/", features = [ "native-activity" ] }
|
||||
|
||||
[lib]
|
||||
crate_type = ["cdylib"]
|
||||
```
|
||||
|
||||
_Note: that you will need to either specify the **"native-activity"** feature or **"game-activity"** feature to identify which `Activity` base class your application is based on_
|
||||
|
||||
lib.rs
|
||||
|
||||
```rust
|
||||
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
|
||||
use log::info;
|
||||
use android_activity::{PollEvent, MainEvent};
|
||||
|
||||
#[no_mangle]
|
||||
fn android_main(app: AndroidApp) {
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default().with_min_level(log::Level::Info)
|
||||
);
|
||||
|
||||
loop {
|
||||
app.poll_events(Some(std::time::Duration::from_millis(500)) /* timeout */, |event| {
|
||||
match event {
|
||||
PollEvent::Wake => { log::info!("Early wake up"); },
|
||||
PollEvent::Timeout => { log::info!("Hello, World!"); },
|
||||
PollEvent::Wake => { info!("Early wake up"); },
|
||||
PollEvent::Timeout => { info!("Hello, World!"); },
|
||||
PollEvent::Main(main_event) => {
|
||||
log::info!("Main event: {:?}", main_event);
|
||||
info!("Main event: {:?}", main_event);
|
||||
match main_event {
|
||||
MainEvent::Destroy => { return; }
|
||||
_ => {}
|
||||
@@ -70,95 +63,188 @@ fn android_main(app: AndroidApp) {
|
||||
}
|
||||
|
||||
app.input_events(|event| {
|
||||
log::info!("Input Event: {event:?}");
|
||||
InputStatus::Unhandled
|
||||
info!("Input Event: {event:?}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
```
|
||||
rustup target add aarch64-linux-android
|
||||
cargo install cargo-apk
|
||||
cargo apk run
|
||||
adb logcat example:V *:S
|
||||
```
|
||||
|
||||
## Full Examples
|
||||
# Game Activity
|
||||
|
||||
See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`).
|
||||
Originally the aim was to enable support for building Rust applications based on the
|
||||
[GameActivity] class provided by [Google's Android Game Development Kit][agdk]
|
||||
which can also facilitate integration with additional AGDK libraries including:
|
||||
1. [Game Text Input](https://developer.android.com/games/agdk/add-support-for-text-input): a library
|
||||
to help fullscreen native applications utilize the Android soft keyboard.
|
||||
2. [Game Controller Library, aka 'Paddleboat'](https://developer.android.com/games/sdk/game-controller):
|
||||
a native library designed to help support access to game controller inputs.
|
||||
3.[Frame Pacing Library, aka ' Swappy'](https://developer.android.com/games/sdk/frame-pacing): a library
|
||||
that helps OpenGL and Vulkan games achieve smooth rendering and correct frame pacing on Android.
|
||||
3. [Memory Advice API](https://developer.android.com/games/sdk/memory-advice/overview): an API to
|
||||
help applications monitor their own memory usage to stay within safe limits for the system.
|
||||
4. [Oboe audio library](https://developer.android.com/games/sdk/oboe): a low-latency audio API for native
|
||||
applications.
|
||||
|
||||
Each example is a standalone project that may also be a convenient templates for starting a new project.
|
||||
Since `GameActivity` is based on the widely used [AppCompatActivity] base class, it also
|
||||
provides a variety of back ported Activity APIs which can make it more practical to
|
||||
support a wider range of devices and Android versions.
|
||||
|
||||
For the examples based on middleware frameworks (Winit and or Egui) they also aim to demonstrate how it's possible to write portable code that will run on Android and other systems.
|
||||
[GameActivity]: https://developer.android.com/games/agdk/integrate-game-activity
|
||||
[agdk]: https://developer.android.com/games/agdk
|
||||
[AppCompatActivity]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
|
||||
|
||||
## Should I use NativeActivity or GameActivity?
|
||||
# Native Activity
|
||||
|
||||
To learn more about the `NativeActivity` class that's shipped with Android see [here](https://developer.android.com/ndk/guides/concepts#naa).
|
||||
This project also supports [`NativeActivity`][NativeActivity] based applications. Although
|
||||
NativeActivity is more limited than `GameActivity` and does not derive from `AppCompatActivity` it
|
||||
can sometimes still be convenient to build on `NativeActivity` in situations where you are using a
|
||||
limited/minimal build system that is not able to compile Java or Kotlin code or fetch from Maven
|
||||
repositories - this is because `NativeActivity` is included as part of the Android platform.
|
||||
|
||||
To learn more about the `GameActivity` class that's part of the [Android Game Developer's Kit][agdk] and also see a comparison with `NativeActivity` see [here](https://developer.android.com/games/agdk/game-activity)
|
||||
[NativeActivity]: https://developer.android.com/reference/android/app/NativeActivity
|
||||
|
||||
Generally speaking, if unsure, `NativeActivity` may be more convenient to start with since you may not need to compile/link any Java or Kotlin code.
|
||||
# Design
|
||||
|
||||
It's expected that the `GameActivity` backend will gain more sophisticated input handling features over time (such as for supporting input via onscreen keyboards or game controllers) and only `GameActivity` is based on the [`AppCompatActivity`] subclass which you may want in some situations to help with compatibility across devices.
|
||||
## Compatibility
|
||||
|
||||
Even if you start out using `NativeActivity` for the convenience, it's likely that most moderately complex applications will eventually need to define their own `Activity` subclass (either subclassing `NativeActivity` or `GameActivity`) which will require compiling at least a small amount of Java or Kotlin code. This is generally due to Android's design which directs numerous events via the `Activity` class which can only be processed by overloading some associated Activity method.
|
||||
All `Activity` classes are supported via a common API that enables you to write
|
||||
`Activity` subclass agnostic code wherever you don't depend on features that are
|
||||
specific to a particular subclass.
|
||||
|
||||
## Switching from ndk-glue to android-activity
|
||||
For example, it makes it possible to have a [Winit backend](https://github.com/rib/winit/tree/agdk-game-activity)
|
||||
that supports Android applications running with different `Activity` classes.
|
||||
|
||||
### Winit-based applications
|
||||
## API Summary
|
||||
|
||||
Firstly; if you have a [Winit](https://crates.io/crates/winit) based application and also have an explicit dependency on `ndk-glue` your application will need to remove its dependency on `ndk-glue` for the 0.28 release of Winit which will be based on android-activity (Since glue crates, due to their nature, can't be compatible with alternative glue crates).
|
||||
|
||||
Winit-based applications can follow the [Android README](https://github.com/rust-windowing/winit#android) guidance for advice on how to switch over. Most Winit-based applications should aim to remove any explicit dependency on a specific glue crate (so not depend directly on `ndk-glue` or `android-activity` and instead rely on Winit to pull in the right glue crate). The main practical change will then be to add a `#[no_mangle]fn android_main(app: AndroidApp)` entry point.
|
||||
|
||||
See the [Android README](https://github.com/rust-windowing/winit#android) for more details and also see the [Winit-based examples here](https://github.com/rust-mobile/rust-android-examples).
|
||||
|
||||
### Middleware crates (i.e. not applications)
|
||||
|
||||
If you have a crate that would be considered a middleware library (for example using JNI to support access to Bluetooth, or Android's Camera APIs) then the crate should almost certainly remove any dependence on a specific glue crate because this imposes a strict compatibility constraint that means the crate can only be used by applications that use that exact same glue crate version.
|
||||
|
||||
Middleware libraries can instead look at using the [ndk-context](https://crates.io/crates/ndk-context) crate as a means for being able to use JNI without making any assumptions about the applications overall architecture. This way a middleware crate can work with alternative glue crates (including `ndk-glue` and `android-activity`) as well as work with embedded use cases (i.e. non-native applications that may just embed a dynamic library written in Rust to implement certain native functions).
|
||||
|
||||
### Other, non-Winit-based applications
|
||||
|
||||
The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be:
|
||||
|
||||
1. Remove `ndk-glue` from your Cargo.toml
|
||||
2. Add a dependency on `android-activity`, like `android-activity = { version="0.4", features = [ "native-activity" ] }`
|
||||
3. Optionally add a dependency on `android_logger = "0.11.0"`
|
||||
4. Update the `main` entry point to look like this:
|
||||
### `android_main` entrypoint
|
||||
The glue crates define a standard entrypoint ABI for your `cdylib` that looks like:
|
||||
|
||||
```rust
|
||||
use android_activity::AndroidApp;
|
||||
|
||||
#[no_mangle]
|
||||
fn android_main(app: AndroidApp) {
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
See this minimal [NativeActivity Mainloop](https://github.com/rust-mobile/android-activity/tree/main/examples/na-mainloop) for more details about how to poll for events.
|
||||
There's currently no high-level macro provided for things like initializing
|
||||
logging or allowing the main function to return a `Result<>` since it's expected
|
||||
that different downstream frameworks may each have differing opinions on the
|
||||
details and may want to provide their own macros.
|
||||
|
||||
There is is no `#[ndk_glue::main]` replacement considering that `android_main()` entry point needs to be passed an `AndroidApp` argument which isn't compatible with a traditional `main()` function. Having an Android specific entry point also gives a place to initialize Android logging and handle other Android specific details (such as building an event loop based on the `app` argument)
|
||||
|
||||
### Design Summary / Motivation behind android-activity
|
||||
### `AndroidApp`
|
||||
|
||||
Prior to working on android-activity, the existing glue crates available for building standalone Rust applications on Android were found to have a number of technical limitations that this crate aimed to solve:
|
||||
Your `android_main()` function is passed an `AndroidApp` struct to access state
|
||||
about your running application and handle synchronized interaction between your
|
||||
native Rust application and the `Activity` running on the Java main thread.
|
||||
|
||||
1. **Support alternative Activity classes**: Prior glue crates were based on `NativeActivity` and their API precluded supporting alternatives. In particular there was an interest in the [`GameActivity`] class in conjunction with it's [`GameTextInput`] library that can facilitate onscreen keyboard support. This also allows building applications based on the standard [`AppCompatActivity`] base class which isn't possible with `NativeActivity`. Finally there was interest in paving the way towards supporting a first-party `RustActivity` that could be best tailored towards the needs of Rust applications on Android.
|
||||
2. **Encapsulate IPC + synchronization between the native thread and the JVM thread**: For example with `ndk-glue` the application itself needs to avoid race conditions between the native and Java thread by following a locking convention) and it wasn't clear how this would extend to support other requests (like state saving) that also require synchronization.
|
||||
3. **Avoid static global state**: Keeping in mind the possibility of supporting applications with multiple native activities there was interest in having an API that didn't rely on global statics to track top-level state. Instead of having global getters for state then `android-activity` passes an explicit `app: AndroidApp` argument to the entry point that encapsulates the state connected with a single `Activity`.
|
||||
For example, the `AndroidApp` API enables:
|
||||
1. Access to Android lifecycle events
|
||||
2. Notifications of SurfaceView lifecycle events
|
||||
3. Access to input events
|
||||
4. Ability to save and restore state each time your process stops and starts
|
||||
5. Access application [`Configuration`] state
|
||||
6. internal/external/obb filesystem paths
|
||||
|
||||
[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input
|
||||
[`AppCompatActivity`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
|
||||
_Note: that some of the `AndroidApp` APIs (such as for polling events) are only
|
||||
deemed safe to use from the application's main thread_
|
||||
|
||||
## MSRV
|
||||
[`Configuration`]: https://developer.android.com/reference/android/content/res/Configuration
|
||||
|
||||
We aim to (at least) support stable releases of Rust from the last three months. Rust has a 6 week release cycle which means we will support the last three stable releases.
|
||||
For example, when Rust 1.69 is released we would limit our `rust_version` to 1.67.
|
||||
### Synchronized event callbacks
|
||||
|
||||
We will only bump the `rust_version` at the point where we either depend on a new features or a dependency has increased its MSRV, and we won't be greedy. In other words we will only set the MSRV to the lowest version that's _needed_.
|
||||
The `AndroidApp::poll_events()` API is similar to the Winit `EventLoop::run` API in that it
|
||||
takes a `FnMut` closure that is called for each outstanding event (such as for lifecycle events).
|
||||
This design ensures the glue layer can transparently handle any required synchronization with
|
||||
Java before and after each callback.
|
||||
|
||||
MSRV updates are not considered to be inherently semver breaking (unless a new feature is exposed in the public API) and so a `rust_version` change may happen in patch releases.
|
||||
For example, when the Java main thread notifies the glue layer that its `SurfaceView` is being
|
||||
destroyed the Java thread will then block until it gets an explicit acknowledgement that the
|
||||
native application has had an opportunity to react to this notification. The glue layer will
|
||||
automatically release the blocked Java thread once it has delivered the corresponding event.
|
||||
|
||||
For example:
|
||||
```rust
|
||||
use native_activity::{PollEvent, MainEvent};
|
||||
use log::info;
|
||||
|
||||
#[no_mangle]
|
||||
extern "C" fn android_main() {
|
||||
android_logger::init_once(
|
||||
android_logger::Config::default().with_min_level(log::Level::Info)
|
||||
);
|
||||
|
||||
let mut quit = false;
|
||||
let mut redraw_pending = true;
|
||||
let mut render_state: Option<()> = Default::default();
|
||||
|
||||
let app = native_activity::android_app();
|
||||
while !quit {
|
||||
app.poll_events(Some(std::time::Duration::from_millis(500)) /* timeout */, |event| {
|
||||
match event {
|
||||
PollEvent::Wake => { info!("Early wake up"); },
|
||||
PollEvent::Timeout => {
|
||||
info!("Timed out");
|
||||
// Real app would probably rely on vblank sync via graphics API...
|
||||
redraw_pending = true;
|
||||
},
|
||||
PollEvent::Main(main_event) => {
|
||||
info!("Main event: {:?}", main_event);
|
||||
match main_event {
|
||||
MainEvent::SaveState { saver, .. } => {
|
||||
saver.store("foo://bar".as_bytes());
|
||||
},
|
||||
MainEvent::Pause => {},
|
||||
MainEvent::Resume { loader, .. } => {
|
||||
if let Some(state) = loader.load() {
|
||||
if let Ok(uri) = String::from_utf8(state) {
|
||||
info!("Resumed with saved state = {uri:#?}");
|
||||
}
|
||||
}
|
||||
},
|
||||
MainEvent::InitWindow { .. } => {
|
||||
render_state = Some(());
|
||||
redraw_pending = true;
|
||||
},
|
||||
MainEvent::TerminateWindow { .. } => {
|
||||
render_state = None;
|
||||
}
|
||||
MainEvent::WindowResized { .. } => { redraw_pending = true; },
|
||||
MainEvent::RedrawNeeded { ..} => { redraw_pending = true; },
|
||||
MainEvent::LowMemory => {},
|
||||
|
||||
MainEvent::Destroy => { quit = true },
|
||||
_ => { /* ... */}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if redraw_pending {
|
||||
if let Some(_rs) = render_state {
|
||||
redraw_pending = false;
|
||||
|
||||
// Handle input
|
||||
app.input_events(|event| {
|
||||
info!("Input Event: {event:?}");
|
||||
|
||||
});
|
||||
|
||||
info!("Render...");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -5,29 +5,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [0.4.2] - 2022-02-16
|
||||
### Changed
|
||||
- The `Activity.finish()` method is now called when `android_main` returns so the `Activity` will be destroyed ([#67](https://github.com/rust-mobile/android-activity/issues/67))
|
||||
- The `native-activity` backend now propagates `NativeWindow` redraw/resize and `ContentRectChanged` callbacks to main loop ([#70](https://github.com/rust-mobile/android-activity/pull/70))
|
||||
- The `game-activity` implementation of `pointer_index()` was fixed to not always return `0` ([#80](https://github.com/rust-mobile/android-activity/pull/84))
|
||||
- Added `panic` guards around application's `android_main()` and native code that could potentially unwind across a Java FFI boundary ([#68](https://github.com/rust-mobile/android-activity/pull/68))
|
||||
|
||||
## [0.4.1] - 2022-02-16
|
||||
### Added
|
||||
- Added `AndroidApp::vm_as_ptr()` to expose JNI `JavaVM` pointer ([#60](https://github.com/rust-mobile/android-activity/issues/60))
|
||||
- Added `AndroidApp::activity_as_ptr()` to expose Android `Activity` JNI reference as pointer ([#60](https://github.com/rust-mobile/android-activity/issues/60))
|
||||
### Changed
|
||||
- Removed some overly-verbose logging in the `native-activity` backend ([#49](https://github.com/rust-mobile/android-activity/pull/49))
|
||||
### Removed
|
||||
- Most of the examples were moved to https://github.com/rust-mobile/rust-android-examples ([#50](https://github.com/rust-mobile/android-activity/pull/50))
|
||||
|
||||
## [0.4] - 2022-11-10
|
||||
### Changed
|
||||
- *Breaking*: `input_events` callback now return whether an event was handled or not to allow for fallback handling ([#31](https://github.com/rust-mobile/android-activity/issues/31))
|
||||
- The native-activity backend is now implemented in Rust only, without building on `android_native_app_glue.c` ([#35](https://github.com/rust-mobile/android-activity/pull/35))
|
||||
### Added
|
||||
- Added `Pointer::tool_type()` API to `GameActivity` backend for compatibility with `ndk` events API ([#38](https://github.com/rust-mobile/android-activity/pull/38))
|
||||
|
||||
## [0.3] - 2022-09-15
|
||||
### Added
|
||||
- `show/hide_sot_input` API for being able to show/hide a soft keyboard (other IME still pending)
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
[package]
|
||||
name = "android-activity"
|
||||
version = "0.4.2"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
keywords = ["android", "ndk"]
|
||||
readme = "../README.md"
|
||||
homepage = "https://github.com/rust-mobile/android-activity"
|
||||
repository = "https://github.com/rust-mobile/android-activity"
|
||||
homepage = "https://github.com/rib/android-activity"
|
||||
repository = "https://github.com/rib/android-activity"
|
||||
documentation = "https://docs.rs/android-activity"
|
||||
description = "Glue for building Rust applications on Android with NativeActivity or GameActivity"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.64"
|
||||
|
||||
[features]
|
||||
# Note: we don't enable any backend by default since features
|
||||
@@ -18,7 +17,7 @@ rust-version = "1.64"
|
||||
#
|
||||
# In general it's only the final application crate that needs
|
||||
# to decide on a backend.
|
||||
default = []
|
||||
default=[]
|
||||
game-activity = []
|
||||
native-activity = []
|
||||
|
||||
@@ -29,7 +28,7 @@ ndk = "0.7"
|
||||
ndk-sys = "0.4"
|
||||
ndk-context = "0.1"
|
||||
android-properties = "0.2"
|
||||
num_enum = "0.6"
|
||||
num_enum = "0.5"
|
||||
bitflags = "1.3"
|
||||
libc = "0.2"
|
||||
|
||||
@@ -44,4 +43,4 @@ targets = [
|
||||
"x86_64-linux-android",
|
||||
]
|
||||
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs" ]
|
||||
@@ -1,5 +1,13 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
fn build_glue_for_native_activity() {
|
||||
cc::Build::new()
|
||||
.include("native-activity-csrc")
|
||||
.include("native-activity-csrc/native-activity/native_app_glue")
|
||||
.file("native-activity-csrc/native-activity/native_app_glue/android_native_app_glue.c")
|
||||
.compile("libnative_app_glue.a");
|
||||
}
|
||||
|
||||
fn build_glue_for_game_activity() {
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
@@ -30,4 +38,6 @@ fn build_glue_for_game_activity() {
|
||||
fn main() {
|
||||
#[cfg(feature = "game-activity")]
|
||||
build_glue_for_game_activity();
|
||||
#[cfg(feature = "native-activity")]
|
||||
build_glue_for_native_activity();
|
||||
}
|
||||
|
||||
@@ -890,7 +890,6 @@ static struct {
|
||||
|
||||
jmethodID getPointerCount;
|
||||
jmethodID getPointerId;
|
||||
jmethodID getToolType;
|
||||
jmethodID getRawX;
|
||||
jmethodID getRawY;
|
||||
jmethodID getXPrecision;
|
||||
@@ -942,8 +941,6 @@ extern "C" int GameActivityMotionEvent_fromJava(
|
||||
env->GetMethodID(motionEventClass, "getPointerCount", "()I");
|
||||
gMotionEventClassInfo.getPointerId =
|
||||
env->GetMethodID(motionEventClass, "getPointerId", "(I)I");
|
||||
gMotionEventClassInfo.getToolType =
|
||||
env->GetMethodID(motionEventClass, "getToolType", "(I)I");
|
||||
if (sdkVersion >= 29) {
|
||||
gMotionEventClassInfo.getRawX =
|
||||
env->GetMethodID(motionEventClass, "getRawX", "(I)F");
|
||||
@@ -1004,8 +1001,6 @@ extern "C" int GameActivityMotionEvent_fromJava(
|
||||
out_event->pointers[i] = {
|
||||
/*id=*/env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getPointerId, i),
|
||||
/*toolType=*/env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getToolType, i),
|
||||
/*axisValues=*/{0},
|
||||
/*rawX=*/gMotionEventClassInfo.getRawX
|
||||
? env->CallFloatMethod(motionEvent,
|
||||
|
||||
@@ -137,7 +137,6 @@ typedef struct GameActivity {
|
||||
*/
|
||||
typedef struct GameActivityPointerAxes {
|
||||
int32_t id;
|
||||
int32_t toolType;
|
||||
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
|
||||
float rawX;
|
||||
float rawY;
|
||||
|
||||
@@ -16,10 +16,6 @@ while read ARCH && read TARGET ; do
|
||||
--blocklist-item 'C?_?JNIEnv' \
|
||||
--blocklist-item '_?JavaVM' \
|
||||
--blocklist-item '_?j\w+' \
|
||||
--blocklist-item 'size_t' \
|
||||
--blocklist-item 'pthread_\w*' \
|
||||
--blocklist-function 'pthread_\w' \
|
||||
--blocklist-item 'ARect' \
|
||||
--blocklist-item 'ALooper\w*' \
|
||||
--blocklist-function 'ALooper\w*' \
|
||||
--blocklist-item 'AAsset\w*' \
|
||||
@@ -37,6 +33,29 @@ while read ARCH && read TARGET ; do
|
||||
-Igame-activity-csrc \
|
||||
--sysroot="$SYSROOT" --target=$TARGET
|
||||
|
||||
bindgen native-activity-ffi.h -o src/native_activity/ffi_$ARCH.rs \
|
||||
--blocklist-item 'JNI\w+' \
|
||||
--blocklist-item 'C?_?JNIEnv' \
|
||||
--blocklist-item '_?JavaVM' \
|
||||
--blocklist-item '_?j\w+' \
|
||||
--blocklist-item 'ALooper\w*' \
|
||||
--blocklist-function 'ALooper\w*' \
|
||||
--blocklist-item 'AAsset\w*' \
|
||||
--blocklist-item 'AAssetManager\w*' \
|
||||
--blocklist-function 'AAssetManager\w*' \
|
||||
--blocklist-item 'ANativeWindow\w*' \
|
||||
--blocklist-function 'ANativeWindow\w*' \
|
||||
--blocklist-item 'AConfiguration\w*' \
|
||||
--blocklist-function 'AConfiguration\w*' \
|
||||
--blocklist-function 'android_main' \
|
||||
--blocklist-item 'AInputQueue\w*' \
|
||||
--blocklist-function 'AInputQueue\w*' \
|
||||
--blocklist-item 'GameActivity_onCreate' \
|
||||
--blocklist-function 'GameActivity_onCreate_C' \
|
||||
--newtype-enum '\w+_(result|status)_t' \
|
||||
-- \
|
||||
-Inative-activity-csrc \
|
||||
--sysroot="$SYSROOT" --target=$TARGET
|
||||
done << EOF
|
||||
arm
|
||||
arm-linux-androideabi
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/resource.h>
|
||||
|
||||
#include "android_native_app_glue.h"
|
||||
#include <android/log.h>
|
||||
|
||||
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
|
||||
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__))
|
||||
|
||||
/* For debug builds, always enable the debug traces in this library */
|
||||
#ifndef NDEBUG
|
||||
# define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__))
|
||||
#else
|
||||
# define LOGV(...) ((void)0)
|
||||
#endif
|
||||
|
||||
static void free_saved_state(struct android_app* android_app) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
if (android_app->savedState != NULL) {
|
||||
free(android_app->savedState);
|
||||
android_app->savedState = NULL;
|
||||
android_app->savedStateSize = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
int8_t android_app_read_cmd(struct android_app* android_app) {
|
||||
int8_t cmd;
|
||||
if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) {
|
||||
switch (cmd) {
|
||||
case APP_CMD_SAVE_STATE:
|
||||
free_saved_state(android_app);
|
||||
break;
|
||||
}
|
||||
return cmd;
|
||||
} else {
|
||||
LOGE("No data on command pipe!");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void print_cur_config(struct android_app* android_app) {
|
||||
char lang[2], country[2];
|
||||
AConfiguration_getLanguage(android_app->config, lang);
|
||||
AConfiguration_getCountry(android_app->config, country);
|
||||
|
||||
LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
|
||||
"keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
|
||||
"modetype=%d modenight=%d",
|
||||
AConfiguration_getMcc(android_app->config),
|
||||
AConfiguration_getMnc(android_app->config),
|
||||
lang[0], lang[1], country[0], country[1],
|
||||
AConfiguration_getOrientation(android_app->config),
|
||||
AConfiguration_getTouchscreen(android_app->config),
|
||||
AConfiguration_getDensity(android_app->config),
|
||||
AConfiguration_getKeyboard(android_app->config),
|
||||
AConfiguration_getNavigation(android_app->config),
|
||||
AConfiguration_getKeysHidden(android_app->config),
|
||||
AConfiguration_getNavHidden(android_app->config),
|
||||
AConfiguration_getSdkVersion(android_app->config),
|
||||
AConfiguration_getScreenSize(android_app->config),
|
||||
AConfiguration_getScreenLong(android_app->config),
|
||||
AConfiguration_getUiModeType(android_app->config),
|
||||
AConfiguration_getUiModeNight(android_app->config));
|
||||
}
|
||||
|
||||
void android_app_attach_input_queue_looper(struct android_app* android_app) {
|
||||
if (android_app->inputQueue != NULL) {
|
||||
LOGV("Attaching input queue to looper");
|
||||
AInputQueue_attachLooper(android_app->inputQueue,
|
||||
android_app->looper, LOOPER_ID_INPUT, NULL,
|
||||
&android_app->inputPollSource);
|
||||
}
|
||||
}
|
||||
|
||||
void android_app_detach_input_queue_looper(struct android_app* android_app) {
|
||||
if (android_app->inputQueue != NULL) {
|
||||
LOGV("Detaching input queue from looper");
|
||||
AInputQueue_detachLooper(android_app->inputQueue);
|
||||
}
|
||||
}
|
||||
|
||||
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
|
||||
switch (cmd) {
|
||||
case APP_CMD_INPUT_CHANGED:
|
||||
LOGV("APP_CMD_INPUT_CHANGED\n");
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
if (android_app->inputQueue != NULL) {
|
||||
android_app_detach_input_queue_looper(android_app);
|
||||
}
|
||||
android_app->inputQueue = android_app->pendingInputQueue;
|
||||
if (android_app->inputQueue != NULL) {
|
||||
android_app_attach_input_queue_looper(android_app);
|
||||
}
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
break;
|
||||
|
||||
case APP_CMD_INIT_WINDOW:
|
||||
LOGV("APP_CMD_INIT_WINDOW\n");
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->window = android_app->pendingWindow;
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
break;
|
||||
|
||||
case APP_CMD_TERM_WINDOW:
|
||||
LOGV("APP_CMD_TERM_WINDOW\n");
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
break;
|
||||
|
||||
case APP_CMD_RESUME:
|
||||
case APP_CMD_START:
|
||||
case APP_CMD_PAUSE:
|
||||
case APP_CMD_STOP:
|
||||
LOGV("activityState=%d\n", cmd);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->activityState = cmd;
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
break;
|
||||
|
||||
case APP_CMD_CONFIG_CHANGED:
|
||||
LOGV("APP_CMD_CONFIG_CHANGED\n");
|
||||
AConfiguration_fromAssetManager(android_app->config,
|
||||
android_app->activity->assetManager);
|
||||
print_cur_config(android_app);
|
||||
break;
|
||||
|
||||
case APP_CMD_DESTROY:
|
||||
LOGV("APP_CMD_DESTROY\n");
|
||||
android_app->destroyRequested = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
|
||||
switch (cmd) {
|
||||
case APP_CMD_TERM_WINDOW:
|
||||
LOGV("APP_CMD_TERM_WINDOW\n");
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->window = NULL;
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
break;
|
||||
|
||||
case APP_CMD_SAVE_STATE:
|
||||
LOGV("APP_CMD_SAVE_STATE\n");
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->stateSaved = 1;
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
break;
|
||||
|
||||
case APP_CMD_RESUME:
|
||||
free_saved_state(android_app);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_dummy() {
|
||||
|
||||
}
|
||||
|
||||
static void android_app_destroy(struct android_app* android_app) {
|
||||
LOGV("android_app_destroy!");
|
||||
free_saved_state(android_app);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
if (android_app->inputQueue != NULL) {
|
||||
AInputQueue_detachLooper(android_app->inputQueue);
|
||||
}
|
||||
AConfiguration_delete(android_app->config);
|
||||
android_app->destroyed = 1;
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
// Can't touch android_app object after this.
|
||||
}
|
||||
|
||||
/*
|
||||
static void process_input(struct android_app* app, __attribute__((unused)) struct android_poll_source* source) {
|
||||
AInputEvent* event = NULL;
|
||||
while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
|
||||
LOGV("New input event: type=%d\n", AInputEvent_getType(event));
|
||||
if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
|
||||
continue;
|
||||
}
|
||||
int32_t handled = 0;
|
||||
if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
|
||||
AInputQueue_finishEvent(app->inputQueue, event, handled);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
static void process_cmd(struct android_app* app, __attribute__((unused)) struct android_poll_source* source) {
|
||||
int8_t cmd = android_app_read_cmd(app);
|
||||
android_app_pre_exec_cmd(app, cmd);
|
||||
if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
|
||||
android_app_post_exec_cmd(app, cmd);
|
||||
}
|
||||
|
||||
static void* android_app_entry(void* param) {
|
||||
struct android_app* android_app = (struct android_app*)param;
|
||||
|
||||
android_app->config = AConfiguration_new();
|
||||
AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager);
|
||||
|
||||
print_cur_config(android_app);
|
||||
|
||||
android_app->cmdPollSource.id = LOOPER_ID_MAIN;
|
||||
android_app->cmdPollSource.app = android_app;
|
||||
android_app->cmdPollSource.process = process_cmd;
|
||||
//android_app->inputPollSource.id = LOOPER_ID_INPUT;
|
||||
//android_app->inputPollSource.app = android_app;
|
||||
//android_app->inputPollSource.process = process_input;
|
||||
|
||||
ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
|
||||
ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
|
||||
&android_app->cmdPollSource);
|
||||
android_app->looper = looper;
|
||||
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->running = 1;
|
||||
pthread_cond_broadcast(&android_app->cond);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
|
||||
_rust_glue_entry(android_app);
|
||||
|
||||
android_app_destroy(android_app);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Native activity interaction (called from main thread)
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
static struct android_app* android_app_create(ANativeActivity* activity,
|
||||
void* savedState, size_t savedStateSize) {
|
||||
struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
|
||||
memset(android_app, 0, sizeof(struct android_app));
|
||||
android_app->activity = activity;
|
||||
|
||||
pthread_mutex_init(&android_app->mutex, NULL);
|
||||
pthread_cond_init(&android_app->cond, NULL);
|
||||
|
||||
if (savedState != NULL) {
|
||||
android_app->savedState = malloc(savedStateSize);
|
||||
android_app->savedStateSize = savedStateSize;
|
||||
memcpy(android_app->savedState, savedState, savedStateSize);
|
||||
}
|
||||
|
||||
int msgpipe[2];
|
||||
if (pipe(msgpipe)) {
|
||||
LOGE("could not create pipe: %s", strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
android_app->msgread = msgpipe[0];
|
||||
android_app->msgwrite = msgpipe[1];
|
||||
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
|
||||
|
||||
// Wait for thread to start.
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
while (!android_app->running) {
|
||||
pthread_cond_wait(&android_app->cond, &android_app->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
|
||||
return android_app;
|
||||
}
|
||||
|
||||
static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
|
||||
if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
|
||||
LOGE("Failure writing android_app cmd: %s\n", strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->pendingInputQueue = inputQueue;
|
||||
android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED);
|
||||
while (android_app->inputQueue != android_app->pendingInputQueue) {
|
||||
pthread_cond_wait(&android_app->cond, &android_app->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
if (android_app->pendingWindow != NULL) {
|
||||
android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
|
||||
}
|
||||
android_app->pendingWindow = window;
|
||||
if (window != NULL) {
|
||||
android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
|
||||
}
|
||||
while (android_app->window != android_app->pendingWindow) {
|
||||
pthread_cond_wait(&android_app->cond, &android_app->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app_write_cmd(android_app, cmd);
|
||||
while (android_app->activityState != cmd) {
|
||||
pthread_cond_wait(&android_app->cond, &android_app->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
static void android_app_free(struct android_app* android_app) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app_write_cmd(android_app, APP_CMD_DESTROY);
|
||||
while (!android_app->destroyed) {
|
||||
pthread_cond_wait(&android_app->cond, &android_app->mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
|
||||
close(android_app->msgread);
|
||||
close(android_app->msgwrite);
|
||||
pthread_cond_destroy(&android_app->cond);
|
||||
pthread_mutex_destroy(&android_app->mutex);
|
||||
free(android_app);
|
||||
}
|
||||
|
||||
static void onDestroy(ANativeActivity* activity) {
|
||||
LOGV("Destroy: %p\n", activity);
|
||||
android_app_free((struct android_app*)activity->instance);
|
||||
}
|
||||
|
||||
static void onStart(ANativeActivity* activity) {
|
||||
LOGV("Start: %p\n", activity);
|
||||
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START);
|
||||
}
|
||||
|
||||
static void onResume(ANativeActivity* activity) {
|
||||
LOGV("Resume: %p\n", activity);
|
||||
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME);
|
||||
}
|
||||
|
||||
static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
|
||||
struct android_app* android_app = (struct android_app*)activity->instance;
|
||||
void* savedState = NULL;
|
||||
|
||||
LOGV("SaveInstanceState: %p\n", activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->stateSaved = 0;
|
||||
android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
|
||||
while (!android_app->stateSaved) {
|
||||
pthread_cond_wait(&android_app->cond, &android_app->mutex);
|
||||
}
|
||||
|
||||
if (android_app->savedState != NULL) {
|
||||
savedState = android_app->savedState;
|
||||
*outLen = android_app->savedStateSize;
|
||||
android_app->savedState = NULL;
|
||||
android_app->savedStateSize = 0;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
|
||||
return savedState;
|
||||
}
|
||||
|
||||
static void onPause(ANativeActivity* activity) {
|
||||
LOGV("Pause: %p\n", activity);
|
||||
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE);
|
||||
}
|
||||
|
||||
static void onStop(ANativeActivity* activity) {
|
||||
LOGV("Stop: %p\n", activity);
|
||||
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP);
|
||||
}
|
||||
|
||||
static void onConfigurationChanged(ANativeActivity* activity) {
|
||||
struct android_app* android_app = (struct android_app*)activity->instance;
|
||||
LOGV("ConfigurationChanged: %p\n", activity);
|
||||
android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED);
|
||||
}
|
||||
|
||||
static void onLowMemory(ANativeActivity* activity) {
|
||||
struct android_app* android_app = (struct android_app*)activity->instance;
|
||||
LOGV("LowMemory: %p\n", activity);
|
||||
android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY);
|
||||
}
|
||||
|
||||
static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
|
||||
LOGV("WindowFocusChanged: %p -- %d\n", activity, focused);
|
||||
android_app_write_cmd((struct android_app*)activity->instance,
|
||||
focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
|
||||
}
|
||||
|
||||
static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
|
||||
LOGV("NativeWindowCreated: %p -- %p\n", activity, window);
|
||||
android_app_set_window((struct android_app*)activity->instance, window);
|
||||
}
|
||||
|
||||
static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
|
||||
LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window);
|
||||
android_app_set_window((struct android_app*)activity->instance, NULL);
|
||||
}
|
||||
|
||||
static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
|
||||
LOGV("InputQueueCreated: %p -- %p\n", activity, queue);
|
||||
android_app_set_input((struct android_app*)activity->instance, queue);
|
||||
}
|
||||
|
||||
static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
|
||||
LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue);
|
||||
android_app_set_input((struct android_app*)activity->instance, NULL);
|
||||
}
|
||||
|
||||
JNIEXPORT
|
||||
void ANativeActivity_onCreate_C(ANativeActivity* activity, void* savedState,
|
||||
size_t savedStateSize) {
|
||||
LOGV("Creating: %p\n", activity);
|
||||
activity->callbacks->onDestroy = onDestroy;
|
||||
activity->callbacks->onStart = onStart;
|
||||
activity->callbacks->onResume = onResume;
|
||||
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
|
||||
activity->callbacks->onPause = onPause;
|
||||
activity->callbacks->onStop = onStop;
|
||||
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
|
||||
activity->callbacks->onLowMemory = onLowMemory;
|
||||
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
|
||||
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
|
||||
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
|
||||
activity->callbacks->onInputQueueCreated = onInputQueueCreated;
|
||||
activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
|
||||
|
||||
activity->instance = android_app_create(activity, savedState, savedStateSize);
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ANDROID_NATIVE_APP_GLUE_H
|
||||
#define _ANDROID_NATIVE_APP_GLUE_H
|
||||
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
|
||||
#include <android/configuration.h>
|
||||
#include <android/looper.h>
|
||||
#include <android/native_activity.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The native activity interface provided by <android/native_activity.h>
|
||||
* is based on a set of application-provided callbacks that will be called
|
||||
* by the Activity's main thread when certain events occur.
|
||||
*
|
||||
* This means that each one of this callbacks _should_ _not_ block, or they
|
||||
* risk having the system force-close the application. This programming
|
||||
* model is direct, lightweight, but constraining.
|
||||
*
|
||||
* The 'android_native_app_glue' static library is used to provide a different
|
||||
* execution model where the application can implement its own main event
|
||||
* loop in a different thread instead. Here's how it works:
|
||||
*
|
||||
* 1/ The application must provide a function named "android_main()" that
|
||||
* will be called when the activity is created, in a new thread that is
|
||||
* distinct from the activity's main thread.
|
||||
*
|
||||
* 2/ android_main() receives a pointer to a valid "android_app" structure
|
||||
* that contains references to other important objects, e.g. the
|
||||
* ANativeActivity obejct instance the application is running in.
|
||||
*
|
||||
* 3/ the "android_app" object holds an ALooper instance that already
|
||||
* listens to two important things:
|
||||
*
|
||||
* - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX
|
||||
* declarations below.
|
||||
*
|
||||
* - input events coming from the AInputQueue attached to the activity.
|
||||
*
|
||||
* Each of these correspond to an ALooper identifier returned by
|
||||
* ALooper_pollOnce with values of LOOPER_ID_MAIN and LOOPER_ID_INPUT,
|
||||
* respectively.
|
||||
*
|
||||
* Your application can use the same ALooper to listen to additional
|
||||
* file-descriptors. They can either be callback based, or with return
|
||||
* identifiers starting with LOOPER_ID_USER.
|
||||
*
|
||||
* 4/ Whenever you receive a LOOPER_ID_MAIN or LOOPER_ID_INPUT event,
|
||||
* the returned data will point to an android_poll_source structure. You
|
||||
* can call the process() function on it, and fill in android_app->onAppCmd
|
||||
* and android_app->onInputEvent to be called for your own processing
|
||||
* of the event.
|
||||
*
|
||||
* Alternatively, you can call the low-level functions to read and process
|
||||
* the data directly... look at the process_cmd() and process_input()
|
||||
* implementations in the glue to see how to do this.
|
||||
*
|
||||
* See the sample named "native-activity" that comes with the NDK with a
|
||||
* full usage example. Also look at the JavaDoc of NativeActivity.
|
||||
*/
|
||||
|
||||
struct android_app;
|
||||
|
||||
/**
|
||||
* Data associated with an ALooper fd that will be returned as the "outData"
|
||||
* when that source has data ready.
|
||||
*/
|
||||
struct android_poll_source {
|
||||
// The identifier of this source. May be LOOPER_ID_MAIN or
|
||||
// LOOPER_ID_INPUT.
|
||||
int32_t id;
|
||||
|
||||
// The android_app this ident is associated with.
|
||||
struct android_app* app;
|
||||
|
||||
// Function to call to perform the standard processing of data from
|
||||
// this source.
|
||||
void (*process)(struct android_app* app, struct android_poll_source* source);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the interface for the standard glue code of a threaded
|
||||
* application. In this model, the application's code is running
|
||||
* in its own thread separate from the main thread of the process.
|
||||
* It is not required that this thread be associated with the Java
|
||||
* VM, although it will need to be in order to make JNI calls any
|
||||
* Java objects.
|
||||
*/
|
||||
struct android_app {
|
||||
// The application can place a pointer to its own state object
|
||||
// here if it likes.
|
||||
void* userData;
|
||||
|
||||
// Fill this in with the function to process main app commands (APP_CMD_*)
|
||||
void (*onAppCmd)(struct android_app* app, int32_t cmd);
|
||||
|
||||
// Fill this in with the function to process input events. At this point
|
||||
// the event has already been pre-dispatched, and it will be finished upon
|
||||
// return. Return 1 if you have handled the event, 0 for any default
|
||||
// dispatching.
|
||||
int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
|
||||
|
||||
// The ANativeActivity object instance that this app is running in.
|
||||
ANativeActivity* activity;
|
||||
|
||||
// The current configuration the app is running in.
|
||||
AConfiguration* config;
|
||||
|
||||
// This is the last instance's saved state, as provided at creation time.
|
||||
// It is NULL if there was no state. You can use this as you need; the
|
||||
// memory will remain around until you call android_app_exec_cmd() for
|
||||
// APP_CMD_RESUME, at which point it will be freed and savedState set to NULL.
|
||||
// These variables should only be changed when processing a APP_CMD_SAVE_STATE,
|
||||
// at which point they will be initialized to NULL and you can malloc your
|
||||
// state and place the information here. In that case the memory will be
|
||||
// freed for you later.
|
||||
void* savedState;
|
||||
size_t savedStateSize;
|
||||
|
||||
// The ALooper associated with the app's thread.
|
||||
ALooper* looper;
|
||||
|
||||
// When non-NULL, this is the input queue from which the app will
|
||||
// receive user input events.
|
||||
AInputQueue* inputQueue;
|
||||
|
||||
// When non-NULL, this is the window surface that the app can draw in.
|
||||
ANativeWindow* window;
|
||||
|
||||
// Current content rectangle of the window; this is the area where the
|
||||
// window's content should be placed to be seen by the user.
|
||||
ARect contentRect;
|
||||
|
||||
// Current state of the app's activity. May be either APP_CMD_START,
|
||||
// APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below.
|
||||
int activityState;
|
||||
|
||||
// This is non-zero when the application's NativeActivity is being
|
||||
// destroyed and waiting for the app thread to complete.
|
||||
int destroyRequested;
|
||||
|
||||
// -------------------------------------------------
|
||||
// Below are "private" implementation of the glue code.
|
||||
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
|
||||
int msgread;
|
||||
int msgwrite;
|
||||
|
||||
pthread_t thread;
|
||||
|
||||
struct android_poll_source cmdPollSource;
|
||||
struct android_poll_source inputPollSource;
|
||||
|
||||
int running;
|
||||
int stateSaved;
|
||||
int destroyed;
|
||||
int redrawNeeded;
|
||||
AInputQueue* pendingInputQueue;
|
||||
ANativeWindow* pendingWindow;
|
||||
ARect pendingContentRect;
|
||||
};
|
||||
|
||||
enum {
|
||||
/**
|
||||
* Looper data ID of commands coming from the app's main thread, which
|
||||
* is returned as an identifier from ALooper_pollOnce(). The data for this
|
||||
* identifier is a pointer to an android_poll_source structure.
|
||||
* These can be retrieved and processed with android_app_read_cmd()
|
||||
* and android_app_exec_cmd().
|
||||
*/
|
||||
LOOPER_ID_MAIN = 1,
|
||||
|
||||
/**
|
||||
* Looper data ID of events coming from the AInputQueue of the
|
||||
* application's window, which is returned as an identifier from
|
||||
* ALooper_pollOnce(). The data for this identifier is a pointer to an
|
||||
* android_poll_source structure. These can be read via the inputQueue
|
||||
* object of android_app.
|
||||
*/
|
||||
LOOPER_ID_INPUT = 2,
|
||||
|
||||
/**
|
||||
* Start of user-defined ALooper identifiers.
|
||||
*/
|
||||
LOOPER_ID_USER = 3,
|
||||
};
|
||||
|
||||
enum {
|
||||
/**
|
||||
* Command from main thread: the AInputQueue has changed. Upon processing
|
||||
* this command, android_app->inputQueue will be updated to the new queue
|
||||
* (or NULL).
|
||||
*/
|
||||
APP_CMD_INPUT_CHANGED,
|
||||
|
||||
/**
|
||||
* Command from main thread: a new ANativeWindow is ready for use. Upon
|
||||
* receiving this command, android_app->window will contain the new window
|
||||
* surface.
|
||||
*/
|
||||
APP_CMD_INIT_WINDOW,
|
||||
|
||||
/**
|
||||
* Command from main thread: the existing ANativeWindow needs to be
|
||||
* terminated. Upon receiving this command, android_app->window still
|
||||
* contains the existing window; after calling android_app_exec_cmd
|
||||
* it will be set to NULL.
|
||||
*/
|
||||
APP_CMD_TERM_WINDOW,
|
||||
|
||||
/**
|
||||
* Command from main thread: the current ANativeWindow has been resized.
|
||||
* Please redraw with its new size.
|
||||
*/
|
||||
APP_CMD_WINDOW_RESIZED,
|
||||
|
||||
/**
|
||||
* Command from main thread: the system needs that the current ANativeWindow
|
||||
* be redrawn. You should redraw the window before handing this to
|
||||
* android_app_exec_cmd() in order to avoid transient drawing glitches.
|
||||
*/
|
||||
APP_CMD_WINDOW_REDRAW_NEEDED,
|
||||
|
||||
/**
|
||||
* Command from main thread: the content area of the window has changed,
|
||||
* such as from the soft input window being shown or hidden. You can
|
||||
* find the new content rect in android_app::contentRect.
|
||||
*/
|
||||
APP_CMD_CONTENT_RECT_CHANGED,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity window has gained
|
||||
* input focus.
|
||||
*/
|
||||
APP_CMD_GAINED_FOCUS,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity window has lost
|
||||
* input focus.
|
||||
*/
|
||||
APP_CMD_LOST_FOCUS,
|
||||
|
||||
/**
|
||||
* Command from main thread: the current device configuration has changed.
|
||||
*/
|
||||
APP_CMD_CONFIG_CHANGED,
|
||||
|
||||
/**
|
||||
* Command from main thread: the system is running low on memory.
|
||||
* Try to reduce your memory use.
|
||||
*/
|
||||
APP_CMD_LOW_MEMORY,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity has been started.
|
||||
*/
|
||||
APP_CMD_START,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity has been resumed.
|
||||
*/
|
||||
APP_CMD_RESUME,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app should generate a new saved state
|
||||
* for itself, to restore from later if needed. If you have saved state,
|
||||
* allocate it with malloc and place it in android_app.savedState with
|
||||
* the size in android_app.savedStateSize. The will be freed for you
|
||||
* later.
|
||||
*/
|
||||
APP_CMD_SAVE_STATE,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity has been paused.
|
||||
*/
|
||||
APP_CMD_PAUSE,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity has been stopped.
|
||||
*/
|
||||
APP_CMD_STOP,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity is being destroyed,
|
||||
* and waiting for the app thread to clean up and exit before proceeding.
|
||||
*/
|
||||
APP_CMD_DESTROY,
|
||||
};
|
||||
|
||||
/**
|
||||
* Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
|
||||
* app command message.
|
||||
*/
|
||||
int8_t android_app_read_cmd(struct android_app* android_app);
|
||||
|
||||
/**
|
||||
* Call with the command returned by android_app_read_cmd() to do the
|
||||
* initial pre-processing of the given command. You can perform your own
|
||||
* actions for the command after calling this function.
|
||||
*/
|
||||
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
|
||||
|
||||
/**
|
||||
* Call with the command returned by android_app_read_cmd() to do the
|
||||
* final post-processing of the given command. You must have done your own
|
||||
* actions for the command before calling this function.
|
||||
*/
|
||||
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
|
||||
|
||||
void android_app_attach_input_queue_looper(struct android_app* android_app);
|
||||
void android_app_detach_input_queue_looper(struct android_app* android_app);
|
||||
|
||||
/**
|
||||
* Dummy function that used to be used to prevent the linker from stripping app
|
||||
* glue code. No longer necessary, since __attribute__((visibility("default")))
|
||||
* does this for us.
|
||||
*/
|
||||
__attribute__((
|
||||
deprecated("Calls to app_dummy are no longer necessary. See "
|
||||
"https://github.com/android-ndk/ndk/issues/381."))) void
|
||||
app_dummy();
|
||||
|
||||
/**
|
||||
* This is the function that application code must implement, representing
|
||||
* the main entry to the app.
|
||||
*/
|
||||
extern void _rust_glue_entry(struct android_app* app);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _ANDROID_NATIVE_APP_GLUE_H */
|
||||
@@ -6,13 +6,6 @@ use ndk::configuration::{
|
||||
ScreenSize, Touchscreen, UiModeNight, UiModeType,
|
||||
};
|
||||
|
||||
/// A (cheaply clonable) reference to this application's [`ndk::configuration::Configuration`]
|
||||
///
|
||||
/// This provides a thread-safe way to access the latest configuration state for
|
||||
/// an application without deeply copying the large [`ndk::configuration::Configuration`] struct.
|
||||
///
|
||||
/// If the application is notified of configuration changes then those changes
|
||||
/// will become visible via pre-existing configuration references.
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigurationRef {
|
||||
config: Arc<RwLock<Configuration>>,
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use jni_sys::*;
|
||||
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t, size_t};
|
||||
use ndk_sys::{AAssetManager, AConfiguration, ALooper, ALooper_callbackFunc, ANativeWindow, ARect};
|
||||
use ndk_sys::AAssetManager;
|
||||
use ndk_sys::ANativeWindow;
|
||||
use ndk_sys::{AConfiguration, ALooper, ALooper_callbackFunc};
|
||||
|
||||
#[cfg(all(
|
||||
any(target_os = "android", feature = "test"),
|
||||
|
||||
@@ -13,19 +13,91 @@
|
||||
// The `Class` was also bound differently to `android-ndk-rs` considering how the class is defined
|
||||
// by masking bits from the `Source`.
|
||||
|
||||
use crate::game_activity::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use std::{convert::TryInto, ops::Deref};
|
||||
|
||||
use crate::game_activity::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
|
||||
use crate::input::{Class, Source};
|
||||
use bitflags::bitflags;
|
||||
|
||||
// Note: try to keep this wrapper API compatible with the AInputEvent API if possible
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum InputEvent<'a> {
|
||||
MotionEvent(MotionEvent<'a>),
|
||||
KeyEvent(KeyEvent<'a>),
|
||||
pub enum InputEvent {
|
||||
MotionEvent(MotionEvent),
|
||||
KeyEvent(KeyEvent),
|
||||
}
|
||||
|
||||
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
|
||||
///
|
||||
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_ANY)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(i32)]
|
||||
pub enum Source {
|
||||
BluetoothStylus = 0x0000c002,
|
||||
Dpad = 0x00000201,
|
||||
/// Either a gamepad or a joystick
|
||||
Gamepad = 0x00000401,
|
||||
Hdmi = 0x02000001,
|
||||
/// Either a gamepad or a joystick
|
||||
Joystick = 0x01000010,
|
||||
/// Pretty much any device with buttons. Query the keyboard type to determine
|
||||
/// if it has alphabetic keys and can be used for text entry.
|
||||
Keyboard = 0x00000101,
|
||||
/// A pointing device, such as a mouse or trackpad
|
||||
Mouse = 0x00002002,
|
||||
/// A pointing device, such as a mouse or trackpad whose relative motions should be treated as navigation events
|
||||
MouseRelative = 0x00020004,
|
||||
/// An input device akin to a scroll wheel
|
||||
RotaryEncoder = 0x00400000,
|
||||
Sensor = 0x04000000,
|
||||
Stylus = 0x00004002,
|
||||
Touchpad = 0x00100008,
|
||||
Touchscreen = 0x00001002,
|
||||
TouchNavigation = 0x00200000,
|
||||
Trackball = 0x00010004,
|
||||
|
||||
Unknown = 0,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
struct SourceFlags: u32 {
|
||||
const CLASS_MASK = 0x000000ff;
|
||||
|
||||
const BUTTON = 0x00000001;
|
||||
const POINTER = 0x00000002;
|
||||
const TRACKBALL = 0x00000004;
|
||||
const POSITION = 0x00000008;
|
||||
const JOYSTICK = 0x00000010;
|
||||
const NONE = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum representing the class of a [`MotionEvent`] or [`KeyEvent`] source
|
||||
///
|
||||
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_CLASS_MASK)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Class {
|
||||
None,
|
||||
Button,
|
||||
Pointer,
|
||||
Trackball,
|
||||
Position,
|
||||
Joystick,
|
||||
}
|
||||
|
||||
impl From<i32> for Class {
|
||||
fn from(source: i32) -> Self {
|
||||
let class = SourceFlags::from_bits_truncate(source as u32);
|
||||
match class {
|
||||
SourceFlags::BUTTON => Class::Button,
|
||||
SourceFlags::POINTER => Class::Pointer,
|
||||
SourceFlags::TRACKBALL => Class::Trackball,
|
||||
SourceFlags::POSITION => Class::Position,
|
||||
SourceFlags::JOYSTICK => Class::Joystick,
|
||||
_ => Class::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bitfield representing the state of modifier keys during an event.
|
||||
@@ -110,15 +182,15 @@ impl MetaState {
|
||||
/// For general discussion of motion events in Android, see [the relevant
|
||||
/// javadoc](https://developer.android.com/reference/android/view/MotionEvent).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MotionEvent<'a> {
|
||||
ga_event: &'a GameActivityMotionEvent,
|
||||
pub struct MotionEvent {
|
||||
ga_event: GameActivityMotionEvent,
|
||||
}
|
||||
|
||||
impl<'a> Deref for MotionEvent<'a> {
|
||||
impl Deref for MotionEvent {
|
||||
type Target = GameActivityMotionEvent;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.ga_event
|
||||
&self.ga_event
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,20 +269,6 @@ pub enum Axis {
|
||||
Generic16 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_16,
|
||||
}
|
||||
|
||||
/// The tool type of a pointer.
|
||||
///
|
||||
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-48)
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum ToolType {
|
||||
Unknown = ndk_sys::AMOTION_EVENT_TOOL_TYPE_UNKNOWN,
|
||||
Finger = ndk_sys::AMOTION_EVENT_TOOL_TYPE_FINGER,
|
||||
Stylus = ndk_sys::AMOTION_EVENT_TOOL_TYPE_STYLUS,
|
||||
Mouse = ndk_sys::AMOTION_EVENT_TOOL_TYPE_MOUSE,
|
||||
Eraser = ndk_sys::AMOTION_EVENT_TOOL_TYPE_ERASER,
|
||||
Palm = ndk_sys::AMOTION_EVENT_TOOL_TYPE_PALM,
|
||||
}
|
||||
|
||||
/// A bitfield representing the state of buttons during a motion event.
|
||||
///
|
||||
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-33)
|
||||
@@ -286,8 +344,8 @@ impl MotionEventFlags {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MotionEvent<'a> {
|
||||
pub(crate) fn new(ga_event: &'a GameActivityMotionEvent) -> Self {
|
||||
impl MotionEvent {
|
||||
pub(crate) fn new(ga_event: GameActivityMotionEvent) -> Self {
|
||||
Self { ga_event }
|
||||
}
|
||||
|
||||
@@ -295,15 +353,14 @@ impl<'a> MotionEvent<'a> {
|
||||
///
|
||||
#[inline]
|
||||
pub fn source(&self) -> Source {
|
||||
let source = self.source as u32;
|
||||
source.try_into().unwrap_or(Source::Unknown)
|
||||
self.source.try_into().unwrap_or(Source::Unknown)
|
||||
}
|
||||
|
||||
/// Get the class of the event source.
|
||||
///
|
||||
#[inline]
|
||||
pub fn class(&self) -> Class {
|
||||
Class::from(self.source())
|
||||
Class::from(self.source)
|
||||
}
|
||||
|
||||
/// Get the device id associated with the event.
|
||||
@@ -332,7 +389,7 @@ impl<'a> MotionEvent<'a> {
|
||||
/// or [`PointerDown`](MotionAction::PointerDown).
|
||||
#[inline]
|
||||
pub fn pointer_index(&self) -> usize {
|
||||
let action = self.action as u32;
|
||||
let action = self.action as u32 & ndk_sys::AMOTION_EVENT_ACTION_MASK;
|
||||
let index = (action & ndk_sys::AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
|
||||
>> ndk_sys::AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
|
||||
index as usize
|
||||
@@ -498,7 +555,7 @@ impl<'a> MotionEvent<'a> {
|
||||
/// A view into the data of a specific pointer in a motion event.
|
||||
#[derive(Debug)]
|
||||
pub struct Pointer<'a> {
|
||||
event: &'a MotionEvent<'a>,
|
||||
event: &'a MotionEvent,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
@@ -576,19 +633,12 @@ impl<'a> Pointer<'a> {
|
||||
pub fn touch_minor(&self) -> f32 {
|
||||
self.axis_value(Axis::TouchMinor)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tool_type(&self) -> ToolType {
|
||||
let pointer = &self.event.pointers[self.index];
|
||||
let tool_type = pointer.toolType as u32;
|
||||
tool_type.try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the pointers in a [`MotionEvent`].
|
||||
#[derive(Debug)]
|
||||
pub struct PointersIter<'a> {
|
||||
event: &'a MotionEvent<'a>,
|
||||
event: &'a MotionEvent,
|
||||
next_index: usize,
|
||||
count: usize,
|
||||
}
|
||||
@@ -933,15 +983,15 @@ impl ExactSizeIterator for HistoricalPointersIter<'_> {
|
||||
/// For general discussion of key events in Android, see [the relevant
|
||||
/// javadoc](https://developer.android.com/reference/android/view/KeyEvent).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeyEvent<'a> {
|
||||
ga_event: &'a GameActivityKeyEvent,
|
||||
pub struct KeyEvent {
|
||||
ga_event: GameActivityKeyEvent,
|
||||
}
|
||||
|
||||
impl<'a> Deref for KeyEvent<'a> {
|
||||
impl Deref for KeyEvent {
|
||||
type Target = GameActivityKeyEvent;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.ga_event
|
||||
&self.ga_event
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1253,8 +1303,8 @@ pub enum Keycode {
|
||||
ProfileSwitch = ndk_sys::AKEYCODE_PROFILE_SWITCH,
|
||||
}
|
||||
|
||||
impl<'a> KeyEvent<'a> {
|
||||
pub(crate) fn new(ga_event: &'a GameActivityKeyEvent) -> Self {
|
||||
impl KeyEvent {
|
||||
pub(crate) fn new(ga_event: GameActivityKeyEvent) -> Self {
|
||||
Self { ga_event }
|
||||
}
|
||||
|
||||
@@ -1262,15 +1312,14 @@ impl<'a> KeyEvent<'a> {
|
||||
///
|
||||
#[inline]
|
||||
pub fn source(&self) -> Source {
|
||||
let source = self.source as u32;
|
||||
source.try_into().unwrap_or(Source::Unknown)
|
||||
self.source.try_into().unwrap_or(Source::Unknown)
|
||||
}
|
||||
|
||||
/// Get the class of the event source.
|
||||
///
|
||||
#[inline]
|
||||
pub fn class(&self) -> Class {
|
||||
Class::from(self.source())
|
||||
Class::from(self.source)
|
||||
}
|
||||
|
||||
/// Get the device id associated with the event.
|
||||
@@ -1391,7 +1440,7 @@ impl KeyEventFlags {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> KeyEvent<'a> {
|
||||
impl KeyEvent {
|
||||
/// Flags associated with this [`KeyEvent`].
|
||||
///
|
||||
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getflags)
|
||||
|
||||
@@ -5,14 +5,13 @@ use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::os::raw;
|
||||
use std::os::unix::prelude::*;
|
||||
use std::panic::catch_unwind;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::{ptr, thread};
|
||||
|
||||
use libc::c_void;
|
||||
use log::{error, trace, Level};
|
||||
|
||||
use jni_sys::*;
|
||||
@@ -24,10 +23,7 @@ use ndk::asset::AssetManager;
|
||||
use ndk::configuration::Configuration;
|
||||
use ndk::native_window::NativeWindow;
|
||||
|
||||
use crate::util::{abort_on_panic, android_log, log_panic};
|
||||
use crate::{
|
||||
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
|
||||
};
|
||||
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
|
||||
|
||||
mod ffi;
|
||||
|
||||
@@ -52,14 +48,14 @@ impl<'a> StateSaver<'a> {
|
||||
|
||||
// In case the application calls store() multiple times for some reason we
|
||||
// make sure to free any pre-existing state...
|
||||
if !(*app_ptr).savedState.is_null() {
|
||||
if (*app_ptr).savedState != ptr::null_mut() {
|
||||
libc::free((*app_ptr).savedState);
|
||||
(*app_ptr).savedState = ptr::null_mut();
|
||||
(*app_ptr).savedStateSize = 0;
|
||||
}
|
||||
|
||||
let buf = libc::malloc(state.len());
|
||||
if buf.is_null() {
|
||||
if buf == ptr::null_mut() {
|
||||
panic!("Failed to allocate save_state buffer");
|
||||
}
|
||||
|
||||
@@ -73,7 +69,7 @@ impl<'a> StateSaver<'a> {
|
||||
}
|
||||
|
||||
(*app_ptr).savedState = buf;
|
||||
(*app_ptr).savedStateSize = state.len() as _;
|
||||
(*app_ptr).savedStateSize = state.len() as ffi::size_t;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,7 +82,7 @@ impl<'a> StateLoader<'a> {
|
||||
pub fn load(&self) -> Option<Vec<u8>> {
|
||||
unsafe {
|
||||
let app_ptr = self.app.native_app.as_ptr();
|
||||
if !(*app_ptr).savedState.is_null() && (*app_ptr).savedStateSize > 0 {
|
||||
if (*app_ptr).savedState != ptr::null_mut() && (*app_ptr).savedStateSize > 0 {
|
||||
let buf: &mut [u8] = std::slice::from_raw_parts_mut(
|
||||
(*app_ptr).savedState.cast(),
|
||||
(*app_ptr).savedStateSize as usize,
|
||||
@@ -157,17 +153,7 @@ pub struct AndroidAppInner {
|
||||
}
|
||||
|
||||
impl AndroidAppInner {
|
||||
pub fn vm_as_ptr(&self) -> *mut c_void {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
unsafe { (*(*app_ptr).activity).vm as _ }
|
||||
}
|
||||
|
||||
pub fn activity_as_ptr(&self) -> *mut c_void {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
unsafe { (*(*app_ptr).activity).javaGameActivity as _ }
|
||||
}
|
||||
|
||||
pub fn native_window(&self) -> Option<NativeWindow> {
|
||||
pub fn native_window<'a>(&self) -> Option<NativeWindow> {
|
||||
self.native_window.read().unwrap().clone()
|
||||
}
|
||||
|
||||
@@ -226,7 +212,7 @@ impl AndroidAppInner {
|
||||
ffi::NativeAppGlueLooperId_LOOPER_ID_MAIN => {
|
||||
trace!("ALooper_pollAll returned ID_MAIN");
|
||||
let source: *mut ffi::android_poll_source = source.cast();
|
||||
if !source.is_null() {
|
||||
if source != ptr::null_mut() {
|
||||
let cmd_i = ffi::android_app_read_cmd(native_app.as_ptr());
|
||||
|
||||
let cmd = match cmd_i as u32 {
|
||||
@@ -260,11 +246,11 @@ impl AndroidAppInner {
|
||||
}
|
||||
ffi::NativeAppGlueAppCmd_APP_CMD_START => MainEvent::Start,
|
||||
ffi::NativeAppGlueAppCmd_APP_CMD_RESUME => MainEvent::Resume {
|
||||
loader: StateLoader { app: self },
|
||||
loader: StateLoader { app: &self },
|
||||
},
|
||||
ffi::NativeAppGlueAppCmd_APP_CMD_SAVE_STATE => {
|
||||
MainEvent::SaveState {
|
||||
saver: StateSaver { app: self },
|
||||
saver: StateSaver { app: &self },
|
||||
}
|
||||
}
|
||||
ffi::NativeAppGlueAppCmd_APP_CMD_PAUSE => MainEvent::Pause,
|
||||
@@ -403,25 +389,23 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_events<F>(&self, mut callback: F)
|
||||
pub fn input_events<'b, F>(&self, mut callback: F)
|
||||
where
|
||||
F: FnMut(&InputEvent) -> InputStatus,
|
||||
F: FnMut(&InputEvent),
|
||||
{
|
||||
let buf = unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
|
||||
if input_buffer.is_null() {
|
||||
if input_buffer == ptr::null_mut() {
|
||||
return;
|
||||
}
|
||||
InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer))
|
||||
};
|
||||
|
||||
let mut keys_iter = KeyEventsLendingIterator::new(&buf);
|
||||
while let Some(key_event) = keys_iter.next() {
|
||||
for key_event in buf.key_events_iter() {
|
||||
callback(&InputEvent::KeyEvent(key_event));
|
||||
}
|
||||
let mut motion_iter = MotionEventsLendingIterator::new(&buf);
|
||||
while let Some(motion_event) = motion_iter.next() {
|
||||
for motion_event in buf.motion_events_iter() {
|
||||
callback(&InputEvent::MotionEvent(motion_event));
|
||||
}
|
||||
}
|
||||
@@ -448,58 +432,46 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
|
||||
struct MotionEventsLendingIterator<'a> {
|
||||
struct MotionEventsIterator<'a> {
|
||||
pos: usize,
|
||||
count: usize,
|
||||
buffer: &'a InputBuffer<'a>,
|
||||
}
|
||||
|
||||
// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
|
||||
// via a generic trait. The iteration of motion events is entirely private
|
||||
// though so this is ok for now.
|
||||
impl<'a> MotionEventsLendingIterator<'a> {
|
||||
fn new(buffer: &'a InputBuffer<'a>) -> Self {
|
||||
Self {
|
||||
pos: 0,
|
||||
count: buffer.motion_events_count(),
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
fn next(&mut self) -> Option<MotionEvent<'a>> {
|
||||
impl<'a> Iterator for MotionEventsIterator<'a> {
|
||||
type Item = MotionEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.count {
|
||||
let ga_event = unsafe { &(*self.buffer.ptr.as_ptr()).motionEvents[self.pos] };
|
||||
let event = MotionEvent::new(ga_event);
|
||||
self.pos += 1;
|
||||
Some(event)
|
||||
unsafe {
|
||||
let ga_event = (*self.buffer.ptr.as_ptr()).motionEvents[self.pos];
|
||||
let event = MotionEvent::new(ga_event);
|
||||
self.pos += 1;
|
||||
Some(event)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyEventsLendingIterator<'a> {
|
||||
struct KeyEventsIterator<'a> {
|
||||
pos: usize,
|
||||
count: usize,
|
||||
buffer: &'a InputBuffer<'a>,
|
||||
}
|
||||
|
||||
// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
|
||||
// via a generic trait. The iteration of key events is entirely private
|
||||
// though so this is ok for now.
|
||||
impl<'a> KeyEventsLendingIterator<'a> {
|
||||
fn new(buffer: &'a InputBuffer<'a>) -> Self {
|
||||
Self {
|
||||
pos: 0,
|
||||
count: buffer.key_events_count(),
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
fn next(&mut self) -> Option<KeyEvent<'a>> {
|
||||
impl<'a> Iterator for KeyEventsIterator<'a> {
|
||||
type Item = KeyEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.count {
|
||||
let ga_event = unsafe { &(*self.buffer.ptr.as_ptr()).keyEvents[self.pos] };
|
||||
let event = KeyEvent::new(ga_event);
|
||||
self.pos += 1;
|
||||
Some(event)
|
||||
unsafe {
|
||||
let ga_event = (*self.buffer.ptr.as_ptr()).keyEvents[self.pos];
|
||||
let event = KeyEvent::new(ga_event);
|
||||
self.pos += 1;
|
||||
Some(event)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -519,12 +491,29 @@ impl<'a> InputBuffer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn motion_events_count(&self) -> usize {
|
||||
unsafe { (*self.ptr.as_ptr()).motionEventsCount as usize }
|
||||
// XXX: It's really not ideal here that Rust iterators can't yield values
|
||||
// that borrow from the iterator, so we implicitly have to copy the
|
||||
// events as we iterate...
|
||||
pub fn motion_events_iter<'b>(&'b self) -> MotionEventsIterator<'b> {
|
||||
unsafe {
|
||||
let count = (*self.ptr.as_ptr()).motionEventsCount as usize;
|
||||
MotionEventsIterator {
|
||||
pos: 0,
|
||||
count,
|
||||
buffer: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_events_count(&self) -> usize {
|
||||
unsafe { (*self.ptr.as_ptr()).keyEventsCount as usize }
|
||||
pub fn key_events_iter<'b>(&'b self) -> KeyEventsIterator<'b> {
|
||||
unsafe {
|
||||
let count = (*self.ptr.as_ptr()).keyEventsCount as usize;
|
||||
KeyEventsIterator {
|
||||
pos: 0,
|
||||
count,
|
||||
buffer: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +547,7 @@ extern "C" {
|
||||
pub fn GameActivity_onCreate_C(
|
||||
activity: *mut ffi::GameActivity,
|
||||
savedState: *mut ::std::os::raw::c_void,
|
||||
savedStateSize: libc::size_t,
|
||||
savedStateSize: ffi::size_t,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -591,11 +580,24 @@ pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_loadNativeC
|
||||
pub unsafe extern "C" fn GameActivity_onCreate(
|
||||
activity: *mut ffi::GameActivity,
|
||||
saved_state: *mut ::std::os::raw::c_void,
|
||||
saved_state_size: libc::size_t,
|
||||
saved_state_size: ffi::size_t,
|
||||
) {
|
||||
GameActivity_onCreate_C(activity, saved_state, saved_state_size);
|
||||
}
|
||||
|
||||
fn android_log(level: Level, tag: &CStr, msg: &CStr) {
|
||||
let prio = match level {
|
||||
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
|
||||
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
|
||||
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
|
||||
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
|
||||
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
|
||||
};
|
||||
unsafe {
|
||||
ndk_sys::__android_log_write(prio.0 as raw::c_int, tag.as_ptr(), msg.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
extern "Rust" {
|
||||
pub fn android_main(app: AndroidApp);
|
||||
}
|
||||
@@ -604,76 +606,59 @@ extern "Rust" {
|
||||
// `app_main` function. This is run on a dedicated thread spawned
|
||||
// by android_native_app_glue.
|
||||
#[no_mangle]
|
||||
#[allow(unused_unsafe)] // Otherwise rust 1.64 moans about using unsafe{} in unsafe functions
|
||||
pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
|
||||
abort_on_panic(|| {
|
||||
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
|
||||
let mut logpipe: [RawFd; 2] = Default::default();
|
||||
libc::pipe(logpipe.as_mut_ptr());
|
||||
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
|
||||
libc::dup2(logpipe[1], libc::STDERR_FILENO);
|
||||
thread::spawn(move || {
|
||||
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
|
||||
let file = File::from_raw_fd(logpipe[0]);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
if let Ok(len) = reader.read_line(&mut buffer) {
|
||||
if len == 0 {
|
||||
break;
|
||||
} else if let Ok(msg) = CString::new(buffer.clone()) {
|
||||
android_log(Level::Info, tag, &msg);
|
||||
}
|
||||
pub unsafe extern "C" fn _rust_glue_entry(app: *mut ffi::android_app) {
|
||||
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
|
||||
let mut logpipe: [RawFd; 2] = Default::default();
|
||||
libc::pipe(logpipe.as_mut_ptr());
|
||||
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
|
||||
libc::dup2(logpipe[1], libc::STDERR_FILENO);
|
||||
thread::spawn(move || {
|
||||
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
|
||||
let file = File::from_raw_fd(logpipe[0]);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
if let Ok(len) = reader.read_line(&mut buffer) {
|
||||
if len == 0 {
|
||||
break;
|
||||
} else if let Ok(msg) = CString::new(buffer.clone()) {
|
||||
android_log(Level::Info, tag, &msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let jvm = unsafe {
|
||||
let jvm = (*(*native_app).activity).vm;
|
||||
let activity: jobject = (*(*native_app).activity).javaGameActivity;
|
||||
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
|
||||
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
|
||||
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
jvm
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap());
|
||||
|
||||
// We want to specifically catch any panic from the application's android_main
|
||||
// so we can finish + destroy the Activity gracefully via the JVM
|
||||
catch_unwind(|| {
|
||||
// XXX: If we were in control of the Java Activity subclass then
|
||||
// we could potentially run the android_main function via a Java native method
|
||||
// springboard (e.g. call an Activity subclass method that calls a jni native
|
||||
// method that then just calls android_main()) that would make sure there was
|
||||
// a Java frame at the base of our call stack which would then be recognised
|
||||
// when calling FindClass to lookup a suitable classLoader, instead of
|
||||
// defaulting to the system loader. Without this then it's difficult for native
|
||||
// code to look up non-standard Java classes.
|
||||
android_main(app);
|
||||
})
|
||||
.unwrap_or_else(|panic| log_panic(panic));
|
||||
|
||||
// Let JVM know that our Activity can be destroyed before detaching from the JVM
|
||||
//
|
||||
// "Note that this method can be called from any thread; it will send a message
|
||||
// to the main thread of the process where the Java finish call will take place"
|
||||
ffi::GameActivity_finish((*native_app).activity);
|
||||
|
||||
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
|
||||
detach_current_thread(jvm);
|
||||
}
|
||||
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let jvm: *mut JavaVM = (*(*app).activity).vm;
|
||||
let activity: jobject = (*(*app).activity).javaGameActivity;
|
||||
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
|
||||
|
||||
let app = AndroidApp::from_ptr(NonNull::new(app).unwrap());
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
|
||||
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
|
||||
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
// XXX: If we were in control of the Java Activity subclass then
|
||||
// we could potentially run the android_main function via a Java native method
|
||||
// springboard (e.g. call an Activity subclass method that calls a jni native
|
||||
// method that then just calls android_main()) that would make sure there was
|
||||
// a Java frame at the base of our call stack which would then be recognised
|
||||
// when calling FindClass to lookup a suitable classLoader, instead of
|
||||
// defaulting to the system loader. Without this then it's difficult for native
|
||||
// code to look up non-standard Java classes.
|
||||
android_main(app);
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
|
||||
detach_current_thread(jvm);
|
||||
}
|
||||
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
use bitflags::bitflags;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
pub use crate::activity_impl::input::*;
|
||||
|
||||
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
|
||||
///
|
||||
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_ANY)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum Source {
|
||||
BluetoothStylus = 0x0000c002,
|
||||
Dpad = 0x00000201,
|
||||
/// Either a gamepad or a joystick
|
||||
Gamepad = 0x00000401,
|
||||
Hdmi = 0x02000001,
|
||||
/// Either a gamepad or a joystick
|
||||
Joystick = 0x01000010,
|
||||
/// Pretty much any device with buttons. Query the keyboard type to determine
|
||||
/// if it has alphabetic keys and can be used for text entry.
|
||||
Keyboard = 0x00000101,
|
||||
/// A pointing device, such as a mouse or trackpad
|
||||
Mouse = 0x00002002,
|
||||
/// A pointing device, such as a mouse or trackpad whose relative motions should be treated as navigation events
|
||||
MouseRelative = 0x00020004,
|
||||
/// An input device akin to a scroll wheel
|
||||
RotaryEncoder = 0x00400000,
|
||||
Sensor = 0x04000000,
|
||||
Stylus = 0x00004002,
|
||||
Touchpad = 0x00100008,
|
||||
Touchscreen = 0x00001002,
|
||||
TouchNavigation = 0x00200000,
|
||||
Trackball = 0x00010004,
|
||||
|
||||
Unknown = 0,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
struct SourceFlags: u32 {
|
||||
const CLASS_MASK = 0x000000ff;
|
||||
|
||||
const BUTTON = 0x00000001;
|
||||
const POINTER = 0x00000002;
|
||||
const TRACKBALL = 0x00000004;
|
||||
const POSITION = 0x00000008;
|
||||
const JOYSTICK = 0x00000010;
|
||||
const NONE = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// An enum representing the class of a [`MotionEvent`] or [`KeyEvent`] source
|
||||
///
|
||||
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_CLASS_MASK)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Class {
|
||||
None,
|
||||
Button,
|
||||
Pointer,
|
||||
Trackball,
|
||||
Position,
|
||||
Joystick,
|
||||
}
|
||||
|
||||
impl From<u32> for Class {
|
||||
fn from(source: u32) -> Self {
|
||||
let class = SourceFlags::from_bits_truncate(source);
|
||||
match class {
|
||||
SourceFlags::BUTTON => Class::Button,
|
||||
SourceFlags::POINTER => Class::Pointer,
|
||||
SourceFlags::TRACKBALL => Class::Trackball,
|
||||
SourceFlags::POSITION => Class::Position,
|
||||
SourceFlags::JOYSTICK => Class::Joystick,
|
||||
_ => Class::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Source> for Class {
|
||||
fn from(source: Source) -> Self {
|
||||
let source: u32 = source.into();
|
||||
source.into()
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,8 @@
|
||||
//! A glue layer for building standalone, Rust applications on Android
|
||||
//!
|
||||
//! This crate provides a "glue" layer for building native Rust
|
||||
//! applications on Android, supporting multiple [`Activity`] base classes.
|
||||
//! It's comparable to [`android_native_app_glue.c`][ndk_concepts]
|
||||
//! for C/C++ applications.
|
||||
//!
|
||||
//! Currently the crate supports two `Activity` base classes:
|
||||
//! 1. [`NativeActivity`] - Built in to Android, this doesn't require compiling any Java or Kotlin code.
|
||||
//! 2. [`GameActivity`] - From the Android Game Development Kit, it has more
|
||||
//! sophisticated input handling support than `NativeActivity`. `GameActivity`
|
||||
//! is also based on the `AndroidAppCompat` class which can help with supporting
|
||||
//! a wider range of devices.
|
||||
//!
|
||||
//! Standalone applications based on this crate need to be built as `cdylib` libraries, like:
|
||||
//! ```
|
||||
//! [lib]
|
||||
//! crate_type=["cdylib"]
|
||||
//! ```
|
||||
//!
|
||||
//! and implement a `#[no_mangle]` `android_main` entry point like this:
|
||||
//! ```rust
|
||||
//! #[no_mangle]
|
||||
//! fn android_main(app: AndroidApp) {
|
||||
//!
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Once your application's `Activity` class has loaded and it calls `onCreate` then
|
||||
//! `android-activity` will spawn a dedicated thread to run your `android_main` function,
|
||||
//! separate from the Java thread that created the corresponding `Activity`.
|
||||
//!
|
||||
//! [`AndroidApp`] provides an interface to query state for the application as
|
||||
//! well as monitor events, such as lifecycle and input events, that are
|
||||
//! marshalled between the Java thread that owns the `Activity` and the native
|
||||
//! thread that runs the `android_main()` code.
|
||||
//!
|
||||
//! # Main Thread Initialization
|
||||
//!
|
||||
//! Before `android_main()` is called, the following application state
|
||||
//! is also initialized:
|
||||
//!
|
||||
//! 1. An I/O thread is spawned that will handle redirecting standard input
|
||||
//! and output to the Android log, visible via `logcat`.
|
||||
//! 2. A `JavaVM` and `Activity` instance will be associated with the [`ndk_context`] crate
|
||||
//! so that other, independent, Rust crates are able to find a JavaVM
|
||||
//! for making JNI calls.
|
||||
//! 3. The `JavaVM` will be attached to the native thread
|
||||
//! 4. A [Looper] is attached to the Rust native thread.
|
||||
//!
|
||||
//!
|
||||
//! These are undone after `android_main()` returns
|
||||
//!
|
||||
//! [`Activity`]: https://developer.android.com/reference/android/app/Activity
|
||||
//! [`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
|
||||
//! [ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa
|
||||
//! [`GameActivity`]: https://developer.android.com/games/agdk/integrate-game-activity
|
||||
//! [Looper]: https://developer.android.com/reference/android/os/Looper
|
||||
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use libc::c_void;
|
||||
use ndk::asset::AssetManager;
|
||||
use ndk::native_window::NativeWindow;
|
||||
|
||||
@@ -73,14 +13,14 @@ compile_error!("android-activity only supports compiling for Android");
|
||||
|
||||
#[cfg(all(feature = "game-activity", feature = "native-activity"))]
|
||||
compile_error!(
|
||||
r#"The "game-activity" and "native-activity" features cannot be enabled at the same time"#
|
||||
"The \"game-activity\" and \"native-activity\" features cannot be enabled at the same time"
|
||||
);
|
||||
#[cfg(all(
|
||||
not(any(feature = "game-activity", feature = "native-activity")),
|
||||
not(doc)
|
||||
))]
|
||||
compile_error!(
|
||||
r#"Either "game-activity" or "native-activity" must be enabled as features
|
||||
r#"Either \"game-activity\" or \"native-activity\" must be enabled as features
|
||||
|
||||
If you have set one of these features then this error indicates that Cargo is trying to
|
||||
link together multiple implementations of android-activity (with incompatible versions)
|
||||
@@ -93,7 +33,8 @@ You can use `cargo tree` (e.g. via `cargo ndk -t arm64-v8a tree`) to identify wh
|
||||
versions have been resolved.
|
||||
|
||||
You may need to add a `[patch]` into your Cargo.toml to ensure a specific version of
|
||||
android-activity is used across all of your application's crates."#
|
||||
android-activity is used across all of your application's crates.
|
||||
"#
|
||||
);
|
||||
|
||||
#[cfg(any(feature = "native-activity", doc))]
|
||||
@@ -106,14 +47,20 @@ mod game_activity;
|
||||
#[cfg(feature = "game-activity")]
|
||||
use game_activity as activity_impl;
|
||||
|
||||
pub mod input;
|
||||
pub use activity_impl::input;
|
||||
|
||||
mod config;
|
||||
pub use config::ConfigurationRef;
|
||||
|
||||
mod util;
|
||||
|
||||
/// A rectangle with integer edge coordinates. Used to represent window insets, for example.
|
||||
// Note: unlike in ndk-glue this has signed components (consistent
|
||||
// with Android's ARect) which generally allows for representing
|
||||
// rectangles with a negative/off-screen origin. Even though this
|
||||
// is currently just used to represent the content rect (that probably
|
||||
// wouldn't have any negative components) we keep the generality
|
||||
// since this is a primitive type that could potentially be used
|
||||
// for more things in the future.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Rect {
|
||||
pub left: i32,
|
||||
@@ -122,44 +69,9 @@ pub struct Rect {
|
||||
pub bottom: i32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// An empty `Rect` with all components set to zero.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub type StateSaver<'a> = activity_impl::StateSaver<'a>;
|
||||
pub type StateLoader<'a> = activity_impl::StateLoader<'a>;
|
||||
|
||||
impl From<Rect> for ndk_sys::ARect {
|
||||
fn from(rect: Rect) -> Self {
|
||||
Self {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ndk_sys::ARect> for Rect {
|
||||
fn from(arect: ndk_sys::ARect) -> Self {
|
||||
Self {
|
||||
left: arect.left,
|
||||
right: arect.right,
|
||||
top: arect.top,
|
||||
bottom: arect.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use activity_impl::StateLoader;
|
||||
pub use activity_impl::StateSaver;
|
||||
|
||||
/// An application event delivered during [`AndroidApp::poll_events`]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum MainEvent<'a> {
|
||||
@@ -251,7 +163,6 @@ pub enum MainEvent<'a> {
|
||||
InsetsChanged {},
|
||||
}
|
||||
|
||||
/// An event delivered during [`AndroidApp::poll_events`]
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum PollEvent<'a> {
|
||||
@@ -260,15 +171,6 @@ pub enum PollEvent<'a> {
|
||||
Main(MainEvent<'a>),
|
||||
}
|
||||
|
||||
/// Indicates whether an application has handled or ignored an event
|
||||
///
|
||||
/// If an event is not handled by an application then some default handling may happen.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum InputStatus {
|
||||
Handled,
|
||||
Unhandled,
|
||||
}
|
||||
|
||||
use activity_impl::AndroidAppInner;
|
||||
pub use activity_impl::AndroidAppWaker;
|
||||
|
||||
@@ -438,13 +340,6 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
/// The top-level state and interface for a native Rust application
|
||||
///
|
||||
/// `AndroidApp` provides an interface to query state for the application as
|
||||
/// well as monitor events, such as lifecycle and input events, that are
|
||||
/// marshalled between the Java thread that owns the `Activity` and the native
|
||||
/// thread that runs the `android_main()` code.
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AndroidApp {
|
||||
pub(crate) inner: Arc<RwLock<AndroidAppInner>>,
|
||||
@@ -464,59 +359,22 @@ impl Hash for AndroidApp {
|
||||
}
|
||||
|
||||
impl AndroidApp {
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-activity")))]
|
||||
#[cfg(feature = "native-activity")]
|
||||
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
|
||||
self.inner.read().unwrap().native_activity()
|
||||
}
|
||||
|
||||
/// Queries the current [`NativeWindow`] for the application.
|
||||
///
|
||||
/// This will only return `Some(window)` between
|
||||
/// [`MainEvent::InitWindow`] and [`MainEvent::TerminateWindow`]
|
||||
/// events.
|
||||
pub fn native_window(&self) -> Option<NativeWindow> {
|
||||
pub fn native_window<'a>(&self) -> Option<NativeWindow> {
|
||||
self.inner.read().unwrap().native_window()
|
||||
}
|
||||
|
||||
/// Returns a pointer to the Java Virtual Machine, for making JNI calls
|
||||
///
|
||||
/// This returns a pointer to the Java Virtual Machine which can be used
|
||||
/// with the [`jni`] crate (or similar crates) to make JNI calls that bridge
|
||||
/// between native Rust code and Java/Kotlin code running within the JVM.
|
||||
///
|
||||
/// If you use the [`jni`] crate you can wrap this as a [`JavaVM`] via:
|
||||
/// ```ignore
|
||||
/// # use jni::JavaVM;
|
||||
/// # let app: AndroidApp = todo!();
|
||||
/// let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr()) };
|
||||
/// ```
|
||||
///
|
||||
/// [`jni`]: https://crates.io/crates/jni
|
||||
/// [`JavaVM`]: https://docs.rs/jni/latest/jni/struct.JavaVM.html
|
||||
pub fn vm_as_ptr(&self) -> *mut c_void {
|
||||
self.inner.read().unwrap().vm_as_ptr()
|
||||
}
|
||||
|
||||
/// Returns a JNI object reference for this application's JVM `Activity` as a pointer
|
||||
///
|
||||
/// If you use the [`jni`] crate you can wrap this as an object reference via:
|
||||
/// ```ignore
|
||||
/// # use jni::objects::JObject;
|
||||
/// # let app: AndroidApp = todo!();
|
||||
/// let activity = unsafe { JObject::from_raw(app.activity_as_ptr()) };
|
||||
/// ```
|
||||
///
|
||||
/// # JNI Safety
|
||||
///
|
||||
/// Note that the object reference will be a JNI global reference, not a
|
||||
/// local reference and it should not be deleted. Don't wrap the reference
|
||||
/// in an [`AutoLocal`] which would try to explicitly delete the reference
|
||||
/// when dropped. Similarly, don't wrap the reference as a [`GlobalRef`]
|
||||
/// which would also try to explicitly delete the reference when dropped.
|
||||
///
|
||||
/// [`jni`]: https://crates.io/crates/jni
|
||||
/// [`AutoLocal`]: https://docs.rs/jni/latest/jni/objects/struct.AutoLocal.html
|
||||
/// [`GlobalRef`]: https://docs.rs/jni/latest/jni/objects/struct.GlobalRef.html
|
||||
pub fn activity_as_ptr(&self) -> *mut c_void {
|
||||
self.inner.read().unwrap().activity_as_ptr()
|
||||
}
|
||||
|
||||
/// Polls for any events associated with this [AndroidApp] and processes those events
|
||||
/// Polls for any events associated with this AndroidApp and processes those events
|
||||
/// (such as lifecycle events) via the given `callback`.
|
||||
///
|
||||
/// It's important to use this API for polling, and not call [`ALooper_pollAll`] directly since
|
||||
@@ -528,11 +386,6 @@ impl AndroidApp {
|
||||
/// main thread. The [`MainEvent::SaveState`] event is also synchronized with the
|
||||
/// Java main thread.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This must only be called from your `android_main()` thread and it may panic if called
|
||||
/// from another thread.
|
||||
///
|
||||
/// [`ALooper_pollAll`]: ndk::looper::ThreadLooper::poll_all
|
||||
pub fn poll_events<F>(&self, timeout: Option<Duration>, callback: F)
|
||||
where
|
||||
@@ -620,10 +473,6 @@ impl AndroidApp {
|
||||
|
||||
/// Query and process all out-standing input event
|
||||
///
|
||||
/// `callback` should return [`InputStatus::Unhandled`] for any input events that aren't directly
|
||||
/// handled by the application, or else [`InputStatus::Handled`]. Unhandled events may lead to a
|
||||
/// fallback interpretation of the event.
|
||||
///
|
||||
/// Applications are generally either expected to call this in-sync with their rendering or
|
||||
/// in response to a [`MainEvent::InputAvailable`] event being delivered. _Note though that your
|
||||
/// application is will only be delivered a single [`MainEvent::InputAvailable`] event between calls
|
||||
@@ -631,11 +480,11 @@ impl AndroidApp {
|
||||
///
|
||||
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
|
||||
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
|
||||
pub fn input_events<F>(&self, callback: F)
|
||||
pub fn input_events<'b, F>(&self, callback: F)
|
||||
where
|
||||
F: FnMut(&input::InputEvent) -> InputStatus,
|
||||
F: FnMut(&input::InputEvent),
|
||||
{
|
||||
self.inner.read().unwrap().input_events(callback)
|
||||
self.inner.read().unwrap().input_events(callback);
|
||||
}
|
||||
|
||||
/// The user-visible SDK version of the framework
|
||||
@@ -644,8 +493,7 @@ impl AndroidApp {
|
||||
pub fn sdk_version() -> i32 {
|
||||
let mut prop = android_properties::getprop("ro.build.version.sdk");
|
||||
if let Some(val) = prop.value() {
|
||||
val.parse::<i32>()
|
||||
.expect("Failed to parse ro.build.version.sdk property")
|
||||
i32::from_str_radix(&val, 10).expect("Failed to parse ro.build.version.sdk property")
|
||||
} else {
|
||||
panic!("Couldn't read ro.build.version.sdk system property");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//! The bindings are pre-generated and the right one for the platform is selected at compile time.
|
||||
|
||||
// Bindgen lints
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(improper_ctypes)]
|
||||
#![allow(clippy::all)]
|
||||
// Temporarily allow UB nullptr dereference in bindgen layout tests until fixed upstream:
|
||||
// https://github.com/rust-lang/rust-bindgen/pull/2055
|
||||
// https://github.com/rust-lang/rust-bindgen/pull/2064
|
||||
#![allow(deref_nullptr)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use jni_sys::*;
|
||||
use ndk_sys::AAssetManager;
|
||||
use ndk_sys::ANativeWindow;
|
||||
use ndk_sys::{AConfiguration, AInputQueue, ALooper};
|
||||
|
||||
#[cfg(all(
|
||||
any(target_os = "android", feature = "test"),
|
||||
any(target_arch = "arm", target_arch = "armv7")
|
||||
))]
|
||||
include!("ffi_arm.rs");
|
||||
|
||||
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "aarch64"))]
|
||||
include!("ffi_aarch64.rs");
|
||||
|
||||
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86"))]
|
||||
include!("ffi_i686.rs");
|
||||
|
||||
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86_64"))]
|
||||
include!("ffi_x86_64.rs");
|
||||
@@ -1,918 +0,0 @@
|
||||
//! This 'glue' layer acts as an IPC shim between the JVM main thread and the Rust
|
||||
//! main thread. Notifying Rust of lifecycle events from the JVM and handling
|
||||
//! synchronization between the two threads.
|
||||
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
ops::Deref,
|
||||
os::unix::prelude::{FromRawFd, RawFd},
|
||||
panic::catch_unwind,
|
||||
ptr::{self, NonNull},
|
||||
sync::{Arc, Condvar, Mutex, Weak},
|
||||
};
|
||||
|
||||
use log::Level;
|
||||
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
|
||||
|
||||
use crate::{
|
||||
util::android_log,
|
||||
util::{abort_on_panic, log_panic},
|
||||
ConfigurationRef,
|
||||
};
|
||||
|
||||
use super::{AndroidApp, Rect};
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub enum AppCmd {
|
||||
InputQueueChanged = 0,
|
||||
InitWindow = 1,
|
||||
TermWindow = 2,
|
||||
WindowResized = 3,
|
||||
WindowRedrawNeeded = 4,
|
||||
ContentRectChanged = 5,
|
||||
GainedFocus = 6,
|
||||
LostFocus = 7,
|
||||
ConfigChanged = 8,
|
||||
LowMemory = 9,
|
||||
Start = 10,
|
||||
Resume = 11,
|
||||
SaveState = 12,
|
||||
Pause = 13,
|
||||
Stop = 14,
|
||||
Destroy = 15,
|
||||
}
|
||||
impl TryFrom<i8> for AppCmd {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: i8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(AppCmd::InputQueueChanged),
|
||||
1 => Ok(AppCmd::InitWindow),
|
||||
2 => Ok(AppCmd::TermWindow),
|
||||
3 => Ok(AppCmd::WindowResized),
|
||||
4 => Ok(AppCmd::WindowRedrawNeeded),
|
||||
5 => Ok(AppCmd::ContentRectChanged),
|
||||
6 => Ok(AppCmd::GainedFocus),
|
||||
7 => Ok(AppCmd::LostFocus),
|
||||
8 => Ok(AppCmd::ConfigChanged),
|
||||
9 => Ok(AppCmd::LowMemory),
|
||||
10 => Ok(AppCmd::Start),
|
||||
11 => Ok(AppCmd::Resume),
|
||||
12 => Ok(AppCmd::SaveState),
|
||||
13 => Ok(AppCmd::Pause),
|
||||
14 => Ok(AppCmd::Stop),
|
||||
15 => Ok(AppCmd::Destroy),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||
pub enum State {
|
||||
Init,
|
||||
Start,
|
||||
Resume,
|
||||
Pause,
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WaitableNativeActivityState {
|
||||
pub activity: *mut ndk_sys::ANativeActivity,
|
||||
|
||||
pub mutex: Mutex<NativeActivityState>,
|
||||
pub cond: Condvar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NativeActivityGlue {
|
||||
pub inner: Arc<WaitableNativeActivityState>,
|
||||
}
|
||||
unsafe impl Send for NativeActivityGlue {}
|
||||
unsafe impl Sync for NativeActivityGlue {}
|
||||
|
||||
impl Deref for NativeActivityGlue {
|
||||
type Target = WaitableNativeActivityState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl NativeActivityGlue {
|
||||
pub fn new(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
saved_state: *const libc::c_void,
|
||||
saved_state_size: libc::size_t,
|
||||
) -> Self {
|
||||
let glue = Self {
|
||||
inner: Arc::new(WaitableNativeActivityState::new(
|
||||
activity,
|
||||
saved_state,
|
||||
saved_state_size,
|
||||
)),
|
||||
};
|
||||
|
||||
let weak_ref = Arc::downgrade(&glue.inner);
|
||||
let weak_ptr = Weak::into_raw(weak_ref);
|
||||
unsafe {
|
||||
(*activity).instance = weak_ptr as *mut _;
|
||||
|
||||
(*(*activity).callbacks).onDestroy = Some(on_destroy);
|
||||
(*(*activity).callbacks).onStart = Some(on_start);
|
||||
(*(*activity).callbacks).onResume = Some(on_resume);
|
||||
(*(*activity).callbacks).onSaveInstanceState = Some(on_save_instance_state);
|
||||
(*(*activity).callbacks).onPause = Some(on_pause);
|
||||
(*(*activity).callbacks).onStop = Some(on_stop);
|
||||
(*(*activity).callbacks).onConfigurationChanged = Some(on_configuration_changed);
|
||||
(*(*activity).callbacks).onLowMemory = Some(on_low_memory);
|
||||
(*(*activity).callbacks).onWindowFocusChanged = Some(on_window_focus_changed);
|
||||
(*(*activity).callbacks).onNativeWindowCreated = Some(on_native_window_created);
|
||||
(*(*activity).callbacks).onNativeWindowResized = Some(on_native_window_resized);
|
||||
(*(*activity).callbacks).onNativeWindowRedrawNeeded =
|
||||
Some(on_native_window_redraw_needed);
|
||||
(*(*activity).callbacks).onNativeWindowDestroyed = Some(on_native_window_destroyed);
|
||||
(*(*activity).callbacks).onInputQueueCreated = Some(on_input_queue_created);
|
||||
(*(*activity).callbacks).onInputQueueDestroyed = Some(on_input_queue_destroyed);
|
||||
(*(*activity).callbacks).onContentRectChanged = Some(on_content_rect_changed);
|
||||
}
|
||||
|
||||
glue
|
||||
}
|
||||
|
||||
/// Returns the file descriptor that needs to be polled by the Rust main thread
|
||||
/// for events/commands from the JVM thread
|
||||
pub fn cmd_read_fd(&self) -> libc::c_int {
|
||||
self.mutex.lock().unwrap().msg_read
|
||||
}
|
||||
|
||||
/// For the Rust main thread to read a single pending command sent from the JVM main thread
|
||||
pub fn read_cmd(&self) -> Option<AppCmd> {
|
||||
self.inner.mutex.lock().unwrap().read_cmd()
|
||||
}
|
||||
|
||||
/// For the Rust main thread to get an [`InputQueue`] that wraps the AInputQueue pointer
|
||||
/// we have and at the same time ensure that the input queue is attached to the given looper.
|
||||
///
|
||||
/// NB: it's expected that the input queue is detached as soon as we know there is new
|
||||
/// input (knowing the app will be notified) and only re-attached when the application
|
||||
/// reads the input (to avoid lots of redundant wake ups)
|
||||
pub fn looper_attached_input_queue(
|
||||
&self,
|
||||
looper: *mut ndk_sys::ALooper,
|
||||
ident: libc::c_int,
|
||||
) -> Option<InputQueue> {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
if guard.input_queue.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Reattach the input queue to the looper so future input will again deliver an
|
||||
// `InputAvailable` event.
|
||||
guard.attach_input_queue_to_looper(looper, ident);
|
||||
Some(InputQueue::from_ptr(NonNull::new_unchecked(
|
||||
guard.input_queue,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detach_input_queue_from_looper(&self) {
|
||||
unsafe {
|
||||
self.inner
|
||||
.mutex
|
||||
.lock()
|
||||
.unwrap()
|
||||
.detach_input_queue_from_looper();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> ConfigurationRef {
|
||||
self.mutex.lock().unwrap().config.clone()
|
||||
}
|
||||
|
||||
pub fn content_rect(&self) -> Rect {
|
||||
self.mutex.lock().unwrap().content_rect.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NativeActivityState {
|
||||
pub msg_read: libc::c_int,
|
||||
pub msg_write: libc::c_int,
|
||||
pub config: super::ConfigurationRef,
|
||||
pub saved_state: Vec<u8>,
|
||||
pub input_queue: *mut ndk_sys::AInputQueue,
|
||||
pub window: Option<NativeWindow>,
|
||||
pub content_rect: ndk_sys::ARect,
|
||||
pub activity_state: State,
|
||||
pub destroy_requested: bool,
|
||||
pub running: bool,
|
||||
pub app_has_saved_state: bool,
|
||||
pub destroyed: bool,
|
||||
pub redraw_needed: bool,
|
||||
pub pending_input_queue: *mut ndk_sys::AInputQueue,
|
||||
pub pending_window: Option<NativeWindow>,
|
||||
}
|
||||
|
||||
impl NativeActivityState {
|
||||
pub fn read_cmd(&mut self) -> Option<AppCmd> {
|
||||
let mut cmd_i: i8 = 0;
|
||||
loop {
|
||||
match unsafe { libc::read(self.msg_read, &mut cmd_i as *mut _ as *mut _, 1) } {
|
||||
1 => {
|
||||
let cmd = AppCmd::try_from(cmd_i);
|
||||
return match cmd {
|
||||
Ok(cmd) => Some(cmd),
|
||||
Err(_) => {
|
||||
log::error!("Spurious, unknown NativeActivityGlue cmd: {}", cmd_i);
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
-1 => {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() != std::io::ErrorKind::Interrupted {
|
||||
log::error!("Failure reading NativeActivityGlue cmd: {}", err);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
count => {
|
||||
log::error!(
|
||||
"Spurious read of {count} bytes while reading NativeActivityGlue cmd"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_cmd(&mut self, cmd: AppCmd) {
|
||||
let cmd = cmd as i8;
|
||||
loop {
|
||||
match unsafe { libc::write(self.msg_write, &cmd as *const _ as *const _, 1) } {
|
||||
1 => break,
|
||||
-1 => {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() != std::io::ErrorKind::Interrupted {
|
||||
log::error!("Failure writing NativeActivityGlue cmd: {}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
count => {
|
||||
log::error!(
|
||||
"Spurious write of {count} bytes while writing NativeActivityGlue cmd"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn attach_input_queue_to_looper(
|
||||
&mut self,
|
||||
looper: *mut ndk_sys::ALooper,
|
||||
ident: libc::c_int,
|
||||
) {
|
||||
if !self.input_queue.is_null() {
|
||||
log::trace!("Attaching input queue to looper");
|
||||
ndk_sys::AInputQueue_attachLooper(
|
||||
self.input_queue,
|
||||
looper,
|
||||
ident,
|
||||
None,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn detach_input_queue_from_looper(&mut self) {
|
||||
if !self.input_queue.is_null() {
|
||||
log::trace!("Detaching input queue from looper");
|
||||
ndk_sys::AInputQueue_detachLooper(self.input_queue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WaitableNativeActivityState {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("WaitableNativeActivityState::drop!");
|
||||
unsafe {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.detach_input_queue_from_looper();
|
||||
guard.destroyed = true;
|
||||
self.cond.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WaitableNativeActivityState {
|
||||
///////////////////////////////
|
||||
// Java-side callback handling
|
||||
///////////////////////////////
|
||||
|
||||
pub fn new(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
saved_state_in: *const libc::c_void,
|
||||
saved_state_size: libc::size_t,
|
||||
) -> Self {
|
||||
let mut msgpipe: [libc::c_int; 2] = [-1, -1];
|
||||
unsafe {
|
||||
if libc::pipe(msgpipe.as_mut_ptr()) != 0 {
|
||||
panic!(
|
||||
"could not create Rust <-> Java IPC pipe: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let saved_state = unsafe {
|
||||
std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size as _)
|
||||
};
|
||||
|
||||
let config = unsafe {
|
||||
let config = ndk_sys::AConfiguration_new();
|
||||
ndk_sys::AConfiguration_fromAssetManager(config, (*activity).assetManager);
|
||||
|
||||
let config = super::ConfigurationRef::new(Configuration::from_ptr(
|
||||
NonNull::new_unchecked(config),
|
||||
));
|
||||
log::trace!("Config: {:#?}", config);
|
||||
config
|
||||
};
|
||||
|
||||
Self {
|
||||
activity,
|
||||
mutex: Mutex::new(NativeActivityState {
|
||||
msg_read: msgpipe[0],
|
||||
msg_write: msgpipe[1],
|
||||
config,
|
||||
saved_state: saved_state.into(),
|
||||
input_queue: ptr::null_mut(),
|
||||
window: None,
|
||||
content_rect: Rect::empty().into(),
|
||||
activity_state: State::Init,
|
||||
destroy_requested: false,
|
||||
running: false,
|
||||
app_has_saved_state: false,
|
||||
destroyed: false,
|
||||
redraw_needed: false,
|
||||
pending_input_queue: ptr::null_mut(),
|
||||
pending_window: None,
|
||||
}),
|
||||
cond: Condvar::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_destroyed(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
unsafe {
|
||||
guard.write_cmd(AppCmd::Destroy);
|
||||
while !guard.destroyed {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
|
||||
libc::close(guard.msg_read);
|
||||
guard.msg_read = -1;
|
||||
libc::close(guard.msg_write);
|
||||
guard.msg_write = -1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_config_changed(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.write_cmd(AppCmd::ConfigChanged);
|
||||
}
|
||||
|
||||
pub fn notify_low_memory(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.write_cmd(AppCmd::LowMemory);
|
||||
}
|
||||
|
||||
pub fn notify_focus_changed(&self, focused: bool) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.write_cmd(if focused {
|
||||
AppCmd::GainedFocus
|
||||
} else {
|
||||
AppCmd::LostFocus
|
||||
});
|
||||
}
|
||||
|
||||
pub fn notify_window_resized(&self, native_window: *mut ndk_sys::ANativeWindow) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
// set_window always syncs .pending_window back to .window before returning. This callback
|
||||
// from Android can never arrive at an interim state, and validates that Android:
|
||||
// 1. Only provides resizes in between onNativeWindowCreated and onNativeWindowDestroyed;
|
||||
// 2. Doesn't call it on a bogus window pointer that we don't know about.
|
||||
debug_assert_eq!(guard.window.as_ref().unwrap().ptr().as_ptr(), native_window);
|
||||
guard.write_cmd(AppCmd::WindowResized);
|
||||
}
|
||||
|
||||
pub fn notify_window_redraw_needed(&self, native_window: *mut ndk_sys::ANativeWindow) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
// set_window always syncs .pending_window back to .window before returning. This callback
|
||||
// from Android can never arrive at an interim state, and validates that Android:
|
||||
// 1. Only provides resizes in between onNativeWindowCreated and onNativeWindowDestroyed;
|
||||
// 2. Doesn't call it on a bogus window pointer that we don't know about.
|
||||
debug_assert_eq!(guard.window.as_ref().unwrap().ptr().as_ptr(), native_window);
|
||||
guard.write_cmd(AppCmd::WindowRedrawNeeded);
|
||||
}
|
||||
|
||||
unsafe fn set_input(&self, input_queue: *mut ndk_sys::AInputQueue) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
// The pending_input_queue state should only be set while in this method, and since
|
||||
// it doesn't allow re-entrance and is cleared before returning then we expect
|
||||
// this to be null
|
||||
debug_assert!(
|
||||
guard.pending_input_queue.is_null(),
|
||||
"InputQueue update clash"
|
||||
);
|
||||
|
||||
guard.pending_input_queue = input_queue;
|
||||
guard.write_cmd(AppCmd::InputQueueChanged);
|
||||
while guard.input_queue != guard.pending_input_queue {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
guard.pending_input_queue = ptr::null_mut();
|
||||
}
|
||||
|
||||
unsafe fn set_window(&self, window: Option<NativeWindow>) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
// The pending_window state should only be set while in this method, and since
|
||||
// it doesn't allow re-entrance and is cleared before returning then we expect
|
||||
// this to be None
|
||||
debug_assert!(guard.pending_window.is_none(), "NativeWindow update clash");
|
||||
|
||||
if guard.window.is_some() {
|
||||
guard.write_cmd(AppCmd::TermWindow);
|
||||
}
|
||||
guard.pending_window = window;
|
||||
if guard.pending_window.is_some() {
|
||||
guard.write_cmd(AppCmd::InitWindow);
|
||||
}
|
||||
while guard.window != guard.pending_window {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
guard.pending_window = None;
|
||||
}
|
||||
|
||||
unsafe fn set_content_rect(&self, rect: *const ndk_sys::ARect) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.content_rect = *rect;
|
||||
guard.write_cmd(AppCmd::ContentRectChanged);
|
||||
}
|
||||
|
||||
unsafe fn set_activity_state(&self, state: State) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
let cmd = match state {
|
||||
State::Init => panic!("Can't explicitly transition into 'init' state"),
|
||||
State::Start => AppCmd::Start,
|
||||
State::Resume => AppCmd::Resume,
|
||||
State::Pause => AppCmd::Pause,
|
||||
State::Stop => AppCmd::Stop,
|
||||
};
|
||||
guard.write_cmd(cmd);
|
||||
|
||||
while guard.activity_state != state {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn request_save_state(&self) -> (*mut libc::c_void, libc::size_t) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
// The state_saved flag should only be set while in this method, and since
|
||||
// it doesn't allow re-entrance and is cleared before returning then we expect
|
||||
// this to be None
|
||||
debug_assert!(!guard.app_has_saved_state, "SaveState request clash");
|
||||
guard.write_cmd(AppCmd::SaveState);
|
||||
while !guard.app_has_saved_state {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
guard.app_has_saved_state = false;
|
||||
|
||||
// `ANativeActivity` explicitly documents that it expects save state to be
|
||||
// given via a `malloc()` allocated pointer since it will automatically
|
||||
// `free()` the state after it has been converted to a buffer for the JVM.
|
||||
if !guard.saved_state.is_empty() {
|
||||
let saved_state_size = guard.saved_state.len() as _;
|
||||
let saved_state_src_ptr = guard.saved_state.as_ptr();
|
||||
unsafe {
|
||||
let saved_state = libc::malloc(saved_state_size);
|
||||
assert!(
|
||||
!saved_state.is_null(),
|
||||
"Failed to allocate {} bytes for restoring saved application state",
|
||||
saved_state_size
|
||||
);
|
||||
libc::memcpy(saved_state, saved_state_src_ptr as _, saved_state_size);
|
||||
(saved_state, saved_state_size)
|
||||
}
|
||||
} else {
|
||||
(ptr::null_mut(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn saved_state(&self) -> Option<Vec<u8>> {
|
||||
let guard = self.mutex.lock().unwrap();
|
||||
if !guard.saved_state.is_empty() {
|
||||
Some(guard.saved_state.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_saved_state(&self, state: &[u8]) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
|
||||
guard.saved_state.clear();
|
||||
guard.saved_state.extend_from_slice(state);
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// Rust-side event loop
|
||||
////////////////////////////
|
||||
|
||||
pub fn notify_main_thread_running(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.running = true;
|
||||
self.cond.notify_one();
|
||||
}
|
||||
|
||||
pub unsafe fn pre_exec_cmd(
|
||||
&self,
|
||||
cmd: AppCmd,
|
||||
looper: *mut ndk_sys::ALooper,
|
||||
input_queue_ident: libc::c_int,
|
||||
) {
|
||||
log::trace!("Pre: AppCmd::{:#?}", cmd);
|
||||
match cmd {
|
||||
AppCmd::InputQueueChanged => {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.detach_input_queue_from_looper();
|
||||
guard.input_queue = guard.pending_input_queue;
|
||||
if !guard.input_queue.is_null() {
|
||||
guard.attach_input_queue_to_looper(looper, input_queue_ident);
|
||||
}
|
||||
self.cond.notify_one();
|
||||
}
|
||||
AppCmd::InitWindow => {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.window = guard.pending_window.clone();
|
||||
self.cond.notify_one();
|
||||
}
|
||||
AppCmd::Resume | AppCmd::Start | AppCmd::Pause | AppCmd::Stop => {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.activity_state = match cmd {
|
||||
AppCmd::Start => State::Start,
|
||||
AppCmd::Pause => State::Pause,
|
||||
AppCmd::Resume => State::Resume,
|
||||
AppCmd::Stop => State::Stop,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
self.cond.notify_one();
|
||||
}
|
||||
AppCmd::ConfigChanged => {
|
||||
let guard = self.mutex.lock().unwrap();
|
||||
let config = ndk_sys::AConfiguration_new();
|
||||
ndk_sys::AConfiguration_fromAssetManager(config, (*self.activity).assetManager);
|
||||
let config = Configuration::from_ptr(NonNull::new_unchecked(config));
|
||||
guard.config.replace(config);
|
||||
log::debug!("Config: {:#?}", guard.config);
|
||||
}
|
||||
AppCmd::Destroy => {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.destroy_requested = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn post_exec_cmd(&self, cmd: AppCmd) {
|
||||
log::trace!("Post: AppCmd::{:#?}", cmd);
|
||||
match cmd {
|
||||
AppCmd::TermWindow => {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.window = None;
|
||||
self.cond.notify_one();
|
||||
}
|
||||
AppCmd::SaveState => {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.app_has_saved_state = true;
|
||||
self.cond.notify_one();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "Rust" {
|
||||
pub fn android_main(app: AndroidApp);
|
||||
}
|
||||
|
||||
unsafe fn try_with_waitable_activity_ref(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
closure: impl FnOnce(Arc<WaitableNativeActivityState>),
|
||||
) {
|
||||
assert!(!(*activity).instance.is_null());
|
||||
let weak_ptr: *const WaitableNativeActivityState = (*activity).instance.cast();
|
||||
let weak_ref = Weak::from_raw(weak_ptr);
|
||||
if let Some(waitable_activity) = weak_ref.upgrade() {
|
||||
closure(waitable_activity);
|
||||
} else {
|
||||
log::error!("Ignoring spurious JVM callback after last activity reference was dropped!")
|
||||
}
|
||||
let _ = weak_ref.into_raw();
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_destroy(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("Destroy: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_destroyed()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_start(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("Start: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_activity_state(State::Start);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_resume(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("Resume: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_activity_state(State::Resume);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_save_instance_state(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
out_len: *mut ndk_sys::size_t,
|
||||
) -> *mut libc::c_void {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("SaveInstanceState: {:p}\n", activity);
|
||||
*out_len = 0;
|
||||
let mut ret = ptr::null_mut();
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
let (state, len) = waitable_activity.request_save_state();
|
||||
*out_len = len as ndk_sys::size_t;
|
||||
ret = state
|
||||
});
|
||||
|
||||
log::debug!("Saved state = {:p}, len = {}", ret, *out_len);
|
||||
ret
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_pause(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("Pause: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_activity_state(State::Pause);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_stop(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("Stop: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_activity_state(State::Stop);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_configuration_changed(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("ConfigurationChanged: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_config_changed();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_low_memory(activity: *mut ndk_sys::ANativeActivity) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("LowMemory: {:p}\n", activity);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_low_memory();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_window_focus_changed(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
focused: libc::c_int,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("WindowFocusChanged: {:p} -- {}\n", activity, focused);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_focus_changed(focused != 0);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_native_window_created(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
window: *mut ndk_sys::ANativeWindow,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("NativeWindowCreated: {:p} -- {:p}\n", activity, window);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
// Use clone_from_ptr to acquire additional ownership on the NativeWindow,
|
||||
// which will unconditionally be _release()'d on Drop.
|
||||
let window = NativeWindow::clone_from_ptr(NonNull::new_unchecked(window));
|
||||
waitable_activity.set_window(Some(window));
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_native_window_resized(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
window: *mut ndk_sys::ANativeWindow,
|
||||
) {
|
||||
log::debug!("NativeWindowResized: {:p} -- {:p}\n", activity, window);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_window_resized(window);
|
||||
});
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_native_window_redraw_needed(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
window: *mut ndk_sys::ANativeWindow,
|
||||
) {
|
||||
log::debug!("NativeWindowRedrawNeeded: {:p} -- {:p}\n", activity, window);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_window_redraw_needed(window)
|
||||
});
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_native_window_destroyed(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
window: *mut ndk_sys::ANativeWindow,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("NativeWindowDestroyed: {:p} -- {:p}\n", activity, window);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_window(None);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_input_queue_created(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
queue: *mut ndk_sys::AInputQueue,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("InputQueueCreated: {:p} -- {:p}\n", activity, queue);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_input(queue);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_input_queue_destroyed(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
queue: *mut ndk_sys::AInputQueue,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("InputQueueDestroyed: {:p} -- {:p}\n", activity, queue);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_input(ptr::null_mut());
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_content_rect_changed(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
rect: *const ndk_sys::ARect,
|
||||
) {
|
||||
log::debug!("ContentRectChanged: {:p} -- {:p}\n", activity, rect);
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.set_content_rect(rect)
|
||||
});
|
||||
}
|
||||
|
||||
/// This is the native entrypoint for our cdylib library that `ANativeActivity` will look for via `dlsym`
|
||||
#[no_mangle]
|
||||
#[allow(unused_unsafe)] // Otherwise rust 1.64 moans about using unsafe{} in unsafe functions
|
||||
extern "C" fn ANativeActivity_onCreate(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
saved_state: *const libc::c_void,
|
||||
saved_state_size: libc::size_t,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
|
||||
unsafe {
|
||||
let mut logpipe: [RawFd; 2] = Default::default();
|
||||
libc::pipe(logpipe.as_mut_ptr());
|
||||
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
|
||||
libc::dup2(logpipe[1], libc::STDERR_FILENO);
|
||||
std::thread::spawn(move || {
|
||||
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
|
||||
let file = File::from_raw_fd(logpipe[0]);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
if let Ok(len) = reader.read_line(&mut buffer) {
|
||||
if len == 0 {
|
||||
break;
|
||||
} else if let Ok(msg) = CString::new(buffer.clone()) {
|
||||
android_log(Level::Info, tag, &msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
"Creating: {:p}, saved_state = {:p}, save_state_size = {}",
|
||||
activity,
|
||||
saved_state,
|
||||
saved_state_size
|
||||
);
|
||||
|
||||
// Conceptually we associate a glue reference with the JVM main thread, and another
|
||||
// reference with the Rust main thread
|
||||
let jvm_glue = NativeActivityGlue::new(activity, saved_state, saved_state_size);
|
||||
|
||||
let rust_glue = jvm_glue.clone();
|
||||
// Let us Send the NativeActivity pointer to the Rust main() thread without a wrapper type
|
||||
let activity_ptr: libc::intptr_t = activity as _;
|
||||
|
||||
// Note: we drop the thread handle which will detach the thread
|
||||
std::thread::spawn(move || {
|
||||
let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _;
|
||||
|
||||
let jvm = unsafe {
|
||||
let na = activity;
|
||||
let jvm = (*na).vm;
|
||||
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
|
||||
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
|
||||
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
|
||||
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
jvm
|
||||
};
|
||||
|
||||
let app = AndroidApp::new(rust_glue.clone());
|
||||
|
||||
rust_glue.notify_main_thread_running();
|
||||
|
||||
unsafe {
|
||||
// We want to specifically catch any panic from the application's android_main
|
||||
// so we can finish + destroy the Activity gracefully via the JVM
|
||||
catch_unwind(|| {
|
||||
// XXX: If we were in control of the Java Activity subclass then
|
||||
// we could potentially run the android_main function via a Java native method
|
||||
// springboard (e.g. call an Activity subclass method that calls a jni native
|
||||
// method that then just calls android_main()) that would make sure there was
|
||||
// a Java frame at the base of our call stack which would then be recognised
|
||||
// when calling FindClass to lookup a suitable classLoader, instead of
|
||||
// defaulting to the system loader. Without this then it's difficult for native
|
||||
// code to look up non-standard Java classes.
|
||||
android_main(app);
|
||||
})
|
||||
.unwrap_or_else(|panic| log_panic(panic));
|
||||
|
||||
// Let JVM know that our Activity can be destroyed before detaching from the JVM
|
||||
//
|
||||
// "Note that this method can be called from any thread; it will send a message
|
||||
// to the main thread of the process where the Java finish call will take place"
|
||||
ndk_sys::ANativeActivity_finish(activity);
|
||||
|
||||
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
|
||||
detach_current_thread(jvm);
|
||||
}
|
||||
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for thread to start.
|
||||
let mut guard = jvm_glue.mutex.lock().unwrap();
|
||||
while !guard.running {
|
||||
guard = jvm_glue.cond.wait(guard).unwrap();
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub use ndk::event::{
|
||||
Axis, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction,
|
||||
MotionEventFlags, Pointer, PointersIter,
|
||||
};
|
||||
|
||||
use crate::input::{Class, Source};
|
||||
|
||||
/// A motion event
|
||||
///
|
||||
/// For general discussion of motion events in Android, see [the relevant
|
||||
/// javadoc](https://developer.android.com/reference/android/view/MotionEvent).
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct MotionEvent<'a> {
|
||||
ndk_event: ndk::event::MotionEvent,
|
||||
_lifetime: PhantomData<&'a ndk::event::MotionEvent>,
|
||||
}
|
||||
impl<'a> MotionEvent<'a> {
|
||||
pub(crate) fn new(ndk_event: ndk::event::MotionEvent) -> Self {
|
||||
Self {
|
||||
ndk_event,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
}
|
||||
pub(crate) fn into_ndk_event(self) -> ndk::event::MotionEvent {
|
||||
self.ndk_event
|
||||
}
|
||||
|
||||
/// Get the source of the event.
|
||||
///
|
||||
#[inline]
|
||||
pub fn source(&self) -> Source {
|
||||
// XXX: we use `AInputEvent_getSource` directly (instead of calling
|
||||
// ndk_event.source()) since we have our own `Source` enum that we
|
||||
// share between backends, which may not exactly match the ndk crate's
|
||||
// `Source` enum.
|
||||
let source =
|
||||
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
source.try_into().unwrap_or(Source::Unknown)
|
||||
}
|
||||
|
||||
/// Get the class of the event source.
|
||||
///
|
||||
#[inline]
|
||||
pub fn class(&self) -> Class {
|
||||
Class::from(self.source())
|
||||
}
|
||||
|
||||
/// Get the device id associated with the event.
|
||||
///
|
||||
#[inline]
|
||||
pub fn device_id(&self) -> i32 {
|
||||
self.ndk_event.device_id()
|
||||
}
|
||||
|
||||
/// Returns the motion action associated with the event.
|
||||
///
|
||||
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionMasked())
|
||||
#[inline]
|
||||
pub fn action(&self) -> MotionAction {
|
||||
self.ndk_event.action()
|
||||
}
|
||||
|
||||
/// Returns the pointer index of an `Up` or `Down` event.
|
||||
///
|
||||
/// Pointer indices can change per motion event. For an identifier that stays the same, see
|
||||
/// [`Pointer::pointer_id()`].
|
||||
///
|
||||
/// This only has a meaning when the [action](Self::action) is one of [`Up`](MotionAction::Up),
|
||||
/// [`Down`](MotionAction::Down), [`PointerUp`](MotionAction::PointerUp),
|
||||
/// or [`PointerDown`](MotionAction::PointerDown).
|
||||
#[inline]
|
||||
pub fn pointer_index(&self) -> usize {
|
||||
self.ndk_event.pointer_index()
|
||||
}
|
||||
|
||||
/*
|
||||
/// Returns the pointer id associated with the given pointer index.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getpointerid)
|
||||
// TODO: look at output with out-of-range pointer index
|
||||
// Probably -1 though
|
||||
pub fn pointer_id_for(&self, pointer_index: usize) -> i32 {
|
||||
unsafe { ndk_sys::AMotionEvent_getPointerId(self.ndk_event.ptr.as_ptr(), pointer_index) }
|
||||
}
|
||||
*/
|
||||
|
||||
/// Returns the number of pointers in this event
|
||||
///
|
||||
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getPointerCount())
|
||||
#[inline]
|
||||
pub fn pointer_count(&self) -> usize {
|
||||
self.ndk_event.pointer_count()
|
||||
}
|
||||
|
||||
/// An iterator over the pointers in this motion event
|
||||
#[inline]
|
||||
pub fn pointers(&self) -> PointersIter<'_> {
|
||||
self.ndk_event.pointers()
|
||||
}
|
||||
|
||||
/// The pointer at a given pointer index. Panics if the pointer index is out of bounds.
|
||||
///
|
||||
/// If you need to loop over all the pointers, prefer the [`pointers()`](Self::pointers) method.
|
||||
#[inline]
|
||||
pub fn pointer_at_index(&self, index: usize) -> Pointer<'_> {
|
||||
self.ndk_event.pointer_at_index(index)
|
||||
}
|
||||
|
||||
/*
|
||||
XXX: Not currently supported with GameActivity so we don't currently expose for NativeActivity
|
||||
either, for consistency.
|
||||
|
||||
/// Returns the size of the history contained in this event.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_gethistorysize)
|
||||
#[inline]
|
||||
pub fn history_size(&self) -> usize {
|
||||
self.ndk_event.history_size()
|
||||
}
|
||||
|
||||
/// An iterator over the historical events contained in this event.
|
||||
#[inline]
|
||||
pub fn history(&self) -> HistoricalMotionEventsIter<'_> {
|
||||
self.ndk_event.history()
|
||||
}
|
||||
*/
|
||||
|
||||
/// Returns the state of any modifier keys that were pressed during the event.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getmetastate)
|
||||
#[inline]
|
||||
pub fn meta_state(&self) -> MetaState {
|
||||
self.ndk_event.meta_state()
|
||||
}
|
||||
|
||||
/// Returns the button state during this event, as a bitfield.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getbuttonstate)
|
||||
#[inline]
|
||||
pub fn button_state(&self) -> ButtonState {
|
||||
self.ndk_event.button_state()
|
||||
}
|
||||
|
||||
/// Returns the time of the start of this gesture, in the `java.lang.System.nanoTime()` time
|
||||
/// base
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getdowntime)
|
||||
#[inline]
|
||||
pub fn down_time(&self) -> i64 {
|
||||
self.ndk_event.down_time()
|
||||
}
|
||||
|
||||
/// Returns a bitfield indicating which edges were touched by this event.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getedgeflags)
|
||||
#[inline]
|
||||
pub fn edge_flags(&self) -> EdgeFlags {
|
||||
self.ndk_event.edge_flags()
|
||||
}
|
||||
|
||||
/// Returns the time of this event, in the `java.lang.System.nanoTime()` time base
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_geteventtime)
|
||||
#[inline]
|
||||
pub fn event_time(&self) -> i64 {
|
||||
self.ndk_event.event_time()
|
||||
}
|
||||
|
||||
/// The flags associated with a motion event.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getflags)
|
||||
#[inline]
|
||||
pub fn flags(&self) -> MotionEventFlags {
|
||||
self.ndk_event.flags()
|
||||
}
|
||||
|
||||
/* Missing from GameActivity currently...
|
||||
/// Returns the offset in the x direction between the coordinates and the raw coordinates
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getxoffset)
|
||||
#[inline]
|
||||
pub fn x_offset(&self) -> f32 {
|
||||
self.ndk_event.x_offset()
|
||||
}
|
||||
|
||||
/// Returns the offset in the y direction between the coordinates and the raw coordinates
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getyoffset)
|
||||
#[inline]
|
||||
pub fn y_offset(&self) -> f32 {
|
||||
self.ndk_event.y_offset()
|
||||
}
|
||||
*/
|
||||
|
||||
/// Returns the precision of the x value of the coordinates
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getxprecision)
|
||||
#[inline]
|
||||
pub fn x_precision(&self) -> f32 {
|
||||
self.ndk_event.x_precision()
|
||||
}
|
||||
|
||||
/// Returns the precision of the y value of the coordinates
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getyprecision)
|
||||
#[inline]
|
||||
pub fn y_precision(&self) -> f32 {
|
||||
self.ndk_event.y_precision()
|
||||
}
|
||||
}
|
||||
|
||||
/// A key event
|
||||
///
|
||||
/// For general discussion of key events in Android, see [the relevant
|
||||
/// javadoc](https://developer.android.com/reference/android/view/KeyEvent).
|
||||
#[derive(Debug)]
|
||||
#[repr(transparent)]
|
||||
pub struct KeyEvent<'a> {
|
||||
ndk_event: ndk::event::KeyEvent,
|
||||
_lifetime: PhantomData<&'a ndk::event::KeyEvent>,
|
||||
}
|
||||
impl<'a> KeyEvent<'a> {
|
||||
pub(crate) fn new(ndk_event: ndk::event::KeyEvent) -> Self {
|
||||
Self {
|
||||
ndk_event,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
}
|
||||
pub(crate) fn into_ndk_event(self) -> ndk::event::KeyEvent {
|
||||
self.ndk_event
|
||||
}
|
||||
|
||||
/// Get the source of the event.
|
||||
///
|
||||
#[inline]
|
||||
pub fn source(&self) -> Source {
|
||||
// XXX: we use `AInputEvent_getSource` directly (instead of calling
|
||||
// ndk_event.source()) since we have our own `Source` enum that we
|
||||
// share between backends, which may not exactly match the ndk crate's
|
||||
// `Source` enum.
|
||||
let source =
|
||||
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
source.try_into().unwrap_or(Source::Unknown)
|
||||
}
|
||||
|
||||
/// Get the class of the event source.
|
||||
///
|
||||
#[inline]
|
||||
pub fn class(&self) -> Class {
|
||||
Class::from(self.source())
|
||||
}
|
||||
|
||||
/// Get the device id associated with the event.
|
||||
///
|
||||
#[inline]
|
||||
pub fn device_id(&self) -> i32 {
|
||||
self.ndk_event.device_id()
|
||||
}
|
||||
|
||||
/// Returns the key action associated with the event.
|
||||
///
|
||||
/// See [the KeyEvent docs](https://developer.android.com/reference/android/view/KeyEvent#getAction())
|
||||
#[inline]
|
||||
pub fn action(&self) -> KeyAction {
|
||||
self.ndk_event.action()
|
||||
}
|
||||
|
||||
/// Returns the last time the key was pressed. This is on the scale of
|
||||
/// `java.lang.System.nanoTime()`, which has nanosecond precision, but no defined start time.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getdowntime)
|
||||
#[inline]
|
||||
pub fn down_time(&self) -> i64 {
|
||||
self.ndk_event.down_time()
|
||||
}
|
||||
|
||||
/// Returns the time this event occured. This is on the scale of
|
||||
/// `java.lang.System.nanoTime()`, which has nanosecond precision, but no defined start time.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_geteventtime)
|
||||
#[inline]
|
||||
pub fn event_time(&self) -> i64 {
|
||||
self.ndk_event.event_time()
|
||||
}
|
||||
|
||||
/// Returns the keycode associated with this key event
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getkeycode)
|
||||
#[inline]
|
||||
pub fn key_code(&self) -> Keycode {
|
||||
self.ndk_event.key_code()
|
||||
}
|
||||
|
||||
/// Returns the number of repeats of a key.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getrepeatcount)
|
||||
#[inline]
|
||||
pub fn repeat_count(&self) -> i32 {
|
||||
self.ndk_event.repeat_count()
|
||||
}
|
||||
|
||||
/// Returns the hardware keycode of a key. This varies from device to device.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getscancode)
|
||||
#[inline]
|
||||
pub fn scan_code(&self) -> i32 {
|
||||
self.ndk_event.scan_code()
|
||||
}
|
||||
}
|
||||
|
||||
// We use our own wrapper type for input events to have better consistency
|
||||
// with GameActivity and ensure the enum can be extended without needing a
|
||||
// semver bump
|
||||
/// Enum of possible input events
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum InputEvent<'a> {
|
||||
MotionEvent(self::MotionEvent<'a>),
|
||||
KeyEvent(self::KeyEvent<'a>),
|
||||
}
|
||||
@@ -1,150 +1,179 @@
|
||||
#![cfg(any(feature = "native-activity", doc))]
|
||||
|
||||
use std::ptr;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::ops::Deref;
|
||||
use std::os::raw;
|
||||
use std::os::unix::prelude::*;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::{ptr, thread};
|
||||
|
||||
use libc::c_void;
|
||||
use log::{error, trace};
|
||||
use ndk::{asset::AssetManager, native_window::NativeWindow};
|
||||
use log::{error, info, trace, Level};
|
||||
|
||||
use crate::{
|
||||
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
|
||||
};
|
||||
use ndk_sys::ALooper_wake;
|
||||
use ndk_sys::{ALooper, ALooper_pollAll};
|
||||
|
||||
pub mod input;
|
||||
use ndk::asset::AssetManager;
|
||||
use ndk::configuration::Configuration;
|
||||
use ndk::input_queue::InputQueue;
|
||||
use ndk::native_window::NativeWindow;
|
||||
|
||||
mod glue;
|
||||
use self::glue::NativeActivityGlue;
|
||||
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
|
||||
|
||||
pub const LOOPER_ID_MAIN: libc::c_int = 1;
|
||||
pub const LOOPER_ID_INPUT: libc::c_int = 2;
|
||||
//pub const LOOPER_ID_USER: ::std::os::raw::c_uint = 3;
|
||||
mod ffi;
|
||||
|
||||
/// An interface for saving application state during [MainEvent::SaveState] events
|
||||
///
|
||||
/// This interface is only available temporarily while handling a [MainEvent::SaveState] event.
|
||||
pub mod input {
|
||||
pub use ndk::event::{
|
||||
Axis, ButtonState, EdgeFlags, KeyAction, KeyEvent, KeyEventFlags, Keycode, MetaState,
|
||||
MotionAction, MotionEvent, MotionEventFlags, Pointer, Source,
|
||||
};
|
||||
|
||||
// We use our own wrapper type for input events to have better consistency
|
||||
// with GameActivity and ensure the enum can be extended without needing a
|
||||
// semver bump
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum InputEvent {
|
||||
MotionEvent(self::MotionEvent),
|
||||
KeyEvent(self::KeyEvent),
|
||||
}
|
||||
}
|
||||
|
||||
// The only time it's safe to update the android_app->savedState pointer is
|
||||
// while handling a SaveState event, so this API is only exposed for those
|
||||
// events...
|
||||
#[derive(Debug)]
|
||||
pub struct StateSaver<'a> {
|
||||
app: &'a AndroidAppInner,
|
||||
}
|
||||
|
||||
impl<'a> StateSaver<'a> {
|
||||
/// Stores the given `state` such that it will be available to load the next
|
||||
/// time that the application resumes.
|
||||
pub fn store(&self, state: &'a [u8]) {
|
||||
self.app.native_activity.set_saved_state(state);
|
||||
// android_native_app_glue specifically expects savedState to have been allocated
|
||||
// via libc::malloc since it will automatically handle freeing the data once it
|
||||
// has been handed over to the Java Activity / main thread.
|
||||
unsafe {
|
||||
let app_ptr = self.app.native_app.as_ptr();
|
||||
|
||||
// In case the application calls store() multiple times for some reason we
|
||||
// make sure to free any pre-existing state...
|
||||
if (*app_ptr).savedState != ptr::null_mut() {
|
||||
libc::free((*app_ptr).savedState);
|
||||
(*app_ptr).savedState = ptr::null_mut();
|
||||
(*app_ptr).savedStateSize = 0;
|
||||
}
|
||||
|
||||
let buf = libc::malloc(state.len());
|
||||
if buf == ptr::null_mut() {
|
||||
panic!("Failed to allocate save_state buffer");
|
||||
}
|
||||
|
||||
// Since it's a byte array there's no special alignment requirement here.
|
||||
//
|
||||
// Since we re-define `buf` we ensure it's not possible to access the buffer
|
||||
// via its original pointer for the lifetime of the slice.
|
||||
{
|
||||
let buf: &mut [u8] = std::slice::from_raw_parts_mut(buf.cast(), state.len());
|
||||
buf.copy_from_slice(state);
|
||||
}
|
||||
|
||||
(*app_ptr).savedState = buf;
|
||||
(*app_ptr).savedStateSize = state.len() as _;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface for loading application state during [MainEvent::Resume] events
|
||||
///
|
||||
/// This interface is only available temporarily while handling a [MainEvent::Resume] event.
|
||||
#[derive(Debug)]
|
||||
pub struct StateLoader<'a> {
|
||||
app: &'a AndroidAppInner,
|
||||
}
|
||||
impl<'a> StateLoader<'a> {
|
||||
/// Returns whatever state was saved during the last [MainEvent::SaveState] event or `None`
|
||||
pub fn load(&self) -> Option<Vec<u8>> {
|
||||
self.app.native_activity.saved_state()
|
||||
unsafe {
|
||||
let app_ptr = self.app.native_app.as_ptr();
|
||||
if (*app_ptr).savedState != ptr::null_mut() && (*app_ptr).savedStateSize > 0 {
|
||||
let buf: &mut [u8] = std::slice::from_raw_parts_mut(
|
||||
(*app_ptr).savedState.cast(),
|
||||
(*app_ptr).savedStateSize as usize,
|
||||
);
|
||||
let state = buf.to_vec();
|
||||
Some(state)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A means to wake up the main thread while it is blocked waiting for I/O
|
||||
#[derive(Clone)]
|
||||
pub struct AndroidAppWaker {
|
||||
// The looper pointer is owned by the android_app and effectively
|
||||
// has a 'static lifetime, and the ALooper_wake C API is thread
|
||||
// safe, so this can be cloned safely and is send + sync safe
|
||||
looper: NonNull<ndk_sys::ALooper>,
|
||||
looper: NonNull<ALooper>,
|
||||
}
|
||||
unsafe impl Send for AndroidAppWaker {}
|
||||
unsafe impl Sync for AndroidAppWaker {}
|
||||
|
||||
impl AndroidAppWaker {
|
||||
/// Interrupts the main thread if it is blocked within [`AndroidApp::poll_events()`]
|
||||
///
|
||||
/// If [`AndroidApp::poll_events()`] is interrupted it will invoke the poll
|
||||
/// callback with a [PollEvent::Wake][wake_event] event.
|
||||
///
|
||||
/// [wake_event]: crate::PollEvent::Wake
|
||||
pub fn wake(&self) {
|
||||
unsafe {
|
||||
ndk_sys::ALooper_wake(self.looper.as_ptr());
|
||||
ALooper_wake(self.looper.as_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AndroidApp {
|
||||
pub(crate) fn new(native_activity: NativeActivityGlue) -> Self {
|
||||
let app = Self {
|
||||
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>) -> AndroidApp {
|
||||
// Note: we don't use from_ptr since we don't own the android_app.config
|
||||
// and need to keep in mind that the Drop handler is going to call
|
||||
// AConfiguration_delete()
|
||||
let config = Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config));
|
||||
|
||||
AndroidApp {
|
||||
inner: Arc::new(RwLock::new(AndroidAppInner {
|
||||
native_activity,
|
||||
looper: Looper {
|
||||
ptr: ptr::null_mut(),
|
||||
},
|
||||
native_app: NativeAppGlue { ptr },
|
||||
config: ConfigurationRef::new(config),
|
||||
native_window: Default::default(),
|
||||
})),
|
||||
};
|
||||
|
||||
{
|
||||
let mut guard = app.inner.write().unwrap();
|
||||
|
||||
let main_fd = guard.native_activity.cmd_read_fd();
|
||||
unsafe {
|
||||
guard.looper.ptr = ndk_sys::ALooper_prepare(
|
||||
ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int,
|
||||
);
|
||||
ndk_sys::ALooper_addFd(
|
||||
guard.looper.ptr,
|
||||
main_fd,
|
||||
LOOPER_ID_MAIN,
|
||||
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
|
||||
None,
|
||||
//&mut guard.cmd_poll_source as *mut _ as *mut _);
|
||||
ptr::null_mut(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Looper {
|
||||
pub ptr: *mut ndk_sys::ALooper,
|
||||
struct NativeAppGlue {
|
||||
ptr: NonNull<ffi::android_app>,
|
||||
}
|
||||
unsafe impl Send for Looper {}
|
||||
unsafe impl Sync for Looper {}
|
||||
impl Deref for NativeAppGlue {
|
||||
type Target = NonNull<ffi::android_app>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.ptr
|
||||
}
|
||||
}
|
||||
unsafe impl Send for NativeAppGlue {}
|
||||
unsafe impl Sync for NativeAppGlue {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AndroidAppInner {
|
||||
pub(crate) native_activity: NativeActivityGlue,
|
||||
looper: Looper,
|
||||
native_app: NativeAppGlue,
|
||||
config: ConfigurationRef,
|
||||
native_window: RwLock<Option<NativeWindow>>,
|
||||
}
|
||||
|
||||
impl AndroidAppInner {
|
||||
pub(crate) fn vm_as_ptr(&self) -> *mut c_void {
|
||||
unsafe { (*self.native_activity.activity).vm as _ }
|
||||
}
|
||||
|
||||
pub(crate) fn activity_as_ptr(&self) -> *mut c_void {
|
||||
// "clazz" is a completely bogus name; this is the _instance_ not class pointer
|
||||
unsafe { (*self.native_activity.activity).clazz as _ }
|
||||
}
|
||||
|
||||
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
|
||||
self.native_activity.activity
|
||||
unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
(*app_ptr).activity.cast()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn looper(&self) -> *mut ndk_sys::ALooper {
|
||||
self.looper.ptr
|
||||
}
|
||||
|
||||
pub fn native_window(&self) -> Option<NativeWindow> {
|
||||
self.native_activity.mutex.lock().unwrap().window.clone()
|
||||
pub fn native_window<'a>(&self) -> Option<NativeWindow> {
|
||||
self.native_window.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
|
||||
@@ -154,6 +183,8 @@ impl AndroidAppInner {
|
||||
trace!("poll_events");
|
||||
|
||||
unsafe {
|
||||
let native_app = &self.native_app;
|
||||
|
||||
let mut fd: i32 = 0;
|
||||
let mut events: i32 = 0;
|
||||
let mut source: *mut core::ffi::c_void = ptr::null_mut();
|
||||
@@ -163,102 +194,126 @@ impl AndroidAppInner {
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
|
||||
trace!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
|
||||
assert!(
|
||||
!ndk_sys::ALooper_forThread().is_null(),
|
||||
"Application tried to poll events from non-main thread"
|
||||
);
|
||||
let id = ndk_sys::ALooper_pollAll(
|
||||
info!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
|
||||
let id = ALooper_pollAll(
|
||||
timeout_milliseconds,
|
||||
&mut fd,
|
||||
&mut events,
|
||||
&mut source as *mut *mut core::ffi::c_void,
|
||||
);
|
||||
trace!("pollAll id = {id}");
|
||||
info!("pollAll id = {id}");
|
||||
match id {
|
||||
ndk_sys::ALOOPER_POLL_WAKE => {
|
||||
ffi::ALOOPER_POLL_WAKE => {
|
||||
trace!("ALooper_pollAll returned POLL_WAKE");
|
||||
callback(PollEvent::Wake);
|
||||
}
|
||||
ndk_sys::ALOOPER_POLL_CALLBACK => {
|
||||
ffi::ALOOPER_POLL_CALLBACK => {
|
||||
// ALooper_pollAll is documented to handle all callback sources internally so it should
|
||||
// never return a _CALLBACK source id...
|
||||
error!("Spurious ALOOPER_POLL_CALLBACK from ALopper_pollAll() (ignored)");
|
||||
}
|
||||
ndk_sys::ALOOPER_POLL_TIMEOUT => {
|
||||
ffi::ALOOPER_POLL_TIMEOUT => {
|
||||
trace!("ALooper_pollAll returned POLL_TIMEOUT");
|
||||
callback(PollEvent::Timeout);
|
||||
}
|
||||
ndk_sys::ALOOPER_POLL_ERROR => {
|
||||
ffi::ALOOPER_POLL_ERROR => {
|
||||
// If we have an IO error with our pipe to the main Java thread that's surely
|
||||
// not something we can recover from
|
||||
panic!("ALooper_pollAll returned POLL_ERROR");
|
||||
}
|
||||
id if id >= 0 => {
|
||||
match id {
|
||||
LOOPER_ID_MAIN => {
|
||||
match id as u32 {
|
||||
ffi::LOOPER_ID_MAIN => {
|
||||
trace!("ALooper_pollAll returned ID_MAIN");
|
||||
if let Some(ipc_cmd) = self.native_activity.read_cmd() {
|
||||
let main_cmd = match ipc_cmd {
|
||||
let source: *mut ffi::android_poll_source = source.cast();
|
||||
if source != ptr::null_mut() {
|
||||
let cmd_i = ffi::android_app_read_cmd(native_app.as_ptr());
|
||||
|
||||
let cmd = match cmd_i as u32 {
|
||||
// We don't forward info about the AInputQueue to apps since it's
|
||||
// an implementation details that's also not compatible with
|
||||
// GameActivity
|
||||
glue::AppCmd::InputQueueChanged => None,
|
||||
ffi::APP_CMD_INPUT_CHANGED => None,
|
||||
|
||||
glue::AppCmd::InitWindow => Some(MainEvent::InitWindow {}),
|
||||
glue::AppCmd::TermWindow => Some(MainEvent::TerminateWindow {}),
|
||||
glue::AppCmd::WindowResized => {
|
||||
ffi::APP_CMD_INIT_WINDOW => Some(MainEvent::InitWindow {}),
|
||||
ffi::APP_CMD_TERM_WINDOW => Some(MainEvent::TerminateWindow {}),
|
||||
ffi::APP_CMD_WINDOW_RESIZED => {
|
||||
Some(MainEvent::WindowResized {})
|
||||
}
|
||||
glue::AppCmd::WindowRedrawNeeded => {
|
||||
ffi::APP_CMD_WINDOW_REDRAW_NEEDED => {
|
||||
Some(MainEvent::RedrawNeeded {})
|
||||
}
|
||||
glue::AppCmd::ContentRectChanged => {
|
||||
ffi::APP_CMD_CONTENT_RECT_CHANGED => {
|
||||
Some(MainEvent::ContentRectChanged {})
|
||||
}
|
||||
glue::AppCmd::GainedFocus => Some(MainEvent::GainedFocus),
|
||||
glue::AppCmd::LostFocus => Some(MainEvent::LostFocus),
|
||||
glue::AppCmd::ConfigChanged => {
|
||||
ffi::APP_CMD_GAINED_FOCUS => Some(MainEvent::GainedFocus),
|
||||
ffi::APP_CMD_LOST_FOCUS => Some(MainEvent::LostFocus),
|
||||
ffi::APP_CMD_CONFIG_CHANGED => {
|
||||
Some(MainEvent::ConfigChanged {})
|
||||
}
|
||||
glue::AppCmd::LowMemory => Some(MainEvent::LowMemory),
|
||||
glue::AppCmd::Start => Some(MainEvent::Start),
|
||||
glue::AppCmd::Resume => Some(MainEvent::Resume {
|
||||
loader: StateLoader { app: self },
|
||||
ffi::APP_CMD_LOW_MEMORY => Some(MainEvent::LowMemory),
|
||||
ffi::APP_CMD_START => Some(MainEvent::Start),
|
||||
ffi::APP_CMD_RESUME => Some(MainEvent::Resume {
|
||||
loader: StateLoader { app: &self },
|
||||
}),
|
||||
glue::AppCmd::SaveState => Some(MainEvent::SaveState {
|
||||
saver: StateSaver { app: self },
|
||||
ffi::APP_CMD_SAVE_STATE => Some(MainEvent::SaveState {
|
||||
saver: StateSaver { app: &self },
|
||||
}),
|
||||
glue::AppCmd::Pause => Some(MainEvent::Pause),
|
||||
glue::AppCmd::Stop => Some(MainEvent::Stop),
|
||||
glue::AppCmd::Destroy => Some(MainEvent::Destroy),
|
||||
ffi::APP_CMD_PAUSE => Some(MainEvent::Pause),
|
||||
ffi::APP_CMD_STOP => Some(MainEvent::Stop),
|
||||
ffi::APP_CMD_DESTROY => Some(MainEvent::Destroy),
|
||||
|
||||
//ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_INSETS_CHANGED => MainEvent::InsetsChanged {},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
trace!("Calling pre_exec_cmd({ipc_cmd:#?})");
|
||||
self.native_activity.pre_exec_cmd(
|
||||
ipc_cmd,
|
||||
self.looper(),
|
||||
LOOPER_ID_INPUT,
|
||||
);
|
||||
trace!("Calling android_app_pre_exec_cmd({cmd_i})");
|
||||
ffi::android_app_pre_exec_cmd(native_app.as_ptr(), cmd_i);
|
||||
|
||||
if let Some(main_cmd) = main_cmd {
|
||||
trace!("Invoking callback for ID_MAIN command = {main_cmd:?}");
|
||||
callback(PollEvent::Main(main_cmd));
|
||||
if let Some(cmd) = cmd {
|
||||
trace!("Read ID_MAIN command {cmd_i} = {cmd:?}");
|
||||
match cmd {
|
||||
MainEvent::ConfigChanged { .. } => {
|
||||
self.config.replace(Configuration::clone_from_ptr(
|
||||
NonNull::new_unchecked(
|
||||
(*native_app.as_ptr()).config,
|
||||
),
|
||||
));
|
||||
}
|
||||
MainEvent::InitWindow { .. } => {
|
||||
let win_ptr = (*native_app.as_ptr()).window;
|
||||
// It's important that we use ::clone_from_ptr() here
|
||||
// because NativeWindow has a Drop implementation that
|
||||
// will unconditionally _release() the native window
|
||||
*self.native_window.write().unwrap() =
|
||||
Some(NativeWindow::clone_from_ptr(
|
||||
NonNull::new(win_ptr).unwrap(),
|
||||
));
|
||||
}
|
||||
MainEvent::TerminateWindow { .. } => {
|
||||
*self.native_window.write().unwrap() = None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
trace!("Invoking callback for ID_MAIN command = {:?}", cmd);
|
||||
callback(PollEvent::Main(cmd));
|
||||
}
|
||||
|
||||
trace!("Calling post_exec_cmd({ipc_cmd:#?})");
|
||||
self.native_activity.post_exec_cmd(ipc_cmd);
|
||||
trace!("Calling android_app_post_exec_cmd({cmd_i})");
|
||||
ffi::android_app_post_exec_cmd(native_app.as_ptr(), cmd_i);
|
||||
} else {
|
||||
panic!("ALooper_pollAll returned ID_MAIN event with NULL android_poll_source!");
|
||||
}
|
||||
}
|
||||
LOOPER_ID_INPUT => {
|
||||
ffi::LOOPER_ID_INPUT => {
|
||||
trace!("ALooper_pollAll returned ID_INPUT");
|
||||
|
||||
// To avoid spamming the application with event loop iterations notifying them of
|
||||
// input events then we only send one `InputAvailable` per iteration of input
|
||||
// handling. We re-attach the looper when the application calls
|
||||
// `AndroidApp::input_events()`
|
||||
self.native_activity.detach_input_queue_from_looper();
|
||||
ffi::android_app_detach_input_queue_looper(native_app.as_ptr());
|
||||
callback(PollEvent::Main(MainEvent::InputAvailable))
|
||||
}
|
||||
_ => {
|
||||
@@ -275,26 +330,35 @@ impl AndroidAppInner {
|
||||
|
||||
pub fn create_waker(&self) -> AndroidAppWaker {
|
||||
unsafe {
|
||||
// From the application's pov we assume the looper pointer has a static
|
||||
// lifetimes and we can safely assume it is never NULL.
|
||||
// From the application's pov we assume the app_ptr and looper pointer
|
||||
// have static lifetimes and we can safely assume they are never NULL.
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
AndroidAppWaker {
|
||||
looper: NonNull::new_unchecked(self.looper.ptr),
|
||||
looper: NonNull::new_unchecked((*app_ptr).looper),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> ConfigurationRef {
|
||||
self.native_activity.config()
|
||||
self.config.clone()
|
||||
}
|
||||
|
||||
pub fn content_rect(&self) -> Rect {
|
||||
self.native_activity.content_rect()
|
||||
unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
Rect {
|
||||
left: (*app_ptr).contentRect.left,
|
||||
right: (*app_ptr).contentRect.right,
|
||||
top: (*app_ptr).contentRect.top,
|
||||
bottom: (*app_ptr).contentRect.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn asset_manager(&self) -> AssetManager {
|
||||
unsafe {
|
||||
let activity_ptr = self.native_activity.activity;
|
||||
let am_ptr = NonNull::new_unchecked((*activity_ptr).assetManager);
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
let am_ptr = NonNull::new_unchecked((*(*app_ptr).activity).assetManager);
|
||||
AssetManager::from_ptr(am_ptr)
|
||||
}
|
||||
}
|
||||
@@ -307,7 +371,7 @@ impl AndroidAppInner {
|
||||
let na = self.native_activity();
|
||||
let na_mut = na as *mut ndk_sys::ANativeActivity;
|
||||
unsafe {
|
||||
ndk_sys::ANativeActivity_setWindowFlags(
|
||||
ffi::ANativeActivity_setWindowFlags(
|
||||
na_mut.cast(),
|
||||
add_flags.bits(),
|
||||
remove_flags.bits(),
|
||||
@@ -320,11 +384,11 @@ impl AndroidAppInner {
|
||||
let na = self.native_activity();
|
||||
unsafe {
|
||||
let flags = if show_implicit {
|
||||
ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT
|
||||
ffi::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ndk_sys::ANativeActivity_showSoftInput(na as *mut _, flags);
|
||||
ffi::ANativeActivity_showSoftInput(na as *mut _, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,11 +397,11 @@ impl AndroidAppInner {
|
||||
let na = self.native_activity();
|
||||
unsafe {
|
||||
let flags = if hide_implicit_only {
|
||||
ndk_sys::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY
|
||||
ffi::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ndk_sys::ANativeActivity_hideSoftInput(na as *mut _, flags);
|
||||
ffi::ANativeActivity_hideSoftInput(na as *mut _, flags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,19 +413,22 @@ impl AndroidAppInner {
|
||||
// NOP - The InputQueue API doesn't let us optimize which axis values are read
|
||||
}
|
||||
|
||||
pub fn input_events<F>(&self, mut callback: F)
|
||||
pub fn input_events<'b, F>(&self, mut callback: F)
|
||||
where
|
||||
F: FnMut(&input::InputEvent) -> InputStatus,
|
||||
F: FnMut(&input::InputEvent),
|
||||
{
|
||||
// Get the InputQueue for the NativeActivity (if there is one) and also ensure
|
||||
// the queue is re-attached to our event Looper (so new input events will again
|
||||
// trigger a wake up)
|
||||
let queue = self
|
||||
.native_activity
|
||||
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
|
||||
let queue = match queue {
|
||||
Some(queue) => queue,
|
||||
None => return,
|
||||
let queue = unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
if (*app_ptr).inputQueue == ptr::null_mut() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reattach the input queue to the looper so future input will again deliver an
|
||||
// `InputAvailable` event.
|
||||
ffi::android_app_attach_input_queue_looper(app_ptr);
|
||||
|
||||
let queue = NonNull::new_unchecked((*app_ptr).inputQueue);
|
||||
InputQueue::from_ptr(queue)
|
||||
};
|
||||
|
||||
// Note: we basically ignore errors from get_event() currently. Looking
|
||||
@@ -374,24 +441,23 @@ impl AndroidAppInner {
|
||||
while let Ok(Some(event)) = queue.get_event() {
|
||||
if let Some(ndk_event) = queue.pre_dispatch(event) {
|
||||
let event = match ndk_event {
|
||||
ndk::event::InputEvent::MotionEvent(e) => {
|
||||
input::InputEvent::MotionEvent(input::MotionEvent::new(e))
|
||||
}
|
||||
ndk::event::InputEvent::KeyEvent(e) => {
|
||||
input::InputEvent::KeyEvent(input::KeyEvent::new(e))
|
||||
}
|
||||
ndk::event::InputEvent::MotionEvent(e) => input::InputEvent::MotionEvent(e),
|
||||
ndk::event::InputEvent::KeyEvent(e) => input::InputEvent::KeyEvent(e),
|
||||
};
|
||||
let handled = callback(&event);
|
||||
callback(&event);
|
||||
|
||||
let ndk_event = match event {
|
||||
input::InputEvent::MotionEvent(e) => {
|
||||
ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
|
||||
}
|
||||
input::InputEvent::KeyEvent(e) => {
|
||||
ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
|
||||
}
|
||||
input::InputEvent::MotionEvent(e) => ndk::event::InputEvent::MotionEvent(e),
|
||||
input::InputEvent::KeyEvent(e) => ndk::event::InputEvent::KeyEvent(e),
|
||||
};
|
||||
queue.finish_event(ndk_event, matches!(handled, InputStatus::Handled));
|
||||
|
||||
// Always report events as 'handled'. This means we won't get
|
||||
// so called 'fallback' events generated (such as converting trackball
|
||||
// events into emulated keypad events), but we could conceivably
|
||||
// implement similar emulation somewhere else in the stack if
|
||||
// necessary, and this will be more consistent with the GameActivity
|
||||
// input handling that doesn't do any kind of emulation.
|
||||
queue.finish_event(ndk_event, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -411,3 +477,104 @@ impl AndroidAppInner {
|
||||
unsafe { util::try_get_path_from_ptr((*na).obbPath) }
|
||||
}
|
||||
}
|
||||
|
||||
// Rust doesn't give us a clean way to directly export symbols from C/C++
|
||||
// so we rename the C/C++ symbols and re-export this entrypoint from
|
||||
// Rust...
|
||||
//
|
||||
// https://github.com/rust-lang/rfcs/issues/2771
|
||||
extern "C" {
|
||||
pub fn ANativeActivity_onCreate_C(
|
||||
activity: *mut std::os::raw::c_void,
|
||||
savedState: *mut ::std::os::raw::c_void,
|
||||
savedStateSize: usize,
|
||||
);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
unsafe extern "C" fn ANativeActivity_onCreate(
|
||||
activity: *mut std::os::raw::c_void,
|
||||
saved_state: *mut std::os::raw::c_void,
|
||||
saved_state_size: usize,
|
||||
) {
|
||||
ANativeActivity_onCreate_C(activity, saved_state, saved_state_size);
|
||||
}
|
||||
|
||||
fn android_log(level: Level, tag: &CStr, msg: &CStr) {
|
||||
let prio = match level {
|
||||
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
|
||||
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
|
||||
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
|
||||
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
|
||||
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
|
||||
};
|
||||
unsafe {
|
||||
ndk_sys::__android_log_write(prio.0 as raw::c_int, tag.as_ptr(), msg.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
extern "Rust" {
|
||||
pub fn android_main(app: AndroidApp);
|
||||
}
|
||||
|
||||
// This is a spring board between android_native_app_glue and the user's
|
||||
// `app_main` function. This is run on a dedicated thread spawned
|
||||
// by android_native_app_glue.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn _rust_glue_entry(app: *mut ffi::android_app) {
|
||||
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
|
||||
let mut logpipe: [RawFd; 2] = Default::default();
|
||||
libc::pipe(logpipe.as_mut_ptr());
|
||||
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
|
||||
libc::dup2(logpipe[1], libc::STDERR_FILENO);
|
||||
thread::spawn(move || {
|
||||
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
|
||||
let file = File::from_raw_fd(logpipe[0]);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
if let Ok(len) = reader.read_line(&mut buffer) {
|
||||
if len == 0 {
|
||||
break;
|
||||
} else if let Ok(msg) = CString::new(buffer.clone()) {
|
||||
android_log(Level::Info, tag, &msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let app = AndroidApp::from_ptr(NonNull::new(app).unwrap());
|
||||
|
||||
let na = app.native_activity();
|
||||
let jvm = (*na).vm;
|
||||
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
|
||||
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
|
||||
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
|
||||
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
// XXX: If we were in control of the Java Activity subclass then
|
||||
// we could potentially run the android_main function via a Java native method
|
||||
// springboard (e.g. call an Activity subclass method that calls a jni native
|
||||
// method that then just calls android_main()) that would make sure there was
|
||||
// a Java frame at the base of our call stack which would then be recognised
|
||||
// when calling FindClass to lookup a suitable classLoader, instead of
|
||||
// defaulting to the system loader. Without this then it's difficult for native
|
||||
// code to look up non-standard Java classes.
|
||||
android_main(app);
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
|
||||
detach_current_thread(jvm);
|
||||
}
|
||||
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
|
||||
@@ -1,70 +1,15 @@
|
||||
use log::Level;
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
os::raw::c_char,
|
||||
};
|
||||
use std::{ffi::CStr, os::raw::c_char, ptr};
|
||||
|
||||
pub fn try_get_path_from_ptr(path: *const c_char) -> Option<std::path::PathBuf> {
|
||||
if path.is_null() {
|
||||
if path == ptr::null() {
|
||||
return None;
|
||||
}
|
||||
let cstr = unsafe {
|
||||
let cstr_slice = CStr::from_ptr(path.cast());
|
||||
cstr_slice.to_str().ok()?
|
||||
};
|
||||
if cstr.is_empty() {
|
||||
if cstr.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(std::path::PathBuf::from(cstr))
|
||||
}
|
||||
|
||||
pub(crate) fn android_log(level: Level, tag: &CStr, msg: &CStr) {
|
||||
let prio = match level {
|
||||
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
|
||||
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
|
||||
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
|
||||
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
|
||||
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
|
||||
};
|
||||
unsafe {
|
||||
ndk_sys::__android_log_write(prio.0 as libc::c_int, tag.as_ptr(), msg.as_ptr());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn log_panic(panic: Box<dyn std::any::Any + Send>) {
|
||||
let rust_panic = unsafe { CStr::from_bytes_with_nul_unchecked(b"RustPanic\0") };
|
||||
|
||||
if let Some(panic) = panic.downcast_ref::<String>() {
|
||||
if let Ok(msg) = CString::new(panic.clone()) {
|
||||
android_log(Level::Error, rust_panic, &msg);
|
||||
}
|
||||
} else if let Ok(panic) = panic.downcast::<&str>() {
|
||||
if let Ok(msg) = CString::new(*panic) {
|
||||
android_log(Level::Error, rust_panic, &msg);
|
||||
}
|
||||
} else {
|
||||
let unknown_panic = unsafe { CStr::from_bytes_with_nul_unchecked(b"UnknownPanic\0") };
|
||||
android_log(Level::Error, unknown_panic, unsafe {
|
||||
CStr::from_bytes_with_nul_unchecked(b"\0")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a closure and abort the program if it panics.
|
||||
///
|
||||
/// This is generally used to ensure Rust callbacks won't unwind past the JNI boundary, which leads
|
||||
/// to undefined behaviour.
|
||||
///
|
||||
/// TODO(rib): throw a Java exception instead of aborting. An Android Activity does not necessarily
|
||||
/// own the entire process because other application Services (or even Activities) may run in
|
||||
/// threads within the same process, and so we're tearing down too much by aborting the process.
|
||||
pub(crate) fn abort_on_panic<R>(f: impl FnOnce() -> R) -> R {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|panic| {
|
||||
// Try logging the panic before aborting
|
||||
//
|
||||
// Just in case our attempt to log a panic could itself cause a panic we use a
|
||||
// second catch_unwind here.
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| log_panic(panic)));
|
||||
std::process::abort();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
*.so
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
Cargo.lock
|
||||
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "agdk-cpal"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
android_logger = "0.11.0"
|
||||
android-activity = { path="../../android-activity", features = ["game-activity"] }
|
||||
cpal = "0.14"
|
||||
atomic_float = "0.1"
|
||||
anyhow = "1"
|
||||
|
||||
[lib]
|
||||
name="main"
|
||||
crate_type=["cdylib"]
|
||||
@@ -0,0 +1,16 @@
|
||||
This is a minimal test application based on `GameActivity` that just
|
||||
runs a mainloop based on android_activity::poll_events() and plays a
|
||||
sine wave audio test using the Cpal audio library.
|
||||
|
||||
```
|
||||
export ANDROID_NDK_HOME="path/to/ndk"
|
||||
export ANDROID_HOME="path/to/sdk"
|
||||
|
||||
rustup target add aarch64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ build
|
||||
./gradlew build
|
||||
./gradlew installDebug
|
||||
adb shell am start -n co.realfit.agdkcpal/.MainActivity
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
|
||||
defaultConfig {
|
||||
applicationId "co.realfit.agdkcpal"
|
||||
minSdk 28
|
||||
targetSdk 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
//packagingOptions {
|
||||
// doNotStrip '**/*.so'
|
||||
//}
|
||||
//debuggable true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
// To use the Android Frame Pacing library
|
||||
//implementation "androidx.games:games-frame-pacing:1.9.1"
|
||||
|
||||
// To use the Android Performance Tuner
|
||||
//implementation "androidx.games:games-performance-tuner:1.5.0"
|
||||
|
||||
// To use the Games Activity library
|
||||
implementation "androidx.games:games-activity:1.1.0"
|
||||
|
||||
// To use the Games Controller Library
|
||||
//implementation "androidx.games:games-controller:1.1.0"
|
||||
|
||||
// To use the Games Text Input Library
|
||||
//implementation "androidx.games:games-text-input:1.1.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="co.realfit.agdkcpal">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="AGDK Cpal"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RustTemplate">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.lib_name" android:value="main" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,70 @@
|
||||
package co.realfit.agdkcpal;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.androidgamesdk.GameActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
public class MainActivity extends GameActivity {
|
||||
|
||||
static {
|
||||
// Load the STL first to workaround issues on old Android versions:
|
||||
// "if your app targets a version of Android earlier than Android 4.3
|
||||
// (Android API level 18),
|
||||
// and you use libc++_shared.so, you must load the shared library before any other
|
||||
// library that depends on it."
|
||||
// See https://developer.android.com/ndk/guides/cpp-support#shared_runtimes
|
||||
//System.loadLibrary("c++_shared");
|
||||
|
||||
// Load the native library.
|
||||
// The name "android-game" depends on your CMake configuration, must be
|
||||
// consistent here and inside AndroidManifect.xml
|
||||
System.loadLibrary("main");
|
||||
}
|
||||
|
||||
private void hideSystemUI() {
|
||||
// This will put the game behind any cutouts and waterfalls on devices which have
|
||||
// them, so the corresponding insets will be non-zero.
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode
|
||||
= WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
}
|
||||
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
|
||||
// using View.setSystemUiVisibility.
|
||||
View decorView = getWindow().getDecorView();
|
||||
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
|
||||
decorView);
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||
controller.hide(WindowInsetsCompat.Type.displayCutout());
|
||||
controller.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// When true, the app will fit inside any system UI windows.
|
||||
// When false, we render behind any system UI windows.
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
hideSystemUI();
|
||||
// You can set IME fields here or in native code using GameActivity_setImeEditorInfoFields.
|
||||
// We set the fields in native_engine.cpp.
|
||||
// super.setImeEditorInfoFields(InputType.TYPE_CLASS_TEXT,
|
||||
// IME_ACTION_NONE, IME_FLAG_NO_FULLSCREEN );
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public boolean isGooglePlayGames() {
|
||||
PackageManager pm = getPackageManager();
|
||||
return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '7.1.2' apply false
|
||||
id 'com.android.library' version '7.1.2' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
@@ -0,0 +1,6 @@
|
||||
#Mon May 02 15:39:12 BST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,16 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
//rootProject.name = "Rust Template"
|
||||
include ':app'
|
||||
@@ -0,0 +1,142 @@
|
||||
use android_activity::{AndroidApp, MainEvent, PollEvent};
|
||||
use log::info;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
extern crate cpal;
|
||||
|
||||
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
|
||||
where
|
||||
T: cpal::Sample,
|
||||
{
|
||||
for frame in output.chunks_mut(channels) {
|
||||
let value: T = cpal::Sample::from::<f32>(&next_sample());
|
||||
for sample in frame.iter_mut() {
|
||||
*sample = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_audio_stream<T>(
|
||||
device: &cpal::Device,
|
||||
config: &cpal::StreamConfig,
|
||||
) -> Result<cpal::Stream, anyhow::Error>
|
||||
where
|
||||
T: cpal::Sample,
|
||||
{
|
||||
let sample_rate = config.sample_rate.0 as f32;
|
||||
let channels = config.channels as usize;
|
||||
|
||||
// Produce a sinusoid of maximum amplitude.
|
||||
let mut sample_clock = 0f32;
|
||||
let mut next_value = move || {
|
||||
sample_clock = (sample_clock + 1.0) % sample_rate;
|
||||
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
|
||||
};
|
||||
|
||||
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
|
||||
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
write_data(data, channels, &mut next_value)
|
||||
},
|
||||
err_fn,
|
||||
)?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn android_main(app: AndroidApp) {
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
|
||||
let mut quit = false;
|
||||
let mut redraw_pending = true;
|
||||
let mut render_state: Option<()> = Default::default();
|
||||
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("failed to find output device");
|
||||
|
||||
let config = device.default_output_config().unwrap();
|
||||
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => make_audio_stream::<f32>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::I16 => make_audio_stream::<i16>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::U16 => make_audio_stream::<u16>(&device, &config.into()).unwrap(),
|
||||
};
|
||||
|
||||
while !quit {
|
||||
app.poll_events(
|
||||
Some(std::time::Duration::from_millis(500)), /* timeout */
|
||||
|event| {
|
||||
match event {
|
||||
PollEvent::Wake => {
|
||||
info!("Early wake up");
|
||||
}
|
||||
PollEvent::Timeout => {
|
||||
info!("Timed out");
|
||||
// Real app would probably rely on vblank sync via graphics API...
|
||||
redraw_pending = true;
|
||||
}
|
||||
PollEvent::Main(main_event) => {
|
||||
info!("Main event: {:?}", main_event);
|
||||
match main_event {
|
||||
MainEvent::SaveState { saver, .. } => {
|
||||
saver.store("foo://bar".as_bytes());
|
||||
}
|
||||
MainEvent::Pause => {
|
||||
if let Err(err) = stream.pause() {
|
||||
log::error!("Failed to pause audio playback: {err}");
|
||||
}
|
||||
}
|
||||
MainEvent::Resume { loader, .. } => {
|
||||
if let Some(state) = loader.load() {
|
||||
if let Ok(uri) = String::from_utf8(state) {
|
||||
info!("Resumed with saved state = {uri:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = stream.play() {
|
||||
log::error!("Failed to start audio playback: {err}");
|
||||
}
|
||||
}
|
||||
MainEvent::InitWindow { .. } => {
|
||||
render_state = Some(());
|
||||
redraw_pending = true;
|
||||
}
|
||||
MainEvent::TerminateWindow { .. } => {
|
||||
render_state = None;
|
||||
}
|
||||
MainEvent::WindowResized { .. } => {
|
||||
redraw_pending = true;
|
||||
}
|
||||
MainEvent::RedrawNeeded { .. } => {
|
||||
redraw_pending = true;
|
||||
}
|
||||
MainEvent::LowMemory => {}
|
||||
|
||||
MainEvent::Destroy => quit = true,
|
||||
_ => { /* ... */ }
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if redraw_pending {
|
||||
if let Some(_rs) = render_state {
|
||||
redraw_pending = false;
|
||||
|
||||
// Handle input
|
||||
app.input_events(|event| {
|
||||
info!("Input Event: {event:?}");
|
||||
});
|
||||
|
||||
info!("Render...");
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
*.so
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="..\:/Users/Robert/src/agdk-rust/examples/agdk-winit-wgpu/app/src/main/res/layout/activity_main.xml" value="0.5546875" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,66 @@
|
||||
[package]
|
||||
name = "agdk-eframe"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
winit = "0.27.2"
|
||||
wgpu = "0.13.0"
|
||||
pollster = "0.2"
|
||||
egui = "0.19"
|
||||
eframe = { version = "0.19", features = [ "wgpu" ] }
|
||||
egui_demo_lib = "0.19"
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
env_logger = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_logger = "0.11.0"
|
||||
android-activity = { version = "0.3", features = [ "game-activity" ] }
|
||||
|
||||
[patch.crates-io]
|
||||
|
||||
# This branch of Winit has an updated Android backend based on android-activity
|
||||
# Note: The winit branches are current misnamed:
|
||||
# - "android-activity" is based on Winit 0.27 (required for Egui compatibility)
|
||||
# - "android-activity-0.27" is based on Winit master
|
||||
# The -0.27 branch is currently associated with a pull request so we'll just
|
||||
# stick with these names for now
|
||||
winit = { git = "https://github.com/rib/winit", branch = "android-activity" }
|
||||
#winit = { path = "../../../winit" }
|
||||
|
||||
# Note:
|
||||
# Since android-activity is responsible for invoking the `android_main`
|
||||
# entrypoint for a native Rust application there can only be a single
|
||||
# implementation of the crate linked with the application.
|
||||
#
|
||||
# Since Winit also depends on android-activity (version = "0.2") but we'd like
|
||||
# to build against the local version of android-activity in this repo then we
|
||||
# use a [patch] to ensure we only end up with a single implementation.
|
||||
android-activity = { path = "../../android-activity" }
|
||||
|
||||
# Egui 0.19 is missing some fixes for Android so we need to build against
|
||||
# git master for now
|
||||
egui = { git = "https://github.com/emilk/egui" }
|
||||
eframe = { git = "https://github.com/emilk/egui" }
|
||||
egui_demo_lib = { git = "https://github.com/emilk/egui" }
|
||||
#egui = { path = "../../../egui/crates/egui" }
|
||||
#eframe = { path = "../../../egui/crates/eframe" }
|
||||
#egui_demo_lib = { path = "../../../egui/crates/egui_demo_lib" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
desktop = []
|
||||
|
||||
[lib]
|
||||
name="main"
|
||||
crate_type=["cdylib"]
|
||||
|
||||
[[bin]]
|
||||
path="src/lib.rs"
|
||||
name="agdk-eframe"
|
||||
required-features = [ "desktop" ]
|
||||
@@ -0,0 +1,17 @@
|
||||
This tests using `GameActivity` with egui, winit and wgpu.
|
||||
|
||||
This is based on a re-worked winit backend here:
|
||||
https://github.com/rib/winit/tree/android-activity
|
||||
|
||||
```
|
||||
export ANDROID_NDK_HOME="path/to/ndk"
|
||||
export ANDROID_HOME="path/to/sdk"
|
||||
|
||||
rustup target add aarch64-linux-android
|
||||
cargo install cargo-ndk
|
||||
|
||||
cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ build
|
||||
./gradlew build
|
||||
./gradlew installDebug
|
||||
adb shell am start -n co.realfit.agdkeframe/.MainActivity
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk 31
|
||||
|
||||
defaultConfig {
|
||||
applicationId "co.realfit.agdkeframe"
|
||||
minSdk 28
|
||||
targetSdk 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
//packagingOptions {
|
||||
// doNotStrip '**/*.so'
|
||||
//}
|
||||
//debuggable true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
// To use the Android Frame Pacing library
|
||||
//implementation "androidx.games:games-frame-pacing:1.9.1"
|
||||
|
||||
// To use the Android Performance Tuner
|
||||
//implementation "androidx.games:games-performance-tuner:1.5.0"
|
||||
|
||||
// To use the Games Activity library
|
||||
implementation "androidx.games:games-activity:1.1.0"
|
||||
|
||||
// To use the Games Controller Library
|
||||
//implementation "androidx.games:games-controller:1.1.0"
|
||||
|
||||
// To use the Games Text Input Library
|
||||
//implementation "androidx.games:games-text-input:1.1.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="co.realfit.agdkeframe">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="AGDK EFrame"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RustTemplate">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.lib_name" android:value="main" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,70 @@
|
||||
package co.realfit.agdkeframe;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.google.androidgamesdk.GameActivity;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
public class MainActivity extends GameActivity {
|
||||
|
||||
static {
|
||||
// Load the STL first to workaround issues on old Android versions:
|
||||
// "if your app targets a version of Android earlier than Android 4.3
|
||||
// (Android API level 18),
|
||||
// and you use libc++_shared.so, you must load the shared library before any other
|
||||
// library that depends on it."
|
||||
// See https://developer.android.com/ndk/guides/cpp-support#shared_runtimes
|
||||
//System.loadLibrary("c++_shared");
|
||||
|
||||
// Load the native library.
|
||||
// The name "android-game" depends on your CMake configuration, must be
|
||||
// consistent here and inside AndroidManifect.xml
|
||||
System.loadLibrary("main");
|
||||
}
|
||||
|
||||
private void hideSystemUI() {
|
||||
// This will put the game behind any cutouts and waterfalls on devices which have
|
||||
// them, so the corresponding insets will be non-zero.
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.P) {
|
||||
getWindow().getAttributes().layoutInDisplayCutoutMode
|
||||
= WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
|
||||
}
|
||||
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
|
||||
// using View.setSystemUiVisibility.
|
||||
View decorView = getWindow().getDecorView();
|
||||
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
|
||||
decorView);
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars());
|
||||
controller.hide(WindowInsetsCompat.Type.displayCutout());
|
||||
controller.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// When true, the app will fit inside any system UI windows.
|
||||
// When false, we render behind any system UI windows.
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
hideSystemUI();
|
||||
// You can set IME fields here or in native code using GameActivity_setImeEditorInfoFields.
|
||||
// We set the fields in native_engine.cpp.
|
||||
// super.setImeEditorInfoFields(InputType.TYPE_CLASS_TEXT,
|
||||
// IME_ACTION_NONE, IME_FLAG_NO_FULLSCREEN );
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public boolean isGooglePlayGames() {
|
||||
PackageManager pm = getPackageManager();
|
||||
return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Hello World!"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,16 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
</style>
|
||||
</resources>
|
||||