Compare commits

..

105 Commits

Author SHA1 Message Date
John Nunley 37730905d1 feat: Add waker() method to AndroidAppWaker
This commit adds an "into_waker()" method to AndroidAppWaker. It converts it
into an `std::task::Waker`, which is the type of waker used by
asynchronous tasks for scheduling. The goal is to allow AndroidAppWaker
to be easily used to set up an asynchronous context.

The implementation is a straightforward wrapper for
`ALooper_acquire/release/wake()`.

Signed-off-by: John Nunley <dev@notgull.net>
Co-authored-by: Robert Bragg <robert@sixbynine.org>
2026-03-19 22:17:10 +00:00
Jan Češpivo c3ed6ba77d Use InputMethodManager#showSoftInput to show_soft_input
This updates `AndroidApp::show/hide_soft_input` to be implemented
manually with JNI (instead of `ANativeActivity_show/hideSoftInput`) so
that we can pass the root, decor view to
`InputMethodManager.showSoftInput` instead of the private
`mNativeContentView` created by `NativeActivity`.

Unlike the private `mNativeContentView`, the root decor view is
considered to be the current "served" view for a vanilla
`NativeActivity`-based application.

Co-authored-by: Robert Bragg <robert@sixbynine.org>
2026-03-19 21:35:20 +00:00
Robert Bragg c1d00b9191 Support an optional 'android_on_create' entrypoint
This adds support for an optional `android_on_crate` entrypoint which is
called from within the Activity.onCreate native method callback from the
Java main / UI thread.

This gives applications an opportunity initialize state while the
`Activity`'s class loader is on the stack, so `FindClass` will be able
to find application classes.

This can be a more-convenient place to initialize JNI bindings, without
needing to explicitly get the class loader from the Activity to be able
to look up application classes from the android_main thread.

This may also be convenient for initially using JNI to interact with
your new Activity in case you need to use SDK APIs that are only safe to
use from the Java main / UI thread.

The moves the thread initialization functions out of util.rs into a new
init.rs

While adding documentation for this feature, this also does a
more-general pass over the top-level crate documentation to try and
ensure it's up-to-date.

Fixes: #169
Addresses: #82
2026-03-19 16:33:15 +00:00
Robert Bragg 4acfd2d59c Import android-games-sdk changes for 4.0.0
This imports the SDK from commit 090732c3ca7d8b47ed39e028081d685e4097db7f, from:
https://github.com/rust-mobile/android-games-sdk/commits/android-activity-4.0.0

This imports a patch to revert the recent addition of a
`_rust_glue_on_create_hook` in favour of fixing the Rust wrapper for
`GameActivity_onCreate` which is more consistent with the
`ANativeActivity_onCreate` entrypoint that we have in the `native-activity`
backend.

This also:
- Fixes a related rerun-if-changed path in build.rs
- Removes the reference to _rust_glue_on_create_hook src/game_activity/mod.rs
2026-03-19 00:57:16 +00:00
Robert Bragg b042af60f2 Drop Weak<WaitableNativeActivityState> in on_destroy
When we know we're done with the `Weak` reference that is associated with
the `NativeActivity` callbacks we make sure to drop the `Weak` reference
so that the underlying allocation for the `WaitableNativeActivityState`
can be freed.

