Compare commits
157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4ddf059b7 | |||
| 11a5a54483 | |||
| 8de2b6dbaf | |||
| 57b5192366 | |||
| dd66428b14 | |||
| f17b25b673 | |||
| 5c091cd7bb | |||
| 8124bc786d | |||
| c3ed6ba77d | |||
| c1d00b9191 | |||
| 4acfd2d59c | |||
| b042af60f2 | |||
| 9163368955 | |||
| 91cf9d7229 | |||
| ae5553288c | |||
| 0c32e9d8fa | |||
| 43de2770b9 | |||
| 2a05cd2763 | |||
| 0062cfc7a0 | |||
| 0f49d96fa0 | |||
| 2b20da72bd | |||
| f44d837bf7 | |||
| 4ff35807fb | |||
| ae24c96dcc | |||
| 42e0f88287 | |||
| fdcf4ce28d | |||
| 483164c333 | |||
| 279d73889f | |||
| 7577299c84 | |||
| 4e93184d8b | |||
| 31feb32f07 | |||
| 25f4220fef | |||
| 7e8990fd92 | |||
| fe2c50ccc6 | |||
| a20a7e4ee4 | |||
| 0b0e19ed44 | |||
| e686e80112 | |||
| b9e883866e | |||
| 9e8c85c647 | |||
| a97cf1ceae | |||
| 1652ebb229 | |||
| b943f58863 | |||
| 019ad634a2 | |||
| 87cda3c560 | |||
| bde1cb3436 | |||
| 69f3642499 | |||
| c0f3fa6754 | |||
| 42af0cccfa | |||
| 5d7616e30e | |||
| 3755ed7e7a | |||
| 5367c865e3 | |||
| eacddd744a | |||
| 36832feacf | |||
| 88714f0b6a | |||
| 85eb7274f4 | |||
| 49f2b86424 | |||
| 976e9d06af | |||
| ac2e17e977 | |||
| db3ea3386f | |||
| 51d05d48c8 | |||
| fe171bc532 | |||
| 0d299300f4 | |||
| 0a87a84c57 | |||
| 7bd3ba6dde | |||
| 6a0197c28f | |||
| e5b8242ff2 | |||
| c9faa9c44e | |||
| 4b9b8d754b | |||
| 526d39c1f3 | |||
| 4ffa3ac2e1 | |||
| 967882f3d9 | |||
| 35e080baf0 | |||
| 5cb67a2b89 | |||
| 672360c5e6 | |||
| 9fce890219 | |||
| 2deec162b5 | |||
| eeeb80209f | |||
| 6c3583dc24 | |||
| bfd8bfd04c | |||
| af341897a2 | |||
| a84722ff23 | |||
| d9af67008a | |||
| c2f467c174 | |||
| e14d2c1deb | |||
| 100d5bc1d4 | |||
| 98aef99419 | |||
| a7114c807f | |||
| a7dc90d9bb | |||
| 6af4d61227 | |||
| 6e036c99e4 | |||
| 2a917ca5c4 | |||
| add58dbb2e | |||
| d16cb79350 | |||
| b590ec5484 | |||
| 74d9669854 | |||
| a92237fab4 | |||
| 969ba5adf9 | |||
| ce4413b2c6 | |||
| a291e378ee | |||
| 2ecaab9f15 | |||
| 3d5e479a4e | |||
| 219a14bda1 | |||
| 733fabffd3 | |||
| f2132c4dab | |||
| 9930b9bf90 | |||
| 0eefd623ed | |||
| 83cdb56e24 | |||
| 942053d88e | |||
| 865cc6a780 | |||
| 4f6d7d68de | |||
| 7ea440d6c1 | |||
| 75e9e8672d | |||
| 47a073f702 | |||
| 499d09595b | |||
| 23a8570d48 | |||
| c9f57a734f | |||
| e91176cb08 | |||
| 2a61f84c70 | |||
| 2654c9659b | |||
| 35fe600235 | |||
| 242285b205 | |||
| 379f064170 | |||
| e2f69421a0 | |||
| 1b3334178b | |||
| 535994f4a2 | |||
| 6b3307410e | |||
| b4cf0eeabf | |||
| af331e3bff | |||
| 6f72dde55d | |||
| d0f10a0dd9 | |||
| 3464ba20bc | |||
| 1abb02c820 | |||
| c0a9e20c5a | |||
| 66cfc68dac | |||
| ed2dc53ee4 | |||
| 74f510a99a | |||
| 741e633ea8 | |||
| 41f30c39ad | |||
| 96497f9da9 | |||
| c22a5453df | |||
| 9bb5f9c9cf | |||
| a604c0aa9f | |||
| b09526a4a9 | |||
| cc3983ca21 | |||
| 2a2f27637f | |||
| d2d18154d9 | |||
| 3e3fb84c03 | |||
| 202ab4c1e9 | |||
| d6345abb2a | |||
| c471fdf903 | |||
| 1a8a92b3fb | |||
| bb97af154f | |||
| 8a21219695 | |||
| c10a2fb67a | |||
| ab2606a73d | |||
| a84a7b54cd | |||
| a9e91f4308 |
@@ -2,7 +2,7 @@ name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: "*"
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
@@ -12,18 +12,32 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# See top README for MSRV policy
|
||||
rust_version: [1.64.0, stable]
|
||||
rust-version: [1.85.0, stable]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: hecrj/setup-rust-action@v1
|
||||
# Downgrade all dependencies to their minimum version, both to ensure our
|
||||
# minimum version bounds are correct and buildable, as well as to satisfy
|
||||
# our MSRV check when arbitrary dependencies bump their MSRV beyond our
|
||||
# MSRV in a patch-release.
|
||||
# This implies that downstream consumers can only rely on our MSRV when
|
||||
# downgrading various (transitive) dependencies.
|
||||
- uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust_version }}
|
||||
rust-version: nightly
|
||||
if: ${{ matrix.rust-version != 'stable' }}
|
||||
- name: Downgrade dependencies
|
||||
run: cargo +nightly generate-lockfile -Zminimal-versions
|
||||
if: ${{ matrix.rust-version != 'stable' }}
|
||||
|
||||
- uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
rust-version: ${{ matrix.rust-version }}
|
||||
|
||||
- name: Install Rust targets
|
||||
run: >
|
||||
@@ -34,18 +48,7 @@ jobs:
|
||||
i686-linux-android
|
||||
|
||||
- name: Install cargo-ndk
|
||||
# We're currently sticking with cargo-ndk 2, until we bump our
|
||||
# MSRV to 1.68+
|
||||
run: cargo install cargo-ndk --version "^2"
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
run: cargo +stable install cargo-ndk
|
||||
|
||||
- name: Build game-activity
|
||||
working-directory: android-activity
|
||||
@@ -68,7 +71,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
|
||||
@@ -79,7 +82,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
|
||||
@@ -93,17 +96,20 @@ jobs:
|
||||
run: >
|
||||
cargo ndk -t arm64-v8a doc --no-deps
|
||||
|
||||
- name: Build doctests
|
||||
# All doctests are set to no_run, because they require running in the
|
||||
# context of an Android app.
|
||||
# Only run on stable because cross-compiling doctests is only supported
|
||||
# since Rust 1.89.
|
||||
if: ${{ matrix.rust-version == 'stable' }}
|
||||
run: |
|
||||
cargo test --doc -F native-activity --target aarch64-linux-android
|
||||
cargo ndk -t arm64-v8a -- test --doc -F game-activity
|
||||
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
target
|
||||
/target
|
||||
/Cargo.lock
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"android-activity"
|
||||
]
|
||||
resolver = "2"
|
||||
members = ["android-activity"]
|
||||
|
||||
exclude = [
|
||||
"examples",
|
||||
]
|
||||
exclude = ["examples"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://github.com/rust-mobile/android-activity/actions/workflows/ci.yml)
|
||||
[](https://crates.io/crates/android-activity)
|
||||
[](https://docs.rs/android-activity)
|
||||
[](https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html)
|
||||
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -13,7 +13,7 @@ It's comparable to [`android_native_app_glue.c`][ndk_concepts]
|
||||
for C/C++ applications and is an alternative to the [ndk-glue] crate.
|
||||
|
||||
`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()`
|
||||
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.
|
||||
|
||||
@@ -25,34 +25,49 @@ applications.
|
||||
[`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
|
||||
[`GameActivity`]: https://developer.android.com/games/agdk/game-activity
|
||||
[ndk-glue]: https://crates.io/crates/ndk-glue
|
||||
[agdk]: https://developer.android.com/games/agdk
|
||||
[agdk]: https://developer.android.com/games/agdk/overview
|
||||
|
||||
## Example
|
||||
## Quick Start
|
||||
|
||||
Cargo.toml
|
||||
**Cargo.toml:**
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
android_logger = "0.11"
|
||||
android-activity = { version = "0.4", features = [ "native-activity" ] }
|
||||
android_logger = "0.13"
|
||||
android-activity = { version = "0.6", features = [ "native-activity" ] }
|
||||
|
||||
[lib]
|
||||
crate_type = ["cdylib"]
|
||||
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_
|
||||
_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
|
||||
**lib.rs:**
|
||||
|
||||
```rust
|
||||
use std::sync::OnceLock;
|
||||
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
|
||||
|
||||
#[no_mangle]
|
||||
// - Called on a dedicated Activity main loop thread, spawned after `android_on_create` returns
|
||||
// - May be called multiple times if your Activity is destroyed and recreated.
|
||||
// - Note: this symbol has a "Rust" ABI (default), not "C" ABI.
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_main(app: AndroidApp) {
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
|
||||
// `android_main` is tied to your `Activity` lifecycle, not your application lifecycle
|
||||
// and so it may be called multiple times if your Activity is destroyed and recreated.
|
||||
//
|
||||
// Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state
|
||||
// multiple times.
|
||||
static APP_ONCE: OnceLock<()> = OnceLock::new();
|
||||
APP_ONCE.get_or_init(|| {
|
||||
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| {
|
||||
@@ -62,6 +77,11 @@ fn android_main(app: AndroidApp) {
|
||||
PollEvent::Main(main_event) => {
|
||||
log::info!("Main event: {:?}", main_event);
|
||||
match main_event {
|
||||
// Once you receive a `Destroy` event, your `AndroidApp` will no longer
|
||||
// be associated with any `Activity` and it's methods will effectively be no-ops.
|
||||
//
|
||||
// You should return from `android_main` and if your `Activity` gets recreated then
|
||||
// a new `AndroidApp` will be passed to a new invocation of `android_main`.
|
||||
MainEvent::Destroy => { return; }
|
||||
_ => {}
|
||||
}
|
||||
@@ -85,80 +105,247 @@ cargo apk run
|
||||
adb logcat example:V *:S
|
||||
```
|
||||
|
||||
_Note: although `cargo apk` is convenient for this quick start example, it's
|
||||
generally recommended that you should use a more-standard, Gradle-based build
|
||||
system for your Android application and use something like `cargo ndk` for
|
||||
building your Rust code into a `cdylib` that is then packaged via Gradle._
|
||||
|
||||
## Full Examples
|
||||
|
||||
See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`).
|
||||
See [this collection of
|
||||
examples](https://github.com/rust-mobile/rust-android-examples) (based on both
|
||||
`GameActivity` and `NativeActivity`).
|
||||
|
||||
Each example is a standalone project that may also be a convenient templates for starting a new project.
|
||||
Each example is a standalone Android Studio project that can serve as a
|
||||
convenient template for starting a new project.
|
||||
|
||||
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.
|
||||
For the examples based on middleware frameworks (Winit or Egui) they also
|
||||
aim to demonstrate how it's possible to write portable code that will run on
|
||||
Android and other systems.
|
||||
|
||||
## Should I use NativeActivity or GameActivity?
|
||||
## Optional `android_on_create` entry point
|
||||
|
||||
To learn more about the `NativeActivity` class that's shipped with Android see [here](https://developer.android.com/ndk/guides/concepts#naa).
|
||||
`android-activity` also supports an optional `android_on_create` entry point
|
||||
that gets called from the `Activity.onCreate()` callback before `android_main()`
|
||||
is called.
|
||||
|
||||
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)
|
||||
`android_on_create` is called from the Java main / UI thread before the
|
||||
`android_main` thread is spawned.
|
||||
|
||||
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.
|
||||
Considering that many Android SDK APIs (such as `android.view.View`) must be
|
||||
accessed from the main thread, `android_on_create` can be a good place to do any
|
||||
setup work that needs to be done on the Java main thread.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Switching from ndk-glue to android-activity
|
||||
|
||||
### Winit-based applications
|
||||
|
||||
Firstly; if you have a [Winit](https://crates.io/crates/winit) based application and also have an explicit dependency on `ndk-glue` your application will need to remove its dependency on `ndk-glue` for the 0.28 release of Winit which will be based on android-activity (Since glue crates, due to their nature, can't be compatible with alternative glue crates).
|
||||
|
||||
Winit-based applications can follow the [Android README](https://github.com/rust-windowing/winit#android) guidance for advice on how to switch over. Most Winit-based applications should aim to remove any explicit dependency on a specific glue crate (so not depend directly on `ndk-glue` or `android-activity` and instead rely on Winit to pull in the right glue crate). The main practical change will then be to add a `#[no_mangle]fn android_main(app: AndroidApp)` entry point.
|
||||
|
||||
See the [Android README](https://github.com/rust-windowing/winit#android) for more details and also see the [Winit-based examples here](https://github.com/rust-mobile/rust-android-examples).
|
||||
|
||||
### Middleware crates (i.e. not applications)
|
||||
|
||||
If you have a crate that would be considered a middleware library (for example using JNI to support access to Bluetooth, or Android's Camera APIs) then the crate should almost certainly remove any dependence on a specific glue crate because this imposes a strict compatibility constraint that means the crate can only be used by applications that use that exact same glue crate version.
|
||||
|
||||
Middleware libraries can instead look at using the [ndk-context](https://crates.io/crates/ndk-context) crate as a means for being able to use JNI without making any assumptions about the applications overall architecture. This way a middleware crate can work with alternative glue crates (including `ndk-glue` and `android-activity`) as well as work with embedded use cases (i.e. non-native applications that may just embed a dynamic library written in Rust to implement certain native functions).
|
||||
|
||||
### Other, non-Winit-based applications
|
||||
|
||||
The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be:
|
||||
|
||||
1. Remove `ndk-glue` from your Cargo.toml
|
||||
2. Add a dependency on `android-activity`, like `android-activity = { version="0.4", features = [ "native-activity" ] }`
|
||||
3. Optionally add a dependency on `android_logger = "0.11.0"`
|
||||
4. Update the `main` entry point to look like this:
|
||||
For example:
|
||||
|
||||
```rust
|
||||
use android_activity::AndroidApp;
|
||||
use std::sync::OnceLock;
|
||||
use jni::{JavaVM, objects::JObject};
|
||||
|
||||
#[no_mangle]
|
||||
fn android_main(app: AndroidApp) {
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_on_create(state: &android_activity::OnCreateState) {
|
||||
|
||||
// `android_on_create` is tied to your `Activity` lifecycle, not your application lifecycle
|
||||
// and so it may be called multiple times if your activity is destroyed and recreated.
|
||||
//
|
||||
// Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state
|
||||
// multiple times.
|
||||
static APP_ONCE: OnceLock<()> = OnceLock::new();
|
||||
APP_ONCE.get_or_init(|| {
|
||||
// Initialize logging...
|
||||
});
|
||||
let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) };
|
||||
let activity = state.activity_as_ptr() as jni::sys::jobject;
|
||||
// Do some other setup work on the Java main thread before `android_main` starts running
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
_(Note: there is also an `AndroidApp::run_on_java_main_thread()` method that
|
||||
gives another way to run code on the Java main thread for some use cases)_
|
||||
|
||||
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)
|
||||
## Should I use NativeActivity or GameActivity?
|
||||
|
||||
### Design Summary / Motivation behind android-activity
|
||||
To learn more about the `NativeActivity` class that's shipped with Android see
|
||||
[here](https://developer.android.com/ndk/guides/concepts#naa).
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
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`.
|
||||
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, but
|
||||
GameActivity is likely to be the better longer-term choice, due to being based
|
||||
on `AppCompatActivity` and having built in support for input methods (such as
|
||||
onscreen keyboards).
|
||||
|
||||
### NativeActivity
|
||||
|
||||
- Good for: Simple apps, quick prototyping, limited text input support
|
||||
- Setup: Just add the feature flag
|
||||
- Limitations: No built-in input method support (can only receive physical key
|
||||
events from soft keyboards that typically only allows basic ascii input)
|
||||
|
||||
The unique advantage of the `NativeActivity` class is that it's shipped as part
|
||||
of the Android OS and so you can use it without needing to compile or link any
|
||||
Java or Kotlin code.
|
||||
|
||||
`NativeActivity` is technically the only way to build a native Android
|
||||
application purely in Rust without any Java or Kotlin code at all.
|
||||
|
||||
The most significant limitation of `NativeActivity` is that it doesn't have
|
||||
built-in support for input methods (such as onscreen keyboards) and so it's
|
||||
often not a good choice for applications that need to support text input.
|
||||
|
||||
Since some soft keyboards will deliver physical key events for basic ascii input
|
||||
then `NativeActivity` can enable basic text input for prototyping but this is
|
||||
unlikely to be sufficient for production applications.
|
||||
|
||||
For advanced use cases, it would be possible to provide custom `InputConnection`
|
||||
support in conjunction with `NativeActivity` but this isn't something that
|
||||
`android-activity` provides out of the box currently.
|
||||
|
||||
### GameActivity
|
||||
|
||||
- Good for: Apps needing text input, modern AndroidX features
|
||||
- Setup requirements:
|
||||
- Add gradle dependency: `androidx.games:games-activity:4.4.0`
|
||||
- Enable the `game-activity` feature in Cargo.toml
|
||||
- **Important**: Do NOT enable prefab support [details here](#don't-compile-and-link-the-upstream-gameactivity-prefab-c-glue-layer)
|
||||
- Provides: IME support, AppCompatActivity features
|
||||
|
||||
`GameActivity` has built in support for input methods via the `GameTextInput`
|
||||
library and so is a better choice for applications that need to support text
|
||||
input.
|
||||
|
||||
`GameActivity` allows you to update the `ImeOptions` and actions associated with
|
||||
the soft keyboard as well as receive IME span updates for tracking the user's
|
||||
text input state.
|
||||
|
||||
`GameActivity` is based on the [`AppCompatActivity`] class, which is a standard
|
||||
Jetpack / AndroidX class that offers a lot of built-in functionality to help
|
||||
with compatibility across different Android versions and devices.
|
||||
|
||||
### Game Activity Library Version
|
||||
|
||||
`android-activity` currently supports the [`GameActivity` 4.4.0 Jetpack
|
||||
library](https://developer.android.com/jetpack/androidx/releases/games) and is
|
||||
backwards compatible with the previous `4.0.0` stable release. We can't
|
||||
guarantee that the next 4.x stable release will be compatible, but it's fairly
|
||||
likely that it will be.
|
||||
|
||||
Your Android package should depend on `androidx.games:games-activity:4.4.0` from
|
||||
Google's Maven repository.
|
||||
|
||||
Read the upstream [GameActivity getting
|
||||
started](https://developer.android.com/games/agdk/game-activity/get-started)
|
||||
guide for more details on how to add the GameActivity library to your project.
|
||||
|
||||
#### Don't compile and link the upstream GameActivity 'prefab' (C++ glue) layer
|
||||
|
||||
**Important**: Do _not_ follow upstream instructions to enable native prefab
|
||||
support for `GameActivity` that will compile and link the upstream C++ glue
|
||||
layer as part of your build. The upstream glue layer is not directly compatible
|
||||
with `android-activity` which provides its own native glue layer that integrates
|
||||
with Rust.
|
||||
|
||||
I.e. you do _not_ need to enable prefabs via your `build.gradle` file:
|
||||
|
||||
```gradle
|
||||
buildFeatures {
|
||||
prefab true
|
||||
}
|
||||
```
|
||||
|
||||
and do _not_ add a snippet like this to your `CMakeLists.txt` file:
|
||||
|
||||
```cmake
|
||||
find_package(game-activity REQUIRED CONFIG)
|
||||
target_link_libraries(${PROJECT_NAME} PUBLIC log android
|
||||
game-activity::game-activity_static)
|
||||
```
|
||||
|
||||
### Planning to Implement an Activity Subclass
|
||||
|
||||
It's not possible to subclass an Activity from Rust / JNI code alone.
|
||||
|
||||
Keep in mind that Android's design directs many events via the `Activity` class
|
||||
which can only be processed by overloading some associated `Activity` method, so
|
||||
if you want to handle those events then you will need to implement an `Activity`
|
||||
subclass and overload the relevant methods.
|
||||
|
||||
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.
|
||||
|
||||
_At the end of the day, Android's application programming model is fundamentally
|
||||
based around a Java VM running Java/Kotlin code that can optionally call into
|
||||
native code (not the other way around)._
|
||||
|
||||
## Design Summary / Motivation behind android-activity
|
||||
|
||||
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:
|
||||
|
||||
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 its [`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`.
|
||||
|
||||
It's possible to write an application with `android-activity` that can
|
||||
gracefully handle repeated create -> run -> destroy cycles of the `Activity`
|
||||
due to its avoidance of global state. Theoretically you could even run
|
||||
multiple `Activity` instances at the same (though since `NativeActivity` and
|
||||
`GameActivity` were designed for fullscreen games, that only need a single
|
||||
Activity, this is not a common use case).
|
||||
|
||||
[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input
|
||||
[`AppCompatActivity`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
|
||||
|
||||
## MSRV
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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_.
|
||||
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_.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Game Activity Library Versioning Policy
|
||||
|
||||
Any single release of `android-activity` will support a specific version of the
|
||||
Game Activity Jetpack / AndroidX library (documented above).
|
||||
|
||||
The required version of the Game Activity library does not form part of our Rust
|
||||
semver contract, since it doesn't affect the public Rust API of
|
||||
`android-activity`.
|
||||
|
||||
This means that a new patch release of `android-activity` may update the
|
||||
required version of `GameActivity`, which may require users to update how they
|
||||
package their application.
|
||||
|
||||
This is similar to how MSRV updates work, where new toolchain requirements can
|
||||
affect how you build your application but that change is orthogonal to the
|
||||
public API of the crate.
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
|
||||
# Added by cargo
|
||||
#
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
||||
#Cargo.lock
|
||||
@@ -1,41 +1,320 @@
|
||||
<!-- 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.1] - 2026-03-30
|
||||
|
||||
## [0.4.2] - 2022-02-16
|
||||
### Added
|
||||
|
||||
- input: `TextInputAction` enum representing action button types on soft keyboards. ([#216](https://github.com/rust-mobile/android-activity/pull/216))
|
||||
- input: `InputEvent::TextAction` event for handling action button presses from soft keyboards. ([#216](https://github.com/rust-mobile/android-activity/pull/216))
|
||||
- The `ndk` and `ndk-sys` crates are now re-exported under `android_activity::ndk` and `android_activity::ndk_sys` ([#194](https://github.com/rust-mobile/android-activity/pull/194))
|
||||
- `AndroidApp::java_main_looper()` gives access to the `ALooper` for the Java main / UI thread ([#198](https://github.com/rust-mobile/android-activity/pull/198))
|
||||
- `AndroidApp::run_on_java_main_thread()` can be used to run boxed closures on the Java main / UI thread ([#232](https://github.com/rust-mobile/android-activity/pull/232))
|
||||
- Support for an optional `android_on_create` entry point that gets called from the `Activity.onCreate()` callback before `android_main()` is called, allowing for doing some setup work on the Java main / UI thread before the `android_main` Rust code starts running.
|
||||
|
||||
For example:
|
||||
|
||||
```rust
|
||||
use std::sync::OnceLock;
|
||||
use android_activity::OnCreateState;
|
||||
use jni::{JavaVM, refs::Global, objects::JObject};
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_on_create(state: &OnCreateState) {
|
||||
static APP_ONCE: OnceLock<()> = OnceLock::new();
|
||||
APP_ONCE.get_or_init(|| {
|
||||
// Initialize logging...
|
||||
//
|
||||
// Remember, `android_on_create` may be called multiple times but some
|
||||
// logger crates will panic if initialized multiple times.
|
||||
});
|
||||
let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) };
|
||||
let activity = state.activity_as_ptr() as jni::sys::jobject;
|
||||
// Although the thread is implicitly already attached (we are inside an onCreate native method)
|
||||
// using `vm.attach_current_thread` here will use the existing attachment, give us an `&Env`
|
||||
// reference and also catch Java exceptions.
|
||||
if let Err(err) = vm.attach_current_thread(|env| -> jni::errors::Result<()> {
|
||||
// SAFETY:
|
||||
// - The `Activity` reference / pointer is at least valid until we return
|
||||
// - By creating a `Cast` we ensure we can't accidentally delete the reference
|
||||
let activity = unsafe { env.as_cast_raw::<JObject>(&activity)? };
|
||||
|
||||
// Do something with the activity on the Java main thread...
|
||||
Ok(())
|
||||
}) {
|
||||
eprintln!("Failed to interact with Android SDK on Java main thread: {err:?}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Support for `MotionEvent` history, providing higher fidelity input data for things like stylus input (`native-activity` + `game-activity` backends). ([#218](https://github.com/rust-mobile/android-activity/pull/218))
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- rust-version bumped to 1.85.0 ([#193](https://github.com/rust-mobile/android-activity/pull/193), [#219](https://github.com/rust-mobile/android-activity/pull/219))
|
||||
- GameActivity updated to 4.4.0 ([#191](https://github.com/rust-mobile/android-activity/pull/191), [#240](https://github.com/rust-mobile/android-activity/pull/240))
|
||||
- `ndk-context` is initialized with an `Application` context instead of an `Activity` context ([#229](https://github.com/rust-mobile/android-activity/pull/229))
|
||||
|
||||
|
||||
#### GameActivity 4.4.0 Update
|
||||
|
||||
**Important:** This release is no longer compatible with GameActivity 2.0.2
|
||||
|
||||
**Android Packaging:** Your Android application must be packaged with the corresponding androidX, GameActivity 4.x.x library from Google.
|
||||
|
||||
This release has been tested with the [`androidx.games:games-activity:4.4.0` stable
|
||||
release](https://developer.android.com/jetpack/androidx/releases/games#games-activity-4.4.0), and is backwards
|
||||
compatible with the 4.0.0 stable release.
|
||||
|
||||
If you use Gradle to build your Android application, you can depend on the 4.4.0 release of the GameActivity library via:
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
|
||||
// To use the Games Activity library
|
||||
implementation "androidx.games:games-activity:4.4.0"
|
||||
// Note: don't include game-text-input separately, since it's integrated into game-activity
|
||||
}
|
||||
```
|
||||
|
||||
Note: there is no guarantee that later 4.x.x releases of GameActivity will be compatible with this release of
|
||||
`android-activity`, so please refer to the `android-activity` release notes for any future updates regarding
|
||||
GameActivity compatibility.
|
||||
|
||||
#### Initializing `ndk-context` with an Application Context
|
||||
|
||||
`ndk-context` is a separate, framework-independent crate that provides a way for library crates to access a Java VM pointer and an `android.content.Context` JNI reference without needing to depend on `android-activity` directly.
|
||||
|
||||
`ndk-context` may be initialized by various framework crates, including `android-activity`, on behalf of library crates.
|
||||
|
||||
Historically `android-activity` has initialized `ndk-context` with an `Activity` context since that was the simplest choice considering that the entrypoint for `android-activity` comes from an `Activity` `onCreate` callback.
|
||||
|
||||
However, in retrospect it was realized that this was a short-sighted mistake when considering that:
|
||||
1. `ndk-context` only provides a single, global context reference for the entire application that can't be updated
|
||||
2. An Android application can have multiple `Activity` instances over its lifetime (and at times could have no `Activity` instances at all, e.g. if the app is running a background `Service`)
|
||||
3. Whatever is put into `ndk-context` needs to leak a corresponding global reference to ensure it remains valid to access safely. This is inappropriate for an `Activity` reference since it can be destroyed and recreated multiple times over the lifetime of the application.
|
||||
|
||||
A far better choice, that aligns with the global nature of the `ndk-context` API is to initialize it with an `Application` context which is valid for the entire lifetime of the application.
|
||||
|
||||
**Note:** Although the `ndk-context` API only promises to provide an `android.content.Context` _and_ specifically warns that user's should not assume the context is an `Activity`, there is still some risk that some users of `ndk-context` could be affected by the change made in [#229](https://github.com/rust-mobile/android-activity/pull/229).
|
||||
|
||||
For example, until recently the `webbrowser` crate (for opening URLs) was assuming it could access an `Activity` context via `ndk-context`. In preparation for making this change, `webbrowser` was updated to ensure it is agnostic to the type of context provided by `ndk-context`, see: <https://github.com/amodm/webbrowser-rs/pull/111>
|
||||
|
||||
Other notable library crates that support Android (such as `app_dirs2`) are expected to be unaffected by this, since they already operate in terms of the `android.content.Context` API, not the `Activity` API.
|
||||
|
||||
_**Note:**: if some crate really needs an `Activity` reference then they should ideally be able to get one via the
|
||||
`AndroidApp::activity_as_ptr()` API. They may need to add some Android-specific initialization API, similar to how Winit has a dedicated `.with_android_app()` API. An Android-specific init API that could accept a JNI reference to an `Activity` could avoid needing to specifically depend on `android-activity`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- *Safety* `AndroidApp::asset_manager()` returns an `AssetManager` that has a safe `'static` lifetime that's not invalidated when `android_main()` returns ([#233](https://github.com/rust-mobile/android-activity/pull/233))
|
||||
- *Safety* The `native-activity` backend clears its `ANativeActivity` ptr after `onDestroy` and `AndroidApp` remains safe to access after `android_main()` returns ([#234](https://github.com/rust-mobile/android-activity/pull/234))
|
||||
- *Safety* `AndroidApp::activity_as_ptr()` returns a pointer to a global reference that remains valid until `AndroidApp` is dropped, instead of the `ANativeActivity`'s `clazz` pointer which is only guaranteed to be valid until `onDestroy` returns (`native-activity` backend) ([#234](https://github.com/rust-mobile/android-activity/pull/234))
|
||||
- *Safety* The `game-activity` backend clears its `android_app` ptr after `onDestroy` and `AndroidApp` remains safe to access after `android_main()` returns ([#236](https://github.com/rust-mobile/android-activity/pull/236))
|
||||
- Support for `AndroidApp::show/hide_soft_input()` APIs in the `native-activity` backend ([#178](https://github.com/rust-mobile/android-activity/pull/178))
|
||||
|
||||
Overall, some effort was made to ensure that `android-activity` can gracefully and safely handle cases where the `Activity` gets repeatedly created, destroyed and recreated (e.g due to configuration changes). Theoretically it should even be possible to run multiple `Activity` instances (although that's not really something that `NativeActivity` or `GameActivity` are designed for).
|
||||
|
||||
## [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 ([#138](https://github.com/rust-mobile/android-activity/pull/138))
|
||||
|
||||
### 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] - 2022-02-16
|
||||
## [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))
|
||||
- Most of the examples were moved to <https://github.com/rust-mobile/rust-android-examples> ([#50](https://github.com/rust-mobile/android-activity/pull/50))
|
||||
|
||||
## [0.4] - 2022-11-10
|
||||
## [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] - 2022-09-15
|
||||
## [0.3.0] - 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] - 2022-08-25
|
||||
## [0.2.0] - 2022-08-25
|
||||
### Added
|
||||
- Emit an `InputAvailable` event for new input with `NativeActivity` and `GameActivity`
|
||||
enabling gui apps that don't render continuously
|
||||
@@ -52,6 +331,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
- Documentation fixes
|
||||
|
||||
## [0.1] - 2022-07-04
|
||||
## [0.1.0] - 2022-07-04
|
||||
### Added
|
||||
- Initial release
|
||||
- Initial release
|
||||
|
||||
[unreleased]: https://github.com/rust-mobile/android-activity/compare/v0.6.1...HEAD
|
||||
[0.6.1]: https://github.com/rust-mobile/android-activity/compare/v0.6.0...v0.6.1
|
||||
[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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "android-activity"
|
||||
version = "0.4.2"
|
||||
version = "0.6.1"
|
||||
edition = "2021"
|
||||
keywords = ["android", "ndk"]
|
||||
readme = "../README.md"
|
||||
@@ -9,7 +9,9 @@ repository = "https://github.com/rust-mobile/android-activity"
|
||||
documentation = "https://docs.rs/android-activity"
|
||||
description = "Glue for building Rust applications on Android with NativeActivity or GameActivity"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.64"
|
||||
include = ["/build.rs", "/android-games-sdk", "/LICENSE*", "/src"]
|
||||
|
||||
rust-version = "1.85.0"
|
||||
|
||||
[features]
|
||||
# Note: we don't enable any backend by default since features
|
||||
@@ -19,22 +21,28 @@ rust-version = "1.64"
|
||||
# In general it's only the final application crate that needs
|
||||
# to decide on a backend.
|
||||
default = []
|
||||
game-activity = []
|
||||
game-activity = ["simd_cesu8"]
|
||||
native-activity = []
|
||||
api-level-30 = ["ndk/api-level-30"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
jni-sys = "0.3"
|
||||
ndk = "0.7"
|
||||
ndk-sys = "0.4"
|
||||
ndk-context = "0.1"
|
||||
simd_cesu8 = { version = "1.0.1", optional = true }
|
||||
jni = "0.22.4"
|
||||
ndk-sys = "0.6.0"
|
||||
ndk = { version = "0.9.0", default-features = false }
|
||||
ndk-context = "0.1.1"
|
||||
android-properties = "0.2"
|
||||
num_enum = "0.6"
|
||||
bitflags = "1.3"
|
||||
libc = "0.2"
|
||||
num_enum = "0.7"
|
||||
bitflags = "2.0"
|
||||
libc = "0.2.139"
|
||||
thiserror = "2"
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1.0", features = ["parallel"] }
|
||||
cc = { version = "1.0.42", features = ["parallel"] }
|
||||
|
||||
[dev-dependencies]
|
||||
jni = "0.22.4"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = [
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
The third-party glue code, under the native-activity-csrc/ and game-activity-csrc/ directories
|
||||
is covered by the Apache 2.0 license only:
|
||||
# License
|
||||
|
||||
Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
## GameActivity
|
||||
|
||||
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 (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 (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 (LICENSE-MIT or <http://opensource.org/licenses/MIT>)
|
||||
- Apache License, Version 2.0 (LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
|
||||
at your option.
|
||||
at your option.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# android-games-sdk
|
||||
|
||||
This is an imported copy of the native "prefab" source for `GameActivity` and
|
||||
`GameTextInput`, from our fork of Google's
|
||||
[android-games-sdk](https://github.com/rust-mobile/android-games-sdk).
|
||||
|
||||
We use an external fork to track our integration patches on top of the Android
|
||||
Game Development Kit (AGDK) in a way that it is easier to update to new upstream
|
||||
versions. It also makes it easier to try and upstream changes when we fix bugs.
|
||||
|
||||
## Updating to new agdk version checklist
|
||||
|
||||
This is a basic checklist for things that need to be done when updating to a new
|
||||
agdk version:
|
||||
|
||||
- [ ] Create a new integration branch based on our last integrated branch and
|
||||
rebase that on the latest *release* branch from Google:
|
||||
|
||||
```bash
|
||||
git clone git@github.com:rust-mobile/android-games-sdk.git
|
||||
cd android-games-sdk
|
||||
git remote add google https://android.googlesource.com/platform/frameworks/opt/gamesdk
|
||||
git fetch google
|
||||
git checkout -b android-activity-5.0.0 origin/android-activity-4.0.0
|
||||
git rebase --onto google/android-games-sdk-game-activity-release <base>
|
||||
# (where <base> is the upstream commit ID below our stack of integration patches)
|
||||
```
|
||||
|
||||
- [ ] Set the `ANDROID_GAMES_SDK` environment variable so you can build
|
||||
android-activity against your external games-sdk branch while updating.
|
||||
- [ ] Re-generate the `GameActivity` FFI bindings with `./generate-bindings.sh`
|
||||
(this can be done with `ANDROID_GAMES_SDK` set in your environment and also
|
||||
repeated after importing)
|
||||
- [ ] Update [build.rs](../build.rs) with any new includes and src files
|
||||
- [ ] Update the `src/game-activity` backend as needed
|
||||
- [ ] Push a new `android-games-sdk` branch like `android-activity-5.0.0` that
|
||||
can be referenced when importing a copy into `android-activity`
|
||||
- [ ] Review and run `./import-games-sdk.sh` when ready to copy external AGDK
|
||||
code into this repo
|
||||
- [ ] Clearly reference the branch name and commit hash from the
|
||||
`android-games-sdk` repo in the `android-activity` commit that imports new
|
||||
games-sdk source.
|
||||
- [ ] Update CHANGELOG.md as required
|
||||
@@ -31,17 +31,44 @@
|
||||
#include <android/input.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/rect.h>
|
||||
#include <common/gamesdk_common.h>
|
||||
#include <game-activity/GameActivityEvents.h>
|
||||
#include <game-text-input/gametextinput.h>
|
||||
#include <jni.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "game-text-input/gametextinput.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define GAMEACTIVITY_VERSION_REVISION a0e943c3a84fd7f344c3d36cdf4e88fd595f81b8
|
||||
#define GAMEACTIVITY_MAJOR_VERSION 4
|
||||
#define GAMEACTIVITY_MINOR_VERSION 4
|
||||
#define GAMEACTIVITY_BUGFIX_VERSION 0
|
||||
#define GAMEACTIVITY_PACKED_VERSION \
|
||||
ANDROID_GAMESDK_PACKED_VERSION(GAMEACTIVITY_MAJOR_VERSION, GAMEACTIVITY_MINOR_VERSION, \
|
||||
GAMEACTIVITY_BUGFIX_VERSION)
|
||||
|
||||
/**
|
||||
* The type of a component for which to retrieve insets. See
|
||||
* https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type
|
||||
*/
|
||||
typedef enum GameCommonInsetsType : uint8_t {
|
||||
GAMECOMMON_INSETS_TYPE_CAPTION_BAR = 0,
|
||||
GAMECOMMON_INSETS_TYPE_DISPLAY_CUTOUT,
|
||||
GAMECOMMON_INSETS_TYPE_IME,
|
||||
GAMECOMMON_INSETS_TYPE_MANDATORY_SYSTEM_GESTURES,
|
||||
GAMECOMMON_INSETS_TYPE_NAVIGATION_BARS,
|
||||
GAMECOMMON_INSETS_TYPE_STATUS_BARS,
|
||||
GAMECOMMON_INSETS_TYPE_SYSTEM_BARS,
|
||||
GAMECOMMON_INSETS_TYPE_SYSTEM_GESTURES,
|
||||
GAMECOMMON_INSETS_TYPE_TAPABLE_ELEMENT,
|
||||
GAMECOMMON_INSETS_TYPE_WATERFALL,
|
||||
GAMECOMMON_INSETS_TYPE_COUNT
|
||||
} GameCommonInsetsType;
|
||||
|
||||
/**
|
||||
* {@link GameActivityCallbacks}
|
||||
*/
|
||||
@@ -115,205 +142,11 @@ 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;
|
||||
int32_t toolType;
|
||||
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.
|
||||
*/
|
||||
typedef void (*SaveInstanceStateRecallback)(const char* bytes, int len,
|
||||
void* context);
|
||||
typedef void (*SaveInstanceStateRecallback)(const char* bytes, int len, void* context);
|
||||
|
||||
/**
|
||||
* These are the callbacks the framework makes into a native application.
|
||||
@@ -342,8 +175,7 @@ typedef struct GameActivityCallbacks {
|
||||
* that the saved state will be persisted, so it can not contain any active
|
||||
* entities (pointers to memory, file descriptors, etc).
|
||||
*/
|
||||
void (*onSaveInstanceState)(GameActivity* activity,
|
||||
SaveInstanceStateRecallback recallback,
|
||||
void (*onSaveInstanceState)(GameActivity* activity, SaveInstanceStateRecallback recallback,
|
||||
void* context);
|
||||
|
||||
/**
|
||||
@@ -374,16 +206,15 @@ typedef struct GameActivityCallbacks {
|
||||
* The drawing window for this native activity has been created. You
|
||||
* can use the given native window object to start drawing.
|
||||
*/
|
||||
void (*onNativeWindowCreated)(GameActivity* activity,
|
||||
ANativeWindow* window);
|
||||
void (*onNativeWindowCreated)(GameActivity* activity, ANativeWindow* window);
|
||||
|
||||
/**
|
||||
* The drawing window for this native activity has been resized. You should
|
||||
* retrieve the new size from the window and ensure that your rendering in
|
||||
* it now matches.
|
||||
*/
|
||||
void (*onNativeWindowResized)(GameActivity* activity, ANativeWindow* window,
|
||||
int32_t newWidth, int32_t newHeight);
|
||||
void (*onNativeWindowResized)(GameActivity* activity, ANativeWindow* window, int32_t newWidth,
|
||||
int32_t newHeight);
|
||||
|
||||
/**
|
||||
* The drawing window for this native activity needs to be redrawn. To
|
||||
@@ -391,8 +222,7 @@ typedef struct GameActivityCallbacks {
|
||||
* rotation), applications should not return from this function until they
|
||||
* have finished drawing their window in its current state.
|
||||
*/
|
||||
void (*onNativeWindowRedrawNeeded)(GameActivity* activity,
|
||||
ANativeWindow* window);
|
||||
void (*onNativeWindowRedrawNeeded)(GameActivity* activity, ANativeWindow* window);
|
||||
|
||||
/**
|
||||
* The drawing window for this native activity is going to be destroyed.
|
||||
@@ -402,11 +232,10 @@ typedef struct GameActivityCallbacks {
|
||||
* properly synchronize with the other thread to stop its drawing before
|
||||
* returning from here.
|
||||
*/
|
||||
void (*onNativeWindowDestroyed)(GameActivity* activity,
|
||||
ANativeWindow* window);
|
||||
void (*onNativeWindowDestroyed)(GameActivity* activity, ANativeWindow* window);
|
||||
|
||||
/**
|
||||
* The current device AConfiguration has changed. The new configuration can
|
||||
* The current device AConfiguration has changed. The new configuration can
|
||||
* be retrieved from assetManager.
|
||||
*/
|
||||
void (*onConfigurationChanged)(GameActivity* activity);
|
||||
@@ -423,18 +252,14 @@ typedef struct GameActivityCallbacks {
|
||||
* SurfaceView. Ownership of `event` is maintained by the library and it is
|
||||
* only valid during the callback.
|
||||
*/
|
||||
bool (*onTouchEvent)(GameActivity* activity,
|
||||
const GameActivityMotionEvent* event,
|
||||
const GameActivityHistoricalPointerAxes* historical,
|
||||
int historicalLen);
|
||||
bool (*onTouchEvent)(GameActivity* activity, const GameActivityMotionEvent* event);
|
||||
|
||||
/**
|
||||
* Callback called for every key down event on the GameActivity SurfaceView.
|
||||
* Ownership of `event` is maintained by the library and it is only valid
|
||||
* during the callback.
|
||||
*/
|
||||
bool (*onKeyDown)(GameActivity* activity,
|
||||
const GameActivityKeyEvent* event);
|
||||
bool (*onKeyDown)(GameActivity* activity, const GameActivityKeyEvent* event);
|
||||
|
||||
/**
|
||||
* Callback called for every key up event on the GameActivity SurfaceView.
|
||||
@@ -448,45 +273,31 @@ typedef struct GameActivityCallbacks {
|
||||
* Ownership of `state` is maintained by the library and it is only valid
|
||||
* during the callback.
|
||||
*/
|
||||
void (*onTextInputEvent)(GameActivity* activity,
|
||||
const GameTextInputState* state);
|
||||
void (*onTextInputEvent)(GameActivity* activity, const GameTextInputState* state);
|
||||
|
||||
/**
|
||||
* Callback called when WindowInsets of the main app window have changed.
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Callback called when the software keyboard is shown or hidden.
|
||||
*/
|
||||
void (*onSoftwareKeyboardVisibilityChanged)(GameActivity* activity, bool visible);
|
||||
|
||||
/**
|
||||
* Callback called when the software keyboard is shown or hidden.
|
||||
*/
|
||||
bool (*onEditorAction)(GameActivity* activity, int action);
|
||||
} 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
|
||||
@@ -518,7 +329,7 @@ void GameActivity_finish(GameActivity* activity);
|
||||
* Flags for GameActivity_setWindowFlags,
|
||||
* as per the Java API at android.view.WindowManager.LayoutParams.
|
||||
*/
|
||||
enum GameActivitySetWindowFlags {
|
||||
enum GameActivitySetWindowFlags : uint32_t {
|
||||
/**
|
||||
* 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
|
||||
@@ -711,14 +522,13 @@ enum GameActivitySetWindowFlags {
|
||||
* *any* thread; it will send a message to the main thread of the process
|
||||
* where the Java finish call will take place.
|
||||
*/
|
||||
void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags,
|
||||
uint32_t removeFlags);
|
||||
void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags, uint32_t removeFlags);
|
||||
|
||||
/**
|
||||
* Flags for GameActivity_showSoftInput; see the Java InputMethodManager
|
||||
* API for documentation.
|
||||
*/
|
||||
enum GameActivityShowSoftInputFlags {
|
||||
enum GameActivityShowSoftInputFlags : uint8_t {
|
||||
/**
|
||||
* Implicit request to show the input window, not as the result
|
||||
* of a direct request by the user.
|
||||
@@ -741,22 +551,27 @@ enum GameActivityShowSoftInputFlags {
|
||||
*/
|
||||
void GameActivity_showSoftInput(GameActivity* activity, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Restarts the input method. Calls InputMethodManager.restartInput().
|
||||
* 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 call will take place.
|
||||
*/
|
||||
void GameActivity_restartInput(GameActivity* activity);
|
||||
|
||||
/**
|
||||
* Set the text entry state (see documentation of the GameTextInputState struct
|
||||
* in the Game Text Input library reference).
|
||||
*
|
||||
* Ownership of the state is maintained by the caller.
|
||||
*/
|
||||
void GameActivity_setTextInputState(GameActivity* activity,
|
||||
const GameTextInputState* state);
|
||||
void GameActivity_setTextInputState(GameActivity* activity, const GameTextInputState* state);
|
||||
|
||||
/**
|
||||
* Get the last-received text entry state (see documentation of the
|
||||
* GameTextInputState struct in the Game Text Input library reference).
|
||||
*
|
||||
*/
|
||||
void GameActivity_getTextInputState(GameActivity* activity,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void GameActivity_getTextInputState(GameActivity* activity, GameTextInputGetStateCallback callback,
|
||||
void* context);
|
||||
|
||||
/**
|
||||
@@ -768,7 +583,7 @@ GameTextInput* GameActivity_getTextInput(const GameActivity* activity);
|
||||
* Flags for GameActivity_hideSoftInput; see the Java InputMethodManager
|
||||
* API for documentation.
|
||||
*/
|
||||
enum GameActivityHideSoftInputFlags {
|
||||
enum GameActivityHideSoftInputFlags : uint16_t {
|
||||
/**
|
||||
* The soft input window should only be hidden if it was not
|
||||
* explicitly shown by the user.
|
||||
@@ -795,8 +610,12 @@ void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags);
|
||||
* for more details.
|
||||
* You can use these insets to influence what you show on the screen.
|
||||
*/
|
||||
void GameActivity_getWindowInsets(GameActivity* activity,
|
||||
GameCommonInsetsType type, ARect* insets);
|
||||
void GameActivity_getWindowInsets(GameActivity* activity, GameCommonInsetsType type, ARect* insets);
|
||||
|
||||
/**
|
||||
* Tells whether the software keyboard is visible or not.
|
||||
*/
|
||||
bool GameActivity_isSoftwareKeyboardVisible(GameActivity* activity);
|
||||
|
||||
/**
|
||||
* Set options on how the IME behaves when it is requested for text input.
|
||||
@@ -804,12 +623,72 @@ void GameActivity_getWindowInsets(GameActivity* activity,
|
||||
* https://developer.android.com/reference/android/view/inputmethod/EditorInfo
|
||||
* for the meaning of inputType, actionId and imeOptions.
|
||||
*
|
||||
* Note that this function will attach the current thread to the JVM if it is
|
||||
* not already attached, so the caller must detach the thread from the JVM
|
||||
* before the thread is destroyed using DetachCurrentThread.
|
||||
* <b>Note:</b> currently only TYPE_NULL AND TYPE_CLASS_NUMBER are supported.
|
||||
*/
|
||||
void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType,
|
||||
int actionId, int imeOptions);
|
||||
void GameActivity_setImeEditorInfo(GameActivity* activity, enum GameTextInputType inputType,
|
||||
enum GameTextInputActionType actionId,
|
||||
enum GameTextInputImeOptions 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_getLocalesCount(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);
|
||||
|
||||
/**
|
||||
* The functions below return Java locale information.
|
||||
*
|
||||
* In simple cases there will be just one locale, but it's possible tha
|
||||
* there are more than one locale objects. Users are encouraged to write code
|
||||
* that handles all locales and not just the first one.
|
||||
*
|
||||
* The functions in the block below return string values in the provided buffer.
|
||||
* Return value is zero if there were no errors, otherwise it's non-zero.
|
||||
* If the return value is zero, `dst` will contain a null-terminated string:
|
||||
* strlen(dst) <= dst_size - 1.
|
||||
* If the return value is non-zero, the content of dst is undefined.
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* dst, dst_size: define a receiver buffer. Locale string can be something
|
||||
* short like "EN/EN", but it may be longer. You should be safe with a buffer
|
||||
* size of 256 bytes.
|
||||
*
|
||||
* If the buffer is too small, ENOBUFS is returned. Try allocating a larger
|
||||
* buffer in this case.
|
||||
*
|
||||
* localeIdx must be between 0 and the value of GameActivity_getLocalesCount().
|
||||
* If localeIdx is out of range, EINVAL is returned.
|
||||
*
|
||||
* Refer to Java documentation of locales for more information.
|
||||
*/
|
||||
int GameActivity_getLocaleLanguage(char* dst, size_t dst_size, GameActivity* activity,
|
||||
size_t localeIdx);
|
||||
int GameActivity_getLocaleScript(char* dst, size_t dst_size, GameActivity* activity,
|
||||
size_t localeIdx);
|
||||
int GameActivity_getLocaleCountry(char* dst, size_t dst_size, GameActivity* activity,
|
||||
size_t localeIdx);
|
||||
int GameActivity_getLocaleVariant(char* dst, size_t dst_size, GameActivity* activity,
|
||||
size_t localeIdx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
@@ -817,4 +696,4 @@ void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType,
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_H
|
||||
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_H
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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_
|
||||
@@ -27,30 +27,10 @@
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -120,16 +100,15 @@ struct android_poll_source {
|
||||
* Function to call to perform the standard processing of data from
|
||||
* this source.
|
||||
*/
|
||||
void (*process)(struct android_app* app,
|
||||
struct android_poll_source* source);
|
||||
void (*process)(struct android_app* app, struct android_poll_source* source);
|
||||
};
|
||||
|
||||
struct android_input_buffer {
|
||||
/**
|
||||
* Pointer to a read-only array of pointers to GameActivityMotionEvent.
|
||||
* Pointer to a read-only array of GameActivityMotionEvent.
|
||||
* Only the first motionEventsCount events are valid.
|
||||
*/
|
||||
GameActivityMotionEvent motionEvents[NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS];
|
||||
GameActivityMotionEvent* motionEvents;
|
||||
|
||||
/**
|
||||
* The number of valid motion events in `motionEvents`.
|
||||
@@ -137,36 +116,25 @@ struct android_input_buffer {
|
||||
uint64_t motionEventsCount;
|
||||
|
||||
/**
|
||||
* 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];
|
||||
* The size of the `motionEvents` buffer.
|
||||
*/
|
||||
GameActivityHistoricalPointerAxes historicalAxisSamples[NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES];
|
||||
uint64_t motionEventsBufferSize;
|
||||
|
||||
/**
|
||||
* The number of valid historical samples in `historicalAxisSamples`.
|
||||
*/
|
||||
uint64_t historicalSamplesCount;
|
||||
|
||||
/**
|
||||
* Pointer to a read-only array of pointers to GameActivityKeyEvent.
|
||||
* Pointer to a read-only array of GameActivityKeyEvent.
|
||||
* Only the first keyEventsCount events are valid.
|
||||
*/
|
||||
GameActivityKeyEvent keyEvents[NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS];
|
||||
GameActivityKeyEvent* keyEvents;
|
||||
|
||||
/**
|
||||
* The number of valid "Key" events in `keyEvents`.
|
||||
*/
|
||||
uint64_t keyEventsCount;
|
||||
|
||||
/**
|
||||
* The size of the `keyEvents` buffer.
|
||||
*/
|
||||
uint64_t keyEventsBufferSize;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -233,6 +201,9 @@ struct android_app {
|
||||
/** The ALooper associated with the app's thread. */
|
||||
ALooper* looper;
|
||||
|
||||
/** The ALooper associated with the app's Java main/UI thread. */
|
||||
ALooper* mainLooper;
|
||||
|
||||
/** When non-NULL, this is the window surface that the app can draw in. */
|
||||
ANativeWindow* window;
|
||||
|
||||
@@ -242,6 +213,27 @@ struct android_app {
|
||||
*/
|
||||
ARect contentRect;
|
||||
|
||||
/**
|
||||
* Whether the software keyboard is visible or not.
|
||||
*/
|
||||
bool softwareKeyboardVisible;
|
||||
|
||||
/**
|
||||
* Last editor action. Valid within APP_CMD_SOFTWARE_KB_VIS_CHANGED handler.
|
||||
*
|
||||
* Note: the upstream comment above isn't accurate.
|
||||
* - `APP_CMD_SOFTWARE_KB_VIS_CHANGED` is associated with `softwareKeyboardVisible`
|
||||
* changes, not `editorAction`.
|
||||
* - `APP_CMD_EDITOR_ACTION` is associated with this state but unlike for
|
||||
* `window` state there's no synchonization that blocks the Java main
|
||||
* thread, so we can't say that this is only valid within the `APP_CMD_` handler.
|
||||
*/
|
||||
int editorAction;
|
||||
/**
|
||||
* true when editorAction has been set
|
||||
*/
|
||||
bool pendingEditorAction;
|
||||
|
||||
/**
|
||||
* Current state of the app's activity. May be either APP_CMD_START,
|
||||
* APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP.
|
||||
@@ -322,7 +314,7 @@ struct android_app {
|
||||
* Looper ID of commands coming from the app's main thread, an AInputQueue or
|
||||
* user-defined sources.
|
||||
*/
|
||||
enum NativeAppGlueLooperId {
|
||||
enum NativeAppGlueLooperId : int8_t {
|
||||
/**
|
||||
* 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
|
||||
@@ -346,8 +338,11 @@ enum NativeAppGlueLooperId {
|
||||
|
||||
/**
|
||||
* Commands passed from the application's main Java thread to the game's thread.
|
||||
*
|
||||
* Values from 0 to 127 are reserved for this library; values from -128 to -1
|
||||
* can be used for custom user's events.
|
||||
*/
|
||||
enum NativeAppGlueAppCmd {
|
||||
enum NativeAppGlueAppCmd : int8_t {
|
||||
/**
|
||||
* Unused. Reserved for future use when usage of AInputQueue will be
|
||||
* supported.
|
||||
@@ -389,6 +384,11 @@ enum NativeAppGlueAppCmd {
|
||||
*/
|
||||
APP_CMD_CONTENT_RECT_CHANGED,
|
||||
|
||||
/**
|
||||
* Command from main thread: the software keyboard was shown or hidden.
|
||||
*/
|
||||
APP_CMD_SOFTWARE_KB_VIS_CHANGED,
|
||||
|
||||
/**
|
||||
* Command from main thread: the app's activity window has gained
|
||||
* input focus.
|
||||
@@ -452,10 +452,25 @@ enum NativeAppGlueAppCmd {
|
||||
*/
|
||||
APP_CMD_WINDOW_INSETS_CHANGED,
|
||||
|
||||
/**
|
||||
* Command from main thread: an editor action has been triggered.
|
||||
*/
|
||||
// APP_CMD_EDITOR_ACTION,
|
||||
|
||||
/**
|
||||
* Command from main thread: a keyboard event has been received.
|
||||
*/
|
||||
// APP_CMD_KEY_EVENT,
|
||||
|
||||
/**
|
||||
* Command from main thread: a touch event has been received.
|
||||
*/
|
||||
// APP_CMD_TOUCH_EVENT,
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
|
||||
* Call when ALooper_pollOnce() returns LOOPER_ID_MAIN, reading the next
|
||||
* app command message.
|
||||
*/
|
||||
int8_t android_app_read_cmd(struct android_app* android_app);
|
||||
@@ -478,8 +493,7 @@ void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
|
||||
* Call this before processing input events to get the events buffer.
|
||||
* The function returns NULL if there are no events to process.
|
||||
*/
|
||||
struct android_input_buffer* android_app_swap_input_buffers(
|
||||
struct android_app* android_app);
|
||||
struct android_input_buffer* android_app_swap_input_buffers(struct android_app* android_app);
|
||||
|
||||
/**
|
||||
* Clear the array of motion events that were waiting to be handled, and release
|
||||
@@ -512,8 +526,7 @@ extern void _rust_glue_entry(struct android_app* app);
|
||||
*
|
||||
* The default key filter will filter out volume and camera button presses.
|
||||
*/
|
||||
void android_app_set_key_event_filter(struct android_app* app,
|
||||
android_key_event_filter filter);
|
||||
void android_app_set_key_event_filter(struct android_app* app, android_key_event_filter filter);
|
||||
|
||||
/**
|
||||
* Set the filter to use when processing touch and motion events.
|
||||
@@ -527,13 +540,23 @@ void android_app_set_key_event_filter(struct android_app* app,
|
||||
void android_app_set_motion_event_filter(struct android_app* app,
|
||||
android_motion_event_filter filter);
|
||||
|
||||
/**
|
||||
* You can send your custom events using the function below.
|
||||
*
|
||||
* Make sure your custom codes do not overlap with this library's ones.
|
||||
*
|
||||
* Values from 0 to 127 are reserved for this library; values from -128 to -1
|
||||
* can be used for custom user's events.
|
||||
*
|
||||
* The function returns true if the write operation was successful.
|
||||
*/
|
||||
bool android_app_write_cmd(struct android_app* android_app, int8_t cmd);
|
||||
|
||||
/**
|
||||
* Determines if a looper wake up was due to new input becoming available
|
||||
*/
|
||||
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
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"export_libraries": [],
|
||||
"library_name": null,
|
||||
"android": {
|
||||
"export_libraries": ["-landroid", "-llog"],
|
||||
"library_name": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* 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 <game-activity/GameActivityEvents.h>
|
||||
#include <game-activity/GameActivityLog.h>
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "GameActivityEvents_internal.h"
|
||||
#include "system_utils.h"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void initMotionEvents(JNIEnv* env) {
|
||||
int sdkVersion = gamesdk::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");
|
||||
}
|
||||
|
||||
extern "C" void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
|
||||
GameActivityMotionEvent* out_event,
|
||||
int pointerCount, int historySize) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
static void initKeyEvents(JNIEnv* env) {
|
||||
int sdkVersion = gamesdk::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");
|
||||
}
|
||||
|
||||
extern "C" void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject keyEvent,
|
||||
GameActivityKeyEvent* out_event) {
|
||||
*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)
|
||||
};
|
||||
}
|
||||
|
||||
extern "C" void GameActivityEventsInit(JNIEnv* env) {
|
||||
initMotionEvents(env);
|
||||
initKeyEvents(env);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 Internal
|
||||
* These functions are internal details of Game Activity Events.
|
||||
* Please do not rely on anything in this file as this can be changed
|
||||
* without notice.
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file GameActivityEvents_internal.h
|
||||
*/
|
||||
#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_INTERNAL_H
|
||||
#define ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_INTERNAL_H
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** \brief Performs necessary initialization steps for GameActivityEvents.
|
||||
*
|
||||
* User must call this function before calling any other functions of this unit.
|
||||
* If you use GameActivity it will call this function for you.
|
||||
*/
|
||||
void GameActivityEventsInit(JNIEnv* env);
|
||||
|
||||
/**
|
||||
* \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.
|
||||
* Note that we pass as much information from Java Activity as possible
|
||||
* to avoid extra JNI calls.
|
||||
*/
|
||||
void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
|
||||
GameActivityMotionEvent* out_event, int pointerCount,
|
||||
int historySize);
|
||||
|
||||
/**
|
||||
* \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_INTERNAL_H
|
||||
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "android_native_app_glue.h"
|
||||
#include "game-activity/native_app_glue/android_native_app_glue.h"
|
||||
|
||||
#include <android/log.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <jni.h>
|
||||
#include <stdlib.h>
|
||||
@@ -24,26 +25,25 @@
|
||||
#include <time.h>
|
||||
#include <unistd.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__))
|
||||
#define LOGW(...) \
|
||||
((void)__android_log_print(ANDROID_LOG_WARN, "threaded_app", __VA_ARGS__))
|
||||
#define LOGW_ONCE(...) \
|
||||
#define NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE 16
|
||||
#define NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE 4
|
||||
#define NATIVE_APP_GLUE_CMD_WAIT_TIMEOUT_SECONDS 2
|
||||
|
||||
#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__))
|
||||
#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "threaded_app", __VA_ARGS__))
|
||||
#define LOGW_ONCE(...) \
|
||||
do { \
|
||||
static bool alogw_once##__FILE__##__LINE__##__ = true; \
|
||||
if (alogw_once##__FILE__##__LINE__##__) { \
|
||||
alogw_once##__FILE__##__LINE__##__ = false; \
|
||||
LOGW(__VA_ARGS__); \
|
||||
LOGW(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* 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__))
|
||||
#define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__))
|
||||
#else
|
||||
#define LOGV(...) ((void)0)
|
||||
#endif
|
||||
@@ -73,25 +73,23 @@ static void print_cur_config(struct android_app* android_app) {
|
||||
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));
|
||||
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_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
|
||||
@@ -128,8 +126,8 @@ void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
|
||||
|
||||
case APP_CMD_CONFIG_CHANGED:
|
||||
LOGV("APP_CMD_CONFIG_CHANGED");
|
||||
AConfiguration_fromAssetManager(
|
||||
android_app->config, android_app->activity->assetManager);
|
||||
AConfiguration_fromAssetManager(android_app->config,
|
||||
android_app->activity->assetManager);
|
||||
print_cur_config(android_app);
|
||||
break;
|
||||
|
||||
@@ -178,8 +176,7 @@ static void android_app_destroy(struct android_app* android_app) {
|
||||
// Can't touch android_app object after this.
|
||||
}
|
||||
|
||||
static void process_cmd(struct android_app* app,
|
||||
struct android_poll_source* source) {
|
||||
static void process_cmd(struct android_app* app, 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);
|
||||
@@ -189,6 +186,7 @@ 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();
|
||||
@@ -196,18 +194,30 @@ static void* android_app_entry(void* param) {
|
||||
LOGV("config = %p", android_app->config);
|
||||
LOGV("activity = %p", android_app->activity);
|
||||
LOGV("assetmanager = %p", android_app->activity->assetManager);
|
||||
AConfiguration_fromAssetManager(android_app->config,
|
||||
android_app->activity->assetManager);
|
||||
AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager);
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
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);
|
||||
@@ -246,19 +256,17 @@ static bool default_key_filter(const GameActivityKeyEvent* event) {
|
||||
|
||||
static bool default_motion_filter(const GameActivityMotionEvent* event) {
|
||||
// Ignore any non-touch events.
|
||||
return event->source == SOURCE_TOUCHSCREEN;
|
||||
return (event->source & SOURCE_TOUCHSCREEN) != 0;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Native activity interaction (called from main thread)
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
static struct android_app* android_app_create(GameActivity* activity,
|
||||
void* savedState,
|
||||
static struct android_app* android_app_create(GameActivity* activity, void* savedState,
|
||||
size_t savedStateSize) {
|
||||
// struct android_app* android_app = calloc(1, sizeof(struct android_app));
|
||||
struct android_app* android_app =
|
||||
(struct android_app*)malloc(sizeof(struct android_app));
|
||||
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;
|
||||
|
||||
@@ -271,6 +279,13 @@ static struct android_app* android_app_create(GameActivity* activity,
|
||||
memcpy(android_app->savedState, savedState, savedStateSize);
|
||||
}
|
||||
|
||||
android_app->mainLooper = ALooper_forThread();
|
||||
if (android_app->mainLooper == NULL) {
|
||||
LOGE("Failed to get main looper");
|
||||
return NULL;
|
||||
}
|
||||
ALooper_acquire(android_app->mainLooper);
|
||||
|
||||
int msgpipe[2];
|
||||
if (pipe(msgpipe)) {
|
||||
LOGE("could not create pipe: %s", strerror(errno));
|
||||
@@ -298,16 +313,26 @@ static struct android_app* android_app_create(GameActivity* activity,
|
||||
return android_app;
|
||||
}
|
||||
|
||||
static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
|
||||
bool 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", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void android_app_set_window(struct android_app* android_app,
|
||||
ANativeWindow* window) {
|
||||
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);
|
||||
}
|
||||
@@ -321,28 +346,69 @@ static void android_app_set_window(struct android_app* android_app,
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
static void android_app_set_activity_state(struct android_app* android_app,
|
||||
int8_t cmd) {
|
||||
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);
|
||||
// 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_write_cmd(android_app, cmd)) {
|
||||
struct timespec timeout;
|
||||
clock_gettime(CLOCK_REALTIME, &timeout);
|
||||
timeout.tv_sec += NATIVE_APP_GLUE_CMD_WAIT_TIMEOUT_SECONDS;
|
||||
|
||||
int wait_result = 0;
|
||||
while (android_app->activityState != cmd) {
|
||||
wait_result = pthread_cond_timedwait(&android_app->cond, &android_app->mutex, &timeout);
|
||||
if (wait_result == ETIMEDOUT) {
|
||||
LOGE("android_app_set_activity_state timed out waiting for cmd %d", cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
pthread_mutex_destroy(&android_app->mutex);
|
||||
|
||||
if (android_app->mainLooper != NULL) {
|
||||
ALooper_release(android_app->mainLooper);
|
||||
}
|
||||
|
||||
free(android_app);
|
||||
}
|
||||
|
||||
@@ -365,13 +431,22 @@ static void onResume(GameActivity* activity) {
|
||||
android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME);
|
||||
}
|
||||
|
||||
static void onSaveInstanceState(GameActivity* activity,
|
||||
SaveInstanceStateRecallback recallback,
|
||||
static void onSaveInstanceState(GameActivity* activity, SaveInstanceStateRecallback recallback,
|
||||
void* context) {
|
||||
LOGV("SaveInstanceState: %p", 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) {
|
||||
@@ -380,8 +455,7 @@ static void onSaveInstanceState(GameActivity* activity,
|
||||
|
||||
if (android_app->savedState != NULL) {
|
||||
// Tell the Java side about our state.
|
||||
recallback((const char*)android_app->savedState,
|
||||
android_app->savedStateSize, context);
|
||||
recallback((const char*)android_app->savedState, android_app->savedStateSize, context);
|
||||
// Now we can free it.
|
||||
free(android_app->savedState);
|
||||
android_app->savedState = NULL;
|
||||
@@ -413,32 +487,27 @@ static void onTrimMemory(GameActivity* activity, int level) {
|
||||
|
||||
static void onWindowFocusChanged(GameActivity* activity, bool focused) {
|
||||
LOGV("WindowFocusChanged: %p -- %d", activity, focused);
|
||||
android_app_write_cmd(ToApp(activity),
|
||||
focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
|
||||
android_app_write_cmd(ToApp(activity), focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
|
||||
}
|
||||
|
||||
static void onNativeWindowCreated(GameActivity* activity,
|
||||
ANativeWindow* window) {
|
||||
static void onNativeWindowCreated(GameActivity* activity, ANativeWindow* window) {
|
||||
LOGV("NativeWindowCreated: %p -- %p", activity, window);
|
||||
android_app_set_window(ToApp(activity), window);
|
||||
}
|
||||
|
||||
static void onNativeWindowDestroyed(GameActivity* activity,
|
||||
ANativeWindow* window) {
|
||||
static void onNativeWindowDestroyed(GameActivity* activity, ANativeWindow* window) {
|
||||
LOGV("NativeWindowDestroyed: %p -- %p", activity, window);
|
||||
android_app_set_window(ToApp(activity), NULL);
|
||||
}
|
||||
|
||||
static void onNativeWindowRedrawNeeded(GameActivity* activity,
|
||||
ANativeWindow* window) {
|
||||
static void onNativeWindowRedrawNeeded(GameActivity* activity, ANativeWindow* window) {
|
||||
LOGV("NativeWindowRedrawNeeded: %p -- %p", activity, window);
|
||||
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_REDRAW_NEEDED);
|
||||
}
|
||||
|
||||
static void onNativeWindowResized(GameActivity* activity, ANativeWindow* window,
|
||||
int32_t width, int32_t height) {
|
||||
LOGV("NativeWindowResized: %p -- %p ( %d x %d )", activity, window, width,
|
||||
height);
|
||||
static void onNativeWindowResized(GameActivity* activity, ANativeWindow* window, int32_t width,
|
||||
int32_t height) {
|
||||
LOGV("NativeWindowResized: %p -- %p ( %d x %d )", activity, window, width, height);
|
||||
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED);
|
||||
}
|
||||
|
||||
@@ -451,7 +520,6 @@ 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);
|
||||
@@ -466,7 +534,6 @@ 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;
|
||||
@@ -474,70 +541,62 @@ static void notifyInput(struct android_app* android_app) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool onTouchEvent(GameActivity* activity,
|
||||
const GameActivityMotionEvent* event,
|
||||
const GameActivityHistoricalPointerAxes* historical,
|
||||
int historicalLen) {
|
||||
static bool onTouchEvent(GameActivity* activity, const GameActivityMotionEvent* event) {
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
if (android_app->motionEventFilter != NULL &&
|
||||
!android_app->motionEventFilter(event)) {
|
||||
// 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);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct android_input_buffer* inputBuffer =
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
|
||||
// Add to the list of active motion events
|
||||
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->motionEventsCount >= inputBuffer->motionEventsBufferSize) {
|
||||
inputBuffer->motionEventsBufferSize *= 2;
|
||||
inputBuffer->motionEvents =
|
||||
(GameActivityMotionEvent*)realloc(inputBuffer->motionEvents,
|
||||
sizeof(GameActivityMotionEvent) *
|
||||
inputBuffer->motionEventsBufferSize);
|
||||
|
||||
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;
|
||||
if (inputBuffer->motionEvents == NULL) {
|
||||
LOGE("onTouchEvent: out of memory");
|
||||
abort();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int new_ix = inputBuffer->motionEventsCount;
|
||||
memcpy(&inputBuffer->motionEvents[new_ix], event, sizeof(GameActivityMotionEvent));
|
||||
++inputBuffer->motionEventsCount;
|
||||
notifyInput(android_app);
|
||||
|
||||
// android_app_write_cmd(android_app, APP_CMD_TOUCH_EVENT);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct android_input_buffer* android_app_swap_input_buffers(
|
||||
struct android_app* android_app) {
|
||||
struct android_input_buffer* android_app_swap_input_buffers(struct android_app* android_app) {
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
struct android_input_buffer* inputBuffer =
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
|
||||
if (inputBuffer->motionEventsCount == 0 &&
|
||||
inputBuffer->keyEventsCount == 0) {
|
||||
if (inputBuffer->motionEventsCount == 0 && inputBuffer->keyEventsCount == 0) {
|
||||
inputBuffer = NULL;
|
||||
} else {
|
||||
android_app->currentInputBuffer =
|
||||
(android_app->currentInputBuffer + 1) %
|
||||
NATIVE_APP_GLUE_MAX_INPUT_BUFFERS;
|
||||
(android_app->currentInputBuffer + 1) % NATIVE_APP_GLUE_MAX_INPUT_BUFFERS;
|
||||
}
|
||||
|
||||
android_app->inputSwapPending = false;
|
||||
@@ -549,11 +608,18 @@ struct android_input_buffer* android_app_swap_input_buffers(
|
||||
}
|
||||
|
||||
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) {
|
||||
inputBuffer->motionEventsCount = 0;
|
||||
// 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);
|
||||
}
|
||||
|
||||
void android_app_set_key_event_filter(struct android_app* app,
|
||||
android_key_event_filter filter) {
|
||||
void android_app_set_key_event_filter(struct android_app* app, android_key_event_filter filter) {
|
||||
pthread_mutex_lock(&app->mutex);
|
||||
app->keyEventFilter = filter;
|
||||
pthread_mutex_unlock(&app->mutex);
|
||||
@@ -563,30 +629,43 @@ static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
if (android_app->keyEventFilter != NULL &&
|
||||
!android_app->keyEventFilter(event)) {
|
||||
// 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);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct android_input_buffer* inputBuffer =
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
&android_app->inputBuffers[android_app->currentInputBuffer];
|
||||
|
||||
// Add to the list of active key down events
|
||||
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->keyEventsCount >= inputBuffer->keyEventsBufferSize) {
|
||||
inputBuffer->keyEventsBufferSize = inputBuffer->keyEventsBufferSize * 2;
|
||||
inputBuffer->keyEvents =
|
||||
(GameActivityKeyEvent*)realloc(inputBuffer->keyEvents,
|
||||
sizeof(GameActivityKeyEvent) *
|
||||
inputBuffer->keyEventsBufferSize);
|
||||
|
||||
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);
|
||||
if (inputBuffer->keyEvents == NULL) {
|
||||
LOGE("onKey: out of memory");
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
int new_ix = inputBuffer->keyEventsCount;
|
||||
memcpy(&inputBuffer->keyEvents[new_ix], event, sizeof(GameActivityKeyEvent));
|
||||
++inputBuffer->keyEventsCount;
|
||||
notifyInput(android_app);
|
||||
|
||||
// android_app_write_cmd(android_app, APP_CMD_KEY_EVENT);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return true;
|
||||
}
|
||||
@@ -595,12 +674,13 @@ void android_app_clear_key_events(struct android_input_buffer* inputBuffer) {
|
||||
inputBuffer->keyEventsCount = 0;
|
||||
}
|
||||
|
||||
static void onTextInputEvent(GameActivity* activity,
|
||||
const GameTextInputState* state) {
|
||||
static void onTextInputEvent(GameActivity* activity, const GameTextInputState* state) {
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
android_app->textInputState = 1;
|
||||
if (!android_app->destroyed) {
|
||||
android_app->textInputState = 1;
|
||||
notifyInput(android_app);
|
||||
}
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
@@ -609,9 +689,61 @@ 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);
|
||||
}
|
||||
|
||||
static void onSoftwareKeyboardVisibilityChanged(GameActivity* activity, bool visible) {
|
||||
LOGV("SoftwareKeyboardVisibilityChanged: %p -- %d", activity, (int)visible);
|
||||
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
android_app->softwareKeyboardVisible = visible;
|
||||
|
||||
android_app_write_cmd(android_app, APP_CMD_SOFTWARE_KB_VIS_CHANGED);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
}
|
||||
|
||||
static bool onEditorAction(GameActivity* activity, int action) {
|
||||
LOGV("EditorAction: %p -- %d", activity, action);
|
||||
|
||||
struct android_app* android_app = ToApp(activity);
|
||||
|
||||
pthread_mutex_lock(&android_app->mutex);
|
||||
|
||||
// XXX: this is a racy design that could lose InputConnection actions if the
|
||||
// application doesn't manage to look at app->editorAction before another
|
||||
// action is delivered.
|
||||
if (android_app->pendingEditorAction) {
|
||||
LOGW("Dropping editor action %d because previous action %d not yet "
|
||||
"handled",
|
||||
action, android_app->editorAction);
|
||||
}
|
||||
android_app->editorAction = action;
|
||||
android_app->pendingEditorAction = true;
|
||||
notifyInput(android_app);
|
||||
// TODO: buffer IME text events and editor actions like other input events
|
||||
|
||||
// android_app_write_cmd(android_app, APP_CMD_EDITOR_ACTION);
|
||||
pthread_mutex_unlock(&android_app->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
// XXX: This symbol is renamed with a _C suffix so we can implement
|
||||
// `GameActivity_onCreate` as a wrapper in Rust that does some additional setup
|
||||
// before calling this function,
|
||||
JNIEXPORT
|
||||
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
|
||||
size_t savedStateSize) {
|
||||
void GameActivity_onCreate_C(GameActivity* activity, void* savedState, size_t savedStateSize) {
|
||||
LOGV("Creating: %p", activity);
|
||||
activity->callbacks->onDestroy = onDestroy;
|
||||
activity->callbacks->onStart = onStart;
|
||||
@@ -628,12 +760,13 @@ void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
|
||||
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
|
||||
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
|
||||
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
|
||||
activity->callbacks->onNativeWindowRedrawNeeded =
|
||||
onNativeWindowRedrawNeeded;
|
||||
activity->callbacks->onNativeWindowRedrawNeeded = onNativeWindowRedrawNeeded;
|
||||
activity->callbacks->onNativeWindowResized = onNativeWindowResized;
|
||||
activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged;
|
||||
activity->callbacks->onContentRectChanged = onContentRectChanged;
|
||||
activity->callbacks->onSoftwareKeyboardVisibilityChanged = onSoftwareKeyboardVisibilityChanged;
|
||||
activity->callbacks->onEditorAction = onEditorAction;
|
||||
LOGV("Callbacks set: %p", activity->callbacks);
|
||||
|
||||
activity->instance =
|
||||
android_app_create(activity, savedState, savedStateSize);
|
||||
activity->instance = android_app_create(activity, savedState, savedStateSize);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "game-activity",
|
||||
"schema_version": 1,
|
||||
"dependencies": [],
|
||||
"version": "0.0.1",
|
||||
"cpp_files": [
|
||||
"src/common/system_utils.cpp",
|
||||
"src/game-activity/GameActivity.cpp",
|
||||
"src/game-activity/native_app_glue/android_native_app_glue.c",
|
||||
"src/game-activity/GameActivityEvents.cpp",
|
||||
"src/game-text-input/gametextinput.cpp"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,918 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup game_text_input Game Text Input
|
||||
* The interface to use GameTextInput.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/rect.h>
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "common/gamesdk_common.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define GAMETEXTINPUT_VERSION_REVISION 7cd950d0022d01f1e7e2b470aba5a7b1abacdfaa
|
||||
#define GAMETEXTINPUT_MAJOR_VERSION 4
|
||||
#define GAMETEXTINPUT_MINOR_VERSION 3
|
||||
#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
|
||||
* start==end. An undefined span is specified with start = end = SPAN_UNDEFINED.
|
||||
*/
|
||||
typedef struct GameTextInputSpan {
|
||||
/** The start of the region (inclusive). */
|
||||
int32_t start;
|
||||
/** The end of the region (exclusive). */
|
||||
int32_t end;
|
||||
} GameTextInputSpan;
|
||||
|
||||
/**
|
||||
* Values with special meaning in a GameTextInputSpan.
|
||||
*/
|
||||
enum GameTextInputSpanFlag : int32_t { SPAN_UNDEFINED = -1 };
|
||||
|
||||
/**
|
||||
* This struct holds the state of an editable section of text.
|
||||
* The text can have a selection and a composing region defined on it.
|
||||
* A composing region is used by IMEs that allow input using multiple steps to
|
||||
* compose a glyph or word. Use functions GameTextInput_getState and
|
||||
* GameTextInput_setState to read and modify the state that an IME is editing.
|
||||
*/
|
||||
typedef struct GameTextInputState {
|
||||
/**
|
||||
* Text owned by the state, as a modified UTF-8 string. Null-terminated.
|
||||
* https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
|
||||
*/
|
||||
const char* text_UTF8;
|
||||
/**
|
||||
* Length in bytes of text_UTF8, *not* including the null at end.
|
||||
*/
|
||||
int32_t text_length;
|
||||
/**
|
||||
* A selection defined on the text.
|
||||
*/
|
||||
GameTextInputSpan selection;
|
||||
/**
|
||||
* A composing region defined on the text.
|
||||
*/
|
||||
GameTextInputSpan composingRegion;
|
||||
} GameTextInputState;
|
||||
|
||||
/**
|
||||
* A callback called by GameTextInput_getState.
|
||||
* @param context User-defined context.
|
||||
* @param state State, owned by the library, that will be valid for the duration
|
||||
* of the callback.
|
||||
*/
|
||||
typedef void (*GameTextInputGetStateCallback)(void* context,
|
||||
const struct GameTextInputState* state);
|
||||
|
||||
/**
|
||||
* Opaque handle to the GameTextInput API.
|
||||
*/
|
||||
typedef struct GameTextInput GameTextInput;
|
||||
|
||||
/**
|
||||
* Initialize the GameTextInput library.
|
||||
* If called twice without GameTextInput_destroy being called, the same pointer
|
||||
* will be returned and a warning will be issued.
|
||||
* @param env A JNI env valid on the calling thread. All other calls to the resulting GameTextInput
|
||||
* object must be done on the same calling thread.
|
||||
* @param max_string_size The maximum length of a string that can be edited. If
|
||||
* zero, the maximum defaults to 65536 bytes. A buffer of this size is allocated
|
||||
* at initialization.
|
||||
* @return A handle to the library.
|
||||
*/
|
||||
GameTextInput* GameTextInput_init(JNIEnv* env, uint32_t max_string_size);
|
||||
|
||||
/**
|
||||
* When using GameTextInput, you need to create a gametextinput.InputConnection
|
||||
* on the Java side and pass it using this function to the library, unless using
|
||||
* GameActivity in which case this will be done for you. See the GameActivity
|
||||
* source code or GameTextInput samples for examples of usage.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param inputConnection A gametextinput.InputConnection object.
|
||||
*/
|
||||
void GameTextInput_setInputConnection(GameTextInput* input, jobject inputConnection);
|
||||
|
||||
/**
|
||||
* Unless using GameActivity, it is required to call this function from your
|
||||
* Java gametextinput.Listener.stateChanged method to convert eventState and
|
||||
* trigger any event callbacks. When using GameActivity, this does not need to
|
||||
* be called as event processing is handled by the Activity.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param eventState A Java gametextinput.State object.
|
||||
*/
|
||||
void GameTextInput_processEvent(GameTextInput* input, jobject eventState);
|
||||
|
||||
/**
|
||||
* Free any resources owned by the GameTextInput library.
|
||||
* Any subsequent calls to the library will fail until GameTextInput_init is
|
||||
* called again.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
*/
|
||||
void GameTextInput_destroy(GameTextInput* input);
|
||||
|
||||
/**
|
||||
* Flags to be passed to GameTextInput_showIme.
|
||||
*/
|
||||
enum ShowImeFlags : uint32_t {
|
||||
SHOW_IME_UNDEFINED = 0, // Default value.
|
||||
SHOW_IMPLICIT = 1, // Indicates that the user has forced the input method open so it
|
||||
// should not be closed until they explicitly do so.
|
||||
SHOW_FORCED = 2 // Indicates that this is an implicit request to show the
|
||||
// input window, not as the result of a direct request by
|
||||
// the user. The window may not be shown in this case.
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the IME. Calls InputMethodManager.showSoftInput().
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param flags Defined in ShowImeFlags above. For more information see:
|
||||
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
|
||||
*/
|
||||
void GameTextInput_showIme(GameTextInput* input, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Flags to be passed to GameTextInput_hideIme.
|
||||
*/
|
||||
enum HideImeFlags : uint32_t {
|
||||
HIDE_IME_UNDEFINED = 0, // Default value.
|
||||
HIDE_IMPLICIT_ONLY = 1, // Indicates that the soft input window should only be hidden if it
|
||||
// was not explicitly shown by the user.
|
||||
HIDE_NOT_ALWAYS = 2, // Indicates that the soft input window should normally be hidden,
|
||||
// unless it was originally shown with SHOW_FORCED.
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the IME. Calls InputMethodManager.hideSoftInputFromWindow().
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param flags Defined in HideImeFlags above. For more information see:
|
||||
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
|
||||
*/
|
||||
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
|
||||
* callback rather than returning the state in order to simplify ownership of
|
||||
* text_UTF8 strings. These strings are only valid during the calling of the
|
||||
* callback.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param callback A function that will be called with valid state.
|
||||
* @param context Context used by the callback.
|
||||
*/
|
||||
void GameTextInput_getState(GameTextInput* input, GameTextInputGetStateCallback callback,
|
||||
void* context);
|
||||
|
||||
/**
|
||||
* Set the current GameTextInput state. This state is reflected to any active
|
||||
* IME.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param state The state to set. Ownership is maintained by the caller and must
|
||||
* remain valid for the duration of the call.
|
||||
*/
|
||||
void GameTextInput_setState(GameTextInput* input, const GameTextInputState* state);
|
||||
|
||||
/**
|
||||
* Type of the callback needed by GameTextInput_setEventCallback that will be
|
||||
* called every time the IME state changes.
|
||||
* @param context User-defined context set in GameTextInput_setEventCallback.
|
||||
* @param current_state Current IME state, owned by the library and valid during
|
||||
* the callback.
|
||||
*/
|
||||
typedef void (*GameTextInputEventCallback)(void* context, const GameTextInputState* current_state);
|
||||
|
||||
/**
|
||||
* Optionally set a callback to be called whenever the IME state changes.
|
||||
* Not necessary if you are using GameActivity, which handles these callbacks
|
||||
* for you.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param callback Called by the library when the IME state changes.
|
||||
* @param context Context passed as first argument to the callback.
|
||||
* <b>This function is deprecated. Don't perform any complex processing inside
|
||||
* the callback other than copying the state variable. Using any synchronization
|
||||
* primitives inside this callback may cause a deadlock.</b>
|
||||
*/
|
||||
void GameTextInput_setEventCallback(GameTextInput* input, GameTextInputEventCallback callback,
|
||||
void* context);
|
||||
|
||||
/**
|
||||
* Type of the callback needed by GameTextInput_setImeInsetsCallback that will
|
||||
* be called every time the IME window insets change.
|
||||
* @param context User-defined context set in
|
||||
* GameTextInput_setImeWIndowInsetsCallback.
|
||||
* @param current_insets Current IME insets, owned by the library and valid
|
||||
* during the callback.
|
||||
*/
|
||||
typedef void (*GameTextInputImeInsetsCallback)(void* context, const ARect* current_insets);
|
||||
|
||||
/**
|
||||
* Optionally set a callback to be called whenever the IME insets change.
|
||||
* Not necessary if you are using GameActivity, which handles these callbacks
|
||||
* for you.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param callback Called by the library when the IME insets change.
|
||||
* @param context Context passed as first argument to the callback.
|
||||
*/
|
||||
void GameTextInput_setImeInsetsCallback(GameTextInput* input,
|
||||
GameTextInputImeInsetsCallback callback, void* context);
|
||||
|
||||
/**
|
||||
* Get the current window insets for the IME.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param insets Filled with the current insets by this function.
|
||||
*/
|
||||
void GameTextInput_getImeInsets(const GameTextInput* input, ARect* insets);
|
||||
|
||||
/**
|
||||
* Unless using GameActivity, it is required to call this function from your
|
||||
* Java gametextinput.Listener.onImeInsetsChanged method to
|
||||
* trigger any event callbacks. When using GameActivity, this does not need to
|
||||
* be called as insets processing is handled by the Activity.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param eventState A Java gametextinput.State object.
|
||||
*/
|
||||
void GameTextInput_processImeInsets(GameTextInput* input, const ARect* insets);
|
||||
|
||||
/**
|
||||
* Convert a GameTextInputState struct to a Java gametextinput.State object.
|
||||
* Don't forget to delete the returned Java local ref when you're done.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param state Input state to convert.
|
||||
* @return A Java object of class gametextinput.State. The caller is required to
|
||||
* delete this local reference.
|
||||
*/
|
||||
jobject GameTextInputState_toJava(const GameTextInput* input, const GameTextInputState* state);
|
||||
|
||||
/**
|
||||
* Convert from a Java gametextinput.State object into a C GameTextInputState
|
||||
* struct.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param state A Java gametextinput.State object.
|
||||
* @param callback A function called with the C struct, valid for the duration
|
||||
* of the call.
|
||||
* @param context Context passed to the callback.
|
||||
*/
|
||||
void GameTextInputState_fromJava(const GameTextInput* input, jobject state,
|
||||
GameTextInputGetStateCallback callback, void* context);
|
||||
|
||||
/**
|
||||
* Definitions for inputType argument of GameActivity_setImeEditorInfo()
|
||||
*
|
||||
* <pre>
|
||||
* |-------|-------|-------|-------|
|
||||
* 1111 TYPE_MASK_CLASS
|
||||
* 11111111 TYPE_MASK_VARIATION
|
||||
* 111111111111 TYPE_MASK_FLAGS
|
||||
* |-------|-------|-------|-------|
|
||||
* TYPE_NULL
|
||||
* |-------|-------|-------|-------|
|
||||
* 1 TYPE_CLASS_TEXT
|
||||
* 1 TYPE_TEXT_VARIATION_URI
|
||||
* 1 TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|
||||
* 11 TYPE_TEXT_VARIATION_EMAIL_SUBJECT
|
||||
* 1 TYPE_TEXT_VARIATION_SHORT_MESSAGE
|
||||
* 1 1 TYPE_TEXT_VARIATION_LONG_MESSAGE
|
||||
* 11 TYPE_TEXT_VARIATION_PERSON_NAME
|
||||
* 111 TYPE_TEXT_VARIATION_POSTAL_ADDRESS
|
||||
* 1 TYPE_TEXT_VARIATION_PASSWORD
|
||||
* 1 1 TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
|
||||
* 1 1 TYPE_TEXT_VARIATION_WEB_EDIT_TEXT
|
||||
* 1 11 TYPE_TEXT_VARIATION_FILTER
|
||||
* 11 TYPE_TEXT_VARIATION_PHONETIC
|
||||
* 11 1 TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
|
||||
* 111 TYPE_TEXT_VARIATION_WEB_PASSWORD
|
||||
* 1 TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||
* 1 TYPE_TEXT_FLAG_CAP_WORDS
|
||||
* 1 TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
* 1 TYPE_TEXT_FLAG_AUTO_CORRECT
|
||||
* 1 TYPE_TEXT_FLAG_AUTO_COMPLETE
|
||||
* 1 TYPE_TEXT_FLAG_MULTI_LINE
|
||||
* 1 TYPE_TEXT_FLAG_IME_MULTI_LINE
|
||||
* 1 TYPE_TEXT_FLAG_NO_SUGGESTIONS
|
||||
* 1 TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS
|
||||
* |-------|-------|-------|-------|
|
||||
* 1 TYPE_CLASS_NUMBER
|
||||
* 1 TYPE_NUMBER_VARIATION_PASSWORD
|
||||
* 1 TYPE_NUMBER_FLAG_SIGNED
|
||||
* 1 TYPE_NUMBER_FLAG_DECIMAL
|
||||
* |-------|-------|-------|-------|
|
||||
* 11 TYPE_CLASS_PHONE
|
||||
* |-------|-------|-------|-------|
|
||||
* 1 TYPE_CLASS_DATETIME
|
||||
* 1 TYPE_DATETIME_VARIATION_DATE
|
||||
* 1 TYPE_DATETIME_VARIATION_TIME
|
||||
* |-------|-------|-------|-------|</pre>
|
||||
*/
|
||||
|
||||
enum GameTextInputType : uint32_t {
|
||||
/**
|
||||
* Mask of bits that determine the overall class
|
||||
* of text being given. Currently supported classes are:
|
||||
* {@link #TYPE_CLASS_TEXT}, {@link #TYPE_CLASS_NUMBER},
|
||||
* {@link #TYPE_CLASS_PHONE}, {@link #TYPE_CLASS_DATETIME}.
|
||||
* <p>IME authors: If the class is not one you
|
||||
* understand, assume {@link #TYPE_CLASS_TEXT} with NO variation
|
||||
* or flags.<p>
|
||||
*/
|
||||
TYPE_MASK_CLASS = 0x0000000f,
|
||||
|
||||
/**
|
||||
* Mask of bits that determine the variation of
|
||||
* the base content class.
|
||||
*/
|
||||
TYPE_MASK_VARIATION = 0x00000ff0,
|
||||
|
||||
/**
|
||||
* Mask of bits that provide addition bit flags
|
||||
* of options.
|
||||
*/
|
||||
TYPE_MASK_FLAGS = 0x00fff000,
|
||||
|
||||
/**
|
||||
* Special content type for when no explicit type has been specified.
|
||||
* This should be interpreted to mean that the target input connection
|
||||
* is not rich, it can not process and show things like candidate text nor
|
||||
* retrieve the current text, so the input method will need to run in a
|
||||
* limited "generate key events" mode, if it supports it. Note that some
|
||||
* input methods may not support it, for example a voice-based input
|
||||
* method will likely not be able to generate key events even if this
|
||||
* flag is set.
|
||||
*/
|
||||
TYPE_NULL = 0x00000000,
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Class for normal text. This class supports the following flags (only
|
||||
* one of which should be set):
|
||||
* {@link #TYPE_TEXT_FLAG_CAP_CHARACTERS},
|
||||
* {@link #TYPE_TEXT_FLAG_CAP_WORDS}, and.
|
||||
* {@link #TYPE_TEXT_FLAG_CAP_SENTENCES}. It also supports the
|
||||
* following variations:
|
||||
* {@link #TYPE_TEXT_VARIATION_NORMAL}, and
|
||||
* {@link #TYPE_TEXT_VARIATION_URI}. If you do not recognize the
|
||||
* variation, normal should be assumed.
|
||||
*/
|
||||
TYPE_CLASS_TEXT = 0x00000001,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: capitalize all characters. Overrides
|
||||
* {@link #TYPE_TEXT_FLAG_CAP_WORDS} and
|
||||
* {@link #TYPE_TEXT_FLAG_CAP_SENTENCES}. This value is explicitly defined
|
||||
* to be the same as {@link TextUtils#CAP_MODE_CHARACTERS}. Of course,
|
||||
* this only affects languages where there are upper-case and lower-case
|
||||
* letters.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_CAP_CHARACTERS = 0x00001000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: capitalize the first character of
|
||||
* every word. Overrides {@link #TYPE_TEXT_FLAG_CAP_SENTENCES}. This
|
||||
* value is explicitly defined
|
||||
* to be the same as {@link TextUtils#CAP_MODE_WORDS}. Of course,
|
||||
* this only affects languages where there are upper-case and lower-case
|
||||
* letters.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_CAP_WORDS = 0x00002000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: capitalize the first character of
|
||||
* each sentence. This value is explicitly defined
|
||||
* to be the same as {@link TextUtils#CAP_MODE_SENTENCES}. For example
|
||||
* in English it means to capitalize after a period and a space (note that
|
||||
* other languages may have different characters for period, or not use
|
||||
* spaces, or use different grammatical rules). Of course, this only affects
|
||||
* languages where there are upper-case and lower-case letters.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_CAP_SENTENCES = 0x00004000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: the user is entering free-form
|
||||
* text that should have auto-correction applied to it. Without this flag,
|
||||
* the IME will not try to correct typos. You should always set this flag
|
||||
* unless you really expect users to type non-words in this field, for
|
||||
* example to choose a name for a character in a game.
|
||||
* Contrast this with {@link #TYPE_TEXT_FLAG_AUTO_COMPLETE} and
|
||||
* {@link #TYPE_TEXT_FLAG_NO_SUGGESTIONS}:
|
||||
* {@code TYPE_TEXT_FLAG_AUTO_CORRECT} means that the IME will try to
|
||||
* auto-correct typos as the user is typing, but does not define whether
|
||||
* the IME offers an interface to show suggestions.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_AUTO_CORRECT = 0x00008000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: the text editor (which means
|
||||
* the application) is performing auto-completion of the text being entered
|
||||
* based on its own semantics, which it will present to the user as they type.
|
||||
* This generally means that the input method should not be showing
|
||||
* candidates itself, but can expect the editor to supply its own
|
||||
* completions/candidates from
|
||||
* {@link android.view.inputmethod.InputMethodSession#displayCompletions
|
||||
* InputMethodSession.displayCompletions()} as a result of the editor calling
|
||||
* {@link android.view.inputmethod.InputMethodManager#displayCompletions
|
||||
* InputMethodManager.displayCompletions()}.
|
||||
* Note the contrast with {@link #TYPE_TEXT_FLAG_AUTO_CORRECT} and
|
||||
* {@link #TYPE_TEXT_FLAG_NO_SUGGESTIONS}:
|
||||
* {@code TYPE_TEXT_FLAG_AUTO_COMPLETE} means the editor should show an
|
||||
* interface for displaying suggestions, but instead of supplying its own
|
||||
* it will rely on the Editor to pass completions/corrections.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_AUTO_COMPLETE = 0x00010000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: multiple lines of text can be
|
||||
* entered into the field. If this flag is not set, the text field
|
||||
* will be constrained to a single line. The IME may also choose not to
|
||||
* display an enter key when this flag is not set, as there should be no
|
||||
* need to create new lines.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_MULTI_LINE = 0x00020000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: the regular text view associated
|
||||
* with this should not be multi-line, but when a fullscreen input method
|
||||
* is providing text it should use multiple lines if it can.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_IME_MULTI_LINE = 0x00040000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: the input method does not need to
|
||||
* display any dictionary-based candidates. This is useful for text views that
|
||||
* do not contain words from the language and do not benefit from any
|
||||
* dictionary-based completions or corrections. It overrides the
|
||||
* {@link #TYPE_TEXT_FLAG_AUTO_CORRECT} value when set.
|
||||
* Please avoid using this unless you are certain this is what you want.
|
||||
* Many input methods need suggestions to work well, for example the ones
|
||||
* based on gesture typing. Consider clearing
|
||||
* {@link #TYPE_TEXT_FLAG_AUTO_CORRECT} instead if you just do not
|
||||
* want the IME to correct typos.
|
||||
* Note the contrast with {@link #TYPE_TEXT_FLAG_AUTO_CORRECT} and
|
||||
* {@link #TYPE_TEXT_FLAG_AUTO_COMPLETE}:
|
||||
* {@code TYPE_TEXT_FLAG_NO_SUGGESTIONS} means the IME does not need to
|
||||
* show an interface to display suggestions. Most IMEs will also take this to
|
||||
* mean they do not need to try to auto-correct what the user is typing.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_NO_SUGGESTIONS = 0x00080000,
|
||||
|
||||
/**
|
||||
* Flag for {@link #TYPE_CLASS_TEXT}: Let the IME know the text conversion
|
||||
* suggestions are required by the application. Text conversion suggestion is
|
||||
* for the transliteration languages which has pronunciation characters and
|
||||
* target characters. When the user is typing the pronunciation charactes, the
|
||||
* IME could provide the possible target characters to the user. When this
|
||||
* flag is set, the IME should insert the text conversion suggestions through
|
||||
* {@link Builder#setTextConversionSuggestions(List)} and
|
||||
* the {@link TextAttribute} with initialized with the text conversion
|
||||
* suggestions is provided by the IME to the application. To receive the
|
||||
* additional information, the application needs to implement {@link
|
||||
* InputConnection#setComposingText(CharSequence, int, TextAttribute)},
|
||||
* {@link InputConnection#setComposingRegion(int, int, TextAttribute)}, and
|
||||
* {@link InputConnection#commitText(CharSequence, int, TextAttribute)}.
|
||||
*/
|
||||
TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS = 0x00100000,
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Default variation of {@link #TYPE_CLASS_TEXT}: plain old normal text.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_NORMAL = 0x00000000,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering a URI.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_URI = 0x00000010,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering an e-mail address.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 0x00000020,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering the subject line of
|
||||
* an e-mail.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_EMAIL_SUBJECT = 0x00000030,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering a short, possibly informal
|
||||
* message such as an instant message or a text message.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_SHORT_MESSAGE = 0x00000040,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering the content of a long,
|
||||
* possibly formal message such as the body of an e-mail.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_LONG_MESSAGE = 0x00000050,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering the name of a person.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_PERSON_NAME = 0x00000060,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering a postal mailing address.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 0x00000070,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering a password.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_PASSWORD = 0x00000080,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering a password, which should
|
||||
* be visible to the user.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 0x00000090,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering text inside of a web form.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_WEB_EDIT_TEXT = 0x000000a0,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering text to filter contents
|
||||
* of a list etc.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_FILTER = 0x000000b0,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering text for phonetic
|
||||
* pronunciation, such as a phonetic name field in contacts. This is mostly
|
||||
* useful for languages where one spelling may have several phonetic
|
||||
* readings, like Japanese.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_PHONETIC = 0x000000c0,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering e-mail address inside
|
||||
* of a web form. This was added in
|
||||
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}. An IME must target
|
||||
* this API version or later to see this input type; if it doesn't, a request
|
||||
* for this type will be seen as {@link #TYPE_TEXT_VARIATION_EMAIL_ADDRESS}
|
||||
* when passed through {@link
|
||||
* android.view.inputmethod.EditorInfo#makeCompatible(int)
|
||||
* EditorInfo.makeCompatible(int)}.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS = 0x000000d0,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_TEXT}: entering password inside
|
||||
* of a web form. This was added in
|
||||
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}. An IME must target
|
||||
* this API version or later to see this input type; if it doesn't, a request
|
||||
* for this type will be seen as {@link #TYPE_TEXT_VARIATION_PASSWORD}
|
||||
* when passed through {@link
|
||||
* android.view.inputmethod.EditorInfo#makeCompatible(int)
|
||||
* EditorInfo.makeCompatible(int)}.
|
||||
*/
|
||||
TYPE_TEXT_VARIATION_WEB_PASSWORD = 0x000000e0,
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Class for numeric text. This class supports the following flags:
|
||||
* {@link #TYPE_NUMBER_FLAG_SIGNED} and
|
||||
* {@link #TYPE_NUMBER_FLAG_DECIMAL}. It also supports the following
|
||||
* variations: {@link #TYPE_NUMBER_VARIATION_NORMAL} and
|
||||
* {@link #TYPE_NUMBER_VARIATION_PASSWORD}.
|
||||
* <p>IME authors: If you do not recognize
|
||||
* the variation, normal should be assumed.</p>
|
||||
*/
|
||||
TYPE_CLASS_NUMBER = 0x00000002,
|
||||
|
||||
/**
|
||||
* Flag of {@link #TYPE_CLASS_NUMBER}: the number is signed, allowing
|
||||
* a positive or negative sign at the start.
|
||||
*/
|
||||
TYPE_NUMBER_FLAG_SIGNED = 0x00001000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #TYPE_CLASS_NUMBER}: the number is decimal, allowing
|
||||
* a decimal point to provide fractional values.
|
||||
*/
|
||||
TYPE_NUMBER_FLAG_DECIMAL = 0x00002000,
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Default variation of {@link #TYPE_CLASS_NUMBER}: plain normal
|
||||
* numeric text. This was added in
|
||||
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}. An IME must target
|
||||
* this API version or later to see this input type; if it doesn't, a request
|
||||
* for this type will be dropped when passed through
|
||||
* {@link android.view.inputmethod.EditorInfo#makeCompatible(int)
|
||||
* EditorInfo.makeCompatible(int)}.
|
||||
*/
|
||||
TYPE_NUMBER_VARIATION_NORMAL = 0x00000000,
|
||||
|
||||
/**
|
||||
* Variation of {@link #TYPE_CLASS_NUMBER}: entering a numeric password.
|
||||
* This was added in {@link android.os.Build.VERSION_CODES#HONEYCOMB}. An
|
||||
* IME must target this API version or later to see this input type; if it
|
||||
* doesn't, a request for this type will be dropped when passed
|
||||
* through {@link android.view.inputmethod.EditorInfo#makeCompatible(int)
|
||||
* EditorInfo.makeCompatible(int)}.
|
||||
*/
|
||||
TYPE_NUMBER_VARIATION_PASSWORD = 0x00000010,
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
/**
|
||||
* Class for a phone number. This class currently supports no variations
|
||||
* or flags.
|
||||
*/
|
||||
TYPE_CLASS_PHONE = 0x00000003,
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Class for dates and times. It supports the
|
||||
* following variations:
|
||||
* {@link #TYPE_DATETIME_VARIATION_NORMAL}
|
||||
* {@link #TYPE_DATETIME_VARIATION_DATE}, and
|
||||
* {@link #TYPE_DATETIME_VARIATION_TIME}.
|
||||
*/
|
||||
TYPE_CLASS_DATETIME = 0x00000004,
|
||||
|
||||
/**
|
||||
* Default variation of {@link #TYPE_CLASS_DATETIME}: allows entering
|
||||
* both a date and time.
|
||||
*/
|
||||
TYPE_DATETIME_VARIATION_NORMAL = 0x00000000,
|
||||
|
||||
/**
|
||||
* Default variation of {@link #TYPE_CLASS_DATETIME}: allows entering
|
||||
* only a date.
|
||||
*/
|
||||
TYPE_DATETIME_VARIATION_DATE = 0x00000010,
|
||||
|
||||
/**
|
||||
* Default variation of {@link #TYPE_CLASS_DATETIME}: allows entering
|
||||
* only a time.
|
||||
*/
|
||||
TYPE_DATETIME_VARIATION_TIME = 0x00000020,
|
||||
};
|
||||
|
||||
/**
|
||||
* actionId and imeOptions argument of GameActivity_setImeEditorInfo().
|
||||
*
|
||||
* <pre>
|
||||
* |-------|-------|-------|-------|
|
||||
* 1111 IME_MASK_ACTION
|
||||
* |-------|-------|-------|-------|
|
||||
* IME_ACTION_UNSPECIFIED
|
||||
* 1 IME_ACTION_NONE
|
||||
* 1 IME_ACTION_GO
|
||||
* 11 IME_ACTION_SEARCH
|
||||
* 1 IME_ACTION_SEND
|
||||
* 1 1 IME_ACTION_NEXT
|
||||
* 11 IME_ACTION_DONE
|
||||
* 111 IME_ACTION_PREVIOUS
|
||||
* 1 IME_FLAG_NO_PERSONALIZED_LEARNING
|
||||
* 1 IME_FLAG_NO_FULLSCREEN
|
||||
* 1 IME_FLAG_NAVIGATE_PREVIOUS
|
||||
* 1 IME_FLAG_NAVIGATE_NEXT
|
||||
* 1 IME_FLAG_NO_EXTRACT_UI
|
||||
* 1 IME_FLAG_NO_ACCESSORY_ACTION
|
||||
* 1 IME_FLAG_NO_ENTER_ACTION
|
||||
* 1 IME_FLAG_FORCE_ASCII
|
||||
* |-------|-------|-------|-------|</pre>
|
||||
*/
|
||||
|
||||
enum GameTextInputActionType : uint32_t {
|
||||
/**
|
||||
* Set of bits in {@link #imeOptions} that provide alternative actions
|
||||
* associated with the "enter" key. This both helps the IME provide
|
||||
* better feedback about what the enter key will do, and also allows it
|
||||
* to provide alternative mechanisms for providing that command.
|
||||
*/
|
||||
IME_MASK_ACTION = 0x000000ff,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: no specific action has been
|
||||
* associated with this editor, let the editor come up with its own if
|
||||
* it can.
|
||||
*/
|
||||
IME_ACTION_UNSPECIFIED = 0x00000000,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: there is no available action.
|
||||
*/
|
||||
IME_ACTION_NONE = 0x00000001,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: the action key performs a "go"
|
||||
* operation to take the user to the target of the text they typed.
|
||||
* Typically used, for example, when entering a URL.
|
||||
*/
|
||||
IME_ACTION_GO = 0x00000002,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: the action key performs a "search"
|
||||
* operation, taking the user to the results of searching for the text
|
||||
* they have typed (in whatever context is appropriate).
|
||||
*/
|
||||
IME_ACTION_SEARCH = 0x00000003,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: the action key performs a "send"
|
||||
* operation, delivering the text to its target. This is typically used
|
||||
* when composing a message in IM or SMS where sending is immediate.
|
||||
*/
|
||||
IME_ACTION_SEND = 0x00000004,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: the action key performs a "next"
|
||||
* operation, taking the user to the next field that will accept text.
|
||||
*/
|
||||
IME_ACTION_NEXT = 0x00000005,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: the action key performs a "done"
|
||||
* operation, typically meaning there is nothing more to input and the
|
||||
* IME will be closed.
|
||||
*/
|
||||
IME_ACTION_DONE = 0x00000006,
|
||||
|
||||
/**
|
||||
* Bits of {@link #IME_MASK_ACTION}: like {@link #IME_ACTION_NEXT}, but
|
||||
* for moving to the previous field. This will normally not be used to
|
||||
* specify an action (since it precludes {@link #IME_ACTION_NEXT}), but
|
||||
* can be returned to the app if it sets {@link #IME_FLAG_NAVIGATE_PREVIOUS}.
|
||||
*/
|
||||
IME_ACTION_PREVIOUS = 0x00000007,
|
||||
};
|
||||
|
||||
enum GameTextInputImeOptions : uint32_t {
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used to request that the IME should not update
|
||||
* any personalized data such as typing history and personalized language
|
||||
* model based on what the user typed on this text editing object. Typical
|
||||
* use cases are: <ul> <li>When the application is in a special mode, where
|
||||
* user's activities are expected to be not recorded in the application's
|
||||
* history. Some web browsers and chat applications may have this kind of
|
||||
* modes.</li> <li>When storing typing history does not make much sense.
|
||||
* Specifying this flag in typing games may help to avoid typing history from
|
||||
* being filled up with words that the user is less likely to type in their
|
||||
* daily life. Another example is that when the application already knows
|
||||
* that the expected input is not a valid word (e.g. a promotion code that is
|
||||
* not a valid word in any natural language).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Applications need to be aware that the flag is not a guarantee, and some
|
||||
* IMEs may not respect it.</p>
|
||||
*/
|
||||
IME_FLAG_NO_PERSONALIZED_LEARNING = 0x1000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used to request that the IME never go
|
||||
* into fullscreen mode.
|
||||
* By default, IMEs may go into full screen mode when they think
|
||||
* it's appropriate, for example on small screens in landscape
|
||||
* orientation where displaying a software keyboard may occlude
|
||||
* such a large portion of the screen that the remaining part is
|
||||
* too small to meaningfully display the application UI.
|
||||
* If this flag is set, compliant IMEs will never go into full screen mode,
|
||||
* and always leave some space to display the application UI.
|
||||
* Applications need to be aware that the flag is not a guarantee, and
|
||||
* some IMEs may ignore it.
|
||||
*/
|
||||
IME_FLAG_NO_FULLSCREEN = 0x2000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: like {@link #IME_FLAG_NAVIGATE_NEXT}, but
|
||||
* specifies there is something interesting that a backward navigation
|
||||
* can focus on. If the user selects the IME's facility to backward
|
||||
* navigate, this will show up in the application as an {@link
|
||||
* #IME_ACTION_PREVIOUS} at {@link InputConnection#performEditorAction(int)
|
||||
* InputConnection.performEditorAction(int)}.
|
||||
*/
|
||||
IME_FLAG_NAVIGATE_PREVIOUS = 0x4000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used to specify that there is something
|
||||
* interesting that a forward navigation can focus on. This is like using
|
||||
* {@link #IME_ACTION_NEXT}, except allows the IME to be multiline (with
|
||||
* an enter key) as well as provide forward navigation. Note that some
|
||||
* IMEs may not be able to do this, especially when running on a small
|
||||
* screen where there is little space. In that case it does not need to
|
||||
* present a UI for this option. Like {@link #IME_ACTION_NEXT}, if the
|
||||
* user selects the IME's facility to forward navigate, this will show up
|
||||
* in the application at {@link InputConnection#performEditorAction(int)
|
||||
* InputConnection.performEditorAction(int)}.
|
||||
*/
|
||||
IME_FLAG_NAVIGATE_NEXT = 0x8000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used to specify that the IME does not need
|
||||
* to show its extracted text UI. For input methods that may be fullscreen,
|
||||
* often when in landscape mode, this allows them to be smaller and let part
|
||||
* of the application be shown behind, through transparent UI parts in the
|
||||
* fullscreen IME. The part of the UI visible to the user may not be
|
||||
* responsive to touch because the IME will receive touch events, which may
|
||||
* confuse the user; use {@link #IME_FLAG_NO_FULLSCREEN} instead for a better
|
||||
* experience. Using this flag is discouraged and it may become deprecated in
|
||||
* the future. Its meaning is unclear in some situations and it may not work
|
||||
* appropriately on older versions of the platform.
|
||||
*/
|
||||
IME_FLAG_NO_EXTRACT_UI = 0x10000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used in conjunction with one of the actions
|
||||
* masked by {@link #IME_MASK_ACTION}, this indicates that the action
|
||||
* should not be available as an accessory button on the right of the
|
||||
* extracted text when the input method is full-screen. Note that by setting
|
||||
* this flag, there can be cases where the action is simply never available to
|
||||
* the user. Setting this generally means that you think that in fullscreen
|
||||
* mode, where there is little space to show the text, it's not worth taking
|
||||
* some screen real estate to display the action and it should be used instead
|
||||
* to show more text.
|
||||
*/
|
||||
IME_FLAG_NO_ACCESSORY_ACTION = 0x20000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used in conjunction with one of the actions
|
||||
* masked by {@link #IME_MASK_ACTION}. If this flag is not set, IMEs will
|
||||
* normally replace the "enter" key with the action supplied. This flag
|
||||
* indicates that the action should not be available in-line as a replacement
|
||||
* for the "enter" key. Typically this is because the action has such a
|
||||
* significant impact or is not recoverable enough that accidentally hitting
|
||||
* it should be avoided, such as sending a message. Note that
|
||||
* {@link android.widget.TextView} will automatically set this flag for you
|
||||
* on multi-line text views.
|
||||
*/
|
||||
IME_FLAG_NO_ENTER_ACTION = 0x40000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #imeOptions}: used to request an IME that is capable of
|
||||
* inputting ASCII characters. The intention of this flag is to ensure that
|
||||
* the user can type Roman alphabet characters in a {@link
|
||||
* android.widget.TextView}. It is typically used for an account ID or
|
||||
* password input. A lot of the time, IMEs are already able to input ASCII
|
||||
* even without being told so (such IMEs already respect this flag in a
|
||||
* sense), but there are cases when this is not the default. For instance,
|
||||
* users of languages using a different script like Arabic, Greek, Hebrew or
|
||||
* Russian typically have a keyboard that can't input ASCII characters by
|
||||
* default. Applications need to be aware that the flag is not a guarantee,
|
||||
* and some IMEs may not respect it. However, it is strongly recommended for
|
||||
* IME authors to respect this flag especially when their IME could end up
|
||||
* with a state where only languages using non-ASCII are enabled.
|
||||
*/
|
||||
IME_FLAG_FORCE_ASCII = 0x80000000,
|
||||
|
||||
/**
|
||||
* Flag of {@link #internalImeOptions}: flag is set when app window containing
|
||||
* this
|
||||
* {@link EditorInfo} is using {@link Configuration#ORIENTATION_PORTRAIT}
|
||||
* mode.
|
||||
* @hide
|
||||
*/
|
||||
IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT = 0x00000001,
|
||||
|
||||
/**
|
||||
* Generic unspecified type for {@link #imeOptions}.
|
||||
*/
|
||||
IME_NULL = 0x00000000,
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"export_libraries": [],
|
||||
"library_name": null,
|
||||
"android": {
|
||||
"export_libraries": null,
|
||||
"library_name": null
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#define LOG_TAG "GameTextInput"
|
||||
@@ -39,47 +40,52 @@ struct StateClassInfo {
|
||||
|
||||
// Main GameTextInput object.
|
||||
struct GameTextInput {
|
||||
public:
|
||||
GameTextInput(JNIEnv *env, uint32_t max_string_size);
|
||||
public:
|
||||
GameTextInput(JNIEnv* env, uint32_t max_string_size);
|
||||
~GameTextInput();
|
||||
void setState(const GameTextInputState &state);
|
||||
const GameTextInputState &getState() const { return currentState_; }
|
||||
void setState(const GameTextInputState& state);
|
||||
GameTextInputState getState() const {
|
||||
std::lock_guard<std::mutex> lock(currentStateMutex_);
|
||||
return currentState_;
|
||||
}
|
||||
void setInputConnection(jobject inputConnection);
|
||||
void processEvent(jobject textInputEvent);
|
||||
void showIme(uint32_t flags);
|
||||
void hideIme(uint32_t flags);
|
||||
void setEventCallback(GameTextInputEventCallback callback, void *context);
|
||||
jobject stateToJava(const GameTextInputState &state) const;
|
||||
void stateFromJava(jobject textInputEvent,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void *context) const;
|
||||
void setImeInsetsCallback(GameTextInputImeInsetsCallback callback,
|
||||
void *context);
|
||||
void processImeInsets(const ARect *insets);
|
||||
const ARect &getImeInsets() const { return currentInsets_; }
|
||||
void restartInput();
|
||||
void setEventCallback(GameTextInputEventCallback callback, void* context);
|
||||
jobject stateToJava(const GameTextInputState& state) const;
|
||||
void stateFromJava(jobject textInputEvent, GameTextInputGetStateCallback callback,
|
||||
void* context) const;
|
||||
void setImeInsetsCallback(GameTextInputImeInsetsCallback callback, void* context);
|
||||
void processImeInsets(const ARect* insets);
|
||||
const ARect& getImeInsets() const {
|
||||
return currentInsets_;
|
||||
}
|
||||
|
||||
private:
|
||||
private:
|
||||
// Copy string and set other fields
|
||||
void setStateInner(const GameTextInputState &state);
|
||||
static void processCallback(void *context, const GameTextInputState *state);
|
||||
JNIEnv *env_ = nullptr;
|
||||
void setStateInner(const GameTextInputState& state);
|
||||
static void processCallback(void* context, const GameTextInputState* state);
|
||||
JNIEnv* env_ = nullptr;
|
||||
// Cached at initialization from
|
||||
// com/google/androidgamesdk/gametextinput/State.
|
||||
jclass stateJavaClass_ = nullptr;
|
||||
// The latest text input update.
|
||||
GameTextInputState currentState_ = {};
|
||||
// A mutex to protect currentState_.
|
||||
mutable std::mutex currentStateMutex_;
|
||||
// An instance of gametextinput.InputConnection.
|
||||
jclass inputConnectionClass_ = nullptr;
|
||||
jobject inputConnection_ = nullptr;
|
||||
jmethodID inputConnectionSetStateMethod_;
|
||||
jmethodID setSoftKeyboardActiveMethod_;
|
||||
void (*eventCallback_)(void *context,
|
||||
const struct GameTextInputState *state) = nullptr;
|
||||
void *eventCallbackContext_ = nullptr;
|
||||
void (*insetsCallback_)(void *context,
|
||||
const struct ARect *insets) = nullptr;
|
||||
jmethodID restartInputMethod_;
|
||||
void (*eventCallback_)(void* context, const struct GameTextInputState* state) = nullptr;
|
||||
void* eventCallbackContext_ = nullptr;
|
||||
void (*insetsCallback_)(void* context, const struct ARect* insets) = nullptr;
|
||||
ARect currentInsets_ = {};
|
||||
void *insetsCallbackContext_ = nullptr;
|
||||
void* insetsCallbackContext_ = nullptr;
|
||||
StateClassInfo stateClassInfo_ = {};
|
||||
// Constant-sized buffer used to store state text.
|
||||
std::vector<char> stateStringBuffer_;
|
||||
@@ -94,17 +100,14 @@ extern "C" {
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
// Convert to a Java structure.
|
||||
jobject currentState_toJava(const GameTextInput *gameTextInput,
|
||||
const GameTextInputState *state) {
|
||||
jobject currentState_toJava(const GameTextInput* gameTextInput, const GameTextInputState* state) {
|
||||
if (state == nullptr) return NULL;
|
||||
return gameTextInput->stateToJava(*state);
|
||||
}
|
||||
|
||||
// Convert from Java structure.
|
||||
void currentState_fromJava(const GameTextInput *gameTextInput,
|
||||
jobject textInputEvent,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void *context) {
|
||||
void currentState_fromJava(const GameTextInput* gameTextInput, jobject textInputEvent,
|
||||
GameTextInputGetStateCallback callback, void* context) {
|
||||
gameTextInput->stateFromJava(textInputEvent, callback, context);
|
||||
}
|
||||
|
||||
@@ -112,8 +115,7 @@ void currentState_fromJava(const GameTextInput *gameTextInput,
|
||||
/// GameTextInput C Functions
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
struct GameTextInput *GameTextInput_init(JNIEnv *env,
|
||||
uint32_t max_string_size) {
|
||||
struct GameTextInput* GameTextInput_init(JNIEnv* env, uint32_t max_string_size) {
|
||||
if (s_gameTextInput.get() != nullptr) {
|
||||
__android_log_print(ANDROID_LOG_WARN, LOG_TAG,
|
||||
"Warning: called GameTextInput_init twice without "
|
||||
@@ -121,95 +123,91 @@ struct GameTextInput *GameTextInput_init(JNIEnv *env,
|
||||
return s_gameTextInput.get();
|
||||
}
|
||||
// Don't use make_unique, for C++11 compatibility
|
||||
s_gameTextInput =
|
||||
std::unique_ptr<GameTextInput>(new GameTextInput(env, max_string_size));
|
||||
s_gameTextInput = std::unique_ptr<GameTextInput>(new GameTextInput(env, max_string_size));
|
||||
return s_gameTextInput.get();
|
||||
}
|
||||
|
||||
void GameTextInput_destroy(GameTextInput *input) {
|
||||
void GameTextInput_destroy(GameTextInput* input) {
|
||||
if (input == nullptr || s_gameTextInput.get() == nullptr) return;
|
||||
s_gameTextInput.reset();
|
||||
}
|
||||
|
||||
void GameTextInput_setState(GameTextInput *input,
|
||||
const GameTextInputState *state) {
|
||||
void GameTextInput_setState(GameTextInput* input, const GameTextInputState* state) {
|
||||
if (state == nullptr) return;
|
||||
input->setState(*state);
|
||||
}
|
||||
|
||||
void GameTextInput_getState(GameTextInput *input,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void *context) {
|
||||
callback(context, &input->getState());
|
||||
void GameTextInput_getState(GameTextInput* input, GameTextInputGetStateCallback callback,
|
||||
void* context) {
|
||||
GameTextInputState state = input->getState();
|
||||
callback(context, &state);
|
||||
}
|
||||
|
||||
void GameTextInput_setInputConnection(GameTextInput *input,
|
||||
jobject inputConnection) {
|
||||
void GameTextInput_setInputConnection(GameTextInput* input, jobject inputConnection) {
|
||||
input->setInputConnection(inputConnection);
|
||||
}
|
||||
|
||||
void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) {
|
||||
void GameTextInput_processEvent(GameTextInput* input, jobject textInputEvent) {
|
||||
input->processEvent(textInputEvent);
|
||||
}
|
||||
|
||||
void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) {
|
||||
void GameTextInput_processImeInsets(GameTextInput* input, const ARect* insets) {
|
||||
input->processImeInsets(insets);
|
||||
}
|
||||
|
||||
void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) {
|
||||
void GameTextInput_showIme(struct GameTextInput* input, uint32_t flags) {
|
||||
input->showIme(flags);
|
||||
}
|
||||
|
||||
void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) {
|
||||
void GameTextInput_hideIme(struct GameTextInput* input, uint32_t flags) {
|
||||
input->hideIme(flags);
|
||||
}
|
||||
|
||||
void GameTextInput_setEventCallback(struct GameTextInput *input,
|
||||
GameTextInputEventCallback callback,
|
||||
void *context) {
|
||||
void GameTextInput_restartInput(struct GameTextInput* input) {
|
||||
input->restartInput();
|
||||
}
|
||||
|
||||
void GameTextInput_setEventCallback(struct GameTextInput* input,
|
||||
GameTextInputEventCallback callback, void* context) {
|
||||
input->setEventCallback(callback, context);
|
||||
}
|
||||
|
||||
void GameTextInput_setImeInsetsCallback(struct GameTextInput *input,
|
||||
GameTextInputImeInsetsCallback callback,
|
||||
void *context) {
|
||||
void GameTextInput_setImeInsetsCallback(struct GameTextInput* input,
|
||||
GameTextInputImeInsetsCallback callback, void* context) {
|
||||
input->setImeInsetsCallback(callback, context);
|
||||
}
|
||||
|
||||
void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) {
|
||||
void GameTextInput_getImeInsets(const GameTextInput* input, ARect* insets) {
|
||||
*insets = input->getImeInsets();
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
} // extern "C"
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
/// GameTextInput C++ class Implementation
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size)
|
||||
: env_(env),
|
||||
stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE
|
||||
: max_string_size) {
|
||||
GameTextInput::GameTextInput(JNIEnv* env, uint32_t max_string_size)
|
||||
: env_(env),
|
||||
stateStringBuffer_(max_string_size == 0 ? DEFAULT_MAX_STRING_SIZE : max_string_size) {
|
||||
stateJavaClass_ = (jclass)env_->NewGlobalRef(
|
||||
env_->FindClass("com/google/androidgamesdk/gametextinput/State"));
|
||||
inputConnectionClass_ = (jclass)env_->NewGlobalRef(env_->FindClass(
|
||||
"com/google/androidgamesdk/gametextinput/InputConnection"));
|
||||
env_->FindClass("com/google/androidgamesdk/gametextinput/State"));
|
||||
inputConnectionClass_ = (jclass)env_->NewGlobalRef(
|
||||
env_->FindClass("com/google/androidgamesdk/gametextinput/InputConnection"));
|
||||
inputConnectionSetStateMethod_ =
|
||||
env_->GetMethodID(inputConnectionClass_, "setState",
|
||||
"(Lcom/google/androidgamesdk/gametextinput/State;)V");
|
||||
setSoftKeyboardActiveMethod_ = env_->GetMethodID(
|
||||
inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V");
|
||||
env_->GetMethodID(inputConnectionClass_, "setState",
|
||||
"(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;");
|
||||
stateClassInfo_.selectionStart =
|
||||
env_->GetFieldID(stateJavaClass_, "selectionStart", "I");
|
||||
stateClassInfo_.selectionEnd =
|
||||
env_->GetFieldID(stateJavaClass_, "selectionEnd", "I");
|
||||
stateClassInfo_.text = env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;");
|
||||
stateClassInfo_.selectionStart = env_->GetFieldID(stateJavaClass_, "selectionStart", "I");
|
||||
stateClassInfo_.selectionEnd = env_->GetFieldID(stateJavaClass_, "selectionEnd", "I");
|
||||
stateClassInfo_.composingRegionStart =
|
||||
env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I");
|
||||
env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I");
|
||||
stateClassInfo_.composingRegionEnd =
|
||||
env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I");
|
||||
env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I");
|
||||
}
|
||||
|
||||
GameTextInput::~GameTextInput() {
|
||||
@@ -227,16 +225,17 @@ GameTextInput::~GameTextInput() {
|
||||
}
|
||||
}
|
||||
|
||||
void GameTextInput::setState(const GameTextInputState &state) {
|
||||
void GameTextInput::setState(const GameTextInputState& state) {
|
||||
if (inputConnection_ == nullptr) return;
|
||||
jobject jstate = stateToJava(state);
|
||||
env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_,
|
||||
jstate);
|
||||
env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_, jstate);
|
||||
env_->DeleteLocalRef(jstate);
|
||||
setStateInner(state);
|
||||
}
|
||||
|
||||
void GameTextInput::setStateInner(const GameTextInputState &state) {
|
||||
void GameTextInput::setStateInner(const GameTextInputState& state) {
|
||||
std::lock_guard<std::mutex> lock(currentStateMutex_);
|
||||
|
||||
// Check if we're setting using our own string (other parts may be
|
||||
// different)
|
||||
if (state.text_UTF8 == currentState_.text_UTF8) {
|
||||
@@ -244,12 +243,10 @@ void GameTextInput::setStateInner(const GameTextInputState &state) {
|
||||
return;
|
||||
}
|
||||
// Otherwise, copy across the string.
|
||||
auto bytes_needed =
|
||||
std::min(static_cast<uint32_t>(state.text_length + 1),
|
||||
static_cast<uint32_t>(stateStringBuffer_.size()));
|
||||
auto bytes_needed = std::min(static_cast<uint32_t>(state.text_length + 1),
|
||||
static_cast<uint32_t>(stateStringBuffer_.size()));
|
||||
currentState_.text_UTF8 = stateStringBuffer_.data();
|
||||
std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1,
|
||||
stateStringBuffer_.data());
|
||||
std::copy(state.text_UTF8, state.text_UTF8 + bytes_needed - 1, stateStringBuffer_.data());
|
||||
currentState_.text_length = state.text_length;
|
||||
currentState_.selection = state.selection;
|
||||
currentState_.composingRegion = state.composingRegion;
|
||||
@@ -263,15 +260,15 @@ void GameTextInput::setInputConnection(jobject inputConnection) {
|
||||
inputConnection_ = env_->NewGlobalRef(inputConnection);
|
||||
}
|
||||
|
||||
/*static*/ void GameTextInput::processCallback(
|
||||
void *context, const GameTextInputState *state) {
|
||||
auto thiz = static_cast<GameTextInput *>(context);
|
||||
/*static*/ void GameTextInput::processCallback(void* context, const GameTextInputState* state) {
|
||||
auto thiz = static_cast<GameTextInput*>(context);
|
||||
if (state != nullptr) thiz->setStateInner(*state);
|
||||
}
|
||||
|
||||
void GameTextInput::processEvent(jobject textInputEvent) {
|
||||
stateFromJava(textInputEvent, processCallback, this);
|
||||
if (eventCallback_) {
|
||||
std::lock_guard<std::mutex> lock(currentStateMutex_);
|
||||
eventCallback_(eventCallbackContext_, ¤tState_);
|
||||
}
|
||||
}
|
||||
@@ -279,22 +276,20 @@ void GameTextInput::processEvent(jobject textInputEvent) {
|
||||
void GameTextInput::showIme(uint32_t flags) {
|
||||
if (inputConnection_ == nullptr) return;
|
||||
env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true,
|
||||
flags);
|
||||
static_cast<jint>(flags));
|
||||
}
|
||||
|
||||
void GameTextInput::setEventCallback(GameTextInputEventCallback callback,
|
||||
void *context) {
|
||||
void GameTextInput::setEventCallback(GameTextInputEventCallback callback, void* context) {
|
||||
eventCallback_ = callback;
|
||||
eventCallbackContext_ = context;
|
||||
}
|
||||
|
||||
void GameTextInput::setImeInsetsCallback(
|
||||
GameTextInputImeInsetsCallback callback, void *context) {
|
||||
void GameTextInput::setImeInsetsCallback(GameTextInputImeInsetsCallback callback, void* context) {
|
||||
insetsCallback_ = callback;
|
||||
insetsCallbackContext_ = context;
|
||||
}
|
||||
|
||||
void GameTextInput::processImeInsets(const ARect *insets) {
|
||||
void GameTextInput::processImeInsets(const ARect* insets) {
|
||||
currentInsets_ = *insets;
|
||||
if (insetsCallback_) {
|
||||
insetsCallback_(insetsCallbackContext_, ¤tInsets_);
|
||||
@@ -304,21 +299,25 @@ void GameTextInput::processImeInsets(const ARect *insets) {
|
||||
void GameTextInput::hideIme(uint32_t flags) {
|
||||
if (inputConnection_ == nullptr) return;
|
||||
env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false,
|
||||
flags);
|
||||
static_cast<jint>(flags));
|
||||
}
|
||||
|
||||
jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
|
||||
void GameTextInput::restartInput() {
|
||||
if (inputConnection_ == nullptr) return;
|
||||
env_->CallVoidMethod(inputConnection_, restartInputMethod_);
|
||||
}
|
||||
|
||||
jobject GameTextInput::stateToJava(const GameTextInputState& state) const {
|
||||
static jmethodID constructor = nullptr;
|
||||
if (constructor == nullptr) {
|
||||
constructor = env_->GetMethodID(stateJavaClass_, "<init>",
|
||||
"(Ljava/lang/String;IIII)V");
|
||||
constructor = env_->GetMethodID(stateJavaClass_, "<init>", "(Ljava/lang/String;IIII)V");
|
||||
if (constructor == nullptr) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG,
|
||||
"Can't find gametextinput.State constructor");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
const char *text = state.text_UTF8;
|
||||
const char* text = state.text_UTF8;
|
||||
if (text == nullptr) {
|
||||
static char empty_string[] = "";
|
||||
text = empty_string;
|
||||
@@ -326,34 +325,27 @@ jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
|
||||
// Note that this expects 'modified' UTF-8 which is not the same as UTF-8
|
||||
// https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
|
||||
jstring jtext = env_->NewStringUTF(text);
|
||||
jobject jobj =
|
||||
env_->NewObject(stateJavaClass_, constructor, jtext,
|
||||
state.selection.start, state.selection.end,
|
||||
state.composingRegion.start, state.composingRegion.end);
|
||||
jobject jobj = env_->NewObject(stateJavaClass_, constructor, jtext, state.selection.start,
|
||||
state.selection.end, state.composingRegion.start,
|
||||
state.composingRegion.end);
|
||||
env_->DeleteLocalRef(jtext);
|
||||
return jobj;
|
||||
}
|
||||
|
||||
void GameTextInput::stateFromJava(jobject textInputEvent,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void *context) const {
|
||||
jstring text =
|
||||
(jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text);
|
||||
void GameTextInput::stateFromJava(jobject textInputEvent, GameTextInputGetStateCallback callback,
|
||||
void* context) const {
|
||||
jstring text = (jstring)env_->GetObjectField(textInputEvent, stateClassInfo_.text);
|
||||
// Note this is 'modified' UTF-8, not true UTF-8. It has no NULLs in it,
|
||||
// except at the end. It's actually not specified whether the value returned
|
||||
// by GetStringUTFChars includes a null at the end, but it *seems to* on
|
||||
// Android.
|
||||
const char *text_chars = env_->GetStringUTFChars(text, NULL);
|
||||
int text_len = env_->GetStringUTFLength(
|
||||
text); // Length in bytes, *not* including the null.
|
||||
int selectionStart =
|
||||
env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart);
|
||||
int selectionEnd =
|
||||
env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd);
|
||||
const char* text_chars = env_->GetStringUTFChars(text, NULL);
|
||||
int text_len = env_->GetStringUTFLength(text); // Length in bytes, *not* including the null.
|
||||
int selectionStart = env_->GetIntField(textInputEvent, stateClassInfo_.selectionStart);
|
||||
int selectionEnd = env_->GetIntField(textInputEvent, stateClassInfo_.selectionEnd);
|
||||
int composingRegionStart =
|
||||
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart);
|
||||
int composingRegionEnd =
|
||||
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd);
|
||||
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart);
|
||||
int composingRegionEnd = env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd);
|
||||
GameTextInputState state{text_chars,
|
||||
text_len,
|
||||
{selectionStart, selectionEnd},
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "game-text-input",
|
||||
"schema_version": 1,
|
||||
"dependencies": [],
|
||||
"version": "0.0.1",
|
||||
"cpp_files": [
|
||||
"src/game-text-input/gametextinput.cpp"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -xe
|
||||
|
||||
# Copies the native, prefab-src for GameActivity + GameTextInput from the
|
||||
# upstream, android-games-sdk, including our android-activity integration
|
||||
# changes.
|
||||
#
|
||||
# This code is maintained out-of-tree, based on a fork of Google's AGDK repo, so
|
||||
# it's more practical to try and upstream changes we make, or to rebase on new
|
||||
# versions.
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <android-games-sdk dir>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_DIR="$1"
|
||||
TOP_DIR=$(git rev-parse --show-toplevel)
|
||||
DEST_DIR="$TOP_DIR/android-activity/android-games-sdk"
|
||||
|
||||
if [ ! -d "$SOURCE_DIR" ]; then
|
||||
echo "Error: Source directory '$SOURCE_DIR' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$DEST_DIR" ]; then
|
||||
echo "Error: expected find destination directory $DEST_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -fr "$DEST_DIR/game-activity"
|
||||
rm -fr "$DEST_DIR/game-text-input"
|
||||
rm -fr "$DEST_DIR/src/common"
|
||||
rm -fr "$DEST_DIR/include/common"
|
||||
|
||||
mkdir -p "$DEST_DIR/game-activity"
|
||||
mkdir -p "$DEST_DIR/game-text-input"
|
||||
mkdir -p "$DEST_DIR/include/common"
|
||||
mkdir -p "$DEST_DIR/src/common"
|
||||
|
||||
cp -av "$SOURCE_DIR/game-activity/prefab-src" "$DEST_DIR/game-activity"
|
||||
cp -av "$SOURCE_DIR/game-text-input/prefab-src" "$DEST_DIR/game-text-input"
|
||||
cp -av "$SOURCE_DIR/include/common/gamesdk_common.h" "$DEST_DIR/include/common"
|
||||
cp -av "$SOURCE_DIR/src/common/system_utils.h" "$DEST_DIR/src/common"
|
||||
cp -av "$SOURCE_DIR/src/common/system_utils.cpp" "$DEST_DIR/src/common"
|
||||
|
||||
# Remove symlinks so the android-activity crate is easily buildable
|
||||
# from Git on Windows.
|
||||
|
||||
rm "$DEST_DIR/game-activity/prefab-src/modules/game-activity/include/common"
|
||||
rm -fr "$DEST_DIR/game-activity/prefab-src/modules/game-activity/include/game-text-input"
|
||||
rm -fr "$DEST_DIR/game-activity/prefab-src/modules/game-activity/src/common"
|
||||
rm -fr "$DEST_DIR/game-activity/prefab-src/modules/game-activity/src/game-text-input"
|
||||
|
||||
rm "$DEST_DIR/game-text-input/prefab-src/modules/game-text-input/include/common"
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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_STRINGIFY(NUMBER) #NUMBER
|
||||
#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX) \
|
||||
AGDK_STRINGIFY(MAJOR) "." AGDK_STRINGIFY(MINOR) "." AGDK_STRINGIFY(BUGFIX)
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2021 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 "system_utils.h"
|
||||
|
||||
#include <android/api-level.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
#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) {
|
||||
std::string prop = GetSystemProp(key);
|
||||
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
|
||||
}
|
||||
|
||||
bool GetSystemPropAsBool(const char* key, bool default_value) {
|
||||
return GetSystemPropAsInt(key, default_value) != 0;
|
||||
}
|
||||
|
||||
} // namespace gamesdk
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "string"
|
||||
|
||||
namespace gamesdk {
|
||||
|
||||
// Get the value of the given system property
|
||||
std::string GetSystemProp(const char* key, const char* default_value = "");
|
||||
|
||||
// Get the value of the given system property as an integer
|
||||
int GetSystemPropAsInt(const char* key, int default_value = 0);
|
||||
|
||||
// Get the value of the given system property as a bool
|
||||
bool GetSystemPropAsBool(const char* key, bool default_value = false);
|
||||
|
||||
} // namespace gamesdk
|
||||
@@ -1,23 +1,90 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
fn build_glue_for_game_activity() {
|
||||
let android_games_sdk =
|
||||
std::env::var("ANDROID_GAMES_SDK").unwrap_or_else(|_err| "android-games-sdk".to_string());
|
||||
|
||||
let activity_path = |src_inc, name| {
|
||||
format!("{android_games_sdk}/game-activity/prefab-src/modules/game-activity/{src_inc}/game-activity/{name}")
|
||||
};
|
||||
let textinput_path = |src_inc, name| {
|
||||
format!("{android_games_sdk}/game-text-input/prefab-src/modules/game-text-input/{src_inc}/game-text-input/{name}")
|
||||
};
|
||||
|
||||
for f in [
|
||||
"GameActivity.cpp",
|
||||
"GameActivityEvents.cpp",
|
||||
"GameActivityEvents_internal.h",
|
||||
] {
|
||||
println!("cargo:rerun-if-changed={}", activity_path("src", f));
|
||||
}
|
||||
|
||||
for f in [
|
||||
"GameActivity.h",
|
||||
"GameActivityEvents.h",
|
||||
"GameActivityLog.h",
|
||||
] {
|
||||
println!("cargo:rerun-if-changed={}", activity_path("include", f));
|
||||
}
|
||||
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("game-activity-csrc")
|
||||
.file("game-activity-csrc/game-activity/GameActivity.cpp")
|
||||
.include("android-games-sdk/src/common")
|
||||
.file("android-games-sdk/src/common/system_utils.cpp")
|
||||
.extra_warnings(false)
|
||||
.cpp_link_stdlib("c++_static")
|
||||
.compile("libgame_common.a");
|
||||
|
||||
println!("cargo:rerun-if-changed=android-games-sdk/src/common/system_utils.cpp");
|
||||
println!("cargo:rerun-if-changed=android-games-sdk/src/common/system_utils.h");
|
||||
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("android-games-sdk/src/common")
|
||||
.include("android-games-sdk/include")
|
||||
.include("android-games-sdk/game-activity/prefab-src/modules/game-activity/include")
|
||||
.include("android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include")
|
||||
.file(activity_path("src", "GameActivity.cpp"))
|
||||
.file(activity_path("src", "GameActivityEvents.cpp"))
|
||||
.extra_warnings(false)
|
||||
.cpp_link_stdlib("c++_static")
|
||||
.compile("libgame_activity.a");
|
||||
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
textinput_path("include", "gametextinput.h")
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
textinput_path("src", "gametextinput.cpp")
|
||||
);
|
||||
|
||||
cc::Build::new()
|
||||
.cpp(true)
|
||||
.include("game-activity-csrc")
|
||||
.file("game-activity-csrc/game-text-input/gametextinput.cpp")
|
||||
.include("android-games-sdk/src/common")
|
||||
.include("android-games-sdk/include")
|
||||
.include("android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include")
|
||||
.file(textinput_path("src", "gametextinput.cpp"))
|
||||
.cpp_link_stdlib("c++_static")
|
||||
.compile("libgame_text_input.a");
|
||||
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
activity_path("src", "native_app_glue/android_native_app_glue.c")
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
activity_path("include", "native_app_glue/android_native_app_glue.h")
|
||||
);
|
||||
|
||||
cc::Build::new()
|
||||
.include("game-activity-csrc")
|
||||
.include("game-activity-csrc/game-activity/native_app_glue")
|
||||
.file("game-activity-csrc/game-activity/native_app_glue/android_native_app_glue.c")
|
||||
.include("android-games-sdk/src/common")
|
||||
.include("android-games-sdk/include")
|
||||
.include("android-games-sdk/game-activity/prefab-src/modules/game-activity/include")
|
||||
.include("android-games-sdk/game-text-input/prefab-src/modules/game-text-input/include")
|
||||
.include(activity_path("include", ""))
|
||||
.file(activity_path(
|
||||
"src",
|
||||
"native_app_glue/android_native_app_glue.c",
|
||||
))
|
||||
.extra_warnings(false)
|
||||
.cpp_link_stdlib("c++_static")
|
||||
.compile("libnative_app_glue.a");
|
||||
@@ -28,6 +95,21 @@ fn build_glue_for_game_activity() {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "game-activity")]
|
||||
build_glue_for_game_activity();
|
||||
// Enable Cargo's change-detection to avoid re-running build script if
|
||||
// irrelvant parts changed. Using build.rs here is just a dummy used to
|
||||
// disable the default "rerun on every change" behaviour Cargo has.
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
|
||||
if cfg!(feature = "game-activity") {
|
||||
build_glue_for_game_activity();
|
||||
}
|
||||
|
||||
// Whether this is used directly in or as a dependency on docs.rs.
|
||||
//
|
||||
// `cfg(docsrs)` cannot be used, since it's only set for the crate being
|
||||
// built, and not for any dependent crates.
|
||||
println!("cargo:rustc-check-cfg=cfg(used_on_docsrs)");
|
||||
if std::env::var("DOCS_RS").is_ok() {
|
||||
println!("cargo:rustc-cfg=used_on_docsrs");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup game_common Game Common
|
||||
* Common structures and functions used within AGDK
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* The type of a component for which to retrieve insets. See
|
||||
* https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type
|
||||
*/
|
||||
typedef enum GameCommonInsetsType {
|
||||
GAMECOMMON_INSETS_TYPE_CAPTION_BAR = 0,
|
||||
GAMECOMMON_INSETS_TYPE_DISPLAY_CUTOUT,
|
||||
GAMECOMMON_INSETS_TYPE_IME,
|
||||
GAMECOMMON_INSETS_TYPE_MANDATORY_SYSTEM_GESTURES,
|
||||
GAMECOMMON_INSETS_TYPE_NAVIGATION_BARS,
|
||||
GAMECOMMON_INSETS_TYPE_STATUS_BARS,
|
||||
GAMECOMMON_INSETS_TYPE_SYSTEM_BARS,
|
||||
GAMECOMMON_INSETS_TYPE_SYSTEM_GESTURES,
|
||||
GAMECOMMON_INSETS_TYPE_TAPABLE_ELEMENT,
|
||||
GAMECOMMON_INSETS_TYPE_WATERFALL,
|
||||
GAMECOMMON_INSETS_TYPE_COUNT
|
||||
} GameCommonInsetsType;
|
||||
@@ -1,290 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @defgroup game_text_input Game Text Input
|
||||
* The interface to use GameTextInput.
|
||||
* @{
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android/rect.h>
|
||||
#include <jni.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "gamecommon.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* 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
|
||||
* start==end. An undefined span is specified with start = end = SPAN_UNDEFINED.
|
||||
*/
|
||||
typedef struct GameTextInputSpan {
|
||||
/** The start of the region (inclusive). */
|
||||
int32_t start;
|
||||
/** The end of the region (exclusive). */
|
||||
int32_t end;
|
||||
} GameTextInputSpan;
|
||||
|
||||
/**
|
||||
* Values with special meaning in a GameTextInputSpan.
|
||||
*/
|
||||
enum GameTextInputSpanFlag { SPAN_UNDEFINED = -1 };
|
||||
|
||||
/**
|
||||
* This struct holds the state of an editable section of text.
|
||||
* The text can have a selection and a composing region defined on it.
|
||||
* A composing region is used by IMEs that allow input using multiple steps to
|
||||
* compose a glyph or word. Use functions GameTextInput_getState and
|
||||
* GameTextInput_setState to read and modify the state that an IME is editing.
|
||||
*/
|
||||
typedef struct GameTextInputState {
|
||||
/**
|
||||
* Text owned by the state, as a modified UTF-8 string. Null-terminated.
|
||||
* https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
|
||||
*/
|
||||
const char *text_UTF8;
|
||||
/**
|
||||
* Length in bytes of text_UTF8, *not* including the null at end.
|
||||
*/
|
||||
int32_t text_length;
|
||||
/**
|
||||
* A selection defined on the text.
|
||||
*/
|
||||
GameTextInputSpan selection;
|
||||
/**
|
||||
* A composing region defined on the text.
|
||||
*/
|
||||
GameTextInputSpan composingRegion;
|
||||
} GameTextInputState;
|
||||
|
||||
/**
|
||||
* A callback called by GameTextInput_getState.
|
||||
* @param context User-defined context.
|
||||
* @param state State, owned by the library, that will be valid for the duration
|
||||
* of the callback.
|
||||
*/
|
||||
typedef void (*GameTextInputGetStateCallback)(
|
||||
void *context, const struct GameTextInputState *state);
|
||||
|
||||
/**
|
||||
* Opaque handle to the GameTextInput API.
|
||||
*/
|
||||
typedef struct GameTextInput GameTextInput;
|
||||
|
||||
/**
|
||||
* Initialize the GameTextInput library.
|
||||
* If called twice without GameTextInput_destroy being called, the same pointer
|
||||
* will be returned and a warning will be issued.
|
||||
* @param env A JNI env valid on the calling thread.
|
||||
* @param max_string_size The maximum length of a string that can be edited. If
|
||||
* zero, the maximum defaults to 65536 bytes. A buffer of this size is allocated
|
||||
* at initialization.
|
||||
* @return A handle to the library.
|
||||
*/
|
||||
GameTextInput *GameTextInput_init(JNIEnv *env, uint32_t max_string_size);
|
||||
|
||||
/**
|
||||
* When using GameTextInput, you need to create a gametextinput.InputConnection
|
||||
* on the Java side and pass it using this function to the library, unless using
|
||||
* GameActivity in which case this will be done for you. See the GameActivity
|
||||
* source code or GameTextInput samples for examples of usage.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param inputConnection A gametextinput.InputConnection object.
|
||||
*/
|
||||
void GameTextInput_setInputConnection(GameTextInput *input,
|
||||
jobject inputConnection);
|
||||
|
||||
/**
|
||||
* Unless using GameActivity, it is required to call this function from your
|
||||
* Java gametextinput.Listener.stateChanged method to convert eventState and
|
||||
* trigger any event callbacks. When using GameActivity, this does not need to
|
||||
* be called as event processing is handled by the Activity.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param eventState A Java gametextinput.State object.
|
||||
*/
|
||||
void GameTextInput_processEvent(GameTextInput *input, jobject eventState);
|
||||
|
||||
/**
|
||||
* Free any resources owned by the GameTextInput library.
|
||||
* Any subsequent calls to the library will fail until GameTextInput_init is
|
||||
* called again.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
*/
|
||||
void GameTextInput_destroy(GameTextInput *input);
|
||||
|
||||
/**
|
||||
* Flags to be passed to GameTextInput_showIme.
|
||||
*/
|
||||
enum ShowImeFlags {
|
||||
SHOW_IME_UNDEFINED = 0, // Default value.
|
||||
SHOW_IMPLICIT =
|
||||
1, // Indicates that the user has forced the input method open so it
|
||||
// should not be closed until they explicitly do so.
|
||||
SHOW_FORCED = 2 // Indicates that this is an implicit request to show the
|
||||
// input window, not as the result of a direct request by
|
||||
// the user. The window may not be shown in this case.
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the IME. Calls InputMethodManager.showSoftInput().
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param flags Defined in ShowImeFlags above. For more information see:
|
||||
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
|
||||
*/
|
||||
void GameTextInput_showIme(GameTextInput *input, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Flags to be passed to GameTextInput_hideIme.
|
||||
*/
|
||||
enum HideImeFlags {
|
||||
HIDE_IME_UNDEFINED = 0, // Default value.
|
||||
HIDE_IMPLICIT_ONLY =
|
||||
1, // Indicates that the soft input window should only be hidden if it
|
||||
// was not explicitly shown by the user.
|
||||
HIDE_NOT_ALWAYS =
|
||||
2, // Indicates that the soft input window should normally be hidden,
|
||||
// unless it was originally shown with SHOW_FORCED.
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the IME. Calls InputMethodManager.hideSoftInputFromWindow().
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param flags Defined in HideImeFlags above. For more information see:
|
||||
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager
|
||||
*/
|
||||
void GameTextInput_hideIme(GameTextInput *input, uint32_t flags);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* callback rather than returning the state in order to simplify ownership of
|
||||
* text_UTF8 strings. These strings are only valid during the calling of the
|
||||
* callback.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param callback A function that will be called with valid state.
|
||||
* @param context Context used by the callback.
|
||||
*/
|
||||
void GameTextInput_getState(GameTextInput *input,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void *context);
|
||||
|
||||
/**
|
||||
* Set the current GameTextInput state. This state is reflected to any active
|
||||
* IME.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param state The state to set. Ownership is maintained by the caller and must
|
||||
* remain valid for the duration of the call.
|
||||
*/
|
||||
void GameTextInput_setState(GameTextInput *input,
|
||||
const GameTextInputState *state);
|
||||
|
||||
/**
|
||||
* Type of the callback needed by GameTextInput_setEventCallback that will be
|
||||
* called every time the IME state changes.
|
||||
* @param context User-defined context set in GameTextInput_setEventCallback.
|
||||
* @param current_state Current IME state, owned by the library and valid during
|
||||
* the callback.
|
||||
*/
|
||||
typedef void (*GameTextInputEventCallback)(
|
||||
void *context, const GameTextInputState *current_state);
|
||||
|
||||
/**
|
||||
* Optionally set a callback to be called whenever the IME state changes.
|
||||
* Not necessary if you are using GameActivity, which handles these callbacks
|
||||
* for you.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param callback Called by the library when the IME state changes.
|
||||
* @param context Context passed as first argument to the callback.
|
||||
*/
|
||||
void GameTextInput_setEventCallback(GameTextInput *input,
|
||||
GameTextInputEventCallback callback,
|
||||
void *context);
|
||||
|
||||
/**
|
||||
* Type of the callback needed by GameTextInput_setImeInsetsCallback that will
|
||||
* be called every time the IME window insets change.
|
||||
* @param context User-defined context set in
|
||||
* GameTextInput_setImeWIndowInsetsCallback.
|
||||
* @param current_insets Current IME insets, owned by the library and valid
|
||||
* during the callback.
|
||||
*/
|
||||
typedef void (*GameTextInputImeInsetsCallback)(void *context,
|
||||
const ARect *current_insets);
|
||||
|
||||
/**
|
||||
* Optionally set a callback to be called whenever the IME insets change.
|
||||
* Not necessary if you are using GameActivity, which handles these callbacks
|
||||
* for you.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param callback Called by the library when the IME insets change.
|
||||
* @param context Context passed as first argument to the callback.
|
||||
*/
|
||||
void GameTextInput_setImeInsetsCallback(GameTextInput *input,
|
||||
GameTextInputImeInsetsCallback callback,
|
||||
void *context);
|
||||
|
||||
/**
|
||||
* Get the current window insets for the IME.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param insets Filled with the current insets by this function.
|
||||
*/
|
||||
void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets);
|
||||
|
||||
/**
|
||||
* Unless using GameActivity, it is required to call this function from your
|
||||
* Java gametextinput.Listener.onImeInsetsChanged method to
|
||||
* trigger any event callbacks. When using GameActivity, this does not need to
|
||||
* be called as insets processing is handled by the Activity.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param eventState A Java gametextinput.State object.
|
||||
*/
|
||||
void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets);
|
||||
|
||||
/**
|
||||
* Convert a GameTextInputState struct to a Java gametextinput.State object.
|
||||
* Don't forget to delete the returned Java local ref when you're done.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param state Input state to convert.
|
||||
* @return A Java object of class gametextinput.State. The caller is required to
|
||||
* delete this local reference.
|
||||
*/
|
||||
jobject GameTextInputState_toJava(const GameTextInput *input,
|
||||
const GameTextInputState *state);
|
||||
|
||||
/**
|
||||
* Convert from a Java gametextinput.State object into a C GameTextInputState
|
||||
* struct.
|
||||
* @param input A valid GameTextInput library handle.
|
||||
* @param state A Java gametextinput.State object.
|
||||
* @param callback A function called with the C struct, valid for the duration
|
||||
* of the call.
|
||||
* @param context Context passed to the callback.
|
||||
*/
|
||||
void GameTextInputState_fromJava(const GameTextInput *input, jobject state,
|
||||
GameTextInputGetStateCallback callback,
|
||||
void *context);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
@@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
# First install bindgen-cli via `cargo install bindgen-cli`
|
||||
|
||||
SDK_DIR="${ANDROID_GAMES_SDK:-android-games-sdk}"
|
||||
if test -z "${ANDROID_NDK_ROOT}"; then
|
||||
export ANDROID_NDK_ROOT=${ANDROID_NDK_HOME}
|
||||
fi
|
||||
@@ -12,6 +15,7 @@ while read ARCH && read TARGET ; do
|
||||
|
||||
# --module-raw-line 'use '
|
||||
bindgen game-activity-ffi.h -o src/game_activity/ffi_$ARCH.rs \
|
||||
--rust-target '1.85.0' \
|
||||
--blocklist-item 'JNI\w+' \
|
||||
--blocklist-item 'C?_?JNIEnv' \
|
||||
--blocklist-item '_?JavaVM' \
|
||||
@@ -34,7 +38,9 @@ while read ARCH && read TARGET ; do
|
||||
--blocklist-function 'GameActivity_onCreate_C' \
|
||||
--newtype-enum '\w+_(result|status)_t' \
|
||||
-- \
|
||||
-Igame-activity-csrc \
|
||||
"-I$SDK_DIR/game-activity/prefab-src/modules/game-activity/include" \
|
||||
"-I$SDK_DIR/game-text-input/prefab-src/modules/game-text-input/include" \
|
||||
"-I$SDK_DIR/include" \
|
||||
--sysroot="$SYSROOT" --target=$TARGET
|
||||
|
||||
done << EOF
|
||||
|
||||
@@ -6,13 +6,14 @@ use ndk::configuration::{
|
||||
ScreenSize, Touchscreen, UiModeNight, UiModeType,
|
||||
};
|
||||
|
||||
/// A (cheaply clonable) reference to this application's [`ndk::configuration::Configuration`]
|
||||
/// A runtime-replacable reference to [`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.
|
||||
/// # Warning
|
||||
///
|
||||
/// If the application is notified of configuration changes then those changes
|
||||
/// will become visible via pre-existing configuration references.
|
||||
/// The value held by this reference **will change** with every [`super::MainEvent::ConfigChanged`]
|
||||
/// event that is raised. You should **not** [`Clone`] this type to compare it against a
|
||||
/// "new" [`super::AndroidApp::config()`] when that event is raised, since both point to the same
|
||||
/// internal [`ndk::configuration::Configuration`] and will be identical.
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigurationRef {
|
||||
config: Arc<RwLock<Configuration>>,
|
||||
@@ -28,8 +29,6 @@ impl PartialEq for ConfigurationRef {
|
||||
}
|
||||
}
|
||||
impl Eq for ConfigurationRef {}
|
||||
unsafe impl Send for ConfigurationRef {}
|
||||
unsafe impl Sync for ConfigurationRef {}
|
||||
|
||||
impl fmt::Debug for ConfigurationRef {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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),
|
||||
// For internal errors that don't lead to a Java exception but are
|
||||
// still JNI related.
|
||||
#[error("A bad argument was passed to a JNI method: {0}")]
|
||||
JniBadArgument(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::JniBadArgument(msg) => AppError::JavaError(msg),
|
||||
InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
|
||||
InternalAppError::InputUnavailable => AppError::InputUnavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,21 +12,18 @@
|
||||
#![allow(deref_nullptr)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use jni_sys::*;
|
||||
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t, size_t};
|
||||
use jni::sys::*;
|
||||
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t};
|
||||
use ndk_sys::{AAssetManager, AConfiguration, ALooper, ALooper_callbackFunc, ANativeWindow, ARect};
|
||||
|
||||
#[cfg(all(
|
||||
any(target_os = "android", feature = "test"),
|
||||
any(target_arch = "arm", target_arch = "armv7")
|
||||
))]
|
||||
#[cfg(all(any(target_os = "android"), target_arch = "arm"))]
|
||||
include!("ffi_arm.rs");
|
||||
|
||||
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "aarch64"))]
|
||||
#[cfg(all(any(target_os = "android"), target_arch = "aarch64"))]
|
||||
include!("ffi_aarch64.rs");
|
||||
|
||||
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86"))]
|
||||
#[cfg(all(any(target_os = "android"), target_arch = "x86"))]
|
||||
include!("ffi_i686.rs");
|
||||
|
||||
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86_64"))]
|
||||
#[cfg(all(any(target_os = "android"), target_arch = "x86_64"))]
|
||||
include!("ffi_x86_64.rs");
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
use jni::{
|
||||
jni_sig, jni_str,
|
||||
objects::{JObject, JString, JThread},
|
||||
vm::JavaVM,
|
||||
};
|
||||
use log::Level;
|
||||
use ndk::asset::AssetManager;
|
||||
use std::{
|
||||
ffi::{c_void, CStr, CString},
|
||||
fs::File,
|
||||
io::{BufRead as _, BufReader},
|
||||
os::fd::{FromRawFd as _, RawFd},
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
main_callbacks::MainCallbacks, util::android_log, OnCreateState, ANDROID_ACTIVITY_TAG,
|
||||
};
|
||||
|
||||
fn forward_stdio_to_logcat() -> std::thread::JoinHandle<std::io::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 || -> std::io::Result<()> {
|
||||
let tag = c"RustStdoutStderr";
|
||||
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) => {
|
||||
android_log(
|
||||
Level::Error,
|
||||
ANDROID_ACTIVITY_TAG,
|
||||
&CString::new(format!(
|
||||
"Logcat forwarder failed to read stdin/stderr: {e:?}"
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
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")
|
||||
}
|
||||
|
||||
unsafe extern "C" fn _android_activity_anchor() {}
|
||||
|
||||
/// Get a handle to the shared library that we are linked into, so that we can
|
||||
/// look up symbols within it.
|
||||
fn dlopen_self() -> Result<*mut c_void, String> {
|
||||
unsafe {
|
||||
let mut info: libc::Dl_info = std::mem::zeroed();
|
||||
|
||||
// NB: `dladdr` does not update the `dlerror` state
|
||||
if libc::dladdr(
|
||||
_android_activity_anchor as *const () as *const c_void,
|
||||
&mut info,
|
||||
) == 0
|
||||
{
|
||||
return Err("dladdr failed".into());
|
||||
}
|
||||
if info.dli_fname.is_null() {
|
||||
return Err("dladdr returned null dli_fname".into());
|
||||
}
|
||||
|
||||
// Clear any existing error
|
||||
libc::dlerror();
|
||||
|
||||
let handle = libc::dlopen(info.dli_fname, libc::RTLD_NOW | libc::RTLD_NOLOAD);
|
||||
if handle.is_null() {
|
||||
let err = CStr::from_ptr(libc::dlerror())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let path = CStr::from_ptr(info.dli_fname)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
return Err(format!("dlopen({path}) failed: {err}"));
|
||||
}
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a symbol within our own shared library
|
||||
///
|
||||
/// This can be used to look up optional application entry points, such as
|
||||
/// `android_on_create`
|
||||
///
|
||||
/// Returns `None` if the symbol is not found (which is not considered an error)
|
||||
fn lookup_self_symbol(symbol: &CStr) -> Option<*mut c_void> {
|
||||
unsafe {
|
||||
let handle = match dlopen_self() {
|
||||
Ok(h) => h,
|
||||
Err(err) => {
|
||||
let msg = format!(
|
||||
"Warning: failed to dlopen self, looking for symbol {}: {err}",
|
||||
symbol.to_string_lossy()
|
||||
);
|
||||
android_log(
|
||||
Level::Warn,
|
||||
ANDROID_ACTIVITY_TAG,
|
||||
&CString::new(msg).unwrap(),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Clear any existing error
|
||||
libc::dlerror();
|
||||
|
||||
let sym = libc::dlsym(handle, symbol.as_ptr());
|
||||
|
||||
// Close the handle to avoid leaking a reference count
|
||||
if libc::dlclose(handle) != 0 {
|
||||
let err = CStr::from_ptr(libc::dlerror())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let msg = format!("dlclose failed for self handle: {err}");
|
||||
android_log(
|
||||
Level::Warn,
|
||||
ANDROID_ACTIVITY_TAG,
|
||||
&CString::new(msg).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if sym.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(sym)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to call an optional "android_on_create" entry point within the
|
||||
/// application's shared library
|
||||
///
|
||||
/// Note: this function does not propagate any errors, while it's assumed that
|
||||
/// this is called within an `onCreate` native method.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - This must be called from the Java main thread, while onCreate is running
|
||||
/// - The `jni_activity` pointer must be a valid JNI reference to the Java
|
||||
/// Activity instance being created
|
||||
///
|
||||
/// The safety here also depends on the application declaring an
|
||||
/// `android_on_create` function with the correct signature. (It's safe to not
|
||||
/// declare an `android_on_create` function at all, and the code will simply
|
||||
/// skip calling it)
|
||||
pub(crate) unsafe fn init_java_main_thread_on_create(
|
||||
jvm: JavaVM,
|
||||
jni_activity: *mut c_void,
|
||||
saved_state: &[u8],
|
||||
) {
|
||||
let _join_log_forwarder = forward_stdio_to_logcat();
|
||||
|
||||
let msg = CString::new(format!(
|
||||
"Creating: Activity = {:p}, saved state size = {}",
|
||||
jni_activity,
|
||||
saved_state.len()
|
||||
))
|
||||
.unwrap();
|
||||
android_log(Level::Info, ANDROID_ACTIVITY_TAG, &msg);
|
||||
|
||||
// SAFETY: It's the application's responsibility to declare any `android_on_create`
|
||||
// function with the correct signature and ABI.
|
||||
let android_on_create: extern "Rust" fn(state: &OnCreateState) = unsafe {
|
||||
let Some(symbol) = lookup_self_symbol(c"android_on_create") else {
|
||||
// android_on_create is optional, so simply return if not found
|
||||
return;
|
||||
};
|
||||
std::mem::transmute(symbol)
|
||||
};
|
||||
|
||||
let state = OnCreateState::new(jvm.clone(), jni_activity, saved_state);
|
||||
// Catch any exceptions from the callback and log them instead of allowing any
|
||||
// exception to propagate back to the Activity.
|
||||
let res = jvm.attach_current_thread(|_env| -> jni::errors::Result<()> {
|
||||
android_on_create(&state);
|
||||
Ok(())
|
||||
});
|
||||
if let Err(err) = res {
|
||||
let msg = CString::new(format!(
|
||||
"JNI error while running android_on_create: {:?}",
|
||||
err
|
||||
))
|
||||
.unwrap();
|
||||
android_log(Level::Error, ANDROID_ACTIVITY_TAG, &msg);
|
||||
}
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
main_callbacks: MainCallbacks,
|
||||
app_asset_manager: AssetManager,
|
||||
}
|
||||
|
||||
static APP_ONCE: OnceLock<AppState> = OnceLock::new();
|
||||
|
||||
// Get the Application instance from the Activity
|
||||
fn get_application<'local, 'any>(
|
||||
env: &mut jni::Env<'local>,
|
||||
activity: &JObject<'any>,
|
||||
) -> jni::errors::Result<JObject<'local>> {
|
||||
let app = env
|
||||
.call_method(
|
||||
activity,
|
||||
jni_str!("getApplication"),
|
||||
jni_sig!(() -> android.app.Application),
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
fn get_assets<'local, 'any>(
|
||||
env: &mut jni::Env<'local>,
|
||||
application: &JObject<'any>,
|
||||
) -> jni::errors::Result<JObject<'local>> {
|
||||
let assets_manager = env
|
||||
.call_method(
|
||||
application,
|
||||
jni_str!("getAssets"),
|
||||
jni_sig!(() -> android.content.res.AssetManager),
|
||||
&[],
|
||||
)?
|
||||
.l()?;
|
||||
Ok(assets_manager)
|
||||
}
|
||||
|
||||
fn try_init_current_thread(env: &mut jni::Env, activity: &JObject) -> jni::errors::Result<()> {
|
||||
let activity_class = env.get_object_class(activity)?;
|
||||
let class_loader = activity_class.get_class_loader(env)?;
|
||||
|
||||
let thread = JThread::current_thread(env)?;
|
||||
thread.set_context_class_loader(env, &class_loader)?;
|
||||
let thread_name = JString::from_jni_str(env, jni_str!("android_main"))?;
|
||||
thread.set_name(env, &thread_name)?;
|
||||
|
||||
// Also name native thread - this needs to happen here after attaching to a JVM thread,
|
||||
// since that changes the thread name to something like "Thread-2".
|
||||
unsafe {
|
||||
let thread_name = c"android_main";
|
||||
let _ = libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Name the Java Thread + native thread "android_main" and set the Java Thread context class loader
|
||||
/// so that jni code can more-easily find non-system Java classes.
|
||||
pub(crate) fn init_android_main_thread(
|
||||
vm: &JavaVM,
|
||||
jni_activity: &JObject,
|
||||
java_main_looper: &ndk::looper::ForeignLooper,
|
||||
) -> jni::errors::Result<(AssetManager, MainCallbacks)> {
|
||||
vm.with_local_frame(10, |env| -> jni::errors::Result<_> {
|
||||
let app_state = APP_ONCE.get_or_init(|| unsafe {
|
||||
let application =
|
||||
get_application(env, jni_activity).expect("Failed to get Application instance");
|
||||
let app_asset_manager =
|
||||
get_assets(env, &application).expect("Failed to get AssetManager");
|
||||
let app_global = env
|
||||
.new_global_ref(application)
|
||||
.expect("Failed to create global ref for Application");
|
||||
// Make sure we don't delete the global reference via Drop
|
||||
let app_global = app_global.into_raw();
|
||||
ndk_context::initialize_android_context(vm.get_raw().cast(), app_global.cast());
|
||||
|
||||
let asset_manager_global = env
|
||||
.new_global_ref(app_asset_manager)
|
||||
.expect("Failed to create global ref for AssetManager");
|
||||
// Make sure we don't delete the global reference via Drop because
|
||||
// the AAssetManager pointer will only be valid while we can
|
||||
// guarantee that the Java AssetManager is not garbage collected
|
||||
let asset_manager_global = asset_manager_global.into_raw();
|
||||
let asset_manager_ptr =
|
||||
ndk_sys::AAssetManager_fromJava(env.get_raw() as _, asset_manager_global as _);
|
||||
assert_ne!(
|
||||
asset_manager_ptr,
|
||||
std::ptr::null_mut(),
|
||||
"Failed to get Application AAssetManager"
|
||||
);
|
||||
let app_asset_manager =
|
||||
AssetManager::from_ptr(std::ptr::NonNull::new(asset_manager_ptr).unwrap());
|
||||
|
||||
let main_callbacks = MainCallbacks::new(java_main_looper);
|
||||
|
||||
AppState {
|
||||
main_callbacks,
|
||||
app_asset_manager,
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(err) = try_init_current_thread(env, jni_activity) {
|
||||
let msg =
|
||||
CString::new(format!("Failed to initialize Java thread state: {:?}", err)).unwrap();
|
||||
android_log(Level::Error, ANDROID_ACTIVITY_TAG, &msg);
|
||||
}
|
||||
|
||||
let asset_manager = unsafe { AssetManager::from_ptr(app_state.app_asset_manager.ptr()) };
|
||||
let main_callbacks = app_state.main_callbacks.clone();
|
||||
|
||||
Ok((asset_manager, main_callbacks))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
use jni::sys::jint;
|
||||
use jni::{objects::Global, JavaVM};
|
||||
|
||||
use crate::error::{AppError, InternalAppError, InternalResult};
|
||||
use crate::input::{Keycode, MetaState};
|
||||
use crate::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),
|
||||
}
|
||||
|
||||
jni::bind_java_type! {
|
||||
pub(crate) AKeyCharacterMap => "android.view.KeyCharacterMap",
|
||||
methods {
|
||||
priv fn _get(key_code: jint, meta_state: jint) -> jint,
|
||||
priv static fn _get_dead_char(accent_char: jint, base_char: jint) -> jint,
|
||||
priv fn _get_keyboard_type() -> jint,
|
||||
}
|
||||
}
|
||||
|
||||
impl AKeyCharacterMap<'_> {
|
||||
pub(crate) fn get(
|
||||
&self,
|
||||
env: &mut jni::Env,
|
||||
key_code: jint,
|
||||
meta_state: jint,
|
||||
) -> Result<jint, InternalAppError> {
|
||||
self._get(env, key_code, meta_state)
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
|
||||
}
|
||||
|
||||
pub(crate) fn get_dead_char(
|
||||
env: &mut jni::Env,
|
||||
accent_char: jint,
|
||||
base_char: jint,
|
||||
) -> Result<jint, InternalAppError> {
|
||||
Self::_get_dead_char(env, accent_char, base_char)
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
|
||||
}
|
||||
|
||||
pub(crate) fn get_keyboard_type(&self, env: &mut jni::Env) -> Result<jint, InternalAppError> {
|
||||
self._get_keyboard_type(env)
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
|
||||
}
|
||||
}
|
||||
|
||||
jni::bind_java_type! {
|
||||
pub(crate) AInputDevice => "android.view.InputDevice",
|
||||
type_map {
|
||||
AKeyCharacterMap => "android.view.KeyCharacterMap",
|
||||
},
|
||||
methods {
|
||||
static fn get_device(id: jint) -> AInputDevice,
|
||||
fn get_key_character_map() -> AKeyCharacterMap,
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the keys provided by a keyboard device and their associated labels.
|
||||
#[derive(Debug)]
|
||||
pub struct KeyCharacterMap {
|
||||
jvm: JavaVM,
|
||||
key_map: Global<AKeyCharacterMap<'static>>,
|
||||
}
|
||||
impl Clone for KeyCharacterMap {
|
||||
fn clone(&self) -> Self {
|
||||
let jvm = self.jvm.clone();
|
||||
jvm.attach_current_thread(|env| -> jni::errors::Result<_> {
|
||||
Ok(Self {
|
||||
jvm: jvm.clone(),
|
||||
key_map: env.new_global_ref(&self.key_map)?,
|
||||
})
|
||||
})
|
||||
.expect("Failed to attach thread to JVM and clone key map")
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyCharacterMap {
|
||||
pub(crate) fn new(jvm: JavaVM, key_map: Global<AKeyCharacterMap<'static>>) -> Self {
|
||||
Self { jvm, 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;
|
||||
|
||||
let vm = self.jvm.clone();
|
||||
vm.attach_current_thread(|env| -> InternalResult<_> {
|
||||
let unicode = self.key_map.get(env, 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)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.map_err(|err| {
|
||||
let err: InternalAppError = err;
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
let vm = self.jvm.clone();
|
||||
vm.attach_current_thread(|env| -> InternalResult<_> {
|
||||
let unicode = AKeyCharacterMap::get_dead_char(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) })
|
||||
})
|
||||
})
|
||||
.map_err(|err| {
|
||||
let err: InternalAppError = err;
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let vm = self.jvm.clone();
|
||||
vm.attach_current_thread(|env| -> InternalResult<_> {
|
||||
let keyboard_type = self.key_map.get_keyboard_type(env)?;
|
||||
let keyboard_type = keyboard_type as u32;
|
||||
Ok(keyboard_type.into())
|
||||
})
|
||||
.map_err(|err| {
|
||||
let err: InternalAppError = err;
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn device_key_character_map_with_env(
|
||||
env: &mut jni::Env<'_>,
|
||||
device_id: i32,
|
||||
) -> jni::errors::Result<KeyCharacterMap> {
|
||||
let device = AInputDevice::get_device(env, device_id)?;
|
||||
if device.is_null() {
|
||||
// This isn't really an error from a JNI perspective but we would only expect
|
||||
// this to return null for a device ID of zero or an invalid device ID.
|
||||
log::error!("No input device with id {}", device_id);
|
||||
return Err(jni::errors::Error::WrongObjectType);
|
||||
}
|
||||
let character_map = device.get_key_character_map(env)?;
|
||||
let character_map = env.new_global_ref(character_map)?;
|
||||
let jvm = JavaVM::singleton().expect("Failed to get singleton JavaVM");
|
||||
Ok(KeyCharacterMap::new(jvm, character_map))
|
||||
}
|
||||
|
||||
pub(crate) fn device_key_character_map(
|
||||
jvm: JavaVM,
|
||||
device_id: i32,
|
||||
) -> InternalResult<KeyCharacterMap> {
|
||||
jvm.attach_current_thread(|env| {
|
||||
if device_id == 0 {
|
||||
return Err(InternalAppError::JniBadArgument(
|
||||
"Can't get key character map for non-physical device_id 0".into(),
|
||||
));
|
||||
}
|
||||
device_key_character_map_with_env(env, device_id)
|
||||
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! 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 crate::error::InternalAppError;
|
||||
|
||||
/// 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::Env<'_>,
|
||||
err: jni::errors::Error,
|
||||
) -> InternalAppError {
|
||||
if matches!(err, jni::errors::Error::JavaException) {
|
||||
env.exception_catch()
|
||||
.expect_err("Spurious JavaException error with no exception to catch")
|
||||
} else {
|
||||
err
|
||||
}
|
||||
.into()
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
use jni::vm::JavaVM;
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
panic::{catch_unwind, AssertUnwindSafe},
|
||||
sync::{atomic::AtomicBool, Arc, Mutex, Weak},
|
||||
};
|
||||
|
||||
use crate::util::abort_on_panic;
|
||||
|
||||
struct CallbackBuffers {
|
||||
pub front: Vec<Box<dyn FnOnce() + Send>>,
|
||||
pub back: Vec<Box<dyn FnOnce() + Send>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CallbackBuffers {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CallbackBuffers")
|
||||
.field("front", &self.front.len())
|
||||
.field("back", &self.back.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl CallbackBuffers {
|
||||
pub fn take_front(&mut self) -> Vec<Box<dyn FnOnce() + Send>> {
|
||||
std::mem::swap(&mut self.front, &mut self.back);
|
||||
std::mem::take(&mut self.back)
|
||||
}
|
||||
|
||||
// After calling `take_front` and draining callbacks then the empty
|
||||
// vec should be put back so the capacity can be reused
|
||||
//
|
||||
// The given `back` vector must be empty
|
||||
pub fn replace_back(&mut self, back: Vec<Box<dyn FnOnce() + Send>>) {
|
||||
assert!(back.is_empty());
|
||||
self.back = back;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MainCallbacksState {
|
||||
_pending_detach: AtomicBool,
|
||||
event_fd: libc::c_int,
|
||||
callbacks: Mutex<CallbackBuffers>,
|
||||
}
|
||||
|
||||
impl Drop for MainCallbacksState {
|
||||
fn drop(&mut self) {
|
||||
eprintln!("Dropping MainCallbacksState");
|
||||
log::warn!("Dropping MainCallbacksState");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MainCallbacks {
|
||||
inner: Arc<MainCallbacksState>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for MainCallbacks {
|
||||
type Target = MainCallbacksState;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl MainCallbacks {
|
||||
pub fn new(java_main_looper: &ndk::looper::ForeignLooper) -> Self {
|
||||
let java_main_callbacks_event_fd =
|
||||
unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) };
|
||||
assert_ne!(
|
||||
java_main_callbacks_event_fd, -1,
|
||||
"Failed to create Java main looper event fd"
|
||||
);
|
||||
|
||||
let inner = Arc::new(MainCallbacksState {
|
||||
_pending_detach: AtomicBool::new(false),
|
||||
event_fd: java_main_callbacks_event_fd,
|
||||
callbacks: Mutex::new(CallbackBuffers {
|
||||
front: Vec::new(),
|
||||
back: Vec::new(),
|
||||
}),
|
||||
});
|
||||
|
||||
let weak = Arc::downgrade(&inner);
|
||||
let weak = weak.into_raw();
|
||||
unsafe {
|
||||
ndk_sys::ALooper_addFd(
|
||||
java_main_looper.ptr().as_ptr(),
|
||||
java_main_callbacks_event_fd,
|
||||
ndk_sys::ALOOPER_POLL_CALLBACK,
|
||||
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
|
||||
Some(run_java_main_callbacks),
|
||||
weak as _,
|
||||
);
|
||||
}
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub fn wake_java_main_for_callbacks(&self) {
|
||||
let count: u64 = 1;
|
||||
|
||||
loop {
|
||||
match unsafe {
|
||||
libc::write(self.event_fd, &count as *const _ as *const libc::c_void, 8)
|
||||
} {
|
||||
8 => break,
|
||||
-1 => {
|
||||
let err = std::io::Error::last_os_error();
|
||||
if err.kind() != std::io::ErrorKind::Interrupted {
|
||||
log::error!("Failure waking up java main loop: {}", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
count => {
|
||||
log::error!("Spurious write of {count} bytes while waking up java main loop");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_on_java_main_thread<F>(&self, f: Box<F>)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
{
|
||||
let mut guard = self.callbacks.lock().unwrap();
|
||||
guard.front.push(f);
|
||||
}
|
||||
|
||||
self.wake_java_main_for_callbacks();
|
||||
}
|
||||
|
||||
// Asynchronously detach the callbacks event fd from the Java main looper
|
||||
//
|
||||
// Note: we can't do this synchronously because ALooper_removeFd can't
|
||||
// guarantee that there isn't already a callback pending (which will still
|
||||
// require a valid data pointer)
|
||||
//
|
||||
// Since the java main Looper runs for the lifetime of the application
|
||||
// process we never actually expect to detach the callbacks event fd, and in
|
||||
// the unlikely case where there is no future callback after calling
|
||||
// `wake_java_main_for_callbacks` then the event fd and `MainCallbacks` will
|
||||
// be leaked - but the implication is that the process is about to terminate
|
||||
// (otherwise the Looper would still be running)
|
||||
pub fn _detach_callbacks_event_fd_from_java_main_looper(&mut self) {
|
||||
self._pending_detach
|
||||
.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
self.wake_java_main_for_callbacks();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn run_java_main_callbacks(fd: i32, events: i32, data: *mut c_void) -> i32 {
|
||||
abort_on_panic(|| {
|
||||
// Reset the eventfd counter
|
||||
if events & ndk_sys::ALOOPER_EVENT_INPUT as i32 != 0 {
|
||||
let counter: u64 = 0;
|
||||
loop {
|
||||
match unsafe { libc::read(fd, &counter as *const _ as *mut libc::c_void, 8) } {
|
||||
8 => break,
|
||||
-1 => {
|
||||
let error = std::io::Error::last_os_error();
|
||||
if error.kind() != std::io::ErrorKind::Interrupted {
|
||||
log::error!("Error reading from fd: {:?}", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
count => {
|
||||
log::error!("Unexpected read count from event fd: {}", count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let weak_ptr: *const MainCallbacksState = data.cast();
|
||||
let weak_ref = Weak::from_raw(weak_ptr);
|
||||
let maybe_upgraded = weak_ref.upgrade();
|
||||
|
||||
// Make sure we don't Drop the Weak reference (so the data pointer
|
||||
// remains valid for future callbacks)
|
||||
let _ = weak_ref.into_raw();
|
||||
|
||||
if let Some(main_callbacks) = maybe_upgraded {
|
||||
if main_callbacks
|
||||
._pending_detach
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
let _ = unsafe { libc::close(main_callbacks.event_fd) };
|
||||
let _drop_weak = Weak::from_raw(weak_ptr);
|
||||
// Returning zero indicates that the fd / callback should be
|
||||
// removed from the Looper
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut callbacks = main_callbacks.callbacks.lock().unwrap().take_front();
|
||||
|
||||
let jvm = JavaVM::singleton().unwrap();
|
||||
|
||||
for callback in callbacks.drain(0..) {
|
||||
let res = jvm.attach_current_thread(|_env| -> jni::errors::Result<()> {
|
||||
let res = catch_unwind(AssertUnwindSafe(|| {
|
||||
callback();
|
||||
}));
|
||||
if let Err(err) = res {
|
||||
log::error!("Panic in Java main/UI thread callback: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
if let Err(err) = res {
|
||||
log::error!(
|
||||
"JNI Error while running Java main/UI thread callback: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// put callbacks vec back so we can keep reusing its capacity
|
||||
let mut guard = main_callbacks.callbacks.lock().unwrap();
|
||||
guard.replace_back(callbacks);
|
||||
}
|
||||
|
||||
1
|
||||
})
|
||||
}
|
||||
@@ -3,21 +3,17 @@
|
||||
//! synchronization between the two threads.
|
||||
|
||||
use std::{
|
||||
ffi::{CStr, CString},
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
ops::Deref,
|
||||
os::unix::prelude::{FromRawFd, RawFd},
|
||||
panic::catch_unwind,
|
||||
ptr::{self, NonNull},
|
||||
sync::{Arc, Condvar, Mutex, Weak},
|
||||
};
|
||||
|
||||
use log::Level;
|
||||
use jni::{objects::JObject, refs::Global, vm::AttachConfig};
|
||||
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
|
||||
|
||||
use crate::{
|
||||
util::android_log,
|
||||
init::{init_android_main_thread, init_java_main_thread_on_create},
|
||||
util::{abort_on_panic, log_panic},
|
||||
ConfigurationRef,
|
||||
};
|
||||
@@ -80,18 +76,18 @@ pub enum State {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WaitableNativeActivityState {
|
||||
pub activity: *mut ndk_sys::ANativeActivity,
|
||||
|
||||
pub mutex: Mutex<NativeActivityState>,
|
||||
pub cond: Condvar,
|
||||
}
|
||||
|
||||
// SAFETY: ndk::NativeActivity is also SendSync.
|
||||
unsafe impl Send for WaitableNativeActivityState {}
|
||||
unsafe impl Sync for WaitableNativeActivityState {}
|
||||
|
||||
#[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;
|
||||
@@ -199,21 +195,55 @@ impl NativeActivityGlue {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Set as soon as the Java main thread notifies us of an `onDestroyed`
|
||||
/// callback.
|
||||
pub destroyed: bool,
|
||||
|
||||
/// The `ANativeActivity` associated with the NativeActivity instance
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This pointer will be reset to `null` when the NativeActivity is
|
||||
/// destroyed.
|
||||
///
|
||||
/// Keep in mind that `NativeActivityState` is ref-counted and can
|
||||
/// potentially out-last an `onDestroy` callback where we may reset this to
|
||||
/// be a null pointer!
|
||||
///
|
||||
/// For example:
|
||||
/// - An application could put an `AndroidApp` into a global `'static` and
|
||||
/// keep it alive beyond `android_main`
|
||||
/// - An application could schedule a callback to run on the Java main
|
||||
/// thread with an `AndroidApp` clone and by the time it runs then the
|
||||
/// associated `ANativeActivity` could have been destroyed.
|
||||
pub activity: *mut ndk_sys::ANativeActivity,
|
||||
|
||||
pub msg_read: libc::c_int,
|
||||
pub msg_write: libc::c_int,
|
||||
pub config: super::ConfigurationRef,
|
||||
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 running: bool,
|
||||
pub thread_state: NativeThreadState,
|
||||
pub app_has_saved_state: bool,
|
||||
pub destroyed: bool,
|
||||
pub redraw_needed: bool,
|
||||
|
||||
pub pending_input_queue: *mut ndk_sys::AInputQueue,
|
||||
pub pending_window: Option<NativeWindow>,
|
||||
}
|
||||
@@ -299,12 +329,10 @@ impl NativeActivityState {
|
||||
|
||||
impl Drop for WaitableNativeActivityState {
|
||||
fn drop(&mut self) {
|
||||
log::debug!("WaitableNativeActivityState::drop!");
|
||||
log::info!("WaitableNativeActivityState::drop!");
|
||||
unsafe {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.detach_input_queue_from_looper();
|
||||
guard.destroyed = true;
|
||||
self.cond.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,8 +357,11 @@ impl WaitableNativeActivityState {
|
||||
}
|
||||
}
|
||||
|
||||
let saved_state = unsafe {
|
||||
std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size as _)
|
||||
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 {
|
||||
@@ -345,21 +376,20 @@ impl WaitableNativeActivityState {
|
||||
};
|
||||
|
||||
Self {
|
||||
activity,
|
||||
mutex: Mutex::new(NativeActivityState {
|
||||
activity,
|
||||
msg_read: msgpipe[0],
|
||||
msg_write: msgpipe[1],
|
||||
config,
|
||||
saved_state: saved_state.into(),
|
||||
saved_state,
|
||||
input_queue: ptr::null_mut(),
|
||||
window: None,
|
||||
content_rect: Rect::empty().into(),
|
||||
activity_state: State::Init,
|
||||
destroy_requested: false,
|
||||
running: false,
|
||||
thread_state: NativeThreadState::Init,
|
||||
app_has_saved_state: false,
|
||||
destroyed: false,
|
||||
redraw_needed: false,
|
||||
pending_input_queue: ptr::null_mut(),
|
||||
pending_window: None,
|
||||
}),
|
||||
@@ -369,10 +399,11 @@ impl WaitableNativeActivityState {
|
||||
|
||||
pub fn notify_destroyed(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.destroyed = true;
|
||||
|
||||
unsafe {
|
||||
guard.write_cmd(AppCmd::Destroy);
|
||||
while !guard.destroyed {
|
||||
while guard.thread_state != NativeThreadState::Stopped {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
|
||||
@@ -380,6 +411,11 @@ impl WaitableNativeActivityState {
|
||||
guard.msg_read = -1;
|
||||
libc::close(guard.msg_write);
|
||||
guard.msg_write = -1;
|
||||
|
||||
// The last thing that `NativeActivity` `onDestroy` does is to call a
|
||||
// native method (`unloadNativeCode`) which will `delete` the
|
||||
// `ANativeActivity` instance.
|
||||
guard.activity = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +471,9 @@ impl WaitableNativeActivityState {
|
||||
|
||||
guard.pending_input_queue = input_queue;
|
||||
guard.write_cmd(AppCmd::InputQueueChanged);
|
||||
while guard.input_queue != guard.pending_input_queue {
|
||||
while guard.thread_state == NativeThreadState::Running
|
||||
&& guard.input_queue != guard.pending_input_queue
|
||||
{
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
guard.pending_input_queue = ptr::null_mut();
|
||||
@@ -456,7 +494,9 @@ impl WaitableNativeActivityState {
|
||||
if guard.pending_window.is_some() {
|
||||
guard.write_cmd(AppCmd::InitWindow);
|
||||
}
|
||||
while guard.window != guard.pending_window {
|
||||
while guard.thread_state == NativeThreadState::Running
|
||||
&& guard.window != guard.pending_window
|
||||
{
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
guard.pending_window = None;
|
||||
@@ -480,7 +520,7 @@ impl WaitableNativeActivityState {
|
||||
};
|
||||
guard.write_cmd(cmd);
|
||||
|
||||
while guard.activity_state != state {
|
||||
while guard.thread_state == NativeThreadState::Running && guard.activity_state != state {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -493,7 +533,7 @@ impl WaitableNativeActivityState {
|
||||
// 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 {
|
||||
while guard.thread_state == NativeThreadState::Running && !guard.app_has_saved_state {
|
||||
guard = self.cond.wait(guard).unwrap();
|
||||
}
|
||||
guard.app_has_saved_state = false;
|
||||
@@ -502,7 +542,7 @@ impl WaitableNativeActivityState {
|
||||
// given via a `malloc()` allocated pointer since it will automatically
|
||||
// `free()` the state after it has been converted to a buffer for the JVM.
|
||||
if !guard.saved_state.is_empty() {
|
||||
let saved_state_size = guard.saved_state.len() as _;
|
||||
let saved_state_size = guard.saved_state.len();
|
||||
let saved_state_src_ptr = guard.saved_state.as_ptr();
|
||||
unsafe {
|
||||
let saved_state = libc::malloc(saved_state_size);
|
||||
@@ -541,10 +581,18 @@ impl WaitableNativeActivityState {
|
||||
|
||||
pub fn notify_main_thread_running(&self) {
|
||||
let mut guard = self.mutex.lock().unwrap();
|
||||
guard.running = true;
|
||||
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;
|
||||
// Notify all waiters to unblock any Android callbacks that would otherwise be waiting
|
||||
// indefinitely for the now-stopped (!) main thread.
|
||||
self.cond.notify_all();
|
||||
}
|
||||
|
||||
pub unsafe fn pre_exec_cmd(
|
||||
&self,
|
||||
cmd: AppCmd,
|
||||
@@ -581,7 +629,7 @@ impl WaitableNativeActivityState {
|
||||
AppCmd::ConfigChanged => {
|
||||
let guard = self.mutex.lock().unwrap();
|
||||
let config = ndk_sys::AConfiguration_new();
|
||||
ndk_sys::AConfiguration_fromAssetManager(config, (*self.activity).assetManager);
|
||||
ndk_sys::AConfiguration_fromAssetManager(config, (*guard.activity).assetManager);
|
||||
let config = Configuration::from_ptr(NonNull::new_unchecked(config));
|
||||
guard.config.replace(config);
|
||||
log::debug!("Config: {:#?}", guard.config);
|
||||
@@ -623,12 +671,19 @@ unsafe fn try_with_waitable_activity_ref(
|
||||
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() {
|
||||
let maybe_upgraded = weak_ref.upgrade();
|
||||
|
||||
// Make sure we don't Drop the Weak reference (even if we failed to upgrade it
|
||||
// and also considering the possibility that we unwind due to a panic in `closure()`)
|
||||
// (The raw weak pointer associated with activity->instance must remain valid
|
||||
// until `on_destroy` is called).
|
||||
let _ = weak_ref.into_raw();
|
||||
|
||||
if let Some(waitable_activity) = maybe_upgraded {
|
||||
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) {
|
||||
@@ -637,6 +692,16 @@ unsafe extern "C" fn on_destroy(activity: *mut ndk_sys::ANativeActivity) {
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
waitable_activity.notify_destroyed()
|
||||
});
|
||||
|
||||
// Once we return from here the `ANativeActivity` will be deleted via an
|
||||
// `unloadNativeCode` native method and so we can't get any more
|
||||
// callbacks and we can release the `Weak<WaitableNativeActivityState>`
|
||||
// reference we have associated with `activity->instance`
|
||||
|
||||
assert!(!(*activity).instance.is_null());
|
||||
let weak_ptr: *const WaitableNativeActivityState = (*activity).instance.cast();
|
||||
let _drop_weak_ref = Weak::from_raw(weak_ptr);
|
||||
(*activity).instance = std::ptr::null_mut();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -660,7 +725,7 @@ unsafe extern "C" fn on_resume(activity: *mut ndk_sys::ANativeActivity) {
|
||||
|
||||
unsafe extern "C" fn on_save_instance_state(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
out_len: *mut ndk_sys::size_t,
|
||||
out_len: *mut usize,
|
||||
) -> *mut libc::c_void {
|
||||
abort_on_panic(|| {
|
||||
log::debug!("SaveInstanceState: {:p}\n", activity);
|
||||
@@ -668,7 +733,7 @@ unsafe extern "C" fn on_save_instance_state(
|
||||
let mut ret = ptr::null_mut();
|
||||
try_with_waitable_activity_ref(activity, |waitable_activity| {
|
||||
let (state, len) = waitable_activity.request_save_state();
|
||||
*out_len = len as ndk_sys::size_t;
|
||||
*out_len = len;
|
||||
ret = state
|
||||
});
|
||||
|
||||
@@ -808,43 +873,28 @@ unsafe extern "C" fn on_content_rect_changed(
|
||||
|
||||
/// This is the native entrypoint for our cdylib library that `ANativeActivity` will look for via `dlsym`
|
||||
#[no_mangle]
|
||||
#[allow(unused_unsafe)] // Otherwise rust 1.64 moans about using unsafe{} in unsafe functions
|
||||
extern "C" fn ANativeActivity_onCreate(
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
saved_state: *const libc::c_void,
|
||||
saved_state_size: libc::size_t,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
|
||||
unsafe {
|
||||
let mut logpipe: [RawFd; 2] = Default::default();
|
||||
libc::pipe(logpipe.as_mut_ptr());
|
||||
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
|
||||
libc::dup2(logpipe[1], libc::STDERR_FILENO);
|
||||
std::thread::spawn(move || {
|
||||
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
|
||||
let file = File::from_raw_fd(logpipe[0]);
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut buffer = String::new();
|
||||
loop {
|
||||
buffer.clear();
|
||||
if let Ok(len) = reader.read_line(&mut buffer) {
|
||||
if len == 0 {
|
||||
break;
|
||||
} else if let Ok(msg) = CString::new(buffer.clone()) {
|
||||
android_log(Level::Info, tag, &msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let main_looper =
|
||||
ndk::looper::ForeignLooper::for_thread().expect("Failed to get Java main looper");
|
||||
|
||||
log::trace!(
|
||||
"Creating: {:p}, saved_state = {:p}, save_state_size = {}",
|
||||
activity,
|
||||
saved_state,
|
||||
saved_state_size
|
||||
);
|
||||
let (jvm, jni_activity) = unsafe {
|
||||
let jvm: *mut jni::sys::JavaVM = (*activity).vm.cast();
|
||||
let jni_activity: jni::sys::jobject = (*activity).clazz as _; // Completely bogus name; this is the _instance_ not class pointer
|
||||
(jni::JavaVM::from_raw(jvm), jni_activity)
|
||||
};
|
||||
unsafe {
|
||||
let saved_state = if !saved_state.is_null() && saved_state_size > 0 {
|
||||
std::slice::from_raw_parts(saved_state.cast(), saved_state_size)
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
init_java_main_thread_on_create(jvm, jni_activity as _, saved_state);
|
||||
};
|
||||
|
||||
// Conceptually we associate a glue reference with the JVM main thread, and another
|
||||
// reference with the Rust main thread
|
||||
@@ -857,62 +907,97 @@ extern "C" fn ANativeActivity_onCreate(
|
||||
// Note: we drop the thread handle which will detach the thread
|
||||
std::thread::spawn(move || {
|
||||
let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _;
|
||||
|
||||
let jvm = unsafe {
|
||||
let na = activity;
|
||||
let jvm = (*na).vm;
|
||||
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
|
||||
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached
|
||||
// to the thread yet. Attach before calling the applications main function
|
||||
// so they can safely make JNI calls
|
||||
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
|
||||
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
|
||||
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
jvm
|
||||
};
|
||||
|
||||
let app = AndroidApp::new(rust_glue.clone());
|
||||
|
||||
rust_glue.notify_main_thread_running();
|
||||
|
||||
unsafe {
|
||||
// We want to specifically catch any panic from the application's android_main
|
||||
// so we can finish + destroy the Activity gracefully via the JVM
|
||||
catch_unwind(|| {
|
||||
// XXX: If we were in control of the Java Activity subclass then
|
||||
// we could potentially run the android_main function via a Java native method
|
||||
// springboard (e.g. call an Activity subclass method that calls a jni native
|
||||
// method that then just calls android_main()) that would make sure there was
|
||||
// a Java frame at the base of our call stack which would then be recognised
|
||||
// when calling FindClass to lookup a suitable classLoader, instead of
|
||||
// defaulting to the system loader. Without this then it's difficult for native
|
||||
// code to look up non-standard Java classes.
|
||||
android_main(app);
|
||||
})
|
||||
.unwrap_or_else(|panic| log_panic(panic));
|
||||
|
||||
// Let JVM know that our Activity can be destroyed before detaching from the JVM
|
||||
//
|
||||
// "Note that this method can be called from any thread; it will send a message
|
||||
// to the main thread of the process where the Java finish call will take place"
|
||||
ndk_sys::ANativeActivity_finish(activity);
|
||||
|
||||
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
|
||||
detach_current_thread(jvm);
|
||||
}
|
||||
|
||||
ndk_context::release_android_context();
|
||||
}
|
||||
rust_glue_entry(rust_glue, activity, main_looper);
|
||||
});
|
||||
|
||||
// Wait for thread to start.
|
||||
let mut guard = jvm_glue.mutex.lock().unwrap();
|
||||
while !guard.running {
|
||||
|
||||
// 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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn rust_glue_entry(
|
||||
rust_glue: NativeActivityGlue,
|
||||
activity: *mut ndk_sys::ANativeActivity,
|
||||
main_looper: ndk::looper::ForeignLooper,
|
||||
) {
|
||||
abort_on_panic(|| {
|
||||
let (jvm, jni_activity) = unsafe {
|
||||
let jvm: *mut jni::sys::JavaVM = (*activity).vm.cast();
|
||||
let jni_activity: jni::sys::jobject = (*activity).clazz as _; // Completely bogus name; this is the _instance_ not class pointer
|
||||
(jni::JavaVM::from_raw(jvm), jni_activity)
|
||||
};
|
||||
// Note: At this point we can assume jni::JavaVM::singleton is initialized
|
||||
|
||||
// Since this is a newly spawned thread then the JVM hasn't been attached to the
|
||||
// thread yet.
|
||||
//
|
||||
// For compatibility we attach before calling the applications main function to
|
||||
// allow it to assume the thread is attached before making JNI calls.
|
||||
jvm.attach_current_thread_with_config(
|
||||
|| AttachConfig::new().thread_name(jni::jni_str!("android_main")),
|
||||
Some(16),
|
||||
|env| -> jni::errors::Result<()> {
|
||||
// SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance
|
||||
// that will remain valid until `onDestroy` is handled (not possible until we start
|
||||
// `android_main()`).
|
||||
let jni_activity = unsafe { env.as_cast_raw::<Global<JObject>>(&jni_activity)? };
|
||||
|
||||
let (app_asset_manager, main_callbacks) =
|
||||
match init_android_main_thread(&jvm, &jni_activity, &main_looper) {
|
||||
Ok((asset_manager, callbacks)) => (asset_manager, callbacks),
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Failed to name Java thread and set thread context class loader: {err}"
|
||||
);
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let app = AndroidApp::new(
|
||||
jvm.clone(),
|
||||
main_looper,
|
||||
main_callbacks,
|
||||
app_asset_manager,
|
||||
rust_glue.clone(),
|
||||
&jni_activity,
|
||||
);
|
||||
|
||||
rust_glue.notify_main_thread_running();
|
||||
|
||||
unsafe {
|
||||
// We want to specifically catch any panic from the application's android_main
|
||||
// so we can finish + destroy the Activity gracefully via the JVM
|
||||
catch_unwind(|| {
|
||||
// XXX: If we were in control of the Java Activity subclass then
|
||||
// we could potentially run the android_main function via a Java native method
|
||||
// springboard (e.g. call an Activity subclass method that calls a jni native
|
||||
// method that then just calls android_main()) that would make sure there was
|
||||
// a Java frame at the base of our call stack which would then be recognised
|
||||
// when calling FindClass to lookup a suitable classLoader, instead of
|
||||
// defaulting to the system loader. Without this then it's difficult for native
|
||||
// code to look up non-standard Java classes.
|
||||
android_main(app);
|
||||
})
|
||||
.unwrap_or_else(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);
|
||||
}
|
||||
|
||||
rust_glue.notify_main_thread_stopped_running();
|
||||
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("Failed to attach thread to JVM");
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::{iter::FusedIterator, marker::PhantomData, ptr::NonNull};
|
||||
|
||||
pub use ndk::event::{
|
||||
Axis, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction,
|
||||
MotionEventFlags, Pointer, PointersIter,
|
||||
use crate::input::{
|
||||
Axis, Button, ButtonState, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction,
|
||||
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
|
||||
};
|
||||
|
||||
use crate::input::{Class, Source};
|
||||
|
||||
/// A motion event
|
||||
///
|
||||
/// For general discussion of motion events in Android, see [the relevant
|
||||
@@ -17,7 +15,7 @@ pub struct MotionEvent<'a> {
|
||||
ndk_event: ndk::event::MotionEvent,
|
||||
_lifetime: PhantomData<&'a ndk::event::MotionEvent>,
|
||||
}
|
||||
impl<'a> MotionEvent<'a> {
|
||||
impl MotionEvent<'_> {
|
||||
pub(crate) fn new(ndk_event: ndk::event::MotionEvent) -> Self {
|
||||
Self {
|
||||
ndk_event,
|
||||
@@ -34,18 +32,11 @@ impl<'a> MotionEvent<'a> {
|
||||
pub fn source(&self) -> Source {
|
||||
// XXX: we use `AInputEvent_getSource` directly (instead of calling
|
||||
// ndk_event.source()) since we have our own `Source` enum that we
|
||||
// share between backends, which may not exactly match the ndk crate's
|
||||
// `Source` enum.
|
||||
// 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.try_into().unwrap_or(Source::Unknown)
|
||||
}
|
||||
|
||||
/// Get the class of the event source.
|
||||
///
|
||||
#[inline]
|
||||
pub fn class(&self) -> Class {
|
||||
Class::from(self.source())
|
||||
source.into()
|
||||
}
|
||||
|
||||
/// Get the device id associated with the event.
|
||||
@@ -60,7 +51,26 @@ impl<'a> MotionEvent<'a> {
|
||||
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionMasked())
|
||||
#[inline]
|
||||
pub fn action(&self) -> MotionAction {
|
||||
self.ndk_event.action()
|
||||
// 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.
|
||||
@@ -99,7 +109,14 @@ impl<'a> MotionEvent<'a> {
|
||||
/// An iterator over the pointers in this motion event
|
||||
#[inline]
|
||||
pub fn pointers(&self) -> PointersIter<'_> {
|
||||
self.ndk_event.pointers()
|
||||
PointersIter {
|
||||
inner: PointersIterImpl {
|
||||
event: self.ndk_event.ptr(),
|
||||
pointer_index: 0,
|
||||
pointer_count: self.ndk_event.pointer_count(),
|
||||
_marker: std::marker::PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer at a given pointer index. Panics if the pointer index is out of bounds.
|
||||
@@ -107,36 +124,22 @@ impl<'a> MotionEvent<'a> {
|
||||
/// If you need to loop over all the pointers, prefer the [`pointers()`](Self::pointers) method.
|
||||
#[inline]
|
||||
pub fn pointer_at_index(&self, index: usize) -> Pointer<'_> {
|
||||
self.ndk_event.pointer_at_index(index)
|
||||
Pointer {
|
||||
inner: PointerImpl {
|
||||
event: self.ndk_event.ptr(),
|
||||
pointer_index: index,
|
||||
_marker: std::marker::PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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()
|
||||
self.ndk_event.meta_state().into()
|
||||
}
|
||||
|
||||
/// Returns the button state during this event, as a bitfield.
|
||||
@@ -145,7 +148,7 @@ impl<'a> MotionEvent<'a> {
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getbuttonstate)
|
||||
#[inline]
|
||||
pub fn button_state(&self) -> ButtonState {
|
||||
self.ndk_event.button_state()
|
||||
self.ndk_event.button_state().into()
|
||||
}
|
||||
|
||||
/// Returns the time of the start of this gesture, in the `java.lang.System.nanoTime()` time
|
||||
@@ -164,7 +167,7 @@ impl<'a> MotionEvent<'a> {
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getedgeflags)
|
||||
#[inline]
|
||||
pub fn edge_flags(&self) -> EdgeFlags {
|
||||
self.ndk_event.edge_flags()
|
||||
self.ndk_event.edge_flags().into()
|
||||
}
|
||||
|
||||
/// Returns the time of this event, in the `java.lang.System.nanoTime()` time base
|
||||
@@ -182,7 +185,7 @@ impl<'a> MotionEvent<'a> {
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getflags)
|
||||
#[inline]
|
||||
pub fn flags(&self) -> MotionEventFlags {
|
||||
self.ndk_event.flags()
|
||||
self.ndk_event.flags().into()
|
||||
}
|
||||
|
||||
/* Missing from GameActivity currently...
|
||||
@@ -224,6 +227,204 @@ impl<'a> MotionEvent<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A view into the data of a specific pointer in a motion event.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PointerImpl<'a> {
|
||||
event: NonNull<ndk_sys::AInputEvent>,
|
||||
pointer_index: usize,
|
||||
_marker: std::marker::PhantomData<&'a MotionEvent<'a>>,
|
||||
}
|
||||
|
||||
impl PointerImpl<'_> {
|
||||
#[inline]
|
||||
pub fn pointer_index(&self) -> usize {
|
||||
self.pointer_index
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pointer_id(&self) -> i32 {
|
||||
unsafe { ndk_sys::AMotionEvent_getPointerId(self.event.as_ptr(), self.pointer_index) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn axis_value(&self, axis: Axis) -> f32 {
|
||||
let value: u32 = axis.into();
|
||||
let value = value as i32;
|
||||
unsafe {
|
||||
ndk_sys::AMotionEvent_getAxisValue(self.event.as_ptr(), value, self.pointer_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn raw_x(&self) -> f32 {
|
||||
unsafe { ndk_sys::AMotionEvent_getRawX(self.event.as_ptr(), self.pointer_index) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn raw_y(&self) -> f32 {
|
||||
unsafe { ndk_sys::AMotionEvent_getRawY(self.event.as_ptr(), self.pointer_index) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tool_type(&self) -> ToolType {
|
||||
let value =
|
||||
unsafe { ndk_sys::AMotionEvent_getToolType(self.event.as_ptr(), self.pointer_index) };
|
||||
let value = value as u32;
|
||||
value.into()
|
||||
}
|
||||
|
||||
pub fn history(&self) -> crate::input::PointerHistoryIter<'_> {
|
||||
let history_size =
|
||||
unsafe { ndk_sys::AMotionEvent_getHistorySize(self.event.as_ptr()) } as usize;
|
||||
crate::input::PointerHistoryIter {
|
||||
inner: PointerHistoryIterImpl {
|
||||
event: self.event,
|
||||
pointer_index: self.pointer_index,
|
||||
front: 0,
|
||||
back: history_size,
|
||||
_marker: std::marker::PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PointersIterImpl<'a> {
|
||||
event: NonNull<ndk_sys::AInputEvent>,
|
||||
pointer_count: usize,
|
||||
pointer_index: usize,
|
||||
_marker: std::marker::PhantomData<&'a MotionEvent<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PointersIterImpl<'a> {
|
||||
type Item = Pointer<'a>;
|
||||
fn next(&mut self) -> Option<Pointer<'a>> {
|
||||
if self.pointer_index == self.pointer_count {
|
||||
return None;
|
||||
}
|
||||
let pointer = Pointer {
|
||||
inner: PointerImpl {
|
||||
event: self.event,
|
||||
pointer_index: self.pointer_index,
|
||||
_marker: std::marker::PhantomData,
|
||||
},
|
||||
};
|
||||
self.pointer_index += 1;
|
||||
Some(pointer)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let remaining = self.pointer_count - self.pointer_index;
|
||||
(remaining, Some(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for PointersIterImpl<'_> {}
|
||||
|
||||
/// A view into a pointer at a historical moment
|
||||
#[derive(Debug)]
|
||||
pub struct HistoricalPointerImpl<'a> {
|
||||
event: NonNull<ndk_sys::AInputEvent>,
|
||||
pointer_index: usize,
|
||||
history_index: usize,
|
||||
_marker: std::marker::PhantomData<&'a MotionEvent<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> HistoricalPointerImpl<'a> {
|
||||
#[inline]
|
||||
pub fn pointer_index(&self) -> usize {
|
||||
self.pointer_index
|
||||
}
|
||||
|
||||
/// Returns the time of the historical event, in the `java.lang.System.nanoTime()` time base
|
||||
///
|
||||
/// See [`MotionEvent.getHistoricalEventTimeNanos`](https://developer.android.com/reference/android/view/MotionEvent#getHistoricalEventTimeNanos(int)) SDK docs
|
||||
#[inline]
|
||||
pub fn event_time(&self) -> i64 {
|
||||
unsafe {
|
||||
ndk_sys::AMotionEvent_getHistoricalEventTime(self.event.as_ptr(), self.history_index)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pointer_id(&self) -> i32 {
|
||||
unsafe { ndk_sys::AMotionEvent_getPointerId(self.event.as_ptr(), self.pointer_index) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn history_index(&self) -> usize {
|
||||
self.history_index
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn axis_value(&self, axis: Axis) -> f32 {
|
||||
unsafe {
|
||||
ndk_sys::AMotionEvent_getHistoricalAxisValue(
|
||||
self.event.as_ptr(),
|
||||
Into::<u32>::into(axis) as i32,
|
||||
self.pointer_index,
|
||||
self.history_index,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the historical points of a specific pointer in a [`MotionEvent`].
|
||||
#[derive(Debug)]
|
||||
pub struct PointerHistoryIterImpl<'a> {
|
||||
event: NonNull<ndk_sys::AInputEvent>,
|
||||
pointer_index: usize,
|
||||
front: usize,
|
||||
back: usize,
|
||||
_marker: std::marker::PhantomData<&'a MotionEvent<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PointerHistoryIterImpl<'a> {
|
||||
type Item = crate::input::HistoricalPointer<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<crate::input::HistoricalPointer<'a>> {
|
||||
if self.front == self.back {
|
||||
return None;
|
||||
}
|
||||
|
||||
let history_index = self.front;
|
||||
self.front += 1;
|
||||
Some(crate::input::HistoricalPointer {
|
||||
inner: crate::input::HistoricalPointerImpl {
|
||||
event: self.event,
|
||||
history_index,
|
||||
pointer_index: self.pointer_index,
|
||||
_marker: std::marker::PhantomData,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let size = self.back - self.front;
|
||||
(size, Some(size))
|
||||
}
|
||||
}
|
||||
impl<'a> DoubleEndedIterator for PointerHistoryIterImpl<'a> {
|
||||
fn next_back(&mut self) -> Option<crate::input::HistoricalPointer<'a>> {
|
||||
if self.front == self.back {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.back -= 1;
|
||||
let history_index = self.back;
|
||||
Some(crate::input::HistoricalPointer {
|
||||
inner: crate::input::HistoricalPointerImpl {
|
||||
event: self.event,
|
||||
history_index,
|
||||
pointer_index: self.pointer_index,
|
||||
_marker: std::marker::PhantomData,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ExactSizeIterator for PointerHistoryIterImpl<'_> {}
|
||||
impl FusedIterator for PointerHistoryIterImpl<'_> {}
|
||||
|
||||
/// A key event
|
||||
///
|
||||
/// For general discussion of key events in Android, see [the relevant
|
||||
@@ -234,7 +435,7 @@ pub struct KeyEvent<'a> {
|
||||
ndk_event: ndk::event::KeyEvent,
|
||||
_lifetime: PhantomData<&'a ndk::event::KeyEvent>,
|
||||
}
|
||||
impl<'a> KeyEvent<'a> {
|
||||
impl KeyEvent<'_> {
|
||||
pub(crate) fn new(ndk_event: ndk::event::KeyEvent) -> Self {
|
||||
Self {
|
||||
ndk_event,
|
||||
@@ -251,18 +452,11 @@ impl<'a> KeyEvent<'a> {
|
||||
pub fn source(&self) -> Source {
|
||||
// XXX: we use `AInputEvent_getSource` directly (instead of calling
|
||||
// ndk_event.source()) since we have our own `Source` enum that we
|
||||
// share between backends, which may not exactly match the ndk crate's
|
||||
// `Source` enum.
|
||||
// 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.try_into().unwrap_or(Source::Unknown)
|
||||
}
|
||||
|
||||
/// Get the class of the event source.
|
||||
///
|
||||
#[inline]
|
||||
pub fn class(&self) -> Class {
|
||||
Class::from(self.source())
|
||||
source.into()
|
||||
}
|
||||
|
||||
/// Get the device id associated with the event.
|
||||
@@ -277,7 +471,11 @@ impl<'a> KeyEvent<'a> {
|
||||
/// See [the KeyEvent docs](https://developer.android.com/reference/android/view/KeyEvent#getAction())
|
||||
#[inline]
|
||||
pub fn action(&self) -> KeyAction {
|
||||
self.ndk_event.action()
|
||||
// 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
|
||||
@@ -306,7 +504,12 @@ impl<'a> KeyEvent<'a> {
|
||||
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getkeycode)
|
||||
#[inline]
|
||||
pub fn key_code(&self) -> Keycode {
|
||||
self.ndk_event.key_code()
|
||||
// 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.
|
||||
@@ -326,6 +529,15 @@ impl<'a> KeyEvent<'a> {
|
||||
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
|
||||
@@ -337,4 +549,6 @@ impl<'a> KeyEvent<'a> {
|
||||
pub enum InputEvent<'a> {
|
||||
MotionEvent(self::MotionEvent<'a>),
|
||||
KeyEvent(self::KeyEvent<'a>),
|
||||
TextEvent(crate::input::TextInputState),
|
||||
TextAction(crate::input::TextInputAction),
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
#![cfg(any(feature = "native-activity", doc))]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::ptr;
|
||||
use std::ptr::NonNull;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::sync::{Arc, Mutex, RwLock, Weak};
|
||||
use std::time::Duration;
|
||||
|
||||
use jni::objects::JObject;
|
||||
use jni::JavaVM;
|
||||
use libc::c_void;
|
||||
use log::{error, trace};
|
||||
use ndk::input_queue::InputQueue;
|
||||
use ndk::{asset::AssetManager, native_window::NativeWindow};
|
||||
|
||||
use crate::error::InternalResult;
|
||||
use crate::main_callbacks::MainCallbacks;
|
||||
use crate::sdk::{Activity, Context, InputMethodManager};
|
||||
use crate::{
|
||||
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
|
||||
util, AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect,
|
||||
WindowManagerFlags,
|
||||
};
|
||||
|
||||
pub mod input;
|
||||
use crate::input::{
|
||||
device_key_character_map, Axis, ImeOptions, InputType, KeyCharacterMap, TextInputAction,
|
||||
TextInputState, TextSpan,
|
||||
};
|
||||
|
||||
mod glue;
|
||||
use self::glue::NativeActivityGlue;
|
||||
@@ -45,102 +56,128 @@ impl<'a> StateSaver<'a> {
|
||||
pub struct StateLoader<'a> {
|
||||
app: &'a AndroidAppInner,
|
||||
}
|
||||
impl<'a> StateLoader<'a> {
|
||||
impl StateLoader<'_> {
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AndroidApp {
|
||||
pub(crate) fn new(native_activity: NativeActivityGlue) -> Self {
|
||||
let app = Self {
|
||||
inner: Arc::new(RwLock::new(AndroidAppInner {
|
||||
native_activity,
|
||||
looper: Looper {
|
||||
ptr: ptr::null_mut(),
|
||||
},
|
||||
})),
|
||||
};
|
||||
pub(crate) fn new(
|
||||
jvm: JavaVM,
|
||||
main_looper: ndk::looper::ForeignLooper,
|
||||
main_callbacks: MainCallbacks,
|
||||
app_asset_manager: AssetManager,
|
||||
native_activity: NativeActivityGlue,
|
||||
jni_activity: &JObject,
|
||||
) -> Self {
|
||||
jvm.with_local_frame(10, |env| -> jni::errors::Result<_> {
|
||||
if let Err(err) = crate::sdk::jni_init(env) {
|
||||
panic!("Failed to init JNI bindings: {err:?}");
|
||||
};
|
||||
|
||||
{
|
||||
let mut guard = app.inner.write().unwrap();
|
||||
|
||||
let main_fd = guard.native_activity.cmd_read_fd();
|
||||
unsafe {
|
||||
guard.looper.ptr = ndk_sys::ALooper_prepare(
|
||||
let looper = unsafe {
|
||||
let 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
ndk::looper::ForeignLooper::from_ptr(ptr::NonNull::new(ptr).unwrap())
|
||||
};
|
||||
|
||||
app
|
||||
// The global reference in `ANativeActivity` is only guaranteed to be valid until
|
||||
// `onDestroy` returns, so we create our own global reference that we can guarantee will
|
||||
// remain valid until `AndroidApp` is dropped.
|
||||
let activity = env
|
||||
.new_global_ref(jni_activity)
|
||||
.expect("Failed to create global ref for Activity instance");
|
||||
|
||||
let app = Self {
|
||||
inner: Arc::new(RwLock::new(AndroidAppInner {
|
||||
jvm: jvm.clone(),
|
||||
main_looper,
|
||||
main_callbacks,
|
||||
app_asset_manager,
|
||||
native_activity,
|
||||
activity,
|
||||
looper,
|
||||
key_maps: Mutex::new(HashMap::new()),
|
||||
input_receiver: Mutex::new(None),
|
||||
})),
|
||||
};
|
||||
|
||||
{
|
||||
let guard = app.inner.write().unwrap();
|
||||
|
||||
let main_fd = guard.native_activity.cmd_read_fd();
|
||||
unsafe {
|
||||
ndk_sys::ALooper_addFd(
|
||||
guard.looper.ptr().as_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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(app)
|
||||
})
|
||||
.expect("Failed to create AndroidApp instance")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Looper {
|
||||
pub ptr: *mut ndk_sys::ALooper,
|
||||
}
|
||||
unsafe impl Send for Looper {}
|
||||
unsafe impl Sync for Looper {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AndroidAppInner {
|
||||
pub(crate) jvm: JavaVM,
|
||||
|
||||
pub(crate) native_activity: NativeActivityGlue,
|
||||
looper: Looper,
|
||||
|
||||
activity: jni::refs::Global<jni::objects::JObject<'static>>,
|
||||
|
||||
main_callbacks: MainCallbacks,
|
||||
|
||||
/// Looper associated with the Rust `android_main` thread
|
||||
looper: ndk::looper::ForeignLooper,
|
||||
|
||||
/// Looper associated with the activity's Java main thread, sometimes called
|
||||
/// the UI thread.
|
||||
main_looper: ndk::looper::ForeignLooper,
|
||||
|
||||
/// 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>>>,
|
||||
|
||||
/// An `AAssetManager` wrapper for the `Application` `AssetManager`
|
||||
/// Note: `AAssetManager_fromJava` specifies that the pointer is only valid
|
||||
/// while we hold a global reference to the `AssetManager` Java object
|
||||
/// to ensure it is not garbage collected. This AssetManager comes from
|
||||
/// a OnceLock initialization that leaks a single global JNI reference
|
||||
/// to guarantee that it remains valid for the lifetime of the process.
|
||||
app_asset_manager: AssetManager,
|
||||
}
|
||||
|
||||
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 _ }
|
||||
// Note: The global reference in `ANativeActivity::clazz` (misnomer for instance reference)
|
||||
// is only guaranteed to be valid until `onDestroy` returns, so we have our own global
|
||||
// reference that we can instead guarantee will remain valid until `AndroidApp` is dropped.
|
||||
self.activity.as_raw() as *mut c_void
|
||||
}
|
||||
|
||||
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
|
||||
self.native_activity.activity
|
||||
pub(crate) fn looper_as_ptr(&self) -> *mut ndk_sys::ALooper {
|
||||
self.looper.ptr().as_ptr()
|
||||
}
|
||||
|
||||
pub(crate) fn looper(&self) -> *mut ndk_sys::ALooper {
|
||||
self.looper.ptr
|
||||
pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper {
|
||||
self.main_looper.clone()
|
||||
}
|
||||
|
||||
pub fn native_window(&self) -> Option<NativeWindow> {
|
||||
@@ -149,14 +186,14 @@ impl AndroidAppInner {
|
||||
|
||||
pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
|
||||
where
|
||||
F: FnMut(PollEvent),
|
||||
F: FnMut(PollEvent<'_>),
|
||||
{
|
||||
trace!("poll_events");
|
||||
|
||||
unsafe {
|
||||
let mut fd: i32 = 0;
|
||||
let mut events: i32 = 0;
|
||||
let mut source: *mut core::ffi::c_void = ptr::null_mut();
|
||||
let mut source: *mut c_void = ptr::null_mut();
|
||||
|
||||
let timeout_milliseconds = if let Some(timeout) = timeout {
|
||||
timeout.as_millis() as i32
|
||||
@@ -164,41 +201,42 @@ impl AndroidAppInner {
|
||||
-1
|
||||
};
|
||||
|
||||
trace!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
|
||||
assert!(
|
||||
!ndk_sys::ALooper_forThread().is_null(),
|
||||
trace!("Calling ALooper_pollOnce, timeout = {timeout_milliseconds}");
|
||||
assert_eq!(
|
||||
ndk_sys::ALooper_forThread(),
|
||||
self.looper_as_ptr(),
|
||||
"Application tried to poll events from non-main thread"
|
||||
);
|
||||
let id = ndk_sys::ALooper_pollAll(
|
||||
let id = ndk_sys::ALooper_pollOnce(
|
||||
timeout_milliseconds,
|
||||
&mut fd,
|
||||
&mut events,
|
||||
&mut source as *mut *mut core::ffi::c_void,
|
||||
&mut source as *mut *mut c_void,
|
||||
);
|
||||
trace!("pollAll id = {id}");
|
||||
trace!("pollOnce id = {id}");
|
||||
match id {
|
||||
ndk_sys::ALOOPER_POLL_WAKE => {
|
||||
trace!("ALooper_pollAll returned POLL_WAKE");
|
||||
trace!("ALooper_pollOnce returned POLL_WAKE");
|
||||
callback(PollEvent::Wake);
|
||||
}
|
||||
ndk_sys::ALOOPER_POLL_CALLBACK => {
|
||||
// ALooper_pollAll is documented to handle all callback sources internally so it should
|
||||
// ALooper_pollOnce 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)");
|
||||
error!("Spurious ALOOPER_POLL_CALLBACK from ALooper_pollOnce() (ignored)");
|
||||
}
|
||||
ndk_sys::ALOOPER_POLL_TIMEOUT => {
|
||||
trace!("ALooper_pollAll returned POLL_TIMEOUT");
|
||||
trace!("ALooper_pollOnce returned POLL_TIMEOUT");
|
||||
callback(PollEvent::Timeout);
|
||||
}
|
||||
ndk_sys::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");
|
||||
panic!("ALooper_pollOnce returned POLL_ERROR");
|
||||
}
|
||||
id if id >= 0 => {
|
||||
match id {
|
||||
LOOPER_ID_MAIN => {
|
||||
trace!("ALooper_pollAll returned ID_MAIN");
|
||||
trace!("ALooper_pollOnce returned ID_MAIN");
|
||||
if let Some(ipc_cmd) = self.native_activity.read_cmd() {
|
||||
let main_cmd = match ipc_cmd {
|
||||
// We don't forward info about the AInputQueue to apps since it's
|
||||
@@ -238,7 +276,7 @@ impl AndroidAppInner {
|
||||
trace!("Calling pre_exec_cmd({ipc_cmd:#?})");
|
||||
self.native_activity.pre_exec_cmd(
|
||||
ipc_cmd,
|
||||
self.looper(),
|
||||
self.looper_as_ptr(),
|
||||
LOOPER_ID_INPUT,
|
||||
);
|
||||
|
||||
@@ -252,7 +290,7 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
LOOPER_ID_INPUT => {
|
||||
trace!("ALooper_pollAll returned ID_INPUT");
|
||||
trace!("ALooper_pollOnce 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
|
||||
@@ -267,20 +305,22 @@ impl AndroidAppInner {
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
error!("Spurious ALooper_pollAll return value {id} (ignored)");
|
||||
error!("Spurious ALooper_pollOnce return value {id} (ignored)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
AndroidAppWaker {
|
||||
looper: NonNull::new_unchecked(self.looper.ptr),
|
||||
}
|
||||
}
|
||||
// Safety: we know that the looper is a valid, non-null pointer
|
||||
unsafe { AndroidAppWaker::new(self.looper_as_ptr()) }
|
||||
}
|
||||
|
||||
pub fn run_on_java_main_thread<F>(&self, f: Box<F>)
|
||||
where
|
||||
F: FnOnce() + Send + 'static,
|
||||
{
|
||||
self.main_callbacks.run_on_java_main_thread(f);
|
||||
}
|
||||
|
||||
pub fn config(&self) -> ConfigurationRef {
|
||||
@@ -292,11 +332,11 @@ impl AndroidAppInner {
|
||||
}
|
||||
|
||||
pub fn asset_manager(&self) -> AssetManager {
|
||||
unsafe {
|
||||
let activity_ptr = self.native_activity.activity;
|
||||
let am_ptr = NonNull::new_unchecked((*activity_ptr).assetManager);
|
||||
AssetManager::from_ptr(am_ptr)
|
||||
}
|
||||
// Safety: While constructing the AndroidApp we do a OnceLock initialization
|
||||
// where we get the Application AssetManager and leak a single global JNI
|
||||
// reference that guarantees it will not be garbage collected, so we can
|
||||
// safely return the corresponding AAssetManager here.
|
||||
unsafe { AssetManager::from_ptr(self.app_asset_manager.ptr()) }
|
||||
}
|
||||
|
||||
pub fn set_window_flags(
|
||||
@@ -304,8 +344,14 @@ impl AndroidAppInner {
|
||||
add_flags: WindowManagerFlags,
|
||||
remove_flags: WindowManagerFlags,
|
||||
) {
|
||||
let na = self.native_activity();
|
||||
let na_mut = na as *mut ndk_sys::ANativeActivity;
|
||||
let guard = self.native_activity.mutex.lock().unwrap();
|
||||
let na = guard.activity;
|
||||
if na.is_null() {
|
||||
log::error!("Can't set window flags after NativeActivity has been destroyed");
|
||||
return;
|
||||
}
|
||||
|
||||
let na_mut = na;
|
||||
unsafe {
|
||||
ndk_sys::ANativeActivity_setWindowFlags(
|
||||
na_mut.cast(),
|
||||
@@ -317,62 +363,218 @@ impl AndroidAppInner {
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn show_soft_input(&self, show_implicit: bool) {
|
||||
let na = self.native_activity();
|
||||
unsafe {
|
||||
let flags = if show_implicit {
|
||||
ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ndk_sys::ANativeActivity_showSoftInput(na as *mut _, flags);
|
||||
let guard = self.native_activity.mutex.lock().unwrap();
|
||||
let na = guard.activity;
|
||||
if na.is_null() {
|
||||
log::error!("Can't show soft input after NativeActivity has been destroyed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: `.attach_current_thread()` will also handle catching any Java exceptions that
|
||||
// might be thrown by the JNI calls we make.
|
||||
let res = self
|
||||
.jvm
|
||||
.attach_current_thread(|env| -> jni::errors::Result<()> {
|
||||
let activity = env.as_cast::<Activity>(self.activity.as_ref())?;
|
||||
|
||||
let ims = Context::INPUT_METHOD_SERVICE(env)?;
|
||||
let im_manager = activity.as_context().get_system_service(env, ims)?;
|
||||
let im_manager = InputMethodManager::cast_local(env, im_manager)?;
|
||||
let jni_window = activity.get_window(env)?;
|
||||
let view = jni_window.get_decor_view(env)?;
|
||||
let flags = if show_implicit {
|
||||
ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT as i32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
im_manager.show_soft_input(env, view, flags)?;
|
||||
Ok(())
|
||||
});
|
||||
if let Err(err) = res {
|
||||
log::warn!("Failed to show soft input: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
|
||||
let na = self.native_activity();
|
||||
unsafe {
|
||||
let flags = if hide_implicit_only {
|
||||
ndk_sys::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY
|
||||
} else {
|
||||
0
|
||||
};
|
||||
ndk_sys::ANativeActivity_hideSoftInput(na as *mut _, flags);
|
||||
let guard = self.native_activity.mutex.lock().unwrap();
|
||||
let na = guard.activity;
|
||||
if na.is_null() {
|
||||
log::error!("Can't hide soft input after NativeActivity has been destroyed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: `.attach_current_thread()` will also handle catching any Java exceptions that
|
||||
// might be thrown by the JNI calls we make.
|
||||
let res = self
|
||||
.jvm
|
||||
.attach_current_thread(|env| -> jni::errors::Result<()> {
|
||||
let activity = env.as_cast::<Activity>(self.activity.as_ref())?;
|
||||
|
||||
let ims = Context::INPUT_METHOD_SERVICE(env)?;
|
||||
let imm_obj = activity.as_context().get_system_service(env, ims)?;
|
||||
let imm = InputMethodManager::cast_local(env, imm_obj)?;
|
||||
|
||||
let window = activity.get_window(env)?;
|
||||
let decor = window.get_decor_view(env)?;
|
||||
let token = decor.get_window_token(env)?;
|
||||
|
||||
// HIDE_IMPLICIT_ONLY == 1, HIDE_NOT_ALWAYS == 2
|
||||
let flags = if hide_implicit_only { 1 } else { 0 };
|
||||
|
||||
let _hidden = imm.hide_soft_input_from_window(env, token, flags)?;
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("Failed to hide soft input: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable_motion_axis(&self, _axis: input::Axis) {
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn set_text_input_state(&self, _state: TextInputState) {
|
||||
// NOP: Unsupported
|
||||
}
|
||||
|
||||
// TODO: move into a trait
|
||||
pub fn set_ime_editor_info(
|
||||
&self,
|
||||
_input_type: InputType,
|
||||
_action: TextInputAction,
|
||||
_options: ImeOptions,
|
||||
) {
|
||||
// NOP: Unsupported
|
||||
}
|
||||
|
||||
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 = device_key_character_map(self.jvm.clone(), device_id)?;
|
||||
vacant.insert(character_map.clone());
|
||||
character_map
|
||||
}
|
||||
};
|
||||
|
||||
Ok(key_map)
|
||||
}
|
||||
|
||||
pub fn enable_motion_axis(&self, _axis: Axis) {
|
||||
// NOP - The InputQueue API doesn't let us optimize which axis values are read
|
||||
}
|
||||
|
||||
pub fn disable_motion_axis(&self, _axis: input::Axis) {
|
||||
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<F>(&self, mut callback: F)
|
||||
where
|
||||
F: FnMut(&input::InputEvent) -> InputStatus,
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
*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);
|
||||
let queue = match queue {
|
||||
Some(queue) => queue,
|
||||
None => return,
|
||||
.looper_attached_input_queue(self.looper_as_ptr(), 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> {
|
||||
let guard = self.native_activity.mutex.lock().unwrap();
|
||||
let na = guard.activity;
|
||||
if na.is_null() {
|
||||
log::error!("Can't get internal data path after NativeActivity has been destroyed");
|
||||
return None;
|
||||
}
|
||||
unsafe { util::try_get_path_from_ptr((*na).internalDataPath) }
|
||||
}
|
||||
|
||||
pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
|
||||
let guard = self.native_activity.mutex.lock().unwrap();
|
||||
let na = guard.activity;
|
||||
if na.is_null() {
|
||||
log::error!("Can't get external data path after NativeActivity has been destroyed");
|
||||
return None;
|
||||
}
|
||||
unsafe { util::try_get_path_from_ptr((*na).externalDataPath) }
|
||||
}
|
||||
|
||||
pub fn obb_path(&self) -> Option<std::path::PathBuf> {
|
||||
let guard = self.native_activity.mutex.lock().unwrap();
|
||||
let na = guard.activity;
|
||||
if na.is_null() {
|
||||
log::error!("Can't get OBB path after NativeActivity has been destroyed");
|
||||
return None;
|
||||
}
|
||||
unsafe { util::try_get_path_from_ptr((*na).obbPath) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct InputReceiver {
|
||||
queue: Option<InputQueue>,
|
||||
}
|
||||
|
||||
impl From<Arc<InputReceiver>> for InputIteratorInner<'_> {
|
||||
fn from(receiver: Arc<InputReceiver>) -> Self {
|
||||
Self {
|
||||
receiver,
|
||||
_lifetime: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InputIteratorInner<'a> {
|
||||
// Held to maintain exclusive access to buffered input events
|
||||
receiver: Arc<InputReceiver>,
|
||||
_lifetime: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl InputIteratorInner<'_> {
|
||||
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 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.
|
||||
// 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
|
||||
//
|
||||
while let Ok(Some(event)) = queue.get_event() {
|
||||
if let Some(ndk_event) = queue.pre_dispatch(event) {
|
||||
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))
|
||||
@@ -380,8 +582,12 @@ impl AndroidAppInner {
|
||||
ndk::event::InputEvent::KeyEvent(e) => {
|
||||
input::InputEvent::KeyEvent(input::KeyEvent::new(e))
|
||||
}
|
||||
_ => todo!("NDK added a new type"),
|
||||
};
|
||||
let handled = callback(&event);
|
||||
|
||||
// `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) => {
|
||||
@@ -390,24 +596,26 @@ impl AndroidAppInner {
|
||||
input::InputEvent::KeyEvent(e) => {
|
||||
ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
queue.finish_event(ndk_event, matches!(handled, InputStatus::Handled));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
log::trace!("queue: no more events");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
|
||||
let na = self.native_activity();
|
||||
unsafe { util::try_get_path_from_ptr((*na).internalDataPath) }
|
||||
}
|
||||
|
||||
pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
|
||||
let na = self.native_activity();
|
||||
unsafe { util::try_get_path_from_ptr((*na).externalDataPath) }
|
||||
}
|
||||
|
||||
pub fn obb_path(&self) -> Option<std::path::PathBuf> {
|
||||
let na = self.native_activity();
|
||||
unsafe { util::try_get_path_from_ptr((*na).obbPath) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
jni::bind_java_type! { pub(crate) IBinder => "android.os.IBinder" }
|
||||
jni::bind_java_type! {
|
||||
pub(crate) View => "android.view.View",
|
||||
type_map {
|
||||
IBinder => "android.os.IBinder",
|
||||
},
|
||||
methods {
|
||||
fn get_window_token() -> IBinder,
|
||||
}
|
||||
}
|
||||
jni::bind_java_type! {
|
||||
pub(crate) InputMethodManager => "android.view.inputmethod.InputMethodManager",
|
||||
type_map {
|
||||
View => "android.view.View",
|
||||
IBinder => "android.os.IBinder",
|
||||
},
|
||||
methods {
|
||||
fn show_soft_input(view: View, flags: i32) -> bool,
|
||||
fn hide_soft_input_from_window(window_token: IBinder, flags: i32) -> bool,
|
||||
}
|
||||
}
|
||||
jni::bind_java_type! {
|
||||
pub(crate) Context => "android.content.Context",
|
||||
fields {
|
||||
#[allow(non_snake_case)]
|
||||
static INPUT_METHOD_SERVICE: JString
|
||||
},
|
||||
methods {
|
||||
fn get_system_service(service_name: JString) -> JObject,
|
||||
}
|
||||
}
|
||||
jni::bind_java_type! {
|
||||
pub(crate) Window => "android.view.Window",
|
||||
type_map {
|
||||
View => "android.view.View",
|
||||
},
|
||||
methods {
|
||||
fn get_decor_view() -> View,
|
||||
}
|
||||
}
|
||||
jni::bind_java_type! {
|
||||
pub(crate) Activity => "android.app.Activity",
|
||||
type_map {
|
||||
Context => "android.content.Context",
|
||||
Window => "android.view.Window",
|
||||
},
|
||||
is_instance_of {
|
||||
context: Context
|
||||
},
|
||||
methods {
|
||||
fn get_window() -> Window,
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly initialize the JNI bindings so we can get and early, upfront,
|
||||
// error if something is wrong.
|
||||
pub(crate) fn jni_init(env: &jni::Env) -> jni::errors::Result<()> {
|
||||
let _ = IBinderAPI::get(env, &Default::default())?;
|
||||
let _ = ViewAPI::get(env, &Default::default())?;
|
||||
let _ = InputMethodManagerAPI::get(env, &Default::default())?;
|
||||
let _ = ContextAPI::get(env, &Default::default())?;
|
||||
let _ = WindowAPI::get(env, &Default::default())?;
|
||||
let _ = ActivityAPI::get(env, &Default::default())?;
|
||||
let _ = crate::input::AKeyCharacterMapAPI::get(env, &Default::default())?;
|
||||
let _ = crate::input::AInputDeviceAPI::get(env, &Default::default())?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -32,7 +32,7 @@ pub(crate) fn android_log(level: Level, tag: &CStr, msg: &CStr) {
|
||||
}
|
||||
|
||||
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") };
|
||||
let rust_panic = c"RustPanic";
|
||||
|
||||
if let Some(panic) = panic.downcast_ref::<String>() {
|
||||
if let Ok(msg) = CString::new(panic.clone()) {
|
||||
@@ -43,10 +43,7 @@ pub(crate) fn log_panic(panic: Box<dyn std::any::Any + Send>) {
|
||||
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")
|
||||
});
|
||||
android_log(Level::Error, rust_panic, c"UnknownPanic");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::ptr::NonNull;
|
||||
|
||||
#[cfg(doc)]
|
||||
use crate::AndroidApp;
|
||||
|
||||
/// A means to wake up the main thread while it is blocked waiting for I/O
|
||||
pub struct AndroidAppWaker {
|
||||
looper: NonNull<ndk_sys::ALooper>,
|
||||
}
|
||||
|
||||
impl Clone for AndroidAppWaker {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe { ndk_sys::ALooper_acquire(self.looper.as_ptr()) }
|
||||
Self {
|
||||
looper: self.looper,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AndroidAppWaker {
|
||||
fn drop(&mut self) {
|
||||
unsafe { ndk_sys::ALooper_release(self.looper.as_ptr()) }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for AndroidAppWaker {}
|
||||
unsafe impl Sync for AndroidAppWaker {}
|
||||
|
||||
impl AndroidAppWaker {
|
||||
/// Acquire a ref to a looper as a means to be able to wake up the event loop
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `ALooper` pointer must be valid and not null.
|
||||
pub(crate) unsafe fn new(looper: *mut ndk_sys::ALooper) -> Self {
|
||||
assert!(!looper.is_null(), "looper pointer must not be null");
|
||||
unsafe {
|
||||
// Give the waker its own reference to the looper
|
||||
ndk_sys::ALooper_acquire(looper);
|
||||
AndroidAppWaker {
|
||||
looper: NonNull::new_unchecked(looper),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea
|
||||
/.vscode
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,19 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,7 +0,0 @@
|
||||
<?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,679 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "agdk-mainloop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"android-activity",
|
||||
"jni",
|
||||
"paranoid-android",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.6.0"
|
||||
dependencies = [
|
||||
"android-properties",
|
||||
"bitflags",
|
||||
"cc",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"num_enum",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-properties"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"jni-sys 0.3.0",
|
||||
"log",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "paranoid-android"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "101795d63d371b43e38d6e7254677657be82f17022f7f7893c268f33ac0caadc"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"ndk-sys 0.5.0+25.2.9519653",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.0.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.4+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.9+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
@@ -6,12 +6,20 @@ 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"] }
|
||||
ndk-sys = "0.4"
|
||||
ndk = "0.7"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"fmt",
|
||||
"env-filter",
|
||||
"tracing-log",
|
||||
] }
|
||||
paranoid-android = "0.2"
|
||||
tracing-log = "0.2"
|
||||
|
||||
android-activity = { path = "../../android-activity", features = [
|
||||
"game-activity",
|
||||
] }
|
||||
jni = "0.22"
|
||||
|
||||
[lib]
|
||||
name="main"
|
||||
crate_type=["cdylib"]
|
||||
name = "main"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
@@ -13,5 +13,5 @@ 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.agdkmainloop/.MainActivity
|
||||
adb shell am start -n com.github.rust_mobile.agdkmainloop/.MainActivity
|
||||
```
|
||||
|
||||
@@ -3,22 +3,19 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
ndkVersion "25.2.9519653"
|
||||
compileSdk 31
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "co.realfit.agdkmainloop"
|
||||
minSdk 28
|
||||
targetSdk 31
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
applicationId = "com.github.rust_mobile.agdkmainloop"
|
||||
minSdk = 31
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled = false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug {
|
||||
@@ -30,34 +27,17 @@ android {
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
namespace 'co.realfit.agdkmainloop'
|
||||
namespace = 'com.github.rust_mobile.agdkmainloop'
|
||||
}
|
||||
|
||||
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"
|
||||
implementation 'androidx.appcompat:appcompat:1.7.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"
|
||||
implementation "androidx.games:games-activity:4.4.0"
|
||||
// Note: don't include game-text-input separately, since it's integrated into game-activity
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="AGDK Mainloop"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.RustTemplate">
|
||||
android:theme="@style/ActivityTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
|
||||
|
||||
|
After Width: | Height: | Size: 63 KiB |
@@ -1,6 +1,5 @@
|
||||
package co.realfit.agdkmainloop;
|
||||
package com.github.rust_mobile.agdkmainloop;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
@@ -11,7 +10,6 @@ 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;
|
||||
|
||||
@@ -35,10 +33,8 @@ public class MainActivity extends GameActivity {
|
||||
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;
|
||||
}
|
||||
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();
|
||||
@@ -63,6 +59,11 @@ public class MainActivity extends GameActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
hideSystemUI();
|
||||
}
|
||||
|
||||
public boolean isGooglePlayGames() {
|
||||
PackageManager pm = getPackageManager();
|
||||
return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,5 +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" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +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" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,16 +0,0 @@
|
||||
<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,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#C8C49F</color>
|
||||
</resources>
|
||||
@@ -1,16 +1,8 @@
|
||||
<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. -->
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="ActivityTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- For full-screen layout, cutout support -->
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,10 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '8.0.0' apply false
|
||||
id 'com.android.library' version '8.0.0' apply false
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
id 'com.android.application' version '9.1.0' apply false
|
||||
id 'com.android.library' version '9.1.0' apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
# 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
|
||||
# Enable Gradle Daemon
|
||||
org.gradle.daemon=true
|
||||
|
||||
# JVM arguments
|
||||
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
# Enable AndroidX
|
||||
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
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonFinalResIds=false
|
||||
# Build caching and parallel execution
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
# Incremental Kotlin compilation
|
||||
kotlin.incremental=true
|
||||
|
||||
# File system watching for faster builds
|
||||
org.gradle.unsafe.watch-fs=true
|
||||
@@ -1,6 +1,7 @@
|
||||
#Mon May 02 15:39:12 BST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,69 +15,104 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# 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
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
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"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# 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
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +122,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
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"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,88 +133,120 @@ 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.
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
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
|
||||
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" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
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, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# 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"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# 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"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
@@ -1,89 +1,94 @@
|
||||
@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
|
||||
@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
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@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=.
|
||||
@rem This is normally unused
|
||||
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% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
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% equ 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!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
||||
@@ -1,17 +1,154 @@
|
||||
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
|
||||
use log::info;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
#[no_mangle]
|
||||
use android_activity::{
|
||||
input::{InputEvent, KeyAction, KeyEvent, KeyMapChar, MotionAction},
|
||||
ndk, ndk_sys, AndroidApp, InputStatus, MainEvent, OnCreateState, PollEvent,
|
||||
};
|
||||
use jni::{
|
||||
objects::{JObject, JString},
|
||||
refs::Global,
|
||||
vm::JavaVM,
|
||||
};
|
||||
use tracing::{error, info};
|
||||
|
||||
jni::bind_java_type! { Context => "android.content.Context" }
|
||||
jni::bind_java_type! {
|
||||
Activity => "android.app.Activity",
|
||||
type_map {
|
||||
Context => "android.content.Context",
|
||||
},
|
||||
is_instance_of {
|
||||
context: Context
|
||||
},
|
||||
}
|
||||
|
||||
jni::bind_java_type! {
|
||||
Toast => "android.widget.Toast",
|
||||
type_map {
|
||||
Context => "android.content.Context",
|
||||
},
|
||||
methods {
|
||||
static fn make_text(context: Context, text: JCharSequence, duration: i32) -> Toast,
|
||||
fn show(),
|
||||
}
|
||||
}
|
||||
|
||||
// Note: The jni bindings will actually initialize lazily but it can be helpful
|
||||
// to initialize explicitly to get an up-front error in case there is an issue
|
||||
// (such as a typo with a method name or incorrect signature) rather than having
|
||||
// an unpredictable error when using the binding.
|
||||
fn jni_init(env: &jni::Env) -> jni::errors::Result<()> {
|
||||
let _ = ContextAPI::get(env, &Default::default())?;
|
||||
let _ = ActivityAPI::get(env, &Default::default())?;
|
||||
let _ = ToastAPI::get(env, &Default::default())?;
|
||||
// .. call other `get` functions for other bindings here as needed ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Called while Activity.onCreate is running
|
||||
// May be called multiple times if the activity is destroyed and recreated.
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_on_create(state: &OnCreateState) {
|
||||
static ONCE: OnceLock<()> = OnceLock::new();
|
||||
ONCE.get_or_init(|| {
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
unsafe { std::env::set_var("RUST_BACKTRACE", "full") };
|
||||
|
||||
const DEFAULT_ENV_FILTER: &str = "debug,wgpu_hal=info,winit=info,naga=info";
|
||||
let filter_layer = tracing_subscriber::EnvFilter::new(DEFAULT_ENV_FILTER);
|
||||
let android_layer = paranoid_android::layer(env!("CARGO_PKG_NAME"))
|
||||
.with_ansi(false)
|
||||
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
|
||||
.with_thread_names(true);
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(android_layer)
|
||||
.init();
|
||||
});
|
||||
|
||||
let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) };
|
||||
// Note: from here on we can also rely on `JavaVM::singleton()` now that we know it's been initialized.
|
||||
|
||||
let activity = state.activity_as_ptr() as jni::sys::jobject;
|
||||
if let Err(err) = vm.attach_current_thread(|env| -> jni::errors::Result<()> {
|
||||
// Initialize JNI bindings
|
||||
jni_init(env).expect("Failed to initialize JNI bindings");
|
||||
|
||||
// SAFETY:
|
||||
// - The reference / pointer is at least valid until we return
|
||||
// - By creating a `Cast` we ensure we can't accidentally delete the reference
|
||||
let _activity = unsafe { env.as_cast_raw::<Global<JObject>>(&activity)? };
|
||||
// Do something with the activity on the Java main thread, such as call a method or access a field
|
||||
Ok(())
|
||||
}) {
|
||||
error!("Failed to interact with Android SDK on Java main thread: {err:?}");
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"android_on_create called on thread {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
info!(
|
||||
"android_on_create called on thread {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
}
|
||||
|
||||
enum ToastDuration {
|
||||
Short = 0,
|
||||
Long = 1,
|
||||
}
|
||||
|
||||
fn send_toast(outer_app: &AndroidApp, msg: impl AsRef<str>, duration: ToastDuration) {
|
||||
let app = outer_app.clone();
|
||||
let msg = msg.as_ref().to_string();
|
||||
outer_app.run_on_java_main_thread(Box::new(move || {
|
||||
// We initialize JavaVM::singleton at the start of `android_main`
|
||||
let jvm = jni::JavaVM::singleton().expect("Failed to get singleton JavaVM instance");
|
||||
// We use `with_top_local_frame` as a minor optimization because it's guaranteed by
|
||||
// `run_on_java_main_thread` that we already have an underlying JNI attachment and local
|
||||
// frame. It would also be perfectly reasonable to use `jvm.attach_current_thread()`.
|
||||
if let Err(err) = jvm.with_top_local_frame(|env| -> jni::errors::Result<()> {
|
||||
let activity: jni::sys::jobject = app.activity_as_ptr() as _;
|
||||
let activity = unsafe { env.as_cast_raw::<Global<Activity>>(&activity)? };
|
||||
|
||||
let message = JString::new(env, &msg)?;
|
||||
let toast = Toast::make_text(env, activity.as_ref(), &message, duration as i32)?;
|
||||
info!("Showing Toast from Rust JNI callback: {msg}");
|
||||
toast.show(env)?;
|
||||
|
||||
Ok(())
|
||||
}) {
|
||||
error!("Failed to execute callback on main thread: {err:?}");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Called on a dedicated Activity main loop thread, spawned after `android_on_create` returns
|
||||
// May be called multiple times if the activity is destroyed and recreated.
|
||||
#[unsafe(no_mangle)]
|
||||
fn android_main(app: AndroidApp) {
|
||||
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
|
||||
eprintln!(
|
||||
"android_main started on thread {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
info!(
|
||||
"android_main started on thread {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
|
||||
let mut quit = false;
|
||||
let mut redraw_pending = true;
|
||||
let mut native_window: Option<ndk::native_window::NativeWindow> = None;
|
||||
|
||||
let mut combining_accent = None;
|
||||
|
||||
send_toast(&app, "Hello from Rust on Android!", ToastDuration::Long);
|
||||
|
||||
while !quit {
|
||||
app.poll_events(
|
||||
Some(std::time::Duration::from_secs(1)), /* timeout */
|
||||
Some(std::time::Duration::from_secs(2)), /* timeout */
|
||||
|event| {
|
||||
match event {
|
||||
PollEvent::Wake => {
|
||||
@@ -35,6 +172,7 @@ fn android_main(app: AndroidApp) {
|
||||
info!("Resumed with saved state = {uri:#?}");
|
||||
}
|
||||
}
|
||||
send_toast(&app, "Resumed!", ToastDuration::Short);
|
||||
}
|
||||
MainEvent::InitWindow { .. } => {
|
||||
native_window = app.native_window();
|
||||
@@ -42,6 +180,7 @@ fn android_main(app: AndroidApp) {
|
||||
}
|
||||
MainEvent::TerminateWindow { .. } => {
|
||||
native_window = None;
|
||||
redraw_pending = false;
|
||||
}
|
||||
MainEvent::WindowResized { .. } => {
|
||||
redraw_pending = true;
|
||||
@@ -54,8 +193,12 @@ fn android_main(app: AndroidApp) {
|
||||
}
|
||||
MainEvent::ConfigChanged { .. } => {
|
||||
info!("Config Changed: {:#?}", app.config());
|
||||
send_toast(&app, "Config Changed!", ToastDuration::Short);
|
||||
}
|
||||
MainEvent::LowMemory => {
|
||||
info!("Low Memory Warning");
|
||||
send_toast(&app, "Low Memory!", ToastDuration::Short);
|
||||
}
|
||||
MainEvent::LowMemory => {}
|
||||
|
||||
MainEvent::Destroy => quit = true,
|
||||
_ => { /* ... */ }
|
||||
@@ -68,11 +211,107 @@ fn android_main(app: AndroidApp) {
|
||||
if let Some(native_window) = &native_window {
|
||||
redraw_pending = false;
|
||||
|
||||
// Handle input
|
||||
app.input_events(|event| {
|
||||
info!("Input Event: {event:?}");
|
||||
InputStatus::Unhandled
|
||||
});
|
||||
// Handle input, via a lending iterator
|
||||
match app.input_events_iter() {
|
||||
Ok(mut iter) => loop {
|
||||
info!("Checking for next input event...");
|
||||
if !iter.next(|event| {
|
||||
match event {
|
||||
InputEvent::KeyEvent(key_event) => {
|
||||
let combined_key_char = character_map_and_combine_key(
|
||||
&app,
|
||||
key_event,
|
||||
&mut combining_accent,
|
||||
);
|
||||
info!("KeyEvent: combined key: {combined_key_char:?}")
|
||||
}
|
||||
InputEvent::MotionEvent(motion_event) => {
|
||||
println!("action = {:?}", motion_event.action());
|
||||
match motion_event.action() {
|
||||
MotionAction::Up => {
|
||||
let pointer = motion_event.pointer_index();
|
||||
let pointer =
|
||||
motion_event.pointer_at_index(pointer);
|
||||
let x = pointer.x();
|
||||
let y = pointer.y();
|
||||
|
||||
println!("POINTER UP {x}, {y}");
|
||||
|
||||
if x < 500.0 && y < 500.0 {
|
||||
println!("Requesting to show keyboard");
|
||||
send_toast(
|
||||
&app,
|
||||
"Requesting to show keyboard",
|
||||
ToastDuration::Short,
|
||||
);
|
||||
app.show_soft_input(true);
|
||||
} else if x >= 500.0 && y < 500.0 {
|
||||
println!("Requesting to hide keyboard");
|
||||
send_toast(
|
||||
&app,
|
||||
"Requesting to hide keyboard",
|
||||
ToastDuration::Short,
|
||||
);
|
||||
app.hide_soft_input(false);
|
||||
} else {
|
||||
send_toast(
|
||||
&app,
|
||||
format!("POINTER UP {x}, {y}"),
|
||||
ToastDuration::Short,
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let num_pointers = motion_event.pointer_count();
|
||||
for i in 0..num_pointers {
|
||||
let pointer = motion_event.pointer_at_index(i);
|
||||
|
||||
println!(
|
||||
"Pointer[{i}]: id={}, time={}, x={}, y={}",
|
||||
pointer.pointer_id(),
|
||||
motion_event.event_time(),
|
||||
pointer.x(),
|
||||
pointer.y(),
|
||||
);
|
||||
for sample in pointer.history() {
|
||||
println!(
|
||||
" History[{}]: x={}, y={}, time={:?}",
|
||||
sample.history_index(),
|
||||
sample.x(),
|
||||
sample.y(),
|
||||
sample.event_time()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
InputEvent::TextEvent(state) => {
|
||||
info!("Input Method State: {state:?}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
info!("Input Event: {event:?}");
|
||||
app.run_on_java_main_thread(Box::new(move || {
|
||||
println!(
|
||||
"Callback on main thread {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
info!(
|
||||
"Callback on main thread {:?}",
|
||||
std::thread::current().id()
|
||||
);
|
||||
}));
|
||||
InputStatus::Unhandled
|
||||
}) {
|
||||
info!("No more input available");
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Failed to get input events iterator: {err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Render...");
|
||||
dummy_render(native_window);
|
||||
@@ -83,6 +322,77 @@ fn android_main(app: AndroidApp) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent
|
||||
///
|
||||
/// This shows how to take a `KeyEvent` and look up its corresponding `KeyCharacterMap` and
|
||||
/// use that to try and map the `key_code` + `meta_state` to a unicode character or a
|
||||
/// dead key that be combined with the next key press.
|
||||
fn character_map_and_combine_key(
|
||||
app: &AndroidApp,
|
||||
key_event: &KeyEvent,
|
||||
combining_accent: &mut Option<char>,
|
||||
) -> Option<KeyMapChar> {
|
||||
let device_id = key_event.device_id();
|
||||
|
||||
let key_map = match app.device_key_character_map(device_id) {
|
||||
Ok(key_map) => key_map,
|
||||
Err(err) => {
|
||||
error!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match key_map.get(key_event.key_code(), key_event.meta_state()) {
|
||||
Ok(KeyMapChar::Unicode(unicode)) => {
|
||||
// Only do dead key combining on key down
|
||||
if key_event.action() == KeyAction::Down {
|
||||
let combined_unicode = if let Some(accent) = combining_accent {
|
||||
match key_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) => {
|
||||
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))
|
||||
} else {
|
||||
Some(KeyMapChar::Unicode(unicode))
|
||||
}
|
||||
}
|
||||
Ok(KeyMapChar::CombiningAccent(accent)) => {
|
||||
if key_event.action() == KeyAction::Down {
|
||||
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
|
||||
*combining_accent = Some(accent);
|
||||
}
|
||||
Some(KeyMapChar::CombiningAccent(accent))
|
||||
}
|
||||
Ok(KeyMapChar::None) => {
|
||||
// Leave any combining_accent state in tact (seems to match how other
|
||||
// Android apps work)
|
||||
info!("KeyEvent: Pressed non-unicode key");
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
error!("KeyEvent: Failed to get key map character: {err:?}");
|
||||
*combining_accent = None;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Post a NOP frame to the window
|
||||
///
|
||||
/// Since this is a bare minimum test app we don't depend
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
/.idea
|
||||
/.vscode
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||