Compare commits

...

71 Commits

Author SHA1 Message Date
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
Robert Bragg 47a073f702 Merge pull request #113 from MarijnS95/ndk-breaking-prep
Upgrade to `ndk-sys 0.5.0-beta.0`, `ndk 0.8.0-beta.0`
2023-08-15 21:55:53 +01:00
Marijn Suijten 499d09595b Upgrade to ndk-sys 0.5.0-beta.0, ndk-0.8.0 beta.0 2023-08-15 22:50:29 +02:00
Robert Bragg 23a8570d48 Merge pull request #112 from rust-mobile/release-0.5.0-beta.0
Release 0.5.0 beta.0
2023-08-15 21:31:23 +01:00
Robert Bragg c9f57a734f Release 0.5.0-beta.0 2023-08-15 21:23:29 +01:00
Robert Bragg e91176cb08 CI: assume NDK + Java in official Ubuntu images 2023-08-15 21:23:29 +01:00
Robert Bragg 2a61f84c70 Remove examples/agdk-mainloop/.idea 2023-08-15 21:23:29 +01:00
Robert Bragg 2654c9659b Merge pull request #110 from rust-mobile/rib/pr/no-missing-input-queue-error
native-activity: don't treat missing input queue as error
2023-08-15 21:13:47 +01:00
Robert Bragg 35fe600235 native-activity: don't treat missing input queue as error
If an app tries to iterate input events while there's no input queue
(e.g. before onStart) then just behave like there are no events
available instead of returning an error.

