Compare commits

..

157 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
Robert Bragg 0f00a58a41 Merge pull request #90 from rust-mobile/release-0.4.2
Release 0.4.2
2023-06-27 17:39:52 +01:00
Robert Bragg 9a713c823d Release 0.4.2 2023-06-27 17:19:52 +01:00
Robert Bragg 230035526b Update na-mainloop 2023-06-26 00:08:48 +01:00
Robert Bragg cd81420638 Update agdk-mainloop 2023-06-26 00:08:42 +01:00
Robert Bragg 1ad3abd934 Merge pull request #87 from rust-mobile/rust-version
cargo: Fix `rust_version` -> `rust-version` property typo
2023-06-25 21:38:48 +01:00
Marijn Suijten ca0d2eb3aa cargo: Fix rust_version -> rust-version property typo
Cargo complains:

    warning: android-activity/Cargo.toml: unused manifest key: package.rust_version

Solve this by replacing the underscore with a hyphen.
2023-06-24 00:43:14 +02:00
Robert Bragg 79e03e08fb Merge pull request #81 from rib/finish-activity
Call Activity.finish() when android_main returns
2023-06-19 21:05:41 +01:00
Robert Bragg 120d2f66c7 Merge pull request #86 from rust-mobile/readme-badges
README: Add badges to CI, crates.io, docs.rs and show the MSRV
2023-06-16 19:34:51 +01:00
Marijn Suijten 4a4efd871a README: Add badges to CI, crates.io, docs.rs and show the MSRV
I was trying to quickly get to the documentation of this crate and had
the GitHub page open... but there was no link on the front-page: let's
fix that.
2023-06-16 17:37:41 +02:00
Robert Bragg 3843a7cfaa Call Activity.finish() when android_main returns
Calling Activity.finish() is what ensures the Activity will get
gracefully destroyed, including calling the Activity's onDestroy
method.

