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
|
||||
@@ -2,7 +2,7 @@ name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: '*'
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
@@ -12,18 +12,20 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# See top README for MSRV policy
|
||||
rust-version: [1.69.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@v4
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: hecrj/setup-rust-action@v2
|
||||
- uses: hecrj/setup-rust-action@v1
|
||||
with:
|
||||
rust-version: ${{ matrix.rust-version }}
|
||||
rust-version: ${{ matrix.rust_version }}
|
||||
|
||||
- name: Install Rust targets
|
||||
run: >
|
||||
@@ -34,7 +36,16 @@ jobs:
|
||||
i686-linux-android
|
||||
|
||||
- name: Install cargo-ndk
|
||||
run: cargo +stable install cargo-ndk
|
||||
run: cargo install cargo-ndk
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Build game-activity
|
||||
working-directory: android-activity
|
||||
@@ -57,7 +68,7 @@ jobs:
|
||||
build --features native-activity
|
||||
|
||||
- name: Build agdk-mainloop example
|
||||
if: matrix.rust-version == 'stable'
|
||||
if: matrix.rust_version == 'stable'
|
||||
working-directory: examples/agdk-mainloop
|
||||
run: >
|
||||
cargo ndk
|
||||
@@ -68,7 +79,7 @@ jobs:
|
||||
-o app/src/main/jniLibs/ -- build
|
||||
|
||||
- name: Build na-mainloop example
|
||||
if: matrix.rust-version == 'stable'
|
||||
if: matrix.rust_version == 'stable'
|
||||
working-directory: examples/na-mainloop
|
||||
run: >
|
||||
cargo ndk
|
||||
@@ -78,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
|
||||
@@ -85,7 +118,7 @@ jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -98,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
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["android-activity"]
|
||||
members = [
|
||||
"android-activity"
|
||||
]
|
||||
|
||||
exclude = ["examples"]
|
||||
exclude = [
|
||||
"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/2023/03/09/Rust-1.68.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.13"
|
||||
android-activity = { version = "0.5", features = [ "native-activity" ] }
|
||||
android_logger = "0.11"
|
||||
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.5", features = [ "native-activity" ] }`
|
||||
3. Optionally add a dependency on `android_logger = "0.13.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...");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,204 +1,18 @@
|
||||
<!-- markdownlint-disable MD022 MD024 MD032 MD033 -->
|
||||
|
||||
# Changelog
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.0] - 2024-04-26
|
||||
|
||||
### Changed
|
||||
- rust-version bumped to 1.69.0 ([#156](https://github.com/rust-mobile/android-activity/pull/156))
|
||||
- Upgrade to `ndk-sys 0.6.0` and `ndk 0.9.0` ([#155](https://github.com/rust-mobile/android-activity/pull/155))
|
||||
|
||||
### Fixed
|
||||
- Check for null `saved_state_in` pointer from `NativeActivity`
|
||||
|
||||
## [0.5.2] - 2024-01-30
|
||||
|
||||
### Fixed
|
||||
- NativeActivity: OR with `EVENT_ACTION_MASK` when extracting action from `MotionEvent` - fixing multi-touch input ([#146](https://github.com/rust-mobile/android-activity/issues/146), [#147](https://github.com/rust-mobile/android-activity/pull/147))
|
||||
|
||||
## [0.5.1] - 2023-12-20
|
||||
|
||||
### Changed
|
||||
- Avoids depending on default features for `ndk` crate to avoid pulling in any `raw-window-handle` dependencies ([#142](https://github.com/rust-mobile/android-activity/pull/142))
|
||||
|
||||
**Note:** Technically, this could be observed as a breaking change in case you
|
||||
were depending on the `rwh_06` feature that was enabled by default in the
|
||||
`ndk` crate. This could be observed via the `NativeWindow` type (exposed via
|
||||
`AndroidApp::native_window()`) no longer implementing `rwh_06::HasWindowHandle`.
|
||||
|
||||
In the unlikely case that you were depending on the `ndk`'s `rwh_06` API
|
||||
being enabled by default via `android-activity`'s `ndk` dependency, your crate
|
||||
should explicitly enable the `rwh_06` feature for the `ndk` crate.
|
||||
|
||||
As far as could be seen though, it's not expected that anything was
|
||||
depending on this (e.g. anything based on Winit enables the `ndk` feature
|
||||
based on an equivalent `winit` feature).
|
||||
|
||||
The benefit of the change is that it can help avoid a redundant
|
||||
`raw-window-handle 0.6` dependency in projects that still need to use older
|
||||
(non-default) `raw-window-handle` versions. (Though note that this may be
|
||||
awkward to achieve in practice since other crates that depend on the `ndk`
|
||||
are still likely to use default features and also pull in
|
||||
`raw-window-handles 0.6`)
|
||||
|
||||
- The IO thread now gets named `stdio-to-logcat` and main thread is named `android_main` ([#145](https://github.com/rust-mobile/android-activity/pull/145))
|
||||
- Improved IO error handling in `stdio-to-logcat` IO loop. ([#133](https://github.com/rust-mobile/android-activity/pull/133))
|
||||
|
||||
## [0.5.0] - 2023-10-16
|
||||
### Added
|
||||
- Added `MotionEvent::action_button()` exposing the button associated with button press/release actions ()
|
||||
|
||||
### Changed
|
||||
- rust-version bumped to 0.68 ([#123](https://github.com/rust-mobile/android-activity/pull/123))
|
||||
- *Breaking*: updates to `ndk 0.8` and `ndk-sys 0.5` ([#128](https://github.com/rust-mobile/android-activity/pull/128))
|
||||
- The `Pointer` and `PointerIter` types from the `ndk` crate are no longer directly exposed in the public API ([#122](https://github.com/rust-mobile/android-activity/pull/122))
|
||||
- All input API enums based on Android SDK enums have been made runtime extensible via hidden `__Unknown(u32)` variants ([#131](https://github.com/rust-mobile/android-activity/pull/131))
|
||||
|
||||
## [0.5.0-beta.1] - 2023-08-15
|
||||
### Changed
|
||||
- Pulled in `ndk-sys 0.5.0-beta.0` and `ndk 0.8.0-beta.0` ([#113](https://github.com/rust-mobile/android-activity/pull/113))
|
||||
|
||||
## [0.5.0-beta.0] - 2023-08-15
|
||||
|
||||
### Added
|
||||
- Added `KeyEvent::meta_state()` for being able to query the state of meta keys, needed for character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
|
||||
- Added `KeyCharacterMap` JNI bindings to the corresponding Android SDK API ([#102](https://github.com/rust-mobile/android-activity/pull/102))
|
||||
- Added `AndroidApp::device_key_character_map()` for being able to get a `KeyCharacterMap` for a given `device_id` for unicode character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
|
||||
|
||||
<details>
|
||||
<summary>Click here for an example of how to handle unicode character mapping:</summary>
|
||||
|
||||
```rust
|
||||
let mut combining_accent = None;
|
||||
// Snip
|
||||
|
||||
|
||||
let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
|
||||
match map.get(key_event.key_code(), key_event.meta_state()) {
|
||||
Ok(KeyMapChar::Unicode(unicode)) => {
|
||||
let combined_unicode = if let Some(accent) = combining_accent {
|
||||
match map.get_dead_char(accent, unicode) {
|
||||
Ok(Some(key)) => {
|
||||
info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
|
||||
Some(key)
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("KeyEvent: Pressed '{unicode}'");
|
||||
Some(unicode)
|
||||
};
|
||||
combining_accent = None;
|
||||
combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
|
||||
}
|
||||
Ok(KeyMapChar::CombiningAccent(accent)) => {
|
||||
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
|
||||
combining_accent = Some(accent);
|
||||
Some(KeyMapChar::CombiningAccent(accent))
|
||||
}
|
||||
Ok(KeyMapChar::None) => {
|
||||
info!("KeyEvent: Pressed non-unicode key");
|
||||
combining_accent = None;
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("KeyEvent: Failed to get key map character: {err:?}");
|
||||
combining_accent = None;
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
```
|
||||
|
||||
</details>
|
||||
- Added `TextEvent` Input Method event for supporting text editing via virtual keyboards ([#24](https://github.com/rust-mobile/android-activity/pull/24))
|
||||
|
||||
### Changed
|
||||
- GameActivity updated to 2.0.2 (requires the corresponding 2.0.2 `.aar` release from Google) ([#88](https://github.com/rust-mobile/android-activity/pull/88))
|
||||
- `AndroidApp::input_events()` is replaced by `AndroidApp::input_events_iter()` ([#102](https://github.com/rust-mobile/android-activity/pull/102))
|
||||
|
||||
<details>
|
||||
<summary>Click here for an example of how to use `input_events_iter()`:</summary>
|
||||
|
||||
```rust
|
||||
match app.input_events_iter() {
|
||||
Ok(mut iter) => {
|
||||
loop {
|
||||
let read_input = iter.next(|event| {
|
||||
let handled = match event {
|
||||
InputEvent::KeyEvent(key_event) => {
|
||||
// Snip
|
||||
}
|
||||
InputEvent::MotionEvent(motion_event) => {
|
||||
// Snip
|
||||
}
|
||||
event => {
|
||||
// Snip
|
||||
}
|
||||
};
|
||||
|
||||
handled
|
||||
});
|
||||
|
||||
if !read_input {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Failed to get input events iterator: {err:?}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## [0.4.3] - 2023-07-30
|
||||
### Fixed
|
||||
- Fixed a deadlock in the `native-activity` backend while waiting for the native thread after getting an `onDestroy` callback from Java ([#94](https://github.com/rust-mobile/android-activity/pull/94))
|
||||
- Fixed numerous deadlocks in the `game-activity` backend with how it would wait for the native thread in various Java callbacks, after the app has returned from `android_main` ([#98](https://github.com/rust-mobile/android-activity/pull/98))
|
||||
|
||||
## [0.4.2] - 2023-06-17
|
||||
### 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] - 2023-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.0] - 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.0] - 2022-09-15
|
||||
## [0.3] - 2022-09-15
|
||||
### Added
|
||||
- `show/hide_sot_input` API for being able to show/hide a soft keyboard (other IME still pending)
|
||||
- `set_window_flags()` API for setting WindowManager params
|
||||
### Changed
|
||||
- *Breaking*: Created extensible, `#[non_exhaustive]` `InputEvent` wrapper enum instead of exposing `ndk` type directly
|
||||
|
||||
## [0.2.0] - 2022-08-25
|
||||
## [0.2] - 2022-08-25
|
||||
### Added
|
||||
- Emit an `InputAvailable` event for new input with `NativeActivity` and `GameActivity`
|
||||
enabling gui apps that don't render continuously
|
||||
@@ -215,20 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
- Documentation fixes
|
||||
|
||||
## [0.1.0] - 2022-07-04
|
||||
## [0.1] - 2022-07-04
|
||||
### Added
|
||||
- Initial release
|
||||
|
||||
[unreleased]: https://github.com/rust-mobile/android-activity/compare/v0.6.0...HEAD
|
||||
[0.6.0]: https://github.com/rust-mobile/android-activity/compare/v0.5.2...v0.6.0
|
||||
[0.5.2]: https://github.com/rust-mobile/android-activity/compare/v0.5.1...v0.5.2
|
||||
[0.5.1]: https://github.com/rust-mobile/android-activity/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/rust-mobile/android-activity/compare/v0.4.3...v0.5.0
|
||||
[0.4.3]: https://github.com/rust-mobile/android-activity/compare/v0.4.2...v0.4.3
|
||||
[0.4.2]: https://github.com/rust-mobile/android-activity/compare/v0.4.1...v0.4.2
|
||||
[0.4.1]: https://github.com/rust-mobile/android-activity/compare/v0.4.0...v0.4.1
|
||||
[0.4.0]: https://github.com/rust-mobile/android-activity/compare/v0.3.0...v0.4.0
|
||||
[0.3.0]: https://github.com/rust-mobile/android-activity/compare/v0.2.0...v0.3.0
|
||||
[0.2.0]: https://github.com/rust-mobile/android-activity/compare/v0.1.1...v0.2.0
|
||||
[0.1.1]: https://github.com/rust-mobile/android-activity/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://github.com/rust-mobile/android-activity/releases/tag/v0.1.0
|
||||
- Initial release
|
||||
@@ -1,22 +1,15 @@
|
||||
[package]
|
||||
name = "android-activity"
|
||||
version = "0.6.0"
|
||||
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"
|
||||
|
||||
# 1.69 was when Rust last updated the Android NDK version used to build the
|
||||
# standard library which avoids needing the -lunwind workaround in build tools.
|
||||
#
|
||||
# We depend on cargo-ndk for building which has dropped support for the above
|
||||
# linker workaround.
|
||||
rust-version = "1.69.0"
|
||||
|
||||
[features]
|
||||
# Note: we don't enable any backend by default since features
|
||||
# are generally supposed to be additive, while these backends
|
||||
@@ -24,23 +17,20 @@ rust-version = "1.69.0"
|
||||
#
|
||||
# In general it's only the final application crate that needs
|
||||
# to decide on a backend.
|
||||
default = []
|
||||
default=[]
|
||||
game-activity = []
|
||||
native-activity = []
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
jni-sys = "0.3"
|
||||
cesu8 = "1"
|
||||
jni = "0.21"
|
||||
ndk-sys = "0.6.0"
|
||||
ndk = { version = "0.9.0", default-features = false }
|
||||
ndk = "0.7"
|
||||
ndk-sys = "0.4"
|
||||
ndk-context = "0.1"
|
||||
android-properties = "0.2"
|
||||
num_enum = "0.7"
|
||||
bitflags = "2.0"
|
||||
num_enum = "0.5"
|
||||
bitflags = "1.3"
|
||||
libc = "0.2"
|
||||
thiserror = "1"
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1.0", features = ["parallel"] }
|
||||
@@ -53,4 +43,4 @@ targets = [
|
||||
"x86_64-linux-android",
|
||||
]
|
||||
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs" ]
|
||||
@@ -1,24 +1,12 @@
|
||||
# License
|
||||
The third-party glue code, under the native-activity-csrc/ and game-activity-csrc/ directories
|
||||
is covered by the Apache 2.0 license only:
|
||||
|
||||
## GameActivity
|
||||
Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
The third-party glue code, under the game-activity-csrc/ directory is covered by
|
||||
the Apache 2.0 license only:
|
||||
|
||||
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
|
||||
## SDK Documentation
|
||||
|
||||
Documentation for APIs that are direct bindings of Android platform APIs are covered
|
||||
by the Apache 2.0 license only:
|
||||
|
||||
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
|
||||
## android-activity
|
||||
|
||||
All other code is dual-licensed under either
|
||||
|
||||
- MIT License (docs/LICENSE-MIT or <http://opensource.org/licenses/MIT>)
|
||||
- Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
* MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT)
|
||||
* Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
at your option.
|
||||
at your option.
|
||||
@@ -1,37 +1,27 @@
|
||||
#![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() {
|
||||
for f in [
|
||||
"GameActivity.h",
|
||||
"GameActivity.cpp",
|
||||
"GameActivityEvents.h",
|
||||
"GameActivityEvents.cpp",
|
||||
"GameActivityLog.h",
|
||||
] {
|
||||
println!("cargo:rerun-if-changed=game-activity-csrc/game-activity/{f}");
|
||||
}
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("game-activity-csrc")
|
||||
.file("game-activity-csrc/game-activity/GameActivity.cpp")
|
||||
.file("game-activity-csrc/game-activity/GameActivityEvents.cpp")
|
||||
.extra_warnings(false)
|
||||
.cpp_link_stdlib("c++_static")
|
||||
.compile("libgame_activity.a");
|
||||
|
||||
for f in ["gamecommon.h", "gametextinput.h", "gametextinput.cpp"] {
|
||||
println!("cargo:rerun-if-changed=game-activity-csrc/game-text-input/{f}");
|
||||
}
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("game-activity-csrc")
|
||||
.file("game-activity-csrc/game-text-input/gametextinput.cpp")
|
||||
.cpp_link_stdlib("c++_static")
|
||||
.compile("libgame_text_input.a");
|
||||
|
||||
for f in ["android_native_app_glue.h", "android_native_app_glue.c"] {
|
||||
println!("cargo:rerun-if-changed=game-activity-csrc/native_app_glue/{f}");
|
||||
}
|
||||
cc::Build::new()
|
||||
.include("game-activity-csrc")
|
||||
.include("game-activity-csrc/game-activity/native_app_glue")
|
||||
@@ -48,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();
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This is the main interface to the Android Performance Tuner library, also
|
||||
* known as Tuning Fork.
|
||||
*
|
||||
* It is part of the Android Games SDK and produces best results when integrated
|
||||
* with the Swappy Frame Pacing Library.
|
||||
*
|
||||
* See the documentation at
|
||||
* https://developer.android.com/games/sdk/performance-tuner/custom-engine for
|
||||
* more information on using this library in a native Android game.
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// There are separate versions for each GameSDK component that use this format:
|
||||
#define ANDROID_GAMESDK_PACKED_VERSION(MAJOR, MINOR, BUGFIX) \
|
||||
((MAJOR << 16) | (MINOR << 8) | (BUGFIX))
|
||||
// Accessors
|
||||
#define ANDROID_GAMESDK_MAJOR_VERSION(PACKED) ((PACKED) >> 16)
|
||||
#define ANDROID_GAMESDK_MINOR_VERSION(PACKED) (((PACKED) >> 8) & 0xff)
|
||||
#define ANDROID_GAMESDK_BUGFIX_VERSION(PACKED) ((PACKED) & 0xff)
|
||||
|
||||
#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX, GIT) \
|
||||
#MAJOR "." #MINOR "." #BUGFIX "." #GIT
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
#define LOG_TAG "GameActivity"
|
||||
|
||||
#include "GameActivity.h"
|
||||
|
||||
@@ -38,10 +39,54 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "GameActivityLog.h"
|
||||
|
||||
// TODO(b/187147166): these functions were extracted from the Game SDK
|
||||
// (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used
|
||||
// instead.
|
||||
namespace {
|
||||
|
||||
#if __ANDROID_API__ >= 26
|
||||
std::string getSystemPropViaCallback(const char *key,
|
||||
const char *default_value = "") {
|
||||
const prop_info *prop = __system_property_find(key);
|
||||
if (prop == nullptr) {
|
||||
return default_value;
|
||||
}
|
||||
std::string return_value;
|
||||
auto thunk = [](void *cookie, const char * /*name*/, const char *value,
|
||||
uint32_t /*serial*/) {
|
||||
if (value != nullptr) {
|
||||
std::string *r = static_cast<std::string *>(cookie);
|
||||
*r = value;
|
||||
}
|
||||
};
|
||||
__system_property_read_callback(prop, thunk, &return_value);
|
||||
return return_value;
|
||||
}
|
||||
#else
|
||||
std::string getSystemPropViaGet(const char *key,
|
||||
const char *default_value = "") {
|
||||
char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator
|
||||
int bufferLen = __system_property_get(key, buffer);
|
||||
if (bufferLen > 0)
|
||||
return buffer;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string GetSystemProp(const char *key, const char *default_value = "") {
|
||||
#if __ANDROID_API__ >= 26
|
||||
return getSystemPropViaCallback(key, default_value);
|
||||
#else
|
||||
return getSystemPropViaGet(key, default_value);
|
||||
#endif
|
||||
}
|
||||
|
||||
int GetSystemPropAsInt(const char *key, int default_value = 0) {
|
||||
std::string prop = GetSystemProp(key);
|
||||
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
|
||||
}
|
||||
|
||||
struct OwnedGameTextInputState {
|
||||
OwnedGameTextInputState &operator=(const GameTextInputState &rhs) {
|
||||
inner = rhs;
|
||||
@@ -55,6 +100,93 @@ struct OwnedGameTextInputState {
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);
|
||||
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);
|
||||
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);
|
||||
#ifdef NDEBUG
|
||||
#define ALOGV(...)
|
||||
#else
|
||||
#define ALOGV(...) \
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
|
||||
#endif
|
||||
|
||||
/* Returns 2nd arg. Used to substitute default value if caller's vararg list
|
||||
* is empty.
|
||||
*/
|
||||
#define __android_second(first, second, ...) second
|
||||
|
||||
/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise
|
||||
* returns nothing.
|
||||
*/
|
||||
#define __android_rest(first, ...) , ##__VA_ARGS__
|
||||
|
||||
#define android_printAssert(cond, tag, fmt...) \
|
||||
__android_log_assert(cond, tag, \
|
||||
__android_second(0, ##fmt, NULL) __android_rest(fmt))
|
||||
|
||||
#define CONDITION(cond) (__builtin_expect((cond) != 0, 0))
|
||||
|
||||
#ifndef LOG_ALWAYS_FATAL_IF
|
||||
#define LOG_ALWAYS_FATAL_IF(cond, ...) \
|
||||
((CONDITION(cond)) \
|
||||
? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \
|
||||
: (void)0)
|
||||
#endif
|
||||
|
||||
#ifndef LOG_ALWAYS_FATAL
|
||||
#define LOG_ALWAYS_FATAL(...) \
|
||||
(((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Simplified macro to send a warning system log message using current LOG_TAG.
|
||||
*/
|
||||
#ifndef SLOGW
|
||||
#define SLOGW(...) \
|
||||
((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
|
||||
#endif
|
||||
|
||||
#ifndef SLOGW_IF
|
||||
#define SLOGW_IF(cond, ...) \
|
||||
((__predict_false(cond)) \
|
||||
? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \
|
||||
: (void)0)
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that
|
||||
* are stripped out of release builds.
|
||||
*/
|
||||
#if LOG_NDEBUG
|
||||
|
||||
#ifndef LOG_FATAL_IF
|
||||
#define LOG_FATAL_IF(cond, ...) ((void)0)
|
||||
#endif
|
||||
#ifndef LOG_FATAL
|
||||
#define LOG_FATAL(...) ((void)0)
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#ifndef LOG_FATAL_IF
|
||||
#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__)
|
||||
#endif
|
||||
#ifndef LOG_FATAL
|
||||
#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Assertion that generates a log message when the assertion fails.
|
||||
* Stripped out of release builds. Uses the current LOG_TAG.
|
||||
*/
|
||||
#ifndef ALOG_ASSERT
|
||||
#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#define LOG_TRACE(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
#ifndef NELEM
|
||||
#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
|
||||
#endif
|
||||
@@ -80,30 +212,6 @@ static struct {
|
||||
jfieldID bottom;
|
||||
} gInsetsClassInfo;
|
||||
|
||||
/*
|
||||
* JNI fields of the Configuration Java class.
|
||||
*/
|
||||
static struct ConfigurationClassInfo {
|
||||
jfieldID colorMode;
|
||||
jfieldID densityDpi;
|
||||
jfieldID fontScale;
|
||||
jfieldID fontWeightAdjustment;
|
||||
jfieldID hardKeyboardHidden;
|
||||
jfieldID keyboard;
|
||||
jfieldID keyboardHidden;
|
||||
jfieldID mcc;
|
||||
jfieldID mnc;
|
||||
jfieldID navigation;
|
||||
jfieldID navigationHidden;
|
||||
jfieldID orientation;
|
||||
jfieldID screenHeightDp;
|
||||
jfieldID screenLayout;
|
||||
jfieldID screenWidthDp;
|
||||
jfieldID smallestScreenWidthDp;
|
||||
jfieldID touchscreen;
|
||||
jfieldID uiMode;
|
||||
} gConfigurationClassInfo;
|
||||
|
||||
/*
|
||||
* JNI methods of the WindowInsetsCompat.Type Java class.
|
||||
*/
|
||||
@@ -120,7 +228,6 @@ struct ActivityWork {
|
||||
int32_t cmd;
|
||||
int64_t arg1;
|
||||
int64_t arg2;
|
||||
int64_t arg3;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -133,46 +240,19 @@ enum {
|
||||
CMD_SET_WINDOW_FLAGS,
|
||||
CMD_SHOW_SOFT_INPUT,
|
||||
CMD_HIDE_SOFT_INPUT,
|
||||
CMD_SET_SOFT_INPUT_STATE,
|
||||
CMD_SET_IME_EDITOR_INFO
|
||||
CMD_SET_SOFT_INPUT_STATE
|
||||
};
|
||||
|
||||
/*
|
||||
* Last known Configuration values. They may be accessed from the different
|
||||
* thread, this is why they are made atomic.
|
||||
*/
|
||||
static struct Configuration {
|
||||
std::atomic_int colorMode;
|
||||
std::atomic_int densityDpi;
|
||||
std::atomic<float> fontScale;
|
||||
std::atomic_int fontWeightAdjustment;
|
||||
std::atomic_int hardKeyboardHidden;
|
||||
std::atomic_int keyboard;
|
||||
std::atomic_int keyboardHidden;
|
||||
std::atomic_int mcc;
|
||||
std::atomic_int mnc;
|
||||
std::atomic_int navigation;
|
||||
std::atomic_int navigationHidden;
|
||||
std::atomic_int orientation;
|
||||
std::atomic_int screenHeightDp;
|
||||
std::atomic_int screenLayout;
|
||||
std::atomic_int screenWidthDp;
|
||||
std::atomic_int smallestScreenWidthDp;
|
||||
std::atomic_int touchscreen;
|
||||
std::atomic_int uiMode;
|
||||
} gConfiguration;
|
||||
|
||||
/*
|
||||
* Write a command to be executed by the GameActivity on the application main
|
||||
* thread.
|
||||
*/
|
||||
static void write_work(int fd, int32_t cmd, int64_t arg1 = 0, int64_t arg2 = 0,
|
||||
int64_t arg3 = 0) {
|
||||
static void write_work(int fd, int32_t cmd, int64_t arg1 = 0,
|
||||
int64_t arg2 = 0) {
|
||||
ActivityWork work;
|
||||
work.cmd = cmd;
|
||||
work.arg1 = arg1;
|
||||
work.arg2 = arg2;
|
||||
work.arg3 = arg3;
|
||||
|
||||
LOG_TRACE("write_work: cmd=%d", cmd);
|
||||
restart:
|
||||
@@ -211,10 +291,12 @@ static bool read_work(int fd, ActivityWork *outWork) {
|
||||
* Native state for interacting with the GameActivity class.
|
||||
*/
|
||||
struct NativeCode : public GameActivity {
|
||||
NativeCode() {
|
||||
NativeCode(void *_dlhandle, GameActivity_createFunc *_createFunc) {
|
||||
memset((GameActivity *)this, 0, sizeof(GameActivity));
|
||||
memset(&callbacks, 0, sizeof(callbacks));
|
||||
memset(&insetsState, 0, sizeof(insetsState));
|
||||
dlhandle = _dlhandle;
|
||||
createActivityFunc = _createFunc;
|
||||
nativeWindow = NULL;
|
||||
mainWorkRead = mainWorkWrite = -1;
|
||||
gameTextInput = NULL;
|
||||
@@ -242,6 +324,12 @@ struct NativeCode : public GameActivity {
|
||||
setSurface(NULL);
|
||||
if (mainWorkRead >= 0) close(mainWorkRead);
|
||||
if (mainWorkWrite >= 0) close(mainWorkWrite);
|
||||
if (dlhandle != NULL) {
|
||||
// for now don't unload... we probably should clean this
|
||||
// up and only keep one open dlhandle per proc, since there
|
||||
// is really no benefit to unloading the code.
|
||||
// dlclose(dlhandle);
|
||||
}
|
||||
}
|
||||
|
||||
void setSurface(jobject _surface) {
|
||||
@@ -257,6 +345,9 @@ struct NativeCode : public GameActivity {
|
||||
|
||||
GameActivityCallbacks callbacks;
|
||||
|
||||
void *dlhandle;
|
||||
GameActivity_createFunc *createActivityFunc;
|
||||
|
||||
std::string internalDataPathObj;
|
||||
std::string externalDataPathObj;
|
||||
std::string obbPathObj;
|
||||
@@ -283,8 +374,6 @@ struct NativeCode : public GameActivity {
|
||||
ARect insetsState[GAMECOMMON_INSETS_TYPE_COUNT];
|
||||
};
|
||||
|
||||
static void readConfigurationValues(NativeCode *code, jobject javaConfig);
|
||||
|
||||
extern "C" void GameActivity_finish(GameActivity *activity) {
|
||||
NativeCode *code = static_cast<NativeCode *>(activity);
|
||||
write_work(code->mainWorkWrite, CMD_FINISH, 0);
|
||||
@@ -387,13 +476,6 @@ static int mainWorkCallback(int fd, int events, void *data) {
|
||||
case CMD_HIDE_SOFT_INPUT: {
|
||||
GameTextInput_hideIme(code->gameTextInput, work.arg1);
|
||||
} break;
|
||||
case CMD_SET_IME_EDITOR_INFO: {
|
||||
code->env->CallVoidMethod(
|
||||
code->javaGameActivity,
|
||||
gGameActivityClassInfo.setImeEditorInfoFields, work.arg1,
|
||||
work.arg2, work.arg3);
|
||||
checkAndClearException(code->env, "setImeEditorInfo");
|
||||
} break;
|
||||
default:
|
||||
ALOGW("Unknown work command: %d", work.cmd);
|
||||
break;
|
||||
@@ -403,16 +485,40 @@ static int mainWorkCallback(int fd, int events, void *data) {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
static thread_local std::string g_error_msg;
|
||||
|
||||
static jlong initializeNativeCode_native(
|
||||
JNIEnv *env, jobject javaGameActivity, jstring internalDataDir,
|
||||
jstring obbDir, jstring externalDataDir, jobject jAssetMgr,
|
||||
jbyteArray savedState, jobject javaConfig) {
|
||||
LOG_TRACE("initializeNativeCode_native");
|
||||
static jlong loadNativeCode_native(JNIEnv *env, jobject javaGameActivity,
|
||||
jstring path, jstring funcName,
|
||||
jstring internalDataDir, jstring obbDir,
|
||||
jstring externalDataDir, jobject jAssetMgr,
|
||||
jbyteArray savedState) {
|
||||
LOG_TRACE("loadNativeCode_native");
|
||||
const char *pathStr = env->GetStringUTFChars(path, NULL);
|
||||
NativeCode *code = NULL;
|
||||
|
||||
code = new NativeCode();
|
||||
void *handle = dlopen(pathStr, RTLD_LAZY);
|
||||
|
||||
env->ReleaseStringUTFChars(path, pathStr);
|
||||
|
||||
if (handle == nullptr) {
|
||||
g_error_msg = dlerror();
|
||||
ALOGE("GameActivity dlopen(\"%s\") failed: %s", pathStr,
|
||||
g_error_msg.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *funcStr = env->GetStringUTFChars(funcName, NULL);
|
||||
code = new NativeCode(handle,
|
||||
(GameActivity_createFunc *)dlsym(handle, funcStr));
|
||||
env->ReleaseStringUTFChars(funcName, funcStr);
|
||||
|
||||
if (code->createActivityFunc == nullptr) {
|
||||
g_error_msg = dlerror();
|
||||
ALOGW("GameActivity_onCreate not found: %s", g_error_msg.c_str());
|
||||
delete code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code->looper = ALooper_forThread();
|
||||
if (code->looper == nullptr) {
|
||||
@@ -482,10 +588,7 @@ static jlong initializeNativeCode_native(
|
||||
rawSavedState = env->GetByteArrayElements(savedState, NULL);
|
||||
rawSavedSize = env->GetArrayLength(savedState);
|
||||
}
|
||||
|
||||
readConfigurationValues(code, javaConfig);
|
||||
|
||||
GameActivity_onCreate_C(code, rawSavedState, rawSavedSize);
|
||||
code->createActivityFunc(code, rawSavedState, rawSavedSize);
|
||||
|
||||
code->gameTextInput = GameTextInput_init(env, 0);
|
||||
GameTextInput_setEventCallback(code->gameTextInput,
|
||||
@@ -506,9 +609,9 @@ static jstring getDlError_native(JNIEnv *env, jobject javaGameActivity) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static void terminateNativeCode_native(JNIEnv *env, jobject javaGameActivity,
|
||||
static void unloadNativeCode_native(JNIEnv *env, jobject javaGameActivity,
|
||||
jlong handle) {
|
||||
LOG_TRACE("terminateNativeCode_native");
|
||||
LOG_TRACE("unloadNativeCode_native");
|
||||
if (handle != 0) {
|
||||
NativeCode *code = (NativeCode *)handle;
|
||||
delete code;
|
||||
@@ -592,57 +695,11 @@ static void onStop_native(JNIEnv *env, jobject javaGameActivity, jlong handle) {
|
||||
}
|
||||
}
|
||||
|
||||
static void readConfigurationValues(NativeCode *code, jobject javaConfig) {
|
||||
if (gConfigurationClassInfo.colorMode != NULL) {
|
||||
gConfiguration.colorMode = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.colorMode);
|
||||
}
|
||||
|
||||
gConfiguration.densityDpi =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.densityDpi);
|
||||
gConfiguration.fontScale =
|
||||
code->env->GetFloatField(javaConfig, gConfigurationClassInfo.fontScale);
|
||||
|
||||
if (gConfigurationClassInfo.fontWeightAdjustment != NULL) {
|
||||
gConfiguration.fontWeightAdjustment = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.fontWeightAdjustment);
|
||||
}
|
||||
|
||||
gConfiguration.hardKeyboardHidden = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.hardKeyboardHidden);
|
||||
gConfiguration.mcc =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.mcc);
|
||||
gConfiguration.mnc =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.mnc);
|
||||
gConfiguration.navigation =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.navigation);
|
||||
gConfiguration.navigationHidden = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.navigationHidden);
|
||||
gConfiguration.orientation =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.orientation);
|
||||
gConfiguration.screenHeightDp = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.screenHeightDp);
|
||||
gConfiguration.screenLayout = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.screenLayout);
|
||||
gConfiguration.screenWidthDp = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.screenWidthDp);
|
||||
gConfiguration.smallestScreenWidthDp = code->env->GetIntField(
|
||||
javaConfig, gConfigurationClassInfo.smallestScreenWidthDp);
|
||||
gConfiguration.touchscreen =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.touchscreen);
|
||||
gConfiguration.uiMode =
|
||||
code->env->GetIntField(javaConfig, gConfigurationClassInfo.uiMode);
|
||||
|
||||
checkAndClearException(code->env, "Configuration.get");
|
||||
}
|
||||
|
||||
static void onConfigurationChanged_native(JNIEnv *env, jobject javaGameActivity,
|
||||
jlong handle, jobject javaNewConfig) {
|
||||
jlong handle) {
|
||||
LOG_TRACE("onConfigurationChanged_native");
|
||||
if (handle != 0) {
|
||||
NativeCode *code = (NativeCode *)handle;
|
||||
readConfigurationValues(code, javaNewConfig);
|
||||
|
||||
if (code->callbacks.onConfigurationChanged != NULL) {
|
||||
code->callbacks.onConfigurationChanged(code);
|
||||
}
|
||||
@@ -719,12 +776,8 @@ static void onSurfaceChanged_native(JNIEnv *env, jobject javaGameActivity,
|
||||
// Maybe it was resized?
|
||||
int32_t newWidth = ANativeWindow_getWidth(code->nativeWindow);
|
||||
int32_t newHeight = ANativeWindow_getHeight(code->nativeWindow);
|
||||
|
||||
if (newWidth != code->lastWindowWidth ||
|
||||
newHeight != code->lastWindowHeight) {
|
||||
code->lastWindowWidth = newWidth;
|
||||
code->lastWindowHeight = newHeight;
|
||||
|
||||
if (code->callbacks.onNativeWindowResized != NULL) {
|
||||
code->callbacks.onNativeWindowResized(
|
||||
code, code->nativeWindow, newWidth, newHeight);
|
||||
@@ -764,84 +817,342 @@ static void onSurfaceDestroyed_native(JNIEnv *env, jobject javaGameActivity,
|
||||
}
|
||||
}
|
||||
|
||||
static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
|
||||
/* AMOTION_EVENT_AXIS_X */ true,
|
||||
/* AMOTION_EVENT_AXIS_Y */ true,
|
||||
// Disable all other axes by default (they can be enabled using
|
||||
// `GameActivityPointerAxes_enableAxis`).
|
||||
false};
|
||||
|
||||
extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledAxes[axis] = true;
|
||||
}
|
||||
|
||||
extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledAxes[axis] = false;
|
||||
}
|
||||
|
||||
static bool enabledHistoricalAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
|
||||
// Disable all axes by default (they can be enabled using
|
||||
// `GameActivityPointerAxes_enableHistoricalAxis`).
|
||||
false};
|
||||
|
||||
extern "C" void GameActivityHistoricalPointerAxes_enableAxis(int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledHistoricalAxes[axis] = true;
|
||||
}
|
||||
|
||||
extern "C" void GameActivityHistoricalPointerAxes_disableAxis(int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledHistoricalAxes[axis] = false;
|
||||
}
|
||||
|
||||
extern "C" void GameActivity_setImeEditorInfo(GameActivity *activity,
|
||||
int inputType, int actionId,
|
||||
int imeOptions) {
|
||||
NativeCode *code = static_cast<NativeCode *>(activity);
|
||||
write_work(code->mainWorkWrite, CMD_SET_IME_EDITOR_INFO, inputType,
|
||||
actionId, imeOptions);
|
||||
JNIEnv *env;
|
||||
if (activity->vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
|
||||
env->CallVoidMethod(activity->javaGameActivity,
|
||||
gGameActivityClassInfo.setImeEditorInfoFields,
|
||||
inputType, actionId, imeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getColorMode(GameActivity *) {
|
||||
return gConfiguration.colorMode;
|
||||
static struct {
|
||||
jmethodID getDeviceId;
|
||||
jmethodID getSource;
|
||||
jmethodID getAction;
|
||||
|
||||
jmethodID getEventTime;
|
||||
jmethodID getDownTime;
|
||||
|
||||
jmethodID getFlags;
|
||||
jmethodID getMetaState;
|
||||
|
||||
jmethodID getActionButton;
|
||||
jmethodID getButtonState;
|
||||
jmethodID getClassification;
|
||||
jmethodID getEdgeFlags;
|
||||
|
||||
jmethodID getPointerCount;
|
||||
jmethodID getPointerId;
|
||||
jmethodID getRawX;
|
||||
jmethodID getRawY;
|
||||
jmethodID getXPrecision;
|
||||
jmethodID getYPrecision;
|
||||
jmethodID getAxisValue;
|
||||
|
||||
jmethodID getHistorySize;
|
||||
jmethodID getHistoricalEventTime;
|
||||
jmethodID getHistoricalAxisValue;
|
||||
} gMotionEventClassInfo;
|
||||
|
||||
extern "C" int GameActivityMotionEvent_fromJava(
|
||||
JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event,
|
||||
GameActivityHistoricalPointerAxes *out_historical) {
|
||||
static bool gMotionEventClassInfoInitialized = false;
|
||||
if (!gMotionEventClassInfoInitialized) {
|
||||
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
|
||||
gMotionEventClassInfo = {0};
|
||||
jclass motionEventClass = env->FindClass("android/view/MotionEvent");
|
||||
gMotionEventClassInfo.getDeviceId =
|
||||
env->GetMethodID(motionEventClass, "getDeviceId", "()I");
|
||||
gMotionEventClassInfo.getSource =
|
||||
env->GetMethodID(motionEventClass, "getSource", "()I");
|
||||
gMotionEventClassInfo.getAction =
|
||||
env->GetMethodID(motionEventClass, "getAction", "()I");
|
||||
gMotionEventClassInfo.getEventTime =
|
||||
env->GetMethodID(motionEventClass, "getEventTime", "()J");
|
||||
gMotionEventClassInfo.getDownTime =
|
||||
env->GetMethodID(motionEventClass, "getDownTime", "()J");
|
||||
gMotionEventClassInfo.getFlags =
|
||||
env->GetMethodID(motionEventClass, "getFlags", "()I");
|
||||
gMotionEventClassInfo.getMetaState =
|
||||
env->GetMethodID(motionEventClass, "getMetaState", "()I");
|
||||
if (sdkVersion >= 23) {
|
||||
gMotionEventClassInfo.getActionButton =
|
||||
env->GetMethodID(motionEventClass, "getActionButton", "()I");
|
||||
}
|
||||
if (sdkVersion >= 14) {
|
||||
gMotionEventClassInfo.getButtonState =
|
||||
env->GetMethodID(motionEventClass, "getButtonState", "()I");
|
||||
}
|
||||
if (sdkVersion >= 29) {
|
||||
gMotionEventClassInfo.getClassification =
|
||||
env->GetMethodID(motionEventClass, "getClassification", "()I");
|
||||
}
|
||||
gMotionEventClassInfo.getEdgeFlags =
|
||||
env->GetMethodID(motionEventClass, "getEdgeFlags", "()I");
|
||||
gMotionEventClassInfo.getPointerCount =
|
||||
env->GetMethodID(motionEventClass, "getPointerCount", "()I");
|
||||
gMotionEventClassInfo.getPointerId =
|
||||
env->GetMethodID(motionEventClass, "getPointerId", "(I)I");
|
||||
if (sdkVersion >= 29) {
|
||||
gMotionEventClassInfo.getRawX =
|
||||
env->GetMethodID(motionEventClass, "getRawX", "(I)F");
|
||||
gMotionEventClassInfo.getRawY =
|
||||
env->GetMethodID(motionEventClass, "getRawY", "(I)F");
|
||||
}
|
||||
gMotionEventClassInfo.getXPrecision =
|
||||
env->GetMethodID(motionEventClass, "getXPrecision", "()F");
|
||||
gMotionEventClassInfo.getYPrecision =
|
||||
env->GetMethodID(motionEventClass, "getYPrecision", "()F");
|
||||
gMotionEventClassInfo.getAxisValue =
|
||||
env->GetMethodID(motionEventClass, "getAxisValue", "(II)F");
|
||||
|
||||
gMotionEventClassInfo.getHistorySize =
|
||||
env->GetMethodID(motionEventClass, "getHistorySize", "()I");
|
||||
gMotionEventClassInfo.getHistoricalEventTime =
|
||||
env->GetMethodID(motionEventClass, "getHistoricalEventTime", "(I)J");
|
||||
gMotionEventClassInfo.getHistoricalAxisValue =
|
||||
env->GetMethodID(motionEventClass, "getHistoricalAxisValue", "(III)F");
|
||||
|
||||
gMotionEventClassInfoInitialized = true;
|
||||
}
|
||||
|
||||
int historySize =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize);
|
||||
historySize =
|
||||
std::min(historySize, GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT);
|
||||
|
||||
int localEnabledHistoricalAxis[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
|
||||
int enabledHistoricalAxisCount = 0;
|
||||
|
||||
for (int axisIndex = 0;
|
||||
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
|
||||
++axisIndex) {
|
||||
if (enabledHistoricalAxes[axisIndex]) {
|
||||
localEnabledHistoricalAxis[enabledHistoricalAxisCount++] = axisIndex;
|
||||
}
|
||||
}
|
||||
out_event->historicalCount = enabledHistoricalAxisCount == 0 ? 0 : historySize;
|
||||
out_event->historicalStart = 0; // Free for caller to use
|
||||
|
||||
// The historical event times aren't unique per-pointer but for simplicity
|
||||
// we output a per-pointer event time, copied from here...
|
||||
int64_t historicalEventTimes[GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT];
|
||||
for (int histIndex = 0; histIndex < historySize; ++histIndex) {
|
||||
historicalEventTimes[histIndex] =
|
||||
env->CallLongMethod(motionEvent,
|
||||
gMotionEventClassInfo.getHistoricalEventTime, histIndex) *
|
||||
1000000;
|
||||
}
|
||||
|
||||
int pointerCount =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount);
|
||||
pointerCount =
|
||||
std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT);
|
||||
out_event->pointerCount = pointerCount;
|
||||
for (int i = 0; i < pointerCount; ++i) {
|
||||
out_event->pointers[i] = {
|
||||
/*id=*/env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getPointerId, i),
|
||||
/*axisValues=*/{0},
|
||||
/*rawX=*/gMotionEventClassInfo.getRawX
|
||||
? env->CallFloatMethod(motionEvent,
|
||||
gMotionEventClassInfo.getRawX, i)
|
||||
: 0,
|
||||
/*rawY=*/gMotionEventClassInfo.getRawY
|
||||
? env->CallFloatMethod(motionEvent,
|
||||
gMotionEventClassInfo.getRawY, i)
|
||||
: 0,
|
||||
};
|
||||
|
||||
for (int axisIndex = 0;
|
||||
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) {
|
||||
if (enabledAxes[axisIndex]) {
|
||||
out_event->pointers[i].axisValues[axisIndex] =
|
||||
env->CallFloatMethod(motionEvent,
|
||||
gMotionEventClassInfo.getAxisValue,
|
||||
axisIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (enabledHistoricalAxisCount > 0) {
|
||||
for (int histIndex = 0; histIndex < historySize; ++histIndex) {
|
||||
int pointerHistIndex = historySize * i;
|
||||
out_historical[pointerHistIndex].eventTime = historicalEventTimes[histIndex];
|
||||
for (int c = 0; c < enabledHistoricalAxisCount; ++c) {
|
||||
int axisIndex = localEnabledHistoricalAxis[c];
|
||||
out_historical[pointerHistIndex].axisValues[axisIndex] =
|
||||
env->CallFloatMethod(motionEvent,
|
||||
gMotionEventClassInfo.getHistoricalAxisValue,
|
||||
axisIndex, i, histIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out_event->deviceId =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId);
|
||||
out_event->source =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource);
|
||||
out_event->action =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction);
|
||||
out_event->eventTime =
|
||||
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) *
|
||||
1000000;
|
||||
out_event->downTime =
|
||||
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) *
|
||||
1000000;
|
||||
out_event->flags =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags);
|
||||
out_event->metaState =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState);
|
||||
out_event->actionButton =
|
||||
gMotionEventClassInfo.getActionButton
|
||||
? env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getActionButton)
|
||||
: 0;
|
||||
out_event->buttonState =
|
||||
gMotionEventClassInfo.getButtonState
|
||||
? env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getButtonState)
|
||||
: 0;
|
||||
out_event->classification =
|
||||
gMotionEventClassInfo.getClassification
|
||||
? env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getClassification)
|
||||
: 0;
|
||||
out_event->edgeFlags =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags);
|
||||
out_event->precisionX =
|
||||
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision);
|
||||
out_event->precisionY =
|
||||
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision);
|
||||
|
||||
return out_event->pointerCount * out_event->historicalCount;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getDensityDpi(GameActivity *) {
|
||||
return gConfiguration.densityDpi;
|
||||
}
|
||||
static struct {
|
||||
jmethodID getDeviceId;
|
||||
jmethodID getSource;
|
||||
jmethodID getAction;
|
||||
|
||||
extern "C" float GameActivity_getFontScale(GameActivity *) {
|
||||
return gConfiguration.fontScale;
|
||||
}
|
||||
jmethodID getEventTime;
|
||||
jmethodID getDownTime;
|
||||
|
||||
extern "C" int GameActivity_getFontWeightAdjustment(GameActivity *) {
|
||||
return gConfiguration.fontWeightAdjustment;
|
||||
}
|
||||
jmethodID getFlags;
|
||||
jmethodID getMetaState;
|
||||
|
||||
extern "C" int GameActivity_getHardKeyboardHidden(GameActivity *) {
|
||||
return gConfiguration.hardKeyboardHidden;
|
||||
}
|
||||
jmethodID getModifiers;
|
||||
jmethodID getRepeatCount;
|
||||
jmethodID getKeyCode;
|
||||
jmethodID getScanCode;
|
||||
} gKeyEventClassInfo;
|
||||
|
||||
extern "C" int GameActivity_getKeyboard(GameActivity *) {
|
||||
return gConfiguration.keyboard;
|
||||
}
|
||||
extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent,
|
||||
GameActivityKeyEvent *out_event) {
|
||||
static bool gKeyEventClassInfoInitialized = false;
|
||||
if (!gKeyEventClassInfoInitialized) {
|
||||
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
|
||||
gKeyEventClassInfo = {0};
|
||||
jclass keyEventClass = env->FindClass("android/view/KeyEvent");
|
||||
gKeyEventClassInfo.getDeviceId =
|
||||
env->GetMethodID(keyEventClass, "getDeviceId", "()I");
|
||||
gKeyEventClassInfo.getSource =
|
||||
env->GetMethodID(keyEventClass, "getSource", "()I");
|
||||
gKeyEventClassInfo.getAction =
|
||||
env->GetMethodID(keyEventClass, "getAction", "()I");
|
||||
gKeyEventClassInfo.getEventTime =
|
||||
env->GetMethodID(keyEventClass, "getEventTime", "()J");
|
||||
gKeyEventClassInfo.getDownTime =
|
||||
env->GetMethodID(keyEventClass, "getDownTime", "()J");
|
||||
gKeyEventClassInfo.getFlags =
|
||||
env->GetMethodID(keyEventClass, "getFlags", "()I");
|
||||
gKeyEventClassInfo.getMetaState =
|
||||
env->GetMethodID(keyEventClass, "getMetaState", "()I");
|
||||
if (sdkVersion >= 13) {
|
||||
gKeyEventClassInfo.getModifiers =
|
||||
env->GetMethodID(keyEventClass, "getModifiers", "()I");
|
||||
}
|
||||
gKeyEventClassInfo.getRepeatCount =
|
||||
env->GetMethodID(keyEventClass, "getRepeatCount", "()I");
|
||||
gKeyEventClassInfo.getKeyCode =
|
||||
env->GetMethodID(keyEventClass, "getKeyCode", "()I");
|
||||
gKeyEventClassInfo.getScanCode =
|
||||
env->GetMethodID(keyEventClass, "getScanCode", "()I");
|
||||
|
||||
extern "C" int GameActivity_getKeyboardHidden(GameActivity *) {
|
||||
return gConfiguration.keyboardHidden;
|
||||
}
|
||||
gKeyEventClassInfoInitialized = true;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getMcc(GameActivity *) {
|
||||
return gConfiguration.mcc;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getMnc(GameActivity *) {
|
||||
return gConfiguration.mnc;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getNavigation(GameActivity *) {
|
||||
return gConfiguration.navigation;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getNavigationHidden(GameActivity *) {
|
||||
return gConfiguration.navigationHidden;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getOrientation(GameActivity *) {
|
||||
return gConfiguration.orientation;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getScreenHeightDp(GameActivity *) {
|
||||
return gConfiguration.screenHeightDp;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getScreenLayout(GameActivity *) {
|
||||
return gConfiguration.screenLayout;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getScreenWidthDp(GameActivity *) {
|
||||
return gConfiguration.screenWidthDp;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getSmallestScreenWidthDp(GameActivity *) {
|
||||
return gConfiguration.smallestScreenWidthDp;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getTouchscreen(GameActivity *) {
|
||||
return gConfiguration.touchscreen;
|
||||
}
|
||||
|
||||
extern "C" int GameActivity_getUIMode(GameActivity *) {
|
||||
return gConfiguration.uiMode;
|
||||
*out_event = {
|
||||
/*deviceId=*/env->CallIntMethod(keyEvent,
|
||||
gKeyEventClassInfo.getDeviceId),
|
||||
/*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource),
|
||||
/*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction),
|
||||
// TODO: introduce a millisecondsToNanoseconds helper:
|
||||
/*eventTime=*/
|
||||
env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) *
|
||||
1000000,
|
||||
/*downTime=*/
|
||||
env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000,
|
||||
/*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags),
|
||||
/*metaState=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState),
|
||||
/*modifiers=*/gKeyEventClassInfo.getModifiers
|
||||
? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers)
|
||||
: 0,
|
||||
/*repeatCount=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount),
|
||||
/*keyCode=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode),
|
||||
/*scanCode=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getScanCode)};
|
||||
}
|
||||
|
||||
static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity,
|
||||
@@ -851,9 +1162,11 @@ static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity,
|
||||
if (code->callbacks.onTouchEvent == nullptr) return false;
|
||||
|
||||
static GameActivityMotionEvent c_event;
|
||||
GameActivityMotionEvent_fromJava(env, motionEvent, &c_event);
|
||||
return code->callbacks.onTouchEvent(code, &c_event);
|
||||
|
||||
// Note the actual data is written contiguously as numPointers x historySize
|
||||
// entries.
|
||||
static GameActivityHistoricalPointerAxes historical[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT * GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT];
|
||||
int historicalLen = GameActivityMotionEvent_fromJava(env, motionEvent, &c_event, historical);
|
||||
return code->callbacks.onTouchEvent(code, &c_event, historical, historicalLen);
|
||||
}
|
||||
|
||||
static bool onKeyUp_native(JNIEnv *env, jobject javaGameActivity, jlong handle,
|
||||
@@ -929,37 +1242,19 @@ static void setInputConnection_native(JNIEnv *env, jobject activity,
|
||||
GameTextInput_setInputConnection(code->gameTextInput, inputConnection);
|
||||
}
|
||||
|
||||
static void onContentRectChangedNative_native(JNIEnv *env, jobject activity,
|
||||
jlong handle, jint x, jint y,
|
||||
jint w, jint h) {
|
||||
if (handle != 0) {
|
||||
NativeCode *code = (NativeCode *)handle;
|
||||
|
||||
if (code->callbacks.onContentRectChanged != nullptr) {
|
||||
ARect rect;
|
||||
rect.left = x;
|
||||
rect.top = y;
|
||||
rect.right = x+w;
|
||||
rect.bottom = y+h;
|
||||
code->callbacks.onContentRectChanged(code, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const JNINativeMethod g_methods[] = {
|
||||
{"initializeNativeCode",
|
||||
"(Ljava/lang/String;Ljava/lang/String;"
|
||||
"Ljava/lang/String;Landroid/content/res/AssetManager;"
|
||||
"[BLandroid/content/res/Configuration;)J",
|
||||
(void *)initializeNativeCode_native},
|
||||
{"loadNativeCode",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/"
|
||||
"String;Ljava/lang/String;Landroid/content/res/AssetManager;[B)J",
|
||||
(void *)loadNativeCode_native},
|
||||
{"getDlError", "()Ljava/lang/String;", (void *)getDlError_native},
|
||||
{"terminateNativeCode", "(J)V", (void *)terminateNativeCode_native},
|
||||
{"unloadNativeCode", "(J)V", (void *)unloadNativeCode_native},
|
||||
{"onStartNative", "(J)V", (void *)onStart_native},
|
||||
{"onResumeNative", "(J)V", (void *)onResume_native},
|
||||
{"onSaveInstanceStateNative", "(J)[B", (void *)onSaveInstanceState_native},
|
||||
{"onPauseNative", "(J)V", (void *)onPause_native},
|
||||
{"onStopNative", "(J)V", (void *)onStop_native},
|
||||
{"onConfigurationChangedNative", "(JLandroid/content/res/Configuration;)V",
|
||||
{"onConfigurationChangedNative", "(J)V",
|
||||
(void *)onConfigurationChanged_native},
|
||||
{"onTrimMemoryNative", "(JI)V", (void *)onTrimMemory_native},
|
||||
{"onWindowFocusChangedNative", "(JZ)V",
|
||||
@@ -984,16 +1279,13 @@ static const JNINativeMethod g_methods[] = {
|
||||
{"setInputConnectionNative",
|
||||
"(JLcom/google/androidgamesdk/gametextinput/InputConnection;)V",
|
||||
(void *)setInputConnection_native},
|
||||
{"onContentRectChangedNative", "(JIIII)V",
|
||||
(void *)onContentRectChangedNative_native},
|
||||
};
|
||||
|
||||
static const char *const kGameActivityPathName =
|
||||
"com/google/androidgamesdk/GameActivity";
|
||||
|
||||
static const char *const kInsetsPathName = "androidx/core/graphics/Insets";
|
||||
static const char *const kConfigurationPathName =
|
||||
"android/content/res/Configuration";
|
||||
|
||||
static const char *const kWindowInsetsCompatTypePathName =
|
||||
"androidx/core/view/WindowInsetsCompat$Type";
|
||||
|
||||
@@ -1051,59 +1343,12 @@ extern "C" int GameActivity_register(JNIEnv *env) {
|
||||
"getWaterfallInsets", "()Landroidx/core/graphics/Insets;");
|
||||
GET_METHOD_ID(gGameActivityClassInfo.setImeEditorInfoFields, activity_class,
|
||||
"setImeEditorInfoFields", "(III)V");
|
||||
|
||||
jclass insets_class;
|
||||
FIND_CLASS(insets_class, kInsetsPathName);
|
||||
GET_FIELD_ID(gInsetsClassInfo.left, insets_class, "left", "I");
|
||||
GET_FIELD_ID(gInsetsClassInfo.right, insets_class, "right", "I");
|
||||
GET_FIELD_ID(gInsetsClassInfo.top, insets_class, "top", "I");
|
||||
GET_FIELD_ID(gInsetsClassInfo.bottom, insets_class, "bottom", "I");
|
||||
|
||||
jclass configuration_class;
|
||||
FIND_CLASS(configuration_class, kConfigurationPathName);
|
||||
|
||||
if (android_get_device_api_level() >= 26) {
|
||||
GET_FIELD_ID(gConfigurationClassInfo.colorMode, configuration_class,
|
||||
"colorMode", "I");
|
||||
}
|
||||
|
||||
GET_FIELD_ID(gConfigurationClassInfo.densityDpi, configuration_class,
|
||||
"densityDpi", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.fontScale, configuration_class,
|
||||
"fontScale", "F");
|
||||
|
||||
if (android_get_device_api_level() >= 31) {
|
||||
GET_FIELD_ID(gConfigurationClassInfo.fontWeightAdjustment,
|
||||
configuration_class, "fontWeightAdjustment", "I");
|
||||
}
|
||||
|
||||
GET_FIELD_ID(gConfigurationClassInfo.hardKeyboardHidden,
|
||||
configuration_class, "hardKeyboardHidden", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.keyboard, configuration_class,
|
||||
"keyboard", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.keyboardHidden, configuration_class,
|
||||
"keyboardHidden", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.mcc, configuration_class, "mcc", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.mnc, configuration_class, "mnc", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.navigation, configuration_class,
|
||||
"navigation", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.navigationHidden, configuration_class,
|
||||
"navigationHidden", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.orientation, configuration_class,
|
||||
"orientation", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.screenHeightDp, configuration_class,
|
||||
"screenHeightDp", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.screenLayout, configuration_class,
|
||||
"screenLayout", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.screenWidthDp, configuration_class,
|
||||
"screenWidthDp", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.smallestScreenWidthDp,
|
||||
configuration_class, "smallestScreenWidthDp", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.touchscreen, configuration_class,
|
||||
"touchscreen", "I");
|
||||
GET_FIELD_ID(gConfigurationClassInfo.uiMode, configuration_class, "uiMode",
|
||||
"I");
|
||||
|
||||
jclass windowInsetsCompatType_class;
|
||||
FIND_CLASS(windowInsetsCompatType_class, kWindowInsetsCompatTypePathName);
|
||||
gWindowInsetsCompatTypeClassInfo.clazz =
|
||||
@@ -1134,14 +1379,15 @@ extern "C" int GameActivity_register(JNIEnv *env) {
|
||||
// Rust because Rust/Cargo don't give us a way to directly export symbols
|
||||
// from C/C++ code: https://github.com/rust-lang/rfcs/issues/2771
|
||||
//
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
|
||||
JNIEnv *env, jobject javaGameActivity, jstring internalDataDir,
|
||||
jstring obbDir, jstring externalDataDir, jobject jAssetMgr,
|
||||
jbyteArray savedState, jobject javaConfig) {
|
||||
// Register this method so that GameActiviy_register does not need to be called
|
||||
// manually.
|
||||
extern "C" jlong Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
|
||||
JNIEnv *env, jobject javaGameActivity, jstring path, jstring funcName,
|
||||
jstring internalDataDir, jstring obbDir, jstring externalDataDir,
|
||||
jobject jAssetMgr, jbyteArray savedState) {
|
||||
GameActivity_register(env);
|
||||
jlong nativeCode = initializeNativeCode_native(
|
||||
env, javaGameActivity, internalDataDir, obbDir, externalDataDir,
|
||||
jAssetMgr, savedState, javaConfig);
|
||||
jlong nativeCode = loadNativeCode_native(
|
||||
env, javaGameActivity, path, funcName, internalDataDir, obbDir,
|
||||
externalDataDir, jAssetMgr, savedState);
|
||||
return nativeCode;
|
||||
}
|
||||
|
||||
@@ -36,22 +36,12 @@
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "common/gamesdk_common.h"
|
||||
#include "game-activity/GameActivityEvents.h"
|
||||
#include "game-text-input/gametextinput.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define GAMEACTIVITY_MAJOR_VERSION 2
|
||||
#define GAMEACTIVITY_MINOR_VERSION 0
|
||||
#define GAMEACTIVITY_BUGFIX_VERSION 2
|
||||
#define GAMEACTIVITY_PACKED_VERSION \
|
||||
ANDROID_GAMESDK_PACKED_VERSION(GAMEACTIVITY_MAJOR_VERSION, \
|
||||
GAMEACTIVITY_MINOR_VERSION, \
|
||||
GAMEACTIVITY_BUGFIX_VERSION)
|
||||
|
||||
/**
|
||||
* {@link GameActivityCallbacks}
|
||||
*/
|
||||
@@ -125,6 +115,198 @@ typedef struct GameActivity {
|
||||
const char* obbPath;
|
||||
} GameActivity;
|
||||
|
||||
/**
|
||||
* The maximum number of axes supported in an Android MotionEvent.
|
||||
* See https://developer.android.com/ndk/reference/group/input.
|
||||
*/
|
||||
#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48
|
||||
|
||||
/**
|
||||
* \brief Describe information about a pointer, found in a
|
||||
* GameActivityMotionEvent.
|
||||
*
|
||||
* You can read values directly from this structure, or use helper functions
|
||||
* (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and
|
||||
* `GameActivityPointerAxes_getAxisValue`).
|
||||
*
|
||||
* The X axis and Y axis are enabled by default but any other axis that you want
|
||||
* to read **must** be enabled first, using
|
||||
* `GameActivityPointerAxes_enableAxis`.
|
||||
*
|
||||
* \see GameActivityMotionEvent
|
||||
*/
|
||||
typedef struct GameActivityPointerAxes {
|
||||
int32_t id;
|
||||
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
|
||||
float rawX;
|
||||
float rawY;
|
||||
} GameActivityPointerAxes;
|
||||
|
||||
typedef struct GameActivityHistoricalPointerAxes {
|
||||
int64_t eventTime;
|
||||
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
|
||||
} GameActivityHistoricalPointerAxes;
|
||||
|
||||
/** \brief Get the current X coordinate of the pointer. */
|
||||
inline float GameActivityPointerAxes_getX(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X];
|
||||
}
|
||||
|
||||
/** \brief Get the current Y coordinate of the pointer. */
|
||||
inline float GameActivityPointerAxes_getY(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y];
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Enable the specified axis, so that its value is reported in the
|
||||
* GameActivityPointerAxes structures stored in a motion event.
|
||||
*
|
||||
* You must enable any axis that you want to read, apart from
|
||||
* `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by
|
||||
* default.
|
||||
*
|
||||
* If the axis index is out of range, nothing is done.
|
||||
*/
|
||||
void GameActivityPointerAxes_enableAxis(int32_t axis);
|
||||
|
||||
/**
|
||||
* \brief Disable the specified axis. Its value won't be reported in the
|
||||
* GameActivityPointerAxes structures stored in a motion event anymore.
|
||||
*
|
||||
* Apart from X and Y, any axis that you want to read **must** be enabled first,
|
||||
* using `GameActivityPointerAxes_enableAxis`.
|
||||
*
|
||||
* If the axis index is out of range, nothing is done.
|
||||
*/
|
||||
void GameActivityPointerAxes_disableAxis(int32_t axis);
|
||||
|
||||
/**
|
||||
* \brief Enable the specified axis, so that its value is reported in the
|
||||
* GameActivityHistoricalPointerAxes structures associated with a motion event.
|
||||
*
|
||||
* You must enable any axis that you want to read (no axes are enabled by
|
||||
* default).
|
||||
*
|
||||
* If the axis index is out of range, nothing is done.
|
||||
*/
|
||||
void GameActivityHistoricalPointerAxes_enableAxis(int32_t axis);
|
||||
|
||||
/**
|
||||
* \brief Disable the specified axis. Its value won't be reported in the
|
||||
* GameActivityHistoricalPointerAxes structures associated with motion events
|
||||
* anymore.
|
||||
*
|
||||
* If the axis index is out of range, nothing is done.
|
||||
*/
|
||||
void GameActivityHistoricalPointerAxes_disableAxis(int32_t axis);
|
||||
|
||||
/**
|
||||
* \brief Get the value of the requested axis.
|
||||
*
|
||||
* Apart from X and Y, any axis that you want to read **must** be enabled first,
|
||||
* using `GameActivityPointerAxes_enableAxis`.
|
||||
*
|
||||
* Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`,
|
||||
* `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...)
|
||||
* in https://developer.android.com/ndk/reference/group/input.
|
||||
*
|
||||
* @param pointerInfo The structure containing information about the pointer,
|
||||
* obtained from GameActivityMotionEvent.
|
||||
* @param axis The axis to get the value from
|
||||
* @return The value of the axis, or 0 if the axis is invalid or was not
|
||||
* enabled.
|
||||
*/
|
||||
inline float GameActivityPointerAxes_getAxisValue(
|
||||
GameActivityPointerAxes* pointerInfo, int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pointerInfo->axisValues[axis];
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of pointers returned inside a motion event.
|
||||
*/
|
||||
#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE)
|
||||
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \
|
||||
GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE
|
||||
#else
|
||||
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The maximum number of historic samples associated with a single motion event.
|
||||
*/
|
||||
#if (defined GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT_OVERRIDE)
|
||||
#define GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT \
|
||||
GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT_OVERRIDE
|
||||
#else
|
||||
#define GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT 8
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Describe a motion event that happened on the GameActivity SurfaceView.
|
||||
*
|
||||
* This is 1:1 mapping to the information contained in a Java `MotionEvent`
|
||||
* (see https://developer.android.com/reference/android/view/MotionEvent).
|
||||
*/
|
||||
typedef struct GameActivityMotionEvent {
|
||||
int32_t deviceId;
|
||||
int32_t source;
|
||||
int32_t action;
|
||||
|
||||
int64_t eventTime;
|
||||
int64_t downTime;
|
||||
|
||||
int32_t flags;
|
||||
int32_t metaState;
|
||||
|
||||
int32_t actionButton;
|
||||
int32_t buttonState;
|
||||
int32_t classification;
|
||||
int32_t edgeFlags;
|
||||
|
||||
uint32_t pointerCount;
|
||||
GameActivityPointerAxes
|
||||
pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
|
||||
|
||||
float precisionX;
|
||||
float precisionY;
|
||||
|
||||
int16_t historicalStart;
|
||||
|
||||
// Note the actual buffer of historical data has a length of
|
||||
// pointerCount * historicalCount, since the historical axis
|
||||
// data is per-pointer.
|
||||
int16_t historicalCount;
|
||||
} GameActivityMotionEvent;
|
||||
|
||||
/**
|
||||
* \brief Describe a key event that happened on the GameActivity SurfaceView.
|
||||
*
|
||||
* This is 1:1 mapping to the information contained in a Java `KeyEvent`
|
||||
* (see https://developer.android.com/reference/android/view/KeyEvent).
|
||||
*/
|
||||
typedef struct GameActivityKeyEvent {
|
||||
int32_t deviceId;
|
||||
int32_t source;
|
||||
int32_t action;
|
||||
|
||||
int64_t eventTime;
|
||||
int64_t downTime;
|
||||
|
||||
int32_t flags;
|
||||
int32_t metaState;
|
||||
|
||||
int32_t modifiers;
|
||||
int32_t repeatCount;
|
||||
int32_t keyCode;
|
||||
int32_t scanCode;
|
||||
} GameActivityKeyEvent;
|
||||
|
||||
/**
|
||||
* A function the user should call from their callback with the data, its length
|
||||
* and the library- supplied context.
|
||||
@@ -241,7 +423,9 @@ typedef struct GameActivityCallbacks {
|
||||
* only valid during the callback.
|
||||
*/
|
||||
bool (*onTouchEvent)(GameActivity* activity,
|
||||
const GameActivityMotionEvent* event);
|
||||
const GameActivityMotionEvent* event,
|
||||
const GameActivityHistoricalPointerAxes* historical,
|
||||
int historicalLen);
|
||||
|
||||
/**
|
||||
* Callback called for every key down event on the GameActivity SurfaceView.
|
||||
@@ -271,14 +455,37 @@ typedef struct GameActivityCallbacks {
|
||||
* Call GameActivity_getWindowInsets to retrieve the insets themselves.
|
||||
*/
|
||||
void (*onWindowInsetsChanged)(GameActivity* activity);
|
||||
|
||||
/**
|
||||
* Callback called when the rectangle in the window where the content
|
||||
* should be placed has changed.
|
||||
*/
|
||||
void (*onContentRectChanged)(GameActivity *activity, const ARect *rect);
|
||||
} GameActivityCallbacks;
|
||||
|
||||
/**
|
||||
* \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`.
|
||||
*
|
||||
* This is done automatically by the GameActivity: see `onTouchEvent` to set
|
||||
* a callback to consume the received events.
|
||||
* This function can be used if you re-implement events handling in your own
|
||||
* activity. On return, the out_event->historicalStart will be zero, and should
|
||||
* be updated to index into whatever buffer out_historical is copied.
|
||||
* On return the length of out_historical is
|
||||
* (out_event->pointerCount x out_event->historicalCount) and is in a
|
||||
* pointer-major order (i.e. all axis for a pointer are contiguous)
|
||||
* Ownership of out_event is maintained by the caller.
|
||||
*/
|
||||
int GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
|
||||
GameActivityMotionEvent* out_event,
|
||||
GameActivityHistoricalPointerAxes *out_historical);
|
||||
|
||||
/**
|
||||
* \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`.
|
||||
*
|
||||
* This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown`
|
||||
* to set a callback to consume the received events.
|
||||
* This function can be used if you re-implement events handling in your own
|
||||
* activity.
|
||||
* Ownership of out_event is maintained by the caller.
|
||||
*/
|
||||
void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent,
|
||||
GameActivityKeyEvent* out_event);
|
||||
|
||||
/**
|
||||
* This is the function that must be in the native code to instantiate the
|
||||
* application's native activity. It is called with the activity instance (see
|
||||
@@ -296,7 +503,7 @@ typedef void GameActivity_createFunc(GameActivity* activity, void* savedState,
|
||||
* "android.app.func_name" string meta-data in your manifest to use a different
|
||||
* function.
|
||||
*/
|
||||
extern GameActivity_createFunc GameActivity_onCreate_C;
|
||||
extern GameActivity_createFunc GameActivity_onCreate;
|
||||
|
||||
/**
|
||||
* Finish the given activity. Its finish() method will be called, causing it
|
||||
@@ -603,30 +810,6 @@ void GameActivity_getWindowInsets(GameActivity* activity,
|
||||
void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType,
|
||||
int actionId, int imeOptions);
|
||||
|
||||
/**
|
||||
* These are getters for Configuration class members. They may be called from
|
||||
* any thread.
|
||||
*/
|
||||
int GameActivity_getOrientation(GameActivity* activity);
|
||||
int GameActivity_getColorMode(GameActivity* activity);
|
||||
int GameActivity_getDensityDpi(GameActivity* activity);
|
||||
float GameActivity_getFontScale(GameActivity* activity);
|
||||
int GameActivity_getFontWeightAdjustment(GameActivity* activity);
|
||||
int GameActivity_getHardKeyboardHidden(GameActivity* activity);
|
||||
int GameActivity_getKeyboard(GameActivity* activity);
|
||||
int GameActivity_getKeyboardHidden(GameActivity* activity);
|
||||
int GameActivity_getMcc(GameActivity* activity);
|
||||
int GameActivity_getMnc(GameActivity* activity);
|
||||
int GameActivity_getNavigation(GameActivity* activity);
|
||||
int GameActivity_getNavigationHidden(GameActivity* activity);
|
||||
int GameActivity_getOrientation(GameActivity* activity);
|
||||
int GameActivity_getScreenHeightDp(GameActivity* activity);
|
||||
int GameActivity_getScreenLayout(GameActivity* activity);
|
||||
int GameActivity_getScreenWidthDp(GameActivity* activity);
|
||||
int GameActivity_getSmallestScreenWidthDp(GameActivity* activity);
|
||||
int GameActivity_getTouchscreen(GameActivity* activity);
|
||||
int GameActivity_getUIMode(GameActivity* activity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 "GameActivityEvents.h"
|
||||
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "GameActivityLog.h"
|
||||
|
||||
// TODO(b/187147166): these functions were extracted from the Game SDK
|
||||
// (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used
|
||||
// instead.
|
||||
namespace {
|
||||
|
||||
std::string getSystemPropViaGet(const char *key,
|
||||
const char *default_value = "") {
|
||||
char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator
|
||||
int bufferLen = __system_property_get(key, buffer);
|
||||
if (bufferLen > 0)
|
||||
return buffer;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string GetSystemProp(const char *key, const char *default_value = "") {
|
||||
return getSystemPropViaGet(key, default_value);
|
||||
}
|
||||
|
||||
int GetSystemPropAsInt(const char *key, int default_value = 0) {
|
||||
std::string prop = GetSystemProp(key);
|
||||
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
#ifndef NELEM
|
||||
#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
|
||||
#endif
|
||||
|
||||
static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
|
||||
/* AMOTION_EVENT_AXIS_X */ true,
|
||||
/* AMOTION_EVENT_AXIS_Y */ true,
|
||||
// Disable all other axes by default (they can be enabled using
|
||||
// `GameActivityPointerAxes_enableAxis`).
|
||||
false};
|
||||
|
||||
extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledAxes[axis] = true;
|
||||
}
|
||||
|
||||
float GameActivityPointerAxes_getAxisValue(
|
||||
const GameActivityPointerAxes *pointerInfo, int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!enabledAxes[axis]) {
|
||||
ALOGW("Axis %d must be enabled before it can be accessed.", axis);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pointerInfo->axisValues[axis];
|
||||
}
|
||||
|
||||
extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
enabledAxes[axis] = false;
|
||||
}
|
||||
|
||||
float GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
const GameActivityMotionEvent *event, int axis, int pointerIndex,
|
||||
int historyPos) {
|
||||
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
|
||||
ALOGE("Invalid axis %d", axis);
|
||||
return -1;
|
||||
}
|
||||
if (pointerIndex < 0 || pointerIndex >= event->pointerCount) {
|
||||
ALOGE("Invalid pointer index %d", pointerIndex);
|
||||
return -1;
|
||||
}
|
||||
if (historyPos < 0 || historyPos >= event->historySize) {
|
||||
ALOGE("Invalid history index %d", historyPos);
|
||||
return -1;
|
||||
}
|
||||
if (!enabledAxes[axis]) {
|
||||
ALOGW("Axis %d must be enabled before it can be accessed.", axis);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int pointerOffset = pointerIndex * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
|
||||
int historyValuesOffset = historyPos * event->pointerCount *
|
||||
GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
|
||||
return event
|
||||
->historicalAxisValues[historyValuesOffset + pointerOffset + axis];
|
||||
}
|
||||
|
||||
static struct {
|
||||
jmethodID getDeviceId;
|
||||
jmethodID getSource;
|
||||
jmethodID getAction;
|
||||
|
||||
jmethodID getEventTime;
|
||||
jmethodID getDownTime;
|
||||
|
||||
jmethodID getFlags;
|
||||
jmethodID getMetaState;
|
||||
|
||||
jmethodID getActionButton;
|
||||
jmethodID getButtonState;
|
||||
jmethodID getClassification;
|
||||
jmethodID getEdgeFlags;
|
||||
|
||||
jmethodID getHistorySize;
|
||||
jmethodID getHistoricalEventTime;
|
||||
|
||||
jmethodID getPointerCount;
|
||||
jmethodID getPointerId;
|
||||
|
||||
jmethodID getToolType;
|
||||
|
||||
jmethodID getRawX;
|
||||
jmethodID getRawY;
|
||||
jmethodID getXPrecision;
|
||||
jmethodID getYPrecision;
|
||||
jmethodID getAxisValue;
|
||||
|
||||
jmethodID getHistoricalAxisValue;
|
||||
} gMotionEventClassInfo;
|
||||
|
||||
extern "C" void GameActivityMotionEvent_destroy(
|
||||
GameActivityMotionEvent *c_event) {
|
||||
delete c_event->historicalAxisValues;
|
||||
delete c_event->historicalEventTimesMillis;
|
||||
delete c_event->historicalEventTimesNanos;
|
||||
}
|
||||
|
||||
extern "C" void GameActivityMotionEvent_fromJava(
|
||||
JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event) {
|
||||
static bool gMotionEventClassInfoInitialized = false;
|
||||
if (!gMotionEventClassInfoInitialized) {
|
||||
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
|
||||
gMotionEventClassInfo = {0};
|
||||
jclass motionEventClass = env->FindClass("android/view/MotionEvent");
|
||||
gMotionEventClassInfo.getDeviceId =
|
||||
env->GetMethodID(motionEventClass, "getDeviceId", "()I");
|
||||
gMotionEventClassInfo.getSource =
|
||||
env->GetMethodID(motionEventClass, "getSource", "()I");
|
||||
gMotionEventClassInfo.getAction =
|
||||
env->GetMethodID(motionEventClass, "getAction", "()I");
|
||||
gMotionEventClassInfo.getEventTime =
|
||||
env->GetMethodID(motionEventClass, "getEventTime", "()J");
|
||||
gMotionEventClassInfo.getDownTime =
|
||||
env->GetMethodID(motionEventClass, "getDownTime", "()J");
|
||||
gMotionEventClassInfo.getFlags =
|
||||
env->GetMethodID(motionEventClass, "getFlags", "()I");
|
||||
gMotionEventClassInfo.getMetaState =
|
||||
env->GetMethodID(motionEventClass, "getMetaState", "()I");
|
||||
if (sdkVersion >= 23) {
|
||||
gMotionEventClassInfo.getActionButton =
|
||||
env->GetMethodID(motionEventClass, "getActionButton", "()I");
|
||||
}
|
||||
if (sdkVersion >= 14) {
|
||||
gMotionEventClassInfo.getButtonState =
|
||||
env->GetMethodID(motionEventClass, "getButtonState", "()I");
|
||||
}
|
||||
if (sdkVersion >= 29) {
|
||||
gMotionEventClassInfo.getClassification =
|
||||
env->GetMethodID(motionEventClass, "getClassification", "()I");
|
||||
}
|
||||
gMotionEventClassInfo.getEdgeFlags =
|
||||
env->GetMethodID(motionEventClass, "getEdgeFlags", "()I");
|
||||
|
||||
gMotionEventClassInfo.getHistorySize =
|
||||
env->GetMethodID(motionEventClass, "getHistorySize", "()I");
|
||||
gMotionEventClassInfo.getHistoricalEventTime = env->GetMethodID(
|
||||
motionEventClass, "getHistoricalEventTime", "(I)J");
|
||||
|
||||
gMotionEventClassInfo.getPointerCount =
|
||||
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");
|
||||
gMotionEventClassInfo.getRawY =
|
||||
env->GetMethodID(motionEventClass, "getRawY", "(I)F");
|
||||
}
|
||||
gMotionEventClassInfo.getXPrecision =
|
||||
env->GetMethodID(motionEventClass, "getXPrecision", "()F");
|
||||
gMotionEventClassInfo.getYPrecision =
|
||||
env->GetMethodID(motionEventClass, "getYPrecision", "()F");
|
||||
gMotionEventClassInfo.getAxisValue =
|
||||
env->GetMethodID(motionEventClass, "getAxisValue", "(II)F");
|
||||
|
||||
gMotionEventClassInfo.getHistoricalAxisValue = env->GetMethodID(
|
||||
motionEventClass, "getHistoricalAxisValue", "(III)F");
|
||||
gMotionEventClassInfoInitialized = true;
|
||||
}
|
||||
|
||||
int pointerCount =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount);
|
||||
pointerCount =
|
||||
std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT);
|
||||
out_event->pointerCount = pointerCount;
|
||||
for (int i = 0; i < pointerCount; ++i) {
|
||||
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,
|
||||
gMotionEventClassInfo.getRawX, i)
|
||||
: 0,
|
||||
/*rawY=*/gMotionEventClassInfo.getRawY
|
||||
? env->CallFloatMethod(motionEvent,
|
||||
gMotionEventClassInfo.getRawY, i)
|
||||
: 0,
|
||||
};
|
||||
|
||||
for (int axisIndex = 0;
|
||||
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) {
|
||||
if (enabledAxes[axisIndex]) {
|
||||
out_event->pointers[i].axisValues[axisIndex] =
|
||||
env->CallFloatMethod(motionEvent,
|
||||
gMotionEventClassInfo.getAxisValue,
|
||||
axisIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int historySize =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize);
|
||||
out_event->historySize = historySize;
|
||||
out_event->historicalAxisValues =
|
||||
new float[historySize * pointerCount *
|
||||
GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
|
||||
out_event->historicalEventTimesMillis = new int64_t[historySize];
|
||||
out_event->historicalEventTimesNanos = new int64_t[historySize];
|
||||
|
||||
for (int historyIndex = 0; historyIndex < historySize; historyIndex++) {
|
||||
out_event->historicalEventTimesMillis[historyIndex] =
|
||||
env->CallLongMethod(motionEvent,
|
||||
gMotionEventClassInfo.getHistoricalEventTime,
|
||||
historyIndex);
|
||||
out_event->historicalEventTimesNanos[historyIndex] =
|
||||
out_event->historicalEventTimesMillis[historyIndex] * 1000000;
|
||||
for (int i = 0; i < pointerCount; ++i) {
|
||||
int pointerOffset = i * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
|
||||
int historyAxisOffset = historyIndex * pointerCount *
|
||||
GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
|
||||
float *axisValues =
|
||||
&out_event
|
||||
->historicalAxisValues[historyAxisOffset + pointerOffset];
|
||||
for (int axisIndex = 0;
|
||||
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
|
||||
++axisIndex) {
|
||||
if (enabledAxes[axisIndex]) {
|
||||
axisValues[axisIndex] = env->CallFloatMethod(
|
||||
motionEvent,
|
||||
gMotionEventClassInfo.getHistoricalAxisValue, axisIndex,
|
||||
i, historyIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out_event->deviceId =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId);
|
||||
out_event->source =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource);
|
||||
out_event->action =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction);
|
||||
out_event->eventTime =
|
||||
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) *
|
||||
1000000;
|
||||
out_event->downTime =
|
||||
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) *
|
||||
1000000;
|
||||
out_event->flags =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags);
|
||||
out_event->metaState =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState);
|
||||
out_event->actionButton =
|
||||
gMotionEventClassInfo.getActionButton
|
||||
? env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getActionButton)
|
||||
: 0;
|
||||
out_event->buttonState =
|
||||
gMotionEventClassInfo.getButtonState
|
||||
? env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getButtonState)
|
||||
: 0;
|
||||
out_event->classification =
|
||||
gMotionEventClassInfo.getClassification
|
||||
? env->CallIntMethod(motionEvent,
|
||||
gMotionEventClassInfo.getClassification)
|
||||
: 0;
|
||||
out_event->edgeFlags =
|
||||
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags);
|
||||
out_event->precisionX =
|
||||
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision);
|
||||
out_event->precisionY =
|
||||
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision);
|
||||
}
|
||||
|
||||
static struct {
|
||||
jmethodID getDeviceId;
|
||||
jmethodID getSource;
|
||||
jmethodID getAction;
|
||||
|
||||
jmethodID getEventTime;
|
||||
jmethodID getDownTime;
|
||||
|
||||
jmethodID getFlags;
|
||||
jmethodID getMetaState;
|
||||
|
||||
jmethodID getModifiers;
|
||||
jmethodID getRepeatCount;
|
||||
jmethodID getKeyCode;
|
||||
jmethodID getScanCode;
|
||||
//jmethodID getUnicodeChar;
|
||||
} gKeyEventClassInfo;
|
||||
|
||||
extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent,
|
||||
GameActivityKeyEvent *out_event) {
|
||||
static bool gKeyEventClassInfoInitialized = false;
|
||||
if (!gKeyEventClassInfoInitialized) {
|
||||
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
|
||||
gKeyEventClassInfo = {0};
|
||||
jclass keyEventClass = env->FindClass("android/view/KeyEvent");
|
||||
gKeyEventClassInfo.getDeviceId =
|
||||
env->GetMethodID(keyEventClass, "getDeviceId", "()I");
|
||||
gKeyEventClassInfo.getSource =
|
||||
env->GetMethodID(keyEventClass, "getSource", "()I");
|
||||
gKeyEventClassInfo.getAction =
|
||||
env->GetMethodID(keyEventClass, "getAction", "()I");
|
||||
gKeyEventClassInfo.getEventTime =
|
||||
env->GetMethodID(keyEventClass, "getEventTime", "()J");
|
||||
gKeyEventClassInfo.getDownTime =
|
||||
env->GetMethodID(keyEventClass, "getDownTime", "()J");
|
||||
gKeyEventClassInfo.getFlags =
|
||||
env->GetMethodID(keyEventClass, "getFlags", "()I");
|
||||
gKeyEventClassInfo.getMetaState =
|
||||
env->GetMethodID(keyEventClass, "getMetaState", "()I");
|
||||
if (sdkVersion >= 13) {
|
||||
gKeyEventClassInfo.getModifiers =
|
||||
env->GetMethodID(keyEventClass, "getModifiers", "()I");
|
||||
}
|
||||
gKeyEventClassInfo.getRepeatCount =
|
||||
env->GetMethodID(keyEventClass, "getRepeatCount", "()I");
|
||||
gKeyEventClassInfo.getKeyCode =
|
||||
env->GetMethodID(keyEventClass, "getKeyCode", "()I");
|
||||
gKeyEventClassInfo.getScanCode =
|
||||
env->GetMethodID(keyEventClass, "getScanCode", "()I");
|
||||
//gKeyEventClassInfo.getUnicodeChar =
|
||||
// env->GetMethodID(keyEventClass, "getUnicodeChar", "()I");
|
||||
|
||||
gKeyEventClassInfoInitialized = true;
|
||||
}
|
||||
|
||||
*out_event = {
|
||||
/*deviceId=*/env->CallIntMethod(keyEvent,
|
||||
gKeyEventClassInfo.getDeviceId),
|
||||
/*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource),
|
||||
/*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction),
|
||||
// TODO: introduce a millisecondsToNanoseconds helper:
|
||||
/*eventTime=*/
|
||||
env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) *
|
||||
1000000,
|
||||
/*downTime=*/
|
||||
env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000,
|
||||
/*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags),
|
||||
/*metaState=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState),
|
||||
/*modifiers=*/gKeyEventClassInfo.getModifiers
|
||||
? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers)
|
||||
: 0,
|
||||
/*repeatCount=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount),
|
||||
/*keyCode=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode),
|
||||
/*scanCode=*/
|
||||
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getScanCode)
|
||||
/*unicodeChar=*/
|
||||
//env->CallIntMethod(keyEvent, gKeyEventClassInfo.getUnicodeChar)
|
||||
};
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup GameActivity Game Activity Events
|
||||
* The interface to use Game Activity Events.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file GameActivityEvents.h
|
||||
*/
|
||||
#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
|
||||
#define ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
|
||||
|
||||
#include <android/input.h>
|
||||
#include <jni.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* The maximum number of axes supported in an Android MotionEvent.
|
||||
* See https://developer.android.com/ndk/reference/group/input.
|
||||
*/
|
||||
#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48
|
||||
|
||||
/**
|
||||
* \brief Describe information about a pointer, found in a
|
||||
* GameActivityMotionEvent.
|
||||
*
|
||||
* You can read values directly from this structure, or use helper functions
|
||||
* (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and
|
||||
* `GameActivityPointerAxes_getAxisValue`).
|
||||
*
|
||||
* The X axis and Y axis are enabled by default but any other axis that you want
|
||||
* to read **must** be enabled first, using
|
||||
* `GameActivityPointerAxes_enableAxis`.
|
||||
*
|
||||
* \see GameActivityMotionEvent
|
||||
*/
|
||||
typedef struct GameActivityPointerAxes {
|
||||
int32_t id;
|
||||
int32_t toolType;
|
||||
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
|
||||
float rawX;
|
||||
float rawY;
|
||||
} GameActivityPointerAxes;
|
||||
|
||||
/** \brief Get the toolType of the pointer. */
|
||||
inline int32_t GameActivityPointerAxes_getToolType(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return pointerInfo->toolType;
|
||||
}
|
||||
|
||||
/** \brief Get the current X coordinate of the pointer. */
|
||||
inline float GameActivityPointerAxes_getX(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X];
|
||||
}
|
||||
|
||||
/** \brief Get the current Y coordinate of the pointer. */
|
||||
inline float GameActivityPointerAxes_getY(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y];
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Enable the specified axis, so that its value is reported in the
|
||||
* GameActivityPointerAxes structures stored in a motion event.
|
||||
*
|
||||
* You must enable any axis that you want to read, apart from
|
||||
* `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by
|
||||
* default.
|
||||
*
|
||||
* If the axis index is out of range, nothing is done.
|
||||
*/
|
||||
void GameActivityPointerAxes_enableAxis(int32_t axis);
|
||||
|
||||
/**
|
||||
* \brief Disable the specified axis. Its value won't be reported in the
|
||||
* GameActivityPointerAxes structures stored in a motion event anymore.
|
||||
*
|
||||
* Apart from X and Y, any axis that you want to read **must** be enabled first,
|
||||
* using `GameActivityPointerAxes_enableAxis`.
|
||||
*
|
||||
* If the axis index is out of range, nothing is done.
|
||||
*/
|
||||
void GameActivityPointerAxes_disableAxis(int32_t axis);
|
||||
|
||||
/**
|
||||
* \brief Get the value of the requested axis.
|
||||
*
|
||||
* Apart from X and Y, any axis that you want to read **must** be enabled first,
|
||||
* using `GameActivityPointerAxes_enableAxis`.
|
||||
*
|
||||
* Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`,
|
||||
* `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...)
|
||||
* in https://developer.android.com/ndk/reference/group/input.
|
||||
*
|
||||
* @param pointerInfo The structure containing information about the pointer,
|
||||
* obtained from GameActivityMotionEvent.
|
||||
* @param axis The axis to get the value from
|
||||
* @return The value of the axis, or 0 if the axis is invalid or was not
|
||||
* enabled.
|
||||
*/
|
||||
float GameActivityPointerAxes_getAxisValue(
|
||||
const GameActivityPointerAxes* pointerInfo, int32_t axis);
|
||||
|
||||
inline float GameActivityPointerAxes_getPressure(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_PRESSURE);
|
||||
}
|
||||
|
||||
inline float GameActivityPointerAxes_getSize(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_SIZE);
|
||||
}
|
||||
|
||||
inline float GameActivityPointerAxes_getTouchMajor(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_TOUCH_MAJOR);
|
||||
}
|
||||
|
||||
inline float GameActivityPointerAxes_getTouchMinor(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_TOUCH_MINOR);
|
||||
}
|
||||
|
||||
inline float GameActivityPointerAxes_getToolMajor(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_TOOL_MAJOR);
|
||||
}
|
||||
|
||||
inline float GameActivityPointerAxes_getToolMinor(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_TOOL_MINOR);
|
||||
}
|
||||
|
||||
inline float GameActivityPointerAxes_getOrientation(
|
||||
const GameActivityPointerAxes* pointerInfo) {
|
||||
return GameActivityPointerAxes_getAxisValue(pointerInfo,
|
||||
AMOTION_EVENT_AXIS_ORIENTATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum number of pointers returned inside a motion event.
|
||||
*/
|
||||
#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE)
|
||||
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \
|
||||
GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE
|
||||
#else
|
||||
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Describe a motion event that happened on the GameActivity SurfaceView.
|
||||
*
|
||||
* This is 1:1 mapping to the information contained in a Java `MotionEvent`
|
||||
* (see https://developer.android.com/reference/android/view/MotionEvent).
|
||||
*/
|
||||
typedef struct GameActivityMotionEvent {
|
||||
int32_t deviceId;
|
||||
int32_t source;
|
||||
int32_t action;
|
||||
|
||||
int64_t eventTime;
|
||||
int64_t downTime;
|
||||
|
||||
int32_t flags;
|
||||
int32_t metaState;
|
||||
|
||||
int32_t actionButton;
|
||||
int32_t buttonState;
|
||||
int32_t classification;
|
||||
int32_t edgeFlags;
|
||||
|
||||
uint32_t pointerCount;
|
||||
GameActivityPointerAxes
|
||||
pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
|
||||
|
||||
int historySize;
|
||||
int64_t* historicalEventTimesMillis;
|
||||
int64_t* historicalEventTimesNanos;
|
||||
float* historicalAxisValues;
|
||||
|
||||
float precisionX;
|
||||
float precisionY;
|
||||
} GameActivityMotionEvent;
|
||||
|
||||
float GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
const GameActivityMotionEvent* event, int axis, int pointerIndex,
|
||||
int historyPos);
|
||||
|
||||
inline int GameActivityMotionEvent_getHistorySize(
|
||||
const GameActivityMotionEvent* event) {
|
||||
return event->historySize;
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalX(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_X, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalY(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_Y, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalPressure(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_PRESSURE, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalSize(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_SIZE, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalTouchMajor(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_TOUCH_MAJOR, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalTouchMinor(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_TOUCH_MINOR, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalToolMajor(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_TOOL_MAJOR, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalToolMinor(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_TOOL_MINOR, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
inline float GameActivityMotionEvent_getHistoricalOrientation(
|
||||
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
|
||||
return GameActivityMotionEvent_getHistoricalAxisValue(
|
||||
event, AMOTION_EVENT_AXIS_ORIENTATION, pointerIndex, historyPos);
|
||||
}
|
||||
|
||||
/** \brief Handle the freeing of the GameActivityMotionEvent struct. */
|
||||
void GameActivityMotionEvent_destroy(GameActivityMotionEvent* c_event);
|
||||
|
||||
/**
|
||||
* \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`.
|
||||
*
|
||||
* This is done automatically by the GameActivity: see `onTouchEvent` to set
|
||||
* a callback to consume the received events.
|
||||
* This function can be used if you re-implement events handling in your own
|
||||
* activity.
|
||||
* Ownership of out_event is maintained by the caller.
|
||||
*/
|
||||
void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
|
||||
GameActivityMotionEvent* out_event);
|
||||
|
||||
/**
|
||||
* \brief Describe a key event that happened on the GameActivity SurfaceView.
|
||||
*
|
||||
* This is 1:1 mapping to the information contained in a Java `KeyEvent`
|
||||
* (see https://developer.android.com/reference/android/view/KeyEvent).
|
||||
* The only exception is the event times, which are reported as
|
||||
* nanoseconds in this struct.
|
||||
*/
|
||||
typedef struct GameActivityKeyEvent {
|
||||
int32_t deviceId;
|
||||
int32_t source;
|
||||
int32_t action;
|
||||
|
||||
int64_t eventTime;
|
||||
int64_t downTime;
|
||||
|
||||
int32_t flags;
|
||||
int32_t metaState;
|
||||
|
||||
int32_t modifiers;
|
||||
int32_t repeatCount;
|
||||
int32_t keyCode;
|
||||
int32_t scanCode;
|
||||
//int32_t unicodeChar;
|
||||
} GameActivityKeyEvent;
|
||||
|
||||
/**
|
||||
* \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`.
|
||||
*
|
||||
* This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown`
|
||||
* to set a callback to consume the received events.
|
||||
* This function can be used if you re-implement events handling in your own
|
||||
* activity.
|
||||
* Ownership of out_event is maintained by the caller.
|
||||
*/
|
||||
void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent,
|
||||
GameActivityKeyEvent* out_event);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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_GAME_SDK_GAME_ACTIVITY_LOG_H_
|
||||
#define ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_
|
||||
|
||||
#define LOG_TAG "GameActivity"
|
||||
#include <android/log.h>
|
||||
|
||||
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);
|
||||
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);
|
||||
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);
|
||||
#ifdef NDEBUG
|
||||
#define ALOGV(...)
|
||||
#else
|
||||
#define ALOGV(...) \
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
|
||||
#endif
|
||||
|
||||
/* Returns 2nd arg. Used to substitute default value if caller's vararg list
|
||||
* is empty.
|
||||
*/
|
||||
#define __android_second(first, second, ...) second
|
||||
|
||||
/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise
|
||||
* returns nothing.
|
||||
*/
|
||||
#define __android_rest(first, ...) , ##__VA_ARGS__
|
||||
|
||||
#define android_printAssert(cond, tag, fmt...) \
|
||||
__android_log_assert(cond, tag, \
|
||||
__android_second(0, ##fmt, NULL) __android_rest(fmt))
|
||||
|
||||
#define CONDITION(cond) (__builtin_expect((cond) != 0, 0))
|
||||
|
||||
#ifndef LOG_ALWAYS_FATAL_IF
|
||||
#define LOG_ALWAYS_FATAL_IF(cond, ...) \
|
||||
((CONDITION(cond)) \
|
||||
? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \
|
||||
: (void)0)
|
||||
#endif
|
||||
|
||||
#ifndef LOG_ALWAYS_FATAL
|
||||
#define LOG_ALWAYS_FATAL(...) \
|
||||
(((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Simplified macro to send a warning system log message using current LOG_TAG.
|
||||
*/
|
||||
#ifndef SLOGW
|
||||
#define SLOGW(...) \
|
||||
((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
|
||||
#endif
|
||||
|
||||
#ifndef SLOGW_IF
|
||||
#define SLOGW_IF(cond, ...) \
|
||||
((__predict_false(cond)) \
|
||||
? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \
|
||||
: (void)0)
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that
|
||||
* are stripped out of release builds.
|
||||
*/
|
||||
#if LOG_NDEBUG
|
||||
|
||||
#ifndef LOG_FATAL_IF
|
||||
#define LOG_FATAL_IF(cond, ...) ((void)0)
|
||||
#endif
|
||||
#ifndef LOG_FATAL
|
||||
#define LOG_FATAL(...) ((void)0)
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#ifndef LOG_FATAL_IF
|
||||
#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__)
|
||||
#endif
|
||||
#ifndef LOG_FATAL
|
||||
#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Assertion that generates a log message when the assertion fails.
|
||||
* Stripped out of release builds. Uses the current LOG_TAG.
|
||||
*/
|
||||
#ifndef ALOG_ASSERT
|
||||
#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
#define LOG_TRACE(...)
|
||||
|
||||
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_
|
||||
@@ -17,7 +17,6 @@
|
||||
#include "android_native_app_glue.h"
|
||||
|
||||
#include <android/log.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
@@ -25,9 +24,6 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE 16
|
||||
#define NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE 4
|
||||
|
||||
#define LOGI(...) \
|
||||
((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
|
||||
#define LOGE(...) \
|
||||
@@ -193,7 +189,6 @@ static void process_cmd(struct android_app* app,
|
||||
// This is run on a separate thread (i.e: not the main thread).
|
||||
static void* android_app_entry(void* param) {
|
||||
struct android_app* android_app = (struct android_app*)param;
|
||||
int input_buf_idx = 0;
|
||||
|
||||
LOGV("android_app_entry called");
|
||||
android_app->config = AConfiguration_new();
|
||||
@@ -206,19 +201,6 @@ static void* android_app_entry(void* param) {
|
||||
|
||||
print_cur_config(android_app);
|
||||
|
||||
/* initialize event buffers */
|
||||
for (input_buf_idx = 0; input_buf_idx < NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; input_buf_idx++) {
|
||||
struct android_input_buffer *buf = &android_app->inputBuffers[input_buf_idx];
|
||||
|
||||
buf->motionEventsBufferSize = NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE;
|
||||
buf->motionEvents = (GameActivityMotionEvent *) malloc(sizeof(GameActivityMotionEvent) *
|
||||
buf->motionEventsBufferSize);
|
||||
|
||||
buf->keyEventsBufferSize = NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE;
|
||||
buf->keyEvents = (GameActivityKeyEvent *) malloc(sizeof(GameActivityKeyEvent) *
|
||||
buf->keyEventsBufferSize);
|
||||
}
|
||||
|
||||
android_app->cmdPollSource.id = LOOPER_ID_MAIN;
|
||||
android_app->cmdPollSource.app = android_app;
|
||||
android_app->cmdPollSource.process = process_cmd;
|
||||
@@ -326,15 +308,6 @@ static void android_app_set_window(struct android_app* android_app,
|
||||
ANativeWindow* window) {
|
||||
LOGV("android_app_set_window called");
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// NB: we have to consider that the native thread could have already
|
||||
// (gracefully) exit (setting android_app->destroyed) and so we need
|
||||
// to be careful to avoid a deadlock waiting for a thread that's
|
||||
// already exit.
|
||||
if (android_app->destroyed) {
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return;
|
||||
}
|
||||
if (android_app->pendingWindow != NULL) {
|
||||
android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
|
||||
}
|
||||
@@ -351,46 +324,21 @@ static void android_app_set_window(struct android_app* android_app,
|
||||
static void android_app_set_activity_state(struct android_app* android_app,
|
||||
int8_t cmd) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// NB: we have to consider that the native thread could have already
|
||||
// (gracefully) exit (setting android_app->destroyed) and so we need
|
||||
// to be careful to avoid a deadlock waiting for a thread that's
|
||||
// already exit.
|
||||
if (!android_app->destroyed) {
|
||||
android_app_write_cmd(android_app, cmd);
|
||||
while (android_app->activityState != cmd) {
|
||||
pthread_cond_wait(&android_app->cond, &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) {
|
||||
int input_buf_idx = 0;
|
||||
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// It's possible that onDestroy is called after we have already 'destroyed'
|
||||
// the app (via `android_app_destroy` due to `android_main` returning.
|
||||
//
|
||||
// In this case `->destroyed` will already be set (so we won't deadlock in
|
||||
// the loop below) but we still need to close the messaging fds and finish
|
||||
// freeing the android_app
|
||||
|
||||
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);
|
||||
|
||||
for (input_buf_idx = 0; input_buf_idx < NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; input_buf_idx++) {
|
||||
struct android_input_buffer *buf = &android_app->inputBuffers[input_buf_idx];
|
||||
|
||||
android_app_clear_motion_events(buf);
|
||||
free(buf->motionEvents);
|
||||
free(buf->keyEvents);
|
||||
}
|
||||
|
||||
close(android_app->msgread);
|
||||
close(android_app->msgwrite);
|
||||
pthread_cond_destroy(&android_app->cond);
|
||||
@@ -424,16 +372,6 @@ static void onSaveInstanceState(GameActivity* activity,
|
||||
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// NB: we have to consider that the native thread could have already
|
||||
// (gracefully) exit (setting android_app->destroyed) and so we need
|
||||
// to be careful to avoid a deadlock waiting for a thread that's
|
||||
// already exit.
|
||||
if (android_app->destroyed) {
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
android_app->stateSaved = 0;
|
||||
android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
|
||||
while (!android_app->stateSaved) {
|
||||
@@ -513,6 +451,7 @@ void android_app_set_motion_event_filter(struct android_app* app,
|
||||
|
||||
bool android_app_input_available_wake_up(struct android_app* app) {
|
||||
pthread_mutex_lock(&app->mutex);
|
||||
// TODO: use atomic ops for this
|
||||
bool available = app->inputAvailableWakeUp;
|
||||
app->inputAvailableWakeUp = false;
|
||||
pthread_mutex_unlock(&app->mutex);
|
||||
@@ -527,6 +466,7 @@ static void notifyInput(struct android_app* android_app) {
|
||||
}
|
||||
|
||||
if (android_app->looper != NULL) {
|
||||
LOGV("Input Notify: %p", android_app);
|
||||
// for the app thread to know why it received the wake() up
|
||||
android_app->inputAvailableWakeUp = true;
|
||||
android_app->inputSwapPending = true;
|
||||
@@ -535,19 +475,12 @@ static void notifyInput(struct android_app* android_app) {
|
||||
}
|
||||
|
||||
static bool onTouchEvent(GameActivity* activity,
|
||||
const GameActivityMotionEvent* event) {
|
||||
const GameActivityMotionEvent* event,
|
||||
const GameActivityHistoricalPointerAxes* historical,
|
||||
int historicalLen) {
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// NB: we have to consider that the native thread could have already
|
||||
// (gracefully) exit (setting android_app->destroyed) and so we need
|
||||
// to be careful to avoid a deadlock waiting for a thread that's
|
||||
// already exit.
|
||||
if (android_app->destroyed) {
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (android_app->motionEventFilter != NULL &&
|
||||
!android_app->motionEventFilter(event)) {
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
@@ -558,21 +491,34 @@ static bool onTouchEvent(GameActivity* activity,
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
|
||||
// Add to the list of active motion events
|
||||
if (inputBuffer->motionEventsCount >= inputBuffer->motionEventsBufferSize) {
|
||||
inputBuffer->motionEventsBufferSize *= 2;
|
||||
inputBuffer->motionEvents = (GameActivityMotionEvent *) realloc(inputBuffer->motionEvents,
|
||||
sizeof(GameActivityMotionEvent) * inputBuffer->motionEventsBufferSize);
|
||||
if (inputBuffer->motionEventsCount <
|
||||
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS) {
|
||||
int new_ix = inputBuffer->motionEventsCount;
|
||||
memcpy(&inputBuffer->motionEvents[new_ix], event,
|
||||
sizeof(GameActivityMotionEvent));
|
||||
++inputBuffer->motionEventsCount;
|
||||
|
||||
if (inputBuffer->motionEvents == NULL) {
|
||||
LOGE("onTouchEvent: out of memory");
|
||||
abort();
|
||||
if (inputBuffer->historicalSamplesCount + historicalLen <=
|
||||
NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES) {
|
||||
|
||||
int start_ix = inputBuffer->historicalSamplesCount;
|
||||
memcpy(&inputBuffer->historicalAxisSamples[start_ix], historical,
|
||||
sizeof(historical[0]) * historicalLen);
|
||||
inputBuffer->historicalSamplesCount += event->historicalCount;
|
||||
|
||||
inputBuffer->motionEvents[new_ix].historicalStart = start_ix;
|
||||
inputBuffer->motionEvents[new_ix].historicalCount = historicalLen;
|
||||
} else {
|
||||
inputBuffer->motionEvents[new_ix].historicalCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int new_ix = inputBuffer->motionEventsCount;
|
||||
memcpy(&inputBuffer->motionEvents[new_ix], event, sizeof(GameActivityMotionEvent));
|
||||
++inputBuffer->motionEventsCount;
|
||||
notifyInput(android_app);
|
||||
notifyInput(android_app);
|
||||
} else {
|
||||
LOGW_ONCE("Motion event will be dropped because the number of unconsumed motion"
|
||||
" events exceeded NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS (%d). Consider setting"
|
||||
" NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE to a larger value",
|
||||
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return true;
|
||||
@@ -603,15 +549,7 @@ struct android_input_buffer* android_app_swap_input_buffers(
|
||||
}
|
||||
|
||||
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) {
|
||||
// We do not need to lock here if the inputBuffer has already been swapped
|
||||
// as is handled by the game loop thread
|
||||
while (inputBuffer->motionEventsCount > 0) {
|
||||
GameActivityMotionEvent_destroy(
|
||||
&inputBuffer->motionEvents[inputBuffer->motionEventsCount - 1]);
|
||||
|
||||
inputBuffer->motionEventsCount--;
|
||||
}
|
||||
assert(inputBuffer->motionEventsCount == 0);
|
||||
inputBuffer->motionEventsCount = 0;
|
||||
}
|
||||
|
||||
void android_app_set_key_event_filter(struct android_app* app,
|
||||
@@ -625,15 +563,6 @@ static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// NB: we have to consider that the native thread could have already
|
||||
// (gracefully) exit (setting android_app->destroyed) and so we need
|
||||
// to be careful to avoid a deadlock waiting for a thread that's
|
||||
// already exit.
|
||||
if (android_app->destroyed) {
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (android_app->keyEventFilter != NULL &&
|
||||
!android_app->keyEventFilter(event)) {
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
@@ -644,22 +573,20 @@ static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
|
||||
// Add to the list of active key down events
|
||||
if (inputBuffer->keyEventsCount >= inputBuffer->keyEventsBufferSize) {
|
||||
inputBuffer->keyEventsBufferSize = inputBuffer->keyEventsBufferSize * 2;
|
||||
inputBuffer->keyEvents = (GameActivityKeyEvent *) realloc(inputBuffer->keyEvents,
|
||||
sizeof(GameActivityKeyEvent) * inputBuffer->keyEventsBufferSize);
|
||||
if (inputBuffer->keyEventsCount < NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS) {
|
||||
int new_ix = inputBuffer->keyEventsCount;
|
||||
memcpy(&inputBuffer->keyEvents[new_ix], event,
|
||||
sizeof(GameActivityKeyEvent));
|
||||
++inputBuffer->keyEventsCount;
|
||||
|
||||
if (inputBuffer->keyEvents == NULL) {
|
||||
LOGE("onKey: out of memory");
|
||||
abort();
|
||||
}
|
||||
notifyInput(android_app);
|
||||
} else {
|
||||
LOGW_ONCE("Key event will be dropped because the number of unconsumed key events exceeded"
|
||||
" NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS (%d). Consider setting"
|
||||
" NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE to a larger value",
|
||||
NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS);
|
||||
}
|
||||
|
||||
int new_ix = inputBuffer->keyEventsCount;
|
||||
memcpy(&inputBuffer->keyEvents[new_ix], event, sizeof(GameActivityKeyEvent));
|
||||
++inputBuffer->keyEventsCount;
|
||||
notifyInput(android_app);
|
||||
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -672,10 +599,8 @@ static void onTextInputEvent(GameActivity* activity,
|
||||
const GameTextInputState* state) {
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
if (!android_app->destroyed) {
|
||||
android_app->textInputState = 1;
|
||||
notifyInput(android_app);
|
||||
}
|
||||
|
||||
android_app->textInputState = 1;
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
@@ -684,23 +609,6 @@ static void onWindowInsetsChanged(GameActivity* activity) {
|
||||
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_INSETS_CHANGED);
|
||||
}
|
||||
|
||||
static void onContentRectChanged(GameActivity* activity, const ARect *rect) {
|
||||
LOGV("ContentRectChanged: %p -- (%d %d) (%d %d)", activity, rect->left, rect->top,
|
||||
rect->right, rect->bottom);
|
||||
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->contentRect = *rect;
|
||||
|
||||
android_app_write_cmd(android_app, APP_CMD_CONTENT_RECT_CHANGED);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
// XXX: This symbol is renamed with a _C suffix and then re-exported from
|
||||
// Rust because Rust/Cargo don't give us a way to directly export symbols
|
||||
// from C/C++ code: https://github.com/rust-lang/rfcs/issues/2771
|
||||
//
|
||||
JNIEXPORT
|
||||
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
|
||||
size_t savedStateSize) {
|
||||
@@ -724,7 +632,6 @@ void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
|
||||
onNativeWindowRedrawNeeded;
|
||||
activity->callbacks->onNativeWindowResized = onNativeWindowResized;
|
||||
activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged;
|
||||
activity->callbacks->onContentRectChanged = onContentRectChanged;
|
||||
LOGV("Callbacks set: %p", activity->callbacks);
|
||||
|
||||
activity->instance =
|
||||
|
||||
@@ -30,6 +30,27 @@
|
||||
|
||||
#include "game-activity/GameActivity.h"
|
||||
|
||||
#if (defined NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE)
|
||||
#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS \
|
||||
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE
|
||||
#else
|
||||
#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS 16
|
||||
#endif
|
||||
|
||||
#if (defined NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES_OVERRIDE)
|
||||
#define NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES \
|
||||
NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES_OVERRIDE
|
||||
#else
|
||||
#define NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES 64
|
||||
#endif
|
||||
|
||||
#if (defined NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE)
|
||||
#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS \
|
||||
NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE
|
||||
#else
|
||||
#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS 4
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -105,10 +126,10 @@ struct android_poll_source {
|
||||
|
||||
struct android_input_buffer {
|
||||
/**
|
||||
* Pointer to a read-only array of GameActivityMotionEvent.
|
||||
* Pointer to a read-only array of pointers to GameActivityMotionEvent.
|
||||
* Only the first motionEventsCount events are valid.
|
||||
*/
|
||||
GameActivityMotionEvent *motionEvents;
|
||||
GameActivityMotionEvent motionEvents[NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS];
|
||||
|
||||
/**
|
||||
* The number of valid motion events in `motionEvents`.
|
||||
@@ -116,25 +137,36 @@ struct android_input_buffer {
|
||||
uint64_t motionEventsCount;
|
||||
|
||||
/**
|
||||
* The size of the `motionEvents` buffer.
|
||||
* Pointer to a read-only array of pointers to GameActivityHistoricalPointerAxes.
|
||||
*
|
||||
* Only the first historicalSamplesCount samples are valid.
|
||||
* Refer to event->historicalStart, event->pointerCount and event->historicalCount
|
||||
* to access the specific samples that relate to an event.
|
||||
*
|
||||
* Each slice of samples for one event has a length of
|
||||
* (event->pointerCount and event->historicalCount) and is in pointer-major
|
||||
* order so the historic samples for each pointer are contiguous.
|
||||
* E.g. you would access historic sample index 3 for pointer 2 of an event with:
|
||||
*
|
||||
* historicalAxisSamples[event->historicalStart + (event->historicalCount * 2) + 3];
|
||||
*/
|
||||
uint64_t motionEventsBufferSize;
|
||||
GameActivityHistoricalPointerAxes historicalAxisSamples[NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES];
|
||||
|
||||
/**
|
||||
* Pointer to a read-only array of GameActivityKeyEvent.
|
||||
* The number of valid historical samples in `historicalAxisSamples`.
|
||||
*/
|
||||
uint64_t historicalSamplesCount;
|
||||
|
||||
/**
|
||||
* Pointer to a read-only array of pointers to GameActivityKeyEvent.
|
||||
* Only the first keyEventsCount events are valid.
|
||||
*/
|
||||
GameActivityKeyEvent *keyEvents;
|
||||
GameActivityKeyEvent keyEvents[NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS];
|
||||
|
||||
/**
|
||||
* The number of valid "Key" events in `keyEvents`.
|
||||
*/
|
||||
uint64_t keyEventsCount;
|
||||
|
||||
/**
|
||||
* The size of the `keyEvents` buffer.
|
||||
*/
|
||||
uint64_t keyEventsBufferSize;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -500,6 +532,8 @@ void android_app_set_motion_event_filter(struct android_app* app,
|
||||
*/
|
||||
bool android_app_input_available_wake_up(struct android_app* app);
|
||||
|
||||
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
|
||||
size_t savedStateSize);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -48,7 +48,6 @@ struct GameTextInput {
|
||||
void processEvent(jobject textInputEvent);
|
||||
void showIme(uint32_t flags);
|
||||
void hideIme(uint32_t flags);
|
||||
void restartInput();
|
||||
void setEventCallback(GameTextInputEventCallback callback, void *context);
|
||||
jobject stateToJava(const GameTextInputState &state) const;
|
||||
void stateFromJava(jobject textInputEvent,
|
||||
@@ -74,7 +73,6 @@ struct GameTextInput {
|
||||
jobject inputConnection_ = nullptr;
|
||||
jmethodID inputConnectionSetStateMethod_;
|
||||
jmethodID setSoftKeyboardActiveMethod_;
|
||||
jmethodID restartInputMethod_;
|
||||
void (*eventCallback_)(void *context,
|
||||
const struct GameTextInputState *state) = nullptr;
|
||||
void *eventCallbackContext_ = nullptr;
|
||||
@@ -166,10 +164,6 @@ void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) {
|
||||
input->hideIme(flags);
|
||||
}
|
||||
|
||||
void GameTextInput_restartInput(struct GameTextInput *input) {
|
||||
input->restartInput();
|
||||
}
|
||||
|
||||
void GameTextInput_setEventCallback(struct GameTextInput *input,
|
||||
GameTextInputEventCallback callback,
|
||||
void *context) {
|
||||
@@ -205,8 +199,6 @@ GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size)
|
||||
"(Lcom/google/androidgamesdk/gametextinput/State;)V");
|
||||
setSoftKeyboardActiveMethod_ = env_->GetMethodID(
|
||||
inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V");
|
||||
restartInputMethod_ =
|
||||
env_->GetMethodID(inputConnectionClass_, "restartInput", "()V");
|
||||
|
||||
stateClassInfo_.text =
|
||||
env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;");
|
||||
@@ -315,11 +307,6 @@ void GameTextInput::hideIme(uint32_t flags) {
|
||||
flags);
|
||||
}
|
||||
|
||||
void GameTextInput::restartInput() {
|
||||
if (inputConnection_ == nullptr) return;
|
||||
env_->CallVoidMethod(inputConnection_, restartInputMethod_, false);
|
||||
}
|
||||
|
||||
jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
|
||||
static jmethodID constructor = nullptr;
|
||||
if (constructor == nullptr) {
|
||||
|
||||
@@ -26,21 +26,12 @@
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "common/gamesdk_common.h"
|
||||
#include "gamecommon.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define GAMETEXTINPUT_MAJOR_VERSION 2
|
||||
#define GAMETEXTINPUT_MINOR_VERSION 0
|
||||
#define GAMETEXTINPUT_BUGFIX_VERSION 0
|
||||
#define GAMETEXTINPUT_PACKED_VERSION \
|
||||
ANDROID_GAMESDK_PACKED_VERSION(GAMETEXTINPUT_MAJOR_VERSION, \
|
||||
GAMETEXTINPUT_MINOR_VERSION, \
|
||||
GAMETEXTINPUT_BUGFIX_VERSION)
|
||||
|
||||
/**
|
||||
* This struct holds a span within a region of text from start (inclusive) to
|
||||
* end (exclusive). An empty span or cursor position is specified with
|
||||
@@ -182,12 +173,6 @@ enum HideImeFlags {
|
||||
*/
|
||||
void GameTextInput_hideIme(GameTextInput *input, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Restarts the input method. Calls InputMethodManager.restartInput().
|
||||
* @param input A valid GameTextInput library handle.
|
||||
*/
|
||||
void GameTextInput_restartInput(GameTextInput *input);
|
||||
|
||||
/**
|
||||
* Call a callback with the current GameTextInput state, which may have been
|
||||
* modified by changes in the IME and calls to GameTextInput_setState. We use a
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
# First install bindgen-cli via `cargo install bindgen-cli`
|
||||
|
||||
if test -z "${ANDROID_NDK_ROOT}"; then
|
||||
export ANDROID_NDK_ROOT=${ANDROID_NDK_HOME}
|
||||
fi
|
||||
@@ -18,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*' \
|
||||
@@ -39,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>>,
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AppError {
|
||||
#[error("Operation only supported from the android_main() thread: {0}")]
|
||||
NonMainThread(String),
|
||||
|
||||
#[error("Java VM or JNI error, including Java exceptions")]
|
||||
JavaError(String),
|
||||
|
||||
#[error("Input unavailable")]
|
||||
InputUnavailable,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AppError>;
|
||||
|
||||
// XXX: we don't want to expose jni-rs in the public API
|
||||
// so we have an internal error type that we can generally
|
||||
// use in the backends and then we can strip the error
|
||||
// in the frontend of the API.
|
||||
//
|
||||
// This way we avoid exposing a public trait implementation for
|
||||
// `From<jni::errors::Error>`
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum InternalAppError {
|
||||
#[error("A JNI error")]
|
||||
JniError(jni::errors::JniError),
|
||||
#[error("A Java Exception was thrown via a JNI method call")]
|
||||
JniException(String),
|
||||
#[error("A Java VM error")]
|
||||
JvmError(jni::errors::Error),
|
||||
#[error("Input unavailable")]
|
||||
InputUnavailable,
|
||||
}
|
||||
|
||||
pub(crate) type InternalResult<T> = std::result::Result<T, InternalAppError>;
|
||||
|
||||
impl From<jni::errors::Error> for InternalAppError {
|
||||
fn from(value: jni::errors::Error) -> Self {
|
||||
InternalAppError::JvmError(value)
|
||||
}
|
||||
}
|
||||
impl From<jni::errors::JniError> for InternalAppError {
|
||||
fn from(value: jni::errors::JniError) -> Self {
|
||||
InternalAppError::JniError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InternalAppError> for AppError {
|
||||
fn from(value: InternalAppError) -> Self {
|
||||
match value {
|
||||
InternalAppError::JniError(err) => AppError::JavaError(err.to_string()),
|
||||
InternalAppError::JniException(msg) => AppError::JavaError(msg),
|
||||
InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
|
||||
InternalAppError::InputUnavailable => AppError::InputUnavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,9 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use jni_sys::*;
|
||||
use libc::{pthread_cond_t, pthread_mutex_t, pthread_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"),
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
#![cfg(feature = "game-activity")]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::panic::catch_unwind;
|
||||
use std::ptr;
|
||||
use std::os::raw;
|
||||
use std::os::unix::prelude::*;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::Weak;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::{ptr, thread};
|
||||
|
||||
use libc::c_void;
|
||||
use log::{error, trace};
|
||||
use log::{error, trace, Level};
|
||||
|
||||
use jni_sys::*;
|
||||
|
||||
@@ -22,19 +23,12 @@ use ndk::asset::AssetManager;
|
||||
use ndk::configuration::Configuration;
|
||||
use ndk::native_window::NativeWindow;
|
||||
|
||||
use crate::error::InternalResult;
|
||||
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
|
||||
use crate::jni_utils::{self, CloneJavaVM};
|
||||
use crate::util::{abort_on_panic, forward_stdio_to_logcat, log_panic, try_get_path_from_ptr};
|
||||
use crate::{
|
||||
AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
|
||||
};
|
||||
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
|
||||
|
||||
mod ffi;
|
||||
|
||||
pub mod input;
|
||||
use crate::input::{TextInputState, TextSpan};
|
||||
use input::{InputEvent, KeyEvent, MotionEvent};
|
||||
use input::{Axis, InputEvent, KeyEvent, MotionEvent};
|
||||
|
||||
// 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
|
||||
@@ -54,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");
|
||||
}
|
||||
|
||||
@@ -75,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,10 +82,10 @@ 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,
|
||||
(*app_ptr).savedStateSize as usize,
|
||||
);
|
||||
let state = buf.to_vec();
|
||||
Some(state)
|
||||
@@ -121,16 +115,7 @@ impl AndroidAppWaker {
|
||||
}
|
||||
|
||||
impl AndroidApp {
|
||||
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>, jvm: CloneJavaVM) -> Self {
|
||||
let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp
|
||||
|
||||
let key_map_binding = match KeyCharacterMapBinding::new(&mut env) {
|
||||
Ok(b) => b,
|
||||
Err(err) => {
|
||||
panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}");
|
||||
}
|
||||
};
|
||||
|
||||
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>) -> Self {
|
||||
// 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()
|
||||
@@ -138,19 +123,15 @@ impl AndroidApp {
|
||||
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(AndroidAppInner {
|
||||
jvm,
|
||||
native_app: NativeAppGlue { ptr },
|
||||
config: ConfigurationRef::new(config),
|
||||
native_window: Default::default(),
|
||||
key_map_binding: Arc::new(key_map_binding),
|
||||
key_maps: Mutex::new(HashMap::new()),
|
||||
input_receiver: Mutex::new(None),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug)]
|
||||
struct NativeAppGlue {
|
||||
ptr: NonNull<ffi::android_app>,
|
||||
}
|
||||
@@ -164,126 +145,15 @@ impl Deref for NativeAppGlue {
|
||||
unsafe impl Send for NativeAppGlue {}
|
||||
unsafe impl Sync for NativeAppGlue {}
|
||||
|
||||
impl NativeAppGlue {
|
||||
// TODO: move into a trait
|
||||
pub fn text_input_state(&self) -> TextInputState {
|
||||
unsafe {
|
||||
let activity = (*self.as_ptr()).activity;
|
||||
let mut out_state = TextInputState {
|
||||
text: String::new(),
|
||||
selection: TextSpan { start: 0, end: 0 },
|
||||
compose_region: None,
|
||||
};
|
||||
let out_ptr = &mut out_state as *mut TextInputState;
|
||||
|
||||
let app_ptr = self.as_ptr();
|
||||
(*app_ptr).textInputState = 0;
|
||||
|
||||
// NEON WARNING:
|
||||
//
|
||||
// It's not clearly documented but the GameActivity API over the
|
||||
// GameTextInput library directly exposes _modified_ UTF8 text
|
||||
// from Java so we need to be careful to convert text to and
|
||||
// from UTF8
|
||||
//
|
||||
// GameTextInput also uses a pre-allocated, fixed-sized buffer for
|
||||
// the current text state and has shared `currentState_` that
|
||||
// appears to have no lock to guard access from multiple threads.
|
||||
//
|
||||
// There's also no locking at the GameActivity level, so I'm fairly
|
||||
// certain that `GameActivity_getTextInputState` isn't thread
|
||||
// safe: https://issuetracker.google.com/issues/294112477
|
||||
//
|
||||
// Overall this is all quite gnarly - and probably a good reminder
|
||||
// of why we want to use Rust instead of C/C++.
|
||||
ffi::GameActivity_getTextInputState(
|
||||
activity,
|
||||
Some(AndroidAppInner::map_input_state_to_text_event_callback),
|
||||
out_ptr.cast(),
|
||||
);
|
||||
|
||||
out_state
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn set_text_input_state(&self, state: TextInputState) {
|
||||
unsafe {
|
||||
let activity = (*self.as_ptr()).activity;
|
||||
let modified_utf8 = cesu8::to_java_cesu8(&state.text);
|
||||
let text_length = modified_utf8.len() as i32;
|
||||
let modified_utf8_bytes = modified_utf8.as_ptr();
|
||||
let ffi_state = ffi::GameTextInputState {
|
||||
text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
|
||||
text_length,
|
||||
selection: ffi::GameTextInputSpan {
|
||||
start: state.selection.start as i32,
|
||||
end: state.selection.end as i32,
|
||||
},
|
||||
composingRegion: match state.compose_region {
|
||||
Some(span) => {
|
||||
// The GameText subclass of InputConnection only has a special case for removing the
|
||||
// compose region if `start == -1` but the docs for `setComposingRegion` imply that
|
||||
// the region should effectively be removed if any empty region is given (unlike for the
|
||||
// selection region, it's not meaningful to maintain an empty compose region)
|
||||
//
|
||||
// We aim for more consistent behaviour by normalizing any empty region into `(-1, -1)`
|
||||
// to remove the compose region.
|
||||
//
|
||||
// NB `setComposingRegion` itself is documented to clamp start/end to the text bounds
|
||||
// so apart from this special-case handling in GameText's implementation of
|
||||
// `setComposingRegion` then there's nothing special about `(-1, -1)` - it's just an empty
|
||||
// region that should get clamped to `(0, 0)` and then get removed.
|
||||
if span.start == span.end {
|
||||
ffi::GameTextInputSpan { start: -1, end: -1 }
|
||||
} else {
|
||||
ffi::GameTextInputSpan {
|
||||
start: span.start as i32,
|
||||
end: span.end as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
None => ffi::GameTextInputSpan { start: -1, end: -1 },
|
||||
},
|
||||
};
|
||||
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AndroidAppInner {
|
||||
pub(crate) jvm: CloneJavaVM,
|
||||
native_app: NativeAppGlue,
|
||||
config: ConfigurationRef,
|
||||
native_window: RwLock<Option<NativeWindow>>,
|
||||
|
||||
/// Shared JNI bindings for the `KeyCharacterMap` class
|
||||
key_map_binding: Arc<KeyCharacterMapBinding>,
|
||||
|
||||
/// A table of `KeyCharacterMap`s per `InputDevice` ID
|
||||
/// these are used to be able to map key presses to unicode
|
||||
/// characters
|
||||
key_maps: Mutex<HashMap<i32, KeyCharacterMap>>,
|
||||
|
||||
/// While an app is reading input events it holds an
|
||||
/// InputReceiver reference which we track to ensure
|
||||
/// we don't hand out more than one receiver at a time
|
||||
input_receiver: Mutex<Option<Weak<InputReceiver>>>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -342,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 {
|
||||
@@ -376,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,
|
||||
@@ -476,80 +346,11 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn map_input_state_to_text_event_callback(
|
||||
context: *mut c_void,
|
||||
state: *const ffi::GameTextInputState,
|
||||
) {
|
||||
// Java uses a modified UTF-8 format, which is a modified cesu8 format
|
||||
let out_ptr: *mut TextInputState = context.cast();
|
||||
let text_modified_utf8: *const u8 = (*state).text_UTF8.cast();
|
||||
let text_modified_utf8 =
|
||||
std::slice::from_raw_parts(text_modified_utf8, (*state).text_length as usize);
|
||||
match cesu8::from_java_cesu8(text_modified_utf8) {
|
||||
Ok(str) => {
|
||||
let len = str.len();
|
||||
(*out_ptr).text = String::from(str);
|
||||
|
||||
let selection_start = (*state).selection.start.clamp(0, len as i32 + 1);
|
||||
let selection_end = (*state).selection.end.clamp(0, len as i32 + 1);
|
||||
(*out_ptr).selection = TextSpan {
|
||||
start: selection_start as usize,
|
||||
end: selection_end as usize,
|
||||
};
|
||||
if (*state).composingRegion.start < 0 || (*state).composingRegion.end < 0 {
|
||||
(*out_ptr).compose_region = None;
|
||||
} else {
|
||||
(*out_ptr).compose_region = Some(TextSpan {
|
||||
start: (*state).composingRegion.start as usize,
|
||||
end: (*state).composingRegion.end as usize,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Invalid UTF8 text in TextEvent: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn text_input_state(&self) -> TextInputState {
|
||||
self.native_app.text_input_state()
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn set_text_input_state(&self, state: TextInputState) {
|
||||
self.native_app.set_text_input_state(state);
|
||||
}
|
||||
|
||||
pub(crate) fn device_key_character_map(
|
||||
&self,
|
||||
device_id: i32,
|
||||
) -> InternalResult<KeyCharacterMap> {
|
||||
let mut guard = self.key_maps.lock().unwrap();
|
||||
|
||||
let key_map = match guard.entry(device_id) {
|
||||
std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
|
||||
std::collections::hash_map::Entry::Vacant(vacant) => {
|
||||
let character_map = jni_utils::device_key_character_map(
|
||||
self.jvm.clone(),
|
||||
self.key_map_binding.clone(),
|
||||
device_id,
|
||||
)?;
|
||||
vacant.insert(character_map.clone());
|
||||
character_map
|
||||
}
|
||||
};
|
||||
|
||||
Ok(key_map)
|
||||
}
|
||||
|
||||
pub fn enable_motion_axis(&mut self, axis: Axis) {
|
||||
let axis: u32 = axis.into();
|
||||
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
|
||||
}
|
||||
|
||||
pub fn disable_motion_axis(&mut self, axis: Axis) {
|
||||
let axis: u32 = axis.into();
|
||||
unsafe { ffi::GameActivityPointerAxes_disableAxis(axis as i32) }
|
||||
}
|
||||
|
||||
@@ -588,112 +389,89 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
|
||||
let mut guard = self.input_receiver.lock().unwrap();
|
||||
|
||||
// Make sure we don't hand out more than one receiver at a time because
|
||||
// turning the reciever into an interator will perform a swap_buffers
|
||||
// for the buffered input events which shouldn't happen while we're in
|
||||
// the middle of iterating events
|
||||
if let Some(receiver) = &*guard {
|
||||
if receiver.strong_count() > 0 {
|
||||
return Err(crate::error::InternalAppError::InputUnavailable);
|
||||
pub fn input_events<'b, F>(&self, mut callback: F)
|
||||
where
|
||||
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 == ptr::null_mut() {
|
||||
return;
|
||||
}
|
||||
InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer))
|
||||
};
|
||||
|
||||
for key_event in buf.key_events_iter() {
|
||||
callback(&InputEvent::KeyEvent(key_event));
|
||||
}
|
||||
for motion_event in buf.motion_events_iter() {
|
||||
callback(&InputEvent::MotionEvent(motion_event));
|
||||
}
|
||||
*guard = None;
|
||||
|
||||
let receiver = Arc::new(InputReceiver {
|
||||
native_app: self.native_app.clone(),
|
||||
});
|
||||
|
||||
*guard = Some(Arc::downgrade(&receiver));
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
|
||||
unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
try_get_path_from_ptr((*(*app_ptr).activity).internalDataPath)
|
||||
util::try_get_path_from_ptr((*(*app_ptr).activity).internalDataPath)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
|
||||
unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
try_get_path_from_ptr((*(*app_ptr).activity).externalDataPath)
|
||||
util::try_get_path_from_ptr((*(*app_ptr).activity).externalDataPath)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn obb_path(&self) -> Option<std::path::PathBuf> {
|
||||
unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
try_get_path_from_ptr((*(*app_ptr).activity).obbPath)
|
||||
util::try_get_path_from_ptr((*(*app_ptr).activity).obbPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MotionEventsLendingIterator {
|
||||
struct MotionEventsIterator<'a> {
|
||||
pos: usize,
|
||||
count: usize,
|
||||
buffer: &'a InputBuffer<'a>,
|
||||
}
|
||||
|
||||
impl MotionEventsLendingIterator {
|
||||
fn new(buffer: &InputBuffer) -> Self {
|
||||
Self {
|
||||
pos: 0,
|
||||
count: buffer.motion_events_count(),
|
||||
}
|
||||
}
|
||||
fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option<MotionEvent<'buf>> {
|
||||
impl<'a> Iterator for MotionEventsIterator<'a> {
|
||||
type Item = MotionEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.count {
|
||||
// Safety:
|
||||
// - This iterator currently has exclusive access to the front buffer of events
|
||||
// - We know the buffer is non-null
|
||||
// - `pos` is less than the number of events stored in the buffer
|
||||
let ga_event = unsafe {
|
||||
(*buffer.ptr.as_ptr())
|
||||
.motionEvents
|
||||
.add(self.pos)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
};
|
||||
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 {
|
||||
struct KeyEventsIterator<'a> {
|
||||
pos: usize,
|
||||
count: usize,
|
||||
buffer: &'a InputBuffer<'a>,
|
||||
}
|
||||
|
||||
impl KeyEventsLendingIterator {
|
||||
fn new(buffer: &InputBuffer) -> Self {
|
||||
Self {
|
||||
pos: 0,
|
||||
count: buffer.key_events_count(),
|
||||
}
|
||||
}
|
||||
fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option<KeyEvent<'buf>> {
|
||||
impl<'a> Iterator for KeyEventsIterator<'a> {
|
||||
type Item = KeyEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.count {
|
||||
// Safety:
|
||||
// - This iterator currently has exclusive access to the front buffer of events
|
||||
// - We know the buffer is non-null
|
||||
// - `pos` is less than the number of events stored in the buffer
|
||||
let ga_event = unsafe {
|
||||
(*buffer.ptr.as_ptr())
|
||||
.keyEvents
|
||||
.add(self.pos)
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
};
|
||||
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
|
||||
}
|
||||
@@ -709,16 +487,33 @@ impl<'a> InputBuffer<'a> {
|
||||
pub(crate) fn from_ptr(ptr: NonNull<ffi::android_input_buffer>) -> InputBuffer<'a> {
|
||||
Self {
|
||||
ptr,
|
||||
_lifetime: PhantomData,
|
||||
_lifetime: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -731,160 +526,53 @@ impl<'a> Drop for InputBuffer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Conceptually we can think of this like the receiver end of an
|
||||
/// input events channel.
|
||||
///
|
||||
/// After being passed back to AndroidApp it gets turned into a
|
||||
/// lending iterator for pending input events.
|
||||
///
|
||||
/// It serves two purposes:
|
||||
/// 1. It represents an exclusive access to input events (the application
|
||||
/// can only have one receiver at a time) and it's intended to support
|
||||
/// the double-buffering design for input events in GameActivity where
|
||||
/// we issue a swap_buffers before iterating events and wouldn't want
|
||||
/// another swap to be possible before finishing - especially since
|
||||
/// we want to borrow directly from the buffer while dispatching.
|
||||
/// 2. It doesn't borrow from AndroidAppInner so we can pass it back to
|
||||
/// AndroidApp which can drop its lock around AndroidAppInner and
|
||||
/// it can then be turned into a lending iterator. (We wouldn't
|
||||
/// be able to pass the iterator back to the application if it
|
||||
/// borrowed from within the lock and we need to drop the lock
|
||||
/// because otherwise the app wouldn't be able to access the AndroidApp
|
||||
/// API in any way while iterating events)
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct InputReceiver {
|
||||
// Safety: the native_app effectively has a static lifetime and it
|
||||
// has its own internal locking when calling
|
||||
// `android_app_swap_input_buffers`
|
||||
native_app: NativeAppGlue,
|
||||
}
|
||||
|
||||
impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
|
||||
fn from(receiver: Arc<InputReceiver>) -> Self {
|
||||
let buffered = unsafe {
|
||||
let app_ptr = receiver.native_app.as_ptr();
|
||||
let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
|
||||
NonNull::new(input_buffer).map(|input_buffer| {
|
||||
let buffer = InputBuffer::from_ptr(input_buffer);
|
||||
let keys_iter = KeyEventsLendingIterator::new(&buffer);
|
||||
let motion_iter = MotionEventsLendingIterator::new(&buffer);
|
||||
BufferedEvents::<'a> {
|
||||
buffer,
|
||||
keys_iter,
|
||||
motion_iter,
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let native_app = receiver.native_app.clone();
|
||||
Self {
|
||||
_receiver: receiver,
|
||||
buffered,
|
||||
native_app,
|
||||
text_event_checked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BufferedEvents<'a> {
|
||||
buffer: InputBuffer<'a>,
|
||||
keys_iter: KeyEventsLendingIterator,
|
||||
motion_iter: MotionEventsLendingIterator,
|
||||
}
|
||||
|
||||
pub(crate) struct InputIteratorInner<'a> {
|
||||
// Held to maintain exclusive access to buffered input events
|
||||
_receiver: Arc<InputReceiver>,
|
||||
|
||||
buffered: Option<BufferedEvents<'a>>,
|
||||
native_app: NativeAppGlue,
|
||||
text_event_checked: bool,
|
||||
}
|
||||
|
||||
impl<'a> InputIteratorInner<'a> {
|
||||
pub(crate) fn next<F>(&mut self, callback: F) -> bool
|
||||
where
|
||||
F: FnOnce(&input::InputEvent) -> InputStatus,
|
||||
{
|
||||
if let Some(buffered) = &mut self.buffered {
|
||||
if let Some(key_event) = buffered.keys_iter.next(&buffered.buffer) {
|
||||
let _ = callback(&InputEvent::KeyEvent(key_event));
|
||||
return true;
|
||||
}
|
||||
if let Some(motion_event) = buffered.motion_iter.next(&buffered.buffer) {
|
||||
let _ = callback(&InputEvent::MotionEvent(motion_event));
|
||||
return true;
|
||||
}
|
||||
self.buffered = None;
|
||||
}
|
||||
|
||||
if !self.text_event_checked {
|
||||
self.text_event_checked = true;
|
||||
unsafe {
|
||||
let app_ptr = self.native_app.as_ptr();
|
||||
|
||||
// XXX: It looks like the GameActivity implementation should
|
||||
// be using atomic ops to set this flag, and require us to
|
||||
// use atomics to check and clear it too.
|
||||
//
|
||||
// We currently just hope that with the lack of atomic ops that
|
||||
// the compiler isn't reordering code so this gets flagged
|
||||
// before the java main thread really updates the state.
|
||||
if (*app_ptr).textInputState != 0 {
|
||||
let state = self.native_app.text_input_state(); // Will clear .textInputState
|
||||
let _ = callback(&InputEvent::TextEvent(state));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// 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 these JNI entrypoints from
|
||||
// Rust...
|
||||
//
|
||||
// https://github.com/rust-lang/rfcs/issues/2771
|
||||
extern "C" {
|
||||
pub fn Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
|
||||
pub fn Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
|
||||
env: *mut JNIEnv,
|
||||
javaGameActivity: jobject,
|
||||
path: jstring,
|
||||
funcName: jstring,
|
||||
internalDataDir: jstring,
|
||||
obbDir: jstring,
|
||||
externalDataDir: jstring,
|
||||
jAssetMgr: jobject,
|
||||
savedState: jbyteArray,
|
||||
javaConfig: jobject,
|
||||
) -> jlong;
|
||||
|
||||
pub fn GameActivity_onCreate_C(
|
||||
activity: *mut ffi::GameActivity,
|
||||
savedState: *mut ::std::os::raw::c_void,
|
||||
savedStateSize: libc::size_t,
|
||||
savedStateSize: ffi::size_t,
|
||||
);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_initializeNativeCode(
|
||||
pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_loadNativeCode(
|
||||
env: *mut JNIEnv,
|
||||
java_game_activity: jobject,
|
||||
path: jstring,
|
||||
func_name: jstring,
|
||||
internal_data_dir: jstring,
|
||||
obb_dir: jstring,
|
||||
external_data_dir: jstring,
|
||||
jasset_mgr: jobject,
|
||||
saved_state: jbyteArray,
|
||||
java_config: jobject,
|
||||
) -> jni_sys::jlong {
|
||||
Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
|
||||
Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
|
||||
env,
|
||||
java_game_activity,
|
||||
path,
|
||||
func_name,
|
||||
internal_data_dir,
|
||||
obb_dir,
|
||||
external_data_dir,
|
||||
jasset_mgr,
|
||||
saved_state,
|
||||
java_config,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -892,11 +580,24 @@ pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_initializeN
|
||||
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);
|
||||
}
|
||||
@@ -905,57 +606,59 @@ extern "Rust" {
|
||||
// `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(native_app: *mut ffi::android_app) {
|
||||
abort_on_panic(|| {
|
||||
let _join_log_forwarder = forward_stdio_to_logcat();
|
||||
|
||||
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());
|
||||
|
||||
let jvm = CloneJavaVM::from_raw(jvm).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
|
||||
jvm.attach_current_thread_permanently().unwrap();
|
||||
jvm
|
||||
};
|
||||
|
||||
unsafe {
|
||||
// Name thread - this needs to happen here after attaching to a JVM thread,
|
||||
// since that changes the thread name to something like "Thread-2".
|
||||
let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap();
|
||||
libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr());
|
||||
|
||||
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone());
|
||||
|
||||
// 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);
|
||||
|
||||
// This should detach automatically but lets detach explicitly to avoid depending
|
||||
// on the TLS trickery in `jni-rs`
|
||||
jvm.detach_current_thread();
|
||||
|
||||
ndk_context::release_android_context();
|
||||
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: *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,351 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use jni::{
|
||||
objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue},
|
||||
signature::{Primitive, ReturnType},
|
||||
JNIEnv,
|
||||
};
|
||||
use jni_sys::jint;
|
||||
|
||||
use crate::{
|
||||
input::{Keycode, MetaState},
|
||||
jni_utils::CloneJavaVM,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{AppError, InternalAppError},
|
||||
jni_utils,
|
||||
};
|
||||
|
||||
/// An enum representing the types of keyboards that may generate key events
|
||||
///
|
||||
/// See [getKeyboardType() docs](https://developer.android.com/reference/android/view/KeyCharacterMap#getKeyboardType())
|
||||
///
|
||||
/// # Android Extensible Enum
|
||||
///
|
||||
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
|
||||
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
|
||||
/// forwards compatibility.
|
||||
///
|
||||
/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
|
||||
/// SDK integer values.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialEq, Eq, Hash, num_enum::FromPrimitive, num_enum::IntoPrimitive,
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
#[repr(u32)]
|
||||
pub enum KeyboardType {
|
||||
/// A numeric (12-key) keyboard.
|
||||
///
|
||||
/// A numeric keyboard supports text entry using a multi-tap approach. It may be necessary to tap a key multiple times to generate the desired letter or symbol.
|
||||
///
|
||||
/// This type of keyboard is generally designed for thumb typing.
|
||||
Numeric,
|
||||
|
||||
/// A keyboard with all the letters, but with more than one letter per key.
|
||||
///
|
||||
/// This type of keyboard is generally designed for thumb typing.
|
||||
Predictive,
|
||||
|
||||
/// A keyboard with all the letters, and maybe some numbers.
|
||||
///
|
||||
/// An alphabetic keyboard supports text entry directly but may have a condensed layout with a small form factor. In contrast to a full keyboard, some symbols may only be accessible using special on-screen character pickers. In addition, to improve typing speed and accuracy, the framework provides special affordances for alphabetic keyboards such as auto-capitalization and toggled / locked shift and alt keys.
|
||||
///
|
||||
/// This type of keyboard is generally designed for thumb typing.
|
||||
Alpha,
|
||||
|
||||
/// A full PC-style keyboard.
|
||||
///
|
||||
/// A full keyboard behaves like a PC keyboard. All symbols are accessed directly by pressing keys on the keyboard without on-screen support or affordances such as auto-capitalization.
|
||||
///
|
||||
/// This type of keyboard is generally designed for full two hand typing.
|
||||
Full,
|
||||
|
||||
/// A keyboard that is only used to control special functions rather than for typing.
|
||||
///
|
||||
/// A special function keyboard consists only of non-printing keys such as HOME and POWER that are not actually used for typing.
|
||||
SpecialFunction,
|
||||
|
||||
#[doc(hidden)]
|
||||
#[num_enum(catch_all)]
|
||||
__Unknown(u32),
|
||||
}
|
||||
|
||||
/// Either represents, a unicode character or combining accent from a
|
||||
/// [`KeyCharacterMap`], or `None` for non-printable keys.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KeyMapChar {
|
||||
None,
|
||||
Unicode(char),
|
||||
CombiningAccent(char),
|
||||
}
|
||||
|
||||
// I've also tried to think here about how to we could potentially automatically
|
||||
// generate a binding struct like `KeyCharacterMapBinding` with a procmacro and
|
||||
// so have intentionally limited the `Binding` being a very thin, un-opinionated
|
||||
// wrapper based on basic JNI types.
|
||||
|
||||
/// Lower-level JNI binding for `KeyCharacterMap` class only holds 'static state
|
||||
/// and can be shared with an `Arc` ref count.
|
||||
///
|
||||
/// The separation here also neatly helps us separate `InternalAppError` from
|
||||
/// `AppError` for mapping JNI errors without exposing any `jni-rs` types in the
|
||||
/// public API.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct KeyCharacterMapBinding {
|
||||
//vm: JavaVM,
|
||||
klass: GlobalRef,
|
||||
get_method_id: JMethodID,
|
||||
get_dead_char_method_id: JStaticMethodID,
|
||||
get_keyboard_type_method_id: JMethodID,
|
||||
}
|
||||
|
||||
impl KeyCharacterMapBinding {
|
||||
pub(crate) fn new(env: &mut JNIEnv) -> Result<Self, InternalAppError> {
|
||||
let binding = env.with_local_frame::<_, _, InternalAppError>(10, |env| {
|
||||
let klass = env.find_class("android/view/KeyCharacterMap")?; // Creates a local ref
|
||||
Ok(Self {
|
||||
get_method_id: env.get_method_id(&klass, "get", "(II)I")?,
|
||||
get_dead_char_method_id: env.get_static_method_id(
|
||||
&klass,
|
||||
"getDeadChar",
|
||||
"(II)I",
|
||||
)?,
|
||||
get_keyboard_type_method_id: env.get_method_id(&klass, "getKeyboardType", "()I")?,
|
||||
klass: env.new_global_ref(&klass)?,
|
||||
})
|
||||
})?;
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
pub fn get<'local>(
|
||||
&self,
|
||||
env: &'local mut JNIEnv,
|
||||
key_map: impl AsRef<JObject<'local>>,
|
||||
key_code: jint,
|
||||
meta_state: jint,
|
||||
) -> Result<jint, InternalAppError> {
|
||||
let key_map = key_map.as_ref();
|
||||
|
||||
// Safety:
|
||||
// - we know our global `key_map` reference is non-null and valid.
|
||||
// - we know `get_method_id` remains valid
|
||||
// - we know that the signature of KeyCharacterMap::get is `(int, int) -> int`
|
||||
// - we know this won't leak any local references as a side effect
|
||||
//
|
||||
// We know it's ok to unwrap the `.i()` value since we explicitly
|
||||
// specify the return type as `Int`
|
||||
let unicode = unsafe {
|
||||
env.call_method_unchecked(
|
||||
key_map,
|
||||
self.get_method_id,
|
||||
ReturnType::Primitive(Primitive::Int),
|
||||
&[
|
||||
JValue::Int(key_code).as_jni(),
|
||||
JValue::Int(meta_state).as_jni(),
|
||||
],
|
||||
)
|
||||
}
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
|
||||
Ok(unicode.i().unwrap())
|
||||
}
|
||||
|
||||
pub fn get_dead_char(
|
||||
&self,
|
||||
env: &mut JNIEnv,
|
||||
accent_char: jint,
|
||||
base_char: jint,
|
||||
) -> Result<jint, InternalAppError> {
|
||||
// Safety:
|
||||
// - we know `get_dead_char_method_id` remains valid
|
||||
// - we know that KeyCharacterMap::getDeadKey is a static method
|
||||
// - we know that the signature of KeyCharacterMap::getDeadKey is `(int, int) -> int`
|
||||
// - we know this won't leak any local references as a side effect
|
||||
//
|
||||
// We know it's ok to unwrap the `.i()` value since we explicitly
|
||||
// specify the return type as `Int`
|
||||
|
||||
// Urgh, it's pretty terrible that there's no ergonomic/safe way to get a JClass reference from a GlobalRef
|
||||
// Safety: we don't do anything that would try to delete the JClass as if it were a real local reference
|
||||
let klass = unsafe { JClass::from_raw(self.klass.as_obj().as_raw()) };
|
||||
let unicode = unsafe {
|
||||
env.call_static_method_unchecked(
|
||||
&klass,
|
||||
self.get_dead_char_method_id,
|
||||
ReturnType::Primitive(Primitive::Int),
|
||||
&[
|
||||
JValue::Int(accent_char).as_jni(),
|
||||
JValue::Int(base_char).as_jni(),
|
||||
],
|
||||
)
|
||||
}
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
|
||||
Ok(unicode.i().unwrap())
|
||||
}
|
||||
|
||||
pub fn get_keyboard_type<'local>(
|
||||
&self,
|
||||
env: &'local mut JNIEnv,
|
||||
key_map: impl AsRef<JObject<'local>>,
|
||||
) -> Result<jint, InternalAppError> {
|
||||
let key_map = key_map.as_ref();
|
||||
|
||||
// Safety:
|
||||
// - we know our global `key_map` reference is non-null and valid.
|
||||
// - we know `get_keyboard_type_method_id` remains valid
|
||||
// - we know that the signature of KeyCharacterMap::getKeyboardType is `() -> int`
|
||||
// - we know this won't leak any local references as a side effect
|
||||
//
|
||||
// We know it's ok to unwrap the `.i()` value since we explicitly
|
||||
// specify the return type as `Int`
|
||||
Ok(unsafe {
|
||||
env.call_method_unchecked(
|
||||
key_map,
|
||||
self.get_keyboard_type_method_id,
|
||||
ReturnType::Primitive(Primitive::Int),
|
||||
&[],
|
||||
)
|
||||
}
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?
|
||||
.i()
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the keys provided by a keyboard device and their associated labels.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyCharacterMap {
|
||||
jvm: CloneJavaVM,
|
||||
binding: Arc<KeyCharacterMapBinding>,
|
||||
key_map: GlobalRef,
|
||||
}
|
||||
|
||||
impl KeyCharacterMap {
|
||||
pub(crate) fn new(
|
||||
jvm: CloneJavaVM,
|
||||
binding: Arc<KeyCharacterMapBinding>,
|
||||
key_map: GlobalRef,
|
||||
) -> Self {
|
||||
Self {
|
||||
jvm,
|
||||
binding,
|
||||
key_map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the Unicode character generated by the specified [`Keycode`] and [`MetaState`] combination.
|
||||
///
|
||||
/// Returns [`KeyMapChar::None`] if the key is not one that is used to type Unicode characters.
|
||||
///
|
||||
/// Returns [`KeyMapChar::CombiningAccent`] if the key is a "dead key" that should be combined with
|
||||
/// another to actually produce a character -- see [`KeyCharacterMap::get_dead_char`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Since this API needs to use JNI internally to call into the Android JVM it may return
|
||||
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
|
||||
/// is caught.
|
||||
pub fn get(&self, key_code: Keycode, meta_state: MetaState) -> Result<KeyMapChar, AppError> {
|
||||
let key_code: u32 = key_code.into();
|
||||
let key_code = key_code as jni_sys::jint;
|
||||
let meta_state: u32 = meta_state.0;
|
||||
let meta_state = meta_state as jni_sys::jint;
|
||||
|
||||
// Since we expect this API to be called from the `main` thread then we expect to already be
|
||||
// attached to the JVM
|
||||
//
|
||||
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
|
||||
// borrow rules that ensure we can only add local references to the top JNI frame.
|
||||
let mut env = self.jvm.get_env().map_err(|err| {
|
||||
let err: InternalAppError = err.into();
|
||||
err
|
||||
})?;
|
||||
let unicode = self
|
||||
.binding
|
||||
.get(&mut env, self.key_map.as_obj(), key_code, meta_state)?;
|
||||
let unicode = unicode as u32;
|
||||
|
||||
const COMBINING_ACCENT: u32 = 0x80000000;
|
||||
const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
|
||||
|
||||
if unicode == 0 {
|
||||
Ok(KeyMapChar::None)
|
||||
} else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
|
||||
let accent = unicode & COMBINING_ACCENT_MASK;
|
||||
// Safety: assumes Android key maps don't contain invalid unicode characters
|
||||
Ok(KeyMapChar::CombiningAccent(unsafe {
|
||||
char::from_u32_unchecked(accent)
|
||||
}))
|
||||
} else {
|
||||
// Safety: assumes Android key maps don't contain invalid unicode characters
|
||||
Ok(KeyMapChar::Unicode(unsafe {
|
||||
char::from_u32_unchecked(unicode)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the character that is produced by combining the dead key producing accent with the key producing character c.
|
||||
///
|
||||
/// For example, ```get_dead_char('`', 'e')``` returns 'è'. `get_dead_char('^', ' ')` returns '^' and `get_dead_char('^', '^')` returns '^'.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Since this API needs to use JNI internally to call into the Android JVM it may return
|
||||
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
|
||||
/// is caught.
|
||||
pub fn get_dead_char(
|
||||
&self,
|
||||
accent_char: char,
|
||||
base_char: char,
|
||||
) -> Result<Option<char>, AppError> {
|
||||
let accent_char = accent_char as jni_sys::jint;
|
||||
let base_char = base_char as jni_sys::jint;
|
||||
|
||||
// Since we expect this API to be called from the `main` thread then we expect to already be
|
||||
// attached to the JVM
|
||||
//
|
||||
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
|
||||
// borrow rules that ensure we can only add local references to the top JNI frame.
|
||||
let mut env = self.jvm.get_env().map_err(|err| {
|
||||
let err: InternalAppError = err.into();
|
||||
err
|
||||
})?;
|
||||
let unicode = self
|
||||
.binding
|
||||
.get_dead_char(&mut env, accent_char, base_char)?;
|
||||
let unicode = unicode as u32;
|
||||
|
||||
// Safety: assumes Android key maps don't contain invalid unicode characters
|
||||
Ok(if unicode == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { char::from_u32_unchecked(unicode) })
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the keyboard type.
|
||||
///
|
||||
/// Different keyboard types have different semantics. See [`KeyboardType`] for details.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Since this API needs to use JNI internally to call into the Android JVM it may return
|
||||
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
|
||||
/// is caught.
|
||||
pub fn get_keyboard_type(&self) -> Result<KeyboardType, AppError> {
|
||||
// Since we expect this API to be called from the `main` thread then we expect to already be
|
||||
// attached to the JVM
|
||||
//
|
||||
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
|
||||
// borrow rules that ensure we can only add local references to the top JNI frame.
|
||||
let mut env = self.jvm.get_env().map_err(|err| {
|
||||
let err: InternalAppError = err.into();
|
||||
err
|
||||
})?;
|
||||
let keyboard_type = self
|
||||
.binding
|
||||
.get_keyboard_type(&mut env, self.key_map.as_obj())?;
|
||||
let keyboard_type = keyboard_type as u32;
|
||||
Ok(keyboard_type.into())
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
//! The JNI calls we make in this crate are often not part of a Java native
|
||||
//! method implementation and so we can't assume we have a JNI local frame that
|
||||
//! is going to unwind and free local references, and we also can't just leave
|
||||
//! exceptions to get thrown when returning to Java.
|
||||
//!
|
||||
//! These utilities help us check + clear exceptions and map them into Rust Errors.
|
||||
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use jni::{
|
||||
objects::{JObject, JString},
|
||||
JavaVM,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{InternalAppError, InternalResult},
|
||||
input::{KeyCharacterMap, KeyCharacterMapBinding},
|
||||
};
|
||||
|
||||
// TODO: JavaVM should implement Clone
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CloneJavaVM {
|
||||
pub jvm: JavaVM,
|
||||
}
|
||||
impl Clone for CloneJavaVM {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
jvm: unsafe { JavaVM::from_raw(self.jvm.get_java_vm_pointer()).unwrap() },
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CloneJavaVM {
|
||||
pub unsafe fn from_raw(jvm: *mut jni_sys::JavaVM) -> InternalResult<Self> {
|
||||
Ok(Self {
|
||||
jvm: JavaVM::from_raw(jvm)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
unsafe impl Send for CloneJavaVM {}
|
||||
unsafe impl Sync for CloneJavaVM {}
|
||||
|
||||
impl Deref for CloneJavaVM {
|
||||
type Target = JavaVM;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.jvm
|
||||
}
|
||||
}
|
||||
|
||||
/// Use with `.map_err()` to map `jni::errors::Error::JavaException` into a
|
||||
/// richer error based on the actual contents of the `JThrowable`
|
||||
///
|
||||
/// (The `jni` crate doesn't do that automatically since it's more
|
||||
/// common to let the exception get thrown when returning to Java)
|
||||
///
|
||||
/// This will also clear the exception
|
||||
pub(crate) fn clear_and_map_exception_to_err(
|
||||
env: &mut jni::JNIEnv<'_>,
|
||||
err: jni::errors::Error,
|
||||
) -> InternalAppError {
|
||||
if matches!(err, jni::errors::Error::JavaException) {
|
||||
let result = env.with_local_frame::<_, _, InternalAppError>(5, |env| {
|
||||
let e = env.exception_occurred()?;
|
||||
assert!(!e.is_null()); // should only be called after receiving a JavaException Result
|
||||
env.exception_clear()?;
|
||||
|
||||
let class = env.get_object_class(&e)?;
|
||||
//let get_stack_trace_method = env.get_method_id(&class, "getStackTrace", "()[Ljava/lang/StackTraceElement;")?;
|
||||
let get_message_method =
|
||||
env.get_method_id(&class, "getMessage", "()Ljava/lang/String;")?;
|
||||
|
||||
let msg = unsafe {
|
||||
env.call_method_unchecked(
|
||||
&e,
|
||||
get_message_method,
|
||||
jni::signature::ReturnType::Object,
|
||||
&[],
|
||||
)?
|
||||
.l()
|
||||
.unwrap()
|
||||
};
|
||||
let msg = unsafe { JString::from_raw(JObject::into_raw(msg)) };
|
||||
let msg = env.get_string(&msg)?;
|
||||
let msg: String = msg.into();
|
||||
|
||||
// TODO: get Java backtrace:
|
||||
/*
|
||||
if let JValue::Object(elements) = env.call_method_unchecked(&e, get_stack_trace_method, jni::signature::ReturnType::Array, &[])? {
|
||||
let elements = env.auto_local(elements);
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(msg)
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(msg) => InternalAppError::JniException(msg),
|
||||
Err(err) => InternalAppError::JniException(format!(
|
||||
"UNKNOWN (Failed to query JThrowable: {err:?})"
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn device_key_character_map(
|
||||
jvm: CloneJavaVM,
|
||||
key_map_binding: Arc<KeyCharacterMapBinding>,
|
||||
device_id: i32,
|
||||
) -> InternalResult<KeyCharacterMap> {
|
||||
// Don't really need to 'attach' since this should be called from the app's main thread that
|
||||
// should already be attached, but the redundancy should be fine
|
||||
//
|
||||
// Attach 'permanently' to avoid any chance of detaching the thread from the VM
|
||||
let mut env = jvm.attach_current_thread_permanently()?;
|
||||
|
||||
// We don't want to accidentally leak any local references while we
|
||||
// aren't going to be returning from here back to the JVM, to unwind, so
|
||||
// we make a local frame
|
||||
let character_map = env.with_local_frame::<_, _, jni::errors::Error>(10, |env| {
|
||||
let input_device_class = env.find_class("android/view/InputDevice")?; // Creates a local ref
|
||||
let device = env
|
||||
.call_static_method(
|
||||
input_device_class,
|
||||
"getDevice",
|
||||
"(I)Landroid/view/InputDevice;",
|
||||
&[device_id.into()],
|
||||
)?
|
||||
.l()?; // Creates a local ref
|
||||
|
||||
let character_map = env
|
||||
.call_method(
|
||||
&device,
|
||||
"getKeyCharacterMap",
|
||||
"()Landroid/view/KeyCharacterMap;",
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
let character_map = env.new_global_ref(character_map)?;
|
||||
|
||||
Ok(character_map)
|
||||
})?;
|
||||
|
||||
Ok(KeyCharacterMap::new(
|
||||
jvm.clone(),
|
||||
key_map_binding,
|
||||
character_map,
|
||||
))
|
||||
}
|
||||
@@ -1,126 +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.
|
||||
//!
|
||||
//! # Cheaply Clonable [`AndroidApp`]
|
||||
//!
|
||||
//! [`AndroidApp`] is intended to be something that can be cheaply passed around
|
||||
//! by referenced within an application. It is reference counted and can be
|
||||
//! cheaply cloned.
|
||||
//!
|
||||
//! # `Send` and `Sync` [`AndroidApp`]
|
||||
//!
|
||||
//! Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
|
||||
//! into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
|
||||
//! explicitly documented to only be usable from your `android_main()` thread.
|
||||
//!
|
||||
//! # 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
|
||||
//!
|
||||
//! # Android Extensible Enums
|
||||
//!
|
||||
//! There are numerous enums in the `android-activity` API which are effectively
|
||||
//! bindings to enums declared in the Android SDK which need to be considered
|
||||
//! _runtime_ extensible.
|
||||
//!
|
||||
//! Any enum variants that come from the Android SDK may be extended in future
|
||||
//! versions of Android and your code could be exposed to new variants if you
|
||||
//! build an application that might be installed on new versions of Android.
|
||||
//!
|
||||
//! This crate follows a convention of adding a hidden `__Unknown(u32)` variant
|
||||
//! to these enum to ensure we can always do lossless conversions between the
|
||||
//! integers from the SDK and our corresponding Rust enums. This can be
|
||||
//! important in case you need to pass certain variants back to the SDK
|
||||
//! regardless of whether you knew about that variants specific semantics at
|
||||
//! compile time.
|
||||
//!
|
||||
//! You should never include this `__Unknown(u32)` variant within any exhaustive
|
||||
//! pattern match and should instead treat the enums like `#[non_exhaustive]`
|
||||
//! enums that require you to add a catch-all for any `unknown => {}` values.
|
||||
//!
|
||||
//! Any code that would exhaustively include the `__Unknown(u32)` variant when
|
||||
//! pattern matching can not be guaranteed to be forwards compatible with new
|
||||
//! releases of `android-activity` which may add new Rust variants to these
|
||||
//! enums without requiring a breaking semver bump.
|
||||
//!
|
||||
//! You can (infallibly) convert these enums to and from primitive `u32` values
|
||||
//! using `.into()`:
|
||||
//!
|
||||
//! For example, here is how you could ensure forwards compatibility with both
|
||||
//! compile-time and runtime extensions of a `SomeEnum` enum:
|
||||
//!
|
||||
//! ```rust
|
||||
//! match some_enum {
|
||||
//! SomeEnum::Foo => {},
|
||||
//! SomeEnum::Bar => {},
|
||||
//! unhandled => {
|
||||
//! let sdk_val: u32 = unhandled.into();
|
||||
//! println!("Unhandled enum variant {some_enum:?} has SDK value: {sdk_val}");
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! [`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
|
||||
|
||||
#![deny(clippy::manual_let_else)]
|
||||
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use input::KeyCharacterMap;
|
||||
use libc::c_void;
|
||||
use ndk::asset::AssetManager;
|
||||
use ndk::native_window::NativeWindow;
|
||||
|
||||
@@ -131,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)
|
||||
@@ -151,26 +33,34 @@ 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_attr(any(feature = "native-activity", doc), path = "native_activity/mod.rs")]
|
||||
#[cfg_attr(any(feature = "game-activity", doc), path = "game_activity/mod.rs")]
|
||||
pub(crate) mod activity_impl;
|
||||
#[cfg(any(feature = "native-activity", doc))]
|
||||
mod native_activity;
|
||||
#[cfg(any(feature = "native-activity", doc))]
|
||||
use native_activity as activity_impl;
|
||||
|
||||
pub mod error;
|
||||
use error::Result;
|
||||
#[cfg(feature = "game-activity")]
|
||||
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;
|
||||
|
||||
mod jni_utils;
|
||||
|
||||
/// 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,
|
||||
@@ -179,55 +69,20 @@ 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> {
|
||||
/// New input events are available via [`AndroidApp::input_events_iter()`]
|
||||
/// New input events are available via [`AndroidApp::input_events()`]
|
||||
///
|
||||
/// _Note: Even if more input is received this event will not be resent
|
||||
/// until [`AndroidApp::input_events_iter()`] has been called, which enables
|
||||
/// until [`AndroidApp::input_events()`] has been called, which enables
|
||||
/// applications to batch up input processing without there being lots of
|
||||
/// redundant event loop wake ups._
|
||||
///
|
||||
/// [`AndroidApp::input_events_iter()`]: AndroidApp::input_events_iter
|
||||
/// [`AndroidApp::input_events()`]: AndroidApp::input_events
|
||||
InputAvailable,
|
||||
|
||||
/// Command from main thread: a new [`NativeWindow`] is ready for use. Upon
|
||||
@@ -308,7 +163,6 @@ pub enum MainEvent<'a> {
|
||||
InsetsChanged {},
|
||||
}
|
||||
|
||||
/// An event delivered during [`AndroidApp::poll_events`]
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum PollEvent<'a> {
|
||||
@@ -317,22 +171,12 @@ 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;
|
||||
|
||||
bitflags! {
|
||||
/// Flags for [`AndroidApp::set_window_flags`]
|
||||
/// as per the [android.view.WindowManager.LayoutParams Java API](https://developer.android.com/reference/android/view/WindowManager.LayoutParams)
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct WindowManagerFlags: u32 {
|
||||
/// As long as this window is visible to the user, allow the lock
|
||||
/// screen to activate while the screen is on. This can be used
|
||||
@@ -496,25 +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.
|
||||
///
|
||||
/// # Cheaply Clonable [`AndroidApp`]
|
||||
///
|
||||
/// [`AndroidApp`] is intended to be something that can be cheaply passed around
|
||||
/// by referenced within an application. It is reference counted and can be
|
||||
/// cheaply cloned.
|
||||
///
|
||||
/// # `Send` and `Sync` [`AndroidApp`]
|
||||
///
|
||||
/// Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
|
||||
/// into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
|
||||
/// explicitly documented to only be usable from your `android_main()` thread.
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AndroidApp {
|
||||
pub(crate) inner: Arc<RwLock<AndroidAppInner>>,
|
||||
@@ -534,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
|
||||
@@ -598,22 +386,17 @@ 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
|
||||
F: FnMut(PollEvent<'_>),
|
||||
F: FnMut(PollEvent),
|
||||
{
|
||||
self.inner.read().unwrap().poll_events(timeout, callback);
|
||||
}
|
||||
|
||||
/// Creates a means to wake up the main loop while it is blocked waiting for
|
||||
/// events within [`AndroidApp::poll_events()`].
|
||||
pub fn create_waker(&self) -> AndroidAppWaker {
|
||||
pub fn create_waker(&self) -> activity_impl::AndroidAppWaker {
|
||||
self.inner.read().unwrap().create_waker()
|
||||
}
|
||||
|
||||
@@ -688,149 +471,20 @@ impl AndroidApp {
|
||||
.hide_soft_input(hide_implicit_only);
|
||||
}
|
||||
|
||||
/// Fetch the current input text state, as updated by any active IME.
|
||||
pub fn text_input_state(&self) -> input::TextInputState {
|
||||
self.inner.read().unwrap().text_input_state()
|
||||
}
|
||||
|
||||
/// Forward the given input text `state` to any active IME.
|
||||
pub fn set_text_input_state(&self, state: input::TextInputState) {
|
||||
self.inner.read().unwrap().set_text_input_state(state);
|
||||
}
|
||||
|
||||
/// Get an exclusive, lending iterator over buffered input events
|
||||
/// Query and process all out-standing input event
|
||||
///
|
||||
/// Applications are expected to call this in-sync with their rendering or
|
||||
/// in response to a [`MainEvent::InputAvailable`] event being delivered.
|
||||
/// 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
|
||||
/// to this API._
|
||||
///
|
||||
/// _**Note:** your application is will only be delivered a single
|
||||
/// [`MainEvent::InputAvailable`] event between calls to this API._
|
||||
///
|
||||
/// To reduce overhead, by default, only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
|
||||
/// 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`].
|
||||
///
|
||||
/// This isn't the most ergonomic iteration API since we can't return a standard `Iterator`:
|
||||
/// - This API returns a lending iterator may borrow from the internal buffer
|
||||
/// of pending events without copying them.
|
||||
/// - For each event we want to ensure the application reports whether the
|
||||
/// event was handled.
|
||||
///
|
||||
/// # Example
|
||||
/// Code to iterate all pending input events would look something like this:
|
||||
///
|
||||
/// ```rust
|
||||
/// match app.input_events_iter() {
|
||||
/// Ok(mut iter) => {
|
||||
/// loop {
|
||||
/// let read_input = iter.next(|event| {
|
||||
/// let handled = match event {
|
||||
/// InputEvent::KeyEvent(key_event) => {
|
||||
/// // Snip
|
||||
/// }
|
||||
/// InputEvent::MotionEvent(motion_event) => {
|
||||
/// // Snip
|
||||
/// }
|
||||
/// event => {
|
||||
/// // Snip
|
||||
/// }
|
||||
/// };
|
||||
///
|
||||
/// handled
|
||||
/// });
|
||||
///
|
||||
/// if !read_input {
|
||||
/// break;
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// Err(err) => {
|
||||
/// log::error!("Failed to get input events iterator: {err:?}");
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This must only be called from your `android_main()` thread and it may panic if called
|
||||
/// from another thread.
|
||||
pub fn input_events_iter(&self) -> Result<input::InputIterator> {
|
||||
let receiver = {
|
||||
let guard = self.inner.read().unwrap();
|
||||
guard.input_events_receiver()?
|
||||
};
|
||||
|
||||
Ok(input::InputIterator {
|
||||
inner: receiver.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Lookup the [`KeyCharacterMap`] for the given input `device_id`
|
||||
///
|
||||
/// Use [`KeyCharacterMap::get`] to map key codes + meta state into unicode characters
|
||||
/// or dead keys that compose with the next key.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Code to handle unicode character mapping as well as combining dead keys could look some thing like:
|
||||
///
|
||||
/// ```rust
|
||||
/// let mut combining_accent = None;
|
||||
/// // Snip
|
||||
///
|
||||
/// let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
|
||||
/// match map.get(key_event.key_code(), key_event.meta_state()) {
|
||||
/// Ok(KeyMapChar::Unicode(unicode)) => {
|
||||
/// let combined_unicode = if let Some(accent) = combining_accent {
|
||||
/// match map.get_dead_char(accent, unicode) {
|
||||
/// Ok(Some(key)) => {
|
||||
/// info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
|
||||
/// Some(key)
|
||||
/// }
|
||||
/// Ok(None) => None,
|
||||
/// Err(err) => {
|
||||
/// log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
|
||||
/// None
|
||||
/// }
|
||||
/// }
|
||||
/// } else {
|
||||
/// info!("KeyEvent: Pressed '{unicode}'");
|
||||
/// Some(unicode)
|
||||
/// };
|
||||
/// combining_accent = None;
|
||||
/// combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
|
||||
/// }
|
||||
/// Ok(KeyMapChar::CombiningAccent(accent)) => {
|
||||
/// info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
|
||||
/// combining_accent = Some(accent);
|
||||
/// Some(KeyMapChar::CombiningAccent(accent))
|
||||
/// }
|
||||
/// Ok(KeyMapChar::None) => {
|
||||
/// info!("KeyEvent: Pressed non-unicode key");
|
||||
/// combining_accent = None;
|
||||
/// None
|
||||
/// }
|
||||
/// Err(err) => {
|
||||
/// log::error!("KeyEvent: Failed to get key map character: {err:?}");
|
||||
/// combining_accent = None;
|
||||
/// None
|
||||
/// }
|
||||
/// }
|
||||
/// } else {
|
||||
/// None
|
||||
/// };
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Since this API needs to use JNI internally to call into the Android JVM it may return
|
||||
/// a [`error::AppError::JavaError`] in case there is a spurious JNI error or an exception
|
||||
/// is caught.
|
||||
pub fn device_key_character_map(&self, device_id: i32) -> Result<KeyCharacterMap> {
|
||||
Ok(self
|
||||
.inner
|
||||
.read()
|
||||
.unwrap()
|
||||
.device_key_character_map(device_id)?)
|
||||
pub fn input_events<'b, F>(&self, callback: F)
|
||||
where
|
||||
F: FnMut(&input::InputEvent),
|
||||
{
|
||||
self.inner.read().unwrap().input_events(callback);
|
||||
}
|
||||
|
||||
/// The user-visible SDK version of the framework
|
||||
@@ -839,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,920 +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::{
|
||||
ops::Deref,
|
||||
panic::catch_unwind,
|
||||
ptr::{self, NonNull},
|
||||
sync::{Arc, Condvar, Mutex, Weak},
|
||||
};
|
||||
|
||||
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
|
||||
|
||||
use crate::{
|
||||
jni_utils::CloneJavaVM,
|
||||
util::{abort_on_panic, forward_stdio_to_logcat, 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// The status of the native thread that's created to run
|
||||
/// `android_main`
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum NativeThreadState {
|
||||
/// The `android_main` thread hasn't been created yet
|
||||
Init,
|
||||
/// The `android_main` thread has been spawned and started running
|
||||
Running,
|
||||
/// The `android_main` thread has finished
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NativeActivityState {
|
||||
pub msg_read: libc::c_int,
|
||||
pub msg_write: libc::c_int,
|
||||
pub config: 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 thread_state: NativeThreadState,
|
||||
pub app_has_saved_state: bool,
|
||||
|
||||
/// Set as soon as the Java main thread notifies us of an
|
||||
/// `onDestroyed` callback.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = if saved_state_in.is_null() {
|
||||
Vec::new()
|
||||
} else {
|
||||
unsafe { std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size) }
|
||||
.to_vec()
|
||||
};
|
||||
|
||||
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,
|
||||
input_queue: ptr::null_mut(),
|
||||
window: None,
|
||||
content_rect: Rect::empty().into(),
|
||||
activity_state: State::Init,
|
||||
destroy_requested: false,
|
||||
thread_state: NativeThreadState::Init,
|
||||
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();
|
||||
guard.destroyed = true;
|
||||
|
||||
unsafe {
|
||||
guard.write_cmd(AppCmd::Destroy);
|
||||
while guard.thread_state != NativeThreadState::Stopped {
|
||||
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();
|
||||
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.thread_state = NativeThreadState::Running;
|
||||
self.cond.notify_one();
|
||||
}
|
||||
|
||||
pub fn notify_main_thread_stopped_running(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.thread_state = NativeThreadState::Stopped;
|
||||
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 usize,
|
||||
) -> *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;
|
||||
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]
|
||||
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(|| {
|
||||
let _join_log_forwarder = forward_stdio_to_logcat();
|
||||
|
||||
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 = abort_on_panic(|| unsafe {
|
||||
let na = activity;
|
||||
let jvm: *mut jni_sys::JavaVM = (*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());
|
||||
|
||||
let jvm = CloneJavaVM::from_raw(jvm).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
|
||||
jvm.attach_current_thread_permanently().unwrap();
|
||||
jvm
|
||||
});
|
||||
|
||||
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
|
||||
|
||||
rust_glue.notify_main_thread_running();
|
||||
|
||||
unsafe {
|
||||
// Name thread - this needs to happen here after attaching to a JVM thread,
|
||||
// since that changes the thread name to something like "Thread-2".
|
||||
let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap();
|
||||
libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr());
|
||||
|
||||
// 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(log_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);
|
||||
|
||||
// This should detach automatically but lets detach explicitly to avoid depending
|
||||
// on the TLS trickery in `jni-rs`
|
||||
jvm.detach_current_thread();
|
||||
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
|
||||
rust_glue.notify_main_thread_stopped_running();
|
||||
});
|
||||
|
||||
// Wait for thread to start.
|
||||
let mut guard = jvm_glue.mutex.lock().unwrap();
|
||||
|
||||
// Don't specifically wait for `Running` just in case `android_main` returns
|
||||
// immediately and the state is set to `Stopped`
|
||||
while guard.thread_state == NativeThreadState::Init {
|
||||
guard = jvm_glue.cond.wait(guard).unwrap();
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,437 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::input::{
|
||||
Axis, Button, ButtonState, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction,
|
||||
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
|
||||
};
|
||||
|
||||
/// 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 also capture unknown variants
|
||||
// added in new versions of Android.
|
||||
let source =
|
||||
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
source.into()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// XXX: we use `AMotionEvent_getAction` directly since we have our own
|
||||
// `MotionAction` enum that we share between backends, which may also
|
||||
// capture unknown variants added in new versions of Android.
|
||||
let action =
|
||||
unsafe { ndk_sys::AMotionEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 }
|
||||
& ndk_sys::AMOTION_EVENT_ACTION_MASK;
|
||||
action.into()
|
||||
}
|
||||
|
||||
/// Returns which button has been modified during a press or release action.
|
||||
///
|
||||
/// For actions other than [`MotionAction::ButtonPress`] and
|
||||
/// [`MotionAction::ButtonRelease`] the returned value is undefined.
|
||||
///
|
||||
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionButton())
|
||||
#[inline]
|
||||
pub fn action_button(&self) -> Button {
|
||||
let action_button =
|
||||
unsafe { ndk_sys::AMotionEvent_getActionButton(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
action_button.into()
|
||||
}
|
||||
|
||||
/// 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<'_> {
|
||||
PointersIter {
|
||||
inner: PointersIterImpl {
|
||||
ndk_pointers_iter: 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<'_> {
|
||||
Pointer {
|
||||
inner: PointerImpl {
|
||||
ndk_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().into()
|
||||
}
|
||||
|
||||
/// 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().into()
|
||||
}
|
||||
|
||||
/// 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().into()
|
||||
}
|
||||
|
||||
/// 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().into()
|
||||
}
|
||||
|
||||
/* 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 view into the data of a specific pointer in a motion event.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PointerImpl<'a> {
|
||||
ndk_pointer: ndk::event::Pointer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> PointerImpl<'a> {
|
||||
#[inline]
|
||||
pub fn pointer_index(&self) -> usize {
|
||||
self.ndk_pointer.pointer_index()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pointer_id(&self) -> i32 {
|
||||
self.ndk_pointer.pointer_id()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn axis_value(&self, axis: Axis) -> f32 {
|
||||
let value: u32 = axis.into();
|
||||
let value = value as i32;
|
||||
self.ndk_pointer.axis_value(value.into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn raw_x(&self) -> f32 {
|
||||
self.ndk_pointer.raw_x()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn raw_y(&self) -> f32 {
|
||||
self.ndk_pointer.raw_y()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tool_type(&self) -> ToolType {
|
||||
let value: i32 = self.ndk_pointer.tool_type().into();
|
||||
let value = value as u32;
|
||||
value.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the pointers in a [`MotionEvent`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PointersIterImpl<'a> {
|
||||
ndk_pointers_iter: ndk::event::PointersIter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PointersIterImpl<'a> {
|
||||
type Item = Pointer<'a>;
|
||||
fn next(&mut self) -> Option<Pointer<'a>> {
|
||||
self.ndk_pointers_iter.next().map(|ndk_pointer| Pointer {
|
||||
inner: PointerImpl { ndk_pointer },
|
||||
})
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.ndk_pointers_iter.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ExactSizeIterator for PointersIterImpl<'a> {
|
||||
fn len(&self) -> usize {
|
||||
self.ndk_pointers_iter.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 also capture unknown variants
|
||||
// added in new versions of Android.
|
||||
let source =
|
||||
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
source.into()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// XXX: we use `AInputEvent_getAction` directly since we have our own
|
||||
// `KeyAction` enum that we share between backends, which may also
|
||||
// capture unknown variants added in new versions of Android.
|
||||
let action = unsafe { ndk_sys::AKeyEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
action.into()
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// XXX: we use `AInputEvent_getKeyCode` directly since we have our own
|
||||
// `Keycode` enum that we share between backends, which may also
|
||||
// capture unknown variants added in new versions of Android.
|
||||
let keycode =
|
||||
unsafe { ndk_sys::AKeyEvent_getKeyCode(self.ndk_event.ptr().as_ptr()) as u32 };
|
||||
keycode.into()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Returns the state of the modifiers during this key event, represented by a bitmask.
|
||||
///
|
||||
/// See [the NDK
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getmetastate)
|
||||
#[inline]
|
||||
pub fn meta_state(&self) -> MetaState {
|
||||
self.ndk_event.meta_state().into()
|
||||
}
|
||||
}
|
||||
|
||||
// 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>),
|
||||
TextEvent(crate::input::TextInputState),
|
||||
}
|
||||
@@ -1,300 +1,319 @@
|
||||
#![cfg(any(feature = "native-activity", doc))]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
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, Mutex, RwLock, Weak};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use std::{ptr, thread};
|
||||
|
||||
use libc::c_void;
|
||||
use log::{error, trace};
|
||||
use log::{error, info, trace, Level};
|
||||
|
||||
use ndk_sys::ALooper_wake;
|
||||
use ndk_sys::{ALooper, ALooper_pollAll};
|
||||
|
||||
use ndk::asset::AssetManager;
|
||||
use ndk::configuration::Configuration;
|
||||
use ndk::input_queue::InputQueue;
|
||||
use ndk::{asset::AssetManager, native_window::NativeWindow};
|
||||
use ndk::native_window::NativeWindow;
|
||||
|
||||
use crate::error::InternalResult;
|
||||
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
|
||||
use crate::input::{TextInputState, TextSpan};
|
||||
use crate::jni_utils::{self, CloneJavaVM};
|
||||
use crate::{
|
||||
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
|
||||
};
|
||||
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
|
||||
|
||||
pub mod input;
|
||||
mod ffi;
|
||||
|
||||
mod glue;
|
||||
use self::glue::NativeActivityGlue;
|
||||
pub mod input {
|
||||
pub use ndk::event::{
|
||||
Axis, ButtonState, EdgeFlags, KeyAction, KeyEvent, KeyEventFlags, Keycode, MetaState,
|
||||
MotionAction, MotionEvent, MotionEventFlags, Pointer, Source,
|
||||
};
|
||||
|
||||
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;
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface for saving application state during [MainEvent::SaveState] events
|
||||
///
|
||||
/// This interface is only available temporarily while handling a [MainEvent::SaveState] event.
|
||||
// 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, jvm: CloneJavaVM) -> Self {
|
||||
let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp
|
||||
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));
|
||||
|
||||
let key_map_binding = match KeyCharacterMapBinding::new(&mut env) {
|
||||
Ok(b) => b,
|
||||
Err(err) => {
|
||||
panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}");
|
||||
}
|
||||
};
|
||||
|
||||
let app = Self {
|
||||
AndroidApp {
|
||||
inner: Arc::new(RwLock::new(AndroidAppInner {
|
||||
jvm,
|
||||
native_activity,
|
||||
looper: Looper {
|
||||
ptr: ptr::null_mut(),
|
||||
},
|
||||
key_map_binding: Arc::new(key_map_binding),
|
||||
key_maps: Mutex::new(HashMap::new()),
|
||||
input_receiver: Mutex::new(None),
|
||||
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) jvm: CloneJavaVM,
|
||||
|
||||
pub(crate) native_activity: NativeActivityGlue,
|
||||
looper: Looper,
|
||||
|
||||
/// Shared JNI bindings for the `KeyCharacterMap` class
|
||||
key_map_binding: Arc<KeyCharacterMapBinding>,
|
||||
|
||||
/// A table of `KeyCharacterMap`s per `InputDevice` ID
|
||||
/// these are used to be able to map key presses to unicode
|
||||
/// characters
|
||||
key_maps: Mutex<HashMap<i32, KeyCharacterMap>>,
|
||||
|
||||
/// While an app is reading input events it holds an
|
||||
/// InputReceiver reference which we track to ensure
|
||||
/// we don't hand out more than one receiver at a time
|
||||
input_receiver: Mutex<Option<Weak<InputReceiver>>>,
|
||||
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)
|
||||
where
|
||||
F: FnMut(PollEvent<'_>),
|
||||
F: FnMut(PollEvent),
|
||||
{
|
||||
trace!("poll_events");
|
||||
|
||||
unsafe {
|
||||
let native_app = &self.native_app;
|
||||
|
||||
let mut fd: i32 = 0;
|
||||
let mut events: i32 = 0;
|
||||
let mut source: *mut c_void = ptr::null_mut();
|
||||
let mut source: *mut core::ffi::c_void = ptr::null_mut();
|
||||
|
||||
let timeout_milliseconds = if let Some(timeout) = timeout {
|
||||
timeout.as_millis() as i32
|
||||
} 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 c_void,
|
||||
&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))
|
||||
}
|
||||
_ => {
|
||||
@@ -311,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)
|
||||
}
|
||||
}
|
||||
@@ -343,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(),
|
||||
@@ -356,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,79 +397,69 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn text_input_state(&self) -> TextInputState {
|
||||
TextInputState {
|
||||
text: String::new(),
|
||||
selection: TextSpan { start: 0, end: 0 },
|
||||
compose_region: None,
|
||||
}
|
||||
pub fn enable_motion_axis(&self, _axis: input::Axis) {
|
||||
// NOP - The InputQueue API doesn't let us optimize which axis values are read
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn set_text_input_state(&self, _state: TextInputState) {
|
||||
// NOP: Unsupported
|
||||
pub fn disable_motion_axis(&self, _axis: input::Axis) {
|
||||
// NOP - The InputQueue API doesn't let us optimize which axis values are read
|
||||
}
|
||||
|
||||
pub fn device_key_character_map(&self, device_id: i32) -> InternalResult<KeyCharacterMap> {
|
||||
let mut guard = self.key_maps.lock().unwrap();
|
||||
|
||||
let key_map = match guard.entry(device_id) {
|
||||
std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
|
||||
std::collections::hash_map::Entry::Vacant(vacant) => {
|
||||
let character_map = jni_utils::device_key_character_map(
|
||||
self.jvm.clone(),
|
||||
self.key_map_binding.clone(),
|
||||
device_id,
|
||||
)?;
|
||||
vacant.insert(character_map.clone());
|
||||
character_map
|
||||
pub fn input_events<'b, F>(&self, mut callback: F)
|
||||
where
|
||||
F: FnMut(&input::InputEvent),
|
||||
{
|
||||
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)
|
||||
};
|
||||
|
||||
Ok(key_map)
|
||||
}
|
||||
// Note: we basically ignore errors from get_event() currently. Looking
|
||||
// at the source code for Android's InputQueue, the only error that
|
||||
// can be returned here is 'WOULD_BLOCK', which we want to just treat as
|
||||
// meaning the queue is empty.
|
||||
//
|
||||
// ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
|
||||
//
|
||||
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(e),
|
||||
ndk::event::InputEvent::KeyEvent(e) => input::InputEvent::KeyEvent(e),
|
||||
};
|
||||
callback(&event);
|
||||
|
||||
pub fn enable_motion_axis(&self, _axis: Axis) {
|
||||
// NOP - The InputQueue API doesn't let us optimize which axis values are read
|
||||
}
|
||||
let ndk_event = match event {
|
||||
input::InputEvent::MotionEvent(e) => ndk::event::InputEvent::MotionEvent(e),
|
||||
input::InputEvent::KeyEvent(e) => ndk::event::InputEvent::KeyEvent(e),
|
||||
};
|
||||
|
||||
pub fn disable_motion_axis(&self, _axis: Axis) {
|
||||
// NOP - The InputQueue API doesn't let us optimize which axis values are read
|
||||
}
|
||||
|
||||
pub fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
|
||||
let mut guard = self.input_receiver.lock().unwrap();
|
||||
|
||||
if let Some(receiver) = &*guard {
|
||||
if receiver.strong_count() > 0 {
|
||||
return Err(crate::error::InternalAppError::InputUnavailable);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
*guard = None;
|
||||
|
||||
// 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);
|
||||
|
||||
// Note: we don't treat it as an error if there is no queue, so if applications
|
||||
// iterate input before a queue has been created (e.g. before onStart) then
|
||||
// it will simply behave like there are no events available currently.
|
||||
let receiver = Arc::new(InputReceiver { queue });
|
||||
|
||||
*guard = Some(Arc::downgrade(&receiver));
|
||||
Ok(receiver)
|
||||
}
|
||||
|
||||
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
|
||||
@@ -460,87 +478,103 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct InputReceiver {
|
||||
queue: Option<InputQueue>,
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
|
||||
fn from(receiver: Arc<InputReceiver>) -> Self {
|
||||
Self {
|
||||
receiver,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InputIteratorInner<'a> {
|
||||
// Held to maintain exclusive access to buffered input events
|
||||
receiver: Arc<InputReceiver>,
|
||||
_lifetime: PhantomData<&'a ()>,
|
||||
extern "Rust" {
|
||||
pub fn android_main(app: AndroidApp);
|
||||
}
|
||||
|
||||
impl<'a> InputIteratorInner<'a> {
|
||||
pub(crate) fn next<F>(&self, callback: F) -> bool
|
||||
where
|
||||
F: FnOnce(&input::InputEvent) -> InputStatus,
|
||||
{
|
||||
let Some(queue) = &self.receiver.queue else {
|
||||
log::trace!("no queue available for events");
|
||||
return false;
|
||||
};
|
||||
|
||||
// Note: we basically ignore errors from event() currently. Looking at the source code for
|
||||
// Android's InputQueue, the only error that can be returned here is 'WOULD_BLOCK', which we
|
||||
// want to just treat as meaning the queue is empty.
|
||||
//
|
||||
// ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
|
||||
//
|
||||
if let Ok(Some(ndk_event)) = queue.event() {
|
||||
log::trace!("queue: got event: {ndk_event:?}");
|
||||
|
||||
if let Some(ndk_event) = queue.pre_dispatch(ndk_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))
|
||||
}
|
||||
_ => todo!("NDK added a new type"),
|
||||
};
|
||||
|
||||
// `finish_event` needs to be called for each event otherwise
|
||||
// the app would likely get an ANR
|
||||
let result = std::panic::catch_unwind(AssertUnwindSafe(|| 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())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let handled = match result {
|
||||
Ok(handled) => handled,
|
||||
Err(payload) => {
|
||||
log::error!("Calling `finish_event` after panic in input event handler, to try and avoid being killed via an ANR");
|
||||
queue.finish_event(ndk_event, false);
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!("queue: finishing event");
|
||||
queue.finish_event(ndk_event, handled == InputStatus::Handled);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
log::trace!("queue: no more events");
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
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,113 +1,15 @@
|
||||
use log::{error, Level};
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
fs::File,
|
||||
io::{BufRead as _, BufReader, Result},
|
||||
os::{
|
||||
fd::{FromRawFd as _, RawFd},
|
||||
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 forward_stdio_to_logcat() -> std::thread::JoinHandle<Result<()>> {
|
||||
// XXX: make this stdout/stderr redirection an optional / opt-in feature?...
|
||||
|
||||
let file = unsafe {
|
||||
let mut logpipe: [RawFd; 2] = Default::default();
|
||||
libc::pipe2(logpipe.as_mut_ptr(), libc::O_CLOEXEC);
|
||||
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
|
||||
libc::dup2(logpipe[1], libc::STDERR_FILENO);
|
||||
libc::close(logpipe[1]);
|
||||
|
||||
File::from_raw_fd(logpipe[0])
|
||||
};
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("stdio-to-logcat".to_string())
|
||||
.spawn(move || -> Result<()> {
|
||||
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
let len = match reader.read_line(&mut buffer) {
|
||||
Ok(len) => len,
|
||||
Err(e) => {
|
||||
error!("Logcat forwarder failed to read stdin/stderr: {e:?}");
|
||||
break Err(e);
|
||||
}
|
||||
};
|
||||
if len == 0 {
|
||||
break Ok(());
|
||||
} else if let Ok(msg) = CString::new(buffer.clone()) {
|
||||
android_log(Level::Info, tag, &msg);
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("Failed to start stdout/stderr to logcat forwarder thread")
|
||||
}
|
||||
|
||||
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>
|
||||