This also reduces the logging level of some messages to reduce the
verbosity of info logs.
2023-08-15 20:59:26 +01:00
Robert Bragg 242285b205 Merge pull request #108 from rust-mobile/dependabot/cargo/num_enum-0.7
build(deps): update num_enum requirement from 0.6 to 0.7
2023-08-15 20:50:59 +01:00
Robert Bragg 379f064170 Merge pull request #107 from rust-mobile/rib/pr/consolidate-more-input-types
Consolidate input types to avoid portability hazards
2023-08-15 20:42:59 +01:00
dependabot[bot] e2f69421a0 build(deps): update num_enum requirement from 0.6 to 0.7
Updates the requirements on [num_enum](https://github.com/illicitonion/num_enum) to permit the latest version.
- [Commits](https://github.com/illicitonion/num_enum/commits)

---
updated-dependencies:
- dependency-name: num_enum
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-14 15:13:55 +00:00
Robert Bragg 1b3334178b Consolidate input types to avoid portability hazards
The following types have been moved from game_activity/input.rs to
input.rs so they can be shared by both backends:

Axis, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode,
MetaState, MotionAction, MotionEventFlags

This addresses a portability hazard whereby code (such as Winit)
would inadvertently use the `ndk` type which works OK with the
native-activity backend but then wouldn't compile against the
game-activity backend.

The alternative of consolidating on the `ndk::events` types instead was
considered but we've repeatedly needed to diverge from the `ndk` API for
the sake of maintaining a consistent input API across the
`game-activity` and `native-activity` backends (input is an area where
the backends differ significantly in their implementation) and so it
generally seems slightly preferable to consolidate on types from this
crate (though it shouldn't make much difference for these types which
are almost direct bindings from ndk_sys).

The types can be converted to their `ndk::events` counterpart
via the `From` trait.

For now some of the `From` trait implementations rely on
`try_from().unwrap()` but the intention is to replace these with
infallible implementations once we bump the MSRV > 1.66
where we can use `num_enum::FromPrimitive` after adding a
catch-all `Other(u32)` to these enums.
2023-08-08 13:46:24 +01:00
Robert Bragg 535994f4a2 Merge pull request #106 from rust-mobile/rib/pr/game-activity-disable-get-unicode-char
GameActivity PATCH: Don't read unicode via getUnicodeChar
2023-08-07 23:21:42 +01:00
Robert Bragg 6b3307410e GameActivity PATCH: Don't read unicode via getUnicodeChar
The `unicodeChar` in `GameActivityKeyEvent` wasn't being exposed by
`android-activity` because we couldn't expose the unicode character in
the same way with the native-activity backend - due to how events are
received via an `InputQueue` that doesn't expose the underlying Java
references for the key events.

Now that we have a consistent way of supporting unicode character
mapping via `KeyCharacterMap` bindings it's redundant for the
`GameActivity` backend to call `getUnicodeChar` automatically for
each key press.
2023-08-07 21:34:31 +01:00
Robert Bragg b4cf0eeabf Merge pull request #102 from rust-mobile/rib/pr/input-api-rework-with-key-character-maps
Rework `input_events` API and expose `KeyCharacterMap` bindings
2023-08-07 20:26:50 +01:00
Robert Bragg af331e3bff Rework input_events API and expose KeyCharacterMap bindings
With the way events are delivered via an `InputQueue` with
`NativeActivity` there is no direct access to the underlying KeyEvent
and MotionEvent Java objects and no `ndk` API that supports the
equivalent of `KeyEvent.getUnicodeChar()`

What `getUnicodeChar` does under the hood though is to do lookups into a
`KeyCharacterMap` for the corresponding `InputDevice` based on the
event's `key_code` and `meta_state` - which are things we can do via
some JNI bindings for `KeyCharacterMap`.

Although it's still awkward to expose an API like
`key_event.get_unicode_char()` we can instead provide an API that
lets you look up a `KeyCharacterMap` for any `device_id` and
applications can then use that for character mapping.

This approach is also more general than the `getUnicodeChar` utility
since it exposes other useful state, such as being able to check what
kind of keyboard input events are coming from (such as a full physical
keyboard vs a virtual / 'predictive' keyboard)

For consistency this exposes the same API through the game-activity
backend, even though the game-activity backend is technically able to
support unicode lookups via `getUnicodeChar` (since it has access to the
Java `KeyEvent` object).

This highlighted a need to be able to use other `AndroidApp` APIs while
processing input, which wasn't possible with the `.input_events()` API
design because the `AndroidApp` held a lock over the backend while
iterating events.

This changes `input_events()` to `input_events_iter()` which now returns
a form of lending iterator and instead of taking a callback that gets
called repeatedly by `input_events()` a similar callback is now passed
to `iter.next(callback)`.

The API isn't as ergonomic as I would have liked, considering that
lending iterators aren't a standard feature for Rust yet but also since
we still want to have the handling for each individual event go via a
callback that can report whether an event was "handled". I think the
slightly awkward ergonomics are acceptable though considering that
the API will generally be used as an implementation detail within
middleware frameworks like Winit.

Since this is the first example where we're creating non-trivial Java
bindings for an Android SDK API this adds some JNI utilities and
establishes a pattern for how we can implement a class binding.

It's an implementation detail but with how I wrote the binding I tried
to keep in mind the possibility of creating a procmacro later that would
generate some of the JNI boilerplate involved.
2023-08-07 18:36:50 +01:00
Robert Bragg 6f72dde55d Merge pull request #105 from rust-mobile/rib/pr/agdk-mainloop-update-v2.0.2
agdk-mainloop: update for GameActivity 2.0.2
2023-08-07 18:29:21 +01:00
Robert Bragg d0f10a0dd9 agdk-mainloop: update for GameActivity 2.0.2 2023-08-07 16:44:07 +01:00
Robert Bragg 3464ba20bc Merge pull request #104 from rust-mobile/rib/pr/revert-msrv-bump-for-winit
Revert 'Bump MSRV to 1.68'
2023-08-07 16:32:14 +01:00
Robert Bragg 1abb02c820 Revert 'Bump MSRV to 1.68'
This effectively reverts 66cfc68dac
and adds some comments explaining that we're currently blocked by
Winit's MSRV policy + CI from being able to increase our
rust-version.

This is a frustrating conflict that I hope can be addressed by
updating Winit's CI system to allow different platforms to require
more recent versions of Rust (which notably isn't in conflict with
setting a conservative rust-version in Winit for supporting Debian
on Linux)

This re-instates building android-activity with cargo-ndk 2 because
building on Android with 1.64 requires a linker workaround that's
not implemented in newer version of cargo-ndk.

This also reinstates the clippy false-negative warning suppression
for unsafe blocks. Again it's frustrating that we can't have good
things because of how Winit wants to support Debian which shouldn't
be relevant for Android development.

Here is an upstream issue to discuss a potential solution for this:
https://github.com/rust-windowing/winit/issues/3000
2023-08-04 17:49:20 +01:00
Robert Bragg c0a9e20c5a Merge pull request #103 from rust-mobile/rib/pr/msrv-1.68
Bump MSRV to 1.68
2023-08-03 17:20:02 +01:00
Robert Bragg 66cfc68dac 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
- Should unblock CI for #102

- 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-08-03 17:10:53 +01:00
Robert Bragg ed2dc53ee4 Merge pull request #85 from daxpedda/bitflags-v2
Bump `bitflags` to v2
2023-08-03 11:16:14 +01:00
dAxpeDDa 74f510a99a Bump bitflags to v2 2023-08-03 11:09:47 +01:00
Robert Bragg 741e633ea8 Merge pull request #24 from rust-mobile/ime-support
Input method (soft keyboard) support
2023-08-01 15:40:44 +01:00
Robert Bragg 41f30c39ad Expose TextEvent and input method state
This also adds `InputEvent::TextEvent` for notifying applications of IME
state changes as well as explicit getter/setter APIs for tracking IME
selection + compose region state. (only supported with GameActivity)

Fixes: #18
2023-07-31 22:29:09 +01:00
Robert Bragg 96497f9da9 Merge pull request #100 from rust-mobile/rib/pr/game-activity-no-input-deref
game-activity: Remove Deref implementations for Key/MotionEvent types
2023-07-30 22:02:12 +01:00
Robert Bragg c22a5453df game-activity: Remove Deref implementations for Key/MotionEvent types 2023-07-30 21:20:54 +01:00
Robert Bragg 9bb5f9c9cf Merge pull request #88 from lexi-the-cute/main
Updated To Games-Activity 2.0.2
2023-07-30 20:56:58 +01:00
Robert Bragg a604c0aa9f game-activity: Integrate GameActivity 2.0.2 2023-07-30 20:46:49 +01:00
Robert Bragg b09526a4a9 game-activity: update ffi bindings for 2.0.2 via ./generate-bindings.sh 2023-07-30 20:42:32 +01:00
Robert Bragg cc3983ca21 generate-bindings: add comment about installing bindgen-cli 2023-07-30 20:42:32 +01:00
Robert Bragg 2a2f27637f GameActivity PATCH: fix deadlocks in java callbacks after app destroyed
This ensures that any java Activity callbacks take into account the
possibility that the `android_app` may have already been marked
destroyed if `android_main` has returned - and so they mustn't block
and wait for a thread that is no longer running.
2023-07-30 20:41:38 +01:00
Robert Bragg d2d18154d9 GameActivity PATCH: Support InputAvailable events
This makes a small change to the C glue code for GameActivity to send
looper wake ups when new input is received (only sending a single wake
up, until the application next handles input).

This makes it possible to recognise that new input is available and send
an `InputAvailable` event to the application - consistent with how
NativeActivity can deliver `InputAvailable` events.

This addresses a significant feature disparity between GameActivity and
NativeActivity that meant GameActivity was not practically usable for
GUI applications that wouldn't want to render continuously like a game.
2023-07-30 20:38:19 +01:00
Robert Bragg 3e3fb84c03 GameActivity PATCH: remove unused variable 2023-07-30 20:38:19 +01:00
Robert Bragg 202ab4c1e9 GameActivity PATCH: Rename android_main _rust_glue_entry
The real `android_main` is going to be written in Rust and
android-activity needs to handle its own initialization before calling
the application's `android_main` and so the C/C++ code
calls an intermediate `_rust_glue_entry` function.
2023-07-30 20:38:19 +01:00
Robert Bragg d6345abb2a GameActivity PATCH: rename C symbols that need export
Give C symbols that need to be exported a `_C` suffix so that they can
be linked into a Rust symbol with the correct name (Since we can't
directly export from C/C++ with Rust+Cargo)

See: https://github.com/rust-lang/rfcs/issues/2771
2023-07-30 20:38:18 +01:00
Alexis c471fdf903 Import unmodified GameActivity 2.0.2 Source
See: https://developer.android.com/jetpack/androidx/releases/games#games-activity_version_20_2
2023-07-30 20:38:14 +01:00
Robert Bragg 1a8a92b3fb Merge pull request #99 from rust-mobile/release-0.4.3
Release 0.4.3
2023-07-30 20:19:32 +01:00
Robert Bragg bb97af154f Release 0.4.3 2023-07-30 20:07:43 +01:00
Robert Bragg 8a21219695 Merge pull request #98 from rust-mobile/rib/pr/fix-game-activity-deadlock
GameActivity PATH: fix deadlocks in java callbacks after app destroyed
2023-07-30 19:51:23 +01:00
Robert Bragg c10a2fb67a GameActivity PATH: fix deadlocks in java callbacks after app destroyed
This ensures that any java Activity callbacks take into account the
possibility that the `android_app` may have already been marked
destroyed if `android_main` has returned - and so they mustn't block
and wait for a thread that is no longer running.
2023-07-30 19:38:50 +01:00
Robert Bragg ab2606a73d build.rs: emit rerun-if-changed lines for compiled C/C++ code 2023-07-30 19:38:50 +01:00
Robert Bragg a84a7b54cd Merge pull request #94 from sagebind/fix-deadlock-on-ondestroy
Fix deadlock on activity onDestroy
2023-07-30 16:01:31 +01:00
Stephen M. Coakley a9e91f4308 Fix deadlock on activity onDestroy
Fix a deadlock that occurs when an activity is destroyed without process
termination, such as when an activity is destroyed and recreated due to
a configuration change.

The deadlock occurs because `notify_destroyed` blocks until `destroyed`
is set to `true`. This only occurs when `WaitableNativeActivityState` is
dropped, but the `WaitableNativeActivityState` instance is the very
thing being used to await for destruction, resulting in a deadlock.

Instead of waiting for the `WaitableNativeActivityState` to be dropped
we now wait until the native `android_main` thread has stopped.

So we can tell the difference between the thread not running because it
hasn't started or because it has finished (in case `android_main`
returns immediately) this replaces the `running` boolean with a
tri-state enum.

Co-authored-by: Robert Bragg <robert@sixbynine.org>
2023-07-21 20:02:13 +01:00
53 changed files with 13980 additions and 12371 deletions
+7 -18
View File
@@ -2,7 +2,7 @@ name: ci
on:
push:
branches: [main]
branches: '*'
pull_request:
env:
@@ -12,16 +12,16 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
# See top README for MSRV policy
rust_version: [1.64.0, stable]
rust_version: [1.68.0, stable]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: hecrj/setup-rust-action@v1
- uses: hecrj/setup-rust-action@v2
with:
rust-version: ${{ matrix.rust_version }}
@@ -34,18 +34,7 @@ jobs:
i686-linux-android
- name: Install cargo-ndk
# We're currently sticking with cargo-ndk 2, until we bump our
# MSRV to 1.68+
run: cargo install cargo-ndk --version "^2"
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
run: cargo install cargo-ndk
- name: Build game-activity
working-directory: android-activity
@@ -96,7 +85,7 @@ jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
+3 -6
View File
@@ -1,8 +1,5 @@
[workspace]
members = [
"android-activity"
]
resolver = "2"
members = ["android-activity"]
exclude = [
"examples",
]
exclude = ["examples"]
+125 -4
View File
@@ -1,25 +1,146 @@
<!-- markdownlint-disable MD022 MD024 MD032 MD033 -->
# Changelog
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.5.0] - 2023-10-16
### Added
- Added `MotionEvent::action_button()` exposing the button associated with button press/release actions ()
## [0.4.2] - 2022-02-16
### Changed
- rust-version bumped to 0.68 ([#123](https://github.com/rust-mobile/android-activity/pull/123))
- *Breaking*: updates to `ndk 0.8` and `ndk-sys 0.5` ([#128](https://github.com/rust-mobile/android-activity/pull/128))
- The `Pointer` and `PointerIter` types from the `ndk` crate are no longer directly exposed in the public API ([#122](https://github.com/rust-mobile/android-activity/pull/122))
- All input API enums based on Android SDK enums have been made runtime extensible via hidden `__Unknown(u32)` variants ([#131](https://github.com/rust-mobile/android-activity/pull/131))
## [0.5.0-beta.1] - 2023-08-15
### Changed
- Pulled in `ndk-sys 0.5.0-beta.0` and `ndk 0.8.0-beta.0` ([#113](https://github.com/rust-mobile/android-activity/pull/113))
## [0.5.0-beta.0] - 2023-08-15
### Added
- Added `KeyEvent::meta_state()` for being able to query the state of meta keys, needed for character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
- Added `KeyCharacterMap` JNI bindings to the corresponding Android SDK API ([#102](https://github.com/rust-mobile/android-activity/pull/102))
- Added `AndroidApp::device_key_character_map()` for being able to get a `KeyCharacterMap` for a given `device_id` for unicode character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
<details>
<summary>Click here for an example of how to handle unicode character mapping:</summary>
```rust
let mut combining_accent = None;
// Snip
let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
match map.get(key_event.key_code(), key_event.meta_state()) {
Ok(KeyMapChar::Unicode(unicode)) => {
let combined_unicode = if let Some(accent) = combining_accent {
match map.get_dead_char(accent, unicode) {
Ok(Some(key)) => {
info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
Some(key)
}
Ok(None) => None,
Err(err) => {
log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
None
}
}
} else {
info!("KeyEvent: Pressed '{unicode}'");
Some(unicode)
};
combining_accent = None;
combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
}
Ok(KeyMapChar::CombiningAccent(accent)) => {
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
combining_accent = Some(accent);
Some(KeyMapChar::CombiningAccent(accent))
}
Ok(KeyMapChar::None) => {
info!("KeyEvent: Pressed non-unicode key");
combining_accent = None;
None
}
Err(err) => {
log::error!("KeyEvent: Failed to get key map character: {err:?}");
combining_accent = None;
None
}
}
} else {
None
};
```
</details>
- Added `TextEvent` Input Method event for supporting text editing via virtual keyboards ([#24](https://github.com/rust-mobile/android-activity/pull/24))
### Changed
- GameActivity updated to 2.0.2 (requires the corresponding 2.0.2 `.aar` release from Google) ([#88](https://github.com/rust-mobile/android-activity/pull/88))
- `AndroidApp::input_events()` is replaced by `AndroidApp::input_events_iter()` ([#102](https://github.com/rust-mobile/android-activity/pull/102))
<details>
<summary>Click here for an example of how to use `input_events_iter()`:</summary>
```rust
match app.input_events_iter() {
Ok(mut iter) => {
loop {
let read_input = iter.next(|event| {
let handled = match event {
InputEvent::KeyEvent(key_event) => {
// Snip
}
InputEvent::MotionEvent(motion_event) => {
// Snip
}
event => {
// Snip
}
};
handled
});
if !read_input {
break;
}
}
}
Err(err) => {
log::error!("Failed to get input events iterator: {err:?}");
}
}
```
</details>
## [0.4.3] - 2023-07-30
### Fixed
- Fixed a deadlock in the `native-activity` backend while waiting for the native thread after getting an `onDestroy` callback from Java ([#94](https://github.com/rust-mobile/android-activity/pull/94))
- Fixed numerous deadlocks in the `game-activity` backend with how it would wait for the native thread in various Java callbacks, after the app has returned from `android_main` ([#98](https://github.com/rust-mobile/android-activity/pull/98))
## [0.4.2] - 2023-06-17
### Changed
- The `Activity.finish()` method is now called when `android_main` returns so the `Activity` will be destroyed ([#67](https://github.com/rust-mobile/android-activity/issues/67))
- The `native-activity` backend now propagates `NativeWindow` redraw/resize and `ContentRectChanged` callbacks to main loop ([#70](https://github.com/rust-mobile/android-activity/pull/70))
- The `game-activity` implementation of `pointer_index()` was fixed to not always return `0` ([#80](https://github.com/rust-mobile/android-activity/pull/84))
- Added `panic` guards around application's `android_main()` and native code that could potentially unwind across a Java FFI boundary ([#68](https://github.com/rust-mobile/android-activity/pull/68))
## [0.4.1] - 2022-02-16
## [0.4.1] - 2023-02-16
### Added
- Added `AndroidApp::vm_as_ptr()` to expose JNI `JavaVM` pointer ([#60](https://github.com/rust-mobile/android-activity/issues/60))
- Added `AndroidApp::activity_as_ptr()` to expose Android `Activity` JNI reference as pointer ([#60](https://github.com/rust-mobile/android-activity/issues/60))
### Changed
- Removed some overly-verbose logging in the `native-activity` backend ([#49](https://github.com/rust-mobile/android-activity/pull/49))
### Removed
- Most of the examples were moved to https://github.com/rust-mobile/rust-android-examples ([#50](https://github.com/rust-mobile/android-activity/pull/50))
- Most of the examples were moved to <https://github.com/rust-mobile/rust-android-examples> ([#50](https://github.com/rust-mobile/android-activity/pull/50))
## [0.4] - 2022-11-10
### Changed
@@ -54,4 +175,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.1] - 2022-07-04
### Added
- Initial release
- Initial release
+15 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "android-activity"
version = "0.4.2"
version = "0.5.0"
edition = "2021"
keywords = ["android", "ndk"]
readme = "../README.md"
@@ -9,7 +9,13 @@ repository = "https://github.com/rust-mobile/android-activity"
documentation = "https://docs.rs/android-activity"
description = "Glue for building Rust applications on Android with NativeActivity or GameActivity"
license = "MIT OR Apache-2.0"
rust-version = "1.64"
# 1.68 was when Rust last updated the Android NDK version used to build the
# standard library which avoids needing the -lunwind workaround in build tools.
#
# We depend on cargo-ndk for building which has dropped support for the above
# linker workaround.
rust-version = "1.68.0"
[features]
# Note: we don't enable any backend by default since features
@@ -25,13 +31,16 @@ native-activity = []
[dependencies]
log = "0.4"
jni-sys = "0.3"
ndk = "0.7"
ndk-sys = "0.4"
cesu8 = "1"
jni = "0.21"
ndk-sys = "0.5.0"
ndk = "0.8.0"
ndk-context = "0.1"
android-properties = "0.2"
num_enum = "0.6"
bitflags = "1.3"
num_enum = "0.7"
bitflags = "2.0"
libc = "0.2"
thiserror = "1"
[build-dependencies]
cc = { version = "1.0", features = ["parallel"] }
+18 -6
View File
@@ -1,12 +1,24 @@
The third-party glue code, under the native-activity-csrc/ and game-activity-csrc/ directories
is covered by the Apache 2.0 license only:
# License
Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
## GameActivity
The third-party glue code, under the game-activity-csrc/ directory is covered by
the Apache 2.0 license only:
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
## SDK Documentation
Documentation for APIs that are direct bindings of Android platform APIs are covered
by the Apache 2.0 license only:
Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
## android-activity
All other code is dual-licensed under either
* MIT License (docs/LICENSE-MIT or http://opensource.org/licenses/MIT)
* Apache License, Version 2.0 (docs/LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License (docs/LICENSE-MIT or <http://opensource.org/licenses/MIT>)
- Apache License, Version 2.0 (docs/LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
at your option.
at your option.
+18
View File
@@ -1,19 +1,37 @@
#![allow(dead_code)]
fn build_glue_for_game_activity() {
for f in [
"GameActivity.h",
"GameActivity.cpp",
"GameActivityEvents.h",
"GameActivityEvents.cpp",
"GameActivityLog.h",
] {
println!("cargo:rerun-if-changed=game-activity-csrc/game-activity/{f}");
}
cc::Build::new()
.cpp(true)
.include("game-activity-csrc")
.file("game-activity-csrc/game-activity/GameActivity.cpp")
.file("game-activity-csrc/game-activity/GameActivityEvents.cpp")
.extra_warnings(false)
.cpp_link_stdlib("c++_static")
.compile("libgame_activity.a");
for f in ["gamecommon.h", "gametextinput.h", "gametextinput.cpp"] {
println!("cargo:rerun-if-changed=game-activity-csrc/game-text-input/{f}");
}
cc::Build::new()
.cpp(true)
.include("game-activity-csrc")
.file("game-activity-csrc/game-text-input/gametextinput.cpp")
.cpp_link_stdlib("c++_static")
.compile("libgame_text_input.a");
for f in ["android_native_app_glue.h", "android_native_app_glue.c"] {
println!("cargo:rerun-if-changed=game-activity-csrc/native_app_glue/{f}");
}
cc::Build::new()
.include("game-activity-csrc")
.include("game-activity-csrc/game-activity/native_app_glue")
@@ -0,0 +1,41 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* This is the main interface to the Android Performance Tuner library, also
* known as Tuning Fork.
*
* It is part of the Android Games SDK and produces best results when integrated
* with the Swappy Frame Pacing Library.
*
* See the documentation at
* https://developer.android.com/games/sdk/performance-tuner/custom-engine for
* more information on using this library in a native Android game.
*
*/
#pragma once
// There are separate versions for each GameSDK component that use this format:
#define ANDROID_GAMESDK_PACKED_VERSION(MAJOR, MINOR, BUGFIX) \
((MAJOR << 16) | (MINOR << 8) | (BUGFIX))
// Accessors
#define ANDROID_GAMESDK_MAJOR_VERSION(PACKED) ((PACKED) >> 16)
#define ANDROID_GAMESDK_MINOR_VERSION(PACKED) (((PACKED) >> 8) & 0xff)
#define ANDROID_GAMESDK_BUGFIX_VERSION(PACKED) ((PACKED) & 0xff)
#define AGDK_STRING_VERSION(MAJOR, MINOR, BUGFIX, GIT) \
#MAJOR "." #MINOR "." #BUGFIX "." #GIT
File diff suppressed because it is too large Load Diff
@@ -36,12 +36,22 @@
#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}
*/
@@ -115,199 +125,6 @@ typedef struct GameActivity {
const char* obbPath;
} GameActivity;
/**
* The maximum number of axes supported in an Android MotionEvent.
* See https://developer.android.com/ndk/reference/group/input.
*/
#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48
/**
* \brief Describe information about a pointer, found in a
* GameActivityMotionEvent.
*
* You can read values directly from this structure, or use helper functions
* (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and
* `GameActivityPointerAxes_getAxisValue`).
*
* The X axis and Y axis are enabled by default but any other axis that you want
* to read **must** be enabled first, using
* `GameActivityPointerAxes_enableAxis`.
*
* \see GameActivityMotionEvent
*/
typedef struct GameActivityPointerAxes {
int32_t id;
int32_t toolType;
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
float rawX;
float rawY;
} GameActivityPointerAxes;
typedef struct GameActivityHistoricalPointerAxes {
int64_t eventTime;
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
} GameActivityHistoricalPointerAxes;
/** \brief Get the current X coordinate of the pointer. */
inline float GameActivityPointerAxes_getX(
const GameActivityPointerAxes* pointerInfo) {
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X];
}
/** \brief Get the current Y coordinate of the pointer. */
inline float GameActivityPointerAxes_getY(
const GameActivityPointerAxes* pointerInfo) {
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y];
}
/**
* \brief Enable the specified axis, so that its value is reported in the
* GameActivityPointerAxes structures stored in a motion event.
*
* You must enable any axis that you want to read, apart from
* `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by
* default.
*
* If the axis index is out of range, nothing is done.
*/
void GameActivityPointerAxes_enableAxis(int32_t axis);
/**
* \brief Disable the specified axis. Its value won't be reported in the
* GameActivityPointerAxes structures stored in a motion event anymore.
*
* Apart from X and Y, any axis that you want to read **must** be enabled first,
* using `GameActivityPointerAxes_enableAxis`.
*
* If the axis index is out of range, nothing is done.
*/
void GameActivityPointerAxes_disableAxis(int32_t axis);
/**
* \brief Enable the specified axis, so that its value is reported in the
* GameActivityHistoricalPointerAxes structures associated with a motion event.
*
* You must enable any axis that you want to read (no axes are enabled by
* default).
*
* If the axis index is out of range, nothing is done.
*/
void GameActivityHistoricalPointerAxes_enableAxis(int32_t axis);
/**
* \brief Disable the specified axis. Its value won't be reported in the
* GameActivityHistoricalPointerAxes structures associated with motion events
* anymore.
*
* If the axis index is out of range, nothing is done.
*/
void GameActivityHistoricalPointerAxes_disableAxis(int32_t axis);
/**
* \brief Get the value of the requested axis.
*
* Apart from X and Y, any axis that you want to read **must** be enabled first,
* using `GameActivityPointerAxes_enableAxis`.
*
* Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`,
* `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...)
* in https://developer.android.com/ndk/reference/group/input.
*
* @param pointerInfo The structure containing information about the pointer,
* obtained from GameActivityMotionEvent.
* @param axis The axis to get the value from
* @return The value of the axis, or 0 if the axis is invalid or was not
* enabled.
*/
inline float GameActivityPointerAxes_getAxisValue(
GameActivityPointerAxes* pointerInfo, int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return 0;
}
return pointerInfo->axisValues[axis];
}
/**
* The maximum number of pointers returned inside a motion event.
*/
#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE)
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \
GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE
#else
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8
#endif
/**
* The maximum number of historic samples associated with a single motion event.
*/
#if (defined GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT_OVERRIDE)
#define GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT \
GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT_OVERRIDE
#else
#define GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT 8
#endif
/**
* \brief Describe a motion event that happened on the GameActivity SurfaceView.
*
* This is 1:1 mapping to the information contained in a Java `MotionEvent`
* (see https://developer.android.com/reference/android/view/MotionEvent).
*/
typedef struct GameActivityMotionEvent {
int32_t deviceId;
int32_t source;
int32_t action;
int64_t eventTime;
int64_t downTime;
int32_t flags;
int32_t metaState;
int32_t actionButton;
int32_t buttonState;
int32_t classification;
int32_t edgeFlags;
uint32_t pointerCount;
GameActivityPointerAxes
pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
float precisionX;
float precisionY;
int16_t historicalStart;
// Note the actual buffer of historical data has a length of
// pointerCount * historicalCount, since the historical axis
// data is per-pointer.
int16_t historicalCount;
} GameActivityMotionEvent;
/**
* \brief Describe a key event that happened on the GameActivity SurfaceView.
*
* This is 1:1 mapping to the information contained in a Java `KeyEvent`
* (see https://developer.android.com/reference/android/view/KeyEvent).
*/
typedef struct GameActivityKeyEvent {
int32_t deviceId;
int32_t source;
int32_t action;
int64_t eventTime;
int64_t downTime;
int32_t flags;
int32_t metaState;
int32_t modifiers;
int32_t repeatCount;
int32_t keyCode;
int32_t scanCode;
} GameActivityKeyEvent;
/**
* A function the user should call from their callback with the data, its length
* and the library- supplied context.
@@ -424,9 +241,7 @@ typedef struct GameActivityCallbacks {
* only valid during the callback.
*/
bool (*onTouchEvent)(GameActivity* activity,
const GameActivityMotionEvent* event,
const GameActivityHistoricalPointerAxes* historical,
int historicalLen);
const GameActivityMotionEvent* event);
/**
* Callback called for every key down event on the GameActivity SurfaceView.
@@ -456,37 +271,14 @@ typedef struct GameActivityCallbacks {
* Call GameActivity_getWindowInsets to retrieve the insets themselves.
*/
void (*onWindowInsetsChanged)(GameActivity* activity);
/**
* Callback called when the rectangle in the window where the content
* should be placed has changed.
*/
void (*onContentRectChanged)(GameActivity *activity, const ARect *rect);
} GameActivityCallbacks;
/**
* \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`.
*
* This is done automatically by the GameActivity: see `onTouchEvent` to set
* a callback to consume the received events.
* This function can be used if you re-implement events handling in your own
* activity. On return, the out_event->historicalStart will be zero, and should
* be updated to index into whatever buffer out_historical is copied.
* On return the length of out_historical is
* (out_event->pointerCount x out_event->historicalCount) and is in a
* pointer-major order (i.e. all axis for a pointer are contiguous)
* Ownership of out_event is maintained by the caller.
*/
int GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
GameActivityMotionEvent* out_event,
GameActivityHistoricalPointerAxes *out_historical);
/**
* \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`.
*
* This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown`
* to set a callback to consume the received events.
* This function can be used if you re-implement events handling in your own
* activity.
* Ownership of out_event is maintained by the caller.
*/
void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent,
GameActivityKeyEvent* out_event);
/**
* This is the function that must be in the native code to instantiate the
* application's native activity. It is called with the activity instance (see
@@ -504,7 +296,7 @@ typedef void GameActivity_createFunc(GameActivity* activity, void* savedState,
* "android.app.func_name" string meta-data in your manifest to use a different
* function.
*/
extern GameActivity_createFunc GameActivity_onCreate;
extern GameActivity_createFunc GameActivity_onCreate_C;
/**
* Finish the given activity. Its finish() method will be called, causing it
@@ -811,6 +603,30 @@ void GameActivity_getWindowInsets(GameActivity* activity,
void GameActivity_setImeEditorInfo(GameActivity* activity, int inputType,
int actionId, int imeOptions);
/**
* These are getters for Configuration class members. They may be called from
* any thread.
*/
int GameActivity_getOrientation(GameActivity* activity);
int GameActivity_getColorMode(GameActivity* activity);
int GameActivity_getDensityDpi(GameActivity* activity);
float GameActivity_getFontScale(GameActivity* activity);
int GameActivity_getFontWeightAdjustment(GameActivity* activity);
int GameActivity_getHardKeyboardHidden(GameActivity* activity);
int GameActivity_getKeyboard(GameActivity* activity);
int GameActivity_getKeyboardHidden(GameActivity* activity);
int GameActivity_getMcc(GameActivity* activity);
int GameActivity_getMnc(GameActivity* activity);
int GameActivity_getNavigation(GameActivity* activity);
int GameActivity_getNavigationHidden(GameActivity* activity);
int GameActivity_getOrientation(GameActivity* activity);
int GameActivity_getScreenHeightDp(GameActivity* activity);
int GameActivity_getScreenLayout(GameActivity* activity);
int GameActivity_getScreenWidthDp(GameActivity* activity);
int GameActivity_getSmallestScreenWidthDp(GameActivity* activity);
int GameActivity_getTouchscreen(GameActivity* activity);
int GameActivity_getUIMode(GameActivity* activity);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,414 @@
/*
* 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)
};
}
@@ -0,0 +1,336 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @addtogroup GameActivity Game Activity Events
* The interface to use Game Activity Events.
* @{
*/
/**
* @file GameActivityEvents.h
*/
#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
#define ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
#include <android/input.h>
#include <jni.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* The maximum number of axes supported in an Android MotionEvent.
* See https://developer.android.com/ndk/reference/group/input.
*/
#define GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT 48
/**
* \brief Describe information about a pointer, found in a
* GameActivityMotionEvent.
*
* You can read values directly from this structure, or use helper functions
* (`GameActivityPointerAxes_getX`, `GameActivityPointerAxes_getY` and
* `GameActivityPointerAxes_getAxisValue`).
*
* The X axis and Y axis are enabled by default but any other axis that you want
* to read **must** be enabled first, using
* `GameActivityPointerAxes_enableAxis`.
*
* \see GameActivityMotionEvent
*/
typedef struct GameActivityPointerAxes {
int32_t id;
int32_t toolType;
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
float rawX;
float rawY;
} GameActivityPointerAxes;
/** \brief Get the toolType of the pointer. */
inline int32_t GameActivityPointerAxes_getToolType(
const GameActivityPointerAxes* pointerInfo) {
return pointerInfo->toolType;
}
/** \brief Get the current X coordinate of the pointer. */
inline float GameActivityPointerAxes_getX(
const GameActivityPointerAxes* pointerInfo) {
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_X];
}
/** \brief Get the current Y coordinate of the pointer. */
inline float GameActivityPointerAxes_getY(
const GameActivityPointerAxes* pointerInfo) {
return pointerInfo->axisValues[AMOTION_EVENT_AXIS_Y];
}
/**
* \brief Enable the specified axis, so that its value is reported in the
* GameActivityPointerAxes structures stored in a motion event.
*
* You must enable any axis that you want to read, apart from
* `AMOTION_EVENT_AXIS_X` and `AMOTION_EVENT_AXIS_Y` that are enabled by
* default.
*
* If the axis index is out of range, nothing is done.
*/
void GameActivityPointerAxes_enableAxis(int32_t axis);
/**
* \brief Disable the specified axis. Its value won't be reported in the
* GameActivityPointerAxes structures stored in a motion event anymore.
*
* Apart from X and Y, any axis that you want to read **must** be enabled first,
* using `GameActivityPointerAxes_enableAxis`.
*
* If the axis index is out of range, nothing is done.
*/
void GameActivityPointerAxes_disableAxis(int32_t axis);
/**
* \brief Get the value of the requested axis.
*
* Apart from X and Y, any axis that you want to read **must** be enabled first,
* using `GameActivityPointerAxes_enableAxis`.
*
* Find the valid enums for the axis (`AMOTION_EVENT_AXIS_X`,
* `AMOTION_EVENT_AXIS_Y`, `AMOTION_EVENT_AXIS_PRESSURE`...)
* in https://developer.android.com/ndk/reference/group/input.
*
* @param pointerInfo The structure containing information about the pointer,
* obtained from GameActivityMotionEvent.
* @param axis The axis to get the value from
* @return The value of the axis, or 0 if the axis is invalid or was not
* enabled.
*/
float GameActivityPointerAxes_getAxisValue(
const GameActivityPointerAxes* pointerInfo, int32_t axis);
inline float GameActivityPointerAxes_getPressure(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_PRESSURE);
}
inline float GameActivityPointerAxes_getSize(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_SIZE);
}
inline float GameActivityPointerAxes_getTouchMajor(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_TOUCH_MAJOR);
}
inline float GameActivityPointerAxes_getTouchMinor(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_TOUCH_MINOR);
}
inline float GameActivityPointerAxes_getToolMajor(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_TOOL_MAJOR);
}
inline float GameActivityPointerAxes_getToolMinor(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_TOOL_MINOR);
}
inline float GameActivityPointerAxes_getOrientation(
const GameActivityPointerAxes* pointerInfo) {
return GameActivityPointerAxes_getAxisValue(pointerInfo,
AMOTION_EVENT_AXIS_ORIENTATION);
}
/**
* The maximum number of pointers returned inside a motion event.
*/
#if (defined GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE)
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT \
GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT_OVERRIDE
#else
#define GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT 8
#endif
/**
* \brief Describe a motion event that happened on the GameActivity SurfaceView.
*
* This is 1:1 mapping to the information contained in a Java `MotionEvent`
* (see https://developer.android.com/reference/android/view/MotionEvent).
*/
typedef struct GameActivityMotionEvent {
int32_t deviceId;
int32_t source;
int32_t action;
int64_t eventTime;
int64_t downTime;
int32_t flags;
int32_t metaState;
int32_t actionButton;
int32_t buttonState;
int32_t classification;
int32_t edgeFlags;
uint32_t pointerCount;
GameActivityPointerAxes
pointers[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT];
int historySize;
int64_t* historicalEventTimesMillis;
int64_t* historicalEventTimesNanos;
float* historicalAxisValues;
float precisionX;
float precisionY;
} GameActivityMotionEvent;
float GameActivityMotionEvent_getHistoricalAxisValue(
const GameActivityMotionEvent* event, int axis, int pointerIndex,
int historyPos);
inline int GameActivityMotionEvent_getHistorySize(
const GameActivityMotionEvent* event) {
return event->historySize;
}
inline float GameActivityMotionEvent_getHistoricalX(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_X, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalY(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_Y, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalPressure(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_PRESSURE, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalSize(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_SIZE, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalTouchMajor(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_TOUCH_MAJOR, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalTouchMinor(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_TOUCH_MINOR, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalToolMajor(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_TOOL_MAJOR, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalToolMinor(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_TOOL_MINOR, pointerIndex, historyPos);
}
inline float GameActivityMotionEvent_getHistoricalOrientation(
const GameActivityMotionEvent* event, int pointerIndex, int historyPos) {
return GameActivityMotionEvent_getHistoricalAxisValue(
event, AMOTION_EVENT_AXIS_ORIENTATION, pointerIndex, historyPos);
}
/** \brief Handle the freeing of the GameActivityMotionEvent struct. */
void GameActivityMotionEvent_destroy(GameActivityMotionEvent* c_event);
/**
* \brief Convert a Java `MotionEvent` to a `GameActivityMotionEvent`.
*
* This is done automatically by the GameActivity: see `onTouchEvent` to set
* a callback to consume the received events.
* This function can be used if you re-implement events handling in your own
* activity.
* Ownership of out_event is maintained by the caller.
*/
void GameActivityMotionEvent_fromJava(JNIEnv* env, jobject motionEvent,
GameActivityMotionEvent* out_event);
/**
* \brief Describe a key event that happened on the GameActivity SurfaceView.
*
* This is 1:1 mapping to the information contained in a Java `KeyEvent`
* (see https://developer.android.com/reference/android/view/KeyEvent).
* The only exception is the event times, which are reported as
* nanoseconds in this struct.
*/
typedef struct GameActivityKeyEvent {
int32_t deviceId;
int32_t source;
int32_t action;
int64_t eventTime;
int64_t downTime;
int32_t flags;
int32_t metaState;
int32_t modifiers;
int32_t repeatCount;
int32_t keyCode;
int32_t scanCode;
//int32_t unicodeChar;
} GameActivityKeyEvent;
/**
* \brief Convert a Java `KeyEvent` to a `GameActivityKeyEvent`.
*
* This is done automatically by the GameActivity: see `onKeyUp` and `onKeyDown`
* to set a callback to consume the received events.
* This function can be used if you re-implement events handling in your own
* activity.
* Ownership of out_event is maintained by the caller.
*/
void GameActivityKeyEvent_fromJava(JNIEnv* env, jobject motionEvent,
GameActivityKeyEvent* out_event);
#ifdef __cplusplus
}
#endif
/** @} */
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_EVENTS_H
@@ -0,0 +1,109 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_
#define ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_
#define LOG_TAG "GameActivity"
#include <android/log.h>
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);
#ifdef NDEBUG
#define ALOGV(...)
#else
#define ALOGV(...) \
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
#endif
/* Returns 2nd arg. Used to substitute default value if caller's vararg list
* is empty.
*/
#define __android_second(first, second, ...) second
/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise
* returns nothing.
*/
#define __android_rest(first, ...) , ##__VA_ARGS__
#define android_printAssert(cond, tag, fmt...) \
__android_log_assert(cond, tag, \
__android_second(0, ##fmt, NULL) __android_rest(fmt))
#define CONDITION(cond) (__builtin_expect((cond) != 0, 0))
#ifndef LOG_ALWAYS_FATAL_IF
#define LOG_ALWAYS_FATAL_IF(cond, ...) \
((CONDITION(cond)) \
? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \
: (void)0)
#endif
#ifndef LOG_ALWAYS_FATAL
#define LOG_ALWAYS_FATAL(...) \
(((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
#endif
/*
* Simplified macro to send a warning system log message using current LOG_TAG.
*/
#ifndef SLOGW
#define SLOGW(...) \
((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
#endif
#ifndef SLOGW_IF
#define SLOGW_IF(cond, ...) \
((__predict_false(cond)) \
? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \
: (void)0)
#endif
/*
* Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that
* are stripped out of release builds.
*/
#if LOG_NDEBUG
#ifndef LOG_FATAL_IF
#define LOG_FATAL_IF(cond, ...) ((void)0)
#endif
#ifndef LOG_FATAL
#define LOG_FATAL(...) ((void)0)
#endif
#else
#ifndef LOG_FATAL_IF
#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__)
#endif
#ifndef LOG_FATAL
#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
#endif
#endif
/*
* Assertion that generates a log message when the assertion fails.
* Stripped out of release builds. Uses the current LOG_TAG.
*/
#ifndef ALOG_ASSERT
#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__)
#endif
#define LOG_TRACE(...)
#endif // ANDROID_GAME_SDK_GAME_ACTIVITY_LOG_H_
@@ -17,6 +17,7 @@
#include "android_native_app_glue.h"
#include <android/log.h>
#include <assert.h>
#include <errno.h>
#include <jni.h>
#include <stdlib.h>
@@ -24,6 +25,9 @@
#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(...) \
@@ -189,6 +193,7 @@ static void process_cmd(struct android_app* app,
// This is run on a separate thread (i.e: not the main thread).
static void* android_app_entry(void* param) {
struct android_app* android_app = (struct android_app*)param;
int input_buf_idx = 0;
LOGV("android_app_entry called");
android_app->config = AConfiguration_new();
@@ -201,6 +206,19 @@ static void* android_app_entry(void* param) {
print_cur_config(android_app);
/* initialize event buffers */
for (input_buf_idx = 0; input_buf_idx < NATIVE_APP_GLUE_MAX_INPUT_BUFFERS; input_buf_idx++) {
struct android_input_buffer *buf = &android_app->inputBuffers[input_buf_idx];
buf->motionEventsBufferSize = NATIVE_APP_GLUE_MOTION_EVENTS_DEFAULT_BUF_SIZE;
buf->motionEvents = (GameActivityMotionEvent *) malloc(sizeof(GameActivityMotionEvent) *
buf->motionEventsBufferSize);
buf->keyEventsBufferSize = NATIVE_APP_GLUE_KEY_EVENTS_DEFAULT_BUF_SIZE;
buf->keyEvents = (GameActivityKeyEvent *) malloc(sizeof(GameActivityKeyEvent) *
buf->keyEventsBufferSize);
}
android_app->cmdPollSource.id = LOOPER_ID_MAIN;
android_app->cmdPollSource.app = android_app;
android_app->cmdPollSource.process = process_cmd;
@@ -308,6 +326,15 @@ 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);
}
@@ -324,21 +351,46 @@ static void android_app_set_window(struct android_app* android_app,
static void android_app_set_activity_state(struct android_app* android_app,
int8_t cmd) {
pthread_mutex_lock(&android_app->mutex);
android_app_write_cmd(android_app, cmd);
while (android_app->activityState != cmd) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (!android_app->destroyed) {
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);
@@ -372,6 +424,16 @@ static void onSaveInstanceState(GameActivity* activity,
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
// NB: we have to consider that the native thread could have already
// (gracefully) exit (setting android_app->destroyed) and so we need
// to be careful to avoid a deadlock waiting for a thread that's
// already exit.
if (android_app->destroyed) {
pthread_mutex_unlock(&android_app->mutex);
return;
}
android_app->stateSaved = 0;
android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
while (!android_app->stateSaved) {
@@ -451,7 +513,6 @@ void android_app_set_motion_event_filter(struct android_app* app,
bool android_app_input_available_wake_up(struct android_app* app) {
pthread_mutex_lock(&app->mutex);
// TODO: use atomic ops for this
bool available = app->inputAvailableWakeUp;
app->inputAvailableWakeUp = false;
pthread_mutex_unlock(&app->mutex);
@@ -466,7 +527,6 @@ static void notifyInput(struct android_app* android_app) {
}
if (android_app->looper != NULL) {
LOGV("Input Notify: %p", android_app);
// for the app thread to know why it received the wake() up
android_app->inputAvailableWakeUp = true;
android_app->inputSwapPending = true;
@@ -475,12 +535,19 @@ static void notifyInput(struct android_app* android_app) {
}
static bool onTouchEvent(GameActivity* activity,
const GameActivityMotionEvent* event,
const GameActivityHistoricalPointerAxes* historical,
int historicalLen) {
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);
@@ -491,35 +558,22 @@ static bool onTouchEvent(GameActivity* activity,
&android_app->inputBuffers[android_app->currentInputBuffer];
// Add to the list of active motion events
if (inputBuffer->motionEventsCount <
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS) {
int new_ix = inputBuffer->motionEventsCount;
memcpy(&inputBuffer->motionEvents[new_ix], event,
sizeof(GameActivityMotionEvent));
++inputBuffer->motionEventsCount;
if (inputBuffer->motionEventsCount >= inputBuffer->motionEventsBufferSize) {
inputBuffer->motionEventsBufferSize *= 2;
inputBuffer->motionEvents = (GameActivityMotionEvent *) realloc(inputBuffer->motionEvents,
sizeof(GameActivityMotionEvent) * inputBuffer->motionEventsBufferSize);
if (inputBuffer->historicalSamplesCount + historicalLen <=
NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES) {
int start_ix = inputBuffer->historicalSamplesCount;
memcpy(&inputBuffer->historicalAxisSamples[start_ix], historical,
sizeof(historical[0]) * historicalLen);
inputBuffer->historicalSamplesCount += event->historicalCount;
inputBuffer->motionEvents[new_ix].historicalStart = start_ix;
inputBuffer->motionEvents[new_ix].historicalCount = historicalLen;
} else {
inputBuffer->motionEvents[new_ix].historicalCount = 0;
if (inputBuffer->motionEvents == NULL) {
LOGE("onTouchEvent: out of memory");
abort();
}
notifyInput(android_app);
} else {
LOGW_ONCE("Motion event will be dropped because the number of unconsumed motion"
" events exceeded NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS (%d). Consider setting"
" NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE to a larger value",
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS);
}
int new_ix = inputBuffer->motionEventsCount;
memcpy(&inputBuffer->motionEvents[new_ix], event, sizeof(GameActivityMotionEvent));
++inputBuffer->motionEventsCount;
notifyInput(android_app);
pthread_mutex_unlock(&android_app->mutex);
return true;
}
@@ -549,7 +603,15 @@ struct android_input_buffer* android_app_swap_input_buffers(
}
void android_app_clear_motion_events(struct android_input_buffer* inputBuffer) {
inputBuffer->motionEventsCount = 0;
// We do not need to lock here if the inputBuffer has already been swapped
// as is handled by the game loop thread
while (inputBuffer->motionEventsCount > 0) {
GameActivityMotionEvent_destroy(
&inputBuffer->motionEvents[inputBuffer->motionEventsCount - 1]);
inputBuffer->motionEventsCount--;
}
assert(inputBuffer->motionEventsCount == 0);
}
void android_app_set_key_event_filter(struct android_app* app,
@@ -563,6 +625,15 @@ 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);
@@ -573,20 +644,22 @@ static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
&android_app->inputBuffers[android_app->currentInputBuffer];
// Add to the list of active key down events
if (inputBuffer->keyEventsCount < NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS) {
int new_ix = inputBuffer->keyEventsCount;
memcpy(&inputBuffer->keyEvents[new_ix], event,
sizeof(GameActivityKeyEvent));
++inputBuffer->keyEventsCount;
if (inputBuffer->keyEventsCount >= inputBuffer->keyEventsBufferSize) {
inputBuffer->keyEventsBufferSize = inputBuffer->keyEventsBufferSize * 2;
inputBuffer->keyEvents = (GameActivityKeyEvent *) realloc(inputBuffer->keyEvents,
sizeof(GameActivityKeyEvent) * inputBuffer->keyEventsBufferSize);
notifyInput(android_app);
} else {
LOGW_ONCE("Key event will be dropped because the number of unconsumed key events exceeded"
" NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS (%d). Consider setting"
" NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE to a larger value",
NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS);
if (inputBuffer->keyEvents == NULL) {
LOGE("onKey: out of memory");
abort();
}
}
int new_ix = inputBuffer->keyEventsCount;
memcpy(&inputBuffer->keyEvents[new_ix], event, sizeof(GameActivityKeyEvent));
++inputBuffer->keyEventsCount;
notifyInput(android_app);
pthread_mutex_unlock(&android_app->mutex);
return true;
}
@@ -599,8 +672,10 @@ static void onTextInputEvent(GameActivity* activity,
const GameTextInputState* state) {
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
android_app->textInputState = 1;
if (!android_app->destroyed) {
android_app->textInputState = 1;
notifyInput(android_app);
}
pthread_mutex_unlock(&android_app->mutex);
}
@@ -609,6 +684,23 @@ static void onWindowInsetsChanged(GameActivity* activity) {
android_app_write_cmd(ToApp(activity), APP_CMD_WINDOW_INSETS_CHANGED);
}
static void onContentRectChanged(GameActivity* activity, const ARect *rect) {
LOGV("ContentRectChanged: %p -- (%d %d) (%d %d)", activity, rect->left, rect->top,
rect->right, rect->bottom);
struct android_app* android_app = ToApp(activity);
pthread_mutex_lock(&android_app->mutex);
android_app->contentRect = *rect;
android_app_write_cmd(android_app, APP_CMD_CONTENT_RECT_CHANGED);
pthread_mutex_unlock(&android_app->mutex);
}
// XXX: This symbol is renamed with a _C suffix and then re-exported from
// Rust because Rust/Cargo don't give us a way to directly export symbols
// from C/C++ code: https://github.com/rust-lang/rfcs/issues/2771
//
JNIEXPORT
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
size_t savedStateSize) {
@@ -632,6 +724,7 @@ void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
onNativeWindowRedrawNeeded;
activity->callbacks->onNativeWindowResized = onNativeWindowResized;
activity->callbacks->onWindowInsetsChanged = onWindowInsetsChanged;
activity->callbacks->onContentRectChanged = onContentRectChanged;
LOGV("Callbacks set: %p", activity->callbacks);
activity->instance =
@@ -30,27 +30,6 @@
#include "game-activity/GameActivity.h"
#if (defined NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE)
#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS \
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE
#else
#define NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS 16
#endif
#if (defined NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES_OVERRIDE)
#define NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES \
NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES_OVERRIDE
#else
#define NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES 64
#endif
#if (defined NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE)
#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS \
NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS_OVERRIDE
#else
#define NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS 4
#endif
#ifdef __cplusplus
extern "C" {
#endif
@@ -126,10 +105,10 @@ struct android_poll_source {
struct android_input_buffer {
/**
* Pointer to a read-only array of pointers to GameActivityMotionEvent.
* Pointer to a read-only array of GameActivityMotionEvent.
* Only the first motionEventsCount events are valid.
*/
GameActivityMotionEvent motionEvents[NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS];
GameActivityMotionEvent *motionEvents;
/**
* The number of valid motion events in `motionEvents`.
@@ -137,36 +116,25 @@ struct android_input_buffer {
uint64_t motionEventsCount;
/**
* Pointer to a read-only array of pointers to GameActivityHistoricalPointerAxes.
*
* Only the first historicalSamplesCount samples are valid.
* Refer to event->historicalStart, event->pointerCount and event->historicalCount
* to access the specific samples that relate to an event.
*
* Each slice of samples for one event has a length of
* (event->pointerCount and event->historicalCount) and is in pointer-major
* order so the historic samples for each pointer are contiguous.
* E.g. you would access historic sample index 3 for pointer 2 of an event with:
*
* historicalAxisSamples[event->historicalStart + (event->historicalCount * 2) + 3];
* The size of the `motionEvents` buffer.
*/
GameActivityHistoricalPointerAxes historicalAxisSamples[NATIVE_APP_GLUE_MAX_HISTORICAL_POINTER_SAMPLES];
uint64_t motionEventsBufferSize;
/**
* The number of valid historical samples in `historicalAxisSamples`.
*/
uint64_t historicalSamplesCount;
/**
* Pointer to a read-only array of pointers to GameActivityKeyEvent.
* Pointer to a read-only array of GameActivityKeyEvent.
* Only the first keyEventsCount events are valid.
*/
GameActivityKeyEvent keyEvents[NATIVE_APP_GLUE_MAX_NUM_KEY_EVENTS];
GameActivityKeyEvent *keyEvents;
/**
* The number of valid "Key" events in `keyEvents`.
*/
uint64_t keyEventsCount;
/**
* The size of the `keyEvents` buffer.
*/
uint64_t keyEventsBufferSize;
};
/**
@@ -532,8 +500,6 @@ void android_app_set_motion_event_filter(struct android_app* app,
*/
bool android_app_input_available_wake_up(struct android_app* app);
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
size_t savedStateSize);
#ifdef __cplusplus
}
#endif
View File
+13
View File
@@ -48,6 +48,7 @@ struct GameTextInput {
void processEvent(jobject textInputEvent);
void showIme(uint32_t flags);
void hideIme(uint32_t flags);
void restartInput();
void setEventCallback(GameTextInputEventCallback callback, void *context);
jobject stateToJava(const GameTextInputState &state) const;
void stateFromJava(jobject textInputEvent,
@@ -73,6 +74,7 @@ struct GameTextInput {
jobject inputConnection_ = nullptr;
jmethodID inputConnectionSetStateMethod_;
jmethodID setSoftKeyboardActiveMethod_;
jmethodID restartInputMethod_;
void (*eventCallback_)(void *context,
const struct GameTextInputState *state) = nullptr;
void *eventCallbackContext_ = nullptr;
@@ -164,6 +166,10 @@ 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) {
@@ -199,6 +205,8 @@ GameTextInput::GameTextInput(JNIEnv *env, uint32_t max_string_size)
"(Lcom/google/androidgamesdk/gametextinput/State;)V");
setSoftKeyboardActiveMethod_ = env_->GetMethodID(
inputConnectionClass_, "setSoftKeyboardActive", "(ZI)V");
restartInputMethod_ =
env_->GetMethodID(inputConnectionClass_, "restartInput", "()V");
stateClassInfo_.text =
env_->GetFieldID(stateJavaClass_, "text", "Ljava/lang/String;");
@@ -307,6 +315,11 @@ void GameTextInput::hideIme(uint32_t flags) {
flags);
}
void GameTextInput::restartInput() {
if (inputConnection_ == nullptr) return;
env_->CallVoidMethod(inputConnection_, restartInputMethod_, false);
}
jobject GameTextInput::stateToJava(const GameTextInputState &state) const {
static jmethodID constructor = nullptr;
if (constructor == nullptr) {
+15
View File
@@ -26,12 +26,21 @@
#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
@@ -173,6 +182,12 @@ enum HideImeFlags {
*/
void GameTextInput_hideIme(GameTextInput *input, uint32_t flags);
/**
* Restarts the input method. Calls InputMethodManager.restartInput().
* @param input A valid GameTextInput library handle.
*/
void GameTextInput_restartInput(GameTextInput *input);
/**
* Call a callback with the current GameTextInput state, which may have been
* modified by changes in the IME and calls to GameTextInput_setState. We use a
+2
View File
@@ -1,5 +1,7 @@
#!/bin/sh
# First install bindgen-cli via `cargo install bindgen-cli`
if test -z "${ANDROID_NDK_ROOT}"; then
export ANDROID_NDK_ROOT=${ANDROID_NDK_HOME}
fi
+58
View File
@@ -0,0 +1,58 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Operation only supported from the android_main() thread: {0}")]
NonMainThread(String),
#[error("Java VM or JNI error, including Java exceptions")]
JavaError(String),
#[error("Input unavailable")]
InputUnavailable,
}
pub type Result<T> = std::result::Result<T, AppError>;
// XXX: we don't want to expose jni-rs in the public API
// so we have an internal error type that we can generally
// use in the backends and then we can strip the error
// in the frontend of the API.
//
// This way we avoid exposing a public trait implementation for
// `From<jni::errors::Error>`
#[derive(Error, Debug)]
pub(crate) enum InternalAppError {
#[error("A JNI error")]
JniError(jni::errors::JniError),
#[error("A Java Exception was thrown via a JNI method call")]
JniException(String),
#[error("A Java VM error")]
JvmError(jni::errors::Error),
#[error("Input unavailable")]
InputUnavailable,
}
pub(crate) type InternalResult<T> = std::result::Result<T, InternalAppError>;
impl From<jni::errors::Error> for InternalAppError {
fn from(value: jni::errors::Error) -> Self {
InternalAppError::JvmError(value)
}
}
impl From<jni::errors::JniError> for InternalAppError {
fn from(value: jni::errors::JniError) -> Self {
InternalAppError::JniError(value)
}
}
impl From<InternalAppError> for AppError {
fn from(value: InternalAppError) -> Self {
match value {
InternalAppError::JniError(err) => AppError::JavaError(err.to_string()),
InternalAppError::JniException(msg) => AppError::JavaError(msg),
InternalAppError::JvmError(err) => AppError::JavaError(err.to_string()),
InternalAppError::InputUnavailable => AppError::InputUnavailable,
}
}
}
+1 -1
View File
@@ -13,7 +13,7 @@
#![allow(dead_code)]
use jni_sys::*;
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t, size_t};
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t};
use ndk_sys::{AAssetManager, AConfiguration, ALooper, ALooper_callbackFunc, ANativeWindow, ARect};
#[cfg(all(
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
File diff suppressed because it is too large Load Diff
+378 -69
View File
@@ -1,5 +1,6 @@
#![cfg(feature = "game-activity")]
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
@@ -8,7 +9,8 @@ use std::ops::Deref;
use std::os::unix::prelude::*;
use std::panic::catch_unwind;
use std::ptr::NonNull;
use std::sync::{Arc, RwLock};
use std::sync::Weak;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use std::{ptr, thread};
@@ -24,6 +26,9 @@ use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;
use crate::error::InternalResult;
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
use crate::jni_utils::{self, CloneJavaVM};
use crate::util::{abort_on_panic, android_log, log_panic};
use crate::{
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
@@ -32,7 +37,8 @@ use crate::{
mod ffi;
pub mod input;
use input::{Axis, InputEvent, KeyEvent, MotionEvent};
use crate::input::{TextInputState, TextSpan};
use input::{InputEvent, KeyEvent, MotionEvent};
// The only time it's safe to update the android_app->savedState pointer is
// while handling a SaveState event, so this API is only exposed for those
@@ -89,7 +95,7 @@ impl<'a> StateLoader<'a> {
if !(*app_ptr).savedState.is_null() && (*app_ptr).savedStateSize > 0 {
let buf: &mut [u8] = std::slice::from_raw_parts_mut(
(*app_ptr).savedState.cast(),
(*app_ptr).savedStateSize as usize,
(*app_ptr).savedStateSize,
);
let state = buf.to_vec();
Some(state)
@@ -119,7 +125,16 @@ impl AndroidAppWaker {
}
impl AndroidApp {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>) -> Self {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>, jvm: CloneJavaVM) -> Self {
let mut env = jvm.get_env().unwrap(); // We attach to the thread before creating the AndroidApp
let key_map_binding = match KeyCharacterMapBinding::new(&mut env) {
Ok(b) => b,
Err(err) => {
panic!("Failed to create KeyCharacterMap JNI bindings: {err:?}");
}
};
// Note: we don't use from_ptr since we don't own the android_app.config
// and need to keep in mind that the Drop handler is going to call
// AConfiguration_delete()
@@ -127,15 +142,19 @@ impl AndroidApp {
Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm,
native_app: NativeAppGlue { ptr },
config: ConfigurationRef::new(config),
native_window: Default::default(),
key_map_binding: Arc::new(key_map_binding),
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
}
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
struct NativeAppGlue {
ptr: NonNull<ffi::android_app>,
}
@@ -149,11 +168,112 @@ impl Deref for NativeAppGlue {
unsafe impl Send for NativeAppGlue {}
unsafe impl Sync for NativeAppGlue {}
impl NativeAppGlue {
// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
unsafe {
let activity = (*self.as_ptr()).activity;
let mut out_state = TextInputState {
text: String::new(),
selection: TextSpan { start: 0, end: 0 },
compose_region: None,
};
let out_ptr = &mut out_state as *mut TextInputState;
let app_ptr = self.as_ptr();
(*app_ptr).textInputState = 0;
// NEON WARNING:
//
// It's not clearly documented but the GameActivity API over the
// GameTextInput library directly exposes _modified_ UTF8 text
// from Java so we need to be careful to convert text to and
// from UTF8
//
// GameTextInput also uses a pre-allocated, fixed-sized buffer for
// the current text state and has shared `currentState_` that
// appears to have no lock to guard access from multiple threads.
//
// There's also no locking at the GameActivity level, so I'm fairly
// certain that `GameActivity_getTextInputState` isn't thread
// safe: https://issuetracker.google.com/issues/294112477
//
// Overall this is all quite gnarly - and probably a good reminder
// of why we want to use Rust instead of C/C++.
ffi::GameActivity_getTextInputState(
activity,
Some(AndroidAppInner::map_input_state_to_text_event_callback),
out_ptr.cast(),
);
out_state
}
}
// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
unsafe {
let activity = (*self.as_ptr()).activity;
let modified_utf8 = cesu8::to_java_cesu8(&state.text);
let text_length = modified_utf8.len() as i32;
let modified_utf8_bytes = modified_utf8.as_ptr();
let ffi_state = ffi::GameTextInputState {
text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
text_length,
selection: ffi::GameTextInputSpan {
start: state.selection.start as i32,
end: state.selection.end as i32,
},
composingRegion: match state.compose_region {
Some(span) => {
// The GameText subclass of InputConnection only has a special case for removing the
// compose region if `start == -1` but the docs for `setComposingRegion` imply that
// the region should effectively be removed if any empty region is given (unlike for the
// selection region, it's not meaningful to maintain an empty compose region)
//
// We aim for more consistent behaviour by normalizing any empty region into `(-1, -1)`
// to remove the compose region.
//
// NB `setComposingRegion` itself is documented to clamp start/end to the text bounds
// so apart from this special-case handling in GameText's implementation of
// `setComposingRegion` then there's nothing special about `(-1, -1)` - it's just an empty
// region that should get clamped to `(0, 0)` and then get removed.
if span.start == span.end {
ffi::GameTextInputSpan { start: -1, end: -1 }
} else {
ffi::GameTextInputSpan {
start: span.start as i32,
end: span.end as i32,
}
}
}
None => ffi::GameTextInputSpan { start: -1, end: -1 },
},
};
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
}
}
}
#[derive(Debug)]
pub struct AndroidAppInner {
pub(crate) jvm: CloneJavaVM,
native_app: NativeAppGlue,
config: ConfigurationRef,
native_window: RwLock<Option<NativeWindow>>,
/// Shared JNI bindings for the `KeyCharacterMap` class
key_map_binding: Arc<KeyCharacterMapBinding>,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
/// characters
key_maps: Mutex<HashMap<i32, KeyCharacterMap>>,
/// While an app is reading input events it holds an
/// InputReceiver reference which we track to ensure
/// we don't hand out more than one receiver at a time
input_receiver: Mutex<Option<Weak<InputReceiver>>>,
}
impl AndroidAppInner {
@@ -360,11 +480,80 @@ impl AndroidAppInner {
}
}
unsafe extern "C" fn map_input_state_to_text_event_callback(
context: *mut c_void,
state: *const ffi::GameTextInputState,
) {
// Java uses a modified UTF-8 format, which is a modified cesu8 format
let out_ptr: *mut TextInputState = context.cast();
let text_modified_utf8: *const u8 = (*state).text_UTF8.cast();
let text_modified_utf8 =
std::slice::from_raw_parts(text_modified_utf8, (*state).text_length as usize);
match cesu8::from_java_cesu8(text_modified_utf8) {
Ok(str) => {
let len = str.len();
(*out_ptr).text = String::from(str);
let selection_start = (*state).selection.start.clamp(0, len as i32 + 1);
let selection_end = (*state).selection.end.clamp(0, len as i32 + 1);
(*out_ptr).selection = TextSpan {
start: selection_start as usize,
end: selection_end as usize,
};
if (*state).composingRegion.start < 0 || (*state).composingRegion.end < 0 {
(*out_ptr).compose_region = None;
} else {
(*out_ptr).compose_region = Some(TextSpan {
start: (*state).composingRegion.start as usize,
end: (*state).composingRegion.end as usize,
});
}
}
Err(err) => {
log::error!("Invalid UTF8 text in TextEvent: {}", err);
}
}
}
// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
self.native_app.text_input_state()
}
// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
self.native_app.set_text_input_state(state);
}
pub(crate) fn device_key_character_map(
&self,
device_id: i32,
) -> InternalResult<KeyCharacterMap> {
let mut guard = self.key_maps.lock().unwrap();
let key_map = match guard.entry(device_id) {
std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
std::collections::hash_map::Entry::Vacant(vacant) => {
let character_map = jni_utils::device_key_character_map(
self.jvm.clone(),
self.key_map_binding.clone(),
device_id,
)?;
vacant.insert(character_map.clone());
character_map
}
};
Ok(key_map)
}
pub fn enable_motion_axis(&mut self, axis: Axis) {
let axis: u32 = axis.into();
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
}
pub fn disable_motion_axis(&mut self, axis: Axis) {
let axis: u32 = axis.into();
unsafe { ffi::GameActivityPointerAxes_disableAxis(axis as i32) }
}
@@ -403,27 +592,26 @@ impl AndroidAppInner {
}
}
pub fn input_events<F>(&self, mut callback: F)
where
F: FnMut(&InputEvent) -> InputStatus,
{
let buf = unsafe {
let app_ptr = self.native_app.as_ptr();
let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
if input_buffer.is_null() {
return;
}
InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer))
};
pub(crate) fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
let mut guard = self.input_receiver.lock().unwrap();
let mut keys_iter = KeyEventsLendingIterator::new(&buf);
while let Some(key_event) = keys_iter.next() {
callback(&InputEvent::KeyEvent(key_event));
}
let mut motion_iter = MotionEventsLendingIterator::new(&buf);
while let Some(motion_event) = motion_iter.next() {
callback(&InputEvent::MotionEvent(motion_event));
// Make sure we don't hand out more than one receiver at a time because
// turning the reciever into an interator will perform a swap_buffers
// for the buffered input events which shouldn't happen while we're in
// the middle of iterating events
if let Some(receiver) = &*guard {
if receiver.strong_count() > 0 {
return Err(crate::error::InternalAppError::InputUnavailable);
}
}
*guard = None;
let receiver = Arc::new(InputReceiver {
native_app: self.native_app.clone(),
});
*guard = Some(Arc::downgrade(&receiver));
Ok(receiver)
}
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
@@ -448,26 +636,31 @@ impl AndroidAppInner {
}
}
struct MotionEventsLendingIterator<'a> {
struct MotionEventsLendingIterator {
pos: usize,
count: usize,
buffer: &'a InputBuffer<'a>,
}
// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
// via a generic trait. The iteration of motion events is entirely private
// though so this is ok for now.
impl<'a> MotionEventsLendingIterator<'a> {
fn new(buffer: &'a InputBuffer<'a>) -> Self {
impl MotionEventsLendingIterator {
fn new(buffer: &InputBuffer) -> Self {
Self {
pos: 0,
count: buffer.motion_events_count(),
buffer,
}
}
fn next(&mut self) -> Option<MotionEvent<'a>> {
fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option<MotionEvent<'buf>> {
if self.pos < self.count {
let ga_event = unsafe { &(*self.buffer.ptr.as_ptr()).motionEvents[self.pos] };
// Safety:
// - This iterator currently has exclusive access to the front buffer of events
// - We know the buffer is non-null
// - `pos` is less than the number of events stored in the buffer
let ga_event = unsafe {
(*buffer.ptr.as_ptr())
.motionEvents
.add(self.pos)
.as_ref()
.unwrap()
};
let event = MotionEvent::new(ga_event);
self.pos += 1;
Some(event)
@@ -477,26 +670,31 @@ impl<'a> MotionEventsLendingIterator<'a> {
}
}
struct KeyEventsLendingIterator<'a> {
struct KeyEventsLendingIterator {
pos: usize,
count: usize,
buffer: &'a InputBuffer<'a>,
}
// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
// via a generic trait. The iteration of key events is entirely private
// though so this is ok for now.
impl<'a> KeyEventsLendingIterator<'a> {
fn new(buffer: &'a InputBuffer<'a>) -> Self {
impl KeyEventsLendingIterator {
fn new(buffer: &InputBuffer) -> Self {
Self {
pos: 0,
count: buffer.key_events_count(),
buffer,
}
}
fn next(&mut self) -> Option<KeyEvent<'a>> {
fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option<KeyEvent<'buf>> {
if self.pos < self.count {
let ga_event = unsafe { &(*self.buffer.ptr.as_ptr()).keyEvents[self.pos] };
// Safety:
// - This iterator currently has exclusive access to the front buffer of events
// - We know the buffer is non-null
// - `pos` is less than the number of events stored in the buffer
let ga_event = unsafe {
(*buffer.ptr.as_ptr())
.keyEvents
.add(self.pos)
.as_ref()
.unwrap()
};
let event = KeyEvent::new(ga_event);
self.pos += 1;
Some(event)
@@ -515,7 +713,7 @@ impl<'a> InputBuffer<'a> {
pub(crate) fn from_ptr(ptr: NonNull<ffi::android_input_buffer>) -> InputBuffer<'a> {
Self {
ptr,
_lifetime: PhantomData::default(),
_lifetime: PhantomData,
}
}
@@ -537,22 +735,133 @@ impl<'a> Drop for InputBuffer<'a> {
}
}
/// Conceptually we can think of this like the receiver end of an
/// input events channel.
///
/// After being passed back to AndroidApp it gets turned into a
/// lending iterator for pending input events.
///
/// It serves two purposes:
/// 1. It represents an exclusive access to input events (the application
/// can only have one receiver at a time) and it's intended to support
/// the double-buffering design for input events in GameActivity where
/// we issue a swap_buffers before iterating events and wouldn't want
/// another swap to be possible before finishing - especially since
/// we want to borrow directly from the buffer while dispatching.
/// 2. It doesn't borrow from AndroidAppInner so we can pass it back to
/// AndroidApp which can drop its lock around AndroidAppInner and
/// it can then be turned into a lending iterator. (We wouldn't
/// be able to pass the iterator back to the application if it
/// borrowed from within the lock and we need to drop the lock
/// because otherwise the app wouldn't be able to access the AndroidApp
/// API in any way while iterating events)
#[derive(Debug)]
pub(crate) struct InputReceiver {
// Safety: the native_app effectively has a static lifetime and it
// has its own internal locking when calling
// `android_app_swap_input_buffers`
native_app: NativeAppGlue,
}
impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
fn from(receiver: Arc<InputReceiver>) -> Self {
let buffered = unsafe {
let app_ptr = receiver.native_app.as_ptr();
let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
if input_buffer.is_null() {
None
} else {
let buffer = InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer));
let keys_iter = KeyEventsLendingIterator::new(&buffer);
let motion_iter = MotionEventsLendingIterator::new(&buffer);
Some(BufferedEvents::<'a> {
buffer,
keys_iter,
motion_iter,
})
}
};
let native_app = receiver.native_app.clone();
Self {
_receiver: receiver,
buffered,
native_app,
text_event_checked: false,
}
}
}
struct BufferedEvents<'a> {
buffer: InputBuffer<'a>,
keys_iter: KeyEventsLendingIterator,
motion_iter: MotionEventsLendingIterator,
}
pub(crate) struct InputIteratorInner<'a> {
// Held to maintain exclusive access to buffered input events
_receiver: Arc<InputReceiver>,
buffered: Option<BufferedEvents<'a>>,
native_app: NativeAppGlue,
text_event_checked: bool,
}
impl<'a> InputIteratorInner<'a> {
pub(crate) fn next<F>(&mut self, callback: F) -> bool
where
F: FnOnce(&input::InputEvent) -> InputStatus,
{
if let Some(buffered) = &mut self.buffered {
if let Some(key_event) = buffered.keys_iter.next(&buffered.buffer) {
let _ = callback(&InputEvent::KeyEvent(key_event));
return true;
}
if let Some(motion_event) = buffered.motion_iter.next(&buffered.buffer) {
let _ = callback(&InputEvent::MotionEvent(motion_event));
return true;
}
self.buffered = None;
}
if !self.text_event_checked {
self.text_event_checked = true;
unsafe {
let app_ptr = self.native_app.as_ptr();
// XXX: It looks like the GameActivity implementation should
// be using atomic ops to set this flag, and require us to
// use atomics to check and clear it too.
//
// We currently just hope that with the lack of atomic ops that
// the compiler isn't reordering code so this gets flagged
// before the java main thread really updates the state.
if (*app_ptr).textInputState != 0 {
let state = self.native_app.text_input_state(); // Will clear .textInputState
let _ = callback(&InputEvent::TextEvent(state));
return true;
}
}
}
false
}
}
// Rust doesn't give us a clean way to directly export symbols from C/C++
// so we rename the C/C++ symbols and re-export these JNI entrypoints from
// Rust...
//
// https://github.com/rust-lang/rfcs/issues/2771
extern "C" {
pub fn Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
pub fn Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
env: *mut JNIEnv,
javaGameActivity: jobject,
path: jstring,
funcName: jstring,
internalDataDir: jstring,
obbDir: jstring,
externalDataDir: jstring,
jAssetMgr: jobject,
savedState: jbyteArray,
javaConfig: jobject,
) -> jlong;
pub fn GameActivity_onCreate_C(
@@ -563,27 +872,25 @@ extern "C" {
}
#[no_mangle]
pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_loadNativeCode(
pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_initializeNativeCode(
env: *mut JNIEnv,
java_game_activity: jobject,
path: jstring,
func_name: jstring,
internal_data_dir: jstring,
obb_dir: jstring,
external_data_dir: jstring,
jasset_mgr: jobject,
saved_state: jbyteArray,
java_config: jobject,
) -> jni_sys::jlong {
Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
env,
java_game_activity,
path,
func_name,
internal_data_dir,
obb_dir,
external_data_dir,
jasset_mgr,
saved_state,
java_config,
)
}
@@ -604,17 +911,22 @@ extern "Rust" {
// `app_main` function. This is run on a dedicated thread spawned
// by android_native_app_glue.
#[no_mangle]
#[allow(unused_unsafe)] // Otherwise rust 1.64 moans about using unsafe{} in unsafe functions
pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
abort_on_panic(|| {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
let file = {
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])
};
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 {
@@ -634,19 +946,16 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
let activity: jobject = (*(*native_app).activity).javaGameActivity;
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let jvm = CloneJavaVM::from_raw(jvm).unwrap();
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
}
jvm.attach_current_thread_permanently().unwrap();
jvm
};
unsafe {
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap());
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone());
// We want to specifically catch any panic from the application's android_main
// so we can finish + destroy the Activity gracefully via the JVM
@@ -669,9 +978,9 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
// to the main thread of the process where the Java finish call will take place"
ffi::GameActivity_finish((*native_app).activity);
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
detach_current_thread(jvm);
}
// 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();
}
File diff suppressed because it is too large Load Diff
+351
View File
@@ -0,0 +1,351 @@
use std::sync::Arc;
use jni::{
objects::{GlobalRef, JClass, JMethodID, JObject, JStaticMethodID, JValue},
signature::{Primitive, ReturnType},
JNIEnv,
};
use jni_sys::jint;
use crate::{
input::{Keycode, MetaState},
jni_utils::CloneJavaVM,
};
use crate::{
error::{AppError, InternalAppError},
jni_utils,
};
/// An enum representing the types of keyboards that may generate key events
///
/// See [getKeyboardType() docs](https://developer.android.com/reference/android/view/KeyCharacterMap#getKeyboardType())
///
/// # Android Extensible Enum
///
/// This is a runtime [extensible enum](`crate#android-extensible-enums`) and
/// should be handled similar to a `#[non_exhaustive]` enum to maintain
/// forwards compatibility.
///
/// This implements `Into<u32>` and `From<u32>` for converting to/from Android
/// SDK integer values.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, num_enum::FromPrimitive, num_enum::IntoPrimitive,
)]
#[non_exhaustive]
#[repr(u32)]
pub enum KeyboardType {
/// A numeric (12-key) keyboard.
///
/// A numeric keyboard supports text entry using a multi-tap approach. It may be necessary to tap a key multiple times to generate the desired letter or symbol.
///
/// This type of keyboard is generally designed for thumb typing.
Numeric,
/// A keyboard with all the letters, but with more than one letter per key.
///
/// This type of keyboard is generally designed for thumb typing.
Predictive,
/// A keyboard with all the letters, and maybe some numbers.
///
/// An alphabetic keyboard supports text entry directly but may have a condensed layout with a small form factor. In contrast to a full keyboard, some symbols may only be accessible using special on-screen character pickers. In addition, to improve typing speed and accuracy, the framework provides special affordances for alphabetic keyboards such as auto-capitalization and toggled / locked shift and alt keys.
///
/// This type of keyboard is generally designed for thumb typing.
Alpha,
/// A full PC-style keyboard.
///
/// A full keyboard behaves like a PC keyboard. All symbols are accessed directly by pressing keys on the keyboard without on-screen support or affordances such as auto-capitalization.
///
/// This type of keyboard is generally designed for full two hand typing.
Full,
/// A keyboard that is only used to control special functions rather than for typing.
///
/// A special function keyboard consists only of non-printing keys such as HOME and POWER that are not actually used for typing.
SpecialFunction,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(u32),
}
/// Either represents, a unicode character or combining accent from a
/// [`KeyCharacterMap`], or `None` for non-printable keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyMapChar {
None,
Unicode(char),
CombiningAccent(char),
}
// I've also tried to think here about how to we could potentially automatically
// generate a binding struct like `KeyCharacterMapBinding` with a procmacro and
// so have intentionally limited the `Binding` being a very thin, un-opinionated
// wrapper based on basic JNI types.
/// Lower-level JNI binding for `KeyCharacterMap` class only holds 'static state
/// and can be shared with an `Arc` ref count.
///
/// The separation here also neatly helps us separate `InternalAppError` from
/// `AppError` for mapping JNI errors without exposing any `jni-rs` types in the
/// public API.
#[derive(Debug)]
pub(crate) struct KeyCharacterMapBinding {
//vm: JavaVM,
klass: GlobalRef,
get_method_id: JMethodID,
get_dead_char_method_id: JStaticMethodID,
get_keyboard_type_method_id: JMethodID,
}
impl KeyCharacterMapBinding {
pub(crate) fn new(env: &mut JNIEnv) -> Result<Self, InternalAppError> {
let binding = env.with_local_frame::<_, _, InternalAppError>(10, |env| {
let klass = env.find_class("android/view/KeyCharacterMap")?; // Creates a local ref
Ok(Self {
get_method_id: env.get_method_id(&klass, "get", "(II)I")?,
get_dead_char_method_id: env.get_static_method_id(
&klass,
"getDeadChar",
"(II)I",
)?,
get_keyboard_type_method_id: env.get_method_id(&klass, "getKeyboardType", "()I")?,
klass: env.new_global_ref(&klass)?,
})
})?;
Ok(binding)
}
pub fn get<'local>(
&self,
env: &'local mut JNIEnv,
key_map: impl AsRef<JObject<'local>>,
key_code: jint,
meta_state: jint,
) -> Result<jint, InternalAppError> {
let key_map = key_map.as_ref();
// Safety:
// - we know our global `key_map` reference is non-null and valid.
// - we know `get_method_id` remains valid
// - we know that the signature of KeyCharacterMap::get is `(int, int) -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
let unicode = unsafe {
env.call_method_unchecked(
key_map,
self.get_method_id,
ReturnType::Primitive(Primitive::Int),
&[
JValue::Int(key_code).as_jni(),
JValue::Int(meta_state).as_jni(),
],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
Ok(unicode.i().unwrap())
}
pub fn get_dead_char(
&self,
env: &mut JNIEnv,
accent_char: jint,
base_char: jint,
) -> Result<jint, InternalAppError> {
// Safety:
// - we know `get_dead_char_method_id` remains valid
// - we know that KeyCharacterMap::getDeadKey is a static method
// - we know that the signature of KeyCharacterMap::getDeadKey is `(int, int) -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
// Urgh, it's pretty terrible that there's no ergonomic/safe way to get a JClass reference from a GlobalRef
// Safety: we don't do anything that would try to delete the JClass as if it were a real local reference
let klass = unsafe { JClass::from_raw(self.klass.as_obj().as_raw()) };
let unicode = unsafe {
env.call_static_method_unchecked(
&klass,
self.get_dead_char_method_id,
ReturnType::Primitive(Primitive::Int),
&[
JValue::Int(accent_char).as_jni(),
JValue::Int(base_char).as_jni(),
],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?;
Ok(unicode.i().unwrap())
}
pub fn get_keyboard_type<'local>(
&self,
env: &'local mut JNIEnv,
key_map: impl AsRef<JObject<'local>>,
) -> Result<jint, InternalAppError> {
let key_map = key_map.as_ref();
// Safety:
// - we know our global `key_map` reference is non-null and valid.
// - we know `get_keyboard_type_method_id` remains valid
// - we know that the signature of KeyCharacterMap::getKeyboardType is `() -> int`
// - we know this won't leak any local references as a side effect
//
// We know it's ok to unwrap the `.i()` value since we explicitly
// specify the return type as `Int`
Ok(unsafe {
env.call_method_unchecked(
key_map,
self.get_keyboard_type_method_id,
ReturnType::Primitive(Primitive::Int),
&[],
)
}
.map_err(|err| jni_utils::clear_and_map_exception_to_err(env, err))?
.i()
.unwrap())
}
}
/// Describes the keys provided by a keyboard device and their associated labels.
#[derive(Clone, Debug)]
pub struct KeyCharacterMap {
jvm: CloneJavaVM,
binding: Arc<KeyCharacterMapBinding>,
key_map: GlobalRef,
}
impl KeyCharacterMap {
pub(crate) fn new(
jvm: CloneJavaVM,
binding: Arc<KeyCharacterMapBinding>,
key_map: GlobalRef,
) -> Self {
Self {
jvm,
binding,
key_map,
}
}
/// Gets the Unicode character generated by the specified [`Keycode`] and [`MetaState`] combination.
///
/// Returns [`KeyMapChar::None`] if the key is not one that is used to type Unicode characters.
///
/// Returns [`KeyMapChar::CombiningAccent`] if the key is a "dead key" that should be combined with
/// another to actually produce a character -- see [`KeyCharacterMap::get_dead_char`].
///
/// # Errors
///
/// Since this API needs to use JNI internally to call into the Android JVM it may return
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn get(&self, key_code: Keycode, meta_state: MetaState) -> Result<KeyMapChar, AppError> {
let key_code: u32 = key_code.into();
let key_code = key_code as jni_sys::jint;
let meta_state: u32 = meta_state.0;
let meta_state = meta_state as jni_sys::jint;
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let err: InternalAppError = err.into();
err
})?;
let unicode = self
.binding
.get(&mut env, self.key_map.as_obj(), key_code, meta_state)?;
let unicode = unicode as u32;
const COMBINING_ACCENT: u32 = 0x80000000;
const COMBINING_ACCENT_MASK: u32 = !COMBINING_ACCENT;
if unicode == 0 {
Ok(KeyMapChar::None)
} else if unicode & COMBINING_ACCENT == COMBINING_ACCENT {
let accent = unicode & COMBINING_ACCENT_MASK;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::CombiningAccent(unsafe {
char::from_u32_unchecked(accent)
}))
} else {
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(KeyMapChar::Unicode(unsafe {
char::from_u32_unchecked(unicode)
}))
}
}
/// Get the character that is produced by combining the dead key producing accent with the key producing character c.
///
/// For example, ```get_dead_char('`', 'e')``` returns 'è'. `get_dead_char('^', ' ')` returns '^' and `get_dead_char('^', '^')` returns '^'.
///
/// # Errors
///
/// Since this API needs to use JNI internally to call into the Android JVM it may return
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn get_dead_char(
&self,
accent_char: char,
base_char: char,
) -> Result<Option<char>, AppError> {
let accent_char = accent_char as jni_sys::jint;
let base_char = base_char as jni_sys::jint;
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let err: InternalAppError = err.into();
err
})?;
let unicode = self
.binding
.get_dead_char(&mut env, accent_char, base_char)?;
let unicode = unicode as u32;
// Safety: assumes Android key maps don't contain invalid unicode characters
Ok(if unicode == 0 {
None
} else {
Some(unsafe { char::from_u32_unchecked(unicode) })
})
}
/// Gets the keyboard type.
///
/// Different keyboard types have different semantics. See [`KeyboardType`] for details.
///
/// # Errors
///
/// Since this API needs to use JNI internally to call into the Android JVM it may return
/// a [`AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn get_keyboard_type(&self) -> Result<KeyboardType, AppError> {
// Since we expect this API to be called from the `main` thread then we expect to already be
// attached to the JVM
//
// Safety: there's no other JNIEnv in scope so this env can't be used to subvert the mutable
// borrow rules that ensure we can only add local references to the top JNI frame.
let mut env = self.jvm.get_env().map_err(|err| {
let err: InternalAppError = err.into();
err
})?;
let keyboard_type = self
.binding
.get_keyboard_type(&mut env, self.key_map.as_obj())?;
let keyboard_type = keyboard_type as u32;
Ok(keyboard_type.into())
}
}
+151
View File
@@ -0,0 +1,151 @@
//! The JNI calls we make in this crate are often not part of a Java native
//! method implementation and so we can't assume we have a JNI local frame that
//! is going to unwind and free local references, and we also can't just leave
//! exceptions to get thrown when returning to Java.
//!
//! These utilities help us check + clear exceptions and map them into Rust Errors.
use std::{ops::Deref, sync::Arc};
use jni::{
objects::{JObject, JString},
JavaVM,
};
use crate::{
error::{InternalAppError, InternalResult},
input::{KeyCharacterMap, KeyCharacterMapBinding},
};
// TODO: JavaVM should implement Clone
#[derive(Debug)]
pub(crate) struct CloneJavaVM {
pub jvm: JavaVM,
}
impl Clone for CloneJavaVM {
fn clone(&self) -> Self {
Self {
jvm: unsafe { JavaVM::from_raw(self.jvm.get_java_vm_pointer()).unwrap() },
}
}
}
impl CloneJavaVM {
pub unsafe fn from_raw(jvm: *mut jni_sys::JavaVM) -> InternalResult<Self> {
Ok(Self {
jvm: JavaVM::from_raw(jvm)?,
})
}
}
unsafe impl Send for CloneJavaVM {}
unsafe impl Sync for CloneJavaVM {}
impl Deref for CloneJavaVM {
type Target = JavaVM;
fn deref(&self) -> &Self::Target {
&self.jvm
}
}
/// Use with `.map_err()` to map `jni::errors::Error::JavaException` into a
/// richer error based on the actual contents of the `JThrowable`
///
/// (The `jni` crate doesn't do that automatically since it's more
/// common to let the exception get thrown when returning to Java)
///
/// This will also clear the exception
pub(crate) fn clear_and_map_exception_to_err(
env: &mut jni::JNIEnv<'_>,
err: jni::errors::Error,
) -> InternalAppError {
if matches!(err, jni::errors::Error::JavaException) {
let result = env.with_local_frame::<_, _, InternalAppError>(5, |env| {
let e = env.exception_occurred()?;
assert!(!e.is_null()); // should only be called after receiving a JavaException Result
env.exception_clear()?;
let class = env.get_object_class(&e)?;
//let get_stack_trace_method = env.get_method_id(&class, "getStackTrace", "()[Ljava/lang/StackTraceElement;")?;
let get_message_method =
env.get_method_id(&class, "getMessage", "()Ljava/lang/String;")?;
let msg = unsafe {
env.call_method_unchecked(
&e,
get_message_method,
jni::signature::ReturnType::Object,
&[],
)?
.l()
.unwrap()
};
let msg = unsafe { JString::from_raw(JObject::into_raw(msg)) };
let msg = env.get_string(&msg)?;
let msg: String = msg.into();
// TODO: get Java backtrace:
/*
if let JValue::Object(elements) = env.call_method_unchecked(&e, get_stack_trace_method, jni::signature::ReturnType::Array, &[])? {
let elements = env.auto_local(elements);
}
*/
Ok(msg)
});
match result {
Ok(msg) => InternalAppError::JniException(msg),
Err(err) => InternalAppError::JniException(format!(
"UNKNOWN (Failed to query JThrowable: {err:?})"
)),
}
} else {
err.into()
}
}
pub(crate) fn device_key_character_map(
jvm: CloneJavaVM,
key_map_binding: Arc<KeyCharacterMapBinding>,
device_id: i32,
) -> InternalResult<KeyCharacterMap> {
// Don't really need to 'attach' since this should be called from the app's main thread that
// should already be attached, but the redundancy should be fine
//
// Attach 'permanently' to avoid any chance of detaching the thread from the VM
let mut env = jvm.attach_current_thread_permanently()?;
// We don't want to accidentally leak any local references while we
// aren't going to be returning from here back to the JVM, to unwind, so
// we make a local frame
let character_map = env.with_local_frame::<_, _, jni::errors::Error>(10, |env| {
let input_device_class = env.find_class("android/view/InputDevice")?; // Creates a local ref
let device = env
.call_static_method(
input_device_class,
"getDevice",
"(I)Landroid/view/InputDevice;",
&[device_id.into()],
)?
.l()?; // Creates a local ref
let character_map = env
.call_method(
&device,
"getKeyCharacterMap",
"()Landroid/view/KeyCharacterMap;",
&[],
)?
.l()?;
let character_map = env.new_global_ref(character_map)?;
Ok(character_map)
})?;
Ok(KeyCharacterMap::new(
jvm.clone(),
key_map_binding,
character_map,
))
}
+220 -27
View File
@@ -35,6 +35,18 @@
//! marshalled between the Java thread that owns the `Activity` and the native
//! thread that runs the `android_main()` code.
//!
//! # Cheaply Clonable [`AndroidApp`]
//!
//! [`AndroidApp`] is intended to be something that can be cheaply passed around
//! by referenced within an application. It is reference counted and can be
//! cheaply cloned.
//!
//! # `Send` and `Sync` [`AndroidApp`]
//!
//! Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
//! into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
//! explicitly documented to only be usable from your `android_main()` thread.
//!
//! # Main Thread Initialization
//!
//! Before `android_main()` is called, the following application state
@@ -51,6 +63,49 @@
//!
//! These are undone after `android_main()` returns
//!
//! # Android Extensible Enums
//!
//! There are numerous enums in the `android-activity` API which are effectively
//! bindings to enums declared in the Android SDK which need to be considered
//! _runtime_ extensible.
//!
//! Any enum variants that come from the Android SDK may be extended in future
//! versions of Android and your code could be exposed to new variants if you
//! build an application that might be installed on new versions of Android.
//!
//! This crate follows a convention of adding a hidden `__Unknown(u32)` variant
//! to these enum to ensure we can always do lossless conversions between the
//! integers from the SDK and our corresponding Rust enums. This can be
//! important in case you need to pass certain variants back to the SDK
//! regardless of whether you knew about that variants specific semantics at
//! compile time.
//!
//! You should never include this `__Unknown(u32)` variant within any exhaustive
//! pattern match and should instead treat the enums like `#[non_exhaustive]`
//! enums that require you to add a catch-all for any `unknown => {}` values.
//!
//! Any code that would exhaustively include the `__Unknown(u32)` variant when
//! pattern matching can not be guaranteed to be forwards compatible with new
//! releases of `android-activity` which may add new Rust variants to these
//! enums without requiring a breaking semver bump.
//!
//! You can (infallibly) convert these enums to and from primitive `u32` values
//! using `.into()`:
//!
//! For example, here is how you could ensure forwards compatibility with both
//! compile-time and runtime extensions of a `SomeEnum` enum:
//!
//! ```rust
//! match some_enum {
//! SomeEnum::Foo => {},
//! SomeEnum::Bar => {},
//! unhandled => {
//! let sdk_val: u32 = unhandled.into();
//! println!("Unhandled enum variant {some_enum:?} has SDK value: {sdk_val}");
//! }
//! }
//! ```
//!
//! [`Activity`]: https://developer.android.com/reference/android/app/Activity
//! [`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
//! [ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa
@@ -62,6 +117,7 @@ use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
use input::KeyCharacterMap;
use libc::c_void;
use ndk::asset::AssetManager;
use ndk::native_window::NativeWindow;
@@ -96,15 +152,12 @@ 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(any(feature = "native-activity", doc))]
mod native_activity;
#[cfg(any(feature = "native-activity", doc))]
use native_activity as activity_impl;
#[cfg_attr(any(feature = "native-activity", doc), path = "native_activity/mod.rs")]
#[cfg_attr(any(feature = "game-activity", doc), path = "game_activity/mod.rs")]
pub(crate) mod activity_impl;
#[cfg(feature = "game-activity")]
mod game_activity;
#[cfg(feature = "game-activity")]
use game_activity as activity_impl;
pub mod error;
use error::Result;
pub mod input;
@@ -113,6 +166,8 @@ pub use config::ConfigurationRef;
mod util;
mod jni_utils;
/// A rectangle with integer edge coordinates. Used to represent window insets, for example.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Rect {
@@ -163,14 +218,14 @@ pub use activity_impl::StateSaver;
#[non_exhaustive]
#[derive(Debug)]
pub enum MainEvent<'a> {
/// New input events are available via [`AndroidApp::input_events()`]
/// New input events are available via [`AndroidApp::input_events_iter()`]
///
/// _Note: Even if more input is received this event will not be resent
/// until [`AndroidApp::input_events()`] has been called, which enables
/// until [`AndroidApp::input_events_iter()`] has been called, which enables
/// applications to batch up input processing without there being lots of
/// redundant event loop wake ups._
///
/// [`AndroidApp::input_events()`]: AndroidApp::input_events
/// [`AndroidApp::input_events_iter()`]: AndroidApp::input_events_iter
InputAvailable,
/// Command from main thread: a new [`NativeWindow`] is ready for use. Upon
@@ -275,6 +330,7 @@ pub use activity_impl::AndroidAppWaker;
bitflags! {
/// Flags for [`AndroidApp::set_window_flags`]
/// as per the [android.view.WindowManager.LayoutParams Java API](https://developer.android.com/reference/android/view/WindowManager.LayoutParams)
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct WindowManagerFlags: u32 {
/// As long as this window is visible to the user, allow the lock
/// screen to activate while the screen is on. This can be used
@@ -445,6 +501,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>>,
@@ -536,14 +604,14 @@ impl AndroidApp {
/// [`ALooper_pollAll`]: ndk::looper::ThreadLooper::poll_all
pub fn poll_events<F>(&self, timeout: Option<Duration>, callback: F)
where
F: FnMut(PollEvent),
F: FnMut(PollEvent<'_>),
{
self.inner.read().unwrap().poll_events(timeout, callback);
}
/// Creates a means to wake up the main loop while it is blocked waiting for
/// events within [`AndroidApp::poll_events()`].
pub fn create_waker(&self) -> activity_impl::AndroidAppWaker {
pub fn create_waker(&self) -> AndroidAppWaker {
self.inner.read().unwrap().create_waker()
}
@@ -618,24 +686,149 @@ impl AndroidApp {
.hide_soft_input(hide_implicit_only);
}
/// Query and process all out-standing input event
/// Fetch the current input text state, as updated by any active IME.
pub fn text_input_state(&self) -> input::TextInputState {
self.inner.read().unwrap().text_input_state()
}
/// Forward the given input text `state` to any active IME.
pub fn set_text_input_state(&self, state: input::TextInputState) {
self.inner.read().unwrap().set_text_input_state(state);
}
/// Get an exclusive, lending iterator over buffered input events
///
/// `callback` should return [`InputStatus::Unhandled`] for any input events that aren't directly
/// handled by the application, or else [`InputStatus::Handled`]. Unhandled events may lead to a
/// fallback interpretation of the event.
/// Applications are expected to call this in-sync with their rendering or
/// in response to a [`MainEvent::InputAvailable`] event being delivered.
///
/// Applications are generally either expected to call this in-sync with their rendering or
/// in response to a [`MainEvent::InputAvailable`] event being delivered. _Note though that your
/// application is will only be delivered a single [`MainEvent::InputAvailable`] event between calls
/// to this API._
/// _**Note:** your application is will only be delivered a single
/// [`MainEvent::InputAvailable`] event between calls to this API._
///
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// To reduce overhead, by default, only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
pub fn input_events<F>(&self, callback: F)
where
F: FnMut(&input::InputEvent) -> InputStatus,
{
self.inner.read().unwrap().input_events(callback)
///
/// This isn't the most ergonomic iteration API since we can't return a standard `Iterator`:
/// - This API returns a lending iterator may borrow from the internal buffer
/// of pending events without copying them.
/// - For each event we want to ensure the application reports whether the
/// event was handled.
///
/// # Example
/// Code to iterate all pending input events would look something like this:
///
/// ```rust
/// match app.input_events_iter() {
/// Ok(mut iter) => {
/// loop {
/// let read_input = iter.next(|event| {
/// let handled = match event {
/// InputEvent::KeyEvent(key_event) => {
/// // Snip
/// }
/// InputEvent::MotionEvent(motion_event) => {
/// // Snip
/// }
/// event => {
/// // Snip
/// }
/// };
///
/// handled
/// });
///
/// if !read_input {
/// break;
/// }
/// }
/// }
/// Err(err) => {
/// log::error!("Failed to get input events iterator: {err:?}");
/// }
/// }
/// ```
///
/// # Panics
///
/// This must only be called from your `android_main()` thread and it may panic if called
/// from another thread.
pub fn input_events_iter(&self) -> Result<input::InputIterator> {
let receiver = {
let guard = self.inner.read().unwrap();
guard.input_events_receiver()?
};
Ok(input::InputIterator {
inner: receiver.into(),
})
}
/// Lookup the [`KeyCharacterMap`] for the given input `device_id`
///
/// Use [`KeyCharacterMap::get`] to map key codes + meta state into unicode characters
/// or dead keys that compose with the next key.
///
/// # Example
///
/// Code to handle unicode character mapping as well as combining dead keys could look some thing like:
///
/// ```rust
/// let mut combining_accent = None;
/// // Snip
///
/// let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
/// match map.get(key_event.key_code(), key_event.meta_state()) {
/// Ok(KeyMapChar::Unicode(unicode)) => {
/// let combined_unicode = if let Some(accent) = combining_accent {
/// match map.get_dead_char(accent, unicode) {
/// Ok(Some(key)) => {
/// info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
/// Some(key)
/// }
/// Ok(None) => None,
/// Err(err) => {
/// log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
/// None
/// }
/// }
/// } else {
/// info!("KeyEvent: Pressed '{unicode}'");
/// Some(unicode)
/// };
/// combining_accent = None;
/// combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
/// }
/// Ok(KeyMapChar::CombiningAccent(accent)) => {
/// info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
/// combining_accent = Some(accent);
/// Some(KeyMapChar::CombiningAccent(accent))
/// }
/// Ok(KeyMapChar::None) => {
/// info!("KeyEvent: Pressed non-unicode key");
/// combining_accent = None;
/// None
/// }
/// Err(err) => {
/// log::error!("KeyEvent: Failed to get key map character: {err:?}");
/// combining_accent = None;
/// None
/// }
/// }
/// } else {
/// None
/// };
/// ```
///
/// # Errors
///
/// Since this API needs to use JNI internally to call into the Android JVM it may return
/// a [`error::AppError::JavaError`] in case there is a spurious JNI error or an exception
/// is caught.
pub fn device_key_character_map(&self, device_id: i32) -> Result<KeyCharacterMap> {
Ok(self
.inner
.read()
.unwrap()
.device_key_character_map(device_id)?)
}
/// The user-visible SDK version of the framework
+69 -45
View File
@@ -17,6 +17,7 @@ use log::Level;
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
use crate::{
jni_utils::CloneJavaVM,
util::android_log,
util::{abort_on_panic, log_panic},
ConfigurationRef,
@@ -199,19 +200,34 @@ impl NativeActivityGlue {
}
}
/// The status of the native thread that's created to run
/// `android_main`
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NativeThreadState {
/// The `android_main` thread hasn't been created yet
Init,
/// The `android_main` thread has been spawned and started running
Running,
/// The `android_main` thread has finished
Stopped,
}
#[derive(Debug)]
pub struct NativeActivityState {
pub msg_read: libc::c_int,
pub msg_write: libc::c_int,
pub config: super::ConfigurationRef,
pub config: ConfigurationRef,
pub saved_state: Vec<u8>,
pub input_queue: *mut ndk_sys::AInputQueue,
pub window: Option<NativeWindow>,
pub content_rect: ndk_sys::ARect,
pub activity_state: State,
pub destroy_requested: bool,
pub running: bool,
pub thread_state: NativeThreadState,
pub app_has_saved_state: bool,
/// 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,
@@ -303,8 +319,6 @@ impl Drop for WaitableNativeActivityState {
unsafe {
let mut guard = self.mutex.lock().unwrap();
guard.detach_input_queue_from_looper();
guard.destroyed = true;
self.cond.notify_one();
}
}
}
@@ -329,9 +343,8 @@ impl WaitableNativeActivityState {
}
}
let saved_state = unsafe {
std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size as _)
};
let saved_state =
unsafe { std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size) };
let config = unsafe {
let config = ndk_sys::AConfiguration_new();
@@ -356,7 +369,7 @@ impl WaitableNativeActivityState {
content_rect: Rect::empty().into(),
activity_state: State::Init,
destroy_requested: false,
running: false,
thread_state: NativeThreadState::Init,
app_has_saved_state: false,
destroyed: false,
redraw_needed: false,
@@ -369,10 +382,11 @@ impl WaitableNativeActivityState {
pub fn notify_destroyed(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.destroyed = true;
unsafe {
guard.write_cmd(AppCmd::Destroy);
while !guard.destroyed {
while guard.thread_state != NativeThreadState::Stopped {
guard = self.cond.wait(guard).unwrap();
}
@@ -502,7 +516,7 @@ impl WaitableNativeActivityState {
// given via a `malloc()` allocated pointer since it will automatically
// `free()` the state after it has been converted to a buffer for the JVM.
if !guard.saved_state.is_empty() {
let saved_state_size = guard.saved_state.len() as _;
let saved_state_size = guard.saved_state.len();
let saved_state_src_ptr = guard.saved_state.as_ptr();
unsafe {
let saved_state = libc::malloc(saved_state_size);
@@ -541,7 +555,13 @@ impl WaitableNativeActivityState {
pub fn notify_main_thread_running(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.running = true;
guard.thread_state = NativeThreadState::Running;
self.cond.notify_one();
}
pub fn notify_main_thread_stopped_running(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.thread_state = NativeThreadState::Stopped;
self.cond.notify_one();
}
@@ -660,7 +680,7 @@ unsafe extern "C" fn on_resume(activity: *mut ndk_sys::ANativeActivity) {
unsafe extern "C" fn on_save_instance_state(
activity: *mut ndk_sys::ANativeActivity,
out_len: *mut ndk_sys::size_t,
out_len: *mut usize,
) -> *mut libc::c_void {
abort_on_panic(|| {
log::debug!("SaveInstanceState: {:p}\n", activity);
@@ -668,7 +688,7 @@ unsafe extern "C" fn on_save_instance_state(
let mut ret = ptr::null_mut();
try_with_waitable_activity_ref(activity, |waitable_activity| {
let (state, len) = waitable_activity.request_save_state();
*out_len = len as ndk_sys::size_t;
*out_len = len;
ret = state
});
@@ -808,7 +828,6 @@ 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,
@@ -816,28 +835,31 @@ extern "C" fn ANativeActivity_onCreate(
) {
abort_on_panic(|| {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
unsafe {
let file = unsafe {
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::pipe2(logpipe.as_mut_ptr(), libc::O_CLOEXEC);
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);
}
libc::close(logpipe[1]);
File::from_raw_fd(logpipe[0])
};
std::thread::spawn(move || {
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();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
}
}
});
}
}
});
log::trace!(
"Creating: {:p}, saved_state = {:p}, save_state_size = {}",
@@ -858,24 +880,21 @@ extern "C" fn ANativeActivity_onCreate(
std::thread::spawn(move || {
let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _;
let jvm = unsafe {
let jvm = abort_on_panic(|| unsafe {
let na = activity;
let jvm = (*na).vm;
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
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
}
jvm.attach_current_thread_permanently().unwrap();
jvm
};
});
let app = AndroidApp::new(rust_glue.clone());
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
rust_glue.notify_main_thread_running();
@@ -893,7 +912,7 @@ extern "C" fn ANativeActivity_onCreate(
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(|panic| log_panic(panic));
.unwrap_or_else(log_panic);
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
@@ -901,17 +920,22 @@ extern "C" fn ANativeActivity_onCreate(
// to the main thread of the process where the Java finish call will take place"
ndk_sys::ANativeActivity_finish(activity);
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
detach_current_thread(jvm);
}
// This should detach automatically but lets detach explicitly to avoid depending
// on the TLS trickery in `jni-rs`
jvm.detach_current_thread();
ndk_context::release_android_context();
}
rust_glue.notify_main_thread_stopped_running();
});
// Wait for thread to start.
let mut guard = jvm_glue.mutex.lock().unwrap();
while !guard.running {
// Don't specifically wait for `Running` just in case `android_main` returns
// immediately and the state is set to `Stopped`
while guard.thread_state == NativeThreadState::Init {
guard = jvm_glue.cond.wait(guard).unwrap();
}
})
+134 -34
View File
@@ -1,12 +1,10 @@
use std::marker::PhantomData;
pub use ndk::event::{
Axis, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter,
use crate::input::{
Axis, Button, ButtonState, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
};
use crate::input::{Class, Source};
/// A motion event
///
/// For general discussion of motion events in Android, see [the relevant
@@ -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,25 @@ impl<'a> MotionEvent<'a> {
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionMasked())
#[inline]
pub fn action(&self) -> MotionAction {
self.ndk_event.action()
// XXX: we use `AMotionEvent_getAction` directly since we have our own
// `MotionAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let action =
unsafe { ndk_sys::AMotionEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 };
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 +108,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 +120,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),
},
}
}
/*
@@ -136,7 +153,7 @@ impl<'a> MotionEvent<'a> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getmetastate)
#[inline]
pub fn meta_state(&self) -> MetaState {
self.ndk_event.meta_state()
self.ndk_event.meta_state().into()
}
/// Returns the button state during this event, as a bitfield.
@@ -145,7 +162,7 @@ impl<'a> MotionEvent<'a> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getbuttonstate)
#[inline]
pub fn button_state(&self) -> ButtonState {
self.ndk_event.button_state()
self.ndk_event.button_state().into()
}
/// Returns the time of the start of this gesture, in the `java.lang.System.nanoTime()` time
@@ -164,7 +181,7 @@ impl<'a> MotionEvent<'a> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getedgeflags)
#[inline]
pub fn edge_flags(&self) -> EdgeFlags {
self.ndk_event.edge_flags()
self.ndk_event.edge_flags().into()
}
/// Returns the time of this event, in the `java.lang.System.nanoTime()` time base
@@ -182,7 +199,7 @@ impl<'a> MotionEvent<'a> {
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getflags)
#[inline]
pub fn flags(&self) -> MotionEventFlags {
self.ndk_event.flags()
self.ndk_event.flags().into()
}
/* Missing from GameActivity currently...
@@ -224,6 +241,77 @@ 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<'a> PointerImpl<'a> {
#[inline]
pub fn pointer_index(&self) -> usize {
self.ndk_pointer.pointer_index()
}
#[inline]
pub fn pointer_id(&self) -> i32 {
self.ndk_pointer.pointer_id()
}
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
let value: u32 = axis.into();
if let Ok(ndk_axis) = value.try_into() {
self.ndk_pointer.axis_value(ndk_axis)
} else {
// FIXME: We should also be able to query `Axis::__Unknown(u32)` values
// that can't currently be queried via the `ndk` `Pointer` API
0.0f32
}
}
#[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: u32 = self.ndk_pointer.tool_type().into();
value.into()
}
}
/// An iterator over the pointers in a [`MotionEvent`].
#[derive(Debug)]
pub(crate) struct PointersIterImpl<'a> {
ndk_pointers_iter: ndk::event::PointersIter<'a>,
}
impl<'a> Iterator for PointersIterImpl<'a> {
type Item = Pointer<'a>;
fn next(&mut self) -> Option<Pointer<'a>> {
self.ndk_pointers_iter.next().map(|ndk_pointer| Pointer {
inner: PointerImpl { ndk_pointer },
})
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.ndk_pointers_iter.size_hint()
}
}
impl<'a> ExactSizeIterator for PointersIterImpl<'a> {
fn len(&self) -> usize {
self.ndk_pointers_iter.len()
}
}
/// A key event
///
/// For general discussion of key events in Android, see [the relevant
@@ -251,18 +339,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 +358,11 @@ impl<'a> KeyEvent<'a> {
/// See [the KeyEvent docs](https://developer.android.com/reference/android/view/KeyEvent#getAction())
#[inline]
pub fn action(&self) -> KeyAction {
self.ndk_event.action()
// XXX: we use `AInputEvent_getAction` directly since we have our own
// `KeyAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let action = unsafe { ndk_sys::AKeyEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 };
action.into()
}
/// Returns the last time the key was pressed. This is on the scale of
@@ -306,7 +391,12 @@ impl<'a> KeyEvent<'a> {
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getkeycode)
#[inline]
pub fn key_code(&self) -> Keycode {
self.ndk_event.key_code()
// XXX: we use `AInputEvent_getKeyCode` directly since we have our own
// `Keycode` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let keycode =
unsafe { ndk_sys::AKeyEvent_getKeyCode(self.ndk_event.ptr().as_ptr()) as u32 };
keycode.into()
}
/// Returns the number of repeats of a key.
@@ -326,6 +416,15 @@ impl<'a> KeyEvent<'a> {
pub fn scan_code(&self) -> i32 {
self.ndk_event.scan_code()
}
/// Returns the state of the modifiers during this key event, represented by a bitmask.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getmetastate)
#[inline]
pub fn meta_state(&self) -> MetaState {
self.ndk_event.meta_state().into()
}
}
// We use our own wrapper type for input events to have better consistency
@@ -337,4 +436,5 @@ impl<'a> KeyEvent<'a> {
pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
}
+180 -44
View File
@@ -1,14 +1,22 @@
#![cfg(any(feature = "native-activity", doc))]
use std::collections::HashMap;
use std::marker::PhantomData;
use std::panic::AssertUnwindSafe;
use std::ptr;
use std::ptr::NonNull;
use std::sync::{Arc, RwLock};
use std::sync::{Arc, Mutex, RwLock, Weak};
use std::time::Duration;
use 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::{
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
};
@@ -78,13 +86,26 @@ impl AndroidAppWaker {
}
impl AndroidApp {
pub(crate) fn new(native_activity: NativeActivityGlue) -> Self {
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
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),
})),
};
@@ -121,8 +142,23 @@ unsafe impl Sync for Looper {}
#[derive(Debug)]
pub(crate) struct AndroidAppInner {
pub(crate) jvm: CloneJavaVM,
pub(crate) native_activity: NativeActivityGlue,
looper: Looper,
/// Shared JNI bindings for the `KeyCharacterMap` class
key_map_binding: Arc<KeyCharacterMapBinding>,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
/// characters
key_maps: Mutex<HashMap<i32, KeyCharacterMap>>,
/// While an app is reading input events it holds an
/// InputReceiver reference which we track to ensure
/// we don't hand out more than one receiver at a time
input_receiver: Mutex<Option<Weak<InputReceiver>>>,
}
impl AndroidAppInner {
@@ -149,14 +185,14 @@ impl AndroidAppInner {
pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
where
F: FnMut(PollEvent),
F: FnMut(PollEvent<'_>),
{
trace!("poll_events");
unsafe {
let mut fd: i32 = 0;
let mut events: i32 = 0;
let mut source: *mut core::ffi::c_void = ptr::null_mut();
let mut source: *mut c_void = ptr::null_mut();
let timeout_milliseconds = if let Some(timeout) = timeout {
timeout.as_millis() as i32
@@ -173,7 +209,7 @@ impl AndroidAppInner {
timeout_milliseconds,
&mut fd,
&mut events,
&mut source as *mut *mut core::ffi::c_void,
&mut source as *mut *mut c_void,
);
trace!("pollAll id = {id}");
match id {
@@ -341,59 +377,71 @@ impl AndroidAppInner {
}
}
pub fn enable_motion_axis(&self, _axis: input::Axis) {
// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
TextInputState {
text: String::new(),
selection: TextSpan { start: 0, end: 0 },
compose_region: None,
}
}
// TODO: move into a trait
pub fn set_text_input_state(&self, _state: TextInputState) {
// NOP: Unsupported
}
pub fn device_key_character_map(&self, device_id: i32) -> InternalResult<KeyCharacterMap> {
let mut guard = self.key_maps.lock().unwrap();
let key_map = match guard.entry(device_id) {
std::collections::hash_map::Entry::Occupied(occupied) => occupied.get().clone(),
std::collections::hash_map::Entry::Vacant(vacant) => {
let character_map = jni_utils::device_key_character_map(
self.jvm.clone(),
self.key_map_binding.clone(),
device_id,
)?;
vacant.insert(character_map.clone());
character_map
}
};
Ok(key_map)
}
pub fn enable_motion_axis(&self, _axis: Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
pub fn disable_motion_axis(&self, _axis: input::Axis) {
pub fn disable_motion_axis(&self, _axis: Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
pub fn input_events<F>(&self, mut callback: F)
where
F: FnMut(&input::InputEvent) -> InputStatus,
{
pub fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
let mut guard = self.input_receiver.lock().unwrap();
if let Some(receiver) = &*guard {
if receiver.strong_count() > 0 {
return Err(crate::error::InternalAppError::InputUnavailable);
}
}
*guard = None;
// Get the InputQueue for the NativeActivity (if there is one) and also ensure
// the queue is re-attached to our event Looper (so new input events will again
// trigger a wake up)
let queue = self
.native_activity
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
let queue = match queue {
Some(queue) => queue,
None => return,
};
// Note: we basically ignore errors from get_event() currently. Looking
// at the source code for Android's InputQueue, the only error that
// can be returned here is 'WOULD_BLOCK', which we want to just treat as
// meaning the queue is empty.
//
// ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
//
while let Ok(Some(event)) = queue.get_event() {
if let Some(ndk_event) = queue.pre_dispatch(event) {
let event = match ndk_event {
ndk::event::InputEvent::MotionEvent(e) => {
input::InputEvent::MotionEvent(input::MotionEvent::new(e))
}
ndk::event::InputEvent::KeyEvent(e) => {
input::InputEvent::KeyEvent(input::KeyEvent::new(e))
}
};
let handled = callback(&event);
// Note: we don't treat it as an error if there is no queue, so if applications
// iterate input before a queue has been created (e.g. before onStart) then
// it will simply behave like there are no events available currently.
let receiver = Arc::new(InputReceiver { queue });
let ndk_event = match event {
input::InputEvent::MotionEvent(e) => {
ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
}
input::InputEvent::KeyEvent(e) => {
ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
}
};
queue.finish_event(ndk_event, matches!(handled, InputStatus::Handled));
}
}
*guard = Some(Arc::downgrade(&receiver));
Ok(receiver)
}
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
@@ -411,3 +459,91 @@ impl AndroidAppInner {
unsafe { util::try_get_path_from_ptr((*na).obbPath) }
}
}
#[derive(Debug)]
pub(crate) struct InputReceiver {
queue: Option<InputQueue>,
}
impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
fn from(receiver: Arc<InputReceiver>) -> Self {
Self {
receiver,
_lifetime: PhantomData,
}
}
}
pub(crate) struct InputIteratorInner<'a> {
// Held to maintain exclusive access to buffered input events
receiver: Arc<InputReceiver>,
_lifetime: PhantomData<&'a ()>,
}
impl<'a> InputIteratorInner<'a> {
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 {
log::trace!("no queue available for events");
return false;
};
// Note: we basically ignore errors from event() currently. Looking at the source code for
// Android's InputQueue, the only error that can be returned here is 'WOULD_BLOCK', which we
// want to just treat as meaning the queue is empty.
//
// ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
//
if let Ok(Some(ndk_event)) = queue.event() {
log::trace!("queue: got event: {ndk_event:?}");
if let Some(ndk_event) = queue.pre_dispatch(ndk_event) {
let event = match ndk_event {
ndk::event::InputEvent::MotionEvent(e) => {
input::InputEvent::MotionEvent(input::MotionEvent::new(e))
}
ndk::event::InputEvent::KeyEvent(e) => {
input::InputEvent::KeyEvent(input::KeyEvent::new(e))
}
};
// `finish_event` needs to be called for each event otherwise
// the app would likely get an ANR
let result = std::panic::catch_unwind(AssertUnwindSafe(|| callback(&event)));
let ndk_event = match event {
input::InputEvent::MotionEvent(e) => {
ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
}
input::InputEvent::KeyEvent(e) => {
ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
}
_ => unreachable!(),
};
let handled = match result {
Ok(handled) => handled,
Err(payload) => {
log::error!("Calling `finish_event` after panic in input event handler, to try and avoid being killed via an ANR");
queue.finish_event(ndk_event, false);
std::panic::resume_unwind(payload);
}
};
log::trace!("queue: finishing event");
queue.finish_event(ndk_event, handled == InputStatus::Handled);
}
true
} else {
log::trace!("queue: no more events");
false
}
}
}
+2 -6
View File
@@ -1,12 +1,8 @@
*.iml
.idea
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea
.DS_Store
/build
/captures
-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>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-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>
+3 -3
View File
@@ -9,9 +9,9 @@ edition = "2021"
log = "0.4"
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = ["game-activity"] }
ndk-sys = "0.4"
ndk = "0.7"
ndk-sys = "0.5.0"
ndk = "0.8.0"
[lib]
name="main"
crate_type=["cdylib"]
crate_type=["cdylib"]
+7 -11
View File
@@ -12,8 +12,6 @@ android {
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -38,12 +36,10 @@ android {
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "androidx.core:core:1.5.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation 'androidx.fragment:fragment:1.2.5'
implementation 'com.google.oboe:oboe:1.5.0'
// To use the Android Frame Pacing library
//implementation "androidx.games:games-frame-pacing:1.9.1"
@@ -52,12 +48,12 @@ dependencies {
//implementation "androidx.games:games-performance-tuner:1.5.0"
// To use the Games Activity library
implementation "androidx.games:games-activity:1.1.0"
implementation "androidx.games:games-activity:2.0.2"
// To use the Games Controller Library
//implementation "androidx.games:games-controller:1.1.0"
//implementation "androidx.games:games-controller:2.0.2"
// To use the Games Text Input Library
//implementation "androidx.games:games-text-input:1.1.0"
//implementation "androidx.games:games-text-input:2.0.2"
}
@@ -63,6 +63,11 @@ public class MainActivity extends GameActivity {
super.onCreate(savedInstanceState);
}
protected void onResume() {
super.onResume();
hideSystemUI();
}
public boolean isGooglePlayGames() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -1,16 +1,3 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="Theme.RustTemplate" parent="Theme.AppCompat.Light.NoActionBar" />
</resources>
+123 -6
View File
@@ -1,4 +1,7 @@
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
use android_activity::{
input::{InputEvent, KeyAction, KeyEvent, KeyMapChar, MotionAction},
AndroidApp, InputStatus, MainEvent, PollEvent,
};
use log::info;
#[no_mangle]
@@ -9,6 +12,8 @@ fn android_main(app: AndroidApp) {
let mut redraw_pending = true;
let mut native_window: Option<ndk::native_window::NativeWindow> = None;
let mut combining_accent = None;
while !quit {
app.poll_events(
Some(std::time::Duration::from_secs(1)), /* timeout */
@@ -68,11 +73,56 @@ fn android_main(app: AndroidApp) {
if let Some(native_window) = &native_window {
redraw_pending = false;
// Handle input
app.input_events(|event| {
info!("Input Event: {event:?}");
InputStatus::Unhandled
});
// Handle input, via a lending iterator
match app.input_events_iter() {
Ok(mut iter) => loop {
info!("Checking for next input event...");
if !iter.next(|event| {
match event {
InputEvent::KeyEvent(key_event) => {
let combined_key_char = character_map_and_combine_key(
&app,
key_event,
&mut combining_accent,
);
info!("KeyEvent: combined key: {combined_key_char:?}")
}
InputEvent::MotionEvent(motion_event) => {
println!("action = {:?}", motion_event.action());
match motion_event.action() {
MotionAction::Up => {
let pointer = motion_event.pointer_index();
let pointer =
motion_event.pointer_at_index(pointer);
let x = pointer.x();
let y = pointer.y();
println!("POINTER UP {x}, {y}");
if x < 200.0 && y < 200.0 {
println!("Requesting to show keyboard");
app.show_soft_input(true);
}
}
_ => {}
}
}
InputEvent::TextEvent(state) => {
info!("Input Method State: {state:?}");
}
_ => {}
}
info!("Input Event: {event:?}");
InputStatus::Unhandled
}) {
info!("No more input available");
break;
}
},
Err(err) => {
log::error!("Failed to get input events iterator: {err:?}");
}
}
info!("Render...");
dummy_render(native_window);
@@ -83,6 +133,73 @@ fn android_main(app: AndroidApp) {
}
}
/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent
///
/// This shows how to take a `KeyEvent` and look up its corresponding `KeyCharacterMap` and
/// use that to try and map the `key_code` + `meta_state` to a unicode character or a
/// dead key that be combined with the next key press.
fn character_map_and_combine_key(
app: &AndroidApp,
key_event: &KeyEvent,
combining_accent: &mut Option<char>,
) -> Option<KeyMapChar> {
let device_id = key_event.device_id();
let key_map = match app.device_key_character_map(device_id) {
Ok(key_map) => key_map,
Err(err) => {
log::error!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}");
return None;
}
};
match key_map.get(key_event.key_code(), key_event.meta_state()) {
Ok(KeyMapChar::Unicode(unicode)) => {
// Only do dead key combining on key down
if key_event.action() == KeyAction::Down {
let combined_unicode = if let Some(accent) = combining_accent {
match key_map.get_dead_char(*accent, unicode) {
Ok(Some(key)) => {
info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
Some(key)
}
Ok(None) => None,
Err(err) => {
log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
None
}
}
} else {
info!("KeyEvent: Pressed '{unicode}'");
Some(unicode)
};
*combining_accent = None;
combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
} else {
Some(KeyMapChar::Unicode(unicode))
}
}
Ok(KeyMapChar::CombiningAccent(accent)) => {
if key_event.action() == KeyAction::Down {
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
*combining_accent = Some(accent);
}
Some(KeyMapChar::CombiningAccent(accent))
}
Ok(KeyMapChar::None) => {
// Leave any combining_accent state in tact (seems to match how other
// Android apps work)
info!("KeyEvent: Pressed non-unicode key");
None
}
Err(err) => {
log::error!("KeyEvent: Failed to get key map character: {err:?}");
*combining_accent = None;
None
}
}
}
/// Post a NOP frame to the window
///
/// Since this is a bare minimum test app we don't depend
-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>
+3 -3
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.4"
ndk = "0.7"
ndk-sys = "0.5.0"
ndk = "0.8.0"
[lib]
#name="na_mainloop"
@@ -181,4 +181,4 @@ label = "Application Name"
#port = "8080"
#path = "/rust-windowing/android-ndk-rs/tree/master/cargo-apk"
#path_prefix = "/rust-windowing/"
#mime_type = "image/jpeg"
#mime_type = "image/jpeg"
+123 -6
View File
@@ -1,4 +1,7 @@
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
use android_activity::{
input::{InputEvent, KeyAction, KeyEvent, KeyMapChar, MotionAction},
AndroidApp, InputStatus, MainEvent, PollEvent,
};
use log::info;
#[no_mangle]
@@ -9,6 +12,8 @@ fn android_main(app: AndroidApp) {
let mut redraw_pending = true;
let mut native_window: Option<ndk::native_window::NativeWindow> = None;
let mut combining_accent = None;
while !quit {
app.poll_events(
Some(std::time::Duration::from_secs(1)), /* timeout */
@@ -68,11 +73,56 @@ fn android_main(app: AndroidApp) {
if let Some(native_window) = &native_window {
redraw_pending = false;
// Handle input
app.input_events(|event| {
info!("Input Event: {event:?}");
InputStatus::Unhandled
});
// Handle input, via a lending iterator
match app.input_events_iter() {
Ok(mut iter) => loop {
info!("Checking for next input event...");
if !iter.next(|event| {
match event {
InputEvent::KeyEvent(key_event) => {
let combined_key_char = character_map_and_combine_key(
&app,
key_event,
&mut combining_accent,
);
info!("KeyEvent: combined key: {combined_key_char:?}")
}
InputEvent::MotionEvent(motion_event) => {
println!("action = {:?}", motion_event.action());
match motion_event.action() {
MotionAction::Up => {
let pointer = motion_event.pointer_index();
let pointer =
motion_event.pointer_at_index(pointer);
let x = pointer.x();
let y = pointer.y();
println!("POINTER UP {x}, {y}");
if x < 200.0 && y < 200.0 {
println!("Requesting to show keyboard");
app.show_soft_input(true);
}
}
_ => {}
}
}
InputEvent::TextEvent(state) => {
info!("Input Method State: {state:?}");
}
_ => {}
}
info!("Input Event: {event:?}");
InputStatus::Unhandled
}) {
info!("No more input available");
break;
}
},
Err(err) => {
log::error!("Failed to get input events iterator: {err:?}");
}
}
info!("Render...");
dummy_render(native_window);
@@ -83,6 +133,73 @@ fn android_main(app: AndroidApp) {
}
}
/// Tries to map the `key_event` to a `KeyMapChar` containing a unicode character or dead key accent
///
/// This shows how to take a `KeyEvent` and look up its corresponding `KeyCharacterMap` and
/// use that to try and map the `key_code` + `meta_state` to a unicode character or a
/// dead key that be combined with the next key press.
fn character_map_and_combine_key(
app: &AndroidApp,
key_event: &KeyEvent,
combining_accent: &mut Option<char>,
) -> Option<KeyMapChar> {
let device_id = key_event.device_id();
let key_map = match app.device_key_character_map(device_id) {
Ok(key_map) => key_map,
Err(err) => {
log::error!("Failed to look up `KeyCharacterMap` for device {device_id}: {err:?}");
return None;
}
};
match key_map.get(key_event.key_code(), key_event.meta_state()) {
Ok(KeyMapChar::Unicode(unicode)) => {
// Only do dead key combining on key down
if key_event.action() == KeyAction::Down {
let combined_unicode = if let Some(accent) = combining_accent {
match key_map.get_dead_char(*accent, unicode) {
Ok(Some(key)) => {
info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
Some(key)
}
Ok(None) => None,
Err(err) => {
log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
None
}
}
} else {
info!("KeyEvent: Pressed '{unicode}'");
Some(unicode)
};
*combining_accent = None;
combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
} else {
Some(KeyMapChar::Unicode(unicode))
}
}
Ok(KeyMapChar::CombiningAccent(accent)) => {
if key_event.action() == KeyAction::Down {
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
*combining_accent = Some(accent);
}
Some(KeyMapChar::CombiningAccent(accent))
}
Ok(KeyMapChar::None) => {
// Leave any combining_accent state in tact (seems to match how other
// Android apps work)
info!("KeyEvent: Pressed non-unicode key");
None
}
Err(err) => {
log::error!("KeyEvent: Failed to get key map character: {err:?}");
*combining_accent = None;
None
}
}
}
/// Post a NOP frame to the window
///
/// Since this is a bare minimum test app we don't depend