Fixes: #67
2023-05-24 23:10:29 +01:00
Robert Bragg 924e5405c2 Merge pull request #84 from yunsash/pointer_index_fix
game_activity: Fix `pointer_index()` always returning `0`
2023-05-24 22:39:49 +01:00
kukie 049e660219 fix: pointer_index always returns 0 2023-05-23 15:43:03 +03:00
Robert Bragg 6559dc8133 Merge pull request #68 from notgull/abort-guards
Add panic guards to extern "C" functions
2023-04-27 20:55:55 +01:00
jtnunley d6ccefaf77 Add catch unwind wrappers to extern "C" functions
Co-authored-by: Robert Bragg <robert@sixbynine.org>
2023-04-27 20:42:45 +01:00
Robert Bragg 9229bb20c1 Merge pull request #77 from rib/rust-version-0-64-typo
Fix rust_version typo s/0.64/1.64/
2023-04-24 16:12:08 +01:00
Robert Bragg 4976cbad44 Fix rust_version typo s/0.64/1.64/
Also removes a stale comment about MSVR policy in ci.yml
2023-04-24 11:42:17 +01:00
jtnunley e0c96ad6b4 Dedup android_log 2023-04-24 11:36:27 +01:00
Robert Bragg c70e5d852f Merge pull request #73 from rust-mobile/dependabot/github_actions/actions/checkout-3
build(deps): bump actions/checkout from 2 to 3
2023-04-23 20:00:03 +01:00
Robert Bragg f28d6adc55 Merge pull request #74 from rust-mobile/dependabot/cargo/num_enum-0.6
build(deps): update num_enum requirement from 0.5 to 0.6
2023-04-23 19:59:50 +01:00
dependabot[bot] 0fa6888484 build(deps): update num_enum requirement from 0.5 to 0.6
Updates the requirements on [num_enum](https://github.com/illicitonion/num_enum) to permit the latest version.
- [Release notes](https://github.com/illicitonion/num_enum/releases)
- [Commits](https://github.com/illicitonion/num_enum/compare/0.5.0...0.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-23 15:14:30 +00:00
dependabot[bot] d4a3d5845d build(deps): bump actions/checkout from 2 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [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/v2...v3)

---
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-04-23 15:11:09 +00:00
Robert Bragg c91745a39d Merge pull request #76 from rib/cargo-ndk-2-for-ci
Add MSRV policy to README and bump `rust_version` to 0.64, due to `ndk` -> `raw_window_handle` dependency
2023-04-23 16:10:34 +01:00
Robert Bragg 1b44d38822 Bump MSRV to 0.64 and and MSRV policy to README 2023-04-23 15:57:07 +01:00
Robert Bragg 9ac7891664 CI: test with cargo-ndk 2, which compiles with rustc 1.60 2023-04-22 17:37:55 +01:00
Robert Bragg 03b06b8c5e Merge pull request #70 from MarijnS95/na-resize
native-activity: Propagate `NativeWindow` redraw/resize and `ContentRectChanged` callbacks to main loop
2023-04-19 13:44:52 +01:00
Robert Bragg caf2a624b2 Merge pull request #72 from notgull/dependabot
Add dependabot support
2023-04-19 13:41:41 +01:00
Marijn Suijten fe9d68c99e Clean up partial module imports
Some items were imported in scope while most of the code uses fully
qualified names.
2023-04-19 14:09:39 +02:00
Marijn Suijten 0c3f16c9ba native-activity: Propagate onContentRectChanged callback to main loop 2023-04-19 14:09:30 +02:00
Marijn Suijten 87fe6a8465 native-activity: Propagate onNativeWindowRedrawNeeded callback to main loop 2023-04-19 14:09:24 +02:00
Marijn Suijten a04c483d79 native-activity: Propagate onNativeWindowResized callback to main loop 2023-04-19 14:09:12 +02:00
jtnunley 507cfe072e Add dependabot support 2023-04-10 09:45:05 -07:00
Robert Bragg 36ddfaa9ce Merge pull request #64 from rib/release-0.4.1
Release 0.4.1
2023-02-15 15:38:40 +00:00
Robert Bragg b2467cb028 Release 0.4.1 2023-02-15 14:28:55 +00:00
Robert Bragg 6dd3c0c02a Merge pull request #62 from rib/rust-mobile-org-urls
Update Github URL since moving to rust-mobile org
2023-02-14 12:02:18 +00:00
Robert Bragg 6ae2e427d6 Merge pull request #61 from rib/jni-vm-and-activity-ptrs
Add AndroidApp::vm_as_ptr() and ::activity_as_ptr() APIs
2023-02-14 12:01:33 +00:00
Robert Bragg dc9ca832c5 Add AndroidApp::vm_as_ptr() and ::activity_as_ptr() APIs
This enables applications to make JNI calls without needing the
`ndk-context` crate - which we would like to deprecate.

Fixes: #60
2023-02-14 11:49:38 +00:00
Robert Bragg 9d06f62a4e Update Github URL since moving to rust-mobile org 2023-02-13 17:20:11 +00:00
Robert Bragg 6942637c3c Merge pull request #56 from rib/remove-example-lock-files
Remove Cargo.lock files for examples
2022-12-23 01:25:51 +00:00
Robert Bragg 47b2e279e0 Remove Cargo.lock files for examples
When splitting out the rust-android-examples we kept the agdk-mainloop
and na-mainloop examples in part so we would have some simple code we
can build as integration tests.

Since it's less likely that these will be referenced directly as
examples now, compared to those in rust-android-examples this removes
the lock files so we will instead always build against the latest semver
compatible dependencies.

Considering the simplicity of these examples, and minimal dependencies
these lock files probably weren't that worthwhile before either.
2022-12-23 01:00:57 +00:00
Robert Bragg 40fb012000 Merge pull request #52 from rib/update-readme-example-links
README: update example links
2022-12-21 23:28:48 +00:00
Robert Bragg 4155fbc9db README: update example links 2022-12-21 23:27:09 +00:00
Robert Bragg 435e183a58 Merge pull request #50 from rib/remove-most-examples
Remove most examples (see https://github.com/rust-mobile/rust-android-examples)
2022-12-21 16:15:30 +00:00
Robert Bragg 2e279210a3 Remove most examples
Most of the examples weren't strictly just demonstrating how to use the
android-activity API - rather they demonstrated using other libraries
in conjunction with android-activity.

Most of the examples have now been split into a standalone repository
under: https://github.com/rust-mobile/rust-android-examples

The na-mainloop and agdk-mainloop examples have been kept here since
they can be built against the local/in-tree version of android-activity
and are useful to keep for CI purposes.

This also runs `cargo update` for the na-mainloop and agdk-mainloop.
2022-12-20 19:55:10 +00:00
Robert Bragg 14eb2f0a17 Merge pull request #49 from rust-mobile/demote-spam
native_activity: Demote spammy `info!`/`eprintln!` debug logs to `trace!`
2022-12-20 16:59:40 +00:00
Robert Bragg 8f68cb5db9 Merge pull request #47 from rust-mobile/remove-unneeded-backslash-escape
Remove backslash-escaping of quotes in raw string
2022-12-20 16:29:53 +00:00
Marijn Suijten b217ad546c Remove backslash-escaping of quotes in raw string
In a raw string [no escaping is processed] nor are these quotes treated
specially unless it was postfixed with `#`; the backslashes show up in
the compiler error displayed to the user instead.

For consistency the string this was likely copied from is now also
turned into a raw string literal, with backslashes equivalently removed.

[no escaping is processed]: https://doc.rust-lang.org/reference/tokens.html#raw-string-literals
2022-12-14 20:51:39 +01:00
Marijn Suijten bf251c63cd native_activity: Demote spammy info!/eprintln! debug logs to trace!
Ideally information - especially when spamming per `Looper` poll - used
for debugging `android-activity` doesn't show to the user unless they
use the `Trace` level (eventually for this specific crate/module).  This
is already adhered to in most places of the code but there were a few
high-volume cases still remaining.
2022-12-14 20:09:16 +01:00
Robert Bragg 1633f62d0d Merge pull request #39 from rib/release-0.4
Release 0.4
2022-11-14 16:06:52 +00:00
Robert Bragg 6ca4ea2e30 Release 0.4.0 2022-11-13 15:15:24 +00:00
Robert Bragg 9640893477 Update README 2022-11-13 15:15:24 +00:00
Robert Bragg fab31d3408 Thin MotionEvent + KeyEvent types with lifetimes
This patch is addressing two notable issues:
1. MotionEvents with the GameActivity backend were extremely large and
   forced us to copy lots of redundant padding from the internal
   circular buffer of events when calling an apps `input_events()`
   callback.
2. MotionEvents + KeyEvents with the NativeActivity backend (re-exported
   from the ndk crate) simply wrapped a raw pointer but had no lifetime
   that would stop applications from keeping them too long and then
   potentially dereferencing an invalid pointer.

The common change is that `InputEvent` is now defined with a lifetime
parameter which makes it possible for Motion and Key events to hold
references that let them avoid copying large amounts of state.

The `Source` and `Class` enums were also moved out of the GameActivity
backend so they could be shared since there was some inconsistency
between the backends with these types.

Overall this doesn't, practically, affect the public API

GameActivity
============

On the GameActivity side the events will now point into the internal
circular buffer that is iterated during `input_events()` and so
the `MotionEvent` type is now a thin wrapper over a reference.

This removes the `Iterator` implementations we had internally for
iterating key/motion events because we effectively need a lending
iterator now. It's also noted that while our MSRV is 1.60 we can't use
GATs to implement the interation in terms of a LendingIterator trait.
(This is fine in practice because this iteration is a private
implementation detail and so we can have lending iteration without any
traits for now)

NativeActivity
==============

On the NativeActivity side we now have newtype wrappers around
MotionEvent and KeyEvent from the ndk crate. These newtypes add a
lifetime and implement all the same passthrough methods as the
corresponding GameActivity types.

This added more boilerplate to the NativeActivity backend but it
also improves consistency between the backends.

Fixes: #40
Fixes: #41
2022-11-11 20:44:19 +00:00
Robert Bragg 38554d98db Clippy fixes 2022-11-11 20:44:19 +00:00
Robert Bragg aa5fcc53bb Improve docs
This removes the indirection of the `StateSaver` and `StateLoader`
type aliases (which resulted in no documentation for these interfaces)
and we now simply re-export the types from the backend implementation.
2022-11-11 20:44:18 +00:00
Robert Bragg 4669508823 examples/*-winit-wgpu: make less verbose
This updates the *-winit-wgpu examples to use `log::info!` instead of `trace!`
and sets the log level to `Info`

The examples now also print info about and Winit Window events.

This makes them more practical to use to see how Winit events
are delivered without lots of tracing spam from dependency crates.
2022-11-11 20:44:18 +00:00
Robert Bragg 649b65c0c0 Update examples
- Updates deps
- Some README updates considering the Winit backend based on
  android-activity has been merged upstream
- runs cargo fmt over examples
- Top-level Cargo.toml simply excludes "examples" instead of
  listing each sub-directory separately
2022-11-10 19:28:01 +00:00
Robert Bragg c75f33a81e game_activity/input: support Pointer::tool_type()
This maintains compatibility with the `ndk` crate's `Pointer` API

Ref: https://github.com/rust-windowing/android-ndk-rs/pull/323

This will also be required for enabling pen pressure support to
Winit, re: https://github.com/rust-windowing/winit/pull/2396
2022-10-23 01:36:56 +01:00
Robert Bragg bd8cc86ec7 Adds a minimal na-winit-glutin example
This example shows how to draw a triangle with GL[ES via the Glutin
crate (the app works on desktop and Android) and on Android it
demonstrates how to re-create the applications surface state when
it is paused and resumed.

The Renderer code and some utilities are from the upstream Glutin example.
2022-10-23 00:51:32 +01:00
Robert Bragg e8ae198653 Update example deps + update for latest winit branch
The winit based examples no longer have an explicit dependency on
android-activity and they instead consume the `android-activity` API via
the Winit crate so there's no need to keep the versions synchronized.
2022-10-22 21:51:27 +01:00
Robert Bragg 1ece2ad87d Update README.md
Update example (copy from na-mainloop)
2022-10-18 19:38:22 +01:00
Robert Bragg 8077a4b0da Update Winit-based examples for Release 0.4.0-beta.1 2022-10-11 20:19:15 +01:00
Robert Bragg 364dffb2f7 Release 0.4.0-beta.1 2022-10-11 19:44:18 +01:00
Robert Bragg 0590bf601a Merge pull request #35 from rib/pure-rust-native-activity
Pure-Rust native activity backend
2022-10-11 19:12:40 +01:00
Robert Bragg c3d115fd7b CHANGELOG: note that native-activity backend is pure-Rust now 2022-10-11 18:50:11 +01:00
Robert Bragg 1ed7d383e0 native-activity: track saved state as a Vec<u8>
This simplifies the tracking of saved state by simply using a Vec and
only copying into a `malloc()` allocation when passing the saved state
to `ANativeActivity`.

This also ensures saved state persists (in Rust) between a
`MainEvent::SaveState` event and a `Resume` event - otherwise the saved
state would only be restored in the situation where `onCreate` is called
again for a new process.
2022-10-11 18:50:05 +01:00
Robert Bragg 2e25e2ebed native-activity: fix 1.60.0 compilation - don't derive Default for enum 2022-10-11 17:57:18 +01:00
Robert Bragg 949386ea4e native-activity: fix Weak ref from_raw + upgrade + into_raw handling for callbacks
This adds a common `try_with_waitable_activity_ref` utility that handles
upgrading the Weak reference associated with `ANativeActivity` whenever
we get an activity callback.

Previously we weren't converting the Weak reference back to a pointer
before returning from the callback which would result in us dropping the
Weak reference after the first callback.
2022-10-11 17:14:04 +01:00
Robert Bragg 879d7cac5a native-activity: use eprintln for early logging (before app sets up logging) 2022-10-11 17:10:15 +01:00
Robert Bragg cd32b4a064 native-activity: re-work ported glue code into idiomatic Rust
This is a fairly thorough re-working of all the code that was initially
naively ported from android_native_app_glue.c to be more idiomatic Rust
code (though by it's nature it still involves a lot of unsafe code)

Most of the glue code now lives in src/native_activity/glue.rs as part
of a NativeActivityGlue API

The design as far as the threading model, IPC and synchronization goes
is unchanged.
2022-10-08 14:39:21 +01:00
Robert Bragg 2870a84fbe native-activity: rename ported android_app state to be more idiomatic 2022-10-08 14:39:21 +01:00
Robert Bragg 3de1433f82 native-activity: complete (first-pass) port of C code to Rust
At this point the C code has been fully ported to Rust but it's not yet
well integrated with the pre-existing Rust code and the port is not yet
particularly idiomatic Rust code.
2022-10-08 14:39:21 +01:00
Robert Bragg 1507b37425 native-activity: port android_app_entry (mainloop init) from C to Rust 2022-10-08 14:39:21 +01:00
Robert Bragg 1314210be4 native-activity: complete port of sender/java-side C code to Rust 2022-10-08 14:39:21 +01:00
Robert Bragg a65729400b native-activity: Port android_app_create/free from C to Rust
A literal port for now, with the intention of re-working into more
idiomatic Rust code once we have a port of all the C code.
2022-10-08 14:39:21 +01:00
Robert Bragg 2d44950d14 native-activity: Migrate ANativeActivity callbacks to Rust
A first step towards replacing the android_native_app_glue code with a
pure Rust implementation.
2022-10-08 14:39:21 +01:00
Robert Bragg b13a53f182 Update Winit-based examples for Release 0.4.0-beta 2022-09-20 03:43:34 +01:00
Marijn Suijten b717c03dda Update README.md 2022-09-20 03:24:00 +01:00
Robert Bragg 42d6a7247e Merge pull request #33 from rib/v0.4
Release 0.4.0-beta
2022-09-20 03:21:10 +01:00
Robert Bragg 47afecda36 Release 0.4.0-beta 2022-09-20 02:56:05 +01:00
Robert Bragg 17f0c674c0 Don't ignore Cargo.lock files for examples 2022-09-20 00:41:21 +01:00
Robert Bragg 8ae1059aec na-openxr-wgpu: don't depend on specific version of android-activity 2022-09-20 00:32:09 +01:00
Robert Bragg 7cdb77eca4 Let applications report if they handled input events
The callback given to `AndroidApp::input_events()` is now expected to return
`InputStatus::Handled` or `InputStatus::Unhandled`.

When running with NativeActivity then if we know an input event hasn't been
handled we can notify the InputQueue which may result in fallback
handling.

Although the status is currently ignored with the GameActivity backend.

Since this is a breaking change that also affects the current Winit
backend this updates the winit based examples to stick with the 0.3
release of android-activity for now.

Fixes: #31
2022-09-20 00:14:38 +01:00
Robert Bragg 6245866228 Add 0.3 changes to Changelog 2022-09-19 23:12:34 +01:00
Robert Bragg 5c876308c1 examples: update deps (including bump to cpal 0.14) 2022-09-19 15:02:26 +01:00
Robert Bragg 351d0e9ddb Merge pull request #29 from msiglreith/na-winit-wgpu
update na-winit-wgpu example
2022-09-18 16:58:07 +01:00
msiglreith e689461580 na-winit-wgpu: update shader and use native-activity instead of game-activity 2022-09-18 16:10:07 +02:00
Robert Bragg ac46815956 Merge pull request #28 from rib/v0.3
Release 0.3
2022-09-17 18:09:01 +01:00
392 changed files with 14395 additions and 68410 deletions
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
+13 -42
View File
@@ -2,7 +2,7 @@ name: ci
on:
push:
branches: [main]
branches: '*'
pull_request:
env:
@@ -12,18 +12,16 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
# We need to support the same MSRV as Winit which we are currently
# assuming will be bumped to 1.60.0 soon:
# https://github.com/rust-windowing/winit/pull/2453
rust_version: [1.60.0, stable]
# See top README for MSRV policy
rust_version: [1.68.0, stable]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: hecrj/setup-rust-action@v1
- uses: hecrj/setup-rust-action@v2
with:
rust-version: ${{ matrix.rust_version }}
@@ -38,15 +36,6 @@ jobs:
- name: Install cargo-ndk
run: cargo install cargo-ndk
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Build game-activity
working-directory: android-activity
run: >
@@ -89,28 +78,6 @@ jobs:
-t x86
-o app/src/main/jniLibs/ -- build
- name: Build agdk-egui example
if: matrix.rust_version == 'stable'
working-directory: examples/agdk-egui
run: >
cargo ndk
-t arm64-v8a
-t armeabi-v7a
-t x86_64
-t x86
-o app/src/main/jniLibs/ -- build
- name: Build agdk-eframe example
if: matrix.rust_version == 'stable'
working-directory: examples/agdk-eframe
run: >
cargo ndk
-t arm64-v8a
-t armeabi-v7a
-t x86_64
-t x86
-o app/src/main/jniLibs/ -- build
- name: Documentation
run: >
cargo ndk -t arm64-v8a doc --no-deps
@@ -118,7 +85,7 @@ jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
@@ -131,6 +98,10 @@ jobs:
run: cargo fmt --all -- --check
working-directory: android-activity
- name: Format agdk-egui example
- name: Format na-mainloop example
run: cargo fmt --all -- --check
working-directory: examples/agdk-egui
working-directory: examples/na-mainloop
- name: Format agdk-mainloop example
run: cargo fmt --all -- --check
working-directory: examples/agdk-mainloop
+3 -16
View File
@@ -1,18 +1,5 @@
[workspace]
members = [
"android-activity"
]
resolver = "2"
members = ["android-activity"]
exclude = [
"examples/agdk-mainloop",
"examples/agdk-winit-wgpu",
"examples/agdk-eframe",
"examples/agdk-egui",
"examples/agdk-oboe",
"examples/agdk-cpal",
"examples/na-mainloop",
"examples/na-winit-wgpu",
"examples/na-subclass-jni",
"examples/na-openxr-info",
"examples/na-openxr-wgpu"
]
exclude = ["examples"]
+80 -166
View File
@@ -1,59 +1,66 @@
# Overview
# `android-activity`
[![ci](https://github.com/rust-mobile/android-activity/actions/workflows/ci.yml/badge.svg)](https://github.com/rust-mobile/android-activity/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/android-activity.svg)](https://crates.io/crates/android-activity)
[![Docs](https://docs.rs/android-activity/badge.svg)](https://docs.rs/android-activity)
[![MSRV](https://img.shields.io/badge/rustc-1.64.0+-ab6000.svg)](https://blog.rust-lang.org/2022/09/22/Rust-1.64.0.html)
## Overview
`android-activity` provides a "glue" layer for building native Rust
applications on Android, supporting multiple [`Activity`] base classes.
It's comparable to [`android_native_app_glue.c`][ndk_concepts]
for C/C++ applications.
for C/C++ applications and is an alternative to the [ndk-glue] crate.
`android-activity` supports [`NativeActivity`] or [`GameActivity`] from the
Android Game Development Kit and can be extended to support additional base
classes.
`android-activity` provides a way to load your crate as a `cdylib` library via
the `onCreate` method of your Android `Activity` class; run an `android_main()`
function in a separate thread from the Java main thread and marshal events (such
as lifecycle events and input events) between Java and your native thread.
`android-activity` provides a way to load a `cdylib` via the `onCreate` method of
your `Activity` class; run an `android_main()` function in a separate thread from the Java
main thread and marshal events (such as lifecycle events and input events) between
Java and your native thread.
So far it supports [`NativeActivity`] or [`GameActivity`] (from the
[Android Game Development Kit][agdk]) and there's also interest in supporting a first-party
`RustActivity` base class that could be better tailored to the needs of Rust
applications.
[`Activity`]: https://developer.android.com/reference/android/app/Activity
[`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
[ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa
[`GameActivity`]: https://developer.android.com/games/agdk/integrate-game-activity
[ndk-glue]: https://crates.io/crates/ndk-glue
[agdk]: https://developer.android.com/games/agdk
### Example
```
cargo init --lib --name=example
```
## Example
Cargo.toml
```
```toml
[dependencies]
log = "0.4"
android_logger = "0.11"
android-activity = { git = "https://github.com/rib/android-activity/", features = [ "native-activity" ] }
android-activity = { version = "0.4", features = [ "native-activity" ] }
[lib]
crate_type = ["cdylib"]
```
_Note: that you will need to either specify the **"native-activity"** feature or **"game-activity"** feature to identify which `Activity` base class your application is based on_
lib.rs
```rust
use log::info;
use android_activity::{PollEvent, MainEvent};
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
#[no_mangle]
fn android_main(app: AndroidApp) {
android_logger::init_once(
android_logger::Config::default().with_min_level(log::Level::Info)
);
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
loop {
app.poll_events(Some(std::time::Duration::from_millis(500)) /* timeout */, |event| {
match event {
PollEvent::Wake => { info!("Early wake up"); },
PollEvent::Timeout => { info!("Hello, World!"); },
PollEvent::Wake => { log::info!("Early wake up"); },
PollEvent::Timeout => { log::info!("Hello, World!"); },
PollEvent::Main(main_event) => {
info!("Main event: {:?}", main_event);
log::info!("Main event: {:?}", main_event);
match main_event {
MainEvent::Destroy => { return; }
_ => {}
@@ -63,188 +70,95 @@ fn android_main(app: AndroidApp) {
}
app.input_events(|event| {
info!("Input Event: {event:?}");
log::info!("Input Event: {event:?}");
InputStatus::Unhandled
});
});
}
}
```
```
```sh
rustup target add aarch64-linux-android
cargo install cargo-apk
cargo apk run
adb logcat example:V *:S
```
# Game Activity
## Full Examples
Originally the aim was to enable support for building Rust applications based on the
[GameActivity] class provided by [Google's Android Game Development Kit][agdk]
which can also facilitate integration with additional AGDK libraries including:
1. [Game Text Input](https://developer.android.com/games/agdk/add-support-for-text-input): a library
to help fullscreen native applications utilize the Android soft keyboard.
2. [Game Controller Library, aka 'Paddleboat'](https://developer.android.com/games/sdk/game-controller):
a native library designed to help support access to game controller inputs.
3.[Frame Pacing Library, aka ' Swappy'](https://developer.android.com/games/sdk/frame-pacing): a library
that helps OpenGL and Vulkan games achieve smooth rendering and correct frame pacing on Android.
3. [Memory Advice API](https://developer.android.com/games/sdk/memory-advice/overview): an API to
help applications monitor their own memory usage to stay within safe limits for the system.
4. [Oboe audio library](https://developer.android.com/games/sdk/oboe): a low-latency audio API for native
applications.
See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`).
Since `GameActivity` is based on the widely used [AppCompatActivity] base class, it also
provides a variety of back ported Activity APIs which can make it more practical to
support a wider range of devices and Android versions.
Each example is a standalone project that may also be a convenient templates for starting a new project.
[GameActivity]: https://developer.android.com/games/agdk/integrate-game-activity
[agdk]: https://developer.android.com/games/agdk
[AppCompatActivity]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
For the examples based on middleware frameworks (Winit and or Egui) they also aim to demonstrate how it's possible to write portable code that will run on Android and other systems.
# Native Activity
## Should I use NativeActivity or GameActivity?
This project also supports [`NativeActivity`][NativeActivity] based applications. Although
NativeActivity is more limited than `GameActivity` and does not derive from `AppCompatActivity` it
can sometimes still be convenient to build on `NativeActivity` in situations where you are using a
limited/minimal build system that is not able to compile Java or Kotlin code or fetch from Maven
repositories - this is because `NativeActivity` is included as part of the Android platform.
To learn more about the `NativeActivity` class that's shipped with Android see [here](https://developer.android.com/ndk/guides/concepts#naa).
[NativeActivity]: https://developer.android.com/reference/android/app/NativeActivity
To learn more about the `GameActivity` class that's part of the [Android Game Developer's Kit][agdk] and also see a comparison with `NativeActivity` see [here](https://developer.android.com/games/agdk/game-activity)
# Design
Generally speaking, if unsure, `NativeActivity` may be more convenient to start with since you may not need to compile/link any Java or Kotlin code.
## Compatibility
It's expected that the `GameActivity` backend will gain more sophisticated input handling features over time (such as for supporting input via onscreen keyboards or game controllers) and only `GameActivity` is based on the [`AppCompatActivity`] subclass which you may want in some situations to help with compatibility across devices.
All `Activity` classes are supported via a common API that enables you to write
`Activity` subclass agnostic code wherever you don't depend on features that are
specific to a particular subclass.
Even if you start out using `NativeActivity` for the convenience, it's likely that most moderately complex applications will eventually need to define their own `Activity` subclass (either subclassing `NativeActivity` or `GameActivity`) which will require compiling at least a small amount of Java or Kotlin code. This is generally due to Android's design which directs numerous events via the `Activity` class which can only be processed by overloading some associated Activity method.
For example, it makes it possible to have a [Winit backend](https://github.com/rib/winit/tree/agdk-game-activity)
that supports Android applications running with different `Activity` classes.
## Switching from ndk-glue to android-activity
## API Summary
### Winit-based applications
Firstly; if you have a [Winit](https://crates.io/crates/winit) based application and also have an explicit dependency on `ndk-glue` your application will need to remove its dependency on `ndk-glue` for the 0.28 release of Winit which will be based on android-activity (Since glue crates, due to their nature, can't be compatible with alternative glue crates).
### `android_main` entrypoint
The glue crates define a standard entrypoint ABI for your `cdylib` that looks like:
Winit-based applications can follow the [Android README](https://github.com/rust-windowing/winit#android) guidance for advice on how to switch over. Most Winit-based applications should aim to remove any explicit dependency on a specific glue crate (so not depend directly on `ndk-glue` or `android-activity` and instead rely on Winit to pull in the right glue crate). The main practical change will then be to add a `#[no_mangle]fn android_main(app: AndroidApp)` entry point.
See the [Android README](https://github.com/rust-windowing/winit#android) for more details and also see the [Winit-based examples here](https://github.com/rust-mobile/rust-android-examples).
### Middleware crates (i.e. not applications)
If you have a crate that would be considered a middleware library (for example using JNI to support access to Bluetooth, or Android's Camera APIs) then the crate should almost certainly remove any dependence on a specific glue crate because this imposes a strict compatibility constraint that means the crate can only be used by applications that use that exact same glue crate version.
Middleware libraries can instead look at using the [ndk-context](https://crates.io/crates/ndk-context) crate as a means for being able to use JNI without making any assumptions about the applications overall architecture. This way a middleware crate can work with alternative glue crates (including `ndk-glue` and `android-activity`) as well as work with embedded use cases (i.e. non-native applications that may just embed a dynamic library written in Rust to implement certain native functions).
### Other, non-Winit-based applications
The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be:
1. Remove `ndk-glue` from your Cargo.toml
2. Add a dependency on `android-activity`, like `android-activity = { version="0.4", features = [ "native-activity" ] }`
3. Optionally add a dependency on `android_logger = "0.11.0"`
4. Update the `main` entry point to look like this:
```rust
use android_activity::AndroidApp;
#[no_mangle]
fn android_main(app: AndroidApp) {
...
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
}
```
There's currently no high-level macro provided for things like initializing
logging or allowing the main function to return a `Result<>` since it's expected
that different downstream frameworks may each have differing opinions on the
details and may want to provide their own macros.
See this minimal [NativeActivity Mainloop](https://github.com/rust-mobile/android-activity/tree/main/examples/na-mainloop) for more details about how to poll for events.
There is is no `#[ndk_glue::main]` replacement considering that `android_main()` entry point needs to be passed an `AndroidApp` argument which isn't compatible with a traditional `main()` function. Having an Android specific entry point also gives a place to initialize Android logging and handle other Android specific details (such as building an event loop based on the `app` argument)
### `AndroidApp`
### Design Summary / Motivation behind android-activity
Your `android_main()` function is passed an `AndroidApp` struct to access state
about your running application and handle synchronized interaction between your
native Rust application and the `Activity` running on the Java main thread.
Prior to working on android-activity, the existing glue crates available for building standalone Rust applications on Android were found to have a number of technical limitations that this crate aimed to solve:
For example, the `AndroidApp` API enables:
1. Access to Android lifecycle events
2. Notifications of SurfaceView lifecycle events
3. Access to input events
4. Ability to save and restore state each time your process stops and starts
5. Access application [`Configuration`] state
6. internal/external/obb filesystem paths
1. **Support alternative Activity classes**: Prior glue crates were based on `NativeActivity` and their API precluded supporting alternatives. In particular there was an interest in the [`GameActivity`] class in conjunction with it's [`GameTextInput`] library that can facilitate onscreen keyboard support. This also allows building applications based on the standard [`AppCompatActivity`] base class which isn't possible with `NativeActivity`. Finally there was interest in paving the way towards supporting a first-party `RustActivity` that could be best tailored towards the needs of Rust applications on Android.
2. **Encapsulate IPC + synchronization between the native thread and the JVM thread**: For example with `ndk-glue` the application itself needs to avoid race conditions between the native and Java thread by following a locking convention) and it wasn't clear how this would extend to support other requests (like state saving) that also require synchronization.
3. **Avoid static global state**: Keeping in mind the possibility of supporting applications with multiple native activities there was interest in having an API that didn't rely on global statics to track top-level state. Instead of having global getters for state then `android-activity` passes an explicit `app: AndroidApp` argument to the entry point that encapsulates the state connected with a single `Activity`.
_Note: that some of the `AndroidApp` APIs (such as for polling events) are only
deemed safe to use from the application's main thread_
[`GameTextInput`]: https://developer.android.com/games/agdk/add-support-for-text-input
[`AppCompatActivity`]: https://developer.android.com/reference/androidx/appcompat/app/AppCompatActivity
[`Configuration`]: https://developer.android.com/reference/android/content/res/Configuration
## MSRV
### Synchronized event callbacks
We aim to (at least) support stable releases of Rust from the last three months. Rust has a 6 week release cycle which means we will support the last three stable releases.
For example, when Rust 1.69 is released we would limit our `rust_version` to 1.67.
The `AndroidApp::poll_events()` API is similar to the Winit `EventLoop::run` API in that it
takes a `FnMut` closure that is called for each outstanding event (such as for lifecycle events).
This design ensures the glue layer can transparently handle any required synchronization with
Java before and after each callback.
We will only bump the `rust_version` at the point where we either depend on a new features or a dependency has increased its MSRV, and we won't be greedy. In other words we will only set the MSRV to the lowest version that's _needed_.
For example, when the Java main thread notifies the glue layer that its `SurfaceView` is being
destroyed the Java thread will then block until it gets an explicit acknowledgement that the
native application has had an opportunity to react to this notification. The glue layer will
automatically release the blocked Java thread once it has delivered the corresponding event.
For example:
```rust
use native_activity::{PollEvent, MainEvent};
use log::info;
#[no_mangle]
extern "C" fn android_main() {
android_logger::init_once(
android_logger::Config::default().with_min_level(log::Level::Info)
);
let mut quit = false;
let mut redraw_pending = true;
let mut render_state: Option<()> = Default::default();
let app = native_activity::android_app();
while !quit {
app.poll_events(Some(std::time::Duration::from_millis(500)) /* timeout */, |event| {
match event {
PollEvent::Wake => { info!("Early wake up"); },
PollEvent::Timeout => {
info!("Timed out");
// Real app would probably rely on vblank sync via graphics API...
redraw_pending = true;
},
PollEvent::Main(main_event) => {
info!("Main event: {:?}", main_event);
match main_event {
MainEvent::SaveState { saver, .. } => {
saver.store("foo://bar".as_bytes());
},
MainEvent::Pause => {},
MainEvent::Resume { loader, .. } => {
if let Some(state) = loader.load() {
if let Ok(uri) = String::from_utf8(state) {
info!("Resumed with saved state = {uri:#?}");
}
}
},
MainEvent::InitWindow { .. } => {
render_state = Some(());
redraw_pending = true;
},
MainEvent::TerminateWindow { .. } => {
render_state = None;
}
MainEvent::WindowResized { .. } => { redraw_pending = true; },
MainEvent::RedrawNeeded { ..} => { redraw_pending = true; },
MainEvent::LowMemory => {},
MainEvent::Destroy => { quit = true },
_ => { /* ... */}
}
},
_ => {}
}
if redraw_pending {
if let Some(_rs) = render_state {
redraw_pending = false;
// Handle input
app.input_events(|event| {
info!("Input Event: {event:?}");
});
info!("Render...");
}
}
});
}
}
```
MSRV updates are not considered to be inherently semver breaking (unless a new feature is exposed in the public API) and so a `rust_version` change may happen in patch releases.
+152 -1
View File
@@ -1,9 +1,160 @@
<!-- 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 ()
### Changed
- rust-version bumped to 0.68 ([#123](https://github.com/rust-mobile/android-activity/pull/123))
- *Breaking*: updates to `ndk 0.8` and `ndk-sys 0.5` ([#128](https://github.com/rust-mobile/android-activity/pull/128))
- The `Pointer` and `PointerIter` types from the `ndk` crate are no longer directly exposed in the public API ([#122](https://github.com/rust-mobile/android-activity/pull/122))
- All input API enums based on Android SDK enums have been made runtime extensible via hidden `__Unknown(u32)` variants ([#131](https://github.com/rust-mobile/android-activity/pull/131))
## [0.5.0-beta.1] - 2023-08-15
### Changed
- Pulled in `ndk-sys 0.5.0-beta.0` and `ndk 0.8.0-beta.0` ([#113](https://github.com/rust-mobile/android-activity/pull/113))
## [0.5.0-beta.0] - 2023-08-15
### Added
- Added `KeyEvent::meta_state()` for being able to query the state of meta keys, needed for character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
- Added `KeyCharacterMap` JNI bindings to the corresponding Android SDK API ([#102](https://github.com/rust-mobile/android-activity/pull/102))
- Added `AndroidApp::device_key_character_map()` for being able to get a `KeyCharacterMap` for a given `device_id` for unicode character mapping ([#102](https://github.com/rust-mobile/android-activity/pull/102))
<details>
<summary>Click here for an example of how to handle unicode character mapping:</summary>
```rust
let mut combining_accent = None;
// Snip
let combined_key_char = if let Ok(map) = app.device_key_character_map(device_id) {
match map.get(key_event.key_code(), key_event.meta_state()) {
Ok(KeyMapChar::Unicode(unicode)) => {
let combined_unicode = if let Some(accent) = combining_accent {
match map.get_dead_char(accent, unicode) {
Ok(Some(key)) => {
info!("KeyEvent: Combined '{unicode}' with accent '{accent}' to give '{key}'");
Some(key)
}
Ok(None) => None,
Err(err) => {
log::error!("KeyEvent: Failed to combine 'dead key' accent '{accent}' with '{unicode}': {err:?}");
None
}
}
} else {
info!("KeyEvent: Pressed '{unicode}'");
Some(unicode)
};
combining_accent = None;
combined_unicode.map(|unicode| KeyMapChar::Unicode(unicode))
}
Ok(KeyMapChar::CombiningAccent(accent)) => {
info!("KeyEvent: Pressed 'dead key' combining accent '{accent}'");
combining_accent = Some(accent);
Some(KeyMapChar::CombiningAccent(accent))
}
Ok(KeyMapChar::None) => {
info!("KeyEvent: Pressed non-unicode key");
combining_accent = None;
None
}
Err(err) => {
log::error!("KeyEvent: Failed to get key map character: {err:?}");
combining_accent = None;
None
}
}
} else {
None
};
```
</details>
- Added `TextEvent` Input Method event for supporting text editing via virtual keyboards ([#24](https://github.com/rust-mobile/android-activity/pull/24))
### Changed
- GameActivity updated to 2.0.2 (requires the corresponding 2.0.2 `.aar` release from Google) ([#88](https://github.com/rust-mobile/android-activity/pull/88))
- `AndroidApp::input_events()` is replaced by `AndroidApp::input_events_iter()` ([#102](https://github.com/rust-mobile/android-activity/pull/102))
<details>
<summary>Click here for an example of how to use `input_events_iter()`:</summary>
```rust
match app.input_events_iter() {
Ok(mut iter) => {
loop {
let read_input = iter.next(|event| {
let handled = match event {
InputEvent::KeyEvent(key_event) => {
// Snip
}
InputEvent::MotionEvent(motion_event) => {
// Snip
}
event => {
// Snip
}
};
handled
});
if !read_input {
break;
}
}
}
Err(err) => {
log::error!("Failed to get input events iterator: {err:?}");
}
}
```
</details>
## [0.4.3] - 2023-07-30
### Fixed
- Fixed a deadlock in the `native-activity` backend while waiting for the native thread after getting an `onDestroy` callback from Java ([#94](https://github.com/rust-mobile/android-activity/pull/94))
- Fixed numerous deadlocks in the `game-activity` backend with how it would wait for the native thread in various Java callbacks, after the app has returned from `android_main` ([#98](https://github.com/rust-mobile/android-activity/pull/98))
## [0.4.2] - 2023-06-17
### Changed
- The `Activity.finish()` method is now called when `android_main` returns so the `Activity` will be destroyed ([#67](https://github.com/rust-mobile/android-activity/issues/67))
- The `native-activity` backend now propagates `NativeWindow` redraw/resize and `ContentRectChanged` callbacks to main loop ([#70](https://github.com/rust-mobile/android-activity/pull/70))
- The `game-activity` implementation of `pointer_index()` was fixed to not always return `0` ([#80](https://github.com/rust-mobile/android-activity/pull/84))
- Added `panic` guards around application's `android_main()` and native code that could potentially unwind across a Java FFI boundary ([#68](https://github.com/rust-mobile/android-activity/pull/68))
## [0.4.1] - 2023-02-16
### Added
- Added `AndroidApp::vm_as_ptr()` to expose JNI `JavaVM` pointer ([#60](https://github.com/rust-mobile/android-activity/issues/60))
- Added `AndroidApp::activity_as_ptr()` to expose Android `Activity` JNI reference as pointer ([#60](https://github.com/rust-mobile/android-activity/issues/60))
### Changed
- Removed some overly-verbose logging in the `native-activity` backend ([#49](https://github.com/rust-mobile/android-activity/pull/49))
### Removed
- Most of the examples were moved to <https://github.com/rust-mobile/rust-android-examples> ([#50](https://github.com/rust-mobile/android-activity/pull/50))
## [0.4] - 2022-11-10
### Changed
- *Breaking*: `input_events` callback now return whether an event was handled or not to allow for fallback handling ([#31](https://github.com/rust-mobile/android-activity/issues/31))
- The native-activity backend is now implemented in Rust only, without building on `android_native_app_glue.c` ([#35](https://github.com/rust-mobile/android-activity/pull/35))
### Added
- Added `Pointer::tool_type()` API to `GameActivity` backend for compatibility with `ndk` events API ([#38](https://github.com/rust-mobile/android-activity/pull/38))
## [0.3] - 2022-09-15
### Added
- `show/hide_sot_input` API for being able to show/hide a soft keyboard (other IME still pending)
- `set_window_flags()` API for setting WindowManager params
### Changed
- *Breaking*: Created extensible, `#[non_exhaustive]` `InputEvent` wrapper enum instead of exposing `ndk` type directly
## [0.2] - 2022-08-25
### Added
@@ -24,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
+19 -9
View File
@@ -1,15 +1,22 @@
[package]
name = "android-activity"
version = "0.3.0"
version = "0.5.0"
edition = "2021"
keywords = ["android", "ndk"]
readme = "../README.md"
homepage = "https://github.com/rib/android-activity"
repository = "https://github.com/rib/android-activity"
homepage = "https://github.com/rust-mobile/android-activity"
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"
# 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
# are generally supposed to be additive, while these backends
@@ -17,20 +24,23 @@ license = "MIT OR Apache-2.0"
#
# In general it's only the final application crate that needs
# to decide on a backend.
default=[]
default = []
game-activity = []
native-activity = []
[dependencies]
log = "0.4"
jni-sys = "0.3"
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.5"
bitflags = "1.3"
num_enum = "0.7"
bitflags = "2.0"
libc = "0.2"
thiserror = "1"
[build-dependencies]
cc = { version = "1.0", features = ["parallel"] }
@@ -43,4 +53,4 @@ targets = [
"x86_64-linux-android",
]
rustdoc-args = ["--cfg", "docsrs" ]
rustdoc-args = ["--cfg", "docsrs"]
+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 -10
View File
@@ -1,27 +1,37 @@
#![allow(dead_code)]
fn build_glue_for_native_activity() {
cc::Build::new()
.include("native-activity-csrc")
.include("native-activity-csrc/native-activity/native_app_glue")
.file("native-activity-csrc/native-activity/native_app_glue/android_native_app_glue.c")
.compile("libnative_app_glue.a");
}
fn build_glue_for_game_activity() {
for f in [
"GameActivity.h",
"GameActivity.cpp",
"GameActivityEvents.h",
"GameActivityEvents.cpp",
"GameActivityLog.h",
] {
println!("cargo:rerun-if-changed=game-activity-csrc/game-activity/{f}");
}
cc::Build::new()
.cpp(true)
.include("game-activity-csrc")
.file("game-activity-csrc/game-activity/GameActivity.cpp")
.file("game-activity-csrc/game-activity/GameActivityEvents.cpp")
.extra_warnings(false)
.cpp_link_stdlib("c++_static")
.compile("libgame_activity.a");
for f in ["gamecommon.h", "gametextinput.h", "gametextinput.cpp"] {
println!("cargo:rerun-if-changed=game-activity-csrc/game-text-input/{f}");
}
cc::Build::new()
.cpp(true)
.include("game-activity-csrc")
.file("game-activity-csrc/game-text-input/gametextinput.cpp")
.cpp_link_stdlib("c++_static")
.compile("libgame_text_input.a");
for f in ["android_native_app_glue.h", "android_native_app_glue.c"] {
println!("cargo:rerun-if-changed=game-activity-csrc/native_app_glue/{f}");
}
cc::Build::new()
.include("game-activity-csrc")
.include("game-activity-csrc/game-activity/native_app_glue")
@@ -38,6 +48,4 @@ fn build_glue_for_game_activity() {
fn main() {
#[cfg(feature = "game-activity")]
build_glue_for_game_activity();
#[cfg(feature = "native-activity")]
build_glue_for_native_activity();
}
@@ -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
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "GameActivity"
#include "GameActivity.h"
@@ -39,54 +38,10 @@
#include <mutex>
#include <string>
// 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.
#include "GameActivityLog.h"
namespace {
#if __ANDROID_API__ >= 26
std::string getSystemPropViaCallback(const char *key,
const char *default_value = "") {
const prop_info *prop = __system_property_find(key);
if (prop == nullptr) {
return default_value;
}
std::string return_value;
auto thunk = [](void *cookie, const char * /*name*/, const char *value,
uint32_t /*serial*/) {
if (value != nullptr) {
std::string *r = static_cast<std::string *>(cookie);
*r = value;
}
};
__system_property_read_callback(prop, thunk, &return_value);
return return_value;
}
#else
std::string getSystemPropViaGet(const char *key,
const char *default_value = "") {
char buffer[PROP_VALUE_MAX + 1] = ""; // +1 for terminator
int bufferLen = __system_property_get(key, buffer);
if (bufferLen > 0)
return buffer;
else
return "";
}
#endif
std::string GetSystemProp(const char *key, const char *default_value = "") {
#if __ANDROID_API__ >= 26
return getSystemPropViaCallback(key, default_value);
#else
return getSystemPropViaGet(key, default_value);
#endif
}
int GetSystemPropAsInt(const char *key, int default_value = 0) {
std::string prop = GetSystemProp(key);
return prop == "" ? default_value : strtoll(prop.c_str(), nullptr, 10);
}
struct OwnedGameTextInputState {
OwnedGameTextInputState &operator=(const GameTextInputState &rhs) {
inner = rhs;
@@ -100,93 +55,6 @@ struct OwnedGameTextInputState {
} // anonymous namespace
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__);
#define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__);
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__);
#ifdef NDEBUG
#define ALOGV(...)
#else
#define ALOGV(...) \
__android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__);
#endif
/* Returns 2nd arg. Used to substitute default value if caller's vararg list
* is empty.
*/
#define __android_second(first, second, ...) second
/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise
* returns nothing.
*/
#define __android_rest(first, ...) , ##__VA_ARGS__
#define android_printAssert(cond, tag, fmt...) \
__android_log_assert(cond, tag, \
__android_second(0, ##fmt, NULL) __android_rest(fmt))
#define CONDITION(cond) (__builtin_expect((cond) != 0, 0))
#ifndef LOG_ALWAYS_FATAL_IF
#define LOG_ALWAYS_FATAL_IF(cond, ...) \
((CONDITION(cond)) \
? ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__)) \
: (void)0)
#endif
#ifndef LOG_ALWAYS_FATAL
#define LOG_ALWAYS_FATAL(...) \
(((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__)))
#endif
/*
* Simplified macro to send a warning system log message using current LOG_TAG.
*/
#ifndef SLOGW
#define SLOGW(...) \
((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
#endif
#ifndef SLOGW_IF
#define SLOGW_IF(cond, ...) \
((__predict_false(cond)) \
? ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)) \
: (void)0)
#endif
/*
* Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that
* are stripped out of release builds.
*/
#if LOG_NDEBUG
#ifndef LOG_FATAL_IF
#define LOG_FATAL_IF(cond, ...) ((void)0)
#endif
#ifndef LOG_FATAL
#define LOG_FATAL(...) ((void)0)
#endif
#else
#ifndef LOG_FATAL_IF
#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__)
#endif
#ifndef LOG_FATAL
#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
#endif
#endif
/*
* Assertion that generates a log message when the assertion fails.
* Stripped out of release builds. Uses the current LOG_TAG.
*/
#ifndef ALOG_ASSERT
#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__)
#endif
#define LOG_TRACE(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#ifndef NELEM
#define NELEM(x) ((int)(sizeof(x) / sizeof((x)[0])))
#endif
@@ -212,6 +80,30 @@ static struct {
jfieldID bottom;
} gInsetsClassInfo;
/*
* JNI fields of the Configuration Java class.
*/
static struct ConfigurationClassInfo {
jfieldID colorMode;
jfieldID densityDpi;
jfieldID fontScale;
jfieldID fontWeightAdjustment;
jfieldID hardKeyboardHidden;
jfieldID keyboard;
jfieldID keyboardHidden;
jfieldID mcc;
jfieldID mnc;
jfieldID navigation;
jfieldID navigationHidden;
jfieldID orientation;
jfieldID screenHeightDp;
jfieldID screenLayout;
jfieldID screenWidthDp;
jfieldID smallestScreenWidthDp;
jfieldID touchscreen;
jfieldID uiMode;
} gConfigurationClassInfo;
/*
* JNI methods of the WindowInsetsCompat.Type Java class.
*/
@@ -228,6 +120,7 @@ struct ActivityWork {
int32_t cmd;
int64_t arg1;
int64_t arg2;
int64_t arg3;
};
/*
@@ -240,19 +133,46 @@ enum {
CMD_SET_WINDOW_FLAGS,
CMD_SHOW_SOFT_INPUT,
CMD_HIDE_SOFT_INPUT,
CMD_SET_SOFT_INPUT_STATE
CMD_SET_SOFT_INPUT_STATE,
CMD_SET_IME_EDITOR_INFO
};
/*
* Last known Configuration values. They may be accessed from the different
* thread, this is why they are made atomic.
*/
static struct Configuration {
std::atomic_int colorMode;
std::atomic_int densityDpi;
std::atomic<float> fontScale;
std::atomic_int fontWeightAdjustment;
std::atomic_int hardKeyboardHidden;
std::atomic_int keyboard;
std::atomic_int keyboardHidden;
std::atomic_int mcc;
std::atomic_int mnc;
std::atomic_int navigation;
std::atomic_int navigationHidden;
std::atomic_int orientation;
std::atomic_int screenHeightDp;
std::atomic_int screenLayout;
std::atomic_int screenWidthDp;
std::atomic_int smallestScreenWidthDp;
std::atomic_int touchscreen;
std::atomic_int uiMode;
} gConfiguration;
/*
* Write a command to be executed by the GameActivity on the application main
* thread.
*/
static void write_work(int fd, int32_t cmd, int64_t arg1 = 0,
int64_t arg2 = 0) {
static void write_work(int fd, int32_t cmd, int64_t arg1 = 0, int64_t arg2 = 0,
int64_t arg3 = 0) {
ActivityWork work;
work.cmd = cmd;
work.arg1 = arg1;
work.arg2 = arg2;
work.arg3 = arg3;
LOG_TRACE("write_work: cmd=%d", cmd);
restart:
@@ -291,12 +211,10 @@ static bool read_work(int fd, ActivityWork *outWork) {
* Native state for interacting with the GameActivity class.
*/
struct NativeCode : public GameActivity {
NativeCode(void *_dlhandle, GameActivity_createFunc *_createFunc) {
NativeCode() {
memset((GameActivity *)this, 0, sizeof(GameActivity));
memset(&callbacks, 0, sizeof(callbacks));
memset(&insetsState, 0, sizeof(insetsState));
dlhandle = _dlhandle;
createActivityFunc = _createFunc;
nativeWindow = NULL;
mainWorkRead = mainWorkWrite = -1;
gameTextInput = NULL;
@@ -324,12 +242,6 @@ struct NativeCode : public GameActivity {
setSurface(NULL);
if (mainWorkRead >= 0) close(mainWorkRead);
if (mainWorkWrite >= 0) close(mainWorkWrite);
if (dlhandle != NULL) {
// for now don't unload... we probably should clean this
// up and only keep one open dlhandle per proc, since there
// is really no benefit to unloading the code.
// dlclose(dlhandle);
}
}
void setSurface(jobject _surface) {
@@ -345,9 +257,6 @@ struct NativeCode : public GameActivity {
GameActivityCallbacks callbacks;
void *dlhandle;
GameActivity_createFunc *createActivityFunc;
std::string internalDataPathObj;
std::string externalDataPathObj;
std::string obbPathObj;
@@ -374,6 +283,8 @@ struct NativeCode : public GameActivity {
ARect insetsState[GAMECOMMON_INSETS_TYPE_COUNT];
};
static void readConfigurationValues(NativeCode *code, jobject javaConfig);
extern "C" void GameActivity_finish(GameActivity *activity) {
NativeCode *code = static_cast<NativeCode *>(activity);
write_work(code->mainWorkWrite, CMD_FINISH, 0);
@@ -476,6 +387,13 @@ static int mainWorkCallback(int fd, int events, void *data) {
case CMD_HIDE_SOFT_INPUT: {
GameTextInput_hideIme(code->gameTextInput, work.arg1);
} break;
case CMD_SET_IME_EDITOR_INFO: {
code->env->CallVoidMethod(
code->javaGameActivity,
gGameActivityClassInfo.setImeEditorInfoFields, work.arg1,
work.arg2, work.arg3);
checkAndClearException(code->env, "setImeEditorInfo");
} break;
default:
ALOGW("Unknown work command: %d", work.cmd);
break;
@@ -485,40 +403,16 @@ static int mainWorkCallback(int fd, int events, void *data) {
}
// ------------------------------------------------------------------------
static thread_local std::string g_error_msg;
static jlong loadNativeCode_native(JNIEnv *env, jobject javaGameActivity,
jstring path, jstring funcName,
jstring internalDataDir, jstring obbDir,
jstring externalDataDir, jobject jAssetMgr,
jbyteArray savedState) {
LOG_TRACE("loadNativeCode_native");
const char *pathStr = env->GetStringUTFChars(path, NULL);
static jlong initializeNativeCode_native(
JNIEnv *env, jobject javaGameActivity, jstring internalDataDir,
jstring obbDir, jstring externalDataDir, jobject jAssetMgr,
jbyteArray savedState, jobject javaConfig) {
LOG_TRACE("initializeNativeCode_native");
NativeCode *code = NULL;
void *handle = dlopen(pathStr, RTLD_LAZY);
env->ReleaseStringUTFChars(path, pathStr);
if (handle == nullptr) {
g_error_msg = dlerror();
ALOGE("GameActivity dlopen(\"%s\") failed: %s", pathStr,
g_error_msg.c_str());
return 0;
}
const char *funcStr = env->GetStringUTFChars(funcName, NULL);
code = new NativeCode(handle,
(GameActivity_createFunc *)dlsym(handle, funcStr));
env->ReleaseStringUTFChars(funcName, funcStr);
if (code->createActivityFunc == nullptr) {
g_error_msg = dlerror();
ALOGW("GameActivity_onCreate not found: %s", g_error_msg.c_str());
delete code;
return 0;
}
code = new NativeCode();
code->looper = ALooper_forThread();
if (code->looper == nullptr) {
@@ -588,7 +482,10 @@ static jlong loadNativeCode_native(JNIEnv *env, jobject javaGameActivity,
rawSavedState = env->GetByteArrayElements(savedState, NULL);
rawSavedSize = env->GetArrayLength(savedState);
}
code->createActivityFunc(code, rawSavedState, rawSavedSize);
readConfigurationValues(code, javaConfig);
GameActivity_onCreate_C(code, rawSavedState, rawSavedSize);
code->gameTextInput = GameTextInput_init(env, 0);
GameTextInput_setEventCallback(code->gameTextInput,
@@ -609,9 +506,9 @@ static jstring getDlError_native(JNIEnv *env, jobject javaGameActivity) {
return result;
}
static void unloadNativeCode_native(JNIEnv *env, jobject javaGameActivity,
static void terminateNativeCode_native(JNIEnv *env, jobject javaGameActivity,
jlong handle) {
LOG_TRACE("unloadNativeCode_native");
LOG_TRACE("terminateNativeCode_native");
if (handle != 0) {
NativeCode *code = (NativeCode *)handle;
delete code;
@@ -695,11 +592,57 @@ static void onStop_native(JNIEnv *env, jobject javaGameActivity, jlong handle) {
}
}
static void readConfigurationValues(NativeCode *code, jobject javaConfig) {
if (gConfigurationClassInfo.colorMode != NULL) {
gConfiguration.colorMode = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.colorMode);
}
gConfiguration.densityDpi =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.densityDpi);
gConfiguration.fontScale =
code->env->GetFloatField(javaConfig, gConfigurationClassInfo.fontScale);
if (gConfigurationClassInfo.fontWeightAdjustment != NULL) {
gConfiguration.fontWeightAdjustment = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.fontWeightAdjustment);
}
gConfiguration.hardKeyboardHidden = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.hardKeyboardHidden);
gConfiguration.mcc =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.mcc);
gConfiguration.mnc =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.mnc);
gConfiguration.navigation =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.navigation);
gConfiguration.navigationHidden = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.navigationHidden);
gConfiguration.orientation =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.orientation);
gConfiguration.screenHeightDp = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.screenHeightDp);
gConfiguration.screenLayout = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.screenLayout);
gConfiguration.screenWidthDp = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.screenWidthDp);
gConfiguration.smallestScreenWidthDp = code->env->GetIntField(
javaConfig, gConfigurationClassInfo.smallestScreenWidthDp);
gConfiguration.touchscreen =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.touchscreen);
gConfiguration.uiMode =
code->env->GetIntField(javaConfig, gConfigurationClassInfo.uiMode);
checkAndClearException(code->env, "Configuration.get");
}
static void onConfigurationChanged_native(JNIEnv *env, jobject javaGameActivity,
jlong handle) {
jlong handle, jobject javaNewConfig) {
LOG_TRACE("onConfigurationChanged_native");
if (handle != 0) {
NativeCode *code = (NativeCode *)handle;
readConfigurationValues(code, javaNewConfig);
if (code->callbacks.onConfigurationChanged != NULL) {
code->callbacks.onConfigurationChanged(code);
}
@@ -776,8 +719,12 @@ static void onSurfaceChanged_native(JNIEnv *env, jobject javaGameActivity,
// Maybe it was resized?
int32_t newWidth = ANativeWindow_getWidth(code->nativeWindow);
int32_t newHeight = ANativeWindow_getHeight(code->nativeWindow);
if (newWidth != code->lastWindowWidth ||
newHeight != code->lastWindowHeight) {
code->lastWindowWidth = newWidth;
code->lastWindowHeight = newHeight;
if (code->callbacks.onNativeWindowResized != NULL) {
code->callbacks.onNativeWindowResized(
code, code->nativeWindow, newWidth, newHeight);
@@ -817,342 +764,84 @@ static void onSurfaceDestroyed_native(JNIEnv *env, jobject javaGameActivity,
}
}
static bool enabledAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
/* AMOTION_EVENT_AXIS_X */ true,
/* AMOTION_EVENT_AXIS_Y */ true,
// Disable all other axes by default (they can be enabled using
// `GameActivityPointerAxes_enableAxis`).
false};
extern "C" void GameActivityPointerAxes_enableAxis(int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return;
}
enabledAxes[axis] = true;
}
extern "C" void GameActivityPointerAxes_disableAxis(int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return;
}
enabledAxes[axis] = false;
}
static bool enabledHistoricalAxes[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT] = {
// Disable all axes by default (they can be enabled using
// `GameActivityPointerAxes_enableHistoricalAxis`).
false};
extern "C" void GameActivityHistoricalPointerAxes_enableAxis(int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return;
}
enabledHistoricalAxes[axis] = true;
}
extern "C" void GameActivityHistoricalPointerAxes_disableAxis(int32_t axis) {
if (axis < 0 || axis >= GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT) {
return;
}
enabledHistoricalAxes[axis] = false;
}
extern "C" void GameActivity_setImeEditorInfo(GameActivity *activity,
int inputType, int actionId,
int imeOptions) {
JNIEnv *env;
if (activity->vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
env->CallVoidMethod(activity->javaGameActivity,
gGameActivityClassInfo.setImeEditorInfoFields,
inputType, actionId, imeOptions);
}
NativeCode *code = static_cast<NativeCode *>(activity);
write_work(code->mainWorkWrite, CMD_SET_IME_EDITOR_INFO, inputType,
actionId, imeOptions);
}
static struct {
jmethodID getDeviceId;
jmethodID getSource;
jmethodID getAction;
jmethodID getEventTime;
jmethodID getDownTime;
jmethodID getFlags;
jmethodID getMetaState;
jmethodID getActionButton;
jmethodID getButtonState;
jmethodID getClassification;
jmethodID getEdgeFlags;
jmethodID getPointerCount;
jmethodID getPointerId;
jmethodID getRawX;
jmethodID getRawY;
jmethodID getXPrecision;
jmethodID getYPrecision;
jmethodID getAxisValue;
jmethodID getHistorySize;
jmethodID getHistoricalEventTime;
jmethodID getHistoricalAxisValue;
} gMotionEventClassInfo;
extern "C" int GameActivityMotionEvent_fromJava(
JNIEnv *env, jobject motionEvent, GameActivityMotionEvent *out_event,
GameActivityHistoricalPointerAxes *out_historical) {
static bool gMotionEventClassInfoInitialized = false;
if (!gMotionEventClassInfoInitialized) {
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
gMotionEventClassInfo = {0};
jclass motionEventClass = env->FindClass("android/view/MotionEvent");
gMotionEventClassInfo.getDeviceId =
env->GetMethodID(motionEventClass, "getDeviceId", "()I");
gMotionEventClassInfo.getSource =
env->GetMethodID(motionEventClass, "getSource", "()I");
gMotionEventClassInfo.getAction =
env->GetMethodID(motionEventClass, "getAction", "()I");
gMotionEventClassInfo.getEventTime =
env->GetMethodID(motionEventClass, "getEventTime", "()J");
gMotionEventClassInfo.getDownTime =
env->GetMethodID(motionEventClass, "getDownTime", "()J");
gMotionEventClassInfo.getFlags =
env->GetMethodID(motionEventClass, "getFlags", "()I");
gMotionEventClassInfo.getMetaState =
env->GetMethodID(motionEventClass, "getMetaState", "()I");
if (sdkVersion >= 23) {
gMotionEventClassInfo.getActionButton =
env->GetMethodID(motionEventClass, "getActionButton", "()I");
}
if (sdkVersion >= 14) {
gMotionEventClassInfo.getButtonState =
env->GetMethodID(motionEventClass, "getButtonState", "()I");
}
if (sdkVersion >= 29) {
gMotionEventClassInfo.getClassification =
env->GetMethodID(motionEventClass, "getClassification", "()I");
}
gMotionEventClassInfo.getEdgeFlags =
env->GetMethodID(motionEventClass, "getEdgeFlags", "()I");
gMotionEventClassInfo.getPointerCount =
env->GetMethodID(motionEventClass, "getPointerCount", "()I");
gMotionEventClassInfo.getPointerId =
env->GetMethodID(motionEventClass, "getPointerId", "(I)I");
if (sdkVersion >= 29) {
gMotionEventClassInfo.getRawX =
env->GetMethodID(motionEventClass, "getRawX", "(I)F");
gMotionEventClassInfo.getRawY =
env->GetMethodID(motionEventClass, "getRawY", "(I)F");
}
gMotionEventClassInfo.getXPrecision =
env->GetMethodID(motionEventClass, "getXPrecision", "()F");
gMotionEventClassInfo.getYPrecision =
env->GetMethodID(motionEventClass, "getYPrecision", "()F");
gMotionEventClassInfo.getAxisValue =
env->GetMethodID(motionEventClass, "getAxisValue", "(II)F");
gMotionEventClassInfo.getHistorySize =
env->GetMethodID(motionEventClass, "getHistorySize", "()I");
gMotionEventClassInfo.getHistoricalEventTime =
env->GetMethodID(motionEventClass, "getHistoricalEventTime", "(I)J");
gMotionEventClassInfo.getHistoricalAxisValue =
env->GetMethodID(motionEventClass, "getHistoricalAxisValue", "(III)F");
gMotionEventClassInfoInitialized = true;
}
int historySize =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getHistorySize);
historySize =
std::min(historySize, GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT);
int localEnabledHistoricalAxis[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
int enabledHistoricalAxisCount = 0;
for (int axisIndex = 0;
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT;
++axisIndex) {
if (enabledHistoricalAxes[axisIndex]) {
localEnabledHistoricalAxis[enabledHistoricalAxisCount++] = axisIndex;
}
}
out_event->historicalCount = enabledHistoricalAxisCount == 0 ? 0 : historySize;
out_event->historicalStart = 0; // Free for caller to use
// The historical event times aren't unique per-pointer but for simplicity
// we output a per-pointer event time, copied from here...
int64_t historicalEventTimes[GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT];
for (int histIndex = 0; histIndex < historySize; ++histIndex) {
historicalEventTimes[histIndex] =
env->CallLongMethod(motionEvent,
gMotionEventClassInfo.getHistoricalEventTime, histIndex) *
1000000;
}
int pointerCount =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getPointerCount);
pointerCount =
std::min(pointerCount, GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT);
out_event->pointerCount = pointerCount;
for (int i = 0; i < pointerCount; ++i) {
out_event->pointers[i] = {
/*id=*/env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getPointerId, i),
/*axisValues=*/{0},
/*rawX=*/gMotionEventClassInfo.getRawX
? env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getRawX, i)
: 0,
/*rawY=*/gMotionEventClassInfo.getRawY
? env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getRawY, i)
: 0,
};
for (int axisIndex = 0;
axisIndex < GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT; ++axisIndex) {
if (enabledAxes[axisIndex]) {
out_event->pointers[i].axisValues[axisIndex] =
env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getAxisValue,
axisIndex, i);
}
}
if (enabledHistoricalAxisCount > 0) {
for (int histIndex = 0; histIndex < historySize; ++histIndex) {
int pointerHistIndex = historySize * i;
out_historical[pointerHistIndex].eventTime = historicalEventTimes[histIndex];
for (int c = 0; c < enabledHistoricalAxisCount; ++c) {
int axisIndex = localEnabledHistoricalAxis[c];
out_historical[pointerHistIndex].axisValues[axisIndex] =
env->CallFloatMethod(motionEvent,
gMotionEventClassInfo.getHistoricalAxisValue,
axisIndex, i, histIndex);
}
}
}
}
out_event->deviceId =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getDeviceId);
out_event->source =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getSource);
out_event->action =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getAction);
out_event->eventTime =
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getEventTime) *
1000000;
out_event->downTime =
env->CallLongMethod(motionEvent, gMotionEventClassInfo.getDownTime) *
1000000;
out_event->flags =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getFlags);
out_event->metaState =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getMetaState);
out_event->actionButton =
gMotionEventClassInfo.getActionButton
? env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getActionButton)
: 0;
out_event->buttonState =
gMotionEventClassInfo.getButtonState
? env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getButtonState)
: 0;
out_event->classification =
gMotionEventClassInfo.getClassification
? env->CallIntMethod(motionEvent,
gMotionEventClassInfo.getClassification)
: 0;
out_event->edgeFlags =
env->CallIntMethod(motionEvent, gMotionEventClassInfo.getEdgeFlags);
out_event->precisionX =
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getXPrecision);
out_event->precisionY =
env->CallFloatMethod(motionEvent, gMotionEventClassInfo.getYPrecision);
return out_event->pointerCount * out_event->historicalCount;
extern "C" int GameActivity_getColorMode(GameActivity *) {
return gConfiguration.colorMode;
}
static struct {
jmethodID getDeviceId;
jmethodID getSource;
jmethodID getAction;
extern "C" int GameActivity_getDensityDpi(GameActivity *) {
return gConfiguration.densityDpi;
}
jmethodID getEventTime;
jmethodID getDownTime;
extern "C" float GameActivity_getFontScale(GameActivity *) {
return gConfiguration.fontScale;
}
jmethodID getFlags;
jmethodID getMetaState;
extern "C" int GameActivity_getFontWeightAdjustment(GameActivity *) {
return gConfiguration.fontWeightAdjustment;
}
jmethodID getModifiers;
jmethodID getRepeatCount;
jmethodID getKeyCode;
jmethodID getScanCode;
} gKeyEventClassInfo;
extern "C" int GameActivity_getHardKeyboardHidden(GameActivity *) {
return gConfiguration.hardKeyboardHidden;
}
extern "C" void GameActivityKeyEvent_fromJava(JNIEnv *env, jobject keyEvent,
GameActivityKeyEvent *out_event) {
static bool gKeyEventClassInfoInitialized = false;
if (!gKeyEventClassInfoInitialized) {
int sdkVersion = GetSystemPropAsInt("ro.build.version.sdk");
gKeyEventClassInfo = {0};
jclass keyEventClass = env->FindClass("android/view/KeyEvent");
gKeyEventClassInfo.getDeviceId =
env->GetMethodID(keyEventClass, "getDeviceId", "()I");
gKeyEventClassInfo.getSource =
env->GetMethodID(keyEventClass, "getSource", "()I");
gKeyEventClassInfo.getAction =
env->GetMethodID(keyEventClass, "getAction", "()I");
gKeyEventClassInfo.getEventTime =
env->GetMethodID(keyEventClass, "getEventTime", "()J");
gKeyEventClassInfo.getDownTime =
env->GetMethodID(keyEventClass, "getDownTime", "()J");
gKeyEventClassInfo.getFlags =
env->GetMethodID(keyEventClass, "getFlags", "()I");
gKeyEventClassInfo.getMetaState =
env->GetMethodID(keyEventClass, "getMetaState", "()I");
if (sdkVersion >= 13) {
gKeyEventClassInfo.getModifiers =
env->GetMethodID(keyEventClass, "getModifiers", "()I");
}
gKeyEventClassInfo.getRepeatCount =
env->GetMethodID(keyEventClass, "getRepeatCount", "()I");
gKeyEventClassInfo.getKeyCode =
env->GetMethodID(keyEventClass, "getKeyCode", "()I");
gKeyEventClassInfo.getScanCode =
env->GetMethodID(keyEventClass, "getScanCode", "()I");
extern "C" int GameActivity_getKeyboard(GameActivity *) {
return gConfiguration.keyboard;
}
gKeyEventClassInfoInitialized = true;
}
extern "C" int GameActivity_getKeyboardHidden(GameActivity *) {
return gConfiguration.keyboardHidden;
}
*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)};
extern "C" int GameActivity_getMcc(GameActivity *) {
return gConfiguration.mcc;
}
extern "C" int GameActivity_getMnc(GameActivity *) {
return gConfiguration.mnc;
}
extern "C" int GameActivity_getNavigation(GameActivity *) {
return gConfiguration.navigation;
}
extern "C" int GameActivity_getNavigationHidden(GameActivity *) {
return gConfiguration.navigationHidden;
}
extern "C" int GameActivity_getOrientation(GameActivity *) {
return gConfiguration.orientation;
}
extern "C" int GameActivity_getScreenHeightDp(GameActivity *) {
return gConfiguration.screenHeightDp;
}
extern "C" int GameActivity_getScreenLayout(GameActivity *) {
return gConfiguration.screenLayout;
}
extern "C" int GameActivity_getScreenWidthDp(GameActivity *) {
return gConfiguration.screenWidthDp;
}
extern "C" int GameActivity_getSmallestScreenWidthDp(GameActivity *) {
return gConfiguration.smallestScreenWidthDp;
}
extern "C" int GameActivity_getTouchscreen(GameActivity *) {
return gConfiguration.touchscreen;
}
extern "C" int GameActivity_getUIMode(GameActivity *) {
return gConfiguration.uiMode;
}
static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity,
@@ -1162,11 +851,9 @@ static bool onTouchEvent_native(JNIEnv *env, jobject javaGameActivity,
if (code->callbacks.onTouchEvent == nullptr) return false;
static GameActivityMotionEvent c_event;
// Note the actual data is written contiguously as numPointers x historySize
// entries.
static GameActivityHistoricalPointerAxes historical[GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT * GAMEACTIVITY_MAX_NUM_HISTORICAL_IN_MOTION_EVENT];
int historicalLen = GameActivityMotionEvent_fromJava(env, motionEvent, &c_event, historical);
return code->callbacks.onTouchEvent(code, &c_event, historical, historicalLen);
GameActivityMotionEvent_fromJava(env, motionEvent, &c_event);
return code->callbacks.onTouchEvent(code, &c_event);
}
static bool onKeyUp_native(JNIEnv *env, jobject javaGameActivity, jlong handle,
@@ -1242,19 +929,37 @@ static void setInputConnection_native(JNIEnv *env, jobject activity,
GameTextInput_setInputConnection(code->gameTextInput, inputConnection);
}
static void onContentRectChangedNative_native(JNIEnv *env, jobject activity,
jlong handle, jint x, jint y,
jint w, jint h) {
if (handle != 0) {
NativeCode *code = (NativeCode *)handle;
if (code->callbacks.onContentRectChanged != nullptr) {
ARect rect;
rect.left = x;
rect.top = y;
rect.right = x+w;
rect.bottom = y+h;
code->callbacks.onContentRectChanged(code, &rect);
}
}
}
static const JNINativeMethod g_methods[] = {
{"loadNativeCode",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/"
"String;Ljava/lang/String;Landroid/content/res/AssetManager;[B)J",
(void *)loadNativeCode_native},
{"initializeNativeCode",
"(Ljava/lang/String;Ljava/lang/String;"
"Ljava/lang/String;Landroid/content/res/AssetManager;"
"[BLandroid/content/res/Configuration;)J",
(void *)initializeNativeCode_native},
{"getDlError", "()Ljava/lang/String;", (void *)getDlError_native},
{"unloadNativeCode", "(J)V", (void *)unloadNativeCode_native},
{"terminateNativeCode", "(J)V", (void *)terminateNativeCode_native},
{"onStartNative", "(J)V", (void *)onStart_native},
{"onResumeNative", "(J)V", (void *)onResume_native},
{"onSaveInstanceStateNative", "(J)[B", (void *)onSaveInstanceState_native},
{"onPauseNative", "(J)V", (void *)onPause_native},
{"onStopNative", "(J)V", (void *)onStop_native},
{"onConfigurationChangedNative", "(J)V",
{"onConfigurationChangedNative", "(JLandroid/content/res/Configuration;)V",
(void *)onConfigurationChanged_native},
{"onTrimMemoryNative", "(JI)V", (void *)onTrimMemory_native},
{"onWindowFocusChangedNative", "(JZ)V",
@@ -1279,13 +984,16 @@ static const JNINativeMethod g_methods[] = {
{"setInputConnectionNative",
"(JLcom/google/androidgamesdk/gametextinput/InputConnection;)V",
(void *)setInputConnection_native},
{"onContentRectChangedNative", "(JIIII)V",
(void *)onContentRectChangedNative_native},
};
static const char *const kGameActivityPathName =
"com/google/androidgamesdk/GameActivity";
static const char *const kInsetsPathName = "androidx/core/graphics/Insets";
static const char *const kConfigurationPathName =
"android/content/res/Configuration";
static const char *const kWindowInsetsCompatTypePathName =
"androidx/core/view/WindowInsetsCompat$Type";
@@ -1343,12 +1051,59 @@ extern "C" int GameActivity_register(JNIEnv *env) {
"getWaterfallInsets", "()Landroidx/core/graphics/Insets;");
GET_METHOD_ID(gGameActivityClassInfo.setImeEditorInfoFields, activity_class,
"setImeEditorInfoFields", "(III)V");
jclass insets_class;
FIND_CLASS(insets_class, kInsetsPathName);
GET_FIELD_ID(gInsetsClassInfo.left, insets_class, "left", "I");
GET_FIELD_ID(gInsetsClassInfo.right, insets_class, "right", "I");
GET_FIELD_ID(gInsetsClassInfo.top, insets_class, "top", "I");
GET_FIELD_ID(gInsetsClassInfo.bottom, insets_class, "bottom", "I");
jclass configuration_class;
FIND_CLASS(configuration_class, kConfigurationPathName);
if (android_get_device_api_level() >= 26) {
GET_FIELD_ID(gConfigurationClassInfo.colorMode, configuration_class,
"colorMode", "I");
}
GET_FIELD_ID(gConfigurationClassInfo.densityDpi, configuration_class,
"densityDpi", "I");
GET_FIELD_ID(gConfigurationClassInfo.fontScale, configuration_class,
"fontScale", "F");
if (android_get_device_api_level() >= 31) {
GET_FIELD_ID(gConfigurationClassInfo.fontWeightAdjustment,
configuration_class, "fontWeightAdjustment", "I");
}
GET_FIELD_ID(gConfigurationClassInfo.hardKeyboardHidden,
configuration_class, "hardKeyboardHidden", "I");
GET_FIELD_ID(gConfigurationClassInfo.keyboard, configuration_class,
"keyboard", "I");
GET_FIELD_ID(gConfigurationClassInfo.keyboardHidden, configuration_class,
"keyboardHidden", "I");
GET_FIELD_ID(gConfigurationClassInfo.mcc, configuration_class, "mcc", "I");
GET_FIELD_ID(gConfigurationClassInfo.mnc, configuration_class, "mnc", "I");
GET_FIELD_ID(gConfigurationClassInfo.navigation, configuration_class,
"navigation", "I");
GET_FIELD_ID(gConfigurationClassInfo.navigationHidden, configuration_class,
"navigationHidden", "I");
GET_FIELD_ID(gConfigurationClassInfo.orientation, configuration_class,
"orientation", "I");
GET_FIELD_ID(gConfigurationClassInfo.screenHeightDp, configuration_class,
"screenHeightDp", "I");
GET_FIELD_ID(gConfigurationClassInfo.screenLayout, configuration_class,
"screenLayout", "I");
GET_FIELD_ID(gConfigurationClassInfo.screenWidthDp, configuration_class,
"screenWidthDp", "I");
GET_FIELD_ID(gConfigurationClassInfo.smallestScreenWidthDp,
configuration_class, "smallestScreenWidthDp", "I");
GET_FIELD_ID(gConfigurationClassInfo.touchscreen, configuration_class,
"touchscreen", "I");
GET_FIELD_ID(gConfigurationClassInfo.uiMode, configuration_class, "uiMode",
"I");
jclass windowInsetsCompatType_class;
FIND_CLASS(windowInsetsCompatType_class, kWindowInsetsCompatTypePathName);
gWindowInsetsCompatTypeClassInfo.clazz =
@@ -1379,15 +1134,14 @@ extern "C" int GameActivity_register(JNIEnv *env) {
// Rust because Rust/Cargo don't give us a way to directly export symbols
// from C/C++ code: https://github.com/rust-lang/rfcs/issues/2771
//
// Register this method so that GameActiviy_register does not need to be called
// manually.
extern "C" jlong Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
JNIEnv *env, jobject javaGameActivity, jstring path, jstring funcName,
jstring internalDataDir, jstring obbDir, jstring externalDataDir,
jobject jAssetMgr, jbyteArray savedState) {
extern "C" JNIEXPORT jlong JNICALL
Java_com_google_androidgamesdk_GameActivity_initializeNativeCode_C(
JNIEnv *env, jobject javaGameActivity, jstring internalDataDir,
jstring obbDir, jstring externalDataDir, jobject jAssetMgr,
jbyteArray savedState, jobject javaConfig) {
GameActivity_register(env);
jlong nativeCode = loadNativeCode_native(
env, javaGameActivity, path, funcName, internalDataDir, obbDir,
externalDataDir, jAssetMgr, savedState);
jlong nativeCode = initializeNativeCode_native(
env, javaGameActivity, internalDataDir, obbDir, externalDataDir,
jAssetMgr, savedState, javaConfig);
return nativeCode;
}
@@ -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,198 +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;
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.
@@ -423,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.
@@ -455,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
@@ -503,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
@@ -810,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
+6 -23
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
@@ -16,6 +18,10 @@ while read ARCH && read TARGET ; do
--blocklist-item 'C?_?JNIEnv' \
--blocklist-item '_?JavaVM' \
--blocklist-item '_?j\w+' \
--blocklist-item 'size_t' \
--blocklist-item 'pthread_\w*' \
--blocklist-function 'pthread_\w' \
--blocklist-item 'ARect' \
--blocklist-item 'ALooper\w*' \
--blocklist-function 'ALooper\w*' \
--blocklist-item 'AAsset\w*' \
@@ -33,29 +39,6 @@ while read ARCH && read TARGET ; do
-Igame-activity-csrc \
--sysroot="$SYSROOT" --target=$TARGET
bindgen native-activity-ffi.h -o src/native_activity/ffi_$ARCH.rs \
--blocklist-item 'JNI\w+' \
--blocklist-item 'C?_?JNIEnv' \
--blocklist-item '_?JavaVM' \
--blocklist-item '_?j\w+' \
--blocklist-item 'ALooper\w*' \
--blocklist-function 'ALooper\w*' \
--blocklist-item 'AAsset\w*' \
--blocklist-item 'AAssetManager\w*' \
--blocklist-function 'AAssetManager\w*' \
--blocklist-item 'ANativeWindow\w*' \
--blocklist-function 'ANativeWindow\w*' \
--blocklist-item 'AConfiguration\w*' \
--blocklist-function 'AConfiguration\w*' \
--blocklist-function 'android_main' \
--blocklist-item 'AInputQueue\w*' \
--blocklist-function 'AInputQueue\w*' \
--blocklist-item 'GameActivity_onCreate' \
--blocklist-function 'GameActivity_onCreate_C' \
--newtype-enum '\w+_(result|status)_t' \
-- \
-Inative-activity-csrc \
--sysroot="$SYSROOT" --target=$TARGET
done << EOF
arm
arm-linux-androideabi
@@ -1,457 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#include <jni.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/resource.h>
#include "android_native_app_glue.h"
#include <android/log.h>
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__))
/* For debug builds, always enable the debug traces in this library */
#ifndef NDEBUG
# define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__))
#else
# define LOGV(...) ((void)0)
#endif
static void free_saved_state(struct android_app* android_app) {
pthread_mutex_lock(&android_app->mutex);
if (android_app->savedState != NULL) {
free(android_app->savedState);
android_app->savedState = NULL;
android_app->savedStateSize = 0;
}
pthread_mutex_unlock(&android_app->mutex);
}
int8_t android_app_read_cmd(struct android_app* android_app) {
int8_t cmd;
if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) {
switch (cmd) {
case APP_CMD_SAVE_STATE:
free_saved_state(android_app);
break;
}
return cmd;
} else {
LOGE("No data on command pipe!");
}
return -1;
}
static void print_cur_config(struct android_app* android_app) {
char lang[2], country[2];
AConfiguration_getLanguage(android_app->config, lang);
AConfiguration_getCountry(android_app->config, country);
LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d "
"keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d "
"modetype=%d modenight=%d",
AConfiguration_getMcc(android_app->config),
AConfiguration_getMnc(android_app->config),
lang[0], lang[1], country[0], country[1],
AConfiguration_getOrientation(android_app->config),
AConfiguration_getTouchscreen(android_app->config),
AConfiguration_getDensity(android_app->config),
AConfiguration_getKeyboard(android_app->config),
AConfiguration_getNavigation(android_app->config),
AConfiguration_getKeysHidden(android_app->config),
AConfiguration_getNavHidden(android_app->config),
AConfiguration_getSdkVersion(android_app->config),
AConfiguration_getScreenSize(android_app->config),
AConfiguration_getScreenLong(android_app->config),
AConfiguration_getUiModeType(android_app->config),
AConfiguration_getUiModeNight(android_app->config));
}
void android_app_attach_input_queue_looper(struct android_app* android_app) {
if (android_app->inputQueue != NULL) {
LOGV("Attaching input queue to looper");
AInputQueue_attachLooper(android_app->inputQueue,
android_app->looper, LOOPER_ID_INPUT, NULL,
&android_app->inputPollSource);
}
}
void android_app_detach_input_queue_looper(struct android_app* android_app) {
if (android_app->inputQueue != NULL) {
LOGV("Detaching input queue from looper");
AInputQueue_detachLooper(android_app->inputQueue);
}
}
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) {
switch (cmd) {
case APP_CMD_INPUT_CHANGED:
LOGV("APP_CMD_INPUT_CHANGED\n");
pthread_mutex_lock(&android_app->mutex);
if (android_app->inputQueue != NULL) {
android_app_detach_input_queue_looper(android_app);
}
android_app->inputQueue = android_app->pendingInputQueue;
if (android_app->inputQueue != NULL) {
android_app_attach_input_queue_looper(android_app);
}
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_INIT_WINDOW:
LOGV("APP_CMD_INIT_WINDOW\n");
pthread_mutex_lock(&android_app->mutex);
android_app->window = android_app->pendingWindow;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_TERM_WINDOW:
LOGV("APP_CMD_TERM_WINDOW\n");
pthread_cond_broadcast(&android_app->cond);
break;
case APP_CMD_RESUME:
case APP_CMD_START:
case APP_CMD_PAUSE:
case APP_CMD_STOP:
LOGV("activityState=%d\n", cmd);
pthread_mutex_lock(&android_app->mutex);
android_app->activityState = cmd;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_CONFIG_CHANGED:
LOGV("APP_CMD_CONFIG_CHANGED\n");
AConfiguration_fromAssetManager(android_app->config,
android_app->activity->assetManager);
print_cur_config(android_app);
break;
case APP_CMD_DESTROY:
LOGV("APP_CMD_DESTROY\n");
android_app->destroyRequested = 1;
break;
}
}
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) {
switch (cmd) {
case APP_CMD_TERM_WINDOW:
LOGV("APP_CMD_TERM_WINDOW\n");
pthread_mutex_lock(&android_app->mutex);
android_app->window = NULL;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_SAVE_STATE:
LOGV("APP_CMD_SAVE_STATE\n");
pthread_mutex_lock(&android_app->mutex);
android_app->stateSaved = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
break;
case APP_CMD_RESUME:
free_saved_state(android_app);
break;
}
}
void app_dummy() {
}
static void android_app_destroy(struct android_app* android_app) {
LOGV("android_app_destroy!");
free_saved_state(android_app);
pthread_mutex_lock(&android_app->mutex);
if (android_app->inputQueue != NULL) {
AInputQueue_detachLooper(android_app->inputQueue);
}
AConfiguration_delete(android_app->config);
android_app->destroyed = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
// Can't touch android_app object after this.
}
/*
static void process_input(struct android_app* app, __attribute__((unused)) struct android_poll_source* source) {
AInputEvent* event = NULL;
while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) {
LOGV("New input event: type=%d\n", AInputEvent_getType(event));
if (AInputQueue_preDispatchEvent(app->inputQueue, event)) {
continue;
}
int32_t handled = 0;
if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event);
AInputQueue_finishEvent(app->inputQueue, event, handled);
}
}
*/
static void process_cmd(struct android_app* app, __attribute__((unused)) struct android_poll_source* source) {
int8_t cmd = android_app_read_cmd(app);
android_app_pre_exec_cmd(app, cmd);
if (app->onAppCmd != NULL) app->onAppCmd(app, cmd);
android_app_post_exec_cmd(app, cmd);
}
static void* android_app_entry(void* param) {
struct android_app* android_app = (struct android_app*)param;
android_app->config = AConfiguration_new();
AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager);
print_cur_config(android_app);
android_app->cmdPollSource.id = LOOPER_ID_MAIN;
android_app->cmdPollSource.app = android_app;
android_app->cmdPollSource.process = process_cmd;
//android_app->inputPollSource.id = LOOPER_ID_INPUT;
//android_app->inputPollSource.app = android_app;
//android_app->inputPollSource.process = process_input;
ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS);
ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL,
&android_app->cmdPollSource);
android_app->looper = looper;
pthread_mutex_lock(&android_app->mutex);
android_app->running = 1;
pthread_cond_broadcast(&android_app->cond);
pthread_mutex_unlock(&android_app->mutex);
_rust_glue_entry(android_app);
android_app_destroy(android_app);
return NULL;
}
// --------------------------------------------------------------------
// Native activity interaction (called from main thread)
// --------------------------------------------------------------------
static struct android_app* android_app_create(ANativeActivity* activity,
void* savedState, size_t savedStateSize) {
struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app));
memset(android_app, 0, sizeof(struct android_app));
android_app->activity = activity;
pthread_mutex_init(&android_app->mutex, NULL);
pthread_cond_init(&android_app->cond, NULL);
if (savedState != NULL) {
android_app->savedState = malloc(savedStateSize);
android_app->savedStateSize = savedStateSize;
memcpy(android_app->savedState, savedState, savedStateSize);
}
int msgpipe[2];
if (pipe(msgpipe)) {
LOGE("could not create pipe: %s", strerror(errno));
return NULL;
}
android_app->msgread = msgpipe[0];
android_app->msgwrite = msgpipe[1];
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&android_app->thread, &attr, android_app_entry, android_app);
// Wait for thread to start.
pthread_mutex_lock(&android_app->mutex);
while (!android_app->running) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
return android_app;
}
static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) {
if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) {
LOGE("Failure writing android_app cmd: %s\n", strerror(errno));
}
}
static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) {
pthread_mutex_lock(&android_app->mutex);
android_app->pendingInputQueue = inputQueue;
android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED);
while (android_app->inputQueue != android_app->pendingInputQueue) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) {
pthread_mutex_lock(&android_app->mutex);
if (android_app->pendingWindow != NULL) {
android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW);
}
android_app->pendingWindow = window;
if (window != NULL) {
android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW);
}
while (android_app->window != android_app->pendingWindow) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) {
pthread_mutex_lock(&android_app->mutex);
android_app_write_cmd(android_app, cmd);
while (android_app->activityState != cmd) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
}
static void android_app_free(struct android_app* android_app) {
pthread_mutex_lock(&android_app->mutex);
android_app_write_cmd(android_app, APP_CMD_DESTROY);
while (!android_app->destroyed) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
pthread_mutex_unlock(&android_app->mutex);
close(android_app->msgread);
close(android_app->msgwrite);
pthread_cond_destroy(&android_app->cond);
pthread_mutex_destroy(&android_app->mutex);
free(android_app);
}
static void onDestroy(ANativeActivity* activity) {
LOGV("Destroy: %p\n", activity);
android_app_free((struct android_app*)activity->instance);
}
static void onStart(ANativeActivity* activity) {
LOGV("Start: %p\n", activity);
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START);
}
static void onResume(ANativeActivity* activity) {
LOGV("Resume: %p\n", activity);
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME);
}
static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) {
struct android_app* android_app = (struct android_app*)activity->instance;
void* savedState = NULL;
LOGV("SaveInstanceState: %p\n", activity);
pthread_mutex_lock(&android_app->mutex);
android_app->stateSaved = 0;
android_app_write_cmd(android_app, APP_CMD_SAVE_STATE);
while (!android_app->stateSaved) {
pthread_cond_wait(&android_app->cond, &android_app->mutex);
}
if (android_app->savedState != NULL) {
savedState = android_app->savedState;
*outLen = android_app->savedStateSize;
android_app->savedState = NULL;
android_app->savedStateSize = 0;
}
pthread_mutex_unlock(&android_app->mutex);
return savedState;
}
static void onPause(ANativeActivity* activity) {
LOGV("Pause: %p\n", activity);
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE);
}
static void onStop(ANativeActivity* activity) {
LOGV("Stop: %p\n", activity);
android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP);
}
static void onConfigurationChanged(ANativeActivity* activity) {
struct android_app* android_app = (struct android_app*)activity->instance;
LOGV("ConfigurationChanged: %p\n", activity);
android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED);
}
static void onLowMemory(ANativeActivity* activity) {
struct android_app* android_app = (struct android_app*)activity->instance;
LOGV("LowMemory: %p\n", activity);
android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY);
}
static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
LOGV("WindowFocusChanged: %p -- %d\n", activity, focused);
android_app_write_cmd((struct android_app*)activity->instance,
focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
}
static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) {
LOGV("NativeWindowCreated: %p -- %p\n", activity, window);
android_app_set_window((struct android_app*)activity->instance, window);
}
static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) {
LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window);
android_app_set_window((struct android_app*)activity->instance, NULL);
}
static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) {
LOGV("InputQueueCreated: %p -- %p\n", activity, queue);
android_app_set_input((struct android_app*)activity->instance, queue);
}
static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) {
LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue);
android_app_set_input((struct android_app*)activity->instance, NULL);
}
JNIEXPORT
void ANativeActivity_onCreate_C(ANativeActivity* activity, void* savedState,
size_t savedStateSize) {
LOGV("Creating: %p\n", activity);
activity->callbacks->onDestroy = onDestroy;
activity->callbacks->onStart = onStart;
activity->callbacks->onResume = onResume;
activity->callbacks->onSaveInstanceState = onSaveInstanceState;
activity->callbacks->onPause = onPause;
activity->callbacks->onStop = onStop;
activity->callbacks->onConfigurationChanged = onConfigurationChanged;
activity->callbacks->onLowMemory = onLowMemory;
activity->callbacks->onWindowFocusChanged = onWindowFocusChanged;
activity->callbacks->onNativeWindowCreated = onNativeWindowCreated;
activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed;
activity->callbacks->onInputQueueCreated = onInputQueueCreated;
activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed;
activity->instance = android_app_create(activity, savedState, savedStateSize);
}
@@ -1,357 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
#ifndef _ANDROID_NATIVE_APP_GLUE_H
#define _ANDROID_NATIVE_APP_GLUE_H
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <android/configuration.h>
#include <android/looper.h>
#include <android/native_activity.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* The native activity interface provided by <android/native_activity.h>
* is based on a set of application-provided callbacks that will be called
* by the Activity's main thread when certain events occur.
*
* This means that each one of this callbacks _should_ _not_ block, or they
* risk having the system force-close the application. This programming
* model is direct, lightweight, but constraining.
*
* The 'android_native_app_glue' static library is used to provide a different
* execution model where the application can implement its own main event
* loop in a different thread instead. Here's how it works:
*
* 1/ The application must provide a function named "android_main()" that
* will be called when the activity is created, in a new thread that is
* distinct from the activity's main thread.
*
* 2/ android_main() receives a pointer to a valid "android_app" structure
* that contains references to other important objects, e.g. the
* ANativeActivity obejct instance the application is running in.
*
* 3/ the "android_app" object holds an ALooper instance that already
* listens to two important things:
*
* - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX
* declarations below.
*
* - input events coming from the AInputQueue attached to the activity.
*
* Each of these correspond to an ALooper identifier returned by
* ALooper_pollOnce with values of LOOPER_ID_MAIN and LOOPER_ID_INPUT,
* respectively.
*
* Your application can use the same ALooper to listen to additional
* file-descriptors. They can either be callback based, or with return
* identifiers starting with LOOPER_ID_USER.
*
* 4/ Whenever you receive a LOOPER_ID_MAIN or LOOPER_ID_INPUT event,
* the returned data will point to an android_poll_source structure. You
* can call the process() function on it, and fill in android_app->onAppCmd
* and android_app->onInputEvent to be called for your own processing
* of the event.
*
* Alternatively, you can call the low-level functions to read and process
* the data directly... look at the process_cmd() and process_input()
* implementations in the glue to see how to do this.
*
* See the sample named "native-activity" that comes with the NDK with a
* full usage example. Also look at the JavaDoc of NativeActivity.
*/
struct android_app;
/**
* Data associated with an ALooper fd that will be returned as the "outData"
* when that source has data ready.
*/
struct android_poll_source {
// The identifier of this source. May be LOOPER_ID_MAIN or
// LOOPER_ID_INPUT.
int32_t id;
// The android_app this ident is associated with.
struct android_app* app;
// Function to call to perform the standard processing of data from
// this source.
void (*process)(struct android_app* app, struct android_poll_source* source);
};
/**
* This is the interface for the standard glue code of a threaded
* application. In this model, the application's code is running
* in its own thread separate from the main thread of the process.
* It is not required that this thread be associated with the Java
* VM, although it will need to be in order to make JNI calls any
* Java objects.
*/
struct android_app {
// The application can place a pointer to its own state object
// here if it likes.
void* userData;
// Fill this in with the function to process main app commands (APP_CMD_*)
void (*onAppCmd)(struct android_app* app, int32_t cmd);
// Fill this in with the function to process input events. At this point
// the event has already been pre-dispatched, and it will be finished upon
// return. Return 1 if you have handled the event, 0 for any default
// dispatching.
int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event);
// The ANativeActivity object instance that this app is running in.
ANativeActivity* activity;
// The current configuration the app is running in.
AConfiguration* config;
// This is the last instance's saved state, as provided at creation time.
// It is NULL if there was no state. You can use this as you need; the
// memory will remain around until you call android_app_exec_cmd() for
// APP_CMD_RESUME, at which point it will be freed and savedState set to NULL.
// These variables should only be changed when processing a APP_CMD_SAVE_STATE,
// at which point they will be initialized to NULL and you can malloc your
// state and place the information here. In that case the memory will be
// freed for you later.
void* savedState;
size_t savedStateSize;
// The ALooper associated with the app's thread.
ALooper* looper;
// When non-NULL, this is the input queue from which the app will
// receive user input events.
AInputQueue* inputQueue;
// When non-NULL, this is the window surface that the app can draw in.
ANativeWindow* window;
// Current content rectangle of the window; this is the area where the
// window's content should be placed to be seen by the user.
ARect contentRect;
// Current state of the app's activity. May be either APP_CMD_START,
// APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below.
int activityState;
// This is non-zero when the application's NativeActivity is being
// destroyed and waiting for the app thread to complete.
int destroyRequested;
// -------------------------------------------------
// Below are "private" implementation of the glue code.
pthread_mutex_t mutex;
pthread_cond_t cond;
int msgread;
int msgwrite;
pthread_t thread;
struct android_poll_source cmdPollSource;
struct android_poll_source inputPollSource;
int running;
int stateSaved;
int destroyed;
int redrawNeeded;
AInputQueue* pendingInputQueue;
ANativeWindow* pendingWindow;
ARect pendingContentRect;
};
enum {
/**
* Looper data ID of commands coming from the app's main thread, which
* is returned as an identifier from ALooper_pollOnce(). The data for this
* identifier is a pointer to an android_poll_source structure.
* These can be retrieved and processed with android_app_read_cmd()
* and android_app_exec_cmd().
*/
LOOPER_ID_MAIN = 1,
/**
* Looper data ID of events coming from the AInputQueue of the
* application's window, which is returned as an identifier from
* ALooper_pollOnce(). The data for this identifier is a pointer to an
* android_poll_source structure. These can be read via the inputQueue
* object of android_app.
*/
LOOPER_ID_INPUT = 2,
/**
* Start of user-defined ALooper identifiers.
*/
LOOPER_ID_USER = 3,
};
enum {
/**
* Command from main thread: the AInputQueue has changed. Upon processing
* this command, android_app->inputQueue will be updated to the new queue
* (or NULL).
*/
APP_CMD_INPUT_CHANGED,
/**
* Command from main thread: a new ANativeWindow is ready for use. Upon
* receiving this command, android_app->window will contain the new window
* surface.
*/
APP_CMD_INIT_WINDOW,
/**
* Command from main thread: the existing ANativeWindow needs to be
* terminated. Upon receiving this command, android_app->window still
* contains the existing window; after calling android_app_exec_cmd
* it will be set to NULL.
*/
APP_CMD_TERM_WINDOW,
/**
* Command from main thread: the current ANativeWindow has been resized.
* Please redraw with its new size.
*/
APP_CMD_WINDOW_RESIZED,
/**
* Command from main thread: the system needs that the current ANativeWindow
* be redrawn. You should redraw the window before handing this to
* android_app_exec_cmd() in order to avoid transient drawing glitches.
*/
APP_CMD_WINDOW_REDRAW_NEEDED,
/**
* Command from main thread: the content area of the window has changed,
* such as from the soft input window being shown or hidden. You can
* find the new content rect in android_app::contentRect.
*/
APP_CMD_CONTENT_RECT_CHANGED,
/**
* Command from main thread: the app's activity window has gained
* input focus.
*/
APP_CMD_GAINED_FOCUS,
/**
* Command from main thread: the app's activity window has lost
* input focus.
*/
APP_CMD_LOST_FOCUS,
/**
* Command from main thread: the current device configuration has changed.
*/
APP_CMD_CONFIG_CHANGED,
/**
* Command from main thread: the system is running low on memory.
* Try to reduce your memory use.
*/
APP_CMD_LOW_MEMORY,
/**
* Command from main thread: the app's activity has been started.
*/
APP_CMD_START,
/**
* Command from main thread: the app's activity has been resumed.
*/
APP_CMD_RESUME,
/**
* Command from main thread: the app should generate a new saved state
* for itself, to restore from later if needed. If you have saved state,
* allocate it with malloc and place it in android_app.savedState with
* the size in android_app.savedStateSize. The will be freed for you
* later.
*/
APP_CMD_SAVE_STATE,
/**
* Command from main thread: the app's activity has been paused.
*/
APP_CMD_PAUSE,
/**
* Command from main thread: the app's activity has been stopped.
*/
APP_CMD_STOP,
/**
* Command from main thread: the app's activity is being destroyed,
* and waiting for the app thread to clean up and exit before proceeding.
*/
APP_CMD_DESTROY,
};
/**
* Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next
* app command message.
*/
int8_t android_app_read_cmd(struct android_app* android_app);
/**
* Call with the command returned by android_app_read_cmd() to do the
* initial pre-processing of the given command. You can perform your own
* actions for the command after calling this function.
*/
void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd);
/**
* Call with the command returned by android_app_read_cmd() to do the
* final post-processing of the given command. You must have done your own
* actions for the command before calling this function.
*/
void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd);
void android_app_attach_input_queue_looper(struct android_app* android_app);
void android_app_detach_input_queue_looper(struct android_app* android_app);
/**
* Dummy function that used to be used to prevent the linker from stripping app
* glue code. No longer necessary, since __attribute__((visibility("default")))
* does this for us.
*/
__attribute__((
deprecated("Calls to app_dummy are no longer necessary. See "
"https://github.com/android-ndk/ndk/issues/381."))) void
app_dummy();
/**
* This is the function that application code must implement, representing
* the main entry to the app.
*/
extern void _rust_glue_entry(struct android_app* app);
#ifdef __cplusplus
}
#endif
#endif /* _ANDROID_NATIVE_APP_GLUE_H */
+7
View File
@@ -6,6 +6,13 @@ use ndk::configuration::{
ScreenSize, Touchscreen, UiModeNight, UiModeType,
};
/// A (cheaply clonable) reference to this application's [`ndk::configuration::Configuration`]
///
/// This provides a thread-safe way to access the latest configuration state for
/// an application without deeply copying the large [`ndk::configuration::Configuration`] struct.
///
/// If the application is notified of configuration changes then those changes
/// will become visible via pre-existing configuration references.
#[derive(Clone)]
pub struct ConfigurationRef {
config: Arc<RwLock<Configuration>>,
+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,
}
}
}
+2 -3
View File
@@ -13,9 +13,8 @@
#![allow(dead_code)]
use jni_sys::*;
use ndk_sys::AAssetManager;
use ndk_sys::ANativeWindow;
use ndk_sys::{AConfiguration, ALooper, ALooper_callbackFunc};
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t};
use ndk_sys::{AAssetManager, AConfiguration, ALooper, ALooper_callbackFunc, ANativeWindow, ARect};
#[cfg(all(
any(target_os = "android", feature = "test"),
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
+478 -154
View File
@@ -1,17 +1,20 @@
#![cfg(feature = "game-activity")]
use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::marker::PhantomData;
use std::ops::Deref;
use std::os::raw;
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};
use libc::c_void;
use log::{error, trace, Level};
use jni_sys::*;
@@ -23,12 +26,19 @@ use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
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,
};
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
@@ -48,14 +58,14 @@ impl<'a> StateSaver<'a> {
// In case the application calls store() multiple times for some reason we
// make sure to free any pre-existing state...
if (*app_ptr).savedState != ptr::null_mut() {
if !(*app_ptr).savedState.is_null() {
libc::free((*app_ptr).savedState);
(*app_ptr).savedState = ptr::null_mut();
(*app_ptr).savedStateSize = 0;
}
let buf = libc::malloc(state.len());
if buf == ptr::null_mut() {
if buf.is_null() {
panic!("Failed to allocate save_state buffer");
}
@@ -69,7 +79,7 @@ impl<'a> StateSaver<'a> {
}
(*app_ptr).savedState = buf;
(*app_ptr).savedStateSize = state.len() as ffi::size_t;
(*app_ptr).savedStateSize = state.len() as _;
}
}
}
@@ -82,10 +92,10 @@ impl<'a> StateLoader<'a> {
pub fn load(&self) -> Option<Vec<u8>> {
unsafe {
let app_ptr = self.app.native_app.as_ptr();
if (*app_ptr).savedState != ptr::null_mut() && (*app_ptr).savedStateSize > 0 {
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)
@@ -115,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()
@@ -123,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>,
}
@@ -145,15 +168,126 @@ impl Deref for NativeAppGlue {
unsafe impl Send for NativeAppGlue {}
unsafe impl Sync for NativeAppGlue {}
impl NativeAppGlue {
// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
unsafe {
let activity = (*self.as_ptr()).activity;
let mut out_state = TextInputState {
text: String::new(),
selection: TextSpan { start: 0, end: 0 },
compose_region: None,
};
let out_ptr = &mut out_state as *mut TextInputState;
let app_ptr = self.as_ptr();
(*app_ptr).textInputState = 0;
// NEON WARNING:
//
// It's not clearly documented but the GameActivity API over the
// GameTextInput library directly exposes _modified_ UTF8 text
// from Java so we need to be careful to convert text to and
// from UTF8
//
// GameTextInput also uses a pre-allocated, fixed-sized buffer for
// the current text state and has shared `currentState_` that
// appears to have no lock to guard access from multiple threads.
//
// There's also no locking at the GameActivity level, so I'm fairly
// certain that `GameActivity_getTextInputState` isn't thread
// safe: https://issuetracker.google.com/issues/294112477
//
// Overall this is all quite gnarly - and probably a good reminder
// of why we want to use Rust instead of C/C++.
ffi::GameActivity_getTextInputState(
activity,
Some(AndroidAppInner::map_input_state_to_text_event_callback),
out_ptr.cast(),
);
out_state
}
}
// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
unsafe {
let activity = (*self.as_ptr()).activity;
let modified_utf8 = cesu8::to_java_cesu8(&state.text);
let text_length = modified_utf8.len() as i32;
let modified_utf8_bytes = modified_utf8.as_ptr();
let ffi_state = ffi::GameTextInputState {
text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
text_length,
selection: ffi::GameTextInputSpan {
start: state.selection.start as i32,
end: state.selection.end as i32,
},
composingRegion: match state.compose_region {
Some(span) => {
// The GameText subclass of InputConnection only has a special case for removing the
// compose region if `start == -1` but the docs for `setComposingRegion` imply that
// the region should effectively be removed if any empty region is given (unlike for the
// selection region, it's not meaningful to maintain an empty compose region)
//
// We aim for more consistent behaviour by normalizing any empty region into `(-1, -1)`
// to remove the compose region.
//
// NB `setComposingRegion` itself is documented to clamp start/end to the text bounds
// so apart from this special-case handling in GameText's implementation of
// `setComposingRegion` then there's nothing special about `(-1, -1)` - it's just an empty
// region that should get clamped to `(0, 0)` and then get removed.
if span.start == span.end {
ffi::GameTextInputSpan { start: -1, end: -1 }
} else {
ffi::GameTextInputSpan {
start: span.start as i32,
end: span.end as i32,
}
}
}
None => ffi::GameTextInputSpan { start: -1, end: -1 },
},
};
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
}
}
}
#[derive(Debug)]
pub struct AndroidAppInner {
pub(crate) jvm: CloneJavaVM,
native_app: NativeAppGlue,
config: ConfigurationRef,
native_window: RwLock<Option<NativeWindow>>,
/// Shared JNI bindings for the `KeyCharacterMap` class
key_map_binding: Arc<KeyCharacterMapBinding>,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
/// characters
key_maps: Mutex<HashMap<i32, KeyCharacterMap>>,
/// While an app is reading input events it holds an
/// InputReceiver reference which we track to ensure
/// we don't hand out more than one receiver at a time
input_receiver: Mutex<Option<Weak<InputReceiver>>>,
}
impl AndroidAppInner {
pub fn native_window<'a>(&self) -> Option<NativeWindow> {
pub fn vm_as_ptr(&self) -> *mut c_void {
let app_ptr = self.native_app.as_ptr();
unsafe { (*(*app_ptr).activity).vm as _ }
}
pub fn activity_as_ptr(&self) -> *mut c_void {
let app_ptr = self.native_app.as_ptr();
unsafe { (*(*app_ptr).activity).javaGameActivity as _ }
}
pub fn native_window(&self) -> Option<NativeWindow> {
self.native_window.read().unwrap().clone()
}
@@ -212,7 +346,7 @@ impl AndroidAppInner {
ffi::NativeAppGlueLooperId_LOOPER_ID_MAIN => {
trace!("ALooper_pollAll returned ID_MAIN");
let source: *mut ffi::android_poll_source = source.cast();
if source != ptr::null_mut() {
if !source.is_null() {
let cmd_i = ffi::android_app_read_cmd(native_app.as_ptr());
let cmd = match cmd_i as u32 {
@@ -246,11 +380,11 @@ impl AndroidAppInner {
}
ffi::NativeAppGlueAppCmd_APP_CMD_START => MainEvent::Start,
ffi::NativeAppGlueAppCmd_APP_CMD_RESUME => MainEvent::Resume {
loader: StateLoader { app: &self },
loader: StateLoader { app: self },
},
ffi::NativeAppGlueAppCmd_APP_CMD_SAVE_STATE => {
MainEvent::SaveState {
saver: StateSaver { app: &self },
saver: StateSaver { app: self },
}
}
ffi::NativeAppGlueAppCmd_APP_CMD_PAUSE => MainEvent::Pause,
@@ -346,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) }
}
@@ -389,25 +592,26 @@ impl AndroidAppInner {
}
}
pub fn input_events<'b, F>(&self, mut callback: F)
where
F: FnMut(&InputEvent),
{
let buf = unsafe {
let app_ptr = self.native_app.as_ptr();
let input_buffer = ffi::android_app_swap_input_buffers(app_ptr);
if input_buffer == ptr::null_mut() {
return;
}
InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer))
};
pub(crate) fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
let mut guard = self.input_receiver.lock().unwrap();
for key_event in buf.key_events_iter() {
callback(&InputEvent::KeyEvent(key_event));
}
for motion_event in buf.motion_events_iter() {
callback(&InputEvent::MotionEvent(motion_event));
// 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> {
@@ -432,46 +636,68 @@ impl AndroidAppInner {
}
}
struct MotionEventsIterator<'a> {
struct MotionEventsLendingIterator {
pos: usize,
count: usize,
buffer: &'a InputBuffer<'a>,
}
impl<'a> Iterator for MotionEventsIterator<'a> {
type Item = MotionEvent;
fn next(&mut self) -> Option<Self::Item> {
impl MotionEventsLendingIterator {
fn new(buffer: &InputBuffer) -> Self {
Self {
pos: 0,
count: buffer.motion_events_count(),
}
}
fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option<MotionEvent<'buf>> {
if self.pos < self.count {
unsafe {
let ga_event = (*self.buffer.ptr.as_ptr()).motionEvents[self.pos];
let event = MotionEvent::new(ga_event);
self.pos += 1;
Some(event)
}
// 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)
} else {
None
}
}
}
struct KeyEventsIterator<'a> {
struct KeyEventsLendingIterator {
pos: usize,
count: usize,
buffer: &'a InputBuffer<'a>,
}
impl<'a> Iterator for KeyEventsIterator<'a> {
type Item = KeyEvent;
fn next(&mut self) -> Option<Self::Item> {
impl KeyEventsLendingIterator {
fn new(buffer: &InputBuffer) -> Self {
Self {
pos: 0,
count: buffer.key_events_count(),
}
}
fn next<'buf>(&mut self, buffer: &'buf InputBuffer) -> Option<KeyEvent<'buf>> {
if self.pos < self.count {
unsafe {
let ga_event = (*self.buffer.ptr.as_ptr()).keyEvents[self.pos];
let event = KeyEvent::new(ga_event);
self.pos += 1;
Some(event)
}
// 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)
} else {
None
}
@@ -487,33 +713,16 @@ impl<'a> InputBuffer<'a> {
pub(crate) fn from_ptr(ptr: NonNull<ffi::android_input_buffer>) -> InputBuffer<'a> {
Self {
ptr,
_lifetime: PhantomData::default(),
_lifetime: PhantomData,
}
}
// XXX: It's really not ideal here that Rust iterators can't yield values
// that borrow from the iterator, so we implicitly have to copy the
// events as we iterate...
pub fn motion_events_iter<'b>(&'b self) -> MotionEventsIterator<'b> {
unsafe {
let count = (*self.ptr.as_ptr()).motionEventsCount as usize;
MotionEventsIterator {
pos: 0,
count,
buffer: self,
}
}
pub fn motion_events_count(&self) -> usize {
unsafe { (*self.ptr.as_ptr()).motionEventsCount as usize }
}
pub fn key_events_iter<'b>(&'b self) -> KeyEventsIterator<'b> {
unsafe {
let count = (*self.ptr.as_ptr()).keyEventsCount as usize;
KeyEventsIterator {
pos: 0,
count,
buffer: self,
}
}
pub fn key_events_count(&self) -> usize {
unsafe { (*self.ptr.as_ptr()).keyEventsCount as usize }
}
}
@@ -526,53 +735,162 @@ 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(
activity: *mut ffi::GameActivity,
savedState: *mut ::std::os::raw::c_void,
savedStateSize: ffi::size_t,
savedStateSize: libc::size_t,
);
}
#[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,
)
}
@@ -580,24 +898,11 @@ pub unsafe extern "C" fn Java_com_google_androidgamesdk_GameActivity_loadNativeC
pub unsafe extern "C" fn GameActivity_onCreate(
activity: *mut ffi::GameActivity,
saved_state: *mut ::std::os::raw::c_void,
saved_state_size: ffi::size_t,
saved_state_size: libc::size_t,
) {
GameActivity_onCreate_C(activity, saved_state, saved_state_size);
}
fn android_log(level: Level, tag: &CStr, msg: &CStr) {
let prio = match level {
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
};
unsafe {
ndk_sys::__android_log_write(prio.0 as raw::c_int, tag.as_ptr(), msg.as_ptr());
}
}
extern "Rust" {
pub fn android_main(app: AndroidApp);
}
@@ -606,59 +911,78 @@ extern "Rust" {
// `app_main` function. This is run on a dedicated thread spawned
// by android_native_app_glue.
#[no_mangle]
pub unsafe extern "C" fn _rust_glue_entry(app: *mut ffi::android_app) {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
thread::spawn(move || {
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
let file = File::from_raw_fd(logpipe[0]);
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
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 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 mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
}
}
}
});
let jvm = unsafe {
let jvm = (*(*native_app).activity).vm;
let activity: jobject = (*(*native_app).activity).javaGameActivity;
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let jvm = CloneJavaVM::from_raw(jvm).unwrap();
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
jvm.attach_current_thread_permanently().unwrap();
jvm
};
unsafe {
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone());
// We want to specifically catch any panic from the application's android_main
// so we can finish + destroy the Activity gracefully via the JVM
catch_unwind(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(|panic| log_panic(panic));
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ffi::GameActivity_finish((*native_app).activity);
// This should detach automatically but lets detach explicitly to avoid depending
// on the TLS trickery in `jni-rs`
jvm.detach_current_thread();
ndk_context::release_android_context();
}
});
let jvm: *mut JavaVM = (*(*app).activity).vm;
let activity: jobject = (*(*app).activity).javaGameActivity;
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let app = AndroidApp::from_ptr(NonNull::new(app).unwrap());
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
}
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
detach_current_thread(jvm);
}
ndk_context::release_android_context();
})
}
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,
))
}
+392 -47
View File
@@ -1,8 +1,124 @@
//! A glue layer for building standalone, Rust applications on Android
//!
//! This crate provides a "glue" layer for building native Rust
//! applications on Android, supporting multiple [`Activity`] base classes.
//! It's comparable to [`android_native_app_glue.c`][ndk_concepts]
//! for C/C++ applications.
//!
//! Currently the crate supports two `Activity` base classes:
//! 1. [`NativeActivity`] - Built in to Android, this doesn't require compiling any Java or Kotlin code.
//! 2. [`GameActivity`] - From the Android Game Development Kit, it has more
//! sophisticated input handling support than `NativeActivity`. `GameActivity`
//! is also based on the `AndroidAppCompat` class which can help with supporting
//! a wider range of devices.
//!
//! Standalone applications based on this crate need to be built as `cdylib` libraries, like:
//! ```
//! [lib]
//! crate_type=["cdylib"]
//! ```
//!
//! and implement a `#[no_mangle]` `android_main` entry point like this:
//! ```rust
//! #[no_mangle]
//! fn android_main(app: AndroidApp) {
//!
//! }
//! ```
//!
//! Once your application's `Activity` class has loaded and it calls `onCreate` then
//! `android-activity` will spawn a dedicated thread to run your `android_main` function,
//! separate from the Java thread that created the corresponding `Activity`.
//!
//! [`AndroidApp`] provides an interface to query state for the application as
//! well as monitor events, such as lifecycle and input events, that are
//! marshalled between the Java thread that owns the `Activity` and the native
//! thread that runs the `android_main()` code.
//!
//! # Cheaply Clonable [`AndroidApp`]
//!
//! [`AndroidApp`] is intended to be something that can be cheaply passed around
//! by referenced within an application. It is reference counted and can be
//! cheaply cloned.
//!
//! # `Send` and `Sync` [`AndroidApp`]
//!
//! Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
//! into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
//! explicitly documented to only be usable from your `android_main()` thread.
//!
//! # Main Thread Initialization
//!
//! Before `android_main()` is called, the following application state
//! is also initialized:
//!
//! 1. An I/O thread is spawned that will handle redirecting standard input
//! and output to the Android log, visible via `logcat`.
//! 2. A `JavaVM` and `Activity` instance will be associated with the [`ndk_context`] crate
//! so that other, independent, Rust crates are able to find a JavaVM
//! for making JNI calls.
//! 3. The `JavaVM` will be attached to the native thread
//! 4. A [Looper] is attached to the Rust native thread.
//!
//!
//! These are undone after `android_main()` returns
//!
//! # Android Extensible Enums
//!
//! There are numerous enums in the `android-activity` API which are effectively
//! bindings to enums declared in the Android SDK which need to be considered
//! _runtime_ extensible.
//!
//! Any enum variants that come from the Android SDK may be extended in future
//! versions of Android and your code could be exposed to new variants if you
//! build an application that might be installed on new versions of Android.
//!
//! This crate follows a convention of adding a hidden `__Unknown(u32)` variant
//! to these enum to ensure we can always do lossless conversions between the
//! integers from the SDK and our corresponding Rust enums. This can be
//! important in case you need to pass certain variants back to the SDK
//! regardless of whether you knew about that variants specific semantics at
//! compile time.
//!
//! You should never include this `__Unknown(u32)` variant within any exhaustive
//! pattern match and should instead treat the enums like `#[non_exhaustive]`
//! enums that require you to add a catch-all for any `unknown => {}` values.
//!
//! Any code that would exhaustively include the `__Unknown(u32)` variant when
//! pattern matching can not be guaranteed to be forwards compatible with new
//! releases of `android-activity` which may add new Rust variants to these
//! enums without requiring a breaking semver bump.
//!
//! You can (infallibly) convert these enums to and from primitive `u32` values
//! using `.into()`:
//!
//! For example, here is how you could ensure forwards compatibility with both
//! compile-time and runtime extensions of a `SomeEnum` enum:
//!
//! ```rust
//! match some_enum {
//! SomeEnum::Foo => {},
//! SomeEnum::Bar => {},
//! unhandled => {
//! let sdk_val: u32 = unhandled.into();
//! println!("Unhandled enum variant {some_enum:?} has SDK value: {sdk_val}");
//! }
//! }
//! ```
//!
//! [`Activity`]: https://developer.android.com/reference/android/app/Activity
//! [`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity
//! [ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa
//! [`GameActivity`]: https://developer.android.com/games/agdk/integrate-game-activity
//! [Looper]: https://developer.android.com/reference/android/os/Looper
use std::hash::Hash;
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
use input::KeyCharacterMap;
use libc::c_void;
use ndk::asset::AssetManager;
use ndk::native_window::NativeWindow;
@@ -13,14 +129,14 @@ compile_error!("android-activity only supports compiling for Android");
#[cfg(all(feature = "game-activity", feature = "native-activity"))]
compile_error!(
"The \"game-activity\" and \"native-activity\" features cannot be enabled at the same time"
r#"The "game-activity" and "native-activity" features cannot be enabled at the same time"#
);
#[cfg(all(
not(any(feature = "game-activity", feature = "native-activity")),
not(doc)
))]
compile_error!(
r#"Either \"game-activity\" or \"native-activity\" must be enabled as features
r#"Either "game-activity" or "native-activity" must be enabled as features
If you have set one of these features then this error indicates that Cargo is trying to
link together multiple implementations of android-activity (with incompatible versions)
@@ -33,34 +149,26 @@ You can use `cargo tree` (e.g. via `cargo ndk -t arm64-v8a tree`) to identify wh
versions have been resolved.
You may need to add a `[patch]` into your Cargo.toml to ensure a specific version of
android-activity is used across all of your application's crates.
"#
android-activity is used across all of your application's crates."#
);
#[cfg(any(feature = "native-activity", doc))]
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 use activity_impl::input;
pub mod input;
mod config;
pub use config::ConfigurationRef;
mod util;
// Note: unlike in ndk-glue this has signed components (consistent
// with Android's ARect) which generally allows for representing
// rectangles with a negative/off-screen origin. Even though this
// is currently just used to represent the content rect (that probably
// wouldn't have any negative components) we keep the generality
// since this is a primitive type that could potentially be used
// for more things in the future.
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 {
pub left: i32,
@@ -69,20 +177,55 @@ pub struct Rect {
pub bottom: i32,
}
pub type StateSaver<'a> = activity_impl::StateSaver<'a>;
pub type StateLoader<'a> = activity_impl::StateLoader<'a>;
impl Rect {
/// An empty `Rect` with all components set to zero.
pub fn empty() -> Self {
Self {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
}
impl From<Rect> for ndk_sys::ARect {
fn from(rect: Rect) -> Self {
Self {
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
}
}
}
impl From<ndk_sys::ARect> for Rect {
fn from(arect: ndk_sys::ARect) -> Self {
Self {
left: arect.left,
right: arect.right,
top: arect.top,
bottom: arect.bottom,
}
}
}
pub use activity_impl::StateLoader;
pub use activity_impl::StateSaver;
/// An application event delivered during [`AndroidApp::poll_events`]
#[non_exhaustive]
#[derive(Debug)]
pub enum MainEvent<'a> {
/// New input events are available via [`AndroidApp::input_events()`]
/// 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
@@ -163,6 +306,7 @@ pub enum MainEvent<'a> {
InsetsChanged {},
}
/// An event delivered during [`AndroidApp::poll_events`]
#[derive(Debug)]
#[non_exhaustive]
pub enum PollEvent<'a> {
@@ -171,12 +315,22 @@ pub enum PollEvent<'a> {
Main(MainEvent<'a>),
}
/// Indicates whether an application has handled or ignored an event
///
/// If an event is not handled by an application then some default handling may happen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputStatus {
Handled,
Unhandled,
}
use activity_impl::AndroidAppInner;
pub use activity_impl::AndroidAppWaker;
bitflags! {
/// Flags for [`AndroidApp::set_window_flags`]
/// as per the [android.view.WindowManager.LayoutParams Java API](https://developer.android.com/reference/android/view/WindowManager.LayoutParams)
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct WindowManagerFlags: u32 {
/// As long as this window is visible to the user, allow the lock
/// screen to activate while the screen is on. This can be used
@@ -340,6 +494,25 @@ bitflags! {
}
}
/// The top-level state and interface for a native Rust application
///
/// `AndroidApp` provides an interface to query state for the application as
/// well as monitor events, such as lifecycle and input events, that are
/// marshalled between the Java thread that owns the `Activity` and the native
/// thread that runs the `android_main()` code.
///
/// # Cheaply Clonable [`AndroidApp`]
///
/// [`AndroidApp`] is intended to be something that can be cheaply passed around
/// by referenced within an application. It is reference counted and can be
/// cheaply cloned.
///
/// # `Send` and `Sync` [`AndroidApp`]
///
/// Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take
/// into consideration that some APIs, such as [`AndroidApp::poll_events()`] are
/// explicitly documented to only be usable from your `android_main()` thread.
///
#[derive(Debug, Clone)]
pub struct AndroidApp {
pub(crate) inner: Arc<RwLock<AndroidAppInner>>,
@@ -359,22 +532,59 @@ impl Hash for AndroidApp {
}
impl AndroidApp {
#[cfg_attr(docsrs, doc(cfg(feature = "native-activity")))]
#[cfg(feature = "native-activity")]
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
self.inner.read().unwrap().native_activity()
}
/// Queries the current [`NativeWindow`] for the application.
///
/// This will only return `Some(window)` between
/// [`MainEvent::InitWindow`] and [`MainEvent::TerminateWindow`]
/// events.
pub fn native_window<'a>(&self) -> Option<NativeWindow> {
pub fn native_window(&self) -> Option<NativeWindow> {
self.inner.read().unwrap().native_window()
}
/// Polls for any events associated with this AndroidApp and processes those events
/// Returns a pointer to the Java Virtual Machine, for making JNI calls
///
/// This returns a pointer to the Java Virtual Machine which can be used
/// with the [`jni`] crate (or similar crates) to make JNI calls that bridge
/// between native Rust code and Java/Kotlin code running within the JVM.
///
/// If you use the [`jni`] crate you can wrap this as a [`JavaVM`] via:
/// ```ignore
/// # use jni::JavaVM;
/// # let app: AndroidApp = todo!();
/// let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr()) };
/// ```
///
/// [`jni`]: https://crates.io/crates/jni
/// [`JavaVM`]: https://docs.rs/jni/latest/jni/struct.JavaVM.html
pub fn vm_as_ptr(&self) -> *mut c_void {
self.inner.read().unwrap().vm_as_ptr()
}
/// Returns a JNI object reference for this application's JVM `Activity` as a pointer
///
/// If you use the [`jni`] crate you can wrap this as an object reference via:
/// ```ignore
/// # use jni::objects::JObject;
/// # let app: AndroidApp = todo!();
/// let activity = unsafe { JObject::from_raw(app.activity_as_ptr()) };
/// ```
///
/// # JNI Safety
///
/// Note that the object reference will be a JNI global reference, not a
/// local reference and it should not be deleted. Don't wrap the reference
/// in an [`AutoLocal`] which would try to explicitly delete the reference
/// when dropped. Similarly, don't wrap the reference as a [`GlobalRef`]
/// which would also try to explicitly delete the reference when dropped.
///
/// [`jni`]: https://crates.io/crates/jni
/// [`AutoLocal`]: https://docs.rs/jni/latest/jni/objects/struct.AutoLocal.html
/// [`GlobalRef`]: https://docs.rs/jni/latest/jni/objects/struct.GlobalRef.html
pub fn activity_as_ptr(&self) -> *mut c_void {
self.inner.read().unwrap().activity_as_ptr()
}
/// Polls for any events associated with this [AndroidApp] and processes those events
/// (such as lifecycle events) via the given `callback`.
///
/// It's important to use this API for polling, and not call [`ALooper_pollAll`] directly since
@@ -386,17 +596,22 @@ impl AndroidApp {
/// main thread. The [`MainEvent::SaveState`] event is also synchronized with the
/// Java main thread.
///
/// # Panics
///
/// This must only be called from your `android_main()` thread and it may panic if called
/// from another thread.
///
/// [`ALooper_pollAll`]: ndk::looper::ThreadLooper::poll_all
pub fn poll_events<F>(&self, timeout: Option<Duration>, callback: F)
where
F: FnMut(PollEvent),
F: FnMut(PollEvent<'_>),
{
self.inner.read().unwrap().poll_events(timeout, callback);
}
/// Creates a means to wake up the main loop while it is blocked waiting for
/// events within [`AndroidApp::poll_events()`].
pub fn create_waker(&self) -> activity_impl::AndroidAppWaker {
pub fn create_waker(&self) -> AndroidAppWaker {
self.inner.read().unwrap().create_waker()
}
@@ -471,20 +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
///
/// 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._
/// Applications are expected to call this in-sync with their rendering or
/// in response to a [`MainEvent::InputAvailable`] event being delivered.
///
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// _**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
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
pub fn input_events<'b, F>(&self, callback: F)
where
F: FnMut(&input::InputEvent),
{
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
@@ -493,7 +837,8 @@ impl AndroidApp {
pub fn sdk_version() -> i32 {
let mut prop = android_properties::getprop("ro.build.version.sdk");
if let Some(val) = prop.value() {
i32::from_str_radix(&val, 10).expect("Failed to parse ro.build.version.sdk property")
val.parse::<i32>()
.expect("Failed to parse ro.build.version.sdk property")
} else {
panic!("Couldn't read ro.build.version.sdk system property");
}
@@ -1,33 +0,0 @@
//! The bindings are pre-generated and the right one for the platform is selected at compile time.
// Bindgen lints
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(improper_ctypes)]
#![allow(clippy::all)]
// Temporarily allow UB nullptr dereference in bindgen layout tests until fixed upstream:
// https://github.com/rust-lang/rust-bindgen/pull/2055
// https://github.com/rust-lang/rust-bindgen/pull/2064
#![allow(deref_nullptr)]
#![allow(dead_code)]
use jni_sys::*;
use ndk_sys::AAssetManager;
use ndk_sys::ANativeWindow;
use ndk_sys::{AConfiguration, AInputQueue, ALooper};
#[cfg(all(
any(target_os = "android", feature = "test"),
any(target_arch = "arm", target_arch = "armv7")
))]
include!("ffi_arm.rs");
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "aarch64"))]
include!("ffi_aarch64.rs");
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86"))]
include!("ffi_i686.rs");
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86_64"))]
include!("ffi_x86_64.rs");
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
@@ -0,0 +1,942 @@
//! This 'glue' layer acts as an IPC shim between the JVM main thread and the Rust
//! main thread. Notifying Rust of lifecycle events from the JVM and handling
//! synchronization between the two threads.
use std::{
ffi::{CStr, CString},
fs::File,
io::{BufRead, BufReader},
ops::Deref,
os::unix::prelude::{FromRawFd, RawFd},
panic::catch_unwind,
ptr::{self, NonNull},
sync::{Arc, Condvar, Mutex, Weak},
};
use log::Level;
use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow};
use crate::{
jni_utils::CloneJavaVM,
util::android_log,
util::{abort_on_panic, log_panic},
ConfigurationRef,
};
use super::{AndroidApp, Rect};
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum AppCmd {
InputQueueChanged = 0,
InitWindow = 1,
TermWindow = 2,
WindowResized = 3,
WindowRedrawNeeded = 4,
ContentRectChanged = 5,
GainedFocus = 6,
LostFocus = 7,
ConfigChanged = 8,
LowMemory = 9,
Start = 10,
Resume = 11,
SaveState = 12,
Pause = 13,
Stop = 14,
Destroy = 15,
}
impl TryFrom<i8> for AppCmd {
type Error = ();
fn try_from(value: i8) -> Result<Self, Self::Error> {
match value {
0 => Ok(AppCmd::InputQueueChanged),
1 => Ok(AppCmd::InitWindow),
2 => Ok(AppCmd::TermWindow),
3 => Ok(AppCmd::WindowResized),
4 => Ok(AppCmd::WindowRedrawNeeded),
5 => Ok(AppCmd::ContentRectChanged),
6 => Ok(AppCmd::GainedFocus),
7 => Ok(AppCmd::LostFocus),
8 => Ok(AppCmd::ConfigChanged),
9 => Ok(AppCmd::LowMemory),
10 => Ok(AppCmd::Start),
11 => Ok(AppCmd::Resume),
12 => Ok(AppCmd::SaveState),
13 => Ok(AppCmd::Pause),
14 => Ok(AppCmd::Stop),
15 => Ok(AppCmd::Destroy),
_ => Err(()),
}
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum State {
Init,
Start,
Resume,
Pause,
Stop,
}
#[derive(Debug)]
pub struct WaitableNativeActivityState {
pub activity: *mut ndk_sys::ANativeActivity,
pub mutex: Mutex<NativeActivityState>,
pub cond: Condvar,
}
#[derive(Debug, Clone)]
pub struct NativeActivityGlue {
pub inner: Arc<WaitableNativeActivityState>,
}
unsafe impl Send for NativeActivityGlue {}
unsafe impl Sync for NativeActivityGlue {}
impl Deref for NativeActivityGlue {
type Target = WaitableNativeActivityState;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl NativeActivityGlue {
pub fn new(
activity: *mut ndk_sys::ANativeActivity,
saved_state: *const libc::c_void,
saved_state_size: libc::size_t,
) -> Self {
let glue = Self {
inner: Arc::new(WaitableNativeActivityState::new(
activity,
saved_state,
saved_state_size,
)),
};
let weak_ref = Arc::downgrade(&glue.inner);
let weak_ptr = Weak::into_raw(weak_ref);
unsafe {
(*activity).instance = weak_ptr as *mut _;
(*(*activity).callbacks).onDestroy = Some(on_destroy);
(*(*activity).callbacks).onStart = Some(on_start);
(*(*activity).callbacks).onResume = Some(on_resume);
(*(*activity).callbacks).onSaveInstanceState = Some(on_save_instance_state);
(*(*activity).callbacks).onPause = Some(on_pause);
(*(*activity).callbacks).onStop = Some(on_stop);
(*(*activity).callbacks).onConfigurationChanged = Some(on_configuration_changed);
(*(*activity).callbacks).onLowMemory = Some(on_low_memory);
(*(*activity).callbacks).onWindowFocusChanged = Some(on_window_focus_changed);
(*(*activity).callbacks).onNativeWindowCreated = Some(on_native_window_created);
(*(*activity).callbacks).onNativeWindowResized = Some(on_native_window_resized);
(*(*activity).callbacks).onNativeWindowRedrawNeeded =
Some(on_native_window_redraw_needed);
(*(*activity).callbacks).onNativeWindowDestroyed = Some(on_native_window_destroyed);
(*(*activity).callbacks).onInputQueueCreated = Some(on_input_queue_created);
(*(*activity).callbacks).onInputQueueDestroyed = Some(on_input_queue_destroyed);
(*(*activity).callbacks).onContentRectChanged = Some(on_content_rect_changed);
}
glue
}
/// Returns the file descriptor that needs to be polled by the Rust main thread
/// for events/commands from the JVM thread
pub fn cmd_read_fd(&self) -> libc::c_int {
self.mutex.lock().unwrap().msg_read
}
/// For the Rust main thread to read a single pending command sent from the JVM main thread
pub fn read_cmd(&self) -> Option<AppCmd> {
self.inner.mutex.lock().unwrap().read_cmd()
}
/// For the Rust main thread to get an [`InputQueue`] that wraps the AInputQueue pointer
/// we have and at the same time ensure that the input queue is attached to the given looper.
///
/// NB: it's expected that the input queue is detached as soon as we know there is new
/// input (knowing the app will be notified) and only re-attached when the application
/// reads the input (to avoid lots of redundant wake ups)
pub fn looper_attached_input_queue(
&self,
looper: *mut ndk_sys::ALooper,
ident: libc::c_int,
) -> Option<InputQueue> {
let mut guard = self.mutex.lock().unwrap();
if guard.input_queue.is_null() {
return None;
}
unsafe {
// Reattach the input queue to the looper so future input will again deliver an
// `InputAvailable` event.
guard.attach_input_queue_to_looper(looper, ident);
Some(InputQueue::from_ptr(NonNull::new_unchecked(
guard.input_queue,
)))
}
}
pub fn detach_input_queue_from_looper(&self) {
unsafe {
self.inner
.mutex
.lock()
.unwrap()
.detach_input_queue_from_looper();
}
}
pub fn config(&self) -> ConfigurationRef {
self.mutex.lock().unwrap().config.clone()
}
pub fn content_rect(&self) -> Rect {
self.mutex.lock().unwrap().content_rect.into()
}
}
/// The status of the native thread that's created to run
/// `android_main`
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NativeThreadState {
/// The `android_main` thread hasn't been created yet
Init,
/// The `android_main` thread has been spawned and started running
Running,
/// The `android_main` thread has finished
Stopped,
}
#[derive(Debug)]
pub struct NativeActivityState {
pub msg_read: libc::c_int,
pub msg_write: libc::c_int,
pub config: ConfigurationRef,
pub saved_state: Vec<u8>,
pub input_queue: *mut ndk_sys::AInputQueue,
pub window: Option<NativeWindow>,
pub content_rect: ndk_sys::ARect,
pub activity_state: State,
pub destroy_requested: bool,
pub thread_state: NativeThreadState,
pub app_has_saved_state: bool,
/// Set as soon as the Java main thread notifies us of an
/// `onDestroyed` callback.
pub destroyed: bool,
pub redraw_needed: bool,
pub pending_input_queue: *mut ndk_sys::AInputQueue,
pub pending_window: Option<NativeWindow>,
}
impl NativeActivityState {
pub fn read_cmd(&mut self) -> Option<AppCmd> {
let mut cmd_i: i8 = 0;
loop {
match unsafe { libc::read(self.msg_read, &mut cmd_i as *mut _ as *mut _, 1) } {
1 => {
let cmd = AppCmd::try_from(cmd_i);
return match cmd {
Ok(cmd) => Some(cmd),
Err(_) => {
log::error!("Spurious, unknown NativeActivityGlue cmd: {}", cmd_i);
None
}
};
}
-1 => {
let err = std::io::Error::last_os_error();
if err.kind() != std::io::ErrorKind::Interrupted {
log::error!("Failure reading NativeActivityGlue cmd: {}", err);
return None;
}
}
count => {
log::error!(
"Spurious read of {count} bytes while reading NativeActivityGlue cmd"
);
return None;
}
}
}
}
fn write_cmd(&mut self, cmd: AppCmd) {
let cmd = cmd as i8;
loop {
match unsafe { libc::write(self.msg_write, &cmd as *const _ as *const _, 1) } {
1 => break,
-1 => {
let err = std::io::Error::last_os_error();
if err.kind() != std::io::ErrorKind::Interrupted {
log::error!("Failure writing NativeActivityGlue cmd: {}", err);
return;
}
}
count => {
log::error!(
"Spurious write of {count} bytes while writing NativeActivityGlue cmd"
);
return;
}
}
}
}
pub unsafe fn attach_input_queue_to_looper(
&mut self,
looper: *mut ndk_sys::ALooper,
ident: libc::c_int,
) {
if !self.input_queue.is_null() {
log::trace!("Attaching input queue to looper");
ndk_sys::AInputQueue_attachLooper(
self.input_queue,
looper,
ident,
None,
ptr::null_mut(),
);
}
}
pub unsafe fn detach_input_queue_from_looper(&mut self) {
if !self.input_queue.is_null() {
log::trace!("Detaching input queue from looper");
ndk_sys::AInputQueue_detachLooper(self.input_queue);
}
}
}
impl Drop for WaitableNativeActivityState {
fn drop(&mut self) {
log::debug!("WaitableNativeActivityState::drop!");
unsafe {
let mut guard = self.mutex.lock().unwrap();
guard.detach_input_queue_from_looper();
}
}
}
impl WaitableNativeActivityState {
///////////////////////////////
// Java-side callback handling
///////////////////////////////
pub fn new(
activity: *mut ndk_sys::ANativeActivity,
saved_state_in: *const libc::c_void,
saved_state_size: libc::size_t,
) -> Self {
let mut msgpipe: [libc::c_int; 2] = [-1, -1];
unsafe {
if libc::pipe(msgpipe.as_mut_ptr()) != 0 {
panic!(
"could not create Rust <-> Java IPC pipe: {}",
std::io::Error::last_os_error()
);
}
}
let saved_state =
unsafe { std::slice::from_raw_parts(saved_state_in as *const u8, saved_state_size) };
let config = unsafe {
let config = ndk_sys::AConfiguration_new();
ndk_sys::AConfiguration_fromAssetManager(config, (*activity).assetManager);
let config = super::ConfigurationRef::new(Configuration::from_ptr(
NonNull::new_unchecked(config),
));
log::trace!("Config: {:#?}", config);
config
};
Self {
activity,
mutex: Mutex::new(NativeActivityState {
msg_read: msgpipe[0],
msg_write: msgpipe[1],
config,
saved_state: saved_state.into(),
input_queue: ptr::null_mut(),
window: None,
content_rect: Rect::empty().into(),
activity_state: State::Init,
destroy_requested: false,
thread_state: NativeThreadState::Init,
app_has_saved_state: false,
destroyed: false,
redraw_needed: false,
pending_input_queue: ptr::null_mut(),
pending_window: None,
}),
cond: Condvar::new(),
}
}
pub fn notify_destroyed(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.destroyed = true;
unsafe {
guard.write_cmd(AppCmd::Destroy);
while guard.thread_state != NativeThreadState::Stopped {
guard = self.cond.wait(guard).unwrap();
}
libc::close(guard.msg_read);
guard.msg_read = -1;
libc::close(guard.msg_write);
guard.msg_write = -1;
}
}
pub fn notify_config_changed(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.write_cmd(AppCmd::ConfigChanged);
}
pub fn notify_low_memory(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.write_cmd(AppCmd::LowMemory);
}
pub fn notify_focus_changed(&self, focused: bool) {
let mut guard = self.mutex.lock().unwrap();
guard.write_cmd(if focused {
AppCmd::GainedFocus
} else {
AppCmd::LostFocus
});
}
pub fn notify_window_resized(&self, native_window: *mut ndk_sys::ANativeWindow) {
let mut guard = self.mutex.lock().unwrap();
// set_window always syncs .pending_window back to .window before returning. This callback
// from Android can never arrive at an interim state, and validates that Android:
// 1. Only provides resizes in between onNativeWindowCreated and onNativeWindowDestroyed;
// 2. Doesn't call it on a bogus window pointer that we don't know about.
debug_assert_eq!(guard.window.as_ref().unwrap().ptr().as_ptr(), native_window);
guard.write_cmd(AppCmd::WindowResized);
}
pub fn notify_window_redraw_needed(&self, native_window: *mut ndk_sys::ANativeWindow) {
let mut guard = self.mutex.lock().unwrap();
// set_window always syncs .pending_window back to .window before returning. This callback
// from Android can never arrive at an interim state, and validates that Android:
// 1. Only provides resizes in between onNativeWindowCreated and onNativeWindowDestroyed;
// 2. Doesn't call it on a bogus window pointer that we don't know about.
debug_assert_eq!(guard.window.as_ref().unwrap().ptr().as_ptr(), native_window);
guard.write_cmd(AppCmd::WindowRedrawNeeded);
}
unsafe fn set_input(&self, input_queue: *mut ndk_sys::AInputQueue) {
let mut guard = self.mutex.lock().unwrap();
// The pending_input_queue state should only be set while in this method, and since
// it doesn't allow re-entrance and is cleared before returning then we expect
// this to be null
debug_assert!(
guard.pending_input_queue.is_null(),
"InputQueue update clash"
);
guard.pending_input_queue = input_queue;
guard.write_cmd(AppCmd::InputQueueChanged);
while guard.input_queue != guard.pending_input_queue {
guard = self.cond.wait(guard).unwrap();
}
guard.pending_input_queue = ptr::null_mut();
}
unsafe fn set_window(&self, window: Option<NativeWindow>) {
let mut guard = self.mutex.lock().unwrap();
// The pending_window state should only be set while in this method, and since
// it doesn't allow re-entrance and is cleared before returning then we expect
// this to be None
debug_assert!(guard.pending_window.is_none(), "NativeWindow update clash");
if guard.window.is_some() {
guard.write_cmd(AppCmd::TermWindow);
}
guard.pending_window = window;
if guard.pending_window.is_some() {
guard.write_cmd(AppCmd::InitWindow);
}
while guard.window != guard.pending_window {
guard = self.cond.wait(guard).unwrap();
}
guard.pending_window = None;
}
unsafe fn set_content_rect(&self, rect: *const ndk_sys::ARect) {
let mut guard = self.mutex.lock().unwrap();
guard.content_rect = *rect;
guard.write_cmd(AppCmd::ContentRectChanged);
}
unsafe fn set_activity_state(&self, state: State) {
let mut guard = self.mutex.lock().unwrap();
let cmd = match state {
State::Init => panic!("Can't explicitly transition into 'init' state"),
State::Start => AppCmd::Start,
State::Resume => AppCmd::Resume,
State::Pause => AppCmd::Pause,
State::Stop => AppCmd::Stop,
};
guard.write_cmd(cmd);
while guard.activity_state != state {
guard = self.cond.wait(guard).unwrap();
}
}
fn request_save_state(&self) -> (*mut libc::c_void, libc::size_t) {
let mut guard = self.mutex.lock().unwrap();
// The state_saved flag should only be set while in this method, and since
// it doesn't allow re-entrance and is cleared before returning then we expect
// this to be None
debug_assert!(!guard.app_has_saved_state, "SaveState request clash");
guard.write_cmd(AppCmd::SaveState);
while !guard.app_has_saved_state {
guard = self.cond.wait(guard).unwrap();
}
guard.app_has_saved_state = false;
// `ANativeActivity` explicitly documents that it expects save state to be
// given via a `malloc()` allocated pointer since it will automatically
// `free()` the state after it has been converted to a buffer for the JVM.
if !guard.saved_state.is_empty() {
let saved_state_size = guard.saved_state.len();
let saved_state_src_ptr = guard.saved_state.as_ptr();
unsafe {
let saved_state = libc::malloc(saved_state_size);
assert!(
!saved_state.is_null(),
"Failed to allocate {} bytes for restoring saved application state",
saved_state_size
);
libc::memcpy(saved_state, saved_state_src_ptr as _, saved_state_size);
(saved_state, saved_state_size)
}
} else {
(ptr::null_mut(), 0)
}
}
pub fn saved_state(&self) -> Option<Vec<u8>> {
let guard = self.mutex.lock().unwrap();
if !guard.saved_state.is_empty() {
Some(guard.saved_state.clone())
} else {
None
}
}
pub fn set_saved_state(&self, state: &[u8]) {
let mut guard = self.mutex.lock().unwrap();
guard.saved_state.clear();
guard.saved_state.extend_from_slice(state);
}
////////////////////////////
// Rust-side event loop
////////////////////////////
pub fn notify_main_thread_running(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.thread_state = NativeThreadState::Running;
self.cond.notify_one();
}
pub fn notify_main_thread_stopped_running(&self) {
let mut guard = self.mutex.lock().unwrap();
guard.thread_state = NativeThreadState::Stopped;
self.cond.notify_one();
}
pub unsafe fn pre_exec_cmd(
&self,
cmd: AppCmd,
looper: *mut ndk_sys::ALooper,
input_queue_ident: libc::c_int,
) {
log::trace!("Pre: AppCmd::{:#?}", cmd);
match cmd {
AppCmd::InputQueueChanged => {
let mut guard = self.mutex.lock().unwrap();
guard.detach_input_queue_from_looper();
guard.input_queue = guard.pending_input_queue;
if !guard.input_queue.is_null() {
guard.attach_input_queue_to_looper(looper, input_queue_ident);
}
self.cond.notify_one();
}
AppCmd::InitWindow => {
let mut guard = self.mutex.lock().unwrap();
guard.window = guard.pending_window.clone();
self.cond.notify_one();
}
AppCmd::Resume | AppCmd::Start | AppCmd::Pause | AppCmd::Stop => {
let mut guard = self.mutex.lock().unwrap();
guard.activity_state = match cmd {
AppCmd::Start => State::Start,
AppCmd::Pause => State::Pause,
AppCmd::Resume => State::Resume,
AppCmd::Stop => State::Stop,
_ => unreachable!(),
};
self.cond.notify_one();
}
AppCmd::ConfigChanged => {
let guard = self.mutex.lock().unwrap();
let config = ndk_sys::AConfiguration_new();
ndk_sys::AConfiguration_fromAssetManager(config, (*self.activity).assetManager);
let config = Configuration::from_ptr(NonNull::new_unchecked(config));
guard.config.replace(config);
log::debug!("Config: {:#?}", guard.config);
}
AppCmd::Destroy => {
let mut guard = self.mutex.lock().unwrap();
guard.destroy_requested = true;
}
_ => {}
}
}
pub unsafe fn post_exec_cmd(&self, cmd: AppCmd) {
log::trace!("Post: AppCmd::{:#?}", cmd);
match cmd {
AppCmd::TermWindow => {
let mut guard = self.mutex.lock().unwrap();
guard.window = None;
self.cond.notify_one();
}
AppCmd::SaveState => {
let mut guard = self.mutex.lock().unwrap();
guard.app_has_saved_state = true;
self.cond.notify_one();
}
_ => {}
}
}
}
extern "Rust" {
pub fn android_main(app: AndroidApp);
}
unsafe fn try_with_waitable_activity_ref(
activity: *mut ndk_sys::ANativeActivity,
closure: impl FnOnce(Arc<WaitableNativeActivityState>),
) {
assert!(!(*activity).instance.is_null());
let weak_ptr: *const WaitableNativeActivityState = (*activity).instance.cast();
let weak_ref = Weak::from_raw(weak_ptr);
if let Some(waitable_activity) = weak_ref.upgrade() {
closure(waitable_activity);
} else {
log::error!("Ignoring spurious JVM callback after last activity reference was dropped!")
}
let _ = weak_ref.into_raw();
}
unsafe extern "C" fn on_destroy(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("Destroy: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.notify_destroyed()
});
})
}
unsafe extern "C" fn on_start(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("Start: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_activity_state(State::Start);
});
})
}
unsafe extern "C" fn on_resume(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("Resume: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_activity_state(State::Resume);
});
})
}
unsafe extern "C" fn on_save_instance_state(
activity: *mut ndk_sys::ANativeActivity,
out_len: *mut usize,
) -> *mut libc::c_void {
abort_on_panic(|| {
log::debug!("SaveInstanceState: {:p}\n", activity);
*out_len = 0;
let mut ret = ptr::null_mut();
try_with_waitable_activity_ref(activity, |waitable_activity| {
let (state, len) = waitable_activity.request_save_state();
*out_len = len;
ret = state
});
log::debug!("Saved state = {:p}, len = {}", ret, *out_len);
ret
})
}
unsafe extern "C" fn on_pause(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("Pause: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_activity_state(State::Pause);
});
})
}
unsafe extern "C" fn on_stop(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("Stop: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_activity_state(State::Stop);
});
})
}
unsafe extern "C" fn on_configuration_changed(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("ConfigurationChanged: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.notify_config_changed();
});
})
}
unsafe extern "C" fn on_low_memory(activity: *mut ndk_sys::ANativeActivity) {
abort_on_panic(|| {
log::debug!("LowMemory: {:p}\n", activity);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.notify_low_memory();
});
})
}
unsafe extern "C" fn on_window_focus_changed(
activity: *mut ndk_sys::ANativeActivity,
focused: libc::c_int,
) {
abort_on_panic(|| {
log::debug!("WindowFocusChanged: {:p} -- {}\n", activity, focused);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.notify_focus_changed(focused != 0);
});
})
}
unsafe extern "C" fn on_native_window_created(
activity: *mut ndk_sys::ANativeActivity,
window: *mut ndk_sys::ANativeWindow,
) {
abort_on_panic(|| {
log::debug!("NativeWindowCreated: {:p} -- {:p}\n", activity, window);
try_with_waitable_activity_ref(activity, |waitable_activity| {
// Use clone_from_ptr to acquire additional ownership on the NativeWindow,
// which will unconditionally be _release()'d on Drop.
let window = NativeWindow::clone_from_ptr(NonNull::new_unchecked(window));
waitable_activity.set_window(Some(window));
});
})
}
unsafe extern "C" fn on_native_window_resized(
activity: *mut ndk_sys::ANativeActivity,
window: *mut ndk_sys::ANativeWindow,
) {
log::debug!("NativeWindowResized: {:p} -- {:p}\n", activity, window);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.notify_window_resized(window);
});
}
unsafe extern "C" fn on_native_window_redraw_needed(
activity: *mut ndk_sys::ANativeActivity,
window: *mut ndk_sys::ANativeWindow,
) {
log::debug!("NativeWindowRedrawNeeded: {:p} -- {:p}\n", activity, window);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.notify_window_redraw_needed(window)
});
}
unsafe extern "C" fn on_native_window_destroyed(
activity: *mut ndk_sys::ANativeActivity,
window: *mut ndk_sys::ANativeWindow,
) {
abort_on_panic(|| {
log::debug!("NativeWindowDestroyed: {:p} -- {:p}\n", activity, window);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_window(None);
});
})
}
unsafe extern "C" fn on_input_queue_created(
activity: *mut ndk_sys::ANativeActivity,
queue: *mut ndk_sys::AInputQueue,
) {
abort_on_panic(|| {
log::debug!("InputQueueCreated: {:p} -- {:p}\n", activity, queue);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_input(queue);
});
})
}
unsafe extern "C" fn on_input_queue_destroyed(
activity: *mut ndk_sys::ANativeActivity,
queue: *mut ndk_sys::AInputQueue,
) {
abort_on_panic(|| {
log::debug!("InputQueueDestroyed: {:p} -- {:p}\n", activity, queue);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_input(ptr::null_mut());
});
})
}
unsafe extern "C" fn on_content_rect_changed(
activity: *mut ndk_sys::ANativeActivity,
rect: *const ndk_sys::ARect,
) {
log::debug!("ContentRectChanged: {:p} -- {:p}\n", activity, rect);
try_with_waitable_activity_ref(activity, |waitable_activity| {
waitable_activity.set_content_rect(rect)
});
}
/// This is the native entrypoint for our cdylib library that `ANativeActivity` will look for via `dlsym`
#[no_mangle]
extern "C" fn ANativeActivity_onCreate(
activity: *mut ndk_sys::ANativeActivity,
saved_state: *const libc::c_void,
saved_state_size: libc::size_t,
) {
abort_on_panic(|| {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
let file = unsafe {
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe2(logpipe.as_mut_ptr(), libc::O_CLOEXEC);
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
libc::close(logpipe[1]);
File::from_raw_fd(logpipe[0])
};
std::thread::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 = {}",
activity,
saved_state,
saved_state_size
);
// Conceptually we associate a glue reference with the JVM main thread, and another
// reference with the Rust main thread
let jvm_glue = NativeActivityGlue::new(activity, saved_state, saved_state_size);
let rust_glue = jvm_glue.clone();
// Let us Send the NativeActivity pointer to the Rust main() thread without a wrapper type
let activity_ptr: libc::intptr_t = activity as _;
// Note: we drop the thread handle which will detach the thread
std::thread::spawn(move || {
let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _;
let jvm = abort_on_panic(|| unsafe {
let na = activity;
let jvm: *mut jni_sys::JavaVM = (*na).vm;
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
let jvm = CloneJavaVM::from_raw(jvm).unwrap();
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
jvm.attach_current_thread_permanently().unwrap();
jvm
});
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
rust_glue.notify_main_thread_running();
unsafe {
// We want to specifically catch any panic from the application's android_main
// so we can finish + destroy the Activity gracefully via the JVM
catch_unwind(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(log_panic);
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ndk_sys::ANativeActivity_finish(activity);
// This should detach automatically but lets detach explicitly to avoid depending
// on the TLS trickery in `jni-rs`
jvm.detach_current_thread();
ndk_context::release_android_context();
}
rust_glue.notify_main_thread_stopped_running();
});
// Wait for thread to start.
let mut guard = jvm_glue.mutex.lock().unwrap();
// Don't specifically wait for `Running` just in case `android_main` returns
// immediately and the state is set to `Stopped`
while guard.thread_state == NativeThreadState::Init {
guard = jvm_glue.cond.wait(guard).unwrap();
}
})
}
@@ -0,0 +1,440 @@
use std::marker::PhantomData;
use crate::input::{
Axis, Button, ButtonState, EdgeFlags, KeyAction, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter, Source, ToolType,
};
/// A motion event
///
/// For general discussion of motion events in Android, see [the relevant
/// javadoc](https://developer.android.com/reference/android/view/MotionEvent).
#[derive(Debug)]
#[repr(transparent)]
pub struct MotionEvent<'a> {
ndk_event: ndk::event::MotionEvent,
_lifetime: PhantomData<&'a ndk::event::MotionEvent>,
}
impl<'a> MotionEvent<'a> {
pub(crate) fn new(ndk_event: ndk::event::MotionEvent) -> Self {
Self {
ndk_event,
_lifetime: PhantomData,
}
}
pub(crate) fn into_ndk_event(self) -> ndk::event::MotionEvent {
self.ndk_event
}
/// Get the source of the event.
///
#[inline]
pub fn source(&self) -> Source {
// XXX: we use `AInputEvent_getSource` directly (instead of calling
// ndk_event.source()) since we have our own `Source` enum that we
// share between backends, which may also capture unknown variants
// added in new versions of Android.
let source =
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
source.into()
}
/// Get the device id associated with the event.
///
#[inline]
pub fn device_id(&self) -> i32 {
self.ndk_event.device_id()
}
/// Returns the motion action associated with the event.
///
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionMasked())
#[inline]
pub fn action(&self) -> MotionAction {
// XXX: we use `AMotionEvent_getAction` directly since we have our own
// `MotionAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let action =
unsafe { ndk_sys::AMotionEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 };
action.into()
}
/// Returns which button has been modified during a press or release action.
///
/// For actions other than [`MotionAction::ButtonPress`] and
/// [`MotionAction::ButtonRelease`] the returned value is undefined.
///
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getActionButton())
#[inline]
pub fn action_button(&self) -> Button {
let action_button =
unsafe { ndk_sys::AMotionEvent_getActionButton(self.ndk_event.ptr().as_ptr()) as u32 };
action_button.into()
}
/// Returns the pointer index of an `Up` or `Down` event.
///
/// Pointer indices can change per motion event. For an identifier that stays the same, see
/// [`Pointer::pointer_id()`].
///
/// This only has a meaning when the [action](Self::action) is one of [`Up`](MotionAction::Up),
/// [`Down`](MotionAction::Down), [`PointerUp`](MotionAction::PointerUp),
/// or [`PointerDown`](MotionAction::PointerDown).
#[inline]
pub fn pointer_index(&self) -> usize {
self.ndk_event.pointer_index()
}
/*
/// Returns the pointer id associated with the given pointer index.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getpointerid)
// TODO: look at output with out-of-range pointer index
// Probably -1 though
pub fn pointer_id_for(&self, pointer_index: usize) -> i32 {
unsafe { ndk_sys::AMotionEvent_getPointerId(self.ndk_event.ptr.as_ptr(), pointer_index) }
}
*/
/// Returns the number of pointers in this event
///
/// See [the MotionEvent docs](https://developer.android.com/reference/android/view/MotionEvent#getPointerCount())
#[inline]
pub fn pointer_count(&self) -> usize {
self.ndk_event.pointer_count()
}
/// An iterator over the pointers in this motion event
#[inline]
pub fn pointers(&self) -> PointersIter<'_> {
PointersIter {
inner: PointersIterImpl {
ndk_pointers_iter: self.ndk_event.pointers(),
},
}
}
/// The pointer at a given pointer index. Panics if the pointer index is out of bounds.
///
/// If you need to loop over all the pointers, prefer the [`pointers()`](Self::pointers) method.
#[inline]
pub fn pointer_at_index(&self, index: usize) -> Pointer<'_> {
Pointer {
inner: PointerImpl {
ndk_pointer: self.ndk_event.pointer_at_index(index),
},
}
}
/*
XXX: Not currently supported with GameActivity so we don't currently expose for NativeActivity
either, for consistency.
/// Returns the size of the history contained in this event.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_gethistorysize)
#[inline]
pub fn history_size(&self) -> usize {
self.ndk_event.history_size()
}
/// An iterator over the historical events contained in this event.
#[inline]
pub fn history(&self) -> HistoricalMotionEventsIter<'_> {
self.ndk_event.history()
}
*/
/// Returns the state of any modifier keys that were pressed during the event.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getmetastate)
#[inline]
pub fn meta_state(&self) -> MetaState {
self.ndk_event.meta_state().into()
}
/// Returns the button state during this event, as a bitfield.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getbuttonstate)
#[inline]
pub fn button_state(&self) -> ButtonState {
self.ndk_event.button_state().into()
}
/// Returns the time of the start of this gesture, in the `java.lang.System.nanoTime()` time
/// base
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getdowntime)
#[inline]
pub fn down_time(&self) -> i64 {
self.ndk_event.down_time()
}
/// Returns a bitfield indicating which edges were touched by this event.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getedgeflags)
#[inline]
pub fn edge_flags(&self) -> EdgeFlags {
self.ndk_event.edge_flags().into()
}
/// Returns the time of this event, in the `java.lang.System.nanoTime()` time base
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_geteventtime)
#[inline]
pub fn event_time(&self) -> i64 {
self.ndk_event.event_time()
}
/// The flags associated with a motion event.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getflags)
#[inline]
pub fn flags(&self) -> MotionEventFlags {
self.ndk_event.flags().into()
}
/* Missing from GameActivity currently...
/// Returns the offset in the x direction between the coordinates and the raw coordinates
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getxoffset)
#[inline]
pub fn x_offset(&self) -> f32 {
self.ndk_event.x_offset()
}
/// Returns the offset in the y direction between the coordinates and the raw coordinates
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getyoffset)
#[inline]
pub fn y_offset(&self) -> f32 {
self.ndk_event.y_offset()
}
*/
/// Returns the precision of the x value of the coordinates
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getxprecision)
#[inline]
pub fn x_precision(&self) -> f32 {
self.ndk_event.x_precision()
}
/// Returns the precision of the y value of the coordinates
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#amotionevent_getyprecision)
#[inline]
pub fn y_precision(&self) -> f32 {
self.ndk_event.y_precision()
}
}
/// A view into the data of a specific pointer in a motion event.
#[derive(Debug)]
pub(crate) struct PointerImpl<'a> {
ndk_pointer: ndk::event::Pointer<'a>,
}
impl<'a> PointerImpl<'a> {
#[inline]
pub fn pointer_index(&self) -> usize {
self.ndk_pointer.pointer_index()
}
#[inline]
pub fn pointer_id(&self) -> i32 {
self.ndk_pointer.pointer_id()
}
#[inline]
pub fn axis_value(&self, axis: Axis) -> f32 {
let value: u32 = axis.into();
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
/// javadoc](https://developer.android.com/reference/android/view/KeyEvent).
#[derive(Debug)]
#[repr(transparent)]
pub struct KeyEvent<'a> {
ndk_event: ndk::event::KeyEvent,
_lifetime: PhantomData<&'a ndk::event::KeyEvent>,
}
impl<'a> KeyEvent<'a> {
pub(crate) fn new(ndk_event: ndk::event::KeyEvent) -> Self {
Self {
ndk_event,
_lifetime: PhantomData,
}
}
pub(crate) fn into_ndk_event(self) -> ndk::event::KeyEvent {
self.ndk_event
}
/// Get the source of the event.
///
#[inline]
pub fn source(&self) -> Source {
// XXX: we use `AInputEvent_getSource` directly (instead of calling
// ndk_event.source()) since we have our own `Source` enum that we
// share between backends, which may also capture unknown variants
// added in new versions of Android.
let source =
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
source.into()
}
/// Get the device id associated with the event.
///
#[inline]
pub fn device_id(&self) -> i32 {
self.ndk_event.device_id()
}
/// Returns the key action associated with the event.
///
/// See [the KeyEvent docs](https://developer.android.com/reference/android/view/KeyEvent#getAction())
#[inline]
pub fn action(&self) -> KeyAction {
// XXX: we use `AInputEvent_getAction` directly since we have our own
// `KeyAction` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let action = unsafe { ndk_sys::AKeyEvent_getAction(self.ndk_event.ptr().as_ptr()) as u32 };
action.into()
}
/// Returns the last time the key was pressed. This is on the scale of
/// `java.lang.System.nanoTime()`, which has nanosecond precision, but no defined start time.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getdowntime)
#[inline]
pub fn down_time(&self) -> i64 {
self.ndk_event.down_time()
}
/// Returns the time this event occured. This is on the scale of
/// `java.lang.System.nanoTime()`, which has nanosecond precision, but no defined start time.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_geteventtime)
#[inline]
pub fn event_time(&self) -> i64 {
self.ndk_event.event_time()
}
/// Returns the keycode associated with this key event
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getkeycode)
#[inline]
pub fn key_code(&self) -> Keycode {
// XXX: we use `AInputEvent_getKeyCode` directly since we have our own
// `Keycode` enum that we share between backends, which may also
// capture unknown variants added in new versions of Android.
let keycode =
unsafe { ndk_sys::AKeyEvent_getKeyCode(self.ndk_event.ptr().as_ptr()) as u32 };
keycode.into()
}
/// Returns the number of repeats of a key.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getrepeatcount)
#[inline]
pub fn repeat_count(&self) -> i32 {
self.ndk_event.repeat_count()
}
/// Returns the hardware keycode of a key. This varies from device to device.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getscancode)
#[inline]
pub fn scan_code(&self) -> i32 {
self.ndk_event.scan_code()
}
/// Returns the state of the modifiers during this key event, represented by a bitmask.
///
/// See [the NDK
/// docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getmetastate)
#[inline]
pub fn meta_state(&self) -> MetaState {
self.ndk_event.meta_state().into()
}
}
// We use our own wrapper type for input events to have better consistency
// with GameActivity and ensure the enum can be extended without needing a
// semver bump
/// Enum of possible input events
#[derive(Debug)]
#[non_exhaustive]
pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
}
+317 -348
View File
@@ -1,319 +1,300 @@
#![cfg(any(feature = "native-activity", doc))]
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::ops::Deref;
use std::os::raw;
use std::os::unix::prelude::*;
use std::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 std::{ptr, thread};
use log::{error, info, trace, Level};
use ndk_sys::ALooper_wake;
use ndk_sys::{ALooper, ALooper_pollAll};
use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
use libc::c_void;
use log::{error, trace};
use ndk::input_queue::InputQueue;
use ndk::native_window::NativeWindow;
use ndk::{asset::AssetManager, native_window::NativeWindow};
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
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,
};
mod ffi;
pub mod input;
pub mod input {
pub use ndk::event::{
Axis, ButtonState, EdgeFlags, KeyAction, KeyEvent, KeyEventFlags, Keycode, MetaState,
MotionAction, MotionEvent, MotionEventFlags, Pointer, Source,
};
mod glue;
use self::glue::NativeActivityGlue;
// We use our own wrapper type for input events to have better consistency
// with GameActivity and ensure the enum can be extended without needing a
// semver bump
#[derive(Debug)]
#[non_exhaustive]
pub enum InputEvent {
MotionEvent(self::MotionEvent),
KeyEvent(self::KeyEvent),
}
}
pub const LOOPER_ID_MAIN: libc::c_int = 1;
pub const LOOPER_ID_INPUT: libc::c_int = 2;
//pub const LOOPER_ID_USER: ::std::os::raw::c_uint = 3;
// The only time it's safe to update the android_app->savedState pointer is
// while handling a SaveState event, so this API is only exposed for those
// events...
/// An interface for saving application state during [MainEvent::SaveState] events
///
/// This interface is only available temporarily while handling a [MainEvent::SaveState] event.
#[derive(Debug)]
pub struct StateSaver<'a> {
app: &'a AndroidAppInner,
}
impl<'a> StateSaver<'a> {
/// Stores the given `state` such that it will be available to load the next
/// time that the application resumes.
pub fn store(&self, state: &'a [u8]) {
// android_native_app_glue specifically expects savedState to have been allocated
// via libc::malloc since it will automatically handle freeing the data once it
// has been handed over to the Java Activity / main thread.
unsafe {
let app_ptr = self.app.native_app.as_ptr();
// In case the application calls store() multiple times for some reason we
// make sure to free any pre-existing state...
if (*app_ptr).savedState != ptr::null_mut() {
libc::free((*app_ptr).savedState);
(*app_ptr).savedState = ptr::null_mut();
(*app_ptr).savedStateSize = 0;
}
let buf = libc::malloc(state.len());
if buf == ptr::null_mut() {
panic!("Failed to allocate save_state buffer");
}
// Since it's a byte array there's no special alignment requirement here.
//
// Since we re-define `buf` we ensure it's not possible to access the buffer
// via its original pointer for the lifetime of the slice.
{
let buf: &mut [u8] = std::slice::from_raw_parts_mut(buf.cast(), state.len());
buf.copy_from_slice(state);
}
(*app_ptr).savedState = buf;
(*app_ptr).savedStateSize = state.len() as _;
}
self.app.native_activity.set_saved_state(state);
}
}
/// An interface for loading application state during [MainEvent::Resume] events
///
/// This interface is only available temporarily while handling a [MainEvent::Resume] event.
#[derive(Debug)]
pub struct StateLoader<'a> {
app: &'a AndroidAppInner,
}
impl<'a> StateLoader<'a> {
/// Returns whatever state was saved during the last [MainEvent::SaveState] event or `None`
pub fn load(&self) -> Option<Vec<u8>> {
unsafe {
let app_ptr = self.app.native_app.as_ptr();
if (*app_ptr).savedState != ptr::null_mut() && (*app_ptr).savedStateSize > 0 {
let buf: &mut [u8] = std::slice::from_raw_parts_mut(
(*app_ptr).savedState.cast(),
(*app_ptr).savedStateSize as usize,
);
let state = buf.to_vec();
Some(state)
} else {
None
}
}
self.app.native_activity.saved_state()
}
}
/// A means to wake up the main thread while it is blocked waiting for I/O
#[derive(Clone)]
pub struct AndroidAppWaker {
// The looper pointer is owned by the android_app and effectively
// has a 'static lifetime, and the ALooper_wake C API is thread
// safe, so this can be cloned safely and is send + sync safe
looper: NonNull<ALooper>,
looper: NonNull<ndk_sys::ALooper>,
}
unsafe impl Send for AndroidAppWaker {}
unsafe impl Sync for AndroidAppWaker {}
impl AndroidAppWaker {
/// Interrupts the main thread if it is blocked within [`AndroidApp::poll_events()`]
///
/// If [`AndroidApp::poll_events()`] is interrupted it will invoke the poll
/// callback with a [PollEvent::Wake][wake_event] event.
///
/// [wake_event]: crate::PollEvent::Wake
pub fn wake(&self) {
unsafe {
ALooper_wake(self.looper.as_ptr());
ndk_sys::ALooper_wake(self.looper.as_ptr());
}
}
}
impl AndroidApp {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>) -> AndroidApp {
// Note: we don't use from_ptr since we don't own the android_app.config
// and need to keep in mind that the Drop handler is going to call
// AConfiguration_delete()
let config = Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config));
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
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 {
native_app: NativeAppGlue { ptr },
config: ConfigurationRef::new(config),
native_window: Default::default(),
jvm,
native_activity,
looper: Looper {
ptr: ptr::null_mut(),
},
key_map_binding: Arc::new(key_map_binding),
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
};
{
let mut guard = app.inner.write().unwrap();
let main_fd = guard.native_activity.cmd_read_fd();
unsafe {
guard.looper.ptr = ndk_sys::ALooper_prepare(
ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int,
);
ndk_sys::ALooper_addFd(
guard.looper.ptr,
main_fd,
LOOPER_ID_MAIN,
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
None,
//&mut guard.cmd_poll_source as *mut _ as *mut _);
ptr::null_mut(),
);
}
}
app
}
}
#[derive(Debug)]
struct NativeAppGlue {
ptr: NonNull<ffi::android_app>,
struct Looper {
pub ptr: *mut ndk_sys::ALooper,
}
impl Deref for NativeAppGlue {
type Target = NonNull<ffi::android_app>;
fn deref(&self) -> &Self::Target {
&self.ptr
}
}
unsafe impl Send for NativeAppGlue {}
unsafe impl Sync for NativeAppGlue {}
unsafe impl Send for Looper {}
unsafe impl Sync for Looper {}
#[derive(Debug)]
pub(crate) struct AndroidAppInner {
native_app: NativeAppGlue,
config: ConfigurationRef,
native_window: RwLock<Option<NativeWindow>>,
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 {
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
unsafe {
let app_ptr = self.native_app.as_ptr();
(*app_ptr).activity.cast()
}
pub(crate) fn vm_as_ptr(&self) -> *mut c_void {
unsafe { (*self.native_activity.activity).vm as _ }
}
pub fn native_window<'a>(&self) -> Option<NativeWindow> {
self.native_window.read().unwrap().clone()
pub(crate) fn activity_as_ptr(&self) -> *mut c_void {
// "clazz" is a completely bogus name; this is the _instance_ not class pointer
unsafe { (*self.native_activity.activity).clazz as _ }
}
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
self.native_activity.activity
}
pub(crate) fn looper(&self) -> *mut ndk_sys::ALooper {
self.looper.ptr
}
pub fn native_window(&self) -> Option<NativeWindow> {
self.native_activity.mutex.lock().unwrap().window.clone()
}
pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
where
F: FnMut(PollEvent),
F: FnMut(PollEvent<'_>),
{
trace!("poll_events");
unsafe {
let native_app = &self.native_app;
let mut fd: i32 = 0;
let mut events: i32 = 0;
let mut source: *mut 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
} else {
-1
};
info!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
let id = ALooper_pollAll(
trace!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
assert!(
!ndk_sys::ALooper_forThread().is_null(),
"Application tried to poll events from non-main thread"
);
let id = ndk_sys::ALooper_pollAll(
timeout_milliseconds,
&mut fd,
&mut events,
&mut source as *mut *mut core::ffi::c_void,
&mut source as *mut *mut c_void,
);
info!("pollAll id = {id}");
trace!("pollAll id = {id}");
match id {
ffi::ALOOPER_POLL_WAKE => {
ndk_sys::ALOOPER_POLL_WAKE => {
trace!("ALooper_pollAll returned POLL_WAKE");
callback(PollEvent::Wake);
}
ffi::ALOOPER_POLL_CALLBACK => {
ndk_sys::ALOOPER_POLL_CALLBACK => {
// ALooper_pollAll is documented to handle all callback sources internally so it should
// never return a _CALLBACK source id...
error!("Spurious ALOOPER_POLL_CALLBACK from ALopper_pollAll() (ignored)");
}
ffi::ALOOPER_POLL_TIMEOUT => {
ndk_sys::ALOOPER_POLL_TIMEOUT => {
trace!("ALooper_pollAll returned POLL_TIMEOUT");
callback(PollEvent::Timeout);
}
ffi::ALOOPER_POLL_ERROR => {
ndk_sys::ALOOPER_POLL_ERROR => {
// If we have an IO error with our pipe to the main Java thread that's surely
// not something we can recover from
panic!("ALooper_pollAll returned POLL_ERROR");
}
id if id >= 0 => {
match id as u32 {
ffi::LOOPER_ID_MAIN => {
match id {
LOOPER_ID_MAIN => {
trace!("ALooper_pollAll returned ID_MAIN");
let source: *mut ffi::android_poll_source = source.cast();
if source != ptr::null_mut() {
let cmd_i = ffi::android_app_read_cmd(native_app.as_ptr());
let cmd = match cmd_i as u32 {
if let Some(ipc_cmd) = self.native_activity.read_cmd() {
let main_cmd = match ipc_cmd {
// We don't forward info about the AInputQueue to apps since it's
// an implementation details that's also not compatible with
// GameActivity
ffi::APP_CMD_INPUT_CHANGED => None,
glue::AppCmd::InputQueueChanged => None,
ffi::APP_CMD_INIT_WINDOW => Some(MainEvent::InitWindow {}),
ffi::APP_CMD_TERM_WINDOW => Some(MainEvent::TerminateWindow {}),
ffi::APP_CMD_WINDOW_RESIZED => {
glue::AppCmd::InitWindow => Some(MainEvent::InitWindow {}),
glue::AppCmd::TermWindow => Some(MainEvent::TerminateWindow {}),
glue::AppCmd::WindowResized => {
Some(MainEvent::WindowResized {})
}
ffi::APP_CMD_WINDOW_REDRAW_NEEDED => {
glue::AppCmd::WindowRedrawNeeded => {
Some(MainEvent::RedrawNeeded {})
}
ffi::APP_CMD_CONTENT_RECT_CHANGED => {
glue::AppCmd::ContentRectChanged => {
Some(MainEvent::ContentRectChanged {})
}
ffi::APP_CMD_GAINED_FOCUS => Some(MainEvent::GainedFocus),
ffi::APP_CMD_LOST_FOCUS => Some(MainEvent::LostFocus),
ffi::APP_CMD_CONFIG_CHANGED => {
glue::AppCmd::GainedFocus => Some(MainEvent::GainedFocus),
glue::AppCmd::LostFocus => Some(MainEvent::LostFocus),
glue::AppCmd::ConfigChanged => {
Some(MainEvent::ConfigChanged {})
}
ffi::APP_CMD_LOW_MEMORY => Some(MainEvent::LowMemory),
ffi::APP_CMD_START => Some(MainEvent::Start),
ffi::APP_CMD_RESUME => Some(MainEvent::Resume {
loader: StateLoader { app: &self },
glue::AppCmd::LowMemory => Some(MainEvent::LowMemory),
glue::AppCmd::Start => Some(MainEvent::Start),
glue::AppCmd::Resume => Some(MainEvent::Resume {
loader: StateLoader { app: self },
}),
ffi::APP_CMD_SAVE_STATE => Some(MainEvent::SaveState {
saver: StateSaver { app: &self },
glue::AppCmd::SaveState => Some(MainEvent::SaveState {
saver: StateSaver { app: self },
}),
ffi::APP_CMD_PAUSE => Some(MainEvent::Pause),
ffi::APP_CMD_STOP => Some(MainEvent::Stop),
ffi::APP_CMD_DESTROY => Some(MainEvent::Destroy),
//ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_INSETS_CHANGED => MainEvent::InsetsChanged {},
_ => unreachable!(),
glue::AppCmd::Pause => Some(MainEvent::Pause),
glue::AppCmd::Stop => Some(MainEvent::Stop),
glue::AppCmd::Destroy => Some(MainEvent::Destroy),
};
trace!("Calling android_app_pre_exec_cmd({cmd_i})");
ffi::android_app_pre_exec_cmd(native_app.as_ptr(), cmd_i);
trace!("Calling pre_exec_cmd({ipc_cmd:#?})");
self.native_activity.pre_exec_cmd(
ipc_cmd,
self.looper(),
LOOPER_ID_INPUT,
);
if let Some(cmd) = cmd {
trace!("Read ID_MAIN command {cmd_i} = {cmd:?}");
match cmd {
MainEvent::ConfigChanged { .. } => {
self.config.replace(Configuration::clone_from_ptr(
NonNull::new_unchecked(
(*native_app.as_ptr()).config,
),
));
}
MainEvent::InitWindow { .. } => {
let win_ptr = (*native_app.as_ptr()).window;
// It's important that we use ::clone_from_ptr() here
// because NativeWindow has a Drop implementation that
// will unconditionally _release() the native window
*self.native_window.write().unwrap() =
Some(NativeWindow::clone_from_ptr(
NonNull::new(win_ptr).unwrap(),
));
}
MainEvent::TerminateWindow { .. } => {
*self.native_window.write().unwrap() = None;
}
_ => {}
}
trace!("Invoking callback for ID_MAIN command = {:?}", cmd);
callback(PollEvent::Main(cmd));
if let Some(main_cmd) = main_cmd {
trace!("Invoking callback for ID_MAIN command = {main_cmd:?}");
callback(PollEvent::Main(main_cmd));
}
trace!("Calling android_app_post_exec_cmd({cmd_i})");
ffi::android_app_post_exec_cmd(native_app.as_ptr(), cmd_i);
} else {
panic!("ALooper_pollAll returned ID_MAIN event with NULL android_poll_source!");
trace!("Calling post_exec_cmd({ipc_cmd:#?})");
self.native_activity.post_exec_cmd(ipc_cmd);
}
}
ffi::LOOPER_ID_INPUT => {
LOOPER_ID_INPUT => {
trace!("ALooper_pollAll returned ID_INPUT");
// To avoid spamming the application with event loop iterations notifying them of
// input events then we only send one `InputAvailable` per iteration of input
// handling. We re-attach the looper when the application calls
// `AndroidApp::input_events()`
ffi::android_app_detach_input_queue_looper(native_app.as_ptr());
self.native_activity.detach_input_queue_from_looper();
callback(PollEvent::Main(MainEvent::InputAvailable))
}
_ => {
@@ -330,35 +311,26 @@ impl AndroidAppInner {
pub fn create_waker(&self) -> AndroidAppWaker {
unsafe {
// From the application's pov we assume the app_ptr and looper pointer
// have static lifetimes and we can safely assume they are never NULL.
let app_ptr = self.native_app.as_ptr();
// From the application's pov we assume the looper pointer has a static
// lifetimes and we can safely assume it is never NULL.
AndroidAppWaker {
looper: NonNull::new_unchecked((*app_ptr).looper),
looper: NonNull::new_unchecked(self.looper.ptr),
}
}
}
pub fn config(&self) -> ConfigurationRef {
self.config.clone()
self.native_activity.config()
}
pub fn content_rect(&self) -> Rect {
unsafe {
let app_ptr = self.native_app.as_ptr();
Rect {
left: (*app_ptr).contentRect.left,
right: (*app_ptr).contentRect.right,
top: (*app_ptr).contentRect.top,
bottom: (*app_ptr).contentRect.bottom,
}
}
self.native_activity.content_rect()
}
pub fn asset_manager(&self) -> AssetManager {
unsafe {
let app_ptr = self.native_app.as_ptr();
let am_ptr = NonNull::new_unchecked((*(*app_ptr).activity).assetManager);
let activity_ptr = self.native_activity.activity;
let am_ptr = NonNull::new_unchecked((*activity_ptr).assetManager);
AssetManager::from_ptr(am_ptr)
}
}
@@ -371,7 +343,7 @@ impl AndroidAppInner {
let na = self.native_activity();
let na_mut = na as *mut ndk_sys::ANativeActivity;
unsafe {
ffi::ANativeActivity_setWindowFlags(
ndk_sys::ANativeActivity_setWindowFlags(
na_mut.cast(),
add_flags.bits(),
remove_flags.bits(),
@@ -384,11 +356,11 @@ impl AndroidAppInner {
let na = self.native_activity();
unsafe {
let flags = if show_implicit {
ffi::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT
ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT
} else {
0
};
ffi::ANativeActivity_showSoftInput(na as *mut _, flags);
ndk_sys::ANativeActivity_showSoftInput(na as *mut _, flags);
}
}
@@ -397,69 +369,79 @@ impl AndroidAppInner {
let na = self.native_activity();
unsafe {
let flags = if hide_implicit_only {
ffi::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY
ndk_sys::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY
} else {
0
};
ffi::ANativeActivity_hideSoftInput(na as *mut _, flags);
ndk_sys::ANativeActivity_hideSoftInput(na as *mut _, flags);
}
}
pub fn enable_motion_axis(&self, _axis: input::Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
TextInputState {
text: String::new(),
selection: TextSpan { start: 0, end: 0 },
compose_region: None,
}
}
pub fn disable_motion_axis(&self, _axis: input::Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
// TODO: move into a trait
pub fn set_text_input_state(&self, _state: TextInputState) {
// NOP: Unsupported
}
pub fn input_events<'b, F>(&self, mut callback: F)
where
F: FnMut(&input::InputEvent),
{
let queue = unsafe {
let app_ptr = self.native_app.as_ptr();
if (*app_ptr).inputQueue == ptr::null_mut() {
return;
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
}
// Reattach the input queue to the looper so future input will again deliver an
// `InputAvailable` event.
ffi::android_app_attach_input_queue_looper(app_ptr);
let queue = NonNull::new_unchecked((*app_ptr).inputQueue);
InputQueue::from_ptr(queue)
};
// Note: we basically ignore errors from get_event() currently. Looking
// at the source code for Android's InputQueue, the only error that
// can be returned here is 'WOULD_BLOCK', which we want to just treat as
// meaning the queue is empty.
//
// ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
//
while let Ok(Some(event)) = queue.get_event() {
if let Some(ndk_event) = queue.pre_dispatch(event) {
let event = match ndk_event {
ndk::event::InputEvent::MotionEvent(e) => input::InputEvent::MotionEvent(e),
ndk::event::InputEvent::KeyEvent(e) => input::InputEvent::KeyEvent(e),
};
callback(&event);
Ok(key_map)
}
let ndk_event = match event {
input::InputEvent::MotionEvent(e) => ndk::event::InputEvent::MotionEvent(e),
input::InputEvent::KeyEvent(e) => ndk::event::InputEvent::KeyEvent(e),
};
pub fn enable_motion_axis(&self, _axis: Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
// Always report events as 'handled'. This means we won't get
// so called 'fallback' events generated (such as converting trackball
// events into emulated keypad events), but we could conceivably
// implement similar emulation somewhere else in the stack if
// necessary, and this will be more consistent with the GameActivity
// input handling that doesn't do any kind of emulation.
queue.finish_event(ndk_event, true);
pub fn disable_motion_axis(&self, _axis: Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
pub fn input_events_receiver(&self) -> InternalResult<Arc<InputReceiver>> {
let mut guard = self.input_receiver.lock().unwrap();
if let Some(receiver) = &*guard {
if receiver.strong_count() > 0 {
return Err(crate::error::InternalAppError::InputUnavailable);
}
}
*guard = None;
// Get the InputQueue for the NativeActivity (if there is one) and also ensure
// the queue is re-attached to our event Looper (so new input events will again
// trigger a wake up)
let queue = self
.native_activity
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
// Note: we don't treat it as an error if there is no queue, so if applications
// iterate input before a queue has been created (e.g. before onStart) then
// it will simply behave like there are no events available currently.
let receiver = Arc::new(InputReceiver { queue });
*guard = Some(Arc::downgrade(&receiver));
Ok(receiver)
}
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
@@ -478,103 +460,90 @@ impl AndroidAppInner {
}
}
// Rust doesn't give us a clean way to directly export symbols from C/C++
// so we rename the C/C++ symbols and re-export this entrypoint from
// Rust...
//
// https://github.com/rust-lang/rfcs/issues/2771
extern "C" {
pub fn ANativeActivity_onCreate_C(
activity: *mut std::os::raw::c_void,
savedState: *mut ::std::os::raw::c_void,
savedStateSize: usize,
);
#[derive(Debug)]
pub(crate) struct InputReceiver {
queue: Option<InputQueue>,
}
#[no_mangle]
unsafe extern "C" fn ANativeActivity_onCreate(
activity: *mut std::os::raw::c_void,
saved_state: *mut std::os::raw::c_void,
saved_state_size: usize,
) {
ANativeActivity_onCreate_C(activity, saved_state, saved_state_size);
}
fn android_log(level: Level, tag: &CStr, msg: &CStr) {
let prio = match level {
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
};
unsafe {
ndk_sys::__android_log_write(prio.0 as raw::c_int, tag.as_ptr(), msg.as_ptr());
}
}
extern "Rust" {
pub fn android_main(app: AndroidApp);
}
// This is a spring board between android_native_app_glue and the user's
// `app_main` function. This is run on a dedicated thread spawned
// by android_native_app_glue.
#[no_mangle]
pub unsafe extern "C" fn _rust_glue_entry(app: *mut ffi::android_app) {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
thread::spawn(move || {
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
let file = File::from_raw_fd(logpipe[0]);
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
}
}
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
}
});
let app = AndroidApp::from_ptr(NonNull::new(app).unwrap());
let na = app.native_activity();
let jvm = (*na).vm;
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
}
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
detach_current_thread(jvm);
}
ndk_context::release_android_context();
}
+58 -3
View File
@@ -1,15 +1,70 @@
use std::{ffi::CStr, os::raw::c_char, ptr};
use log::Level;
use std::{
ffi::{CStr, CString},
os::raw::c_char,
};
pub fn try_get_path_from_ptr(path: *const c_char) -> Option<std::path::PathBuf> {
if path == ptr::null() {
if path.is_null() {
return None;
}
let cstr = unsafe {
let cstr_slice = CStr::from_ptr(path.cast());
cstr_slice.to_str().ok()?
};
if cstr.len() == 0 {
if cstr.is_empty() {
return None;
}
Some(std::path::PathBuf::from(cstr))
}
pub(crate) fn android_log(level: Level, tag: &CStr, msg: &CStr) {
let prio = match level {
Level::Error => ndk_sys::android_LogPriority::ANDROID_LOG_ERROR,
Level::Warn => ndk_sys::android_LogPriority::ANDROID_LOG_WARN,
Level::Info => ndk_sys::android_LogPriority::ANDROID_LOG_INFO,
Level::Debug => ndk_sys::android_LogPriority::ANDROID_LOG_DEBUG,
Level::Trace => ndk_sys::android_LogPriority::ANDROID_LOG_VERBOSE,
};
unsafe {
ndk_sys::__android_log_write(prio.0 as libc::c_int, tag.as_ptr(), msg.as_ptr());
}
}
pub(crate) fn log_panic(panic: Box<dyn std::any::Any + Send>) {
let rust_panic = unsafe { CStr::from_bytes_with_nul_unchecked(b"RustPanic\0") };
if let Some(panic) = panic.downcast_ref::<String>() {
if let Ok(msg) = CString::new(panic.clone()) {
android_log(Level::Error, rust_panic, &msg);
}
} else if let Ok(panic) = panic.downcast::<&str>() {
if let Ok(msg) = CString::new(*panic) {
android_log(Level::Error, rust_panic, &msg);
}
} else {
let unknown_panic = unsafe { CStr::from_bytes_with_nul_unchecked(b"UnknownPanic\0") };
android_log(Level::Error, unknown_panic, unsafe {
CStr::from_bytes_with_nul_unchecked(b"\0")
});
}
}
/// Run a closure and abort the program if it panics.
///
/// This is generally used to ensure Rust callbacks won't unwind past the JNI boundary, which leads
/// to undefined behaviour.
///
/// TODO(rib): throw a Java exception instead of aborting. An Android Activity does not necessarily
/// own the entire process because other application Services (or even Activities) may run in
/// threads within the same process, and so we're tearing down too much by aborting the process.
pub(crate) fn abort_on_panic<R>(f: impl FnOnce() -> R) -> R {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|panic| {
// Try logging the panic before aborting
//
// Just in case our attempt to log a panic could itself cause a panic we use a
// second catch_unwind here.
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| log_panic(panic)));
std::process::abort();
})
}
-21
View File
@@ -1,21 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
*.so
# Added by cargo
/target
Cargo.lock
-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>
-20
View File
@@ -1,20 +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>
<option name="resolveModulePerSourceSet" value="false" />
</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>
-23
View File
@@ -1,23 +0,0 @@
[package]
name = "agdk-cpal"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = ["game-activity"] }
cpal = "0.13"
atomic_float = "0.1"
anyhow = "1"
# We currently need to use cpal master which doesn't have ndk-glue as a dependency
# and is also updated to ndk 0.7
[patch.crates-io]
cpal = { git = "https://github.com/RustAudio/cpal", commit = "9b7bb339650" }
[lib]
name="main"
crate_type=["cdylib"]
-16
View File
@@ -1,16 +0,0 @@
This is a minimal test application based on `GameActivity` that just
runs a mainloop based on android_activity::poll_events() and plays a
sine wave audio test using the Cpal audio library.
```
export ANDROID_NDK_HOME="path/to/ndk"
export ANDROID_HOME="path/to/sdk"
rustup target add aarch64-linux-android
cargo install cargo-ndk
cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ build
./gradlew build
./gradlew installDebug
adb shell am start -n co.realfit.agdkcpal/.MainActivity
```
-1
View File
@@ -1 +0,0 @@
/build
-61
View File
@@ -1,61 +0,0 @@
plugins {
id 'com.android.application'
}
android {
compileSdk 31
defaultConfig {
applicationId "co.realfit.agdkcpal"
minSdk 28
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
//packagingOptions {
// doNotStrip '**/*.so'
//}
//debuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// To use the Android Frame Pacing library
//implementation "androidx.games:games-frame-pacing:1.9.1"
// To use the Android Performance Tuner
//implementation "androidx.games:games-performance-tuner:1.5.0"
// To use the Games Activity library
implementation "androidx.games:games-activity:1.1.0"
// To use the Games Controller Library
//implementation "androidx.games:games-controller:1.1.0"
// To use the Games Text Input Library
//implementation "androidx.games:games-text-input:1.1.0"
}
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="co.realfit.agdkcpal">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="AGDK Cpal"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RustTemplate">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="main" />
</activity>
</application>
</manifest>
@@ -1,70 +0,0 @@
package co.realfit.agdkcpal;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.androidgamesdk.GameActivity;
import android.os.Bundle;
import android.content.pm.PackageManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
public class MainActivity extends GameActivity {
static {
// Load the STL first to workaround issues on old Android versions:
// "if your app targets a version of Android earlier than Android 4.3
// (Android API level 18),
// and you use libc++_shared.so, you must load the shared library before any other
// library that depends on it."
// See https://developer.android.com/ndk/guides/cpp-support#shared_runtimes
//System.loadLibrary("c++_shared");
// Load the native library.
// The name "android-game" depends on your CMake configuration, must be
// consistent here and inside AndroidManifect.xml
System.loadLibrary("main");
}
private void hideSystemUI() {
// This will put the game behind any cutouts and waterfalls on devices which have
// them, so the corresponding insets will be non-zero.
if (VERSION.SDK_INT >= VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode
= WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
}
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
// using View.setSystemUiVisibility.
View decorView = getWindow().getDecorView();
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
decorView);
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.hide(WindowInsetsCompat.Type.displayCutout());
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// When true, the app will fit inside any system UI windows.
// When false, we render behind any system UI windows.
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
hideSystemUI();
// You can set IME fields here or in native code using GameActivity_setImeEditorInfoFields.
// We set the fields in native_engine.cpp.
// super.setImeEditorInfoFields(InputType.TYPE_CLASS_TEXT,
// IME_ACTION_NONE, IME_FLAG_NO_FULLSCREEN );
super.onCreate(savedInstanceState);
}
public boolean isGooglePlayGames() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
}
}
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

@@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.RustTemplate" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -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_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>
-10
View File
@@ -1,10 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.2' apply false
id 'com.android.library' version '7.1.2' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
-21
View File
@@ -1,21 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
Binary file not shown.
@@ -1,6 +0,0 @@
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
-89
View File
@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-16
View File
@@ -1,16 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
//rootProject.name = "Rust Template"
include ':app'
-142
View File
@@ -1,142 +0,0 @@
use android_activity::{AndroidApp, MainEvent, PollEvent};
use log::info;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
extern crate cpal;
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
where
T: cpal::Sample,
{
for frame in output.chunks_mut(channels) {
let value: T = cpal::Sample::from::<f32>(&next_sample());
for sample in frame.iter_mut() {
*sample = value;
}
}
}
fn make_audio_stream<T>(
device: &cpal::Device,
config: &cpal::StreamConfig,
) -> Result<cpal::Stream, anyhow::Error>
where
T: cpal::Sample,
{
let sample_rate = config.sample_rate.0 as f32;
let channels = config.channels as usize;
// Produce a sinusoid of maximum amplitude.
let mut sample_clock = 0f32;
let mut next_value = move || {
sample_clock = (sample_clock + 1.0) % sample_rate;
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
};
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
err_fn,
)?;
Ok(stream)
}
#[no_mangle]
fn android_main(app: AndroidApp) {
android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info));
let mut quit = false;
let mut redraw_pending = true;
let mut render_state: Option<()> = Default::default();
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("failed to find output device");
let config = device.default_output_config().unwrap();
let stream = match config.sample_format() {
cpal::SampleFormat::F32 => make_audio_stream::<f32>(&device, &config.into()).unwrap(),
cpal::SampleFormat::I16 => make_audio_stream::<i16>(&device, &config.into()).unwrap(),
cpal::SampleFormat::U16 => make_audio_stream::<u16>(&device, &config.into()).unwrap(),
};
while !quit {
app.poll_events(
Some(std::time::Duration::from_millis(500)), /* timeout */
|event| {
match event {
PollEvent::Wake => {
info!("Early wake up");
}
PollEvent::Timeout => {
info!("Timed out");
// Real app would probably rely on vblank sync via graphics API...
redraw_pending = true;
}
PollEvent::Main(main_event) => {
info!("Main event: {:?}", main_event);
match main_event {
MainEvent::SaveState { saver, .. } => {
saver.store("foo://bar".as_bytes());
}
MainEvent::Pause => {
if let Err(err) = stream.pause() {
log::error!("Failed to pause audio playback: {err}");
}
}
MainEvent::Resume { loader, .. } => {
if let Some(state) = loader.load() {
if let Ok(uri) = String::from_utf8(state) {
info!("Resumed with saved state = {uri:#?}");
}
}
if let Err(err) = stream.play() {
log::error!("Failed to start audio playback: {err}");
}
}
MainEvent::InitWindow { .. } => {
render_state = Some(());
redraw_pending = true;
}
MainEvent::TerminateWindow { .. } => {
render_state = None;
}
MainEvent::WindowResized { .. } => {
redraw_pending = true;
}
MainEvent::RedrawNeeded { .. } => {
redraw_pending = true;
}
MainEvent::LowMemory => {}
MainEvent::Destroy => quit = true,
_ => { /* ... */ }
}
}
_ => {}
}
if redraw_pending {
if let Some(_rs) = render_state {
redraw_pending = false;
// Handle input
app.input_events(|event| {
info!("Input Event: {event:?}");
});
info!("Render...");
}
}
},
);
}
}
-21
View File
@@ -1,21 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
*.so
# Added by cargo
/target
-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>
-16
View File
@@ -1,16 +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/agdk-winit-wgpu/app/src/main/res/layout/activity_main.xml" value="0.5546875" />
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
-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>
-2398
View File
File diff suppressed because it is too large Load Diff
-66
View File
@@ -1,66 +0,0 @@
[package]
name = "agdk-eframe"
version = "0.1.0"
edition = "2021"
resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
winit = "0.27.2"
wgpu = "0.13.0"
pollster = "0.2"
egui = "0.19"
eframe = { version = "0.19", features = [ "wgpu" ] }
egui_demo_lib = "0.19"
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.9"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.0"
android-activity = { version = "0.3", features = [ "game-activity" ] }
[patch.crates-io]
# This branch of Winit has an updated Android backend based on android-activity
# Note: The winit branches are current misnamed:
# - "android-activity" is based on Winit 0.27 (required for Egui compatibility)
# - "android-activity-0.27" is based on Winit master
# The -0.27 branch is currently associated with a pull request so we'll just
# stick with these names for now
winit = { git = "https://github.com/rib/winit", branch = "android-activity" }
#winit = { path = "../../../winit" }
# Note:
# Since android-activity is responsible for invoking the `android_main`
# entrypoint for a native Rust application there can only be a single
# implementation of the crate linked with the application.
#
# Since Winit also depends on android-activity (version = "0.2") but we'd like
# to build against the local version of android-activity in this repo then we
# use a [patch] to ensure we only end up with a single implementation.
android-activity = { path = "../../android-activity" }
# Egui 0.19 is missing some fixes for Android so we need to build against
# git master for now
egui = { git = "https://github.com/emilk/egui" }
eframe = { git = "https://github.com/emilk/egui" }
egui_demo_lib = { git = "https://github.com/emilk/egui" }
#egui = { path = "../../../egui/crates/egui" }
#eframe = { path = "../../../egui/crates/eframe" }
#egui_demo_lib = { path = "../../../egui/crates/egui_demo_lib" }
[features]
default = []
desktop = []
[lib]
name="main"
crate_type=["cdylib"]
[[bin]]
path="src/lib.rs"
name="agdk-eframe"
required-features = [ "desktop" ]
-17
View File
@@ -1,17 +0,0 @@
This tests using `GameActivity` with egui, winit and wgpu.
This is based on a re-worked winit backend here:
https://github.com/rib/winit/tree/android-activity
```
export ANDROID_NDK_HOME="path/to/ndk"
export ANDROID_HOME="path/to/sdk"
rustup target add aarch64-linux-android
cargo install cargo-ndk
cargo ndk -t arm64-v8a -o app/src/main/jniLibs/ build
./gradlew build
./gradlew installDebug
adb shell am start -n co.realfit.agdkeframe/.MainActivity
```
-1
View File
@@ -1 +0,0 @@
/build
-61
View File
@@ -1,61 +0,0 @@
plugins {
id 'com.android.application'
}
android {
compileSdk 31
defaultConfig {
applicationId "co.realfit.agdkeframe"
minSdk 28
targetSdk 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
//packagingOptions {
// doNotStrip '**/*.so'
//}
//debuggable true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// To use the Android Frame Pacing library
//implementation "androidx.games:games-frame-pacing:1.9.1"
// To use the Android Performance Tuner
//implementation "androidx.games:games-performance-tuner:1.5.0"
// To use the Games Activity library
implementation "androidx.games:games-activity:1.1.0"
// To use the Games Controller Library
//implementation "androidx.games:games-controller:1.1.0"
// To use the Games Text Input Library
//implementation "androidx.games:games-text-input:1.1.0"
}
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="co.realfit.agdkeframe">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="AGDK EFrame"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RustTemplate">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.lib_name" android:value="main" />
</activity>
</application>
</manifest>
@@ -1,70 +0,0 @@
package co.realfit.agdkeframe;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.androidgamesdk.GameActivity;
import android.os.Bundle;
import android.content.pm.PackageManager;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
public class MainActivity extends GameActivity {
static {
// Load the STL first to workaround issues on old Android versions:
// "if your app targets a version of Android earlier than Android 4.3
// (Android API level 18),
// and you use libc++_shared.so, you must load the shared library before any other
// library that depends on it."
// See https://developer.android.com/ndk/guides/cpp-support#shared_runtimes
//System.loadLibrary("c++_shared");
// Load the native library.
// The name "android-game" depends on your CMake configuration, must be
// consistent here and inside AndroidManifect.xml
System.loadLibrary("main");
}
private void hideSystemUI() {
// This will put the game behind any cutouts and waterfalls on devices which have
// them, so the corresponding insets will be non-zero.
if (VERSION.SDK_INT >= VERSION_CODES.P) {
getWindow().getAttributes().layoutInDisplayCutoutMode
= WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
}
// From API 30 onwards, this is the recommended way to hide the system UI, rather than
// using View.setSystemUiVisibility.
View decorView = getWindow().getDecorView();
WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(),
decorView);
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.hide(WindowInsetsCompat.Type.displayCutout());
controller.setSystemBarsBehavior(
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// When true, the app will fit inside any system UI windows.
// When false, we render behind any system UI windows.
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
hideSystemUI();
// You can set IME fields here or in native code using GameActivity_setImeEditorInfoFields.
// We set the fields in native_engine.cpp.
// super.setImeEditorInfoFields(InputType.TYPE_CLASS_TEXT,
// IME_ACTION_NONE, IME_FLAG_NO_FULLSCREEN );
super.onCreate(savedInstanceState);
}
public boolean isGooglePlayGames() {
PackageManager pm = getPackageManager();
return pm.hasSystemFeature("com.google.android.play.feature.HPE_EXPERIENCE");
}
}
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Some files were not shown because too many files have changed in this diff Show More