This also updates `try_with_waitable_activity_ref` to be more careful
about converting the `Weak` ref back into a raw pointer _before_ calling
the handler, just in case the handler triggers a panic and unwinds
(where we wouldn't want to lose/Drop our weak ref).
2026-03-19 00:48:14 +00:00
Robert Bragg 9163368955 Track GameActivity android_app pointer lifetime more carefully
Most of the same issues found in the native-activity backend when
working on #234 (to safely drop ANativeActivity via onDestroy callback)
also apply to the game-activity backend, which this PR addresses.

This ensures that the game-activity backend cleanly drops its
`android_app` pointer once we're notified that the `GameActivity` is being
destroyed and adds a mutex around the pointer that guarantees that
it can't be freed while it's being dereferenced (because the same
lock is required to respond to the onDestroy callback where the
state gets freed).

This makes a number of backend details consistent with the
native-activity backend:
- The backend retains its own Looper reference instead of relying on
  the android_app reference.
- The backend allocates its own JNI global reference for the Activity,
  instead of relying on the android_app reference.

Since this needed to add a hook to clear the android_app pointer after
dispatching the callback for `MainEvent::Destroy` it also made sense to
fix the MainEvent::TerminateWindow hook for clearing our `NativeWindow`
so it also happens _after_ the callback (as the API docs state).

Testing these changes with a minimal agdk-mainloop and agdk-egui example
I see it's now possible to cleanly handle repeated activity start ->
destroy -> start -> destroy cycles (e.g. due to config changes
triggering a recreation of the activity). (When testing egui I did also
have to patch Winit to ensure it exits the loop when receiving a Destroy
event)

Fixes: #235
Fixes: #162
2026-03-18 01:28:52 +00:00
Robert Bragg 91cf9d7229 Track ANativeActivity pointer lifetime more carefully
Once `on_destroy()` returns then the `NativeActivity.java` code will call an
`unloadNativeCode` native method that will `delete` the `ANativeActivity`
and invalidate any pointers we hold.

Considering the possibility that an `AndroidApp` could be retained
beyond the lifetime of the original `NativeActivity`, this ensures we
always hold the `WaitableNativeActivityState::mutex` before
dereferencing this pointer and ensures we clear the pointer before
returning from `on_destroy` so we're also able to perform `null` pointer
checks before dereferencing.

Considering that `AndroidApp::vm_as_ptr` previously depended on
dereferencing the `ANativeActivity`, this updates it to instead use
`JavaVM::singleton()` which we guarantee will be initialized before the
`AndroidApp` is created.

Considering that `AndroidApp::activity_as_ptr()` promises to return a
global reference that remains valid for the lifetime of the
`AndroidApp`, but the `ANativeActivity::clazz` reference is deleted
after `on_destroy()` returns, we now create our own `Global` reference
for the `Activity` that is owned by `AndroidAppInner`.
2026-03-17 13:38:12 +00:00
Robert Bragg ae5553288c Return Application AssetManager from AndroidApp::asset_manager
This makes sure that the `AssetManager` we return from
`AndroidApp::asset_manager` can be retained with a static lifetime and
never become a wrapper for an invalid pointer.

The key change here is that we now return the Application AssetManager
(i.e. from Application.getAssets()) instead of the Activity
AssetManager.

Theoretically there could be some applications that could associate an
Activity AssetManager with unique resources but that's not expected to
be common (and at least no expected to affect anyone currently using
`AndroidApp::asset_manager`).

As part of the `APP_ONCE` initialization in `init_android_main_thread`
we now get a global reference to the Application AssetManager and get
the corresponding AAssetManager that we can trust will be valid for the
lifetime of the process since we leak the global reference.

Note: The Application `AssetManager` is logically a process-wide
resource and so the leaked global is just a technical formality to
ensure it can't be garbage collected, but that's assumed to be
redundant.

Note: If anyone _strictly_ needs the `Activity` `AssetManager` then they
could at least resort to calling `Activity.getAssets()` via JNI
manually, but perhaps we can later consider adding a separate
`AndroidApp::activity_asset_manager()` that will pair an `AAssetManager`
pointer with a JNI global reference to ensure the pointer remains valid.

Fixes #161
2026-03-17 11:25:16 +00:00
Robert Bragg 0c32e9d8fa Add AndroidApp::run_on_java_main_thread
This makes it easy to schedule boxed closures to be run on the Java main
/ ui thread.

When the closure is run then:
- Any panic will be caught, so we don't unwind into the Looper and abort
  the process
- The JVM will be attached (for JNI) and any exceptions that are thrown
  will be caught and logged as errors.
- A JNI stack frame will be pushed and popped before running your closure
 (so you don't have to worry about leaking local JNI references)

This bumps the jni dependency to 0.22.4 because that version adds a
`JCharSequence` binding that we use in the `Toast` example in the
documentation.
2026-03-17 10:27:54 +00:00
Robert Bragg 43de2770b9 use Env::exception_catch in clear_and_map_exception_to_err
This simplifies the `clear_and_map_exception_to_err` utility so it's based
on `jni::Env::exception_catch`
2026-03-12 22:31:28 +00:00
Mark Kimsal 2a05cd2763 Expose Java main/UI Looper via AndroidApp::java_main_looper
This makes it possible to register file descriptors that can wake up the
Java main / UI thread as well as callbacks that will run on the Java
main / UI thread.

Although it can be common to refer to this thread as the "main" thread,
we choose to explicitly refer to it as the "java main" thread thread in
the API to avoid confusion with the Rust thread that runs
"android_main".

Co-authored-by: Robert Bragg <robert@sixbynine.org>
2026-03-12 20:51:46 +00:00
Robert Bragg 0062cfc7a0 import android-games-sdk patches for mainLooper + onCreate hook
This imports the SDK from commit
30b8bfcc9a12942d1268820e8a83d7643e99ee92, from:
https://github.com/rust-mobile/android-games-sdk/commits/android-activity-4.0.0

this includes these patches:

# PATCH: Add mainLooper to android_app

Track the Looper for the Java main/UI thread in the android_app.

This makes it possible to add file descriptors and callbacks to the Java
UI Looper from the android_main thread.

This needs to be initialized by the android_native_app_glue before
spawning the android_main thread because the looper needs to be
discovered via `ALooper_forThread` while still running on the Java main
thread (in the onCreate callback).

# PATCH: Enable Rust glue to hook into onCreate

This declares an extern `_rust_glue_on_create_hook` that is called from
`GameActivity` `onCreate` native method callback, before the
`android_main` thread is spawned.

This gives Rust code an opportunity to run code and initialize state
while still running on the Java main/UI thread.

For example, this could be used to initialize JNI bindings while we can
assume that the current thread has an associated ClassLoader that will
be able to find application classes.

It may also be a convenient place to make some initial JNI calls into
Android SDK APIs that can only be used from the Java main thread.

# Updated import-games-sdk.sh to remove symlinks

While updating the SDK the import script has been updated to remove any
symlinks which make it difficult to build android-activity from Git on
Windows.

Note: the symlinks were redundant based on how the include paths were
already configured in `build.rs`
2026-03-10 21:07:08 +00:00
Robert Bragg 0f49d96fa0 Only init ndk-context once with an Application ref
Instead of initializing `ndk-context` with an `Activity` reference (for
the `android.context.Context` subclass) we now initialize with
an `android.app.Application` reference (also an
`android.context.Context` subclass).

The benefit of this is that we can strictly initialize `ndk-context`
once (via a `OnceLock`) so there's no risk of a panic in case an
application starts more than one Activity within the same process.

Fixes: #58
Fixes: #228
2026-03-10 13:41:39 +00:00
Robert Bragg 2b20da72bd Ensure AndroidAppWaker owns an ALooper reference
This ensures we call `ALooper_acquire` before `create_waker()` wraps the
Looper pointer with `AndroidAppWaker` and it also ensures that
`::clone()` and `::drop()` call `ALooper_acquire()` and
`ALooper_release()` respectively.

Contrary to what the comment for the `looper` member said previously, it
was not safe to assume that the application's looper pointer had a
`'static` lifetime.

The looper pointer would only be valid up until `android_main` returns,
but unlike a traditional `main()` function an `android_main()` runs
with respect to an `Activity` lifecycle and not a process lifecycle.

It's technically possible for `android_main()` to return (at which point
any looper stored in `'static` storage would have previously become an
invalid pointer) and then JNI could be used to re-enter Rust and
potentially try and dereference that invalid pointer.

This adds a shared implementation of `AndroidAppWaker` to `src/waker.rs`
instead of having each backend implement `AndroidAppWaker`.

Fixes: #226
2026-03-03 00:31:34 +00:00
dependabot[bot] f44d837bf7 build(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 21:04:48 +00:00
Robert Bragg 4ff35807fb Use ALooper_pollOnce instead of pollAll
The `ALooper_pollAll` API is deprecated and considering that `poll_events`
never promised the behaviour of `pollAll` we simply change the
implementation to use `ALoooper_pollOnce` and assume the caller is going
to anyway be calling `poll_events` within its own loop.

Note: `pollOnce` can still deliver multiple callback events, and the
"once" effectively just refers to only calling `epoll_wait` at most
once.

Considering winit for example, this should have no effect since winit
will be calling `poll_events` in a loop with no assumption about how
concurrent events could potentially be batched.

Fixes: #170
2026-03-02 21:03:47 +00:00
Robert Bragg ae24c96dcc set_ime_editor_info: accept an 'action: TextInputAction' arg
Makes it possible to configure the action of the IME enter key via
`set_ime_editor_info()`
2026-03-02 15:59:09 +00:00
Robert Bragg 42e0f88287 move ImeOptions + InputType to src/input.rs + fills out
This adds and documents the remaining `ImeOptions` (addressing TODO comment)

`ImeOptions` now has a getter/setter for the action, based on the
`TextInputAction` enum added in #216

There is also a separate `InputTypeClass` that lets you query the
mutually-exclusive type class bits from an `InputType`
2026-03-02 15:59:09 +00:00
William Casarin fdcf4ce28d input: add set_ime_editor_info
This corresponds to the GameActivity_setImeEditorInfo function on
GameActivity. This is not supported on NativeActivity.

Signed-off-by: William Casarin <jb55@jb55.com>
2026-03-02 15:59:09 +00:00
Robert Bragg 483164c333 Replace cesu8 with simd_cesu8 (consistent with jni 0.22.2)
The `cesu8` crate hasn't been updated for 10 years where as the
`simd_cesu8` crate is more actively maintained and is expected to have
better performance in all conditions:

<https://docs.rs/simd_cesu8/latest/simd_cesu8/#benchmarks>

This change is consistent with the `jni` crate which switched to using
`simd_cesu8` in the 0.22.2 release - so this avoids needing to build
two separate crates for mutf8 encoding.

This also addresses the current CI issue that comes from incorrectly
depending on `cesu8 = "1"` instead of `"1.1.0"` (which adds the
java/mutf8 APIs that we used). Previously this went unnoticed because
of the `jni` crate pulling in the correct minimum version.
2026-03-02 15:02:03 +00:00
Robert Bragg 279d73889f Avoid deprecated AttachConfig::name API
This was replaced by `AttachConfig::thread_name` in jni 0.22.2, which
takes a `&JNIStr` and doesn't require an extra allocation for the name
to be mutf8 encoded.
2026-03-02 14:55:34 +00:00
Robert Bragg 7577299c84 Update to jni 0.22 and jni-sys 0.4.1
This adds a common init_android_main_thread() utility that's called by
both backends in order to get the ClassLoader from the Activity and
associate that with the thread via `JThreadthread::set_context_class_loader`
(which jni 0.22 can use automatically when loading classes).

This change notably starts to use the `jni::bind_java_type!` macro for the
`KeyCharacterMap` and `InputDevice` Java SDK API bindings in
`src/input/sdk.rs` which is a nice simplification.
2026-02-20 21:28:50 +00:00
Alex Touchet 4e93184d8b Update Readme links 2026-02-18 03:07:36 +00:00
Alex Touchet 31feb32f07 Update MSRV badge in Readme 2026-02-17 23:54:39 +00:00
Robert Bragg 25f4220fef Bump MSRV to 1.85.0 (will be required by jni 0.22)
This re-generates the FFI bindings with `--rust-target '1.85.0'`
2026-02-17 22:19:08 +00:00
Robert Bragg 7e8990fd92 Add support for InputEvent::TextAction events
This exposes IME actions via an InputEvent::TextAction event so that
it's possible to recognise when text entry via an input method is
finished.

This adds a `TextInputAction` enum to represent the action key on a soft
keyboard, such as "Done".

For example, this makes it possible to emit Ime::Commit events in Winit.
2026-02-17 21:58:59 +00:00
Robert Bragg fe2c50ccc6 game-activty: ignore APP_CMD_SOFTWARE_KB_VIS_CHANGED w/o panic
APP_CMD_SOFTWARE_KB_VIS_CHANGED in the GameActivity backend is
intended for notifying the android_main thread that the soft keyboard
visibility has changed.

There's currently no Rust event / API for this, and so it wasn't being
handled in poll_events but that was leading to a unreachable panic when
GameActivity would send this APP_CMD when showing soft keyboards.

We don't currently plan to expose any public API / event for this since
it's based on monitoring IME insets and applications should instead be able
to check insets after getting InsetsChanged events.

For the sake of minimizing patches to the upstream GameActivity code
this makes it so poll_events can ignore this APP_CMD as a NOOP.
2026-02-17 21:46:27 +00:00
Robert Bragg a20a7e4ee4 Import android-games-sdk changes for 4.0.0
This imports the SDK from commit 8fa58b0e145ec28e726fa2b1c7e7a52af925ca35, from:
https://github.com/rust-mobile/android-games-sdk/commits/android-activity-4.0.0

This includes one "notify android_main of editor actions" patch which will make
it possible to forward editor actions and support IME Commit events in Winit)

# notify android_main of editor actions

This adds a pendingEditorActions member to android_app that is set via
onEditorAction and the android_main thread is notified via notifyInput
instead of re-instating APP_CMD_EDITOR_ACTION.

The idea is that the android_main thread should check for
android_app->pendingEditorActions whenever input events are polled/iterated.

# FFI bindings update

Also updates the FFI bindings via generate-bindings.sh
2026-02-17 21:45:22 +00:00
Marijn Suijten 0b0e19ed44 Revert "input: Replace open-coded types with ndk::event definitions (#163)"
This reverts commit 51d05d48c8 for
backwards compatibility with the existing `0.6` releases.

For now, it's creating a lot of busy work having to always make this
revert in order to test various topic branch changes with winit 0.30.

Lets save this breaking change until we have more reasons to break
semver compatibility (in itself this doesn't fix or enable any features,
so we can live without it for now).
2026-02-17 21:43:52 +00:00
Mads Marquart e686e80112 Allow building as dependency on docs.rs with no features enabled 2026-01-07 16:51:08 +00:00
Mads Marquart b9e883866e Clean up gitignores 2026-01-07 16:51:08 +00:00
Marijn Suijten 9e8c85c647 Assert that the thread Looper matches the main one 2025-12-18 15:26:24 +00:00
Marijn Suijten a97cf1ceae native_activity: Only wait for state to update while main thread is running
We see that some Android callbacks like `onStart()` deadlock,
specifically when returning out of the main thread before running
any event loop (but likely also whenever terminating the event loop),
because they don't check if the thread is still even running and are
otherwise guaranteed receive an `activity_state` update or other state
change to unblock themselves.

This is a followup to [#94] which only concerned itself with a deadlock
caused by a destructor not running because that very object was kept
alive to poll on the `destroyed` field that destructor was supposed to
set, but its new `thread_state` can be reused to disable these condvar
waits when the "sending" thread has disappeared.

Separately, that PR mentions `Activity` recreates because of
configuration changes which isn't supported anyway because `Activity` is
still wrongly assumed to be a global singleton.

[#94]: https://togithub.com/rust-mobile/android-activity/pull/94
2025-12-18 15:26:24 +00:00
daxpedda 1652ebb229 Add package.include to Cargo.toml
This reduces package size and notably prevents any bash files from landing on a users device.
2025-08-11 13:23:54 +01:00
Robert Bragg b943f58863 Merge pull request #184 from rust-mobile/doctest
Build-test documentation and fix broken doc samples
2025-08-11 13:15:57 +01:00
Marijn Suijten 019ad634a2 Switch doctests back to native cross-compilation, supported since Rust 1.89
https://blog.rust-lang.org/2025/08/07/Rust-1.89.0/#cross-compiled-doctests
2025-08-11 13:57:02 +02:00
Marijn Suijten 87cda3c560 Build-test (documentation) on the host and fix broken doc samples 2025-08-11 13:57:02 +02:00
Robert Bragg bde1cb3436 Merge pull request #191 from jb55/agdk-submodule
Update to GameActivity 4.0.0
2025-08-11 11:57:45 +01:00
Robert Bragg 69f3642499 Update android-games-sdk/README.md
Update the notes on how to update to new GameActivity releases
2025-04-04 16:41:26 +01:00
Robert Bragg c0f3fa6754 Check $ANDROID_GAMES_SDK for GameActivity source
For convenience, when updating to new GameActivity versions, this makes
it possible to build against the out-of-tree `android-games-sdk` repo.

This also updates `generate-bindings.sh` to point at $ANDROID_GAMES_SDK
if set.

E.g.

```
git clone git@github.com:rust-mobile/android-games-sdk.git \
    --branch android-activity-4.0.0
export ANDROID_GAMES_SDK=$PWD/android-games-sdk

./generate-bindings.sh
cargo build --features=game-activity --target=aarch64-linux-android
```
2025-04-01 15:41:51 +01:00
Robert Bragg 42af0cccfa examples/agdk-mainloop: pull in games-activity:4.0.0 2025-04-01 15:41:51 +01:00
Robert Bragg 5d7616e30e examples/agdk-mainloop: Use Gradle 8.4 (compatible with Java 21) 2025-04-01 15:41:51 +01:00
Robert Bragg 3755ed7e7a game-activity: build fixes for rust-bindgen 0.71 ffi API 2025-04-01 15:41:51 +01:00
Robert Bragg 5367c865e3 Re-generate bindings with rust-bindgen 0.71.1 2025-04-01 15:41:51 +01:00
William Casarin eacddd744a bindgen: update paths
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-01 15:41:51 +01:00
Robert Bragg 36832feacf Add import-games-sdk.sh and import some APP_CMD_ changes for 4.0.0
This replaces `copy-files` + `file_list.txt` (subjective simplification)

This imports the SDK from commit 1b544f896646b29e798c5be0a151a488906797f7, from:
https://github.com/rust-mobile/android-games-sdk/commits/android-activity-4.0.0
2025-04-01 15:41:51 +01:00
Robert Bragg 88714f0b6a Add CHANGELOG.md entry for GameActivity bump to 4.0.0 2025-04-01 15:41:49 +01:00
William Casarin 85eb7274f4 android-game-sdk-rs: bump v2.0.2 -> v4.0.0
Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-01 15:41:33 +01:00
William Casarin 49f2b86424 switch to android-game-sdk-rs grafted repo at v2.0.2
Also includes our patches on top (branch android-activity-2.0.2). This
is mainly to test to make sure everything is still working. We will
switch to the android-activity-4.0.0 branch when we're done

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-01 15:41:33 +01:00
William Casarin 976e9d06af tree: remove local copy of v2.0.2 android-games-sdk
We are going to use a submodule so that it is easier to track and rebase
our local changes onto new versions of android-games-sdk

Signed-off-by: William Casarin <jb55@jb55.com>
2025-04-01 15:41:33 +01:00
Robert Bragg ac2e17e977 Re-export 'ndk' and 'ndk_sys' crates
Since we expose `ndk` types in the public API it makes sense to
re-export these APIs so users of android-activity can defer to these
without needing to manually sync the versions for explicit dependencies.
2025-04-01 15:38:44 +01:00
Robert Bragg db3ea3386f Bump rust-version to 1.73.0
There was a fix for the definition of the `stat` struct on Android in
1.73, and even though it's unlikely to affect many applications it still
seems worthwhile to draw a line under this and ensure that all
android-activity based applications will have that fix.

Rust 1.73 was released October 2023, which is still well over a year old
and very conservative.

This updates `generate-bindings.sh` to pass `--rust-target 1.73.0` so we
avoid generating bindings that require a more recent compiler.

(This doesn't actually regenerate the bindings but does ensure that
future updates will be constrained to generate code that is backwards
compatible with 1.73.)
2025-04-01 15:29:59 +01:00
Marijn Suijten 51d05d48c8 input: Replace open-coded types with ndk::event definitions (#163) 2025-01-27 18:12:06 +01:00
Marijn Suijten fe171bc532 Fix various codebase rots (stale CI, new Rust lints, broken MSRV checks by transitive dependency upgrades) (#164)
* game_activity/ffi: Drop cfg for inexistant `target_arch = "armv7"`

[Rust 1.80 from July 25th 2024] points out that `armv7` is not a known,
valid value for the `target_arch` cfg variable.  This is confirmed by
the docs not listing it either:
https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch

Hence drop this entirely, and rely purely on `target_arch = "arm"`.

[Rust 1.80 from July 25th 2024]: https://blog.rust-lang.org/2024/07/25/Rust-1.80.0.html

* Fix `unexpected-cfgs` by adding `api-level-30` feature and removing `test`

Some code copied from the NDK carried over the respective `feature`
`cfg` guards, without ever adding the feature to the `[features]` list
in `Cargo.toml`.  Now that Rust detects these mishaps, we can fix it
by removing `test` (bindings don't seem to be run-tested) and reexpose
`ConfigurationRef::screen_round()` which was behind a previously
unsettable `feature = "api-level-30"`.

Also remove `unsafe impl Send/Sync for ConfigurationRef` since the
upstream `ndk` already declares `Configuration` to be `Send` and `Sync`,
and `RwLock` and `Arc` carry that through.

* native_activity: Fix clippy lints around `NativeActivityGlue` not `SendSync` and unwritten `redraw_needed` field

* CI: Remove deprecated/unmaintained `actions-rs` toolchain setup

The `actions-rs` containers haven't been maintained and updated for
years and don't need to: GitHub's actions environment already comes
fully loaded with a complete `stable` Rust installation with the
standard tools (in this case `rustfmt`).  Remove the remaining toolchain
setup (which was already replaced with `hecrj/setup-rust-action`
elsewhere) to get rid of ancient Node 12 deprecation warnings.

* Bump dependency patch-versions to fix `-Zminimal-versions` and MSRV check

Use `-Zminimal-versions` in our MSRV check.  This not only ensures
our minimum version bounds are actually solid and tested (even if
they may be a bit conservative at times, i.e. we could allow older
versions except for the crates that are bumped in this patch which were
explicitly build-tested), it also allows us to use this as a base for
the MSRV test, and preempt us from failing it whenever a (transitive!)
dependency bumps its MSRV beyond ours in a *semver-compatible* release.

* Elide redundant `impl` block lifetimes following stricter Rust 1.83 lint

Rust now points out that `impl<'a> (Trait for) Struct<'a>` is
superfluous whenever `'a` is not used anywhere else in the `impl` block.
2025-01-27 17:14:13 +01:00
Robert Bragg 0d299300f4 Merge pull request #158 from rust-mobile/release-0.6.0
Release 0.6.0
2024-04-26 17:23:04 +01:00
Robert Bragg 0a87a84c57 Release 0.6.0 2024-04-26 17:16:38 +01:00
SkyGrel19 7bd3ba6dde native-activity: Check for null saved_state_in pointer
Avoids calling `std::slice::from_raw_parts` with a null `saved_state_in`
pointer.

Fixes: #153
2024-04-26 16:37:42 +01:00
Marijn Suijten 6a0197c28f Upgrade to ndk-sys 0.6.0 and ndk 0.9.0
The next breaking `ndk` release puts a lot of emphasis in improving
`enum`s to finally be marked `non_exhaustive`, and carry possible future
values in `__Unknown(i32)` variants.  This removes the lossy conversions
that previously required `android-activity` to redefine its types, which
could all be removed again.

The `repr()` types have also been updated, as `enum` constants in C are
translated to `u32` by default in `bindgen` even though they're commonly
passed as `int` to every API function that consumes them.
2024-04-26 16:36:38 +01:00
Robert Bragg e5b8242ff2 Bump MSRV to 1.69.0 considering we can't build cargo ndk with 1.68
cargo ndk will fail to build with 1.68 due to a toml_edit dep.

Technically android-activity itself should still build with 1.68
but it's simpler to synchronize the `rust-version` with the minimum
version that we actually test in CI (where we need to build cargo ndk)
2024-04-26 16:11:36 +01:00
Robert Bragg c9faa9c44e Merge pull request #151 from rust-mobile/release-0.5.2
Release 0.5.2
2024-01-30 13:09:08 +00:00
Robert Bragg 4b9b8d754b Force cargo-ndk to only be built with stable toolchain
This fixes CI builds with rust 0.68 because cargo ndk depends on
cargo platform which depends on 0.70.
2024-01-30 12:42:36 +00:00
Robert Bragg 526d39c1f3 Release 0.5.2 2024-01-30 12:15:21 +00:00
Robert Bragg 4ffa3ac2e1 Merge pull request #147 from ArthurCose/motion-event-mask
native-activity/input: OR with `EVENT_ACTION_MASK` when extracting action
2024-01-30 12:05:30 +00:00
Robert Bragg 967882f3d9 Merge pull request #149 from rust-mobile/release-0.5.1
Release 0.5.1
2023-12-20 22:11:18 +00:00
Robert Bragg 35e080baf0 Release 0.5.1 2023-12-20 22:03:15 +00:00
Robert Bragg 5cb67a2b89 Remove ndk dev-dependency added in #142
Although this crate has some examples that depend on the ndk, they
aren't regular Cargo examples, they are completely standalone apps
that depend on dev-dependencies.
2023-12-20 17:12:25 +00:00
Arthur Cosentino 672360c5e6 Fix multitouch MotionActions processing as unknown in native activities 2023-12-13 09:05:59 -05:00
Robert Bragg 9fce890219 Merge pull request #143 from rust-mobile/readme-update-versions
README: Update crate version in `Cargo.toml` example
2023-11-20 16:13:38 +00:00
Robert Bragg 2deec162b5 Merge pull request #145 from fornwall/android-main-thread-name
Name spawned threads
2023-11-20 16:10:27 +00:00
Fredrik Fornwall eeeb80209f Fix error after merge conflict 2023-11-20 15:36:04 +01:00
Fredrik Fornwall 6c3583dc24 Merge branch 'main' into android-main-thread-name 2023-11-20 14:35:37 +01:00
Robert Bragg bfd8bfd04c Merge pull request #133 from rust-mobile/marijn/bail-log-thread-on-read_line-error
Stop log-forwarding thread on IO errors
2023-11-20 13:24:23 +00:00
Marijn Suijten af341897a2 Generalize log-forwarding setup and stop thread on IO errors
When `read_line()` starts returning `Err` the current `if let Ok`
condition ignores those, likely causing the `loop` to spin indefinitely
while this function keeps returning errors.

Note that we don't currently store the join handle for this thread
anywhere, so won't see the error surface either (just like how the join
handle for the main thread is never checked).  Perhaps we should call
`log::error!()` to make the user aware that their IO logging has
mysteriously terminated.
2023-11-20 14:15:54 +01:00
Marijn Suijten a84722ff23 Clean up a let-else that is possible in Rust 1.68 2023-11-20 13:30:29 +01:00
Fredrik Fornwall d9af67008a Rename threads 2023-11-20 12:48:37 +01:00
Fredrik Fornwall c2f467c174 Name spawned threads
Name spawned threads to make things more clear during debugging and
profiling.
2023-11-18 19:20:15 +01:00
Marijn Suijten e14d2c1deb README: Fix MSRV badge 2023-11-04 22:40:15 +01:00
Marijn Suijten 100d5bc1d4 README: Update crate version in Cargo.toml example 2023-10-28 20:25:08 +02:00
Thierry Berger 98aef99419 Disable ndk default features to remove raw-window-handle 0.6 (#142)
The `ndk` crate enables `raw-window-handle 0.6` by default (because of
https://github.com/rust-mobile/ndk/pull/434#issuecomment-1752089087)
which might not be used by consumers of the `android-activity` crate
at all, or might (still) be a mismatching version. Even if the `rwh_0x`
features are additive, figuring that out leads to cryptic errors and it
is best to turn off these defaults completely and leave it to the user
to turn it back on in their own `[dependencies]` section if desired.
2023-10-25 23:15:21 +02:00
Robert Bragg a7114c807f Merge pull request #137 from rust-mobile/release-0.5.0
Release 0.5.0
2023-10-17 00:17:12 +01:00
Robert Bragg a7dc90d9bb Release 0.5.0 2023-10-17 00:03:48 +01:00
Robert Bragg 6af4d61227 Remove redundant examples/na-mainloop/.idea directory 2023-10-17 00:03:48 +01:00
Robert Bragg 6e036c99e4 Update CHANGELOG 2023-10-17 00:03:48 +01:00
Robert Bragg 2a917ca5c4 Expose MotionEvent::action_button() state
This exposes the button associated with a button press or release
action.
2023-10-17 00:03:03 +01:00
Robert Bragg add58dbb2e native-activity: Fix copy&paste mistake in MotionEvent::action() 2023-10-17 00:03:03 +01:00
dependabot[bot] d16cb79350 build(deps): bump hecrj/setup-rust-action from 1 to 2
Bumps [hecrj/setup-rust-action](https://github.com/hecrj/setup-rust-action) from 1 to 2.
- [Release notes](https://github.com/hecrj/setup-rust-action/releases)
- [Commits](https://github.com/hecrj/setup-rust-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: hecrj/setup-rust-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 22:55:13 +01:00
Robert Bragg b590ec5484 Set workspace.resolver = "2" and avoid cargo warning 2023-10-16 21:29:01 +01:00
Robert Bragg 74d9669854 Document that AndroidApp is cheaply clonable
Fixes: #125
2023-10-16 20:44:51 +01:00
Robert Bragg a92237fab4 Add non_exhaustive to input enums
This change was meant to be squashed into #131 before landing
2023-10-16 20:31:32 +01:00
Robert Bragg 969ba5adf9 Improve forwards compatibility of input API
This adds a `#[doc(hidden)]` `__Unknown(u32)` variant to the various
enums to keep them extensible without requiring API breaks.

We need to consider that most enums that are based on Android SDK enums
may be extended across different versions of Android (i.e. effectively
at runtime) or extended in new versions of `android-activity` when we
pull in the latest NDK/SDK constants.

In particular in the case that there is some unknown variant we at least
want to be able to preserve the integer value to allow the values to be
either passed back into the SDK (it doesn't always matter whether we
know the semantics of a variant at compile time) or passed on to
something downstream that could be independently updated to know the
semantics.

We don't want it to be an API break to extend these enums in future
releases of `android-activity`.

It's not enough to rely on `#[non-exhaustive]` because that only really
helps when adding new variants in sync with android-activity releases.

On the other hand we also can't rely on a catch-all `Unknown(u32)` that
only really helps with unknown variants seen at runtime. (If code were
to have an exhaustive match that would include matching on `Unknown(_)`
values then they wouldn't be compatible with new versions of
android-activity that would promote unknown values to known ones).

What we aim for instead is to have a hidden catch-all variant that is
considered (practically) unmatchable so code is forced to have a
`unknown => {}` catch-all pattern match that will cover unknown variants
either in the form of Rust variants added in future versions or in the
form of an `__Unknown(u32)` integer that represents an unknown variant
seen at runtime.

Any `unknown => {}` pattern match can rely on `IntoPrimitive` to convert
the `unknown` variant to the integer that comes from the Android SDK in
case that values needs to be passed on, even without knowing it's
semantic meaning at compile time.

Instead of adding an `__Unknown(u32)` variant to the `Class` enum though
this enum has been removed in favour of adding methods like
`is_button_class()` and `is_pointer_class()` to the `Source` type, since
the class flags aren't guaranteed to be mutually exclusive and since
they are an attribute of the `Source`.

This removes some reliance `try_into().unwrap()` that was put in place
anticipating that we would support `into()` via `num_enum`, once we
could update our rust-version.
2023-10-16 20:01:52 +01:00
Marijn Suijten ce4413b2c6 Close logpipe input file descriptor after dup2()
When the input file descriptor of the `pipe()` is `dup2()`'d into
`stdin` and `stdout` it is effectively copied, leaving the original file
descriptor open and leaking at the end of these statements.  Only the
output file descriptor has its ownership transferred to `File` and will
be cleaned up properly.

This should cause the reading end to read EOF and return zero bytes when
`stdin` and `stdout` is open, rather than remaining open "indefinitely"
(barring the whole process being taken down) as there will always be
that one file descriptor referencing the input end of the pipe.
2023-10-16 19:12:25 +01:00
Robert Bragg a291e378ee Merge pull request #128 from rust-mobile/ndk-stable
Upgrade to stable `ndk 0.8` and `ndk-sys 0.5` releases
2023-10-15 20:40:09 +01:00
Marijn Suijten 2ecaab9f15 Upgrade to stable ndk 0.8 and ndk-sys 0.5 releases 2023-10-15 20:00:19 +02:00
Marijn Suijten 3d5e479a4e Merge pull request #118 from rust-mobile/dependabot/github_actions/actions/checkout-4
build(deps): bump actions/checkout from 3 to 4
2023-10-04 22:28:53 +02:00
Robert Bragg 219a14bda1 Merge pull request #122 from fornwall/pointer-away-from-ndk
Avoid exposing Pointer and PointersIter from ndk
2023-09-26 21:41:44 +01:00
Robert Bragg 733fabffd3 Merge pull request #124 from fornwall/enable-ci-on-all-branches
Enable CI on all branches
2023-09-26 20:34:43 +01:00
Fredrik Fornwall f2132c4dab Enable CI on all branches 2023-09-26 08:48:19 +02:00
Fredrik Fornwall 9930b9bf90 Avoid exposing Pointer and PointersIter from ndk 2023-09-26 08:36:49 +02:00
dependabot[bot] 0eefd623ed build(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-25 18:36:49 +00:00
Robert Bragg 83cdb56e24 Merge pull request #123 from rust-mobile/rib/pr/msrv-1.68-v2
Bump MSRV to 1.68
2023-09-25 19:36:15 +01:00
Robert Bragg 942053d88e Bump MSRV to 1.68
- Lets us build with cargo ndk 3+
- Lets us remove suppression for false-negative clippy warning about unsafe
  blocks in unsafe functions

- 1.68.0 notably also builds the standard library with a newer r25 NDK
  toolchain which avoid the need for awkward libgcc workarounds, so it's
  anyway a desirable baseline for Android projects.
2023-09-25 16:50:08 +01:00
Robert Bragg 865cc6a780 Merge pull request #115 from rust-mobile/rib/pr/changelog-fixups
CHANGELOG fixups
2023-08-15 23:17:22 +01:00
Robert Bragg 4f6d7d68de CHANGELOG fixups
Some of the dates were wrong from copy&pasting, there was no changelog
entry for adding `InputEvent::TextEvent`, and the release date for
0.5.0-beta.0/1 was missing.
2023-08-15 22:50:03 +01:00
Robert Bragg 7ea440d6c1 Merge pull request #114 from rust-mobile/release-0.5.0-beta.1
Release 0.5.0-beta.1
2023-08-15 21:59:00 +01:00
Robert Bragg 75e9e8672d Release 0.5.0-beta.1 2023-08-15 21:56:28 +01:00
68 changed files with 17131 additions and 28386 deletions
+34 -21
View File
@@ -2,7 +2,7 @@ name: ci
on:
push:
branches: [main]
branches: "*"
pull_request:
env:
@@ -16,15 +16,28 @@ jobs:
strategy:
fail-fast: false
matrix:
# XXX: We are currently constrained by Winit's MSRV policy + CI system
# See Cargo.toml for details
rust_version: [1.64.0, stable]
# See top README for MSRV policy
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: >
@@ -35,10 +48,7 @@ jobs:
i686-linux-android
- name: Install cargo-ndk
# XXX: We have to use an old version of cargo-ndk that supports the
# libgcc linker workaround for rust < 1.68 because Winit's CI system
# currently requires this crate to be buildable with 1.64
run: cargo install cargo-ndk --version "^2"
run: cargo +stable install cargo-ndk
- name: Build game-activity
working-directory: android-activity
@@ -61,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
@@ -72,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
@@ -86,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
+2 -1
View File
@@ -1 +1,2 @@
target
/target
/Cargo.lock
+1
View File
@@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = ["android-activity"]
exclude = ["examples"]
+62 -16
View File
@@ -3,7 +3,7 @@
[![ci](https://github.com/rust-mobile/android-activity/actions/workflows/ci.yml/badge.svg)](https://github.com/rust-mobile/android-activity/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/android-activity.svg)](https://crates.io/crates/android-activity)
[![Docs](https://docs.rs/android-activity/badge.svg)](https://docs.rs/android-activity)
[![MSRV](https://img.shields.io/badge/rustc-1.64.0+-ab6000.svg)](https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html)
[![MSRV](https://img.shields.io/badge/rustc-1.85.0+-ab6000.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/)
## Overview
@@ -25,9 +25,9 @@ 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
@@ -36,23 +36,34 @@ 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
```rust
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
#[no_mangle]
#[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 +73,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 passsed to a new invocation of `android_main`.
MainEvent::Destroy => { return; }
_ => {}
}
@@ -85,6 +101,36 @@ cargo apk run
adb logcat example:V *:S
```
## Optional `android_on_create` entry point
`android-activity` also supports 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
thread before the main Rust code starts running.
For example:
```rust
use std::sync::OnceLock;
use jni::{JavaVM, objects::JObject};
#[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
}
```
## Full Examples
See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`).
@@ -111,9 +157,9 @@ Even if you start out using `NativeActivity` for the convenience, it's likely th
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.
Winit-based applications can follow the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) 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 `#[unsafe(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).
See the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) 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)
@@ -126,8 +172,8 @@ Middleware libraries can instead look at using the [ndk-context](https://crates.
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"`
2. Add a dependency on `android-activity`, like `android-activity = { version="0.6", features = [ "native-activity" ] }`
3. Optionally add a dependency on `android_logger = "0.13.0"`
4. Update the `main` entry point to look like this:
```rust
@@ -157,8 +203,8 @@ Prior to working on android-activity, the existing glue crates available for bui
## 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.
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.
-10
View File
@@ -1,10 +0,0 @@
/target
Cargo.lock
# Added by cargo
#
# already existing elements were commented out
#/target
#Cargo.lock
+137 -7
View File
@@ -6,6 +6,121 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### 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:?}");
}
}
```
### 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.0.0 (requires the corresponding 4.0.0 `.aar` release from Google) ([#191](https://github.com/rust-mobile/android-activity/pull/191))
### 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))
## [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))
@@ -63,6 +178,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
```
</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))
@@ -104,19 +220,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
</details>
## [0.4.3] - 2022-07-30
## [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] - 2022-06-17
## [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))
@@ -125,21 +241,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- Most of the examples were moved to <https://github.com/rust-mobile/rust-android-examples> ([#50](https://github.com/rust-mobile/android-activity/pull/50))
## [0.4] - 2022-11-10
## [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
@@ -156,6 +272,20 @@ 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
[unreleased]: https://github.com/rust-mobile/android-activity/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/rust-mobile/android-activity/compare/v0.5.2...v0.6.0
[0.5.2]: https://github.com/rust-mobile/android-activity/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/rust-mobile/android-activity/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/rust-mobile/android-activity/compare/v0.4.3...v0.5.0
[0.4.3]: https://github.com/rust-mobile/android-activity/compare/v0.4.2...v0.4.3
[0.4.2]: https://github.com/rust-mobile/android-activity/compare/v0.4.1...v0.4.2
[0.4.1]: https://github.com/rust-mobile/android-activity/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/rust-mobile/android-activity/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/rust-mobile/android-activity/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/rust-mobile/android-activity/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/rust-mobile/android-activity/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/rust-mobile/android-activity/releases/tag/v0.1.0
+15 -22
View File
@@ -1,6 +1,6 @@
[package]
name = "android-activity"
version = "0.5.0-beta.0"
version = "0.6.0"
edition = "2021"
keywords = ["android", "ndk"]
readme = "../README.md"
@@ -9,19 +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"
include = ["/build.rs", "/android-games-sdk", "/LICENSE*", "/src"]
# XXX: Even though we have our own MSRV policy that says we only promise to
# support stable releases over the last three months we actually end up
# constrained by the MSRV policy of Winit, which is currently based on
# supporting Alacritty on Debian Sid, and requires a > 10 month old Rust version
#
# This Winit policiy is unfortunately in conflict with what makes sense for
# Android because versions below 1.68 for Android requires awkward toolchain
# linker workarounds, and can't even be compiled with newer versions of
# `cargo ndk` that removed these linker workarounds.
#
# TODO: Open a PR for Winit's CI to test Android builds using a newer toolchain.
rust-version = "1.64"
rust-version = "1.85.0"
[features]
# Note: we don't enable any backend by default since features
@@ -31,25 +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"
cesu8 = "1"
jni = "0.21"
ndk-sys = "0.5.0-beta.0"
ndk = "0.8.0-beta.0"
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.7"
bitflags = "2.0"
libc = "0.2"
libc = "0.2.139"
thiserror = "1"
[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 = [
+4 -4
View File
@@ -5,20 +5,20 @@
The third-party glue code, under the game-activity-csrc/ directory is covered by
the Apache 2.0 license only:
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
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 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
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.
@@ -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
@@ -0,0 +1,711 @@
/*
* 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.
*/
/**
* @addtogroup GameActivity Game Activity
* The interface to use GameActivity.
* @{
*/
/**
* @file GameActivity.h
*/
#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_H
#define ANDROID_GAME_SDK_GAME_ACTIVITY_H
#include <android/asset_manager.h>
#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>
#ifdef __cplusplus
extern "C" {
#endif
#define GAMEACTIVITY_MAJOR_VERSION 4
#define GAMEACTIVITY_MINOR_VERSION 0
#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}
*/
struct GameActivityCallbacks;
/**
* This structure defines the native side of an android.app.GameActivity.
* It is created by the framework, and handed to the application's native
* code as it is being launched.
*/
typedef struct GameActivity {
/**
* Pointer to the callback function table of the native application.
* You can set the functions here to your own callbacks. The callbacks
* pointer itself here should not be changed; it is allocated and managed
* for you by the framework.
*/
struct GameActivityCallbacks* callbacks;
/**
* The global handle on the process's Java VM.
*/
JavaVM* vm;
/**
* JNI context for the main thread of the app. Note that this field
* can ONLY be used from the main thread of the process; that is, the
* thread that calls into the GameActivityCallbacks.
*/
JNIEnv* env;
/**
* The GameActivity object handle.
*/
jobject javaGameActivity;
/**
* Path to this application's internal data directory.
*/
const char* internalDataPath;
/**
* Path to this application's external (removable/mountable) data directory.
*/
const char* externalDataPath;
/**
* The platform's SDK version code.
*/
int32_t sdkVersion;
/**
* This is the native instance of the application. It is not used by
* the framework, but can be set by the application to its own instance
* state.
*/
void* instance;
/**
* Pointer to the Asset Manager instance for the application. The
* application uses this to access binary assets bundled inside its own .apk
* file.
*/
AAssetManager* assetManager;
/**
* Available starting with Honeycomb: path to the directory containing
* the application's OBB files (if any). If the app doesn't have any
* OBB files, this directory may not exist.
*/
const char* obbPath;
} GameActivity;
/**
* 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);
/**
* These are the callbacks the framework makes into a native application.
* All of these callbacks happen on the main thread of the application.
* By default, all callbacks are NULL; set to a pointer to your own function
* to have it called.
*/
typedef struct GameActivityCallbacks {
/**
* GameActivity has started. See Java documentation for Activity.onStart()
* for more information.
*/
void (*onStart)(GameActivity* activity);
/**
* GameActivity has resumed. See Java documentation for Activity.onResume()
* for more information.
*/
void (*onResume)(GameActivity* activity);
/**
* The framework is asking GameActivity to save its current instance state.
* See the Java documentation for Activity.onSaveInstanceState() for more
* information. The user should call the recallback with their data, its
* length and the provided context; they retain ownership of the data. Note
* 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* context);
/**
* GameActivity has paused. See Java documentation for Activity.onPause()
* for more information.
*/
void (*onPause)(GameActivity* activity);
/**
* GameActivity has stopped. See Java documentation for Activity.onStop()
* for more information.
*/
void (*onStop)(GameActivity* activity);
/**
* GameActivity is being destroyed. See Java documentation for
* Activity.onDestroy() for more information.
*/
void (*onDestroy)(GameActivity* activity);
/**
* Focus has changed in this GameActivity's window. This is often used,
* for example, to pause a game when it loses input focus.
*/
void (*onWindowFocusChanged)(GameActivity* activity, bool hasFocus);
/**
* 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);
/**
* 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);
/**
* The drawing window for this native activity needs to be redrawn. To
* avoid transient artifacts during screen changes (such resizing after
* 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);
/**
* The drawing window for this native activity is going to be destroyed.
* You MUST ensure that you do not touch the window object after returning
* from this function: in the common case of drawing to the window from
* another thread, that means the implementation of this callback must
* properly synchronize with the other thread to stop its drawing before
* returning from here.
*/
void (*onNativeWindowDestroyed)(GameActivity* activity,
ANativeWindow* window);
/**
* The current device AConfiguration has changed. The new configuration can
* be retrieved from assetManager.
*/
void (*onConfigurationChanged)(GameActivity* activity);
/**
* The system is running low on memory. Use this callback to release
* resources you do not need, to help the system avoid killing more
* important processes.
*/
void (*onTrimMemory)(GameActivity* activity, int level);
/**
* Callback called for every MotionEvent done on the GameActivity
* SurfaceView. Ownership of `event` is maintained by the library and it is
* only valid during the callback.
*/
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);
/**
* Callback called for every key up event on the GameActivity SurfaceView.
* Ownership of `event` is maintained by the library and it is only valid
* during the callback.
*/
bool (*onKeyUp)(GameActivity* activity, const GameActivityKeyEvent* event);
/**
* Callback called for every soft-keyboard text input event.
* Ownership of `state` is maintained by the library and it is only valid
* during the callback.
*/
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;
/**
* 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
* above); if the code is being instantiated from a previously saved instance,
* the savedState will be non-NULL and point to the saved data. You must make
* any copy of this data you need -- it will be released after you return from
* this function.
*/
typedef void GameActivity_createFunc(GameActivity* activity, void* savedState,
size_t savedStateSize);
/**
* The name of the function that NativeInstance looks for when launching its
* native code. This is the default function that is used, you can specify
* "android.app.func_name" string meta-data in your manifest to use a different
* function.
*/
extern GameActivity_createFunc GameActivity_onCreate;
/**
* Finish the given activity. Its finish() method will be called, causing it
* to be stopped and destroyed. 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.
*/
void GameActivity_finish(GameActivity* activity);
/**
* Flags for GameActivity_setWindowFlags,
* as per the Java API at android.view.WindowManager.LayoutParams.
*/
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
* independently, or in combination with {@link
* GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} and/or {@link
* GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}
*/
GAMEACTIVITY_FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001,
/** Everything behind this window will be dimmed. */
GAMEACTIVITY_FLAG_DIM_BEHIND = 0x00000002,
/**
* Blur everything behind this window.
* @deprecated Blurring is no longer supported.
*/
GAMEACTIVITY_FLAG_BLUR_BEHIND = 0x00000004,
/**
* This window won't ever get key input focus, so the
* user can not send key or other button events to it. Those will
* instead go to whatever focusable window is behind it. This flag
* will also enable {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL} whether or not
* that is explicitly set.
*
* Setting this flag also implies that the window will not need to
* interact with
* a soft input method, so it will be Z-ordered and positioned
* independently of any active input method (typically this means it
* gets Z-ordered on top of the input method, so it can use the full
* screen for its content and cover the input method if needed. You
* can use {@link GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM} to modify this
* behavior.
*/
GAMEACTIVITY_FLAG_NOT_FOCUSABLE = 0x00000008,
/** This window can never receive touch events. */
GAMEACTIVITY_FLAG_NOT_TOUCHABLE = 0x00000010,
/**
* Even when this window is focusable (its
* {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set), allow any pointer
* events outside of the window to be sent to the windows behind it.
* Otherwise it will consume all pointer events itself, regardless of
* whether they are inside of the window.
*/
GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL = 0x00000020,
/**
* When set, if the device is asleep when the touch
* screen is pressed, you will receive this first touch event. Usually
* the first touch event is consumed by the system since the user can
* not see what they are pressing on.
*
* @deprecated This flag has no effect.
*/
GAMEACTIVITY_FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040,
/**
* As long as this window is visible to the user, keep
* the device's screen turned on and bright.
*/
GAMEACTIVITY_FLAG_KEEP_SCREEN_ON = 0x00000080,
/**
* Place the window within the entire screen, ignoring
* decorations around the border (such as the status bar). The
* window must correctly position its contents to take the screen
* decoration into account.
*/
GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN = 0x00000100,
/** Allows the window to extend outside of the screen. */
GAMEACTIVITY_FLAG_LAYOUT_NO_LIMITS = 0x00000200,
/**
* Hide all screen decorations (such as the status
* bar) while this window is displayed. This allows the window to
* use the entire display space for itself -- the status bar will
* be hidden when an app window with this flag set is on the top
* layer. A fullscreen window will ignore a value of {@link
* GAMEACTIVITY_SOFT_INPUT_ADJUST_RESIZE}; the window will stay
* fullscreen and will not resize.
*/
GAMEACTIVITY_FLAG_FULLSCREEN = 0x00000400,
/**
* Override {@link GAMEACTIVITY_FLAG_FULLSCREEN} and force the
* screen decorations (such as the status bar) to be shown.
*/
GAMEACTIVITY_FLAG_FORCE_NOT_FULLSCREEN = 0x00000800,
/**
* Turn on dithering when compositing this window to
* the screen.
* @deprecated This flag is no longer used.
*/
GAMEACTIVITY_FLAG_DITHER = 0x00001000,
/**
* Treat the content of the window as secure, preventing
* it from appearing in screenshots or from being viewed on non-secure
* displays.
*/
GAMEACTIVITY_FLAG_SECURE = 0x00002000,
/**
* A special mode where the layout parameters are used
* to perform scaling of the surface when it is composited to the
* screen.
*/
GAMEACTIVITY_FLAG_SCALED = 0x00004000,
/**
* Intended for windows that will often be used when the user is
* holding the screen against their face, it will aggressively
* filter the event stream to prevent unintended presses in this
* situation that may not be desired for a particular window, when
* such an event stream is detected, the application will receive
* a {@link AMOTION_EVENT_ACTION_CANCEL} to indicate this so
* applications can handle this accordingly by taking no action on
* the event until the finger is released.
*/
GAMEACTIVITY_FLAG_IGNORE_CHEEK_PRESSES = 0x00008000,
/**
* A special option only for use in combination with
* {@link GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN}. When requesting layout in
* the screen your window may appear on top of or behind screen decorations
* such as the status bar. By also including this flag, the window
* manager will report the inset rectangle needed to ensure your
* content is not covered by screen decorations.
*/
GAMEACTIVITY_FLAG_LAYOUT_INSET_DECOR = 0x00010000,
/**
* Invert the state of {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} with
* respect to how this window interacts with the current method.
* That is, if FLAG_NOT_FOCUSABLE is set and this flag is set,
* then the window will behave as if it needs to interact with the
* input method and thus be placed behind/away from it; if {@link
* GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set and this flag is set,
* then the window will behave as if it doesn't need to interact
* with the input method and can be placed to use more space and
* cover the input method.
*/
GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM = 0x00020000,
/**
* If you have set {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL}, you
* can set this flag to receive a single special MotionEvent with
* the action
* {@link AMOTION_EVENT_ACTION_OUTSIDE} for
* touches that occur outside of your window. Note that you will not
* receive the full down/move/up gesture, only the location of the
* first down as an {@link AMOTION_EVENT_ACTION_OUTSIDE}.
*/
GAMEACTIVITY_FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000,
/**
* Special flag to let windows be shown when the screen
* is locked. This will let application windows take precedence over
* key guard or any other lock screens. Can be used with
* {@link GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} to turn screen on and display
* windows directly before showing the key guard window. Can be used with
* {@link GAMEACTIVITY_FLAG_DISMISS_KEYGUARD} to automatically fully
* dismisss non-secure keyguards. This flag only applies to the top-most
* full-screen window.
*/
GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED = 0x00080000,
/**
* Ask that the system wallpaper be shown behind
* your window. The window surface must be translucent to be able
* to actually see the wallpaper behind it; this flag just ensures
* that the wallpaper surface will be there if this window actually
* has translucent regions.
*/
GAMEACTIVITY_FLAG_SHOW_WALLPAPER = 0x00100000,
/**
* When set as a window is being added or made
* visible, once the window has been shown then the system will
* poke the power manager's user activity (as if the user had woken
* up the device) to turn the screen on.
*/
GAMEACTIVITY_FLAG_TURN_SCREEN_ON = 0x00200000,
/**
* When set the window will cause the keyguard to
* be dismissed, only if it is not a secure lock keyguard. Because such
* a keyguard is not needed for security, it will never re-appear if
* the user navigates to another window (in contrast to
* {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}, which will only temporarily
* hide both secure and non-secure keyguards but ensure they reappear
* when the user moves to another UI that doesn't hide them).
* If the keyguard is currently active and is secure (requires an
* unlock pattern) than the user will still need to confirm it before
* seeing this window, unless {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} has
* also been set.
*/
GAMEACTIVITY_FLAG_DISMISS_KEYGUARD = 0x00400000,
};
/**
* Change the window flags of the given activity. Calls getWindow().setFlags()
* of the given activity.
* Note that some flags must be set before the window decoration is created,
* see
* https://developer.android.com/reference/android/view/Window#setFlags(int,%20int).
* Note also 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.
*/
void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags,
uint32_t removeFlags);
/**
* Flags for GameActivity_showSoftInput; see the Java InputMethodManager
* API for documentation.
*/
enum GameActivityShowSoftInputFlags : uint8_t {
/**
* Implicit request to show the input window, not as the result
* of a direct request by the user.
*/
GAMEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT = 0x0001,
/**
* The user has forced the input method open (such as by
* long-pressing menu) so it should not be closed until they
* explicitly do so.
*/
GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED = 0x0002,
};
/**
* Show the IME while in the given activity. Calls
* InputMethodManager.showSoftInput() for the given activity. 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_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);
/**
* 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* context);
/**
* Get a pointer to the GameTextInput library instance.
*/
GameTextInput* GameActivity_getTextInput(const GameActivity* activity);
/**
* Flags for GameActivity_hideSoftInput; see the Java InputMethodManager
* API for documentation.
*/
enum GameActivityHideSoftInputFlags : uint16_t {
/**
* The soft input window should only be hidden if it was not
* explicitly shown by the user.
*/
GAMEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY = 0x0001,
/**
* The soft input window should normally be hidden, unless it was
* originally shown with {@link GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED}.
*/
GAMEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS = 0x0002,
};
/**
* Hide the IME while in the given activity. Calls
* InputMethodManager.hideSoftInput() for the given activity. 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.
*/
void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags);
/**
* Get the current window insets of the particular component. See
* https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type
* 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);
/**
* 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.
* See
* https://developer.android.com/reference/android/view/inputmethod/EditorInfo
* for the meaning of inputType, actionId and imeOptions.
*
* <b>Note:</b> currently only TYPE_NULL AND TYPE_CLASS_NUMBER are supported.
*/
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
}
#endif
/** @} */
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_H
@@ -57,29 +57,29 @@ extern "C" {
* \see GameActivityMotionEvent
*/
typedef struct GameActivityPointerAxes {
int32_t id;
int32_t toolType;
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
float rawX;
float rawY;
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;
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];
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];
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y];
}
/**
@@ -126,44 +126,44 @@ float GameActivityPointerAxes_getAxisValue(
inline float GameActivityPointerAxes_getPressure(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_PRESSURE);
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_PRESSURE);
}
inline float GameActivityPointerAxes_getSize(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_SIZE);
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_SIZE);
}
inline float GameActivityPointerAxes_getTouchMajor(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_TOUCH_MAJOR);
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);
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);
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);
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_TOOL_MINOR);
}
inline float GameActivityPointerAxes_getOrientation(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_ORIENTATION);
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_ORIENTATION);
}
/**
@@ -171,7 +171,7 @@ inline float GameActivityPointerAxes_getOrientation(
*/
#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
GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE
#else
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8
#endif
@@ -183,32 +183,32 @@ inline float GameActivityPointerAxes_getOrientation(
* (see https://developer.android.com/reference/android/view/MotionEvent).
*/
typedef struct GameActivityMotionEvent {
int32_t deviceId;
int32_t source;
int32_t action;
int32_t deviceId;
int32_t source;
int32_t action;
int64_t eventTime;
int64_t downTime;
int64_t eventTime;
int64_t downTime;
int32_t flags;
int32_t metaState;
int32_t flags;
int32_t metaState;
int32_t actionButton;
int32_t buttonState;
int32_t classification;
int32_t edgeFlags;
int32_t actionButton;
int32_t buttonState;
int32_t classification;
int32_t edgeFlags;
uint32_t pointerCount;
GameActivityPointerAxes
pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
uint32_t pointerCount;
GameActivityPointerAxes
pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
int historySize;
int64_t* historicalEventTimesMillis;
int64_t* historicalEventTimesNanos;
float* historicalAxisValues;
int historySize;
int64_t* historicalEventTimesMillis;
int64_t* historicalEventTimesNanos;
float* historicalAxisValues;
float precisionX;
float precisionY;
float precisionX;
float precisionY;
} GameActivityMotionEvent;
float GameActivityMotionEvent_getHistoricalAxisValue(
@@ -217,78 +217,66 @@ float GameActivityMotionEvent_getHistoricalAxisValue(
inline int GameActivityMotionEvent_getHistorySize(
const GameActivityMotionEvent* event) {
return event->historySize;
return event->historySize;
}
inline float GameActivityMotionEvent_getHistoricalX(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_X, pointerIndex, 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);
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);
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);
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);
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);
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);
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);
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);
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_ORIENTATION, pointerIndex, historyPos);
}
/** \brief Handle the freeing of the GameActivityMotionEvent struct. */
void GameActivityMotionEvent_destroy(GameActivityMotionEvent* c_event);
/**
* \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`.
*
* This is done automatically by the GameActivity: see `onTouchEvent` to set
* a callback to consume the received events.
* This function can be used if you re-implement events handling in your own
* activity.
* Ownership of out_event is maintained by the caller.
*/
void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
GameActivityMotionEvent* out_event);
/**
* \brief Describe a key event that happened on the GameActivity SurfaceView.
*
@@ -298,35 +286,23 @@ void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
* nanoseconds in this struct.
*/
typedef struct GameActivityKeyEvent {
int32_t deviceId;
int32_t source;
int32_t action;
int32_t deviceId;
int32_t source;
int32_t action;
int64_t eventTime;
int64_t downTime;
int64_t eventTime;
int64_t downTime;
int32_t flags;
int32_t metaState;
int32_t flags;
int32_t metaState;
int32_t modifiers;
int32_t repeatCount;
int32_t keyCode;
int32_t scanCode;
//int32_t unicodeChar;
int32_t modifiers;
int32_t repeatCount;
int32_t keyCode;
int32_t scanCode;
// int32_t unicodeChar;
} GameActivityKeyEvent;
/**
* \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`.
*
* This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown`
* to set a callback to consume the received events.
* This function can be used if you re-implement events handling in your own
* activity.
* Ownership of out_event is maintained by the caller.
*/
void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent,
GameActivityKeyEvent* out_event);
#ifdef __cplusplus
}
#endif
@@ -26,7 +26,7 @@
#define ALOGV(...)
#else
#define ALOGV(...) \
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
#endif
/* Returns 2nd arg. Used to substitute default value if caller's vararg list
@@ -40,21 +40,21 @@
#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))
__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)
#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__)))
(((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
#endif
/*
@@ -62,14 +62,14 @@
*/
#ifndef SLOGW
#define SLOGW(...) \
((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
((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)
#define SLOGW_IF(cond, ...) \
((__predict_false(cond)) \
? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \
: (void)0)
#endif
/*
@@ -0,0 +1,564 @@
/*
* 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.
*/
#pragma once
/**
* @addtogroup android_native_app_glue Native App Glue library
* The glue library to interface your game loop with GameActivity.
* @{
*/
#include <android/configuration.h>
#include <android/looper.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include "game-activity/GameActivity.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* The GameActivity interface provided by <game-activity/GameActivity.h>
* is based on a set of application-provided callbacks that will be called
* by the Activity's main thread when certain events occur.
*
* This means that each one of this callbacks _should_ _not_ block, or they
* risk having the system force-close the application. This programming
* model is direct, lightweight, but constraining.
*
* The 'android_native_app_glue' static library is used to provide a different
* execution model where the application can implement its own main event
* loop in a different thread instead. Here's how it works:
*
* 1/ The application must provide a function named "android_main()" that
* will be called when the activity is created, in a new thread that is
* distinct from the activity's main thread.
*
* 2/ android_main() receives a pointer to a valid "android_app" structure
* that contains references to other important objects, e.g. the
* GameActivity obejct instance the application is running in.
*
* 3/ the "android_app" object holds an ALooper instance that already
* listens to activity lifecycle events (e.g. "pause", "resume").
* See APP_CMD_XXX declarations below.
*
* This corresponds to an ALooper identifier returned by
* ALooper_pollOnce with value LOOPER_ID_MAIN.
*
* Your application can use the same ALooper to listen to additional
* file-descriptors. They can either be callback based, or with return
* identifiers starting with LOOPER_ID_USER.
*
* 4/ Whenever you receive a LOOPER_ID_MAIN event,
* the returned data will point to an android_poll_source structure. You
* can call the process() function on it, and fill in android_app->onAppCmd
* to be called for your own processing of the event.
*
* Alternatively, you can call the low-level functions to read and process
* the data directly... look at the process_cmd() and process_input()
* implementations in the glue to see how to do this.
*
* See the sample named "native-activity" that comes with the NDK with a
* full usage example. Also look at the documentation of GameActivity.
*/
struct android_app;
/**
* Data associated with an ALooper fd that will be returned as the "outData"
* when that source has data ready.
*/
struct android_poll_source {
/**
* The identifier of this source. May be LOOPER_ID_MAIN or
* LOOPER_ID_INPUT.
*/
int32_t id;
/** The android_app this ident is associated with. */
struct android_app* app;
/**
* Function to call to perform the standard processing of data from
* this source.
*/
void (*process)(struct android_app* app, struct android_poll_source* source);
};
struct android_input_buffer {
/**
* Pointer to a read-only array of GameActivityMotionEvent.
* Only the first motionEventsCount events are valid.
*/
GameActivityMotionEvent* motionEvents;
/**
* The number of valid motion events in `motionEvents`.
*/
uint64_t motionEventsCount;
/**
* The size of the `motionEvents` buffer.
*/
uint64_t motionEventsBufferSize;
/**
* Pointer to a read-only array of GameActivityKeyEvent.
* Only the first keyEventsCount events are valid.
*/
GameActivityKeyEvent* keyEvents;
/**
* The number of valid "Key" events in `keyEvents`.
*/
uint64_t keyEventsCount;
/**
* The size of the `keyEvents` buffer.
*/
uint64_t keyEventsBufferSize;
};
/**
* Function pointer declaration for the filtering of key events.
* A function with this signature should be passed to
* android_app_set_key_event_filter and return false for any events that should
* not be handled by android_native_app_glue. These events will be handled by
* the system instead.
*/
typedef bool (*android_key_event_filter)(const GameActivityKeyEvent*);
/**
* Function pointer definition for the filtering of motion events.
* A function with this signature should be passed to
* android_app_set_motion_event_filter and return false for any events that
* should not be handled by android_native_app_glue. These events will be
* handled by the system instead.
*/
typedef bool (*android_motion_event_filter)(const GameActivityMotionEvent*);
/**
* This is the interface for the standard glue code of a threaded
* application. In this model, the application's code is running
* in its own thread separate from the main thread of the process.
* It is not required that this thread be associated with the Java
* VM, although it will need to be in order to make JNI calls any
* Java objects.
*/
struct android_app {
/**
* An optional pointer to application-defined state.
*/
void* userData;
/**
* A required callback for processing main app commands (`APP_CMD_*`).
* This is called each frame if there are app commands that need processing.
*/
void (*onAppCmd)(struct android_app* app, int32_t cmd);
/** The GameActivity object instance that this app is running in. */
GameActivity* activity;
/** The current configuration the app is running in. */
AConfiguration* config;
/**
* The last activity saved state, as provided at creation time.
* It is NULL if there was no state. You can use this as you need; the
* memory will remain around until you call android_app_exec_cmd() for
* APP_CMD_RESUME, at which point it will be freed and savedState set to
* NULL. These variables should only be changed when processing a
* APP_CMD_SAVE_STATE, at which point they will be initialized to NULL and
* you can malloc your state and place the information here. In that case
* the memory will be freed for you later.
*/
void* savedState;
/**
* The size of the activity saved state. It is 0 if `savedState` is NULL.
*/
size_t savedStateSize;
/** 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;
/**
* Current content rectangle of the window; this is the area where the
* window's content should be placed to be seen by the user.
*/
ARect contentRect;
/**
* 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.
*/
int activityState;
/**
* This is non-zero when the application's GameActivity is being
* destroyed and waiting for the app thread to complete.
*/
int destroyRequested;
#define NATIVE_APP_GLUE_MAX_INPUT_BUFFERS 2
/**
* This is used for buffering input from GameActivity. Once ready, the
* application thread switches the buffers and processes what was
* accumulated.
*/
struct android_input_buffer inputBuffers[NATIVE_APP_GLUE_MAX_INPUT_BUFFERS];
int currentInputBuffer;
/**
* 0 if no text input event is outstanding, 1 if it is.
* Use `GameActivity_getTextInputState` to get information
* about the text entered by the user.
*/
int textInputState;
// Below are "private" implementation of the glue code.
/** @cond INTERNAL */
pthread_mutex_t mutex;
pthread_cond_t cond;
int msgread;
int msgwrite;
pthread_t thread;
struct android_poll_source cmdPollSource;
int running;
int stateSaved;
int destroyed;
int redrawNeeded;
ANativeWindow* pendingWindow;
ARect pendingContentRect;
android_key_event_filter keyEventFilter;
android_motion_event_filter motionEventFilter;
// When new input is received we set both of these flags and use the looper to
// wake up the application mainloop.
//
// To avoid spamming the mainloop with wake ups from lots of input though we
// don't sent a wake up if the inputSwapPending flag is already set. (i.e.
// we already expect input to be processed in a finite amount of time due to
// our previous wake up)
//
// When a wake up is received then we will check this flag (clearing it
// at the same time). If it was set then an InputAvailable event is sent to
// the application - which should lead to all input being processed within
// a finite amount of time.
//
// The next time android_app_swap_input_buffers is called, both flags will be
// cleared.
//
// NB: both of these should only be read with the app mutex held
bool inputAvailableWakeUp;
bool inputSwapPending;
/** @endcond */
};
/**
* Looper ID of commands coming from the app's main thread, an AInputQueue or
* user-defined sources.
*/
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
* identifier is a pointer to an android_poll_source structure.
* These can be retrieved and processed with android_app_read_cmd()
* and android_app_exec_cmd().
*/
LOOPER_ID_MAIN = 1,
/**
* Unused. Reserved for future use when usage of AInputQueue will be
* supported.
*/
LOOPER_ID_INPUT = 2,
/**
* Start of user-defined ALooper identifiers.
*/
LOOPER_ID_USER = 3,
};
/**
* 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 : int8_t {
/**
* Unused. Reserved for future use when usage of AInputQueue will be
* supported.
*/
UNUSED_APP_CMD_INPUT_CHANGED,
/**
* Command from main thread: a new ANativeWindow is ready for use. Upon
* receiving this command, android_app->window will contain the new window
* surface.
*/
APP_CMD_INIT_WINDOW,
/**
* Command from main thread: the existing ANativeWindow needs to be
* terminated. Upon receiving this command, android_app->window still
* contains the existing window; after calling android_app_exec_cmd
* it will be set to NULL.
*/
APP_CMD_TERM_WINDOW,
/**
* Command from main thread: the current ANativeWindow has been resized.
* Please redraw with its new size.
*/
APP_CMD_WINDOW_RESIZED,
/**
* Command from main thread: the system needs that the current ANativeWindow
* be redrawn. You should redraw the window before handing this to
* android_app_exec_cmd() in order to avoid transient drawing glitches.
*/
APP_CMD_WINDOW_REDRAW_NEEDED,
/**
* Command from main thread: the content area of the window has changed,
* such as from the soft input window being shown or hidden. You can
* find the new content rect in android_app::contentRect.
*/
APP_CMD_CONTENT_RECT_CHANGED,
/**
* Command from main thread: the 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.
*/
APP_CMD_GAINED_FOCUS,
/**
* Command from main thread: the app's activity window has lost
* input focus.
*/
APP_CMD_LOST_FOCUS,
/**
* Command from main thread: the current device configuration has changed.
*/
APP_CMD_CONFIG_CHANGED,
/**
* Command from main thread: the system is running low on memory.
* Try to reduce your memory use.
*/
APP_CMD_LOW_MEMORY,
/**
* Command from main thread: the app's activity has been started.
*/
APP_CMD_START,
/**
* Command from main thread: the app's activity has been resumed.
*/
APP_CMD_RESUME,
/**
* Command from main thread: the app should generate a new saved state
* for itself, to restore from later if needed. If you have saved state,
* allocate it with malloc and place it in android_app.savedState with
* the size in android_app.savedStateSize. The will be freed for you
* later.
*/
APP_CMD_SAVE_STATE,
/**
* Command from main thread: the app's activity has been paused.
*/
APP_CMD_PAUSE,
/**
* Command from main thread: the app's activity has been stopped.
*/
APP_CMD_STOP,
/**
* Command from main thread: the app's activity is being destroyed,
* and waiting for the app thread to clean up and exit before proceeding.
*/
APP_CMD_DESTROY,
/**
* Command from main thread: the app's insets have changed.
*/
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
* app command message.
*/
int8_t android_app_read_cmd(struct android_app* android_app);
/**
* Call with the command returned by android_app_read_cmd() to do the
* initial pre-processing of the given command. You can perform your own
* actions for the command after calling this function.
*/
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
/**
* Call with the command returned by android_app_read_cmd() to do the
* final post-processing of the given command. You must have done your own
* actions for the command before calling this function.
*/
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
/**
* 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);
/**
* Clear the array of motion events that were waiting to be handled, and release
* each of them.
*
* This method should be called after you have processed the motion events in
* your game loop. You should handle events at each iteration of your game loop.
*/
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer);
/**
* Clear the array of key events that were waiting to be handled, and release
* each of them.
*
* This method should be called after you have processed the key up events in
* your game loop. You should handle events at each iteration of your game loop.
*/
void android_app_clear_key_events(struct android_input_buffer* inputBuffer);
/**
* This is a springboard into the Rust glue layer that wraps calling the
* main entry for the app itself.
*/
extern void _rust_glue_entry(struct android_app* app);
/**
* Set the filter to use when processing key events.
* Any events for which the filter returns false will be ignored by
* android_native_app_glue. If filter is set to NULL, no filtering is done.
*
* 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);
/**
* Set the filter to use when processing touch and motion events.
* Any events for which the filter returns false will be ignored by
* android_native_app_glue. If filter is set to NULL, no filtering is done.
*
* Note that the default motion event filter will only allow touchscreen events
* through, in order to mimic NativeActivity's behaviour, so for controller
* events to be passed to the app, set the filter to NULL.
*/
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.
*/
void 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);
#ifdef __cplusplus
}
#endif
/** @} */
@@ -0,0 +1,8 @@
{
"export_libraries": [],
"library_name": null,
"android": {
"export_libraries": ["-landroid", "-llog"],
"library_name": null
}
}
@@ -0,0 +1,335 @@
/*
* 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
@@ -0,0 +1,789 @@
/*
* 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.
*/
#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>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE 16
#define NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE 4
#define LOGI(...) \
((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
#define LOGE(...) \
((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__); \
} \
} 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__))
#else
#define LOGV(...) ((void)0)
#endif
static void free_saved_state(struct android_app* android_app) {
pthread_mutex_lock(&android_app->mutex);
if (android_app->savedState != NULL) {
free(android_app->savedState);
android_app->savedState = NULL;
android_app->savedStateSize = 0;
}
pthread_mutex_unlock(&android_app->mutex);
}
int8_t android_app_read_cmd(struct android_app* android_app) {
int8_t cmd;
if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) {
LOGE("No data on command pipe!");
return -1;
}
if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app);
return cmd;
}
static void print_cur_config(struct android_app* android_app) {
char lang[2], country[2];
AConfiguration_getLanguage(android_app->config, lang);
AConfiguration_getCountry(android_app->config, country);
LOGV(
"Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
"keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
"modetype=%d modenight=%d",
AConfiguration_getMcc(android_app->config),
AConfiguration_getMnc(android_app->config), lang[0], lang[1], country[0],
country[1], AConfiguration_getOrientation(android_app->config),
AConfiguration_getTouchscreen(android_app->config),
AConfiguration_getDensity(android_app->config),
AConfiguration_getKeyboard(android_app->config),
AConfiguration_getNavigation(android_app->config),
AConfiguration_getKeysHidden(android_app->config),
AConfiguration_getNavHidden(android_app->config),
AConfiguration_getSdkVersion(android_app->config),
AConfiguration_getScreenSize(android_app->config),
AConfiguration_getScreenLong(android_app->config),
AConfiguration_getUiModeType(android_app->config),
AConfiguration_getUiModeNight(android_app->config));
}
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
switch (cmd) {
case UNUSED_APP_CMD_INPUT_CHANGED:
LOGV("UNUSED_APP_CMD_INPUT_CHANGED");
// Do nothing. This can be used in the future to handle AInputQueue
// natively, like done in NativeActivity.
break;
case APP_CMD_INIT_WINDOW:
LOGV("APP_CMD_INIT_WINDOW");
pthread_mutex_lock(&android_app->mutex);
android_app->window = android_app->pendingWindow;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_TERM_WINDOW:
LOGV("APP_CMD_TERM_WINDOW");
pthread_cond_broadcast(&android_app->cond);
break;
case APP_CMD_RESUME:
case APP_CMD_START:
case APP_CMD_PAUSE:
case APP_CMD_STOP:
LOGV("activityState=%d", cmd);
pthread_mutex_lock(&android_app->mutex);
android_app->activityState = cmd;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_CONFIG_CHANGED:
LOGV("APP_CMD_CONFIG_CHANGED");
AConfiguration_fromAssetManager(android_app->config,
android_app->activity->assetManager);
print_cur_config(android_app);
break;
case APP_CMD_DESTROY:
LOGV("APP_CMD_DESTROY");
android_app->destroyRequested = 1;
break;
}
}
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
switch (cmd) {
case APP_CMD_TERM_WINDOW:
LOGV("APP_CMD_TERM_WINDOW");
pthread_mutex_lock(&android_app->mutex);
android_app->window = NULL;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_SAVE_STATE:
LOGV("APP_CMD_SAVE_STATE");
pthread_mutex_lock(&android_app->mutex);
android_app->stateSaved = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_RESUME:
free_saved_state(android_app);
break;
}
}
void app_dummy() {}
static void android_app_destroy(struct android_app* android_app) {
LOGV("android_app_destroy!");
free_saved_state(android_app);
pthread_mutex_lock(&android_app->mutex);
AConfiguration_delete(android_app->config);
android_app->destroyed = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
// Can't touch android_app object after this.
}
static void process_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);
android_app_post_exec_cmd(app, cmd);
}
// 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();
LOGV("android_app = %p", android_app);
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);
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);
android_app->looper = looper;
pthread_mutex_lock(&android_app->mutex);
android_app->running = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
_rust_glue_entry(android_app);
android_app_destroy(android_app);
return NULL;
}
// Codes from https://developer.android.com/reference/android/view/KeyEvent
#define KEY_EVENT_KEYCODE_VOLUME_DOWN 25
#define KEY_EVENT_KEYCODE_VOLUME_MUTE 164
#define KEY_EVENT_KEYCODE_VOLUME_UP 24
#define KEY_EVENT_KEYCODE_CAMERA 27
#define KEY_EVENT_KEYCODE_ZOOM_IN 168
#define KEY_EVENT_KEYCODE_ZOOM_OUT 169
// Double-buffer the key event filter to avoid race condition.
static bool default_key_filter(const GameActivityKeyEvent* event) {
// Ignore camera, volume, etc. buttons
return !(event->keyCode == KEY_EVENT_KEYCODE_VOLUME_DOWN ||
event->keyCode == KEY_EVENT_KEYCODE_VOLUME_MUTE ||
event->keyCode == KEY_EVENT_KEYCODE_VOLUME_UP ||
event->keyCode == KEY_EVENT_KEYCODE_CAMERA ||
event->keyCode == KEY_EVENT_KEYCODE_ZOOM_IN ||
event->keyCode == KEY_EVENT_KEYCODE_ZOOM_OUT);
}
// See
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_TOUCHSCREEN
#define SOURCE_TOUCHSCREEN 0x00001002
static bool default_motion_filter(const GameActivityMotionEvent* event) {
// Ignore any non-touch events.
return (event->source & SOURCE_TOUCHSCREEN) != 0;
}
// --------------------------------------------------------------------
// Native activity interaction (called from main thread)
// --------------------------------------------------------------------
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));
memset(android_app, 0, sizeof(struct android_app));
android_app->activity = activity;
pthread_mutex_init(&android_app->mutex, NULL);
pthread_cond_init(&android_app->cond, NULL);
if (savedState != NULL) {
android_app->savedState = malloc(savedStateSize);
android_app->savedStateSize = savedStateSize;
memcpy(android_app->savedState, savedState, savedStateSize);
}
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));
return NULL;
}
android_app->msgread = msgpipe[0];
android_app->msgwrite = msgpipe[1];
android_app->keyEventFilter = default_key_filter;
android_app->motionEventFilter = default_motion_filter;
LOGV("Launching android_app_entry in a thread");
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
// Wait for thread to start.
pthread_mutex_lock(&android_app->mutex);
while (!android_app->running) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
return android_app;
}
void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
LOGE("Failure writing android_app cmd: %s", strerror(errno));
}
}
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);
}
android_app->pendingWindow = window;
if (window != NULL) {
android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
}
while (android_app->window != android_app->pendingWindow) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void android_app_set_activity_state(struct android_app* android_app,
int8_t cmd) {
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (!android_app->destroyed) {
android_app_write_cmd(android_app, cmd);
while (android_app->activityState != cmd) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
}
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);
}
static inline struct android_app* ToApp(GameActivity* activity) {
return (struct android_app*)activity->instance;
}
static void onDestroy(GameActivity* activity) {
LOGV("Destroy: %p", activity);
android_app_free(ToApp(activity));
}
static void onStart(GameActivity* activity) {
LOGV("Start: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_START);
}
static void onResume(GameActivity* activity) {
LOGV("Resume: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME);
}
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) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
if (android_app->savedState != NULL) {
// Tell the Java side about our state.
recallback((const char*)android_app->savedState,
android_app->savedStateSize, context);
// Now we can free it.
free(android_app->savedState);
android_app->savedState = NULL;
android_app->savedStateSize = 0;
}
pthread_mutex_unlock(&android_app->mutex);
}
static void onPause(GameActivity* activity) {
LOGV("Pause: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE);
}
static void onStop(GameActivity* activity) {
LOGV("Stop: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_STOP);
}
static void onConfigurationChanged(GameActivity* activity) {
LOGV("ConfigurationChanged: %p", activity);
android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED);
}
static void onTrimMemory(GameActivity* activity, int level) {
LOGV("TrimMemory: %p %d", activity, level);
android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY);
}
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);
}
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) {
LOGV("NativeWindowDestroyed: %p -- %p", activity, window);
android_app_set_window(ToApp(activity), NULL);
}
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);
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED);
}
void android_app_set_motion_event_filter(struct android_app* app,
android_motion_event_filter filter) {
pthread_mutex_lock(&app->mutex);
app->motionEventFilter = filter;
pthread_mutex_unlock(&app->mutex);
}
bool android_app_input_available_wake_up(struct android_app* app) {
pthread_mutex_lock(&app->mutex);
bool available = app->inputAvailableWakeUp;
app->inputAvailableWakeUp = false;
pthread_mutex_unlock(&app->mutex);
return available;
}
// NB: should be called with the android_app->mutex held already
static void notifyInput(struct android_app* android_app) {
// Don't spam the mainloop with wake ups if we've already sent one
if (android_app->inputSwapPending) {
return;
}
if (android_app->looper != NULL) {
// for the app thread to know why it received the wake() up
android_app->inputAvailableWakeUp = true;
android_app->inputSwapPending = true;
ALooper_wake(android_app->looper);
}
}
static bool onTouchEvent(GameActivity* activity,
const GameActivityMotionEvent* event) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (android_app->destroyed) {
pthread_mutex_unlock(&android_app->mutex);
return false;
}
if (android_app->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];
// Add to the list of active motion events
if (inputBuffer->motionEventsCount >= inputBuffer->motionEventsBufferSize) {
inputBuffer->motionEventsBufferSize *= 2;
inputBuffer->motionEvents = (GameActivityMotionEvent*)realloc(
inputBuffer->motionEvents,
sizeof(GameActivityMotionEvent) * inputBuffer->motionEventsBufferSize);
if (inputBuffer->motionEvents == NULL) {
LOGE("onTouchEvent: out of memory");
abort();
}
}
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) {
pthread_mutex_lock(&android_app->mutex);
struct android_input_buffer* inputBuffer =
&android_app->inputBuffers[android_app->currentInputBuffer];
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->inputSwapPending = false;
android_app->inputAvailableWakeUp = false;
pthread_mutex_unlock(&android_app->mutex);
return inputBuffer;
}
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) {
// We do not need to lock here if the inputBuffer has already been swapped
// as is handled by the game loop thread
while (inputBuffer->motionEventsCount > 0) {
GameActivityMotionEvent_destroy(
&inputBuffer->motionEvents[inputBuffer->motionEventsCount - 1]);
inputBuffer->motionEventsCount--;
}
assert(inputBuffer->motionEventsCount == 0);
}
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);
}
static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (android_app->destroyed) {
pthread_mutex_unlock(&android_app->mutex);
return false;
}
if (android_app->keyEventFilter != NULL &&
!android_app->keyEventFilter(event)) {
pthread_mutex_unlock(&android_app->mutex);
return false;
}
struct android_input_buffer* inputBuffer =
&android_app->inputBuffers[android_app->currentInputBuffer];
// Add to the list of active key down events
if (inputBuffer->keyEventsCount >= inputBuffer->keyEventsBufferSize) {
inputBuffer->keyEventsBufferSize = inputBuffer->keyEventsBufferSize * 2;
inputBuffer->keyEvents = (GameActivityKeyEvent*)realloc(
inputBuffer->keyEvents,
sizeof(GameActivityKeyEvent) * inputBuffer->keyEventsBufferSize);
if (inputBuffer->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;
}
void android_app_clear_key_events(struct android_input_buffer* inputBuffer) {
inputBuffer->keyEventsCount = 0;
}
static void onTextInputEvent(GameActivity* activity,
const GameTextInputState* state) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
if (!android_app->destroyed) {
android_app->textInputState = 1;
notifyInput(android_app);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void onWindowInsetsChanged(GameActivity* activity) {
LOGV("WindowInsetsChanged: %p", 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) {
LOGV("Creating: %p", activity);
activity->callbacks->onDestroy = onDestroy;
activity->callbacks->onStart = onStart;
activity->callbacks->onResume = onResume;
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
activity->callbacks->onPause = onPause;
activity->callbacks->onStop = onStop;
activity->callbacks->onTouchEvent = onTouchEvent;
activity->callbacks->onKeyDown = onKey;
activity->callbacks->onKeyUp = onKey;
activity->callbacks->onTextInputEvent = onTextInputEvent;
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onTrimMemory = onTrimMemory;
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
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);
}
@@ -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,929 @@
/*
* 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_MAJOR_VERSION 4
#define GAMETEXTINPUT_MINOR_VERSION 0
#define GAMETEXTINPUT_BUGFIX_VERSION 0
#define GAMETEXTINPUT_PACKED_VERSION \
ANDROID_GAMESDK_PACKED_VERSION(GAMETEXTINPUT_MAJOR_VERSION, \
GAMETEXTINPUT_MINOR_VERSION, \
GAMETEXTINPUT_BUGFIX_VERSION)
/**
* This struct holds a span within a region of text from start (inclusive) to
* end (exclusive). An empty span or cursor position is specified with
* 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.
* @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
}
}
@@ -0,0 +1,386 @@
/*
* 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.
*/
#include "game-text-input/gametextinput.h"
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <memory>
#include <mutex>
#include <vector>
#define LOG_TAG "GameTextInput"
static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16;
// Cache of field ids in the Java GameTextInputState class
struct StateClassInfo {
jfieldID text;
jfieldID selectionStart;
jfieldID selectionEnd;
jfieldID composingRegionStart;
jfieldID composingRegionEnd;
};
// Main GameTextInput object.
struct GameTextInput {
public:
GameTextInput(JNIEnv *env, uint32_t max_string_size);
~GameTextInput();
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 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:
// Copy string and set other fields
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_;
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;
StateClassInfo stateClassInfo_ = {};
// Constant-sized buffer used to store state text.
std::vector<char> stateStringBuffer_;
};
std::unique_ptr<GameTextInput> s_gameTextInput;
extern "C" {
///////////////////////////////////////////////////////////
/// GameTextInputState C Functions
///////////////////////////////////////////////////////////
// Convert to a Java structure.
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) {
gameTextInput->stateFromJava(textInputEvent, callback, context);
}
///////////////////////////////////////////////////////////
/// GameTextInput C Functions
///////////////////////////////////////////////////////////
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 "
"calling GameTextInput_destroy");
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));
return s_gameTextInput.get();
}
void GameTextInput_destroy(GameTextInput *input) {
if (input == nullptr || s_gameTextInput.get() == nullptr) return;
s_gameTextInput.reset();
}
void GameTextInput_setState(GameTextInput *input,
const GameTextInputState *state) {
if (state == nullptr) return;
input->setState(*state);
}
void GameTextInput_getState(GameTextInput *input,
GameTextInputGetStateCallback callback,
void *context) {
GameTextInputState state = input->getState();
callback(context, &state);
}
void GameTextInput_setInputConnection(GameTextInput *input,
jobject inputConnection) {
input->setInputConnection(inputConnection);
}
void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) {
input->processEvent(textInputEvent);
}
void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) {
input->processImeInsets(insets);
}
void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) {
input->showIme(flags);
}
void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) {
input->hideIme(flags);
}
void GameTextInput_restartInput(struct GameTextInput *input) {
input->restartInput();
}
void GameTextInput_setEventCallback(struct GameTextInput *input,
GameTextInputEventCallback callback,
void *context) {
input->setEventCallback(callback, context);
}
void GameTextInput_setImeInsetsCallback(struct GameTextInput *input,
GameTextInputImeInsetsCallback callback,
void *context) {
input->setImeInsetsCallback(callback, context);
}
void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) {
*insets = input->getImeInsets();
}
} // 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) {
stateJavaClass_ = (jclass)env_->NewGlobalRef(
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");
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_.composingRegionStart =
env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I");
stateClassInfo_.composingRegionEnd =
env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I");
}
GameTextInput::~GameTextInput() {
if (stateJavaClass_ != NULL) {
env_->DeleteGlobalRef(stateJavaClass_);
stateJavaClass_ = NULL;
}
if (inputConnectionClass_ != NULL) {
env_->DeleteGlobalRef(inputConnectionClass_);
inputConnectionClass_ = NULL;
}
if (inputConnection_ != NULL) {
env_->DeleteGlobalRef(inputConnection_);
inputConnection_ = NULL;
}
}
void GameTextInput::setState(const GameTextInputState &state) {
if (inputConnection_ == nullptr) return;
jobject jstate = stateToJava(state);
env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_,
jstate);
env_->DeleteLocalRef(jstate);
setStateInner(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) {
currentState_ = 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()));
currentState_.text_UTF8 = 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;
stateStringBuffer_[bytes_needed - 1] = 0;
}
void GameTextInput::setInputConnection(jobject inputConnection) {
if (inputConnection_ != NULL) {
env_->DeleteGlobalRef(inputConnection_);
}
inputConnection_ = env_->NewGlobalRef(inputConnection);
}
/*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_, &currentState_);
}
}
void GameTextInput::showIme(uint32_t flags) {
if (inputConnection_ == nullptr) return;
env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true,
static_cast<jint>(flags));
}
void GameTextInput::setEventCallback(GameTextInputEventCallback callback,
void *context) {
eventCallback_ = callback;
eventCallbackContext_ = context;
}
void GameTextInput::setImeInsetsCallback(
GameTextInputImeInsetsCallback callback, void *context) {
insetsCallback_ = callback;
insetsCallbackContext_ = context;
}
void GameTextInput::processImeInsets(const ARect *insets) {
currentInsets_ = *insets;
if (insetsCallback_) {
insetsCallback_(insetsCallbackContext_, &currentInsets_);
}
}
void GameTextInput::hideIme(uint32_t flags) {
if (inputConnection_ == nullptr) return;
env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false,
static_cast<jint>(flags));
}
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");
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;
if (text == nullptr) {
static char empty_string[] = "";
text = empty_string;
}
// 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);
env_->DeleteLocalRef(jtext);
return jobj;
}
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);
int composingRegionStart =
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart);
int composingRegionEnd =
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd);
GameTextInputState state{text_chars,
text_len,
{selectionStart, selectionEnd},
{composingRegionStart, composingRegionEnd}};
callback(context, &state);
env_->ReleaseStringUTFChars(text, text_chars);
env_->DeleteLocalRef(text);
}
@@ -0,0 +1,9 @@
{
"name": "game-text-input",
"schema_version": 1,
"dependencies": [],
"version": "0.0.1",
"cpp_files": [
"src/game-text-input/gametextinput.cpp"
]
}
+55
View File
@@ -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"
@@ -31,11 +31,12 @@
// 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))
((MAJOR << 16) | (MINOR << 8) | (BUGFIX))
// Accessors
#define ANDROID_GAMESDK_MAJOR_VERSION(PACKED) ((PACKED) >> 16)
#define ANDROID_GAMESDK_MINOR_VERSION(PACKED) (((PACKED) >> 8) & 0xff)
#define ANDROID_GAMESDK_BUGFIX_VERSION(PACKED) ((PACKED) & 0xff)
#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX, GIT) \
#MAJOR "." #MINOR "." #BUGFIX "." #GIT
#define AGDK_STRINGIFY(NUMBER) #NUMBER
#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX) \
AGDK_STRINGIFY(MAJOR) "." AGDK_STRINGIFY(MINOR) "." AGDK_STRINGIFY(BUGFIX)
@@ -0,0 +1,72 @@
/*
* 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
+85 -21
View File
@@ -1,41 +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",
"GameActivity.cpp",
"GameActivityEvents.h",
"GameActivityEvents.cpp",
"GameActivityLog.h",
] {
println!("cargo:rerun-if-changed=game-activity-csrc/game-activity/{f}");
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")
.file("game-activity-csrc/game-activity/GameActivityEvents.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");
for f in ["gamecommon.h", "gametextinput.h", "gametextinput.cpp"] {
println!("cargo:rerun-if-changed=game-activity-csrc/game-text-input/{f}");
}
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");
for f in ["android_native_app_glue.h", "android_native_app_glue.c"] {
println!("cargo:rerun-if-changed=game-activity-csrc/native_app_glue/{f}");
}
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");
@@ -46,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");
}
}
File diff suppressed because it is too large Load Diff
@@ -1,636 +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.
*/
/**
* @addtogroup GameActivity Game Activity
* The interface to use GameActivity.
* @{
*/
/**
* @file GameActivity.h
*/
#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_H
#define ANDROID_GAME_SDK_GAME_ACTIVITY_H
#include <android/asset_manager.h>
#include <android/input.h>
#include <android/native_window.h>
#include <android/rect.h>
#include <jni.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#include "common/gamesdk_common.h"
#include "game-activity/GameActivityEvents.h"
#include "game-text-input/gametextinput.h"
#ifdef __cplusplus
extern "C" {
#endif
#define GAMEACTIVITY_MAJOR_VERSION 2
#define GAMEACTIVITY_MINOR_VERSION 0
#define GAMEACTIVITY_BUGFIX_VERSION 2
#define GAMEACTIVITY_PACKED_VERSION \
ANDROID_GAMESDK_PACKED_VERSION(GAMEACTIVITY_MAJOR_VERSION, \
GAMEACTIVITY_MINOR_VERSION, \
GAMEACTIVITY_BUGFIX_VERSION)
/**
* {@link GameActivityCallbacks}
*/
struct GameActivityCallbacks;
/**
* This structure defines the native side of an android.app.GameActivity.
* It is created by the framework, and handed to the application's native
* code as it is being launched.
*/
typedef struct GameActivity {
/**
* Pointer to the callback function table of the native application.
* You can set the functions here to your own callbacks. The callbacks
* pointer itself here should not be changed; it is allocated and managed
* for you by the framework.
*/
struct GameActivityCallbacks* callbacks;
/**
* The global handle on the process's Java VM.
*/
JavaVM* vm;
/**
* JNI context for the main thread of the app. Note that this field
* can ONLY be used from the main thread of the process; that is, the
* thread that calls into the GameActivityCallbacks.
*/
JNIEnv* env;
/**
* The GameActivity object handle.
*/
jobject javaGameActivity;
/**
* Path to this application's internal data directory.
*/
const char* internalDataPath;
/**
* Path to this application's external (removable/mountable) data directory.
*/
const char* externalDataPath;
/**
* The platform's SDK version code.
*/
int32_t sdkVersion;
/**
* This is the native instance of the application. It is not used by
* the framework, but can be set by the application to its own instance
* state.
*/
void* instance;
/**
* Pointer to the Asset Manager instance for the application. The
* application uses this to access binary assets bundled inside its own .apk
* file.
*/
AAssetManager* assetManager;
/**
* Available starting with Honeycomb: path to the directory containing
* the application's OBB files (if any). If the app doesn't have any
* OBB files, this directory may not exist.
*/
const char* obbPath;
} GameActivity;
/**
* 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);
/**
* These are the callbacks the framework makes into a native application.
* All of these callbacks happen on the main thread of the application.
* By default, all callbacks are NULL; set to a pointer to your own function
* to have it called.
*/
typedef struct GameActivityCallbacks {
/**
* GameActivity has started. See Java documentation for Activity.onStart()
* for more information.
*/
void (*onStart)(GameActivity* activity);
/**
* GameActivity has resumed. See Java documentation for Activity.onResume()
* for more information.
*/
void (*onResume)(GameActivity* activity);
/**
* The framework is asking GameActivity to save its current instance state.
* See the Java documentation for Activity.onSaveInstanceState() for more
* information. The user should call the recallback with their data, its
* length and the provided context; they retain ownership of the data. Note
* 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* context);
/**
* GameActivity has paused. See Java documentation for Activity.onPause()
* for more information.
*/
void (*onPause)(GameActivity* activity);
/**
* GameActivity has stopped. See Java documentation for Activity.onStop()
* for more information.
*/
void (*onStop)(GameActivity* activity);
/**
* GameActivity is being destroyed. See Java documentation for
* Activity.onDestroy() for more information.
*/
void (*onDestroy)(GameActivity* activity);
/**
* Focus has changed in this GameActivity's window. This is often used,
* for example, to pause a game when it loses input focus.
*/
void (*onWindowFocusChanged)(GameActivity* activity, bool hasFocus);
/**
* 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);
/**
* 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);
/**
* The drawing window for this native activity needs to be redrawn. To
* avoid transient artifacts during screen changes (such resizing after
* 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);
/**
* The drawing window for this native activity is going to be destroyed.
* You MUST ensure that you do not touch the window object after returning
* from this function: in the common case of drawing to the window from
* another thread, that means the implementation of this callback must
* properly synchronize with the other thread to stop its drawing before
* returning from here.
*/
void (*onNativeWindowDestroyed)(GameActivity* activity,
ANativeWindow* window);
/**
* The current device AConfiguration has changed. The new configuration can
* be retrieved from assetManager.
*/
void (*onConfigurationChanged)(GameActivity* activity);
/**
* The system is running low on memory. Use this callback to release
* resources you do not need, to help the system avoid killing more
* important processes.
*/
void (*onTrimMemory)(GameActivity* activity, int level);
/**
* Callback called for every MotionEvent done on the GameActivity
* SurfaceView. Ownership of `event` is maintained by the library and it is
* only valid during the callback.
*/
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);
/**
* Callback called for every key up event on the GameActivity SurfaceView.
* Ownership of `event` is maintained by the library and it is only valid
* during the callback.
*/
bool (*onKeyUp)(GameActivity* activity, const GameActivityKeyEvent* event);
/**
* Callback called for every soft-keyboard text input event.
* Ownership of `state` is maintained by the library and it is only valid
* during the callback.
*/
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);
} GameActivityCallbacks;
/**
* 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
* above); if the code is being instantiated from a previously saved instance,
* the savedState will be non-NULL and point to the saved data. You must make
* any copy of this data you need -- it will be released after you return from
* this function.
*/
typedef void GameActivity_createFunc(GameActivity* activity, void* savedState,
size_t savedStateSize);
/**
* The name of the function that NativeInstance looks for when launching its
* native code. This is the default function that is used, you can specify
* "android.app.func_name" string meta-data in your manifest to use a different
* function.
*/
extern GameActivity_createFunc GameActivity_onCreate_C;
/**
* Finish the given activity. Its finish() method will be called, causing it
* to be stopped and destroyed. 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.
*/
void GameActivity_finish(GameActivity* activity);
/**
* Flags for GameActivity_setWindowFlags,
* as per the Java API at android.view.WindowManager.LayoutParams.
*/
enum GameActivitySetWindowFlags {
/**
* 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
* independently, or in combination with {@link
* GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} and/or {@link
* GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}
*/
GAMEACTIVITY_FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001,
/** Everything behind this window will be dimmed. */
GAMEACTIVITY_FLAG_DIM_BEHIND = 0x00000002,
/**
* Blur everything behind this window.
* @deprecated Blurring is no longer supported.
*/
GAMEACTIVITY_FLAG_BLUR_BEHIND = 0x00000004,
/**
* This window won't ever get key input focus, so the
* user can not send key or other button events to it. Those will
* instead go to whatever focusable window is behind it. This flag
* will also enable {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL} whether or not
* that is explicitly set.
*
* Setting this flag also implies that the window will not need to
* interact with
* a soft input method, so it will be Z-ordered and positioned
* independently of any active input method (typically this means it
* gets Z-ordered on top of the input method, so it can use the full
* screen for its content and cover the input method if needed. You
* can use {@link GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM} to modify this
* behavior.
*/
GAMEACTIVITY_FLAG_NOT_FOCUSABLE = 0x00000008,
/** This window can never receive touch events. */
GAMEACTIVITY_FLAG_NOT_TOUCHABLE = 0x00000010,
/**
* Even when this window is focusable (its
* {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set), allow any pointer
* events outside of the window to be sent to the windows behind it.
* Otherwise it will consume all pointer events itself, regardless of
* whether they are inside of the window.
*/
GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL = 0x00000020,
/**
* When set, if the device is asleep when the touch
* screen is pressed, you will receive this first touch event. Usually
* the first touch event is consumed by the system since the user can
* not see what they are pressing on.
*
* @deprecated This flag has no effect.
*/
GAMEACTIVITY_FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040,
/**
* As long as this window is visible to the user, keep
* the device's screen turned on and bright.
*/
GAMEACTIVITY_FLAG_KEEP_SCREEN_ON = 0x00000080,
/**
* Place the window within the entire screen, ignoring
* decorations around the border (such as the status bar). The
* window must correctly position its contents to take the screen
* decoration into account.
*/
GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN = 0x00000100,
/** Allows the window to extend outside of the screen. */
GAMEACTIVITY_FLAG_LAYOUT_NO_LIMITS = 0x00000200,
/**
* Hide all screen decorations (such as the status
* bar) while this window is displayed. This allows the window to
* use the entire display space for itself -- the status bar will
* be hidden when an app window with this flag set is on the top
* layer. A fullscreen window will ignore a value of {@link
* GAMEACTIVITY_SOFT_INPUT_ADJUST_RESIZE}; the window will stay
* fullscreen and will not resize.
*/
GAMEACTIVITY_FLAG_FULLSCREEN = 0x00000400,
/**
* Override {@link GAMEACTIVITY_FLAG_FULLSCREEN} and force the
* screen decorations (such as the status bar) to be shown.
*/
GAMEACTIVITY_FLAG_FORCE_NOT_FULLSCREEN = 0x00000800,
/**
* Turn on dithering when compositing this window to
* the screen.
* @deprecated This flag is no longer used.
*/
GAMEACTIVITY_FLAG_DITHER = 0x00001000,
/**
* Treat the content of the window as secure, preventing
* it from appearing in screenshots or from being viewed on non-secure
* displays.
*/
GAMEACTIVITY_FLAG_SECURE = 0x00002000,
/**
* A special mode where the layout parameters are used
* to perform scaling of the surface when it is composited to the
* screen.
*/
GAMEACTIVITY_FLAG_SCALED = 0x00004000,
/**
* Intended for windows that will often be used when the user is
* holding the screen against their face, it will aggressively
* filter the event stream to prevent unintended presses in this
* situation that may not be desired for a particular window, when
* such an event stream is detected, the application will receive
* a {@link AMOTION_EVENT_ACTION_CANCEL} to indicate this so
* applications can handle this accordingly by taking no action on
* the event until the finger is released.
*/
GAMEACTIVITY_FLAG_IGNORE_CHEEK_PRESSES = 0x00008000,
/**
* A special option only for use in combination with
* {@link GAMEACTIVITY_FLAG_LAYOUT_IN_SCREEN}. When requesting layout in
* the screen your window may appear on top of or behind screen decorations
* such as the status bar. By also including this flag, the window
* manager will report the inset rectangle needed to ensure your
* content is not covered by screen decorations.
*/
GAMEACTIVITY_FLAG_LAYOUT_INSET_DECOR = 0x00010000,
/**
* Invert the state of {@link GAMEACTIVITY_FLAG_NOT_FOCUSABLE} with
* respect to how this window interacts with the current method.
* That is, if FLAG_NOT_FOCUSABLE is set and this flag is set,
* then the window will behave as if it needs to interact with the
* input method and thus be placed behind/away from it; if {@link
* GAMEACTIVITY_FLAG_NOT_FOCUSABLE} is not set and this flag is set,
* then the window will behave as if it doesn't need to interact
* with the input method and can be placed to use more space and
* cover the input method.
*/
GAMEACTIVITY_FLAG_ALT_FOCUSABLE_IM = 0x00020000,
/**
* If you have set {@link GAMEACTIVITY_FLAG_NOT_TOUCH_MODAL}, you
* can set this flag to receive a single special MotionEvent with
* the action
* {@link AMOTION_EVENT_ACTION_OUTSIDE} for
* touches that occur outside of your window. Note that you will not
* receive the full down/move/up gesture, only the location of the
* first down as an {@link AMOTION_EVENT_ACTION_OUTSIDE}.
*/
GAMEACTIVITY_FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000,
/**
* Special flag to let windows be shown when the screen
* is locked. This will let application windows take precedence over
* key guard or any other lock screens. Can be used with
* {@link GAMEACTIVITY_FLAG_KEEP_SCREEN_ON} to turn screen on and display
* windows directly before showing the key guard window. Can be used with
* {@link GAMEACTIVITY_FLAG_DISMISS_KEYGUARD} to automatically fully
* dismisss non-secure keyguards. This flag only applies to the top-most
* full-screen window.
*/
GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED = 0x00080000,
/**
* Ask that the system wallpaper be shown behind
* your window. The window surface must be translucent to be able
* to actually see the wallpaper behind it; this flag just ensures
* that the wallpaper surface will be there if this window actually
* has translucent regions.
*/
GAMEACTIVITY_FLAG_SHOW_WALLPAPER = 0x00100000,
/**
* When set as a window is being added or made
* visible, once the window has been shown then the system will
* poke the power manager's user activity (as if the user had woken
* up the device) to turn the screen on.
*/
GAMEACTIVITY_FLAG_TURN_SCREEN_ON = 0x00200000,
/**
* When set the window will cause the keyguard to
* be dismissed, only if it is not a secure lock keyguard. Because such
* a keyguard is not needed for security, it will never re-appear if
* the user navigates to another window (in contrast to
* {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED}, which will only temporarily
* hide both secure and non-secure keyguards but ensure they reappear
* when the user moves to another UI that doesn't hide them).
* If the keyguard is currently active and is secure (requires an
* unlock pattern) than the user will still need to confirm it before
* seeing this window, unless {@link GAMEACTIVITY_FLAG_SHOW_WHEN_LOCKED} has
* also been set.
*/
GAMEACTIVITY_FLAG_DISMISS_KEYGUARD = 0x00400000,
};
/**
* Change the window flags of the given activity. Calls getWindow().setFlags()
* of the given activity.
* Note that some flags must be set before the window decoration is created,
* see
* https://developer.android.com/reference/android/view/Window#setFlags(int,%20int).
* Note also 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.
*/
void GameActivity_setWindowFlags(GameActivity* activity, uint32_t addFlags,
uint32_t removeFlags);
/**
* Flags for GameActivity_showSoftInput; see the Java InputMethodManager
* API for documentation.
*/
enum GameActivityShowSoftInputFlags {
/**
* Implicit request to show the input window, not as the result
* of a direct request by the user.
*/
GAMEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT = 0x0001,
/**
* The user has forced the input method open (such as by
* long-pressing menu) so it should not be closed until they
* explicitly do so.
*/
GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED = 0x0002,
};
/**
* Show the IME while in the given activity. Calls
* InputMethodManager.showSoftInput() for the given activity. 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_showSoftInput(GameActivity* activity, uint32_t flags);
/**
* 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);
/**
* 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* context);
/**
* Get a pointer to the GameTextInput library instance.
*/
GameTextInput* GameActivity_getTextInput(const GameActivity* activity);
/**
* Flags for GameActivity_hideSoftInput; see the Java InputMethodManager
* API for documentation.
*/
enum GameActivityHideSoftInputFlags {
/**
* The soft input window should only be hidden if it was not
* explicitly shown by the user.
*/
GAMEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY = 0x0001,
/**
* The soft input window should normally be hidden, unless it was
* originally shown with {@link GAMEACTIVITY_SHOW_SOFT_INPUT_FORCED}.
*/
GAMEACTIVITY_HIDE_SOFT_INPUT_NOT_ALWAYS = 0x0002,
};
/**
* Hide the IME while in the given activity. Calls
* InputMethodManager.hideSoftInput() for the given activity. 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.
*/
void GameActivity_hideSoftInput(GameActivity* activity, uint32_t flags);
/**
* Get the current window insets of the particular component. See
* https://developer.android.com/reference/androidx/core/view/WindowInsetsCompat.Type
* 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);
/**
* Set options on how the IME behaves when it is requested for text input.
* See
* 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.
*/
void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType,
int actionId, int imeOptions);
/**
* These are getters for Configuration class members. They may be called from
* any thread.
*/
int GameActivity_getOrientation(GameActivity* activity);
int GameActivity_getColorMode(GameActivity* activity);
int GameActivity_getDensityDpi(GameActivity* activity);
float GameActivity_getFontScale(GameActivity* activity);
int GameActivity_getFontWeightAdjustment(GameActivity* activity);
int GameActivity_getHardKeyboardHidden(GameActivity* activity);
int GameActivity_getKeyboard(GameActivity* activity);
int GameActivity_getKeyboardHidden(GameActivity* activity);
int GameActivity_getMcc(GameActivity* activity);
int GameActivity_getMnc(GameActivity* activity);
int GameActivity_getNavigation(GameActivity* activity);
int GameActivity_getNavigationHidden(GameActivity* activity);
int GameActivity_getOrientation(GameActivity* activity);
int GameActivity_getScreenHeightDp(GameActivity* activity);
int GameActivity_getScreenLayout(GameActivity* activity);
int GameActivity_getScreenWidthDp(GameActivity* activity);
int GameActivity_getSmallestScreenWidthDp(GameActivity* activity);
int GameActivity_getTouchscreen(GameActivity* activity);
int GameActivity_getUIMode(GameActivity* activity);
#ifdef __cplusplus
}
#endif
/** @} */
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_H
@@ -1,414 +0,0 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "GameActivityEvents.h"
#include <sys/system_properties.h>
#include <string>
#include "GameActivityLog.h"
// TODO(b/187147166): these functions were extracted from the Game SDK
// (gamesdk/src/common/system_utils.h). system_utils.h/cpp should be used
// instead.
namespace {
std::string getSystemPropViaGet(const char *key,
const char *default_value = "") {
char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator
int bufferLen = __system_property_get(key, buffer);
if (bufferLen > 0)
return buffer;
else
return "";
}
std::string GetSystemProp(const char *key, const char *default_value = "") {
return getSystemPropViaGet(key, default_value);
}
int GetSystemPropAsInt(const char *key, int default_value = 0) {
std::string prop = GetSystemProp(key);
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
}
} // anonymous namespace
#ifndef NELEM
#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
#endif
static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
/* AMOTION_EVENT_AXIS_X */ true,
/* AMOTION_EVENT_AXIS_Y */ true,
// Disable all other axes by default (they can be enabled using
// `GameActivityPointerAxes_enableAxis`).
false};
extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return;
}
enabledAxes[axis] = true;
}
float GameActivityPointerAxes_getAxisValue(
const GameActivityPointerAxes *pointerInfo, int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return 0;
}
if (!enabledAxes[axis]) {
ALOGW("Axis %d must be enabled before it can be accessed.", axis);
return 0;
}
return pointerInfo->axisValues[axis];
}
extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return;
}
enabledAxes[axis] = false;
}
float GameActivityMotionEvent_getHistoricalAxisValue(
const GameActivityMotionEvent *event, int axis, int pointerIndex,
int historyPos) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
ALOGE("Invalid axis %d", axis);
return -1;
}
if (pointerIndex < 0 || pointerIndex >= event->pointerCount) {
ALOGE("Invalid pointer index %d", pointerIndex);
return -1;
}
if (historyPos < 0 || historyPos >= event->historySize) {
ALOGE("Invalid history index %d", historyPos);
return -1;
}
if (!enabledAxes[axis]) {
ALOGW("Axis %d must be enabled before it can be accessed.", axis);
return 0;
}
int pointerOffset = pointerIndex * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
int historyValuesOffset = historyPos * event->pointerCount *
GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
return event
->historicalAxisValues[historyValuesOffset + pointerOffset + axis];
}
static struct {
jmethodID getDeviceId;
jmethodID getSource;
jmethodID getAction;
jmethodID getEventTime;
jmethodID getDownTime;
jmethodID getFlags;
jmethodID getMetaState;
jmethodID getActionButton;
jmethodID getButtonState;
jmethodID getClassification;
jmethodID getEdgeFlags;
jmethodID getHistorySize;
jmethodID getHistoricalEventTime;
jmethodID getPointerCount;
jmethodID getPointerId;
jmethodID getToolType;
jmethodID getRawX;
jmethodID getRawY;
jmethodID getXPrecision;
jmethodID getYPrecision;
jmethodID getAxisValue;
jmethodID getHistoricalAxisValue;
} gMotionEventClassInfo;
extern "C" void GameActivityMotionEvent_destroy(
GameActivityMotionEvent *c_event) {
delete c_event->historicalAxisValues;
delete c_event->historicalEventTimesMillis;
delete c_event->historicalEventTimesNanos;
}
extern "C" void GameActivityMotionEvent_fromJava(
JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event) {
static bool gMotionEventClassInfoInitialized = false;
if (!gMotionEventClassInfoInitialized) {
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
gMotionEventClassInfo = {0};
jclass motionEventClass = env->FindClass("android/view/MotionEvent");
gMotionEventClassInfo.getDeviceId =
env->GetMethodID(motionEventClass, "getDeviceId", "()I");
gMotionEventClassInfo.getSource =
env->GetMethodID(motionEventClass, "getSource", "()I");
gMotionEventClassInfo.getAction =
env->GetMethodID(motionEventClass, "getAction", "()I");
gMotionEventClassInfo.getEventTime =
env->GetMethodID(motionEventClass, "getEventTime", "()J");
gMotionEventClassInfo.getDownTime =
env->GetMethodID(motionEventClass, "getDownTime", "()J");
gMotionEventClassInfo.getFlags =
env->GetMethodID(motionEventClass, "getFlags", "()I");
gMotionEventClassInfo.getMetaState =
env->GetMethodID(motionEventClass, "getMetaState", "()I");
if (sdkVersion >= 23) {
gMotionEventClassInfo.getActionButton =
env->GetMethodID(motionEventClass, "getActionButton", "()I");
}
if (sdkVersion >= 14) {
gMotionEventClassInfo.getButtonState =
env->GetMethodID(motionEventClass, "getButtonState", "()I");
}
if (sdkVersion >= 29) {
gMotionEventClassInfo.getClassification =
env->GetMethodID(motionEventClass, "getClassification", "()I");
}
gMotionEventClassInfo.getEdgeFlags =
env->GetMethodID(motionEventClass, "getEdgeFlags", "()I");
gMotionEventClassInfo.getHistorySize =
env->GetMethodID(motionEventClass, "getHistorySize", "()I");
gMotionEventClassInfo.getHistoricalEventTime = env->GetMethodID(
motionEventClass, "getHistoricalEventTime", "(I)J");
gMotionEventClassInfo.getPointerCount =
env->GetMethodID(motionEventClass, "getPointerCount", "()I");
gMotionEventClassInfo.getPointerId =
env->GetMethodID(motionEventClass, "getPointerId", "(I)I");
gMotionEventClassInfo.getToolType =
env->GetMethodID(motionEventClass, "getToolType", "(I)I");
if (sdkVersion >= 29) {
gMotionEventClassInfo.getRawX =
env->GetMethodID(motionEventClass, "getRawX", "(I)F");
gMotionEventClassInfo.getRawY =
env->GetMethodID(motionEventClass, "getRawY", "(I)F");
}
gMotionEventClassInfo.getXPrecision =
env->GetMethodID(motionEventClass, "getXPrecision", "()F");
gMotionEventClassInfo.getYPrecision =
env->GetMethodID(motionEventClass, "getYPrecision", "()F");
gMotionEventClassInfo.getAxisValue =
env->GetMethodID(motionEventClass, "getAxisValue", "(II)F");
gMotionEventClassInfo.getHistoricalAxisValue = env->GetMethodID(
motionEventClass, "getHistoricalAxisValue", "(III)F");
gMotionEventClassInfoInitialized = true;
}
int pointerCount =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount);
pointerCount =
std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT);
out_event->pointerCount = pointerCount;
for (int i = 0; i < pointerCount; ++i) {
out_event->pointers[i] = {
/*id=*/env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getPointerId, i),
/*toolType=*/
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getToolType,
i),
/*axisValues=*/{0},
/*rawX=*/gMotionEventClassInfo.getRawX
? env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getRawX, i)
: 0,
/*rawY=*/gMotionEventClassInfo.getRawY
? env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getRawY, i)
: 0,
};
for (int axisIndex = 0;
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) {
if (enabledAxes[axisIndex]) {
out_event->pointers[i].axisValues[axisIndex] =
env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getAxisValue,
axisIndex, i);
}
}
}
int historySize =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize);
out_event->historySize = historySize;
out_event->historicalAxisValues =
new float[historySize * pointerCount *
GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
out_event->historicalEventTimesMillis = new int64_t[historySize];
out_event->historicalEventTimesNanos = new int64_t[historySize];
for (int historyIndex = 0; historyIndex < historySize; historyIndex++) {
out_event->historicalEventTimesMillis[historyIndex] =
env->CallLongMethod(motionEvent,
gMotionEventClassInfo.getHistoricalEventTime,
historyIndex);
out_event->historicalEventTimesNanos[historyIndex] =
out_event->historicalEventTimesMillis[historyIndex] * 1000000;
for (int i = 0; i < pointerCount; ++i) {
int pointerOffset = i * GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
int historyAxisOffset = historyIndex * pointerCount *
GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
float *axisValues =
&out_event
->historicalAxisValues[historyAxisOffset + pointerOffset];
for (int axisIndex = 0;
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
++axisIndex) {
if (enabledAxes[axisIndex]) {
axisValues[axisIndex] = env->CallFloatMethod(
motionEvent,
gMotionEventClassInfo.getHistoricalAxisValue, axisIndex,
i, historyIndex);
}
}
}
}
out_event->deviceId =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId);
out_event->source =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource);
out_event->action =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction);
out_event->eventTime =
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) *
1000000;
out_event->downTime =
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) *
1000000;
out_event->flags =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags);
out_event->metaState =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState);
out_event->actionButton =
gMotionEventClassInfo.getActionButton
? env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getActionButton)
: 0;
out_event->buttonState =
gMotionEventClassInfo.getButtonState
? env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getButtonState)
: 0;
out_event->classification =
gMotionEventClassInfo.getClassification
? env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getClassification)
: 0;
out_event->edgeFlags =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags);
out_event->precisionX =
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision);
out_event->precisionY =
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision);
}
static struct {
jmethodID getDeviceId;
jmethodID getSource;
jmethodID getAction;
jmethodID getEventTime;
jmethodID getDownTime;
jmethodID getFlags;
jmethodID getMetaState;
jmethodID getModifiers;
jmethodID getRepeatCount;
jmethodID getKeyCode;
jmethodID getScanCode;
//jmethodID getUnicodeChar;
} gKeyEventClassInfo;
extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent,
GameActivityKeyEvent *out_event) {
static bool gKeyEventClassInfoInitialized = false;
if (!gKeyEventClassInfoInitialized) {
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
gKeyEventClassInfo = {0};
jclass keyEventClass = env->FindClass("android/view/KeyEvent");
gKeyEventClassInfo.getDeviceId =
env->GetMethodID(keyEventClass, "getDeviceId", "()I");
gKeyEventClassInfo.getSource =
env->GetMethodID(keyEventClass, "getSource", "()I");
gKeyEventClassInfo.getAction =
env->GetMethodID(keyEventClass, "getAction", "()I");
gKeyEventClassInfo.getEventTime =
env->GetMethodID(keyEventClass, "getEventTime", "()J");
gKeyEventClassInfo.getDownTime =
env->GetMethodID(keyEventClass, "getDownTime", "()J");
gKeyEventClassInfo.getFlags =
env->GetMethodID(keyEventClass, "getFlags", "()I");
gKeyEventClassInfo.getMetaState =
env->GetMethodID(keyEventClass, "getMetaState", "()I");
if (sdkVersion >= 13) {
gKeyEventClassInfo.getModifiers =
env->GetMethodID(keyEventClass, "getModifiers", "()I");
}
gKeyEventClassInfo.getRepeatCount =
env->GetMethodID(keyEventClass, "getRepeatCount", "()I");
gKeyEventClassInfo.getKeyCode =
env->GetMethodID(keyEventClass, "getKeyCode", "()I");
gKeyEventClassInfo.getScanCode =
env->GetMethodID(keyEventClass, "getScanCode", "()I");
//gKeyEventClassInfo.getUnicodeChar =
// env->GetMethodID(keyEventClass, "getUnicodeChar", "()I");
gKeyEventClassInfoInitialized = true;
}
*out_event = {
/*deviceId=*/env->CallIntMethod(keyEvent,
gKeyEventClassInfo.getDeviceId),
/*source=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getSource),
/*action=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getAction),
// TODO: introduce a millisecondsToNanoseconds helper:
/*eventTime=*/
env->CallLongMethod(keyEvent, gKeyEventClassInfo.getEventTime) *
1000000,
/*downTime=*/
env->CallLongMethod(keyEvent, gKeyEventClassInfo.getDownTime) * 1000000,
/*flags=*/env->CallIntMethod(keyEvent, gKeyEventClassInfo.getFlags),
/*metaState=*/
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getMetaState),
/*modifiers=*/gKeyEventClassInfo.getModifiers
? env->CallIntMethod(keyEvent, gKeyEventClassInfo.getModifiers)
: 0,
/*repeatCount=*/
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getRepeatCount),
/*keyCode=*/
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getKeyCode),
/*scanCode=*/
env->CallIntMethod(keyEvent, gKeyEventClassInfo.getScanCode)
/*unicodeChar=*/
//env->CallIntMethod(keyEvent, gKeyEventClassInfo.getUnicodeChar)
};
}
@@ -1,732 +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.
*/
#include "android_native_app_glue.h"
#include <android/log.h>
#include <assert.h>
#include <errno.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE 16
#define NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE 4
#define LOGI(...) \
((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
#define LOGE(...) \
((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__); \
} \
} 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__))
#else
#define LOGV(...) ((void)0)
#endif
static void free_saved_state(struct android_app* android_app) {
pthread_mutex_lock(&android_app->mutex);
if (android_app->savedState != NULL) {
free(android_app->savedState);
android_app->savedState = NULL;
android_app->savedStateSize = 0;
}
pthread_mutex_unlock(&android_app->mutex);
}
int8_t android_app_read_cmd(struct android_app* android_app) {
int8_t cmd;
if (read(android_app->msgread, &cmd, sizeof(cmd)) != sizeof(cmd)) {
LOGE("No data on command pipe!");
return -1;
}
if (cmd == APP_CMD_SAVE_STATE) free_saved_state(android_app);
return cmd;
}
static void print_cur_config(struct android_app* android_app) {
char lang[2], country[2];
AConfiguration_getLanguage(android_app->config, lang);
AConfiguration_getCountry(android_app->config, country);
LOGV(
"Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
"keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
"modetype=%d modenight=%d",
AConfiguration_getMcc(android_app->config),
AConfiguration_getMnc(android_app->config), lang[0], lang[1],
country[0], country[1],
AConfiguration_getOrientation(android_app->config),
AConfiguration_getTouchscreen(android_app->config),
AConfiguration_getDensity(android_app->config),
AConfiguration_getKeyboard(android_app->config),
AConfiguration_getNavigation(android_app->config),
AConfiguration_getKeysHidden(android_app->config),
AConfiguration_getNavHidden(android_app->config),
AConfiguration_getSdkVersion(android_app->config),
AConfiguration_getScreenSize(android_app->config),
AConfiguration_getScreenLong(android_app->config),
AConfiguration_getUiModeType(android_app->config),
AConfiguration_getUiModeNight(android_app->config));
}
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
switch (cmd) {
case UNUSED_APP_CMD_INPUT_CHANGED:
LOGV("UNUSED_APP_CMD_INPUT_CHANGED");
// Do nothing. This can be used in the future to handle AInputQueue
// natively, like done in NativeActivity.
break;
case APP_CMD_INIT_WINDOW:
LOGV("APP_CMD_INIT_WINDOW");
pthread_mutex_lock(&android_app->mutex);
android_app->window = android_app->pendingWindow;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_TERM_WINDOW:
LOGV("APP_CMD_TERM_WINDOW");
pthread_cond_broadcast(&android_app->cond);
break;
case APP_CMD_RESUME:
case APP_CMD_START:
case APP_CMD_PAUSE:
case APP_CMD_STOP:
LOGV("activityState=%d", cmd);
pthread_mutex_lock(&android_app->mutex);
android_app->activityState = cmd;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_CONFIG_CHANGED:
LOGV("APP_CMD_CONFIG_CHANGED");
AConfiguration_fromAssetManager(
android_app->config, android_app->activity->assetManager);
print_cur_config(android_app);
break;
case APP_CMD_DESTROY:
LOGV("APP_CMD_DESTROY");
android_app->destroyRequested = 1;
break;
}
}
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
switch (cmd) {
case APP_CMD_TERM_WINDOW:
LOGV("APP_CMD_TERM_WINDOW");
pthread_mutex_lock(&android_app->mutex);
android_app->window = NULL;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_SAVE_STATE:
LOGV("APP_CMD_SAVE_STATE");
pthread_mutex_lock(&android_app->mutex);
android_app->stateSaved = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_RESUME:
free_saved_state(android_app);
break;
}
}
void app_dummy() {}
static void android_app_destroy(struct android_app* android_app) {
LOGV("android_app_destroy!");
free_saved_state(android_app);
pthread_mutex_lock(&android_app->mutex);
AConfiguration_delete(android_app->config);
android_app->destroyed = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
// Can't touch android_app object after this.
}
static void process_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);
android_app_post_exec_cmd(app, cmd);
}
// 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();
LOGV("android_app = %p", android_app);
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);
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);
android_app->looper = looper;
pthread_mutex_lock(&android_app->mutex);
android_app->running = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
_rust_glue_entry(android_app);
android_app_destroy(android_app);
return NULL;
}
// Codes from https://developer.android.com/reference/android/view/KeyEvent
#define KEY_EVENT_KEYCODE_VOLUME_DOWN 25
#define KEY_EVENT_KEYCODE_VOLUME_MUTE 164
#define KEY_EVENT_KEYCODE_VOLUME_UP 24
#define KEY_EVENT_KEYCODE_CAMERA 27
#define KEY_EVENT_KEYCODE_ZOOM_IN 168
#define KEY_EVENT_KEYCODE_ZOOM_OUT 169
// Double-buffer the key event filter to avoid race condition.
static bool default_key_filter(const GameActivityKeyEvent* event) {
// Ignore camera, volume, etc. buttons
return !(event->keyCode == KEY_EVENT_KEYCODE_VOLUME_DOWN ||
event->keyCode == KEY_EVENT_KEYCODE_VOLUME_MUTE ||
event->keyCode == KEY_EVENT_KEYCODE_VOLUME_UP ||
event->keyCode == KEY_EVENT_KEYCODE_CAMERA ||
event->keyCode == KEY_EVENT_KEYCODE_ZOOM_IN ||
event->keyCode == KEY_EVENT_KEYCODE_ZOOM_OUT);
}
// See
// https://developer.android.com/reference/android/view/InputDevice#SOURCE_TOUCHSCREEN
#define SOURCE_TOUCHSCREEN 0x00001002
static bool default_motion_filter(const GameActivityMotionEvent* event) {
// Ignore any non-touch events.
return event->source == SOURCE_TOUCHSCREEN;
}
// --------------------------------------------------------------------
// Native activity interaction (called from main thread)
// --------------------------------------------------------------------
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));
memset(android_app, 0, sizeof(struct android_app));
android_app->activity = activity;
pthread_mutex_init(&android_app->mutex, NULL);
pthread_cond_init(&android_app->cond, NULL);
if (savedState != NULL) {
android_app->savedState = malloc(savedStateSize);
android_app->savedStateSize = savedStateSize;
memcpy(android_app->savedState, savedState, savedStateSize);
}
int msgpipe[2];
if (pipe(msgpipe)) {
LOGE("could not create pipe: %s", strerror(errno));
return NULL;
}
android_app->msgread = msgpipe[0];
android_app->msgwrite = msgpipe[1];
android_app->keyEventFilter = default_key_filter;
android_app->motionEventFilter = default_motion_filter;
LOGV("Launching android_app_entry in a thread");
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
// Wait for thread to start.
pthread_mutex_lock(&android_app->mutex);
while (!android_app->running) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
return android_app;
}
static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
LOGE("Failure writing android_app cmd: %s", strerror(errno));
}
}
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);
}
android_app->pendingWindow = window;
if (window != NULL) {
android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
}
while (android_app->window != android_app->pendingWindow) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void android_app_set_activity_state(struct android_app* android_app,
int8_t cmd) {
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (!android_app->destroyed) {
android_app_write_cmd(android_app, cmd);
while (android_app->activityState != cmd) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
}
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);
free(android_app);
}
static inline struct android_app* ToApp(GameActivity* activity) {
return (struct android_app*)activity->instance;
}
static void onDestroy(GameActivity* activity) {
LOGV("Destroy: %p", activity);
android_app_free(ToApp(activity));
}
static void onStart(GameActivity* activity) {
LOGV("Start: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_START);
}
static void onResume(GameActivity* activity) {
LOGV("Resume: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_RESUME);
}
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) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
if (android_app->savedState != NULL) {
// Tell the Java side about our state.
recallback((const char*)android_app->savedState,
android_app->savedStateSize, context);
// Now we can free it.
free(android_app->savedState);
android_app->savedState = NULL;
android_app->savedStateSize = 0;
}
pthread_mutex_unlock(&android_app->mutex);
}
static void onPause(GameActivity* activity) {
LOGV("Pause: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_PAUSE);
}
static void onStop(GameActivity* activity) {
LOGV("Stop: %p", activity);
android_app_set_activity_state(ToApp(activity), APP_CMD_STOP);
}
static void onConfigurationChanged(GameActivity* activity) {
LOGV("ConfigurationChanged: %p", activity);
android_app_write_cmd(ToApp(activity), APP_CMD_CONFIG_CHANGED);
}
static void onTrimMemory(GameActivity* activity, int level) {
LOGV("TrimMemory: %p %d", activity, level);
android_app_write_cmd(ToApp(activity), APP_CMD_LOW_MEMORY);
}
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);
}
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) {
LOGV("NativeWindowDestroyed: %p -- %p", activity, window);
android_app_set_window(ToApp(activity), NULL);
}
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);
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_RESIZED);
}
void android_app_set_motion_event_filter(struct android_app* app,
android_motion_event_filter filter) {
pthread_mutex_lock(&app->mutex);
app->motionEventFilter = filter;
pthread_mutex_unlock(&app->mutex);
}
bool android_app_input_available_wake_up(struct android_app* app) {
pthread_mutex_lock(&app->mutex);
bool available = app->inputAvailableWakeUp;
app->inputAvailableWakeUp = false;
pthread_mutex_unlock(&app->mutex);
return available;
}
// NB: should be called with the android_app->mutex held already
static void notifyInput(struct android_app* android_app) {
// Don't spam the mainloop with wake ups if we've already sent one
if (android_app->inputSwapPending) {
return;
}
if (android_app->looper != NULL) {
// for the app thread to know why it received the wake() up
android_app->inputAvailableWakeUp = true;
android_app->inputSwapPending = true;
ALooper_wake(android_app->looper);
}
}
static bool onTouchEvent(GameActivity* activity,
const GameActivityMotionEvent* event) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (android_app->destroyed) {
pthread_mutex_unlock(&android_app->mutex);
return false;
}
if (android_app->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];
// Add to the list of active motion events
if (inputBuffer->motionEventsCount >= inputBuffer->motionEventsBufferSize) {
inputBuffer->motionEventsBufferSize *= 2;
inputBuffer->motionEvents = (GameActivityMotionEvent *) realloc(inputBuffer->motionEvents,
sizeof(GameActivityMotionEvent) * inputBuffer->motionEventsBufferSize);
if (inputBuffer->motionEvents == NULL) {
LOGE("onTouchEvent: out of memory");
abort();
}
}
int new_ix = inputBuffer->motionEventsCount;
memcpy(&inputBuffer->motionEvents[new_ix], event, sizeof(GameActivityMotionEvent));
++inputBuffer->motionEventsCount;
notifyInput(android_app);
pthread_mutex_unlock(&android_app->mutex);
return true;
}
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];
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->inputSwapPending = false;
android_app->inputAvailableWakeUp = false;
pthread_mutex_unlock(&android_app->mutex);
return inputBuffer;
}
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) {
// We do not need to lock here if the inputBuffer has already been swapped
// as is handled by the game loop thread
while (inputBuffer->motionEventsCount > 0) {
GameActivityMotionEvent_destroy(
&inputBuffer->motionEvents[inputBuffer->motionEventsCount - 1]);
inputBuffer->motionEventsCount--;
}
assert(inputBuffer->motionEventsCount == 0);
}
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);
}
static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (android_app->destroyed) {
pthread_mutex_unlock(&android_app->mutex);
return false;
}
if (android_app->keyEventFilter != NULL &&
!android_app->keyEventFilter(event)) {
pthread_mutex_unlock(&android_app->mutex);
return false;
}
struct android_input_buffer* inputBuffer =
&android_app->inputBuffers[android_app->currentInputBuffer];
// Add to the list of active key down events
if (inputBuffer->keyEventsCount >= inputBuffer->keyEventsBufferSize) {
inputBuffer->keyEventsBufferSize = inputBuffer->keyEventsBufferSize * 2;
inputBuffer->keyEvents = (GameActivityKeyEvent *) realloc(inputBuffer->keyEvents,
sizeof(GameActivityKeyEvent) * inputBuffer->keyEventsBufferSize);
if (inputBuffer->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);
pthread_mutex_unlock(&android_app->mutex);
return true;
}
void android_app_clear_key_events(struct android_input_buffer* inputBuffer) {
inputBuffer->keyEventsCount = 0;
}
static void onTextInputEvent(GameActivity* activity,
const GameTextInputState* state) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
if (!android_app->destroyed) {
android_app->textInputState = 1;
notifyInput(android_app);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void onWindowInsetsChanged(GameActivity* activity) {
LOGV("WindowInsetsChanged: %p", activity);
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_INSETS_CHANGED);
}
static void onContentRectChanged(GameActivity* activity, const ARect *rect) {
LOGV("ContentRectChanged: %p -- (%d %d) (%d %d)", activity, rect->left, rect->top,
rect->right, rect->bottom);
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
android_app->contentRect = *rect;
android_app_write_cmd(android_app, APP_CMD_CONTENT_RECT_CHANGED);
pthread_mutex_unlock(&android_app->mutex);
}
// XXX: This symbol is renamed with a _C suffix and then re-exported from
// Rust because Rust/Cargo don't give us a way to directly export symbols
// from C/C++ code: https://github.com/rust-lang/rfcs/issues/2771
//
JNIEXPORT
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
size_t savedStateSize) {
LOGV("Creating: %p", activity);
activity->callbacks->onDestroy = onDestroy;
activity->callbacks->onStart = onStart;
activity->callbacks->onResume = onResume;
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
activity->callbacks->onPause = onPause;
activity->callbacks->onStop = onStop;
activity->callbacks->onTouchEvent = onTouchEvent;
activity->callbacks->onKeyDown = onKey;
activity->callbacks->onKeyUp = onKey;
activity->callbacks->onTextInputEvent = onTextInputEvent;
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onTrimMemory = onTrimMemory;
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
activity->callbacks->onNativeWindowRedrawNeeded =
onNativeWindowRedrawNeeded;
activity->callbacks->onNativeWindowResized = onNativeWindowResized;
activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged;
activity->callbacks->onContentRectChanged = onContentRectChanged;
LOGV("Callbacks set: %p", activity->callbacks);
activity->instance =
android_app_create(activity, savedState, savedStateSize);
}
@@ -1,507 +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.
*/
#pragma once
/**
* @addtogroup android_native_app_glue Native App Glue library
* The glue library to interface your game loop with GameActivity.
* @{
*/
#include <android/configuration.h>
#include <android/looper.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include "game-activity/GameActivity.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* The GameActivity interface provided by <game-activity/GameActivity.h>
* is based on a set of application-provided callbacks that will be called
* by the Activity's main thread when certain events occur.
*
* This means that each one of this callbacks _should_ _not_ block, or they
* risk having the system force-close the application. This programming
* model is direct, lightweight, but constraining.
*
* The 'android_native_app_glue' static library is used to provide a different
* execution model where the application can implement its own main event
* loop in a different thread instead. Here's how it works:
*
* 1/ The application must provide a function named "android_main()" that
* will be called when the activity is created, in a new thread that is
* distinct from the activity's main thread.
*
* 2/ android_main() receives a pointer to a valid "android_app" structure
* that contains references to other important objects, e.g. the
* GameActivity obejct instance the application is running in.
*
* 3/ the "android_app" object holds an ALooper instance that already
* listens to activity lifecycle events (e.g. "pause", "resume").
* See APP_CMD_XXX declarations below.
*
* This corresponds to an ALooper identifier returned by
* ALooper_pollOnce with value LOOPER_ID_MAIN.
*
* Your application can use the same ALooper to listen to additional
* file-descriptors. They can either be callback based, or with return
* identifiers starting with LOOPER_ID_USER.
*
* 4/ Whenever you receive a LOOPER_ID_MAIN event,
* the returned data will point to an android_poll_source structure. You
* can call the process() function on it, and fill in android_app->onAppCmd
* to be called for your own processing of the event.
*
* Alternatively, you can call the low-level functions to read and process
* the data directly... look at the process_cmd() and process_input()
* implementations in the glue to see how to do this.
*
* See the sample named "native-activity" that comes with the NDK with a
* full usage example. Also look at the documentation of GameActivity.
*/
struct android_app;
/**
* Data associated with an ALooper fd that will be returned as the "outData"
* when that source has data ready.
*/
struct android_poll_source {
/**
* The identifier of this source. May be LOOPER_ID_MAIN or
* LOOPER_ID_INPUT.
*/
int32_t id;
/** The android_app this ident is associated with. */
struct android_app* app;
/**
* Function to call to perform the standard processing of data from
* this source.
*/
void (*process)(struct android_app* app,
struct android_poll_source* source);
};
struct android_input_buffer {
/**
* Pointer to a read-only array of GameActivityMotionEvent.
* Only the first motionEventsCount events are valid.
*/
GameActivityMotionEvent *motionEvents;
/**
* The number of valid motion events in `motionEvents`.
*/
uint64_t motionEventsCount;
/**
* The size of the `motionEvents` buffer.
*/
uint64_t motionEventsBufferSize;
/**
* Pointer to a read-only array of GameActivityKeyEvent.
* Only the first keyEventsCount events are valid.
*/
GameActivityKeyEvent *keyEvents;
/**
* The number of valid "Key" events in `keyEvents`.
*/
uint64_t keyEventsCount;
/**
* The size of the `keyEvents` buffer.
*/
uint64_t keyEventsBufferSize;
};
/**
* Function pointer declaration for the filtering of key events.
* A function with this signature should be passed to
* android_app_set_key_event_filter and return false for any events that should
* not be handled by android_native_app_glue. These events will be handled by
* the system instead.
*/
typedef bool (*android_key_event_filter)(const GameActivityKeyEvent*);
/**
* Function pointer definition for the filtering of motion events.
* A function with this signature should be passed to
* android_app_set_motion_event_filter and return false for any events that
* should not be handled by android_native_app_glue. These events will be
* handled by the system instead.
*/
typedef bool (*android_motion_event_filter)(const GameActivityMotionEvent*);
/**
* This is the interface for the standard glue code of a threaded
* application. In this model, the application's code is running
* in its own thread separate from the main thread of the process.
* It is not required that this thread be associated with the Java
* VM, although it will need to be in order to make JNI calls any
* Java objects.
*/
struct android_app {
/**
* An optional pointer to application-defined state.
*/
void* userData;
/**
* A required callback for processing main app commands (`APP_CMD_*`).
* This is called each frame if there are app commands that need processing.
*/
void (*onAppCmd)(struct android_app* app, int32_t cmd);
/** The GameActivity object instance that this app is running in. */
GameActivity* activity;
/** The current configuration the app is running in. */
AConfiguration* config;
/**
* The last activity saved state, as provided at creation time.
* It is NULL if there was no state. You can use this as you need; the
* memory will remain around until you call android_app_exec_cmd() for
* APP_CMD_RESUME, at which point it will be freed and savedState set to
* NULL. These variables should only be changed when processing a
* APP_CMD_SAVE_STATE, at which point they will be initialized to NULL and
* you can malloc your state and place the information here. In that case
* the memory will be freed for you later.
*/
void* savedState;
/**
* The size of the activity saved state. It is 0 if `savedState` is NULL.
*/
size_t savedStateSize;
/** The ALooper associated with the app's thread. */
ALooper* looper;
/** When non-NULL, this is the window surface that the app can draw in. */
ANativeWindow* window;
/**
* Current content rectangle of the window; this is the area where the
* window's content should be placed to be seen by the user.
*/
ARect contentRect;
/**
* Current state of the app's activity. May be either APP_CMD_START,
* APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP.
*/
int activityState;
/**
* This is non-zero when the application's GameActivity is being
* destroyed and waiting for the app thread to complete.
*/
int destroyRequested;
#define NATIVE_APP_GLUE_MAX_INPUT_BUFFERS 2
/**
* This is used for buffering input from GameActivity. Once ready, the
* application thread switches the buffers and processes what was
* accumulated.
*/
struct android_input_buffer inputBuffers[NATIVE_APP_GLUE_MAX_INPUT_BUFFERS];
int currentInputBuffer;
/**
* 0 if no text input event is outstanding, 1 if it is.
* Use `GameActivity_getTextInputState` to get information
* about the text entered by the user.
*/
int textInputState;
// Below are "private" implementation of the glue code.
/** @cond INTERNAL */
pthread_mutex_t mutex;
pthread_cond_t cond;
int msgread;
int msgwrite;
pthread_t thread;
struct android_poll_source cmdPollSource;
int running;
int stateSaved;
int destroyed;
int redrawNeeded;
ANativeWindow* pendingWindow;
ARect pendingContentRect;
android_key_event_filter keyEventFilter;
android_motion_event_filter motionEventFilter;
// When new input is received we set both of these flags and use the looper to
// wake up the application mainloop.
//
// To avoid spamming the mainloop with wake ups from lots of input though we
// don't sent a wake up if the inputSwapPending flag is already set. (i.e.
// we already expect input to be processed in a finite amount of time due to
// our previous wake up)
//
// When a wake up is received then we will check this flag (clearing it
// at the same time). If it was set then an InputAvailable event is sent to
// the application - which should lead to all input being processed within
// a finite amount of time.
//
// The next time android_app_swap_input_buffers is called, both flags will be
// cleared.
//
// NB: both of these should only be read with the app mutex held
bool inputAvailableWakeUp;
bool inputSwapPending;
/** @endcond */
};
/**
* Looper ID of commands coming from the app's main thread, an AInputQueue or
* user-defined sources.
*/
enum NativeAppGlueLooperId {
/**
* Looper data ID of commands coming from the app's main thread, which
* is returned as an identifier from ALooper_pollOnce(). The data for this
* identifier is a pointer to an android_poll_source structure.
* These can be retrieved and processed with android_app_read_cmd()
* and android_app_exec_cmd().
*/
LOOPER_ID_MAIN = 1,
/**
* Unused. Reserved for future use when usage of AInputQueue will be
* supported.
*/
LOOPER_ID_INPUT = 2,
/**
* Start of user-defined ALooper identifiers.
*/
LOOPER_ID_USER = 3,
};
/**
* Commands passed from the application's main Java thread to the game's thread.
*/
enum NativeAppGlueAppCmd {
/**
* Unused. Reserved for future use when usage of AInputQueue will be
* supported.
*/
UNUSED_APP_CMD_INPUT_CHANGED,
/**
* Command from main thread: a new ANativeWindow is ready for use. Upon
* receiving this command, android_app->window will contain the new window
* surface.
*/
APP_CMD_INIT_WINDOW,
/**
* Command from main thread: the existing ANativeWindow needs to be
* terminated. Upon receiving this command, android_app->window still
* contains the existing window; after calling android_app_exec_cmd
* it will be set to NULL.
*/
APP_CMD_TERM_WINDOW,
/**
* Command from main thread: the current ANativeWindow has been resized.
* Please redraw with its new size.
*/
APP_CMD_WINDOW_RESIZED,
/**
* Command from main thread: the system needs that the current ANativeWindow
* be redrawn. You should redraw the window before handing this to
* android_app_exec_cmd() in order to avoid transient drawing glitches.
*/
APP_CMD_WINDOW_REDRAW_NEEDED,
/**
* Command from main thread: the content area of the window has changed,
* such as from the soft input window being shown or hidden. You can
* find the new content rect in android_app::contentRect.
*/
APP_CMD_CONTENT_RECT_CHANGED,
/**
* Command from main thread: the app's activity window has gained
* input focus.
*/
APP_CMD_GAINED_FOCUS,
/**
* Command from main thread: the app's activity window has lost
* input focus.
*/
APP_CMD_LOST_FOCUS,
/**
* Command from main thread: the current device configuration has changed.
*/
APP_CMD_CONFIG_CHANGED,
/**
* Command from main thread: the system is running low on memory.
* Try to reduce your memory use.
*/
APP_CMD_LOW_MEMORY,
/**
* Command from main thread: the app's activity has been started.
*/
APP_CMD_START,
/**
* Command from main thread: the app's activity has been resumed.
*/
APP_CMD_RESUME,
/**
* Command from main thread: the app should generate a new saved state
* for itself, to restore from later if needed. If you have saved state,
* allocate it with malloc and place it in android_app.savedState with
* the size in android_app.savedStateSize. The will be freed for you
* later.
*/
APP_CMD_SAVE_STATE,
/**
* Command from main thread: the app's activity has been paused.
*/
APP_CMD_PAUSE,
/**
* Command from main thread: the app's activity has been stopped.
*/
APP_CMD_STOP,
/**
* Command from main thread: the app's activity is being destroyed,
* and waiting for the app thread to clean up and exit before proceeding.
*/
APP_CMD_DESTROY,
/**
* Command from main thread: the app's insets have changed.
*/
APP_CMD_WINDOW_INSETS_CHANGED,
};
/**
* Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
* app command message.
*/
int8_t android_app_read_cmd(struct android_app* android_app);
/**
* Call with the command returned by android_app_read_cmd() to do the
* initial pre-processing of the given command. You can perform your own
* actions for the command after calling this function.
*/
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
/**
* Call with the command returned by android_app_read_cmd() to do the
* final post-processing of the given command. You must have done your own
* actions for the command before calling this function.
*/
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
/**
* 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);
/**
* Clear the array of motion events that were waiting to be handled, and release
* each of them.
*
* This method should be called after you have processed the motion events in
* your game loop. You should handle events at each iteration of your game loop.
*/
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer);
/**
* Clear the array of key events that were waiting to be handled, and release
* each of them.
*
* This method should be called after you have processed the key up events in
* your game loop. You should handle events at each iteration of your game loop.
*/
void android_app_clear_key_events(struct android_input_buffer* inputBuffer);
/**
* This is a springboard into the Rust glue layer that wraps calling the
* main entry for the app itself.
*/
extern void _rust_glue_entry(struct android_app* app);
/**
* Set the filter to use when processing key events.
* Any events for which the filter returns false will be ignored by
* android_native_app_glue. If filter is set to NULL, no filtering is done.
*
* 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);
/**
* Set the filter to use when processing touch and motion events.
* Any events for which the filter returns false will be ignored by
* android_native_app_glue. If filter is set to NULL, no filtering is done.
*
* Note that the default motion event filter will only allow touchscreen events
* through, in order to mimic NativeActivity's behaviour, so for controller
* events to be passed to the app, set the filter to NULL.
*/
void android_app_set_motion_event_filter(struct android_app* app,
android_motion_event_filter filter);
/**
* Determines if a looper wake up was due to new input becoming available
*/
bool android_app_input_available_wake_up(struct android_app* app);
#ifdef __cplusplus
}
#endif
/** @} */
@@ -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,377 +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.
*/
#include "game-text-input/gametextinput.h"
#include <android/log.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <memory>
#include <vector>
#define LOG_TAG "GameTextInput"
static constexpr int32_t DEFAULT_MAX_STRING_SIZE = 1 << 16;
// Cache of field ids in the Java GameTextInputState class
struct StateClassInfo {
jfieldID text;
jfieldID selectionStart;
jfieldID selectionEnd;
jfieldID composingRegionStart;
jfieldID composingRegionEnd;
};
// Main GameTextInput object.
struct GameTextInput {
public:
GameTextInput(JNIEnv *env, uint32_t max_string_size);
~GameTextInput();
void setState(const GameTextInputState &state);
const GameTextInputState &getState() const { return currentState_; }
void setInputConnection(jobject inputConnection);
void processEvent(jobject textInputEvent);
void showIme(uint32_t flags);
void hideIme(uint32_t flags);
void restartInput();
void setEventCallback(GameTextInputEventCallback callback, void *context);
jobject stateToJava(const GameTextInputState &state) const;
void stateFromJava(jobject textInputEvent,
GameTextInputGetStateCallback callback,
void *context) const;
void setImeInsetsCallback(GameTextInputImeInsetsCallback callback,
void *context);
void processImeInsets(const ARect *insets);
const ARect &getImeInsets() const { return currentInsets_; }
private:
// Copy string and set other fields
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_ = {};
// An instance of gametextinput.InputConnection.
jclass inputConnectionClass_ = nullptr;
jobject inputConnection_ = nullptr;
jmethodID inputConnectionSetStateMethod_;
jmethodID setSoftKeyboardActiveMethod_;
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;
StateClassInfo stateClassInfo_ = {};
// Constant-sized buffer used to store state text.
std::vector<char> stateStringBuffer_;
};
std::unique_ptr<GameTextInput> s_gameTextInput;
extern "C" {
///////////////////////////////////////////////////////////
/// GameTextInputState C Functions
///////////////////////////////////////////////////////////
// Convert to a Java structure.
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) {
gameTextInput->stateFromJava(textInputEvent, callback, context);
}
///////////////////////////////////////////////////////////
/// GameTextInput C Functions
///////////////////////////////////////////////////////////
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 "
"calling GameTextInput_destroy");
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));
return s_gameTextInput.get();
}
void GameTextInput_destroy(GameTextInput *input) {
if (input == nullptr || s_gameTextInput.get() == nullptr) return;
s_gameTextInput.reset();
}
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_setInputConnection(GameTextInput *input,
jobject inputConnection) {
input->setInputConnection(inputConnection);
}
void GameTextInput_processEvent(GameTextInput *input, jobject textInputEvent) {
input->processEvent(textInputEvent);
}
void GameTextInput_processImeInsets(GameTextInput *input, const ARect *insets) {
input->processImeInsets(insets);
}
void GameTextInput_showIme(struct GameTextInput *input, uint32_t flags) {
input->showIme(flags);
}
void GameTextInput_hideIme(struct GameTextInput *input, uint32_t flags) {
input->hideIme(flags);
}
void GameTextInput_restartInput(struct GameTextInput *input) {
input->restartInput();
}
void GameTextInput_setEventCallback(struct GameTextInput *input,
GameTextInputEventCallback callback,
void *context) {
input->setEventCallback(callback, context);
}
void GameTextInput_setImeInsetsCallback(struct GameTextInput *input,
GameTextInputImeInsetsCallback callback,
void *context) {
input->setImeInsetsCallback(callback, context);
}
void GameTextInput_getImeInsets(const GameTextInput *input, ARect *insets) {
*insets = input->getImeInsets();
}
} // 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) {
stateJavaClass_ = (jclass)env_->NewGlobalRef(
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");
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_.composingRegionStart =
env_->GetFieldID(stateJavaClass_, "composingRegionStart", "I");
stateClassInfo_.composingRegionEnd =
env_->GetFieldID(stateJavaClass_, "composingRegionEnd", "I");
}
GameTextInput::~GameTextInput() {
if (stateJavaClass_ != NULL) {
env_->DeleteGlobalRef(stateJavaClass_);
stateJavaClass_ = NULL;
}
if (inputConnectionClass_ != NULL) {
env_->DeleteGlobalRef(inputConnectionClass_);
inputConnectionClass_ = NULL;
}
if (inputConnection_ != NULL) {
env_->DeleteGlobalRef(inputConnection_);
inputConnection_ = NULL;
}
}
void GameTextInput::setState(const GameTextInputState &state) {
if (inputConnection_ == nullptr) return;
jobject jstate = stateToJava(state);
env_->CallVoidMethod(inputConnection_, inputConnectionSetStateMethod_,
jstate);
env_->DeleteLocalRef(jstate);
setStateInner(state);
}
void GameTextInput::setStateInner(const GameTextInputState &state) {
// Check if we're setting using our own string (other parts may be
// different)
if (state.text_UTF8 == currentState_.text_UTF8) {
currentState_ = 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()));
currentState_.text_UTF8 = 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;
stateStringBuffer_[bytes_needed - 1] = 0;
}
void GameTextInput::setInputConnection(jobject inputConnection) {
if (inputConnection_ != NULL) {
env_->DeleteGlobalRef(inputConnection_);
}
inputConnection_ = env_->NewGlobalRef(inputConnection);
}
/*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_) {
eventCallback_(eventCallbackContext_, &currentState_);
}
}
void GameTextInput::showIme(uint32_t flags) {
if (inputConnection_ == nullptr) return;
env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, true,
flags);
}
void GameTextInput::setEventCallback(GameTextInputEventCallback callback,
void *context) {
eventCallback_ = callback;
eventCallbackContext_ = context;
}
void GameTextInput::setImeInsetsCallback(
GameTextInputImeInsetsCallback callback, void *context) {
insetsCallback_ = callback;
insetsCallbackContext_ = context;
}
void GameTextInput::processImeInsets(const ARect *insets) {
currentInsets_ = *insets;
if (insetsCallback_) {
insetsCallback_(insetsCallbackContext_, &currentInsets_);
}
}
void GameTextInput::hideIme(uint32_t flags) {
if (inputConnection_ == nullptr) return;
env_->CallVoidMethod(inputConnection_, setSoftKeyboardActiveMethod_, false,
flags);
}
void GameTextInput::restartInput() {
if (inputConnection_ == nullptr) return;
env_->CallVoidMethod(inputConnection_, restartInputMethod_, false);
}
jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
static jmethodID constructor = nullptr;
if (constructor == nullptr) {
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;
if (text == nullptr) {
static char empty_string[] = "";
text = empty_string;
}
// 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);
env_->DeleteLocalRef(jtext);
return jobj;
}
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);
int composingRegionStart =
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionStart);
int composingRegionEnd =
env_->GetIntField(textInputEvent, stateClassInfo_.composingRegionEnd);
GameTextInputState state{text_chars,
text_len,
{selectionStart, selectionEnd},
{composingRegionStart, composingRegionEnd}};
callback(context, &state);
env_->ReleaseStringUTFChars(text, text_chars);
env_->DeleteLocalRef(text);
}
@@ -1,305 +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 "common/gamesdk_common.h"
#include "gamecommon.h"
#ifdef __cplusplus
extern "C" {
#endif
#define GAMETEXTINPUT_MAJOR_VERSION 2
#define GAMETEXTINPUT_MINOR_VERSION 0
#define GAMETEXTINPUT_BUGFIX_VERSION 0
#define GAMETEXTINPUT_PACKED_VERSION \
ANDROID_GAMESDK_PACKED_VERSION(GAMETEXTINPUT_MAJOR_VERSION, \
GAMETEXTINPUT_MINOR_VERSION, \
GAMETEXTINPUT_BUGFIX_VERSION)
/**
* This struct holds a span within a region of text from start (inclusive) to
* end (exclusive). An empty span or cursor position is specified with
* 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);
/**
* 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.
*/
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
/** @} */
+5 -1
View File
@@ -2,6 +2,7 @@
# 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
@@ -14,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' \
@@ -36,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 -7
View File
@@ -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 {
+5 -3
View File
@@ -25,8 +25,10 @@ pub type Result<T> = std::result::Result<T, AppError>;
pub(crate) enum InternalAppError {
#[error("A JNI error")]
JniError(jni::errors::JniError),
#[error("A Java Exception was thrown via a JNI method call")]
JniException(String),
// 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")]
@@ -50,7 +52,7 @@ impl From<InternalAppError> for AppError {
fn from(value: InternalAppError) -> Self {
match value {
InternalAppError::JniError(err) => AppError::JavaError(err.to_string()),
InternalAppError::JniException(msg) => AppError::JavaError(msg),
InternalAppError::JniBadArgument(msg) => AppError::JavaError(msg),
InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
InternalAppError::InputUnavailable => AppError::InputUnavailable,
}
+5 -8
View File
@@ -12,21 +12,18 @@
#![allow(deref_nullptr)]
#![allow(dead_code)]
use jni_sys::*;
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");
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -83
View File
@@ -13,12 +13,10 @@
// The `Class` was also bound differently to `android-ndk-rs` considering how the class is defined
// by masking bits from the `Source`.
use std::convert::TryInto;
use crate::activity_impl::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
use crate::input::{
Axis, ButtonState, Class, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState,
MotionAction, MotionEventFlags, Source, ToolType,
Axis, Button, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState,
MotionAction, MotionEventFlags, Pointer, PointersIter, Source, ToolType,
};
// Note: try to keep this wrapper API compatible with the AInputEvent API if possible
@@ -29,6 +27,7 @@ pub enum InputEvent<'a> {
MotionEvent(MotionEvent<'a>),
KeyEvent(KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}
/// A motion event.
@@ -50,14 +49,7 @@ impl<'a> MotionEvent<'a> {
#[inline]
pub fn source(&self) -> Source {
let source = self.ga_event.source 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.
@@ -73,7 +65,19 @@ impl<'a> MotionEvent<'a> {
#[inline]
pub fn action(&self) -> MotionAction {
let action = self.ga_event.action as u32 & ndk_sys::AMOTION_EVENT_ACTION_MASK;
action.try_into().unwrap()
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 = self.ga_event.actionButton as u32;
action.into()
}
/// Returns the pointer index of an `Up` or `Down` event.
@@ -116,9 +120,11 @@ impl<'a> MotionEvent<'a> {
#[inline]
pub fn pointers(&self) -> PointersIter<'_> {
PointersIter {
event: self,
next_index: 0,
count: self.pointer_count(),
inner: PointersIterImpl {
event: self,
next_index: 0,
count: self.pointer_count(),
},
}
}
@@ -130,7 +136,9 @@ impl<'a> MotionEvent<'a> {
if index >= self.pointer_count() {
panic!("Pointer index {} is out of bounds", index);
}
Pointer { event: self, index }
Pointer {
inner: PointerImpl { event: self, index },
}
}
/*
@@ -251,12 +259,12 @@ impl<'a> MotionEvent<'a> {
/// A view into the data of a specific pointer in a motion event.
#[derive(Debug)]
pub struct Pointer<'a> {
pub(crate) struct PointerImpl<'a> {
event: &'a MotionEvent<'a>,
index: usize,
}
impl<'a> Pointer<'a> {
impl PointerImpl<'_> {
#[inline]
pub fn pointer_index(&self) -> usize {
self.index
@@ -271,17 +279,8 @@ impl<'a> Pointer<'a> {
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
let pointer = &self.event.ga_event.pointers[self.index];
pointer.axisValues[axis as u32 as usize]
}
#[inline]
pub fn orientation(&self) -> f32 {
self.axis_value(Axis::Orientation)
}
#[inline]
pub fn pressure(&self) -> f32 {
self.axis_value(Axis::Pressure)
let axis: u32 = axis.into();
pointer.axisValues[axis as usize]
}
#[inline]
@@ -296,64 +295,31 @@ impl<'a> Pointer<'a> {
pointer.rawY
}
#[inline]
pub fn x(&self) -> f32 {
self.axis_value(Axis::X)
}
#[inline]
pub fn y(&self) -> f32 {
self.axis_value(Axis::Y)
}
#[inline]
pub fn size(&self) -> f32 {
self.axis_value(Axis::Size)
}
#[inline]
pub fn tool_major(&self) -> f32 {
self.axis_value(Axis::ToolMajor)
}
#[inline]
pub fn tool_minor(&self) -> f32 {
self.axis_value(Axis::ToolMinor)
}
#[inline]
pub fn touch_major(&self) -> f32 {
self.axis_value(Axis::TouchMajor)
}
#[inline]
pub fn touch_minor(&self) -> f32 {
self.axis_value(Axis::TouchMinor)
}
#[inline]
pub fn tool_type(&self) -> ToolType {
let pointer = &self.event.ga_event.pointers[self.index];
let tool_type = pointer.toolType as u32;
tool_type.try_into().unwrap()
tool_type.into()
}
}
/// An iterator over the pointers in a [`MotionEvent`].
#[derive(Debug)]
pub struct PointersIter<'a> {
pub(crate) struct PointersIterImpl<'a> {
event: &'a MotionEvent<'a>,
next_index: usize,
count: usize,
}
impl<'a> Iterator for PointersIter<'a> {
impl<'a> Iterator for PointersIterImpl<'a> {
type Item = Pointer<'a>;
fn next(&mut self) -> Option<Pointer<'a>> {
if self.next_index < self.count {
let ptr = Pointer {
event: self.event,
index: self.next_index,
inner: PointerImpl {
event: self.event,
index: self.next_index,
},
};
self.next_index += 1;
Some(ptr)
@@ -368,7 +334,7 @@ impl<'a> Iterator for PointersIter<'a> {
}
}
impl<'a> ExactSizeIterator for PointersIter<'a> {
impl ExactSizeIterator for PointersIterImpl<'_> {
fn len(&self) -> usize {
self.count - self.next_index
}
@@ -701,14 +667,7 @@ impl<'a> KeyEvent<'a> {
#[inline]
pub fn source(&self) -> Source {
let source = self.ga_event.source 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.
@@ -724,13 +683,13 @@ impl<'a> KeyEvent<'a> {
#[inline]
pub fn action(&self) -> KeyAction {
let action = self.ga_event.action as u32;
action.try_into().unwrap()
action.into()
}
#[inline]
pub fn action_button(&self) -> KeyAction {
let action = self.ga_event.action as u32;
action.try_into().unwrap()
action.into()
}
/// Returns the last time the key was pressed. This is on the scale of
@@ -760,7 +719,7 @@ impl<'a> KeyEvent<'a> {
#[inline]
pub fn key_code(&self) -> Keycode {
let keycode = self.ga_event.keyCode as u32;
keycode.try_into().unwrap_or(Keycode::Unknown)
keycode.into()
}
/// Returns the number of repeats of a key.
@@ -782,7 +741,7 @@ impl<'a> KeyEvent<'a> {
}
}
impl<'a> KeyEvent<'a> {
impl KeyEvent<'_> {
/// Flags associated with this [`KeyEvent`].
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getflags)
File diff suppressed because it is too large Load Diff
+323
View File
@@ -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 = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
let len = match reader.read_line(&mut buffer) {
Ok(len) => len,
Err(e) => {
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 = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap();
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))
})
}
+686 -62
View File
@@ -1,5 +1,4 @@
use bitflags::bitflags;
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use crate::activity_impl::input::*;
use crate::InputStatus;
@@ -10,7 +9,18 @@ pub use sdk::*;
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_ANY)
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
///
/// # 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, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Source {
BluetoothStylus = 0x0000c002,
@@ -36,9 +46,36 @@ pub enum Source {
TouchNavigation = 0x00200000,
Trackball = 0x00010004,
Unknown = 0,
// We need to consider that the enum variants may be extended across
// different versions of Android (i.e. effectively at runtime) but at the
// same time we don't want it to be an API break to extend this enum in
// future releases of `android-activity` with new variants from the latest
// NDK/SDK.
//
// We can't just use `#[non_exhaustive]` because that only really helps
// when adding new variants in sync with android-activity releases.
//
// On the other hand we also can't rely on a catch-all `Unknown(u32)` that
// only really helps with unknown variants seen at runtime.
//
// What we aim for instead is to have a hidden catch-all variant that
// is considered (practically) unmatchable so code is forced to have
// a `unknown => {}` catch-all pattern match that will cover unknown variants
// either in the form of Rust variants added in future versions or
// in the form of an `__Unknown(u32)` integer that represents an unknown
// variant seen at runtime.
//
// Any `unknown => {}` pattern match can rely on `IntoPrimitive` to convert
// the `unknown` variant to the integer that comes from the Android SDK
// in case that values needs to be passed on, even without knowing its
// semantic meaning at compile time.
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
// ndk_sys doesn't currently have the `TRACKBALL` flag so we define our
// own internal class constants for now
bitflags! {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct SourceFlags: u32 {
@@ -53,37 +90,31 @@ bitflags! {
}
}
/// An enum representing the class of a [`MotionEvent`] or [`KeyEvent`] source
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_CLASS_MASK)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
None,
Button,
Pointer,
Trackball,
Position,
Joystick,
}
impl From<u32> for Class {
fn from(source: u32) -> Self {
let class = SourceFlags::from_bits_truncate(source);
match class {
SourceFlags::BUTTON => Class::Button,
SourceFlags::POINTER => Class::Pointer,
SourceFlags::TRACKBALL => Class::Trackball,
SourceFlags::POSITION => Class::Position,
SourceFlags::JOYSTICK => Class::Joystick,
_ => Class::None,
}
impl Source {
#[inline]
pub fn is_button_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::BUTTON)
}
}
impl From<Source> for Class {
fn from(source: Source) -> Self {
let source: u32 = source.into();
source.into()
#[inline]
pub fn is_pointer_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::POINTER)
}
#[inline]
pub fn is_trackball_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::TRACKBALL)
}
#[inline]
pub fn is_position_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::POSITION)
}
#[inline]
pub fn is_joystick_class(self) -> bool {
let class = SourceFlags::from_bits_truncate(self.into());
class.contains(SourceFlags::JOYSTICK)
}
}
@@ -174,7 +205,18 @@ impl From<ndk::event::MetaState> for MetaState {
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-29)
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
///
/// # 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(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum MotionAction {
Down = ndk_sys::AMOTION_EVENT_ACTION_DOWN,
@@ -190,19 +232,58 @@ pub enum MotionAction {
HoverExit = ndk_sys::AMOTION_EVENT_ACTION_HOVER_EXIT,
ButtonPress = ndk_sys::AMOTION_EVENT_ACTION_BUTTON_PRESS,
ButtonRelease = ndk_sys::AMOTION_EVENT_ACTION_BUTTON_RELEASE,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
impl From<ndk::event::MotionAction> for MotionAction {
fn from(value: ndk::event::MotionAction) -> Self {
let inner: u32 = value.into();
inner.try_into().unwrap() // TODO: use into() when we bump the MSRV to 1.68
}
/// Identifies buttons that are associated with motion events.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-47)
///
/// # 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(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Button {
Back = ndk_sys::AMOTION_EVENT_BUTTON_BACK,
Forward = ndk_sys::AMOTION_EVENT_BUTTON_FORWARD,
Primary = ndk_sys::AMOTION_EVENT_BUTTON_PRIMARY,
Secondary = ndk_sys::AMOTION_EVENT_BUTTON_SECONDARY,
StylusPrimary = ndk_sys::AMOTION_EVENT_BUTTON_STYLUS_PRIMARY,
StylusSecondary = ndk_sys::AMOTION_EVENT_BUTTON_STYLUS_SECONDARY,
Tertiary = ndk_sys::AMOTION_EVENT_BUTTON_TERTIARY,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// An axis of a motion event.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-32)
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
///
/// # 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(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Axis {
X = ndk_sys::AMOTION_EVENT_AXIS_X,
@@ -250,19 +331,27 @@ pub enum Axis {
Generic14 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_14,
Generic15 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_15,
Generic16 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_16,
}
impl From<ndk::event::Axis> for Axis {
fn from(value: ndk::event::Axis) -> Self {
let inner: u32 = value.into();
inner.try_into().unwrap() // TODO: replace with into() when we can bump MSRV to 1.68!
}
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// The tool type of a pointer.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-48)
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
///
/// # 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.
///
/// Implements `Into<u32>` and `From<u32>` for converting to/from Android SDK
/// integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum ToolType {
/// Unknown tool type.
@@ -284,6 +373,10 @@ pub enum ToolType {
/// The tool is a palm and should be rejected
Palm = ndk_sys::AMOTION_EVENT_TOOL_TYPE_PALM,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// A bitfield representing the state of buttons during a motion event.
@@ -382,25 +475,44 @@ impl From<ndk::event::MotionEventFlags> for MotionEventFlags {
/// Key actions.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-27)
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
///
/// # 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.
///
/// Implements `Into<u32>` and `From<u32>` for converting to/from Android SDK
/// integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum KeyAction {
Down = ndk_sys::AKEY_EVENT_ACTION_DOWN,
Up = ndk_sys::AKEY_EVENT_ACTION_UP,
Multiple = ndk_sys::AKEY_EVENT_ACTION_MULTIPLE,
}
impl From<ndk::event::KeyAction> for KeyAction {
fn from(value: ndk::event::KeyAction) -> Self {
let inner: u32 = value.into();
inner.try_into().unwrap() // TODO: replace with into() when we can bump MSRV to 1.68!
}
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Key codes.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-39)
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
///
/// # 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.
///
/// Implements `Into<u32>` and `From<u32>` for converting to/from Android SDK
/// integer values.
///
#[derive(Copy, Clone, Debug, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum Keycode {
Unknown = ndk_sys::AKEYCODE_UNKNOWN,
@@ -692,13 +804,10 @@ pub enum Keycode {
ThumbsUp = ndk_sys::AKEYCODE_THUMBS_UP,
ThumbsDown = ndk_sys::AKEYCODE_THUMBS_DOWN,
ProfileSwitch = ndk_sys::AKEYCODE_PROFILE_SWITCH,
}
impl From<ndk::event::Keycode> for Keycode {
fn from(value: ndk::event::Keycode) -> Self {
let inner: u32 = value.into();
inner.try_into().unwrap() // TODO: replace with into() when we can bump MSRV to 1.68!
}
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Flags associated with [`KeyEvent`].
@@ -798,12 +907,421 @@ pub struct TextInputState {
pub compose_region: Option<TextSpan>,
}
impl Default for TextInputState {
fn default() -> Self {
Self {
text: String::new(),
selection: TextSpan { start: 0, end: 0 },
compose_region: None,
}
}
}
// Represents the action button on a soft keyboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(i32)]
pub enum TextInputAction {
/// Let receiver decide what logical action to perform
Unspecified = 0,
/// No action - receiver could instead interpret as an "enter" key that inserts a newline character
None = 1,
/// Navigate to the input location (such as a URL)
Go = 2,
/// Search based on the input text
Search = 3,
/// Send the input to the target
Send = 4,
/// Move to the next input field
Next = 5,
/// Indicate that input is done
Done = 6,
/// Move to the previous input field
Previous = 7,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(i32),
}
bitflags! {
/// Flags for [`AndroidApp::set_ime_editor_info`]
/// as per the [android.view.inputmethod.EditorInfo Java API](https://developer.android.com/reference/android/view/inputmethod/EditorInfo)
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ImeOptions: u32 {
/// The mask of bits that configure alternative actions for the "enter" key. This helps the
/// IME provide clear feedback for what the key will do and provide alternative mechanisms
/// for taking the same action.
const IME_MASK_ACTION = 0x000000ff;
/// Indicates that ascii input is a priority (such as for entering an account ID)
const IME_FLAG_FORCE_ASCII = 0x80000000;
/// Indicates that it's possible to navigate focus forwards to something.
///
/// This is similar to using `IME_ACTION_NEXT` except it allows for multi-line input with
/// an enter key in addition to forward navigation for focus.
///
/// This may not be supported by all IMEs (especially on small screens)
const IME_FLAG_NAVIGATE_NEXT = 0x08000000;
/// Similar to `IME_FLAG_NAVIGATE_NEXT`, except it indicates that it's possible to navigate
/// focus backwards to something.
const IME_FLAG_NAVIGATE_PREVIOUS = 0x04000000;
/// This requests that the IME should not show any accessory actions next to the extracted
/// text UI, when it is in fullscreen mode.
///
/// The implication is that you think it's more important to prioritize having room for
/// previewing more text, instead of showing accessory actions.
///
/// Note: In some cases this can make the action unavailable.
const IME_FLAG_NO_ACCESSORY_ACTION = 0x20000000;
/// 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.
const IME_FLAG_NO_ENTER_ACTION = 0x40000000;
/// Don't show any "extracted-text UI" as part of the on-screen IME.
///
/// Some keyboards may show an additional text box above the keyboard for previewing what
/// you type (referred to as the extracted text UI) and it can sometimes be quite large.
///
/// The exact semantics of this flag can be unclear sometimes and the UI that becomes
/// visible may not respond to input as you would expect.
///
/// This flag may be deprecated in the future and it's recommend to use
/// `IME_FLAG_NO_FULLSCREEN` instead, to avoid having the extracted text UI appear to cover
/// the full screen.
const IMG_FLAG_NO_EXTRACT_UI = 0x10000000;
/// Request that the IME should avoid ever entering a fullscreen mode and should always
/// leave some room for the application UI.
///
/// Note: It's not guaranteed that an IME will honor this state
const IME_FLAG_NO_FULLSCREEN = 0x02000000;
/// Request that the IME should not update personalized data, such as typing history.
///
/// Note: It's not guaranteed that an IME will honor this state
const IME_FLAG_NO_PERSONALIZED_LEARNING = 0x01000000;
/// Generic unspecified type for ImeOptions
const IME_NULL = 0;
}
}
impl ImeOptions {
/// Specify what action the IME's "enter" key should perform.
///
/// This helps the IME provide clear feedback for what the key will do and provide alternative
/// mechanisms for taking the same action.
pub fn set_action(&mut self, action: TextInputAction) {
let action: i32 = action.into();
let action = action as u32;
*self = Self::from_bits_truncate(
(self.bits() & !Self::IME_MASK_ACTION.bits()) | (action & Self::IME_MASK_ACTION.bits()),
);
}
/// Get the current action of the IME's "enter" key.
pub fn action(&self) -> TextInputAction {
let action_bits = self.bits() & Self::IME_MASK_ACTION.bits();
TextInputAction::from(action_bits as i32)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(u32)]
pub enum InputTypeClass {
/// 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.
Null = 0,
/// Class for normal text.
///
/// This class supports the following flags (only one of which should be set):
/// - TYPE_TEXT_FLAG_CAP_CHARACTERS
/// - TYPE_TEXT_FLAG_CAP_WORDS
/// - TYPE_TEXT_FLAG_CAP_SENTENCES.
///
/// It also supports the following variations:
/// - TYPE_TEXT_VARIATION_NORMAL
/// - TYPE_TEXT_VARIATION_URI
///
/// *If you do not recognize the variation, normal should be assumed.*
Text = 1,
/// Class for numeric text.
///
/// This class supports the following flags:
/// - `TYPE_NUMBER_FLAG_SIGNED`
/// - `TYPE_NUMBER_FLAG_DECIMAL`
///
/// It also supports the following variations:
/// - `TYPE_NUMBER_VARIATION_NORMAL`
/// - `TYPE_NUMBER_VARIATION_PASSWORD`
///
/// *IME authors: If you do not recognize the variation, normal should be assumed.*
Number = 2,
/// Class for a phone number.
///
/// This class currently supports no variations or flags.
Phone = 3,
/// Class for dates and times.
///
/// It supports the following variations:
/// - TYPE_DATETIME_VARIATION_NORMAL
/// - TYPE_DATETIME_VARIATION_DATE
/// - TYPE_DATETIME_VARIATION_TIME
DateTime = 4,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
bitflags! {
/// Flags specifying the content type of text being input.
///
/// Corresponds to the Android SDK [InputType](https://developer.android.com/reference/android/text/InputType) API
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct InputType: u32 {
/// Mask of bits that determine the overall class of text being given. Currently
/// supported classes are: TYPE_CLASS_TEXT, TYPE_CLASS_NUMBER, TYPE_CLASS_PHONE,
/// TYPE_CLASS_DATETIME. IME authors: If the class is not one you understand, assume
/// TYPE_CLASS_TEXT with NO variation or flags.
const TYPE_MASK_CLASS = 0x0000000f;
/// Mask of bits that determine the variation of the base content class.
const TYPE_MASK_VARIATION = 0x00000ff0;
/// Mask of bits that provide addition bit flags of options.
const 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.
const TYPE_NULL = 0;
/// Class for normal text. This class supports the following flags (only one of which
/// should be set): TYPE_TEXT_FLAG_CAP_CHARACTERS, TYPE_TEXT_FLAG_CAP_WORDS, and.
/// TYPE_TEXT_FLAG_CAP_SENTENCES. It also supports the following variations:
/// TYPE_TEXT_VARIATION_NORMAL, and TYPE_TEXT_VARIATION_URI. If you do not recognize the
/// variation, normal should be assumed.
const TYPE_CLASS_TEXT = 1;
/// Flag for TYPE_CLASS_TEXT: capitalize all characters. Overrides
/// #TYPE_TEXT_FLAG_CAP_WORDS} and #TYPE_TEXT_FLAG_CAP_SENTENCES}. This value is
/// explicitly defined to be the same as TextUtils#CAP_MODE_CHARACTERS}. Of
/// course, this only affects languages where there are upper-case and lower-case
/// letters.
const TYPE_TEXT_FLAG_CAP_CHARACTERS = 0x00001000;
/// Flag for TYPE_CLASS_TEXT: capitalize the first character of every word.
/// Overrides TYPE_TEXT_FLAG_CAP_SENTENCES. This value is explicitly defined
/// to be the same as TextUtils#CAP_MODE_WORDS. Of course, this only affects
/// languages where there are upper-case and lower-case letters.
const TYPE_TEXT_FLAG_CAP_WORDS = 0x00002000;
/// Flag for TYPE_CLASS_TEXT: capitalize the first character of each sentence. This value
/// is explicitly defined to be the same as 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.
const TYPE_TEXT_FLAG_CAP_SENTENCES = 0x00004000;
/// Flag for 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 TYPE_TEXT_FLAG_AUTO_COMPLETE and TYPE_TEXT_FLAG_NO_SUGGESTIONS:
/// 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.
const TYPE_TEXT_FLAG_AUTO_CORRECT = 0x00008000;
/// Flag for 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
/// android.view.inputmethod.InputMethodSession#displayCompletions
/// InputMethodSession.displayCompletions()} as a result of the editor calling
/// android.view.inputmethod.InputMethodManager#displayCompletions
/// InputMethodManager.displayCompletions()}. Note the contrast with
/// TYPE_TEXT_FLAG_AUTO_CORRECT and TYPE_TEXT_FLAG_NO_SUGGESTIONS:
/// 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.
const TYPE_TEXT_FLAG_AUTO_COMPLETE = 0x00010000;
/// Flag for 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.
const TYPE_TEXT_FLAG_MULTI_LINE = 0x00020000;
/// Flag for 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.
const TYPE_TEXT_FLAG_IME_MULTI_LINE = 0x00040000;
/// Flag for 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 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 TYPE_TEXT_FLAG_AUTO_CORRECT instead if you just do not want the IME to
/// correct typos. Note the contrast with TYPE_TEXT_FLAG_AUTO_CORRECT and
/// TYPE_TEXT_FLAG_AUTO_COMPLETE: 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.
const TYPE_TEXT_FLAG_NO_SUGGESTIONS = 0x00080000;
/// Flag for 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 Builder#setTextConversionSuggestions(List)} and the
/// 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 InputConnection#setComposingText(CharSequence, int,
/// TextAttribute)}, InputConnection#setComposingRegion(int, int, TextAttribute)}, and
/// InputConnection#commitText(CharSequence, int, TextAttribute)}.
const TYPE_TEXT_FLAG_ENABLE_TEXT_CONVERSION_SUGGESTIONS = 0x00100000;
/// Flag for TYPE_CLASS_TEXT: Let the IME know that conversion candidate selection
/// information is requested by the application. Text conversion suggestion is for the
/// transliteration languages, which have the notions of pronunciation and target
/// characters. When the user actively selects a candidate from the conversion suggestions,
/// notifying when candidate selection is occurring helps assistive technologies generate
/// more effective feedback. When this flag is set, and there is an active selected
/// suggestion, the IME should set that a conversion suggestion is selected when
/// initializing the TextAttribute. To receive this information, the application should
/// implement InputConnection.setComposingText(CharSequence, int, TextAttribute),
/// InputConnection.setComposingRegion(int, int, TextAttribute), and
/// InputConnection.commitText(CharSequence, int, TextAttribute)
const TYPE_TEXT_FLAG_ENABLE_TEXT_SUGGESTION_SELECTED = 0x00200000;
/// Default variation of TYPE_CLASS_TEXT: plain old normal text.
const TYPE_TEXT_VARIATION_NORMAL = 0;
/// Variation of TYPE_CLASS_TEXT: entering a URI.
const TYPE_TEXT_VARIATION_URI = 0x00000010;
/// Variation of TYPE_CLASS_TEXT: entering an e-mail address.
const TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 0x00000020;
/// Variation of TYPE_CLASS_TEXT: entering the subject line of an e-mail.
const TYPE_TEXT_VARIATION_EMAIL_SUBJECT = 0x00000030;
/// Variation of TYPE_CLASS_TEXT: entering a short, possibly informal message such as an instant message or a text message.
const TYPE_TEXT_VARIATION_SHORT_MESSAGE = 64;
/// Variation of TYPE_CLASS_TEXT: entering the content of a long, possibly formal message such as the body of an e-mail.
const TYPE_TEXT_VARIATION_LONG_MESSAGE = 0x00000050;
/// Variation of TYPE_CLASS_TEXT: entering the name of a person.
const TYPE_TEXT_VARIATION_PERSON_NAME = 0x00000060;
/// Variation of TYPE_CLASS_TEXT: entering a postal mailing address.
const TYPE_TEXT_VARIATION_POSTAL_ADDRESS = 0x00000070;
/// Variation of TYPE_CLASS_TEXT: entering a password.
const TYPE_TEXT_VARIATION_PASSWORD = 0x00000080;
/// Variation of TYPE_CLASS_TEXT: entering a password, which should be visible to the user.
const TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 0x00000090;
/// Variation of TYPE_CLASS_TEXT: entering text inside of a web form.
const TYPE_TEXT_VARIATION_WEB_EDIT_TEXT = 0x000000a0;
/// Variation of TYPE_CLASS_TEXT: entering text to filter contents of a list etc.
const TYPE_TEXT_VARIATION_FILTER = 0x000000b0;
/// Variation of 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.
const TYPE_TEXT_VARIATION_PHONETIC = 0x000000c0;
/// Variation of TYPE_CLASS_TEXT: entering e-mail address inside of a web form. This
/// was added in 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 #TYPE_TEXT_VARIATION_EMAIL_ADDRESS} when passed through
/// android.view.inputmethod.EditorInfo#makeCompatible(int)
/// EditorInfo.makeCompatible(int)}.
const TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS = 0x000000d0;
/// Variation of TYPE_CLASS_TEXT: entering password inside of a web form. This was
/// added in 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 #TYPE_TEXT_VARIATION_PASSWORD} when passed through
/// android.view.inputmethod.EditorInfo#makeCompatible(int)
/// EditorInfo.makeCompatible(int)}.
const TYPE_TEXT_VARIATION_WEB_PASSWORD = 0x000000e0;
/// Class for numeric text. This class supports the following flags:
/// #TYPE_NUMBER_FLAG_SIGNED} and #TYPE_NUMBER_FLAG_DECIMAL}. It also supports the
/// following variations: #TYPE_NUMBER_VARIATION_NORMAL} and
/// #TYPE_NUMBER_VARIATION_PASSWORD}. <p>IME authors: If you do not recognize the
/// variation, normal should be assumed.</p>
const TYPE_CLASS_NUMBER = 2;
/// Flag of TYPE_CLASS_NUMBER: the number is signed, allowing a positive or negative
/// sign at the start.
const TYPE_NUMBER_FLAG_SIGNED = 0x00001000;
/// Flag of TYPE_CLASS_NUMBER: the number is decimal, allowing a decimal point to
/// provide fractional values.
const TYPE_NUMBER_FLAG_DECIMAL = 0x00002000;
/// Default variation of TYPE_CLASS_NUMBER: plain normal numeric text. This was added
/// in 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 android.view.inputmethod.EditorInfo#makeCompatible(int)
/// EditorInfo.makeCompatible(int)}.
const TYPE_NUMBER_VARIATION_NORMAL = 0;
/// Variation of TYPE_CLASS_NUMBER: entering a numeric password. This was added in
/// 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 android.view.inputmethod.EditorInfo#makeCompatible(int)
/// EditorInfo.makeCompatible(int)}.
const TYPE_NUMBER_VARIATION_PASSWORD = 0x00000010;
/// Class for a phone number. This class currently supports no variations or flags.
const TYPE_CLASS_PHONE = 3;
/// Class for dates and times. It supports the following variations:
/// #TYPE_DATETIME_VARIATION_NORMAL} #TYPE_DATETIME_VARIATION_DATE}, and
/// #TYPE_DATETIME_VARIATION_TIME}.
const TYPE_CLASS_DATETIME = 4;
/// Default variation of #TYPE_CLASS_DATETIME}: allows entering both a date and time.
const TYPE_DATETIME_VARIATION_NORMAL = 0;
/// Default variation of #TYPE_CLASS_DATETIME}: allows entering only a date.
const TYPE_DATETIME_VARIATION_DATE = 16;
/// Default variation of #TYPE_CLASS_DATETIME}: allows entering only a time.
const TYPE_DATETIME_VARIATION_TIME = 32;
}
}
impl InputType {
/// Extract just the class of the input type.
pub fn class(&self) -> InputTypeClass {
let class = self.bits() & InputType::TYPE_MASK_CLASS.bits();
InputTypeClass::from(class)
}
}
/// An exclusive, lending iterator for input events
pub struct InputIterator<'a> {
pub(crate) inner: crate::activity_impl::InputIteratorInner<'a>,
}
impl<'a> InputIterator<'a> {
impl InputIterator<'_> {
/// Reads and handles the next input event by passing it to the given `callback`
///
/// `callback` should return [`InputStatus::Unhandled`] for any input events that aren't directly
@@ -816,3 +1334,109 @@ impl<'a> InputIterator<'a> {
self.inner.next(callback)
}
}
/// A view into the data of a specific pointer in a motion event.
#[derive(Debug)]
pub struct Pointer<'a> {
pub(crate) inner: PointerImpl<'a>,
}
impl Pointer<'_> {
#[inline]
pub fn pointer_index(&self) -> usize {
self.inner.pointer_index()
}
#[inline]
pub fn pointer_id(&self) -> i32 {
self.inner.pointer_id()
}
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
self.inner.axis_value(axis)
}
#[inline]
pub fn orientation(&self) -> f32 {
self.axis_value(Axis::Orientation)
}
#[inline]
pub fn pressure(&self) -> f32 {
self.axis_value(Axis::Pressure)
}
#[inline]
pub fn raw_x(&self) -> f32 {
self.inner.raw_x()
}
#[inline]
pub fn raw_y(&self) -> f32 {
self.inner.raw_y()
}
#[inline]
pub fn x(&self) -> f32 {
self.axis_value(Axis::X)
}
#[inline]
pub fn y(&self) -> f32 {
self.axis_value(Axis::Y)
}
#[inline]
pub fn size(&self) -> f32 {
self.axis_value(Axis::Size)
}
#[inline]
pub fn tool_major(&self) -> f32 {
self.axis_value(Axis::ToolMajor)
}
#[inline]
pub fn tool_minor(&self) -> f32 {
self.axis_value(Axis::ToolMinor)
}
#[inline]
pub fn touch_major(&self) -> f32 {
self.axis_value(Axis::TouchMajor)
}
#[inline]
pub fn touch_minor(&self) -> f32 {
self.axis_value(Axis::TouchMinor)
}
#[inline]
pub fn tool_type(&self) -> ToolType {
self.inner.tool_type()
}
}
/// An iterator over the pointers in a [`MotionEvent`].
#[derive(Debug)]
pub struct PointersIter<'a> {
pub(crate) inner: PointersIterImpl<'a>,
}
impl<'a> Iterator for PointersIter<'a> {
type Item = Pointer<'a>;
fn next(&mut self) -> Option<Pointer<'a>> {
self.inner.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl ExactSizeIterator for PointersIter<'_> {
fn len(&self) -> usize {
self.inner.len()
}
}
+160 -240
View File
@@ -1,23 +1,27 @@
use std::sync::Arc;
use jni::sys::jint;
use jni::{objects::Global, JavaVM};
use jni::{
objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue},
signature::{Primitive, ReturnType},
JNIEnv,
};
use jni_sys::jint;
use crate::error::{AppError, InternalAppError, InternalResult};
use crate::input::{Keycode, MetaState};
use crate::jni_utils;
use crate::{
input::{Keycode, MetaState},
jni_utils::CloneJavaVM,
};
use crate::{
error::{AppError, InternalAppError},
jni_utils,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// 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.
///
@@ -50,33 +54,9 @@ pub enum KeyboardType {
/// A special function keyboard consists only of non-printing keys such as HOME and POWER that are not actually used for typing.
SpecialFunction,
/// An unknown type of keyboard
Unknown(i32),
}
impl From<i32> for KeyboardType {
fn from(value: i32) -> Self {
match value {
1 => KeyboardType::Numeric,
2 => KeyboardType::Predictive,
3 => KeyboardType::Alpha,
4 => KeyboardType::Full,
5 => KeyboardType::SpecialFunction,
unknown => KeyboardType::Unknown(unknown),
}
}
}
impl From<KeyboardType> for i32 {
fn from(value: KeyboardType) -> i32 {
match value {
KeyboardType::Numeric => 1,
KeyboardType::Predictive => 2,
KeyboardType::Alpha => 3,
KeyboardType::Full => 4,
KeyboardType::SpecialFunction => 5,
KeyboardType::Unknown(unknown) => unknown,
}
}
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Either represents, a unicode character or combining accent from a
@@ -88,157 +68,77 @@ pub enum KeyMapChar {
CombiningAccent(char),
}
// I've also tried to think here about how to we could potentially automatically
// generate a binding struct like `KeyCharacterMapBinding` with a procmacro and
// so have intentionally limited the `Binding` being a very thin, un-opinionated
// wrapper based on basic JNI types.
/// Lower-level JNI binding for `KeyCharacterMap` class only holds 'static state
/// and can be shared with an `Arc` ref count.
///
/// The separation here also neatly helps us separate `InternalAppError` from
/// `AppError` for mapping JNI errors without exposing any `jni-rs` types in the
/// public API.
#[derive(Debug)]
pub(crate) struct KeyCharacterMapBinding {
//vm: JavaVM,
klass: GlobalRef,
get_method_id: JMethodID,
get_dead_char_method_id: JStaticMethodID,
get_keyboard_type_method_id: JMethodID,
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 KeyCharacterMapBinding {
pub(crate) fn new(env: &mut JNIEnv) -> Result<Self, InternalAppError> {
let binding = env.with_local_frame::<_, _, InternalAppError>(10, |env| {
let klass = env.find_class("android/view/KeyCharacterMap")?; // Creates a local ref
Ok(Self {
get_method_id: env.get_method_id(&klass, "get", "(II)I")?,
get_dead_char_method_id: env.get_static_method_id(
&klass,
"getDeadChar",
"(II)I",
)?,
get_keyboard_type_method_id: env.get_method_id(&klass, "getKeyboardType", "()I")?,
klass: env.new_global_ref(&klass)?,
})
})?;
Ok(binding)
}
pub fn get<'local>(
impl AKeyCharacterMap<'_> {
pub(crate) fn get<'local>(
&self,
env: &'local mut JNIEnv,
key_map: impl AsRef<JObject<'local>>,
env: &'local mut jni::Env,
key_code: jint,
meta_state: jint,
) -> Result<jint, InternalAppError> {
let key_map = key_map.as_ref();
// Safety:
// - we know our global `key_map` reference is non-null and valid.
// - we know `get_method_id` remains valid
// - we know that the signature of KeyCharacterMap::get is `(int, int) -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
let unicode = unsafe {
env.call_method_unchecked(
key_map,
self.get_method_id,
ReturnType::Primitive(Primitive::Int),
&[
JValue::Int(key_code).as_jni(),
JValue::Int(meta_state).as_jni(),
],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
Ok(unicode.i().unwrap())
self._get(env, key_code, meta_state)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
}
pub fn get_dead_char(
&self,
env: &mut JNIEnv,
pub(crate) fn get_dead_char(
env: &mut jni::Env,
accent_char: jint,
base_char: jint,
) -> Result<jint, InternalAppError> {
// Safety:
// - we know `get_dead_char_method_id` remains valid
// - we know that KeyCharacterMap::getDeadKey is a static method
// - we know that the signature of KeyCharacterMap::getDeadKey is `(int, int) -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
// Urgh, it's pretty terrible that there's no ergonomic/safe way to get a JClass reference from a GlobalRef
// Safety: we don't do anything that would try to delete the JClass as if it were a real local reference
let klass = unsafe { JClass::from_raw(self.klass.as_obj().as_raw()) };
let unicode = unsafe {
env.call_static_method_unchecked(
&klass,
self.get_dead_char_method_id,
ReturnType::Primitive(Primitive::Int),
&[
JValue::Int(accent_char).as_jni(),
JValue::Int(base_char).as_jni(),
],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
Ok(unicode.i().unwrap())
Self::_get_dead_char(env, accent_char, base_char)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
}
pub fn get_keyboard_type<'local>(
pub(crate) fn get_keyboard_type<'local>(
&self,
env: &'local mut JNIEnv,
key_map: impl AsRef<JObject<'local>>,
env: &'local mut jni::Env,
) -> Result<jint, InternalAppError> {
let key_map = key_map.as_ref();
self._get_keyboard_type(env)
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))
}
}
// Safety:
// - we know our global `key_map` reference is non-null and valid.
// - we know `get_keyboard_type_method_id` remains valid
// - we know that the signature of KeyCharacterMap::getKeyboardType is `() -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
Ok(unsafe {
env.call_method_unchecked(
key_map,
self.get_keyboard_type_method_id,
ReturnType::Primitive(Primitive::Int),
&[],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?
.i()
.unwrap())
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(Clone, Debug)]
#[derive(Debug)]
pub struct KeyCharacterMap {
jvm: CloneJavaVM,
binding: Arc<KeyCharacterMapBinding>,
key_map: GlobalRef,
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: CloneJavaVM,
binding: Arc<KeyCharacterMapBinding>,
key_map: GlobalRef,
) -> Self {
Self {
jvm,
binding,
key_map,
}
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.
@@ -255,79 +155,70 @@ impl KeyCharacterMap {
/// 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 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 meta_state = meta_state as jni::sys::jint;
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let 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.into();
err
})?;
let unicode = self
.binding
.get(&mut env, self.key_map.as_obj(), key_code, meta_state)?;
let unicode = unicode as u32;
const COMBINING_ACCENT: u32 = 0x80000000;
const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
if unicode == 0 {
Ok(KeyMapChar::None)
} else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
let accent = unicode & COMBINING_ACCENT_MASK;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::CombiningAccent(unsafe {
char::from_u32_unchecked(accent)
}))
} else {
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::Unicode(unsafe {
char::from_u32_unchecked(unicode)
}))
}
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 '^'.
/// 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.
/// 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 accent_char = accent_char as jni::sys::jint;
let base_char = base_char as jni::sys::jint;
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let 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.into();
err
})?;
let unicode = self
.binding
.get_dead_char(&mut env, accent_char, base_char)?;
let unicode = unicode as u32;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(if unicode == 0 {
None
} else {
Some(unsafe { char::from_u32_unchecked(unicode) })
err.into()
})
}
@@ -341,18 +232,47 @@ impl KeyCharacterMap {
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn get_keyboard_type(&self) -> Result<KeyboardType, AppError> {
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let 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.into();
err
})?;
let keyboard_type = self
.binding
.get_keyboard_type(&mut env, self.key_map.as_obj())?;
Ok(keyboard_type.into())
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))
})
}
+6 -129
View File
@@ -5,47 +5,7 @@
//!
//! These utilities help us check + clear exceptions and map them into Rust Errors.
use std::{ops::Deref, sync::Arc};
use jni::{
objects::{JObject, JString},
JavaVM,
};
use crate::{
error::{InternalAppError, InternalResult},
input::{KeyCharacterMap, KeyCharacterMapBinding},
};
// TODO: JavaVM should implement Clone
#[derive(Debug)]
pub(crate) struct CloneJavaVM {
pub jvm: JavaVM,
}
impl Clone for CloneJavaVM {
fn clone(&self) -> Self {
Self {
jvm: unsafe { JavaVM::from_raw(self.jvm.get_java_vm_pointer()).unwrap() },
}
}
}
impl CloneJavaVM {
pub unsafe fn from_raw(jvm: *mut jni_sys::JavaVM) -> InternalResult<Self> {
Ok(Self {
jvm: JavaVM::from_raw(jvm)?,
})
}
}
unsafe impl Send for CloneJavaVM {}
unsafe impl Sync for CloneJavaVM {}
impl Deref for CloneJavaVM {
type Target = JavaVM;
fn deref(&self) -> &Self::Target {
&self.jvm
}
}
use 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`
@@ -55,97 +15,14 @@ impl Deref for CloneJavaVM {
///
/// This will also clear the exception
pub(crate) fn clear_and_map_exception_to_err(
env: &mut jni::JNIEnv<'_>,
env: &mut jni::Env<'_>,
err: jni::errors::Error,
) -> InternalAppError {
if matches!(err, jni::errors::Error::JavaException) {
let result = env.with_local_frame::<_, _, InternalAppError>(5, |env| {
let e = env.exception_occurred()?;
assert!(!e.is_null()); // should only be called after receiving a JavaException Result
env.exception_clear()?;
let class = env.get_object_class(&e)?;
//let get_stack_trace_method = env.get_method_id(&class, "getStackTrace", "()[Ljava/lang/StackTraceElement;")?;
let get_message_method =
env.get_method_id(&class, "getMessage", "()Ljava/lang/String;")?;
let msg = unsafe {
env.call_method_unchecked(
&e,
get_message_method,
jni::signature::ReturnType::Object,
&[],
)?
.l()
.unwrap()
};
let msg = unsafe { JString::from_raw(JObject::into_raw(msg)) };
let msg = env.get_string(&msg)?;
let msg: String = msg.into();
// TODO: get Java backtrace:
/*
if let JValue::Object(elements) = env.call_method_unchecked(&e, get_stack_trace_method, jni::signature::ReturnType::Array, &[])? {
let elements = env.auto_local(elements);
}
*/
Ok(msg)
});
match result {
Ok(msg) => InternalAppError::JniException(msg),
Err(err) => InternalAppError::JniException(format!(
"UNKNOWN (Failed to query JThrowable: {err:?})"
)),
}
env.exception_catch()
.expect_err("Spurious JavaException error with no exception to catch")
} else {
err.into()
err
}
}
pub(crate) fn device_key_character_map(
jvm: CloneJavaVM,
key_map_binding: Arc<KeyCharacterMapBinding>,
device_id: i32,
) -> InternalResult<KeyCharacterMap> {
// Don't really need to 'attach' since this should be called from the app's main thread that
// should already be attached, but the redundancy should be fine
//
// Attach 'permanently' to avoid any chance of detaching the thread from the VM
let mut env = jvm.attach_current_thread_permanently()?;
// We don't want to accidentally leak any local references while we
// aren't going to be returning from here back to the JVM, to unwind, so
// we make a local frame
let character_map = env.with_local_frame::<_, _, jni::errors::Error>(10, |env| {
let input_device_class = env.find_class("android/view/InputDevice")?; // Creates a local ref
let device = env
.call_static_method(
input_device_class,
"getDevice",
"(I)Landroid/view/InputDevice;",
&[device_id.into()],
)?
.l()?; // Creates a local ref
let character_map = env
.call_method(
&device,
"getKeyCharacterMap",
"()Landroid/view/KeyCharacterMap;",
&[],
)?
.l()?;
let character_map = env.new_global_ref(character_map)?;
Ok(character_map)
})?;
Ok(KeyCharacterMap::new(
jvm.clone(),
key_map_binding,
character_map,
))
.into()
}
+655 -86
View File
@@ -1,73 +1,342 @@
//! A glue layer for building standalone, Rust applications on Android
//!
//! This crate provides a "glue" layer for building native Rust
//! applications on Android, supporting multiple [`Activity`] base classes.
//! It's comparable to [`android_native_app_glue.c`][ndk_concepts]
//! for C/C++ applications.
//! This crate provides a "glue" layer for building native Rust applications on
//! Android, supporting multiple [`Activity`] base classes. It's comparable to
//! [`android_native_app_glue.c`][ndk_concepts] for C/C++ applications.
//!
//! Currently the crate supports two `Activity` base classes:
//! 1. [`NativeActivity`] - Built in to Android, this doesn't require compiling any Java or Kotlin code.
//! 1. [`NativeActivity`] - Built in to Android, this doesn't require compiling
//! any Java or Kotlin code.
//! 2. [`GameActivity`] - From the Android Game Development Kit, it has more
//! sophisticated input handling support than `NativeActivity`. `GameActivity`
//! is also based on the `AndroidAppCompat` class which can help with supporting
//! a wider range of devices.
//! sophisticated input handling support than `NativeActivity`.
//! `GameActivity` is also based on the `AndroidAppCompat` class which can
//! help with supporting a wider range of devices.
//!
//! Standalone applications based on this crate need to be built as `cdylib` libraries, like:
//! ```
//! Standalone applications based on this crate need to be built as `cdylib`
//! libraries, like:
//! ```toml
//! [lib]
//! crate_type=["cdylib"]
//! crate-type=["cdylib"]
//! ```
//!
//! and implement a `#[no_mangle]` `android_main` entry point like this:
//! ```rust
//! #[no_mangle]
//! fn android_main(app: AndroidApp) {
//! ## Lifecycle of an Activity
//!
//! Keep in mind that Android's application programming model is based around
//! the
//! [lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle)
//! of [`Activity`] and [`Service`] components, and not the lifecycle of the
//! application process.
//!
//! An Android application may have multiple [`Activity`] and [`Service`]
//! instances created and destroyed over its lifetime, and each of these
//! [`Activity`] and [`Service`] instances will have their own lifecycles that
//! are independent from the lifecycle of the application process.
//!
//! See the Android SDK [activity lifecycle
//! documentation](https://developer.android.com/guide/components/activities/activity-lifecycle)
//! for more details on the [`Activity`] lifecycle.
//!
//! Although native applications will typically only have a single instance of
//! [`NativeActivity`] or [`GameActivity`], it's possible for these activities
//! to be created and destroyed multiple times within the lifetime of your
//! application process.
//!
//! Although [`NativeActivity`] and [`GameActivity`] were historically designed
//! for full-screen games and based on the assumption that there would only be a
//! single instance of these activities, it is good to keep in mind that Android
//! itself makes no such assumption. It's very common for non-native Android
//! applications to be tracking multiple `Activity` instances at the same time.
//!
//! The `android-activity` crate is designed to be robust to multiple `Activity`
//! instances being created and destroyed over the lifetime of the application
//! process.
//!
//! ## Entrypoints
//!
//! There are currently two supported entrypoints for an `android-activity`
//! application:
//!
//! 1. `android_on_create` **(optional)** - This runs early, on the Java main /
//! UI thread, during `Activity.onCreate()`. It can be a good place to
//! initialize logging and JNI bindings.
//! 2. `android_main` **(required)** - This run a dedicated main loop thread for
//! handling lifecycle and input events for your `Activity`.
//!
//! **Important**: Your `android-activity` entrypoints are tied to the lifecycle
//! of your native **`Activity`** (i.e. [`NativeActivity`] or [`GameActivity`])
//! and not the lifecycle of your application process! This means that if your
//! `Activity` is destroyed and re-created (e.g. depending on how your
//! application handles configuration changes) then these entrypoints may be
//! called multiple times, for each `Activity` instance.
//!
//! #### Your AndroidManifest `configureChanges` state affects Activity re-creation
//!
//! Beware that, by default, certain configuration changes (e.g. device
//! rotation) will cause the Android system to destroy and re-create your
//! `Activity`, which will lead to a [`MainEvent::Destroy`] event being sent to
//! your `android_main()` thread and then `android_main()` will be called again
//! as a new native `Activity` instance is created.
//!
//! Since this can be awkward to handle, it is common practice to set the
//! `android:configChanges` property to indicate that your application can
//! handle these changes at runtime via events instead.
//!
//! **Example**:
//!
//! Here's how you can set `android:configChanges` for your `Activity` in your
//! AndroidManifest.xml:
//!
//! ```xml
//! <activity
//! android:name="android.app.NativeActivity"
//! android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
//! android:label="NativeActivity Example"
//! android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
//! android:exported="true">
//!
//! <!-- ... -->
//! </activity>
//! ```
//!
//! ### onCreate entrypoint: `android_on_create` (optional)
//!
//! The `android_on_create` entry point will be called from the Java main
//! thread, within the `Activity`'s `onCreate` method, before the `android_main`
//! entry point is called.
//!
//! This must be an exported, unmangled, `"Rust"` ABI function with the
//! signature `fn android_on_create(state: &OnCreateState)`.
//!
//! The easiest way to achieve this is with `#[unsafe(no_mangle)]` like this:
//! ```no_run
//! #[unsafe(no_mangle)]
//! fn android_on_create(state: &android_activity::OnCreateState) {
//! // Initialization code here
//! }
//! ```
//! (Note `extern "Rust"` is the default ABI)
//!
//! **I/O redirection**: Before `android_on_create()` is called an I/O thread is
//! spawned that will handle redirecting standard input and output to the
//! Android log, visible via `logcat`.
//!
//! [`OnCreateState`] provides access to the Java VM and a JNI reference to the
//! `Activity` instance, as well as any saved state from a previous instance of
//! the Activity.
//!
//! Due to the way JNI class loading works, this can be a convenient place to
//! initialize JNI bindings because it's called while the `Activity`'s
//! `onCreate` callback is on the stack, so the default class loader will be
//! able to find the application's Java classes. See the Android
//! [JNI tips](https://developer.android.com/ndk/guides/jni-tips#faq:-why-didnt-findclass-find-my-class)
//! guide for more details on this.
//!
//! This can also be a good place to initialize logging, since it's called
//! first.
//!
//! **Important**: This entrypoint must not block for a long time or do heavy
//! work, since it's running on the Java main thread and will block the
//! `Activity` from being created until it returns.
//!
//! Blocking the Java main thread for too long may cause an "Application Not
//! Responding" (ANR) dialog to be shown to the user, and cause users to force
//! close your application.
//!
//! **Panic behavior**: If `android_on_create` panics, the application will
//! abort. This is because the callback runs within a native JNI callback where
//! unwinding is not permitted. Ensure your initialization code either cannot
//! panic or uses `catch_unwind` internally if you want to allow partial
//! initialization failures.
//!
//! #### Example:
//!
//! ```no_run
//! # use std::sync::OnceLock;
//! # use android_activity::OnCreateState;
//! # use jni::{JavaVM, 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, depending on
//! // the crate, logger initialization may panic if attempted 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:?}");
//! }
//! }
//! ```
//!
//! Once your application's `Activity` class has loaded and it calls `onCreate` then
//! `android-activity` will spawn a dedicated thread to run your `android_main` function,
//! separate from the Java thread that created the corresponding `Activity`.
//! ### Main loop thread entrypoint: `android_main` (required)
//!
//! Your application must always define an `android_main` function as an entry
//! point for running a main loop thread for your Activity.
//!
//! This must be an exported, unmangled, `"Rust"` ABI function with the
//! signature `fn android_main(app: AndroidApp)`.
//!
//! The easiest way to achieve this is with `#[unsafe(no_mangle)]` like this:
//! ```no_run
//! #[unsafe(no_mangle)]
//! fn android_main(app: android_activity::AndroidApp) {
//! // Main loop code here
//! }
//! ```
//! (Note `extern "Rust"` is the default ABI)
//!
//! Once your application's `Activity` class has loaded and it calls `onCreate`
//! then `android-activity` will spawn a dedicated thread to run your
//! `android_main` function, separate from the Java thread that created the
//! corresponding `Activity`.
//!
//! Before `android_main()` is called:
//! - A `JavaVM` and
//! [`android.content.Context`](https://developer.android.com/reference/android/content/Context)
//! instance will be associated with the [`ndk_context`] crate so that other,
//! independent, Rust crates are able to find a JavaVM for making JNI calls.
//! - The `JavaVM` will be attached to the native thread (for JNI)
//! - A [Looper] is attached to the Rust native thread.
//!
//! **Important:** This thread *must* call [`AndroidApp::poll_events()`]
//! regularly in order to receive lifecycle and input events for the `Activity`.
//! Some `Activity` lifecycle callbacks on the Java main thread will block until
//! the next time `poll_events()` is called, so if you don't call
//! `poll_events()` regularly you may trigger an ANR dialog and cause users to
//! force close your application.
//!
//! **Important**: You should return from `android_main()` as soon as possible
//! if you receive a [`MainEvent::Destroy`] event from `poll_events()`. Most
//! [`AndroidApp`] methods will become a no-op after [`MainEvent::Destroy`] is
//! received, since it no longer has an associated `Activity`.
//!
//! **Important**: Do *not* call `std::process::exit()` from your
//! `android_main()` function since that will subvert the normal lifecycle of
//! the `Activity` and other components. Keep in mind that code running in
//! `android_main()` does not logically own the entire process since there may
//! be other Android components (e.g. Services) running within the process.
//!
//! ## AndroidApp: State and Event Loop
//!
//! [`AndroidApp`] provides an interface to query state for the application as
//! well as monitor events, such as lifecycle and input events, that are
//! marshalled between the Java thread that owns the `Activity` and the native
//! thread that runs the `android_main()` code.
//! well as monitor events, such as lifecycle and input events for the
//! associated native `Activity` instance.
//!
//! # Main Thread Initialization
//! ### Cheaply Cloneable [`AndroidApp`]
//!
//! Before `android_main()` is called, the following application state
//! is also initialized:
//! [`AndroidApp`] is intended to be something that can be cheaply passed around
//! within an application. It is reference-counted and can be cheaply cloned.
//!
//! 1. An I/O thread is spawned that will handle redirecting standard input
//! and output to the Android log, visible via `logcat`.
//! 2. A `JavaVM` and `Activity` instance will be associated with the [`ndk_context`] crate
//! so that other, independent, Rust crates are able to find a JavaVM
//! for making JNI calls.
//! 3. The `JavaVM` will be attached to the native thread
//! 4. A [Looper] is attached to the Rust native thread.
//! ### `Send` and `Sync` [`AndroidApp`] (**but...**)
//!
//! Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
//! into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
//! explicitly documented to only be usable from your `android_main()` thread.
//!
//! These are undone after `android_main()` returns
//! ### No associated Activity after [`MainEvent::Destroy`]
//!
//! After you receive a [`MainEvent::Destroy`] event from `poll_events()` then
//! the [`AndroidApp`] will no longer have an associated `Activity` and most of
//! its methods will become no-ops. You should return from `android_main()` as
//! soon as possible after receiving a `Destroy` event since your native
//! `Activity` no longer exists.
//!
//! If a new [`Activity`] instance is created after that then a new
//! [`AndroidApp`] will be created for that new [`Activity`] instance and sent
//! to a new call to `android_main()`.
//!
//! **Important**: It's not recommended to store an [`AndroidApp`] as global
//! static state and it should instead be passed around by reference within your
//! application so it can be reliably dropped when the `Activity` is destroyed
//! and you return from `android_main()`.
//!
//! # Android Extensible Enums
//!
//! There are numerous enums in the `android-activity` API which are effectively
//! bindings to enums declared in the Android SDK which need to be considered
//! _runtime_ extensible.
//!
//! Any enum variants that come from the Android SDK may be extended in future
//! versions of Android and your code could be exposed to new variants if you
//! build an application that might be installed on new versions of Android.
//!
//! This crate follows a convention of adding a hidden `__Unknown(u32)` variant
//! to these enums to ensure we can always do lossless conversions between the
//! integers from the SDK and our corresponding Rust enums. This can be
//! important in case you need to pass certain variants back to the SDK
//! regardless of whether you knew about that variants specific semantics at
//! compile time.
//!
//! You should never include this `__Unknown(u32)` variant within any exhaustive
//! pattern match and should instead treat the enums like `#[non_exhaustive]`
//! enums that require you to add a catch-all for any `unknown => {}` values.
//!
//! Any code that would exhaustively include the `__Unknown(u32)` variant when
//! pattern matching can not be guaranteed to be forwards compatible with new
//! releases of `android-activity` which may add new Rust variants to these
//! enums without requiring a breaking semver bump.
//!
//! You can (infallibly) convert these enums to and from primitive `u32` values
//! using `.into()`:
//!
//! For example, here is how you could ensure forwards compatibility with both
//! compile-time and runtime extensions of a `SomeEnum` enum:
//!
//! ```ignore
//! match some_enum {
//! SomeEnum::Foo => {},
//! SomeEnum::Bar => {},
//! unhandled => {
//! let sdk_val: u32 = unhandled.into();
//! println!("Unhandled enum variant {some_enum:?} has SDK value: {sdk_val}");
//! }
//! }
//! ```
//!
//! [`Activity`]: https://developer.android.com/reference/android/app/Activity
//! [`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
//! [`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/integrate-game-activity
//! [`Service`]: https://developer.android.com/reference/android/app/Service
//! [Looper]: https://developer.android.com/reference/android/os/Looper
//! [`Context`]: https://developer.android.com/reference/android/content/Context
#![deny(clippy::manual_let_else)]
use std::ffi::CStr;
use std::hash::Hash;
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
use input::KeyCharacterMap;
use bitflags::bitflags;
use jni::vm::JavaVM;
use libc::c_void;
use ndk::asset::AssetManager;
use ndk::native_window::NativeWindow;
use bitflags::bitflags;
// Since we expose `ndk` types in our public API it's convenient if crates can
// defer to these re-exported APIs and avoid having to bump explicit
// dependencies when they pull in new releases of android-activity.
pub use ndk;
pub use ndk_sys;
#[cfg(not(target_os = "android"))]
compile_error!("android-activity only supports compiling for Android");
@@ -78,7 +347,7 @@ compile_error!(
);
#[cfg(all(
not(any(feature = "game-activity", feature = "native-activity")),
not(doc)
not(any(doc, used_on_docsrs)),
))]
compile_error!(
r#"Either "game-activity" or "native-activity" must be enabled as features
@@ -97,14 +366,27 @@ You may need to add a `[patch]` into your Cargo.toml to ensure a specific versio
android-activity is used across all of your application's crates."#
);
#[cfg_attr(any(feature = "native-activity", doc), path = "native_activity/mod.rs")]
#[cfg_attr(any(feature = "game-activity", doc), path = "game_activity/mod.rs")]
#[cfg_attr(feature = "native-activity", path = "native_activity/mod.rs")]
#[cfg_attr(feature = "game-activity", path = "game_activity/mod.rs")]
#[cfg_attr(
all(
// No activities enabled.
not(any(feature = "native-activity", feature = "game-activity")),
// And building docs.
any(doc, used_on_docsrs),
),
// Fall back to documenting native activity.
path = "native_activity/mod.rs"
)]
pub(crate) mod activity_impl;
pub mod error;
use error::Result;
mod init;
pub mod input;
use input::KeyCharacterMap;
mod config;
pub use config::ConfigurationRef;
@@ -113,6 +395,15 @@ mod util;
mod jni_utils;
mod sdk;
mod waker;
pub use waker::AndroidAppWaker;
mod main_callbacks;
pub(crate) const ANDROID_ACTIVITY_TAG: &CStr = c"android-activity";
/// A rectangle with integer edge coordinates. Used to represent window insets, for example.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Rect {
@@ -211,9 +502,9 @@ pub enum MainEvent<'a> {
/// input focus.
LostFocus,
/// Command from main thread: the current device configuration has changed.
/// You can get a copy of the latest [`ndk::configuration::Configuration`] by calling
/// [`AndroidApp::config()`]
/// Command from main thread: the current device configuration has changed. Any
/// reference gotten via [`AndroidApp::config()`] will automatically contain the latest
/// [`ndk::configuration::Configuration`].
#[non_exhaustive]
ConfigChanged {},
@@ -270,7 +561,6 @@ pub enum InputStatus {
}
use activity_impl::AndroidAppInner;
pub use activity_impl::AndroidAppWaker;
bitflags! {
/// Flags for [`AndroidApp::set_window_flags`]
@@ -446,6 +736,18 @@ bitflags! {
/// marshalled between the Java thread that owns the `Activity` and the native
/// thread that runs the `android_main()` code.
///
/// # Cheaply Clonable [`AndroidApp`]
///
/// [`AndroidApp`] is intended to be something that can be cheaply passed around
/// by referenced within an application. It is reference counted and can be
/// cheaply cloned.
///
/// # `Send` and `Sync` [`AndroidApp`]
///
/// Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
/// into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
/// explicitly documented to only be usable from your `android_main()` thread.
///
#[derive(Debug, Clone)]
pub struct AndroidApp {
pub(crate) inner: Arc<RwLock<AndroidAppInner>>,
@@ -474,67 +776,119 @@ impl AndroidApp {
self.inner.read().unwrap().native_window()
}
/// Returns a [`ndk::looper::ForeignLooper`] associated with the Java
/// main / UI thread.
///
/// This can be used to register file descriptors that may wake up the
/// Java main / UI thread and optionally run callbacks on that thread.
///
/// ```ignore
/// # use ndk;
/// # let app: AndroidApp = todo!();
/// let looper = app.java_main_looper();
/// looper.add_fd_with_callback(todo!(), ndk::looper::FdEvent::INPUT, todo!()).unwrap();
/// ```
pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper {
self.inner.read().unwrap().java_main_looper().clone()
}
/// Returns a pointer to the Java Virtual Machine, for making JNI calls
///
/// This returns a pointer to the Java Virtual Machine which can be used
/// with the [`jni`] crate (or similar crates) to make JNI calls that bridge
/// between native Rust code and Java/Kotlin code running within the JVM.
///
/// If you use the [`jni`] crate you can wrap this as a [`JavaVM`] via:
/// ```ignore
/// If you use the [`jni`] crate you can could this as a [`JavaVM`] via:
/// ```no_run
/// # use jni::JavaVM;
/// # let app: AndroidApp = todo!();
/// let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr()) };
/// # let app: android_activity::AndroidApp = todo!();
/// let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) };
/// ```
///
/// [`jni`]: https://crates.io/crates/jni
/// [`JavaVM`]: https://docs.rs/jni/latest/jni/struct.JavaVM.html
pub fn vm_as_ptr(&self) -> *mut c_void {
self.inner.read().unwrap().vm_as_ptr()
JavaVM::singleton().unwrap().get_raw() as _
}
/// Returns a JNI object reference for this application's JVM `Activity` as a pointer
/// Returns an (*unowned*) JNI global object reference for this
/// application's JVM `Activity` as a pointer
///
/// If you use the [`jni`] crate you can wrap this as an object reference via:
/// ```ignore
/// If you use the [`jni`] crate you can cast this as a `JObject` reference
/// via:
/// ```no_run
/// # use jni::objects::JObject;
/// # let app: AndroidApp = todo!();
/// let activity = unsafe { JObject::from_raw(app.activity_as_ptr()) };
/// # use jni::refs::Global;
/// # fn use_jni(env: &jni::Env, app: &android_activity::AndroidApp) -> jni::errors::Result<()> {
/// let raw_activity_global = app.activity_as_ptr() as jni::sys::jobject;
/// // SAFETY: The reference / pointer is valid as long as `app` is valid
/// let activity = unsafe { env.as_cast_raw::<Global<JObject>>(&raw_activity_global)? };
/// # Ok(()) }
/// ```
///
/// # JNI Safety
///
/// Note that the object reference will be a JNI global reference, not a
/// local reference and it should not be deleted. Don't wrap the reference
/// in an [`AutoLocal`] which would try to explicitly delete the reference
/// when dropped. Similarly, don't wrap the reference as a [`GlobalRef`]
/// which would also try to explicitly delete the reference when dropped.
/// Note that the returned reference will be a JNI global reference *that
/// you do not own*.
/// - Don't wrap the reference as a [`Global`] which would try to delete the
/// reference when dropped.
/// - Don't wrap the reference in an [`Auto`] which would treat the
/// reference like a local reference and try to delete it when dropped.
///
/// The reference is only guaranteed to be valid until you drop the
/// [`AndroidApp`].
///
/// **Warning:** Don't assume the returned reference has a `'static` lifetime
/// since it's possible for `android_main()` to run multiple times over the
/// lifetime of an application with a new `AndroidApp` instance each time.
///
/// [`jni`]: https://crates.io/crates/jni
/// [`AutoLocal`]: https://docs.rs/jni/latest/jni/objects/struct.AutoLocal.html
/// [`GlobalRef`]: https://docs.rs/jni/latest/jni/objects/struct.GlobalRef.html
/// [`Auto`]: https://docs.rs/jni/latest/jni/refs/struct.Auto.html
/// [`Global`]: https://docs.rs/jni/latest/jni/refs/struct.Global.html
pub fn activity_as_ptr(&self) -> *mut c_void {
self.inner.read().unwrap().activity_as_ptr()
}
/// Polls for any events associated with this [AndroidApp] and processes those events
/// (such as lifecycle events) via the given `callback`.
/// Polls for any events associated with this [AndroidApp] and processes
/// those events (such as lifecycle events) via the given `callback`.
///
/// It's important to use this API for polling, and not call [`ALooper_pollAll`] directly since
/// some events require pre- and post-processing either side of the callback. For correct
/// behavior events should be handled immediately, before returning from the callback and
/// not simply queued for batch processing later. For example the existing [`NativeWindow`]
/// is accessible during a [`MainEvent::TerminateWindow`] callback and will be
/// set to `None` once the callback returns, and this is also synchronized with the Java
/// main thread. The [`MainEvent::SaveState`] event is also synchronized with the
/// It's important to use this API for polling, and not call
/// [`ALooper_pollAll`] or [`ALooper_pollOnce`] directly since some events
/// require pre- and post-processing either side of the callback. For
/// correct behavior events should be handled immediately, before returning
/// from the callback and not simply queued for batch processing later. For
/// example the existing [`NativeWindow`] is accessible during a
/// [`MainEvent::TerminateWindow`] callback and will be set to `None` once
/// the callback returns, and this is also synchronized with the Java main
/// thread. The [`MainEvent::SaveState`] event is also synchronized with the
/// Java main thread.
///
/// Internally this is based on [`ALooper_pollOnce`] and will only poll
/// file descriptors once per invocation.
///
/// # Wake Events
///
/// Note that although there is an explicit [PollEvent::Wake] that _can_
/// indicate that the main loop was explicitly woken up (E.g. via
/// [`AndroidAppWaker::wake`]) it's possible that there will be
/// more-specific events that will be delivered after a wake up.
///
/// In other words you should only expect to explicitly see
/// [`PollEvent::Wake`] events after an early wake up if there were no
/// other, more-specific, events that could be delivered after the wake up.
///
/// Again, said another way - it's possible that _any_ event could
/// effectively be delivered after an early wake up so don't assume there is
/// a 1:1 relationship between invoking a wake up via
/// [`AndroidAppWaker::wake`] and the delivery of [PollEvent::Wake].
///
/// # Panics
///
/// This must only be called from your `android_main()` thread and it may panic if called
/// from another thread.
/// This must only be called from your `android_main()` thread and it may
/// panic if called from another thread.
///
/// [`ALooper_pollAll`]: ndk::looper::ThreadLooper::poll_all
/// [`ALooper_pollOnce`]: ndk::looper::ThreadLooper::poll_once
pub fn poll_events<F>(&self, timeout: Option<Duration>, callback: F)
where
F: FnMut(PollEvent<'_>),
@@ -548,7 +902,99 @@ impl AndroidApp {
self.inner.read().unwrap().create_waker()
}
/// Returns a (cheaply clonable) reference to this application's [`ndk::configuration::Configuration`]
/// Runs the given closure on the Java main / UI thread.
///
/// This is useful for performing operations that must be executed on the
/// main thread, such as interacting with Android SDK APIs that require
/// execution on the main thread.
///
/// Any panic within the closure will be caught and logged as an error,
/// (assuming your application is built to allow unwinding).
///
/// The thread will be attached to the JVM (for using JNI) and any
/// un-cleared Java exceptions left over by the callback will be caught,
/// cleared and logged as an error.
///
/// There is no built-in mechanism to propagate results back to the caller
/// but you can use channels or other synchronization primitives that you
/// capture.
///
/// It's important to avoid blocking the `android_main` thread while waiting
/// for any results because this could lead to deadlocks for `Activity`
/// callbacks that require a synchronous response for the `android_activity`
/// thread.
///
/// # Example
///
/// This example demonstrates using the `jni` 0.22 API to show a toast
/// message from the Java main thread.
///
/// ```no_run
/// use android_activity::AndroidApp;
/// use jni::{objects::JString, refs::Global};
///
/// 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(),
/// }
/// }
///
/// 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 || {
/// let jvm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as _) };
/// // As an micro optimization you could use jvm.with_top_local_frame, since we know
/// // we're already attached
/// if let Err(err) = jvm.attach_current_thread(|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)?;
/// toast.show(env)?;
/// Ok(())
/// }) {
/// log::error!("Failed to show toast on main thread: {err:?}");
/// }
/// }));
/// }
/// ```
pub fn run_on_java_main_thread<F>(&self, f: Box<F>)
where
F: FnOnce() + Send + 'static,
{
self.inner.read().unwrap().run_on_java_main_thread(f);
}
/// Returns a **reference** to this application's [`ndk::configuration::Configuration`].
///
/// # Warning
///
/// The value held by this reference **will change** with every [`MainEvent::ConfigChanged`]
/// event that is raised. You should **not** [`Clone`] this type to compare it against a
/// "new" [`AndroidApp::config()`] when that event is raised, since both point to the same
/// internal [`ndk::configuration::Configuration`] and will be identical.
pub fn config(&self) -> ConfigurationRef {
self.inner.read().unwrap().config()
}
@@ -559,9 +1005,28 @@ impl AndroidApp {
self.inner.read().unwrap().content_rect()
}
/// Queries the Asset Manager instance for the application.
/// Returns the `AssetManager` for the application's `Application` context.
///
/// Use this to access binary assets bundled inside your application's .apk file.
/// Use this to access raw files bundled in the application's .apk file.
///
/// This is an `Application`-scoped asset manager, not an `Activity`-scoped
/// one. In normal usage those behave the same for packaged assets, so this
/// is usually the correct API to use.
///
/// In uncommon cases, an `Activity` may have a context-specific
/// asset/resource view that differs from the `Application` context. If you
/// specifically need the current `Activity`'s `AssetManager`, obtain the
/// `Activity` via [`AndroidApp::activity_as_ptr`] and call `getAssets()`
/// through JNI.
///
/// The returned `AssetManager` has a `'static` lifetime and remains valid
/// across `Activity` recreation, including when `android_main()` is
/// re-entered.
///
/// **Beware**: If you consider accessing the `Activity` context's
/// `AssetManager` through JNI you must keep the `AssetManager` alive via a
/// global reference before accessing the ndk `AAssetManager` and
/// `ndk::asset::AssetManager` does not currently handle this for you.
pub fn asset_manager(&self) -> AssetManager {
self.inner.read().unwrap().asset_manager()
}
@@ -629,6 +1094,23 @@ impl AndroidApp {
self.inner.read().unwrap().set_text_input_state(state);
}
/// Specify the type of text being input, how the IME enter/action key
/// should behave and any additional IME options.
///
/// Also see the Android SDK documentation for
/// [android.view.inputmethod.EditorInfo](https://developer.android.com/reference/android/view/inputmethod/EditorInfo)
pub fn set_ime_editor_info(
&self,
input_type: input::InputType,
action: input::TextInputAction,
options: input::ImeOptions,
) {
self.inner
.read()
.unwrap()
.set_ime_editor_info(input_type, action, options);
}
/// Get an exclusive, lending iterator over buffered input events
///
/// Applications are expected to call this in-sync with their rendering or
@@ -649,7 +1131,9 @@ impl AndroidApp {
/// # Example
/// Code to iterate all pending input events would look something like this:
///
/// ```rust
/// ```no_run
/// # use android_activity::{AndroidApp, InputStatus, input::InputEvent};
/// # let app: AndroidApp = todo!();
/// match app.input_events_iter() {
/// Ok(mut iter) => {
/// loop {
@@ -657,12 +1141,13 @@ impl AndroidApp {
/// let handled = match event {
/// InputEvent::KeyEvent(key_event) => {
/// // Snip
/// InputStatus::Handled
/// }
/// InputEvent::MotionEvent(motion_event) => {
/// // Snip
/// InputStatus::Unhandled
/// }
/// event => {
/// // Snip
/// InputStatus::Unhandled
/// }
/// };
///
@@ -684,7 +1169,7 @@ impl AndroidApp {
///
/// This must only be called from your `android_main()` thread and it may panic if called
/// from another thread.
pub fn input_events_iter(&self) -> Result<input::InputIterator> {
pub fn input_events_iter(&self) -> Result<input::InputIterator<'_>> {
let receiver = {
let guard = self.inner.read().unwrap();
guard.input_events_receiver()?
@@ -704,44 +1189,47 @@ impl AndroidApp {
///
/// Code to handle unicode character mapping as well as combining dead keys could look some thing like:
///
/// ```rust
/// ```no_run
/// # use android_activity::{AndroidApp, input::{InputEvent, KeyEvent, KeyMapChar}};
/// # let app: AndroidApp = todo!();
/// # let key_event: KeyEvent = todo!();
/// let mut combining_accent = None;
/// // Snip
///
/// let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
/// let combined_key_char = if let Ok(map) = app.device_key_character_map(key_event.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}'");
/// println!("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:?}");
/// eprintln!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
/// None
/// }
/// }
/// } else {
/// info!("KeyEvent: Pressed '{unicode}'");
/// println!("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}'");
/// println!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
/// combining_accent = Some(accent);
/// Some(KeyMapChar::CombiningAccent(accent))
/// }
/// Ok(KeyMapChar::None) => {
/// info!("KeyEvent: Pressed non-unicode key");
/// println!("KeyEvent: Pressed non-unicode key");
/// combining_accent = None;
/// None
/// }
/// Err(err) => {
/// log::error!("KeyEvent: Failed to get key map character: {err:?}");
/// eprintln!("KeyEvent: Failed to get key map character: {err:?}");
/// combining_accent = None;
/// None
/// }
@@ -756,6 +1244,9 @@ impl AndroidApp {
/// Since this API needs to use JNI internally to call into the Android JVM it may return
/// a [`error::AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
///
/// This API should not be called with a `device_id` of `0`, since that indicates a non-physical
/// device and will result in a [`error::AppError::JavaError`].
pub fn device_key_character_map(&self, device_id: i32) -> Result<KeyCharacterMap> {
Ok(self
.inner
@@ -798,3 +1289,81 @@ fn test_app_is_send_sync() {
fn needs_send_sync<T: Send + Sync>() {}
needs_send_sync::<AndroidApp>();
}
/// The state passed to the optional `android_on_create` entry point if
/// available.
///
/// This gives access to the Java VM, the Java `Activity` and any saved state
/// from a previous instance of the `Activity` that was saved via the
/// `onSaveInstanceState` callback.
///
/// Each time `android_on_create` is called it will receive a new `Activity`
/// reference.
///
/// See the top-level [`android-activity`](crate) documentation for more details
/// on `android_on_create`.
pub struct OnCreateState<'a> {
jvm: JavaVM,
java_activity: *mut c_void,
saved_state: &'a [u8],
}
impl<'a> OnCreateState<'a> {
pub(crate) fn new(jvm: JavaVM, java_activity: *mut c_void, saved_state: &'a [u8]) -> Self {
Self {
jvm,
java_activity,
saved_state,
}
}
/// Returns a pointer to the Java Virtual Machine, for making JNI calls
///
/// If you use the `jni` crate, you can wrap this pointer as a `JavaVM` via:
/// ```no_run
/// # use jni::JavaVM;
/// # let on_create_state: android_activity::OnCreateState = todo!();
/// let vm = unsafe { JavaVM::from_raw(on_create_state.vm_as_ptr().cast()) };
/// ```
pub fn vm_as_ptr(&self) -> *mut c_void {
self.jvm.get_raw().cast()
}
/// Returns an (*unowned*) JNI global object reference for this `Activity`
/// as a pointer
///
/// If you use the `jni` crate, you can cast this as a `JObject` reference
/// via:
///
/// ```no_run
/// # use jni::{JavaVM, objects::JObject};
/// # let on_create_state: android_activity::OnCreateState = todo!();
/// let vm = unsafe { JavaVM::from_raw(on_create_state.vm_as_ptr().cast()) };
/// let _res = vm.attach_current_thread(|env| -> jni::errors::Result<()> {
/// let activity = on_create_state.activity_as_ptr() as jni::sys::jobject;
/// // SAFETY: The reference / pointer is valid at least until we return from `android_on_create`
/// let activity = unsafe { env.as_cast_raw::<JObject>(&activity)? };
/// // Do something with `activity` here
/// Ok(())
/// });
/// ```
///
/// # JNI Safety
///
/// It is not specified whether this will be a global or local reference and
/// in any case you must treat is as a reference that you do not own and
/// must not attempt to delete it.
/// - Don't wrap the reference as a `Global` which would try to delete the
/// reference when dropped.
/// - Don't wrap the reference in an `Auto` which would treat the reference
/// like a local reference and try to delete it when dropped.
pub fn activity_as_ptr(&self) -> *mut c_void {
self.java_activity as *mut c_void
}
/// Returns the saved state of the `Activity` as a byte slice, which may be
/// empty if there is no saved state.
pub fn saved_state(&self) -> &[u8] {
self.saved_state
}
}
+226
View File
@@ -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
})
}
+171 -108
View File
@@ -3,22 +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::{
jni_utils::CloneJavaVM,
util::android_log,
init::{init_android_main_thread, init_java_main_thread_on_create},
util::{abort_on_panic, log_panic},
ConfigurationRef,
};
@@ -81,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;
@@ -214,6 +209,29 @@ pub enum NativeThreadState {
#[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: ConfigurationRef,
@@ -226,10 +244,6 @@ pub struct NativeActivityState {
pub thread_state: NativeThreadState,
pub app_has_saved_state: bool,
/// Set as soon as the Java main thread notifies us of an
/// `onDestroyed` callback.
pub destroyed: bool,
pub redraw_needed: bool,
pub pending_input_queue: *mut ndk_sys::AInputQueue,
pub pending_window: Option<NativeWindow>,
}
@@ -315,7 +329,7 @@ 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();
@@ -343,8 +357,12 @@ impl WaitableNativeActivityState {
}
}
let saved_state =
unsafe { std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size) };
let saved_state = if saved_state_in.is_null() {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size) }
.to_vec()
};
let config = unsafe {
let config = ndk_sys::AConfiguration_new();
@@ -358,12 +376,12 @@ 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(),
@@ -372,7 +390,6 @@ impl WaitableNativeActivityState {
thread_state: NativeThreadState::Init,
app_has_saved_state: false,
destroyed: false,
redraw_needed: false,
pending_input_queue: ptr::null_mut(),
pending_window: None,
}),
@@ -394,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();
}
}
@@ -449,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();
@@ -470,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;
@@ -494,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();
}
}
@@ -507,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;
@@ -562,7 +588,9 @@ impl WaitableNativeActivityState {
pub fn notify_main_thread_stopped_running(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.thread_state = NativeThreadState::Stopped;
self.cond.notify_one();
// 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(
@@ -601,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);
@@ -643,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) {
@@ -657,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();
})
}
@@ -828,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
@@ -877,55 +907,7 @@ 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 = abort_on_panic(|| unsafe {
let na = activity;
let jvm: *mut jni_sys::JavaVM = (*na).vm;
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let jvm = CloneJavaVM::from_raw(jvm).unwrap();
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
jvm.attach_current_thread_permanently().unwrap();
jvm
});
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
rust_glue.notify_main_thread_running();
unsafe {
// 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);
// This should detach automatically but lets detach explicitly to avoid depending
// on the TLS trickery in `jni-rs`
jvm.detach_current_thread();
ndk_context::release_android_context();
}
rust_glue.notify_main_thread_stopped_running();
rust_glue_entry(rust_glue, activity, main_looper);
});
// Wait for thread to start.
@@ -938,3 +920,84 @@ extern "C" fn ANativeActivity_onCreate(
}
})
}
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");
})
}
+119 -31
View File
@@ -1,10 +1,8 @@
use std::marker::PhantomData;
pub use ndk::event::{Pointer, PointersIter};
use crate::input::{
ButtonState, Class, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction, MotionEventFlags,
Source,
Axis, Button, ButtonState, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
};
/// A motion event
@@ -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().into()
// 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,11 @@ 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 {
ndk_pointers_iter: self.ndk_event.pointers(),
},
}
}
/// The pointer at a given pointer index. Panics if the pointer index is out of bounds.
@@ -107,7 +121,11 @@ 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 {
ndk_pointer: self.ndk_event.pointer_at_index(index),
},
}
}
/*
@@ -224,6 +242,73 @@ impl<'a> MotionEvent<'a> {
}
}
/// A view into the data of a specific pointer in a motion event.
#[derive(Debug)]
pub(crate) struct PointerImpl<'a> {
ndk_pointer: ndk::event::Pointer<'a>,
}
impl PointerImpl<'_> {
#[inline]
pub fn pointer_index(&self) -> usize {
self.ndk_pointer.pointer_index()
}
#[inline]
pub fn pointer_id(&self) -> i32 {
self.ndk_pointer.pointer_id()
}
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
let value: u32 = axis.into();
let value = value as i32;
self.ndk_pointer.axis_value(value.into())
}
#[inline]
pub fn raw_x(&self) -> f32 {
self.ndk_pointer.raw_x()
}
#[inline]
pub fn raw_y(&self) -> f32 {
self.ndk_pointer.raw_y()
}
#[inline]
pub fn tool_type(&self) -> ToolType {
let value: i32 = self.ndk_pointer.tool_type().into();
let value = value as u32;
value.into()
}
}
/// An iterator over the pointers in a [`MotionEvent`].
#[derive(Debug)]
pub(crate) struct PointersIterImpl<'a> {
ndk_pointers_iter: ndk::event::PointersIter<'a>,
}
impl<'a> Iterator for PointersIterImpl<'a> {
type Item = Pointer<'a>;
fn next(&mut self) -> Option<Pointer<'a>> {
self.ndk_pointers_iter.next().map(|ndk_pointer| Pointer {
inner: PointerImpl { ndk_pointer },
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.ndk_pointers_iter.size_hint()
}
}
impl ExactSizeIterator for PointersIterImpl<'_> {
fn len(&self) -> usize {
self.ndk_pointers_iter.len()
}
}
/// A key event
///
/// For general discussion of key events in Android, see [the relevant
@@ -234,7 +319,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 +336,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 +355,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().into()
// 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 +388,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().into()
// 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.
@@ -347,4 +434,5 @@ pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}
+229 -158
View File
@@ -1,27 +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, 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::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
use crate::input::{TextInputState, TextSpan};
use crate::jni_utils::{self, CloneJavaVM};
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;
@@ -53,102 +56,94 @@ 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, jvm: CloneJavaVM) -> Self {
let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp
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 key_map_binding = match KeyCharacterMapBinding::new(&mut env) {
Ok(b) => b,
Err(err) => {
panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}");
}
};
let app = Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm,
native_activity,
looper: Looper {
ptr: ptr::null_mut(),
},
key_map_binding: Arc::new(key_map_binding),
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
};
{
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: CloneJavaVM,
pub(crate) jvm: JavaVM,
pub(crate) native_activity: NativeActivityGlue,
looper: Looper,
/// Shared JNI bindings for the `KeyCharacterMap` class
key_map_binding: Arc<KeyCharacterMapBinding>,
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
@@ -159,24 +154,30 @@ pub(crate) struct AndroidAppInner {
/// 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> {
@@ -200,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 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
@@ -274,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,
);
@@ -288,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
@@ -303,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 {
@@ -328,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(
@@ -340,7 +344,13 @@ impl AndroidAppInner {
add_flags: WindowManagerFlags,
remove_flags: WindowManagerFlags,
) {
let na = self.native_activity();
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 as *mut ndk_sys::ANativeActivity;
unsafe {
ndk_sys::ANativeActivity_setWindowFlags(
@@ -353,27 +363,71 @@ 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:?}");
}
}
@@ -391,17 +445,23 @@ impl AndroidAppInner {
// 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 = jni_utils::device_key_character_map(
self.jvm.clone(),
self.key_map_binding.clone(),
device_id,
)?;
let character_map = device_key_character_map(self.jvm.clone(), device_id)?;
vacant.insert(character_map.clone());
character_map
}
@@ -433,7 +493,7 @@ impl AndroidAppInner {
// trigger a wake up)
let queue = self
.native_activity
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
.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
@@ -445,17 +505,32 @@ impl AndroidAppInner {
}
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
let na = self.native_activity();
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 na = self.native_activity();
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 na = self.native_activity();
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) }
}
}
@@ -465,7 +540,7 @@ pub(crate) struct InputReceiver {
queue: Option<InputQueue>,
}
impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
impl From<Arc<InputReceiver>> for InputIteratorInner<'_> {
fn from(receiver: Arc<InputReceiver>) -> Self {
Self {
receiver,
@@ -480,28 +555,23 @@ pub(crate) struct InputIteratorInner<'a> {
_lifetime: PhantomData<&'a ()>,
}
impl<'a> InputIteratorInner<'a> {
impl InputIteratorInner<'_> {
pub(crate) fn next<F>(&self, callback: F) -> bool
where
F: FnOnce(&input::InputEvent) -> InputStatus,
{
// XXX: would use `let Some(queue) = &self.receiver.queue else { return
// false; }` but we're stuck supporting Rust 1.64 for Winit currently
let queue = if let Some(queue) = &self.receiver.queue {
queue
} else {
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
//
if let Ok(Some(ndk_event)) = queue.get_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) {
@@ -512,6 +582,7 @@ impl<'a> InputIteratorInner<'a> {
ndk::event::InputEvent::KeyEvent(e) => {
input::InputEvent::KeyEvent(input::KeyEvent::new(e))
}
_ => todo!("NDK added a new type"),
};
// `finish_event` needs to be called for each event otherwise
+67
View File
@@ -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(())
}
+91
View File
@@ -0,0 +1,91 @@
use std::ptr::NonNull;
use std::task::{RawWaker, RawWakerVTable, Waker};
#[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());
}
}
/// Creates a [`Waker`] that wakes up the [`AndroidApp`].
///
/// This is useful for using this crate in `async` environments.
///
/// [`Waker`]: std::task::Waker
pub fn into_waker(self) -> Waker {
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake, drop);
unsafe fn clone(data: *const ()) -> RawWaker {
ndk_sys::ALooper_acquire(data as *const _ as *mut _);
RawWaker::new(data, &VTABLE)
}
unsafe fn wake(data: *const ()) {
ndk_sys::ALooper_wake(data as *const _ as *mut _)
}
unsafe fn drop(data: *const ()) {
ndk_sys::ALooper_release(data as *const _ as *mut _);
}
// Take the existing reference to the looper and use it for the Waker
let looper_ptr = self.looper.as_ptr() as *const ();
std::mem::forget(self);
unsafe { Waker::from_raw(RawWaker::new(looper_ptr, &VTABLE)) }
}
}
impl From<AndroidAppWaker> for Waker {
fn from(waker: AndroidAppWaker) -> Self {
waker.into_waker()
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ edition = "2021"
log = "0.4"
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = ["game-activity"] }
ndk-sys = "0.5.0-beta.0"
ndk = "0.8.0-beta.0"
ndk-sys = "0.6.0"
ndk = "0.9.0"
[lib]
name="main"
+1 -1
View File
@@ -48,7 +48,7 @@ dependencies {
//implementation "androidx.games:games-performance-tuner:1.5.0"
// To use the Games Activity library
implementation "androidx.games:games-activity:2.0.2"
implementation "androidx.games:games-activity:4.0.0"
// To use the Games Controller Library
//implementation "androidx.games:games-controller:2.0.2"
@@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
-3
View File
@@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>
-19
View File
@@ -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>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="..\:/Users/Robert/src/agdk-rust/examples/na-mainloop/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2595" />
<entry key="..\:/Users/Robert/src/agdk-rust/examples/na-mainloop/app/src/main/res/drawable/ic_launcher_background.xml" value="0.2595" />
<entry key="..\:/Users/Robert/src/agdk-rust/examples/na-mainloop/app/src/main/res/layout/activity_main.xml" value="0.25416666666666665" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-7
View File
@@ -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>
+2 -2
View File
@@ -9,8 +9,8 @@ edition = "2021"
log = "0.4"
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = [ "native-activity" ] }
ndk-sys = "0.5.0-beta.0"
ndk = "0.8.0-beta.0"
ndk-sys = "0.6.0"
ndk = "0.9.0"
[lib]
#name="na_mainloop"