Compare commits

...

152 Commits

Author SHA1 Message Date
Robert Bragg 14a1b0a846 Merge tag 'v0.4.2' into v0.4 2023-06-27 17:48:20 +01:00
Robert Bragg e16b5b82d9 Merge commit '36ddfaa9cea7af9377aa43e0db6cc56e80ccc234' into v0.4 2023-06-27 17:47:32 +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 79d0a07564 Merge branch 'release-0.4' into v0.4 2022-11-14 16:15:44 +00: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
Robert Bragg bc177292d2 Release 0.3 2022-09-16 14:07:06 +01:00
Robert Bragg 8d30454c8d native-activity: InputEvent forward compatibility
When building with native-activity then we no longer directly use
the `ndk::InputEvent` type and instead have our own extensible,
`#[non_exhaustive]` enum that ensures we will be able to add an
event for Ime state changes without necessarily needing a semver
bump.

Addresses: #18
2022-09-15 17:12:08 +01:00
Robert Bragg feff63ae78 Add show/hide_soft_input methods
This adds `AndroidApp::show/hide_soft_input` APIs for showing or
hiding the user's on-screen keyboard. (supported for NativeActivity
and GameActivity)

Addresses: #18
2022-09-15 17:12:08 +01:00
Robert Bragg 48993acf58 Support changing window manager params
This enables support for changing the various WindowManager
parameters documented here:
https://developer.android.com/reference/android/view/WindowManager.LayoutParams

either via NativeActivity_setWindowFlags or GameActivity_setWindowFlags

Fixes: #25
2022-09-15 16:48:37 +01:00
Robert Bragg 7d73e57364 Extend compiler_error for missing feature to warn of duplicate implementations 2022-09-15 16:48:37 +01:00
Robert Bragg 781e1fd658 ci: string in expressions should use single quotes 2022-09-01 14:48:03 +01:00
Robert Bragg 16391c4956 ci: add missing quotes 2022-09-01 14:43:08 +01:00
Robert Bragg da29177b41 Only build examples with stable toolchain
Although we want to ensure android-activity itself builds with the
same MSRV as Winit there's no need to also check that the examples
build with an older toolchain, and in fact in the case of Egui based
examples they don't support being built with 1.60.0.

In addition to adding an `if: rust_version == "stable"` condition
for building agdk-egui, this also adds CI stages to build agdk-mainloop,
na-mainloop and agdk-eframe to cover at least one example based on
`NativeActivity` and cover the key examples that others are most
likely to be interested in.
2022-09-01 14:14:08 +01:00
Robert Bragg b29162d149 Revert "Support building with 1.57.0 compiler"
This reverts the changes from 66e3293f4d
to support the 1.57.0 compiler (leaving the rustdoc fixes).

Making the changes to support 1.57 felt disappointing to require for the
sake of Winit being able to better support Linux distro packaging. Even
with the changes we still wouldn't really support
1.57 without also upstreaming changes to `cargo ndk`.

The new plan is to bank on Winit bumping its own MSRV to at least
1.58 which seems more than reasonable considering that even 1.59
is already >6months old.

Ref: https://github.com/rust-windowing/winit/pull/2453

This updates ci.yml to now build with 1.60, assuming that the above
PR will be accepted.
2022-09-01 12:37:07 +01:00
Robert Bragg 3a2fa5d1fa CI: build docs and ensure support for 1.57.0 compiler 2022-08-31 23:05:24 +01:00
Robert Bragg 66e3293f4d Support building with 1.57.0 compiler
This is to support the same compiler versions as Winit
2022-08-31 23:04:09 +01:00
Robert Bragg d5ff06ffb2 Add OpenXR + Wgpu example
This tests being able to write an OpenXR application based on
Wgpu (using the Vulkan HAL backend) for the Oculus Quest.
2022-08-31 20:44:14 +01:00
Robert Bragg b7f01a43d9 Build fixes for Winit based examples
The latest branches for Winit (which update the Android backend to
use android-activity) now depend on the android-activity 0.2
release which broke the patching assumption for the Winit examples.
(they were patching based on the git url not crates.io)

It was also noticed that the latest android-activity-0.27 branch
isn't compatible with Egui currently because the (badly named) branch
is based on Winit master and there was recently a breaking API change
adding new window events which breaks Egui.

A note about the badly name branches has been added to the Cargo.toml
files for the Winit examples (they can't currently be fixes since
the -0.27 branch is part of a pull request).

For reference here:
- "android-activity" is based on Winit 0.27 (required for Egui compatibility)
- "android-activity-0.27" is based on Winit master

This patch adds Cargo.lock files for the Winit examples.

Fixes: #22
2022-08-31 20:43:45 +01:00
Robert Bragg f673662316 Add a minimal OpenXR Info example 2022-08-26 17:27:03 +01:00
Robert Bragg 5dab74466c Release 0.2 2022-08-25 10:34:31 +01:00
Robert Bragg 11c3af686f examples: winit simplifications
* Since we can rely on `Resumed` events for all platforms with Winit 0.27
we can remove the alternative `NewEvents` path for initializing state.
* Removes some unnecessary abstraction for the app state.
* Bumps agdk-egui to egui 0.19 (Winit 0.27)
2022-08-24 21:23:29 +01:00
John Kåre Alsaker 58bc3f6de7 Add eframe example
Co-authored-by: Robert Bragg <robert@sixbynine.org>
2022-08-24 20:35:41 +01:00
Robert Bragg 6ea3f82fb8 agdk-egui: revert back to updated android-activity-0.27 Winit branch 2022-08-14 11:06:27 +01:00
Robert Bragg fb9a9f15c8 agdk-egui: use staging branch for winit, for CI build 2022-08-14 11:02:22 +01:00
Robert Bragg 3e95c428f5 Remove old portability comment for InputAvailable event 2022-08-14 11:02:22 +01:00
Robert Bragg 5d78da7296 Remove FdEvent while it's unused for now
Considering the possibility of using epoll directly or perhaps mio in
the future instead of the ALooper wrapper API then this just removes
the FdEvent and Error enum values from PollEvent as a simplification
in case they could make it harder to keep compatibility in the future.
2022-08-14 11:02:22 +01:00
Robert Bragg b1a63aeee6 Remove NativeWindowRef type
Since ndk 0.7 the `NativeWindow` type implements Clone and Drop
which makes this wrapper type redundant now.
2022-08-14 11:02:22 +01:00
Robert Bragg 206c8e2c84 Make ContentRectChanged event a non_exhaustive struct
Just to keep open the ability to add state to the event in the future
without needing an API break.
2022-08-14 11:02:22 +01:00
Robert Bragg a654f72f62 Expose the application Configuration by reference
This provides an API to access `Configuration` state for the application
without having to make deep copies of the large `Configuration` struct.

This should avoid the need for Winit to create a global static copy of
the Configuration whenever it changes - and instead it can just get
a `ConfigurationRef` which will always reflect the latest config for
the application.

Fixes: #5
2022-08-14 11:02:22 +01:00
Robert Bragg b161b24ce4 Make AndroidApp Send + Sync
In Winit then we ideally want to be able to associate a cloned reference to
the AndroidApp with the `Window` and `MonitorHandle` types (since that's the
most practical way to access the native_window and config state for the
application) but for that `AndroidApp` needs to be Send + Sync
2022-08-14 11:02:22 +01:00
Robert Bragg a735faa753 Add an initial CHANGELOG.md
Based on https://keepachangelog.com/en/1.0.0
2022-08-14 00:48:48 +01:00
Robert Bragg 2a9ed44d93 agdk-cpal: Use cpal git master
I was mistaken recently, thinking that cpal's dev-dependency on ndk-glue
was somehow being inherited by the example. Actually the real issue was
that cpal 0.13.5 has a regular _dependency_ on ndk-glue, which is a much
simpler explanation for why we were seeing a crash.

This updates the example to just use the master branch of cpal, which we'll
need until there is a new release of cpal.
2022-08-14 00:16:21 +01:00
Robert Bragg 3d1b1c5cb9 Supports InputAvailable events with GameActivity
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).

When a wake up is received and we recognise that new input is available
then an `InputAvailable` event is sent 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.

Addresses #4
2022-08-13 20:26:00 +01:00
Robert Bragg 00b116bcfe Factor out shared try_get_path_from_ptr utility API 2022-08-13 04:02:59 +01:00
John Kåre Alsaker dfc58b49fb Link the C++ standard library correctly. 2022-08-13 03:38:06 +01:00
Robert Bragg ecd03edb1a Add minimal Cpal audio library example
Based on the android example that's in the cpal repo, this test plays
a 440hz sine wave and is based on GameActivity

Note: this requires a workaround branch for cpal that comments out
a dev-dependency on ndk-glue that cpal has for its android example.
This is needed because Cargo is spuriously propagating this dev
dependency outside of the cpal package (which leads to a crash)
2022-08-12 19:28:48 +01:00
Robert Bragg 04c9060329 cargo fmt 2022-08-12 19:20:09 +01:00
Robert Bragg f22d8bbd30 Update generated NDK bindings 2022-08-12 19:10:31 +01:00
Robert Bragg 93828a18a9 Use NativeWindow::clone_from_ptr() to avoid Segfault
Since f24606cc84 in android-ndk-rs then NativeWindow now implements
Clone and Drop which was technically a breaking change since it
changed the ownership contract for existing users of
NativeWindow::from_ptr().

We now use NativeWindow::clone_from_ptr() to account for the fact that
the window will be unconditionally _released() when NativeWindow gets
dropped.

This addresses a crash I was debugging with the Cpal and Oboe
examples which turned out to be nothing to do with the examples
themselves.
2022-08-12 19:10:30 +01:00
Robert Bragg 4818de6709 examples: force Winit to use in-tree android-activity
This fixes build errors about not specifying either of the
"native-activity" or "game-activity" features.

The issue came about because the in-tree examples want to
build against the in-tree version of android-activity located
with a relative `path = ` but these particular examples
depend on Winit which would resolve a second implementation
of android-activity, via a github url - where Cargo will treat
them as completely different crates.
2022-08-12 02:08:44 +01:00
Robert Bragg 190a3b91e7 CStr::from_ptr may expect *i8 or *u8 depending on target 2022-08-12 02:08:44 +01:00
Robert Bragg f54eaa2997 native-activity: coerce savedStateSize with
The type may have different sizes depending on the build target
2022-08-12 02:08:44 +01:00
Robert Bragg 7a77402279 native-activity: Fix InputAvailable support
"native-activity" builds were recently broken by bb8eeb705c which
this patch fixes.

The na-mainloop example has also been updated to verify this by
reducing the fake "render" timeout and also triggering a fake
render when an InputAvailable event is received.

Fixes: #12
2022-08-12 02:08:44 +01:00
Robert Bragg 0d0c10ebb2 Runs cargo fmt across everything 2022-08-11 23:16:55 +01:00
Robert Bragg 5220d35373 Don't depend on a specific micro version of libc 2022-08-11 23:16:46 +01:00
Robert Bragg b05dcb6d44 Merge pull request #9 from Zoxc/ci
Add some initial CI support
2022-08-11 22:16:50 +01:00
Robert Bragg e4608b0791 agdk-winit-wgpu: update to wgpu 0.13 2022-08-11 03:37:22 +01:00
Robert Bragg ce36c38934 Update to expect 0.27 based Winit branch
While egui hasn't yet merged its Winit 0.27 support into master
agdk-egui currently points to an egui branch with Winit 0.27 support
2022-08-11 03:36:02 +01:00
Robert Bragg 7a49db4d61 Update to ndk 0.7 and ndk-sys 0.4 2022-08-11 03:32:28 +01:00
Robert Bragg 73a1acb3c7 examples: bump gradle-wrapper to fetch gradle-7.5-bin.zip 2022-08-11 03:12:33 +01:00
Robert Bragg b07717d8ac Merge pull request #8 from Zoxc/misc-fixes
Misc agdk-egui fixes
2022-08-11 02:12:37 +01:00
John Kåre Alsaker 8601f28eba Split formatting to a separate job 2022-08-11 01:04:09 +02:00
John Kåre Alsaker 49c877085b Fix indentation 2022-08-11 00:55:04 +02:00
John Kåre Alsaker 9e3d8fd977 Add CI 2022-08-11 00:04:53 +02:00
John Kåre Alsaker 5a7bc64bb1 Fix types 2022-08-09 23:05:02 +02:00
John Kåre Alsaker 114e249cbd Fix egui build 2022-08-09 22:44:50 +02:00
John Kåre Alsaker 875952b2a9 Update Gradle to build with Java 18 2022-08-09 22:44:04 +02:00
John Kåre Alsaker b1b7f49c7e Add target/ to .gitignore 2022-08-09 22:43:22 +02:00
Robert Bragg 19c989c640 agdk-mainloop: fix features declaration 2022-07-26 02:03:36 +01:00
Robert Bragg 6278b54494 agdk-winit-wgpu: fix use of android_activity
Fixes issue raised in #6 by @tschrpl
2022-07-26 02:00:24 +01:00
Robert Bragg a73f9202d0 Add minimal Oboe audio library example
Based on the demo in the oboe-rs repo this is a minimal example that
runs with GameActivity and plays a 440Hz sine wave
2022-07-26 01:31:56 +01:00
Robert Bragg bb8eeb705c native-activity: emit an InputAvailable event for new input
This reinstates support for notifying applications of new input events
without requiring them to always check for input as part of their
rendering updates. This makes it possible to build UI applications that
might only need to redraw in response to new input.

For now GameActivity doesn't emit this event but the plan is to also
add support for this in GameActivity.

Addresses: #4
2022-07-06 15:26:14 +01:00
Robert Bragg 1ca5f13874 android_native_app_glue: expose helpers for attaching input queue to looper 2022-07-06 15:26:14 +01:00
Robert Bragg eda89d0e15 generate-bindings: use ANDROID_NDK_HOME if set 2022-07-06 14:58:29 +01:00
Robert Bragg a8e8017918 Bump ndk/ndk-sys deps for consistency with winit master 2022-07-04 20:54:20 +01:00
Robert Bragg a8c0e37d80 Fix for generate-bindings.sh and updates generated ffi bindings 2022-07-04 20:40:03 +01:00
Robert Bragg 3f06bce6ce Cargo.toml: Add docs.rs link 2022-07-04 19:39:03 +01:00
Robert Bragg 74b2f22494 docs: generate docs as if native-activity feature enabled 2022-07-04 02:39:59 +01:00
201 changed files with 4136 additions and 43554 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
+118
View File
@@ -0,0 +1,118 @@
name: ci
on:
push:
branches: [main]
pull_request:
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0 --deny warnings"
RUSTDOCFLAGS: -Dwarnings
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# See top README for MSRV policy
rust_version: [1.64.0, stable]
steps:
- uses: actions/checkout@v3
- uses: hecrj/setup-rust-action@v1
with:
rust-version: ${{ matrix.rust_version }}
- name: Install Rust targets
run: >
rustup target add
aarch64-linux-android
armv7-linux-androideabi
x86_64-linux-android
i686-linux-android
- name: Install cargo-ndk
# We're currently sticking with cargo-ndk 2, until we bump our
# MSRV to 1.68+
run: cargo install cargo-ndk --version "^2"
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Build game-activity
working-directory: android-activity
run: >
cargo ndk
-t arm64-v8a
-t armeabi-v7a
-t x86_64
-t x86
build --features game-activity
- name: Build native-activity
working-directory: android-activity
run: >
cargo ndk
-t arm64-v8a
-t armeabi-v7a
-t x86_64
-t x86
build --features native-activity
- name: Build agdk-mainloop example
if: matrix.rust_version == 'stable'
working-directory: examples/agdk-mainloop
run: >
cargo ndk
-t arm64-v8a
-t armeabi-v7a
-t x86_64
-t x86
-o app/src/main/jniLibs/ -- build
- name: Build na-mainloop example
if: matrix.rust_version == 'stable'
working-directory: examples/na-mainloop
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
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- name: Format
run: cargo fmt --all -- --check
working-directory: android-activity
- name: Format na-mainloop example
run: cargo fmt --all -- --check
working-directory: examples/na-mainloop
- name: Format agdk-mainloop example
run: cargo fmt --all -- --check
working-directory: examples/agdk-mainloop
+1
View File
@@ -0,0 +1 @@
target
+1 -6
View File
@@ -4,10 +4,5 @@ members = [
]
exclude = [
"examples/agdk-mainloop",
"examples/agdk-winit-wgpu",
"examples/agdk-egui",
"examples/na-mainloop",
"examples/na-winit-wgpu",
"examples/na-subclass-jni"
"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.
+57
View File
@@ -0,0 +1,57 @@
# 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.4.2] - 2022-02-16
### Changed
- The `Activity.finish()` method is now called when `android_main` returns so the `Activity` will be destroyed ([#67](https://github.com/rust-mobile/android-activity/issues/67))
- The `native-activity` backend now propagates `NativeWindow` redraw/resize and `ContentRectChanged` callbacks to main loop ([#70](https://github.com/rust-mobile/android-activity/pull/70))
- The `game-activity` implementation of `pointer_index()` was fixed to not always return `0` ([#80](https://github.com/rust-mobile/android-activity/pull/84))
- Added `panic` guards around application's `android_main()` and native code that could potentially unwind across a Java FFI boundary ([#68](https://github.com/rust-mobile/android-activity/pull/68))
## [0.4.1] - 2022-02-16
### 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
- Emit an `InputAvailable` event for new input with `NativeActivity` and `GameActivity`
enabling gui apps that don't render continuously
- Oboe and Cpal audio examples added
- `AndroidApp` is now `Send` + `Sync`
### Changed
- *Breaking*: updates to `ndk 0.7` and `ndk-sys 0.4`
- *Breaking*: `AndroidApp::config()` now returns a clonable `ConfigurationRef` instead of a deep `Configuration` copy
### Removed
- The `NativeWindowRef` wrapper struct was removed since `NativeWindow` now implements `Clone` and `Drop` in `ndk 0.7`
- *Breaking*: The `FdEvent` and `Error` enum values were removed from `PollEvents`
## [0.1.1] - 2022-07-04
### Changed
- Documentation fixes
## [0.1] - 2022-07-04
### Added
- Initial release
+15 -11
View File
@@ -1,13 +1,15 @@
[package]
name = "android-activity"
version = "0.1.0"
version = "0.4.2"
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"
rust-version = "1.64"
[features]
# Note: we don't enable any backend by default since features
@@ -15,21 +17,21 @@ license = "MIT OR Apache-2.0"
# are actually mutually exclusive.
#
# In general it's only the final application crate that needs
# to decide in a backend.
default=[]
# to decide on a backend.
default = []
game-activity = []
native-activity = []
[dependencies]
log = "0.4"
jni-sys = "0.3"
ndk = { version = "0.6" }
ndk-sys = { version = "0.3" }
ndk-context = { version = "0.1" }
ndk = "0.7"
ndk-sys = "0.4"
ndk-context = "0.1"
android-properties = "0.2"
num_enum = "0.5"
num_enum = "0.6"
bitflags = "1.3"
libc = "0.2.84"
libc = "0.2"
[build-dependencies]
cc = { version = "1.0", features = ["parallel"] }
@@ -40,4 +42,6 @@ targets = [
"armv7-linux-androideabi",
"i686-linux-android",
"x86_64-linux-android",
]
]
rustdoc-args = ["--cfg", "docsrs"]
+6 -12
View File
@@ -1,13 +1,5 @@
#![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() {
cc::Build::new()
.cpp(true)
@@ -29,11 +21,13 @@ fn build_glue_for_game_activity() {
.extra_warnings(false)
.cpp_link_stdlib("c++_static")
.compile("libnative_app_glue.a");
// We need to link to both c++_static and c++abi for the static C++ library.
// Ideally we'd link directly to libc++.a.
println!("cargo:rustc-link-lib=c++abi");
}
fn main() {
#[cfg(feature="game-activity")]
#[cfg(feature = "game-activity")]
build_glue_for_game_activity();
#[cfg(feature="native-activity")]
build_glue_for_native_activity();
}
}
@@ -890,6 +890,7 @@ static struct {
jmethodID getPointerCount;
jmethodID getPointerId;
jmethodID getToolType;
jmethodID getRawX;
jmethodID getRawY;
jmethodID getXPrecision;
@@ -941,6 +942,8 @@ extern "C" int GameActivityMotionEvent_fromJava(
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");
@@ -1001,6 +1004,8 @@ extern "C" int GameActivityMotionEvent_fromJava(
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,
@@ -137,6 +137,7 @@ typedef struct GameActivity {
*/
typedef struct GameActivityPointerAxes {
int32_t id;
int32_t toolType;
float axisValues[GAME_ACTIVITY_POINTER_INFO_AXIS_COUNT];
float rawX;
float rawY;
@@ -449,6 +449,31 @@ void android_app_set_motion_event_filter(struct android_app* app,
pthread_mutex_unlock(&app->mutex);
}
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);
return available;
}
// NB: should be called with the android_app->mutex held already
static void notifyInput(struct android_app* android_app) {
// Don't spam the mainloop with wake ups if we've already sent one
if (android_app->inputSwapPending) {
return;
}
if (android_app->looper != NULL) {
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;
ALooper_wake(android_app->looper);
}
}
static bool onTouchEvent(GameActivity* activity,
const GameActivityMotionEvent* event,
const GameActivityHistoricalPointerAxes* historical,
@@ -486,12 +511,15 @@ static bool onTouchEvent(GameActivity* activity,
} else {
inputBuffer->motionEvents[new_ix].historicalCount = 0;
}
notifyInput(android_app);
} else {
LOGW_ONCE("Motion event will be dropped because the number of unconsumed motion"
" events exceeded NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS (%d). Consider setting"
" NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS_OVERRIDE to a larger value",
NATIVE_APP_GLUE_MAX_NUM_MOTION_EVENTS);
}
pthread_mutex_unlock(&android_app->mutex);
return true;
}
@@ -512,6 +540,9 @@ struct android_input_buffer* android_app_swap_input_buffers(
NATIVE_APP_GLUE_MAX_INPUT_BUFFERS;
}
android_app->inputSwapPending = false;
android_app->inputAvailableWakeUp = false;
pthread_mutex_unlock(&android_app->mutex);
return inputBuffer;
@@ -547,6 +578,8 @@ static bool onKey(GameActivity* activity, const GameActivityKeyEvent* event) {
memcpy(&inputBuffer->keyEvents[new_ix], event,
sizeof(GameActivityKeyEvent));
++inputBuffer->keyEventsCount;
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"
@@ -295,6 +295,26 @@ struct android_app {
android_key_event_filter keyEventFilter;
android_motion_event_filter motionEventFilter;
// When new input is received we set both of these flags and use the looper to
// wake up the application mainloop.
//
// To avoid spamming the mainloop with wake ups from lots of input though we
// don't sent a wake up if the inputSwapPending flag is already set. (i.e.
// we already expect input to be processed in a finite amount of time due to
// our previous wake up)
//
// When a wake up is received then we will check this flag (clearing it
// at the same time). If it was set then an InputAvailable event is sent to
// the application - which should lead to all input being processed within
// a finite amount of time.
//
// The next time android_app_swap_input_buffers is called, both flags will be
// cleared.
//
// NB: both of these should only be read with the app mutex held
bool inputAvailableWakeUp;
bool inputSwapPending;
/** @endcond */
};
@@ -507,6 +527,10 @@ void android_app_set_key_event_filter(struct android_app* app,
void android_app_set_motion_event_filter(struct android_app* app,
android_motion_event_filter filter);
/**
* Determines if a looper wake up was due to new input becoming available
*/
bool android_app_input_available_wake_up(struct android_app* app);
void GameActivity_onCreate_C(GameActivity* activity, void* savedState,
size_t savedStateSize);
+8 -24
View File
@@ -1,5 +1,8 @@
#!/bin/sh
if test -z "${ANDROID_NDK_ROOT}"; then
export ANDROID_NDK_ROOT=${ANDROID_NDK_HOME}
fi
SYSROOT="${ANDROID_NDK_ROOT}"/toolchains/llvm/prebuilt/linux-x86_64/sysroot/
if ! test -d $SYSROOT; then
SYSROOT="${ANDROID_NDK_ROOT}"/toolchains/llvm/prebuilt/windows-x86_64/sysroot/
@@ -8,11 +11,15 @@ fi
while read ARCH && read TARGET ; do
# --module-raw-line 'use '
bindgen game-activity-ffi.h -o src/game-activity/ffi_$ARCH.rs \
bindgen game-activity-ffi.h -o src/game_activity/ffi_$ARCH.rs \
--blocklist-item 'JNI\w+' \
--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*' \
@@ -30,29 +37,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,444 +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_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) {
//AInputQueue_detachLooper(android_app->inputQueue);
}
android_app->inputQueue = android_app->pendingInputQueue;
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);
}
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,354 +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);
/**
* 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 */
+151
View File
@@ -0,0 +1,151 @@
use core::fmt;
use std::sync::{Arc, RwLock};
use ndk::configuration::{
Configuration, Keyboard, KeysHidden, LayoutDir, NavHidden, Navigation, Orientation, ScreenLong,
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>>,
}
impl PartialEq for ConfigurationRef {
fn eq(&self, other: &Self) -> bool {
if Arc::ptr_eq(&self.config, &other.config) {
true
} else {
let other_guard = other.config.read().unwrap();
self.config.read().unwrap().eq(&*other_guard)
}
}
}
impl Eq for ConfigurationRef {}
unsafe impl Send for ConfigurationRef {}
unsafe impl Sync for ConfigurationRef {}
impl fmt::Debug for ConfigurationRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.config.read().unwrap().fmt(f)
}
}
impl ConfigurationRef {
pub(crate) fn new(config: Configuration) -> Self {
Self {
config: Arc::new(RwLock::new(config)),
}
}
pub(crate) fn replace(&self, src: Configuration) {
self.config.write().unwrap().copy(&src);
}
// Returns a deep copy of the full application configuration
pub fn copy(&self) -> Configuration {
let mut dest = Configuration::new();
dest.copy(&self.config.read().unwrap());
dest
}
/// Returns the country code, as a [`String`] of two characters, if set
pub fn country(&self) -> Option<String> {
self.config.read().unwrap().country()
}
/// Returns the screen density in dpi.
///
/// On some devices it can return values outside of the density enum.
pub fn density(&self) -> Option<u32> {
self.config.read().unwrap().density()
}
/// Returns the keyboard type.
pub fn keyboard(&self) -> Keyboard {
self.config.read().unwrap().keyboard()
}
/// Returns keyboard visibility/availability.
pub fn keys_hidden(&self) -> KeysHidden {
self.config.read().unwrap().keys_hidden()
}
/// Returns the language, as a `String` of two characters, if a language is set
pub fn language(&self) -> Option<String> {
self.config.read().unwrap().language()
}
/// Returns the layout direction
pub fn layout_direction(&self) -> LayoutDir {
self.config.read().unwrap().layout_direction()
}
/// Returns the mobile country code.
pub fn mcc(&self) -> i32 {
self.config.read().unwrap().mcc()
}
/// Returns the mobile network code, if one is defined
pub fn mnc(&self) -> Option<i32> {
self.config.read().unwrap().mnc()
}
pub fn nav_hidden(&self) -> NavHidden {
self.config.read().unwrap().nav_hidden()
}
pub fn navigation(&self) -> Navigation {
self.config.read().unwrap().navigation()
}
pub fn orientation(&self) -> Orientation {
self.config.read().unwrap().orientation()
}
pub fn screen_height_dp(&self) -> Option<i32> {
self.config.read().unwrap().screen_height_dp()
}
pub fn screen_width_dp(&self) -> Option<i32> {
self.config.read().unwrap().screen_width_dp()
}
pub fn screen_long(&self) -> ScreenLong {
self.config.read().unwrap().screen_long()
}
#[cfg(feature = "api-level-30")]
pub fn screen_round(&self) -> ScreenRound {
self.config.read().unwrap().screen_round()
}
pub fn screen_size(&self) -> ScreenSize {
self.config.read().unwrap().screen_size()
}
pub fn sdk_version(&self) -> i32 {
self.config.read().unwrap().sdk_version()
}
pub fn smallest_screen_width_dp(&self) -> Option<i32> {
self.config.read().unwrap().smallest_screen_width_dp()
}
pub fn touchscreen(&self) -> Touchscreen {
self.config.read().unwrap().touchscreen()
}
pub fn ui_mode_night(&self) -> UiModeNight {
self.config.read().unwrap().ui_mode_night()
}
pub fn ui_mode_type(&self) -> UiModeType {
self.config.read().unwrap().ui_mode_type()
}
}
+3 -5
View File
@@ -13,10 +13,8 @@
#![allow(dead_code)]
use jni_sys::*;
use ndk_sys::AAssetManager;
use ndk_sys::ANativeWindow;
use ndk_sys::{ALooper, ALooper_callbackFunc, AConfiguration};
use libc::{pthread_cond_t, pthread_mutex_t, pthread_t, size_t};
use ndk_sys::{AAssetManager, AConfiguration, ALooper, ALooper_callbackFunc, ANativeWindow, ARect};
#[cfg(all(
any(target_os = "android", feature = "test"),
@@ -31,4 +29,4 @@ include!("ffi_aarch64.rs");
include!("ffi_i686.rs");
#[cfg(all(any(target_os = "android", feature = "test"), target_arch = "x86_64"))]
include!("ffi_x86_64.rs");
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
+50 -104
View File
@@ -15,89 +15,17 @@
use num_enum::{IntoPrimitive, TryFromPrimitive};
use std::{convert::TryInto, ops::Deref};
use crate::game_activity::ffi::{GameActivityMotionEvent, GameActivityKeyEvent};
use bitflags::bitflags;
use crate::game_activity::ffi::{GameActivityKeyEvent, GameActivityMotionEvent};
use crate::input::{Class, Source};
// Note: try to keep this wrapper API compatible with the AInputEvent API if possible
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum InputEvent {
MotionEvent(MotionEvent),
KeyEvent(KeyEvent)
}
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_ANY)
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(i32)]
pub enum Source {
BluetoothStylus = 0x0000c002,
Dpad = 0x00000201,
/// Either a gamepad or a joystick
Gamepad = 0x00000401,
Hdmi = 0x02000001,
/// Either a gamepad or a joystick
Joystick = 0x01000010,
/// Pretty much any device with buttons. Query the keyboard type to determine
/// if it has alphabetic keys and can be used for text entry.
Keyboard = 0x00000101,
/// A pointing device, such as a mouse or trackpad
Mouse = 0x00002002,
/// A pointing device, such as a mouse or trackpad whose relative motions should be treated as navigation events
MouseRelative = 0x00020004,
/// An input device akin to a scroll wheel
RotaryEncoder = 0x00400000,
Sensor = 0x04000000,
Stylus = 0x00004002,
Touchpad = 0x00100008,
Touchscreen = 0x00001002,
TouchNavigation = 0x00200000,
Trackball = 0x00010004,
Unknown = 0,
}
bitflags! {
struct SourceFlags: u32 {
const CLASS_MASK = 0x000000ff;
const BUTTON = 0x00000001;
const POINTER = 0x00000002;
const TRACKBALL = 0x00000004;
const POSITION = 0x00000008;
const JOYSTICK = 0x00000010;
const NONE = 0;
}
}
/// An enum representing the class of a [`MotionEvent`] or [`KeyEvent`] source
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_CLASS_MASK)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
None,
Button,
Pointer,
Trackball,
Position,
Joystick
}
impl From<i32> for Class {
fn from(source: i32) -> Self {
let class = SourceFlags::from_bits_truncate(source as u32);
match class {
SourceFlags::BUTTON => Class::Button,
SourceFlags::POINTER => Class::Pointer,
SourceFlags::TRACKBALL => Class::Trackball,
SourceFlags::POSITION => Class::Position,
SourceFlags::JOYSTICK => Class::Joystick,
_ => Class::None
}
}
pub enum InputEvent<'a> {
MotionEvent(MotionEvent<'a>),
KeyEvent(KeyEvent<'a>),
}
/// A bitfield representing the state of modifier keys during an event.
@@ -182,15 +110,15 @@ impl MetaState {
/// For general discussion of motion events in Android, see [the relevant
/// javadoc](https://developer.android.com/reference/android/view/MotionEvent).
#[derive(Clone, Debug)]
pub struct MotionEvent {
ga_event: GameActivityMotionEvent
pub struct MotionEvent<'a> {
ga_event: &'a GameActivityMotionEvent,
}
impl Deref for MotionEvent {
impl<'a> Deref for MotionEvent<'a> {
type Target = GameActivityMotionEvent;
fn deref(&self) -> &Self::Target {
&self.ga_event
self.ga_event
}
}
@@ -269,6 +197,20 @@ pub enum Axis {
Generic16 = ndk_sys::AMOTION_EVENT_AXIS_GENERIC_16,
}
/// The tool type of a pointer.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-48)
#[derive(Copy, Clone, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum ToolType {
Unknown = ndk_sys::AMOTION_EVENT_TOOL_TYPE_UNKNOWN,
Finger = ndk_sys::AMOTION_EVENT_TOOL_TYPE_FINGER,
Stylus = ndk_sys::AMOTION_EVENT_TOOL_TYPE_STYLUS,
Mouse = ndk_sys::AMOTION_EVENT_TOOL_TYPE_MOUSE,
Eraser = ndk_sys::AMOTION_EVENT_TOOL_TYPE_ERASER,
Palm = ndk_sys::AMOTION_EVENT_TOOL_TYPE_PALM,
}
/// A bitfield representing the state of buttons during a motion event.
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#anonymous-enum-33)
@@ -344,8 +286,8 @@ impl MotionEventFlags {
}
}
impl MotionEvent {
pub(crate) fn new(ga_event: GameActivityMotionEvent) -> Self {
impl<'a> MotionEvent<'a> {
pub(crate) fn new(ga_event: &'a GameActivityMotionEvent) -> Self {
Self { ga_event }
}
@@ -353,14 +295,15 @@ impl MotionEvent {
///
#[inline]
pub fn source(&self) -> Source {
self.source.try_into().unwrap_or(Source::Unknown)
let source = self.source as u32;
source.try_into().unwrap_or(Source::Unknown)
}
/// Get the class of the event source.
///
#[inline]
pub fn class(&self) -> Class {
Class::from(self.source)
Class::from(self.source())
}
/// Get the device id associated with the event.
@@ -389,7 +332,7 @@ impl MotionEvent {
/// or [`PointerDown`](MotionAction::PointerDown).
#[inline]
pub fn pointer_index(&self) -> usize {
let action = self.action as u32 & ndk_sys::AMOTION_EVENT_ACTION_MASK;
let action = self.action as u32;
let index = (action & ndk_sys::AMOTION_EVENT_ACTION_POINTER_INDEX_MASK)
>> ndk_sys::AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
index as usize
@@ -433,10 +376,7 @@ impl MotionEvent {
if index >= self.pointer_count() {
panic!("Pointer index {} is out of bounds", index);
}
Pointer {
event: self,
index,
}
Pointer { event: self, index }
}
/*
@@ -558,7 +498,7 @@ impl MotionEvent {
/// A view into the data of a specific pointer in a motion event.
#[derive(Debug)]
pub struct Pointer<'a> {
event: &'a MotionEvent,
event: &'a MotionEvent<'a>,
index: usize,
}
@@ -636,12 +576,19 @@ impl<'a> Pointer<'a> {
pub fn touch_minor(&self) -> f32 {
self.axis_value(Axis::TouchMinor)
}
#[inline]
pub fn tool_type(&self) -> ToolType {
let pointer = &self.event.pointers[self.index];
let tool_type = pointer.toolType as u32;
tool_type.try_into().unwrap()
}
}
/// An iterator over the pointers in a [`MotionEvent`].
#[derive(Debug)]
pub struct PointersIter<'a> {
event: &'a MotionEvent,
event: &'a MotionEvent<'a>,
next_index: usize,
count: usize,
}
@@ -986,15 +933,15 @@ impl ExactSizeIterator for HistoricalPointersIter<'_> {
/// For general discussion of key events in Android, see [the relevant
/// javadoc](https://developer.android.com/reference/android/view/KeyEvent).
#[derive(Debug, Clone)]
pub struct KeyEvent {
ga_event: GameActivityKeyEvent
pub struct KeyEvent<'a> {
ga_event: &'a GameActivityKeyEvent,
}
impl Deref for KeyEvent {
impl<'a> Deref for KeyEvent<'a> {
type Target = GameActivityKeyEvent;
fn deref(&self) -> &Self::Target {
&self.ga_event
self.ga_event
}
}
@@ -1306,9 +1253,8 @@ pub enum Keycode {
ProfileSwitch = ndk_sys::AKEYCODE_PROFILE_SWITCH,
}
impl KeyEvent {
pub(crate) fn new(ga_event: GameActivityKeyEvent) -> Self {
impl<'a> KeyEvent<'a> {
pub(crate) fn new(ga_event: &'a GameActivityKeyEvent) -> Self {
Self { ga_event }
}
@@ -1316,14 +1262,15 @@ impl KeyEvent {
///
#[inline]
pub fn source(&self) -> Source {
self.source.try_into().unwrap_or(Source::Unknown)
let source = self.source as u32;
source.try_into().unwrap_or(Source::Unknown)
}
/// Get the class of the event source.
///
#[inline]
pub fn class(&self) -> Class {
Class::from(self.source)
Class::from(self.source())
}
/// Get the device id associated with the event.
@@ -1444,8 +1391,7 @@ impl KeyEventFlags {
}
}
impl KeyEvent {
impl<'a> KeyEvent<'a> {
/// Flags associated with this [`KeyEvent`].
///
/// See [the NDK docs](https://developer.android.com/ndk/reference/group/input#akeyevent_getflags)
@@ -1462,4 +1408,4 @@ impl KeyEvent {
pub fn meta_state(&self) -> MetaState {
MetaState(self.metaState as u32)
}
}
}
+336 -235
View File
@@ -1,36 +1,38 @@
#![cfg(feature="game-activity")]
#![cfg(feature = "game-activity")]
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::marker::PhantomData;
use std::os::raw;
use std::ptr::NonNull;
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
use std::{thread, ptr};
use std::ops::Deref;
use std::os::unix::prelude::*;
use std::panic::catch_unwind;
use std::ptr::NonNull;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use std::{ptr, thread};
use log::{Level, error, trace};
use libc::c_void;
use log::{error, trace, Level};
use jni_sys::*;
use ndk_sys::{ALooper_wake};
use ndk_sys::ALooper_wake;
use ndk_sys::{ALooper, ALooper_pollAll};
use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
use ndk::looper::{FdEvent};
use ndk::native_window::NativeWindow;
use crate::{MainEvent, Rect, PollEvent, AndroidApp, NativeWindowRef};
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::{MotionEvent, KeyEvent, Axis, InputEvent};
use input::{Axis, InputEvent, KeyEvent, MotionEvent};
// The only time it's safe to update the android_app->savedState pointer is
// while handling a SaveState event, so this API is only exposed for those
@@ -42,23 +44,22 @@ pub struct StateSaver<'a> {
impl<'a> StateSaver<'a> {
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.ptr.as_ptr();
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() {
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");
}
@@ -72,7 +73,7 @@ impl<'a> StateSaver<'a> {
}
(*app_ptr).savedState = buf;
(*app_ptr).savedStateSize = state.len() as u64;
(*app_ptr).savedStateSize = state.len() as _;
}
}
}
@@ -84,9 +85,12 @@ pub struct StateLoader<'a> {
impl<'a> StateLoader<'a> {
pub fn load(&self) -> Option<Vec<u8>> {
unsafe {
let app_ptr = self.app.ptr.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 app_ptr = self.app.native_app.as_ptr();
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,
);
let state = buf.to_vec();
Some(state)
} else {
@@ -101,74 +105,106 @@ 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<ALooper>,
}
unsafe impl Send for AndroidAppWaker {}
unsafe impl Sync for AndroidAppWaker {}
impl AndroidAppWaker {
pub fn wake(&self) {
unsafe { ALooper_wake(self.looper.as_ptr()); }
unsafe {
ALooper_wake(self.looper.as_ptr());
}
}
}
impl AndroidApp {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>) -> Self {
// Note: we don't use from_ptr since we don't own the android_app.config
// and need to keep in mind that the Drop handler is going to call
// AConfiguration_delete()
//
// Whenever we get a ConfigChanged notification we synchronize this
// config state with a deep copy.
let config = Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config));
Self {
inner: Arc::new(AndroidAppInner {
ptr,
config: RwLock::new(config),
inner: Arc::new(RwLock::new(AndroidAppInner {
native_app: NativeAppGlue { ptr },
config: ConfigurationRef::new(config),
native_window: Default::default(),
})
})),
}
}
}
#[derive(Debug)]
pub struct AndroidAppInner {
struct NativeAppGlue {
ptr: NonNull<ffi::android_app>,
config: RwLock<Configuration>,
}
impl Deref for NativeAppGlue {
type Target = NonNull<ffi::android_app>;
fn deref(&self) -> &Self::Target {
&self.ptr
}
}
unsafe impl Send for NativeAppGlue {}
unsafe impl Sync for NativeAppGlue {}
#[derive(Debug)]
pub struct AndroidAppInner {
native_app: NativeAppGlue,
config: ConfigurationRef,
native_window: RwLock<Option<NativeWindow>>,
}
impl AndroidAppInner {
pub fn vm_as_ptr(&self) -> *mut c_void {
let app_ptr = self.native_app.as_ptr();
unsafe { (*(*app_ptr).activity).vm as _ }
}
pub fn native_window<'a>(&self) -> Option<NativeWindowRef> {
let guard = self.native_window.read().unwrap();
if let Some(ref window) = *guard {
Some(NativeWindowRef::new(window))
} else {
None
}
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()
}
pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
where F: FnMut(PollEvent)
where
F: FnMut(PollEvent),
{
trace!("poll_events");
unsafe {
let app_ptr = self.ptr;
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 timeout_milliseconds = if let Some(timeout) = timeout { timeout.as_millis() as i32 } else { -1 };
let timeout_milliseconds = if let Some(timeout) = timeout {
timeout.as_millis() as i32
} else {
-1
};
trace!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
let id = ALooper_pollAll(timeout_milliseconds, &mut fd, &mut events, &mut source as *mut *mut core::ffi::c_void);
let id = ALooper_pollAll(
timeout_milliseconds,
&mut fd,
&mut events,
&mut source as *mut *mut core::ffi::c_void,
);
match id {
ffi::ALOOPER_POLL_WAKE => {
trace!("ALooper_pollAll returned POLL_WAKE");
if ffi::android_app_input_available_wake_up(native_app.as_ptr()) {
log::debug!("Notifying Input Available");
callback(PollEvent::Main(MainEvent::InputAvailable));
}
callback(PollEvent::Wake);
}
ffi::ALOOPER_POLL_CALLBACK => {
@@ -181,57 +217,84 @@ impl AndroidAppInner {
callback(PollEvent::Timeout);
}
ffi::ALOOPER_POLL_ERROR => {
trace!("ALooper_pollAll returned POLL_ERROR");
callback(PollEvent::Error);
// Considering that this API is quite likely to be used in `android_main`
// it's rather unergonomic to require the call to unwrap a Result for each
// call to poll_events(). Alternatively we could maybe even just panic!()
// here, while it's hard to imagine practically being able to recover
//return Err(LooperError);
// 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::NativeAppGlueLooperId_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(app_ptr.as_ptr());
if !source.is_null() {
let cmd_i = ffi::android_app_read_cmd(native_app.as_ptr());
let cmd = match cmd_i as u32 {
//NativeAppGlueAppCmd_UNUSED_APP_CMD_INPUT_CHANGED => AndroidAppMainEvent::InputChanged,
ffi::NativeAppGlueAppCmd_APP_CMD_INIT_WINDOW => MainEvent::InitWindow {},
ffi::NativeAppGlueAppCmd_APP_CMD_TERM_WINDOW => MainEvent::TerminateWindow {},
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_RESIZED => MainEvent::WindowResized {},
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_REDRAW_NEEDED => MainEvent::RedrawNeeded {},
ffi::NativeAppGlueAppCmd_APP_CMD_CONTENT_RECT_CHANGED => MainEvent::ContentRectChanged,
ffi::NativeAppGlueAppCmd_APP_CMD_GAINED_FOCUS => MainEvent::GainedFocus,
ffi::NativeAppGlueAppCmd_APP_CMD_LOST_FOCUS => MainEvent::LostFocus,
ffi::NativeAppGlueAppCmd_APP_CMD_CONFIG_CHANGED => MainEvent::ConfigChanged,
ffi::NativeAppGlueAppCmd_APP_CMD_LOW_MEMORY => MainEvent::LowMemory,
ffi::NativeAppGlueAppCmd_APP_CMD_INIT_WINDOW => {
MainEvent::InitWindow {}
}
ffi::NativeAppGlueAppCmd_APP_CMD_TERM_WINDOW => {
MainEvent::TerminateWindow {}
}
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_RESIZED => {
MainEvent::WindowResized {}
}
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_REDRAW_NEEDED => {
MainEvent::RedrawNeeded {}
}
ffi::NativeAppGlueAppCmd_APP_CMD_CONTENT_RECT_CHANGED => {
MainEvent::ContentRectChanged {}
}
ffi::NativeAppGlueAppCmd_APP_CMD_GAINED_FOCUS => {
MainEvent::GainedFocus
}
ffi::NativeAppGlueAppCmd_APP_CMD_LOST_FOCUS => {
MainEvent::LostFocus
}
ffi::NativeAppGlueAppCmd_APP_CMD_CONFIG_CHANGED => {
MainEvent::ConfigChanged {}
}
ffi::NativeAppGlueAppCmd_APP_CMD_LOW_MEMORY => {
MainEvent::LowMemory
}
ffi::NativeAppGlueAppCmd_APP_CMD_START => MainEvent::Start,
ffi::NativeAppGlueAppCmd_APP_CMD_RESUME => MainEvent::Resume { loader: StateLoader { app: &self } },
ffi::NativeAppGlueAppCmd_APP_CMD_SAVE_STATE => MainEvent::SaveState { saver: StateSaver { app: &self } },
ffi::NativeAppGlueAppCmd_APP_CMD_RESUME => MainEvent::Resume {
loader: StateLoader { app: self },
},
ffi::NativeAppGlueAppCmd_APP_CMD_SAVE_STATE => {
MainEvent::SaveState {
saver: StateSaver { app: self },
}
}
ffi::NativeAppGlueAppCmd_APP_CMD_PAUSE => MainEvent::Pause,
ffi::NativeAppGlueAppCmd_APP_CMD_STOP => MainEvent::Stop,
ffi::NativeAppGlueAppCmd_APP_CMD_DESTROY => MainEvent::Destroy,
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_INSETS_CHANGED => MainEvent::InsetsChanged {},
_ => unreachable!()
ffi::NativeAppGlueAppCmd_APP_CMD_WINDOW_INSETS_CHANGED => {
MainEvent::InsetsChanged {}
}
_ => unreachable!(),
};
trace!("Read ID_MAIN command {cmd_i} = {cmd:?}");
trace!("Calling android_app_pre_exec_cmd({cmd_i})");
ffi::android_app_pre_exec_cmd(app_ptr.as_ptr(), cmd_i);
ffi::android_app_pre_exec_cmd(native_app.as_ptr(), cmd_i);
match cmd {
MainEvent::ConfigChanged => {
*self.config.write().unwrap() =
Configuration::clone_from_ptr(NonNull::new_unchecked((*app_ptr.as_ptr()).config));
MainEvent::ConfigChanged { .. } => {
self.config.replace(Configuration::clone_from_ptr(
NonNull::new_unchecked((*native_app.as_ptr()).config),
));
}
MainEvent::InitWindow { .. } => {
let win_ptr = (*app_ptr.as_ptr()).window;
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::from_ptr(NonNull::new(win_ptr).unwrap()));
Some(NativeWindow::clone_from_ptr(
NonNull::new(win_ptr).unwrap(),
));
}
MainEvent::TerminateWindow { .. } => {
*self.native_window.write().unwrap() = None;
@@ -243,16 +306,13 @@ impl AndroidAppInner {
callback(PollEvent::Main(cmd));
trace!("Calling android_app_post_exec_cmd({cmd_i})");
ffi::android_app_post_exec_cmd(app_ptr.as_ptr(), 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!");
}
}
_ => {
let events = FdEvent::from_bits(events as u32)
.expect(&format!("Spurious ALooper_pollAll event flags {:#04x}", events as u32));
trace!("Custom ALooper event source: id = {id}, fd = {fd}, events = {events:?}, data = {source:?}");
callback(PollEvent::FdEvent{ ident: id, fd: fd as RawFd, events, data: source });
error!("Ignoring spurious ALooper event source: id = {id}, fd = {fd}, events = {events:?}, data = {source:?}");
}
}
}
@@ -263,34 +323,69 @@ impl AndroidAppInner {
}
}
pub fn enable_motion_axis(&self, axis: Axis) {
pub fn set_window_flags(
&self,
add_flags: WindowManagerFlags,
remove_flags: WindowManagerFlags,
) {
unsafe {
ffi::GameActivityPointerAxes_enableAxis(axis as i32)
let activity = (*self.native_app.as_ptr()).activity;
ffi::GameActivity_setWindowFlags(activity, add_flags.bits(), remove_flags.bits())
}
}
pub fn disable_motion_axis(&self, axis: Axis) {
// TODO: move into a trait
pub fn show_soft_input(&self, show_implicit: bool) {
unsafe {
ffi::GameActivityPointerAxes_disableAxis(axis as i32)
let activity = (*self.native_app.as_ptr()).activity;
let flags = if show_implicit {
ffi::ShowImeFlags_SHOW_IMPLICIT
} else {
0
};
ffi::GameActivity_showSoftInput(activity, flags);
}
}
// TODO: move into a trait
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
unsafe {
let activity = (*self.native_app.as_ptr()).activity;
let flags = if hide_implicit_only {
ffi::HideImeFlags_HIDE_IMPLICIT_ONLY
} else {
0
};
ffi::GameActivity_hideSoftInput(activity, flags);
}
}
pub fn enable_motion_axis(&mut self, axis: Axis) {
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
}
pub fn disable_motion_axis(&mut self, axis: Axis) {
unsafe { ffi::GameActivityPointerAxes_disableAxis(axis as i32) }
}
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.ptr.as_ptr();
AndroidAppWaker { looper: NonNull::new_unchecked((*app_ptr).looper) }
let app_ptr = self.native_app.as_ptr();
AndroidAppWaker {
looper: NonNull::new_unchecked((*app_ptr).looper),
}
}
}
pub fn config(&self) -> Configuration {
self.config.read().unwrap().clone()
pub fn config(&self) -> ConfigurationRef {
self.config.clone()
}
pub fn content_rect(&self) -> Rect {
unsafe {
let app_ptr = self.ptr.as_ptr();
let app_ptr = self.native_app.as_ptr();
Rect {
left: (*app_ptr).contentRect.left,
right: (*app_ptr).contentRect.right,
@@ -302,104 +397,109 @@ impl AndroidAppInner {
pub fn asset_manager(&self) -> AssetManager {
unsafe {
let app_ptr = self.ptr.as_ptr();
let app_ptr = self.native_app.as_ptr();
let am_ptr = NonNull::new_unchecked((*(*app_ptr).activity).assetManager);
AssetManager::from_ptr(am_ptr)
}
}
pub fn input_events<'b, F>(&self, mut callback: F)
where F: FnMut(&InputEvent)
pub fn input_events<F>(&self, mut callback: F)
where
F: FnMut(&InputEvent) -> InputStatus,
{
let buf = unsafe {
let app_ptr = self.ptr.as_ptr();
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() {
if input_buffer.is_null() {
return;
}
InputBuffer::from_ptr(NonNull::new_unchecked(input_buffer))
};
for key_event in buf.key_events_iter() {
let mut keys_iter = KeyEventsLendingIterator::new(&buf);
while let Some(key_event) = keys_iter.next() {
callback(&InputEvent::KeyEvent(key_event));
}
for motion_event in buf.motion_events_iter() {
let mut motion_iter = MotionEventsLendingIterator::new(&buf);
while let Some(motion_event) = motion_iter.next() {
callback(&InputEvent::MotionEvent(motion_event));
}
}
fn try_get_path_from_ptr(path: *const u8) -> Option<std::path::PathBuf> {
if path == ptr::null() { return None; }
let cstr = unsafe {
let cstr_slice = CStr::from_ptr(path);
cstr_slice.to_str().ok()?
};
if cstr.len() == 0 { return None; }
Some(std::path::PathBuf::from(cstr))
}
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
unsafe {
let app_ptr = self.ptr.as_ptr();
Self::try_get_path_from_ptr((*(*app_ptr).activity).internalDataPath)
let app_ptr = self.native_app.as_ptr();
util::try_get_path_from_ptr((*(*app_ptr).activity).internalDataPath)
}
}
pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
unsafe {
let app_ptr = self.ptr.as_ptr();
Self::try_get_path_from_ptr((*(*app_ptr).activity).externalDataPath)
let app_ptr = self.native_app.as_ptr();
util::try_get_path_from_ptr((*(*app_ptr).activity).externalDataPath)
}
}
pub fn obb_path(&self) -> Option<std::path::PathBuf> {
unsafe {
let app_ptr = self.ptr.as_ptr();
Self::try_get_path_from_ptr((*(*app_ptr).activity).obbPath)
let app_ptr = self.native_app.as_ptr();
util::try_get_path_from_ptr((*(*app_ptr).activity).obbPath)
}
}
}
struct MotionEventsIterator<'a> {
struct MotionEventsLendingIterator<'a> {
pos: usize,
count: usize,
buffer: &'a InputBuffer<'a>
buffer: &'a InputBuffer<'a>,
}
impl<'a> Iterator for MotionEventsIterator<'a> {
type Item = MotionEvent;
fn next(&mut self) -> Option<Self::Item> {
// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
// via a generic trait. The iteration of motion events is entirely private
// though so this is ok for now.
impl<'a> MotionEventsLendingIterator<'a> {
fn new(buffer: &'a InputBuffer<'a>) -> Self {
Self {
pos: 0,
count: buffer.motion_events_count(),
buffer,
}
}
fn next(&mut self) -> Option<MotionEvent<'a>> {
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)
}
let ga_event = unsafe { &(*self.buffer.ptr.as_ptr()).motionEvents[self.pos] };
let event = MotionEvent::new(ga_event);
self.pos += 1;
Some(event)
} else {
None
}
}
}
struct KeyEventsIterator<'a> {
struct KeyEventsLendingIterator<'a> {
pos: usize,
count: usize,
buffer: &'a InputBuffer<'a>
buffer: &'a InputBuffer<'a>,
}
impl<'a> Iterator for KeyEventsIterator<'a> {
type Item = KeyEvent;
fn next(&mut self) -> Option<Self::Item> {
// A kind of lending iterator but since our MSRV is 1.60 we can't handle this
// via a generic trait. The iteration of key events is entirely private
// though so this is ok for now.
impl<'a> KeyEventsLendingIterator<'a> {
fn new(buffer: &'a InputBuffer<'a>) -> Self {
Self {
pos: 0,
count: buffer.key_events_count(),
buffer,
}
}
fn next(&mut self) -> Option<KeyEvent<'a>> {
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)
}
let ga_event = unsafe { &(*self.buffer.ptr.as_ptr()).keyEvents[self.pos] };
let event = KeyEvent::new(ga_event);
self.pos += 1;
Some(event)
} else {
None
}
@@ -408,32 +508,23 @@ impl<'a> Iterator for KeyEventsIterator<'a> {
struct InputBuffer<'a> {
ptr: NonNull<ffi::android_input_buffer>,
_lifetime: PhantomData<&'a ffi::android_input_buffer>
_lifetime: PhantomData<&'a ffi::android_input_buffer>,
}
impl<'a> InputBuffer<'a> {
pub(crate) fn from_ptr(ptr: NonNull<ffi::android_input_buffer>) -> InputBuffer<'a> {
Self {
ptr,
_lifetime: PhantomData::default()
_lifetime: PhantomData::default(),
}
}
// 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 }
}
}
@@ -467,50 +558,44 @@ extern "C" {
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(
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,
) -> jni_sys::jlong
{
Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(env, java_game_activity, path, func_name,
internal_data_dir, obb_dir, external_data_dir, jasset_mgr, saved_state)
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,
) -> jni_sys::jlong {
Java_com_google_androidgamesdk_GameActivity_loadNativeCode_C(
env,
java_game_activity,
path,
func_name,
internal_data_dir,
obb_dir,
external_data_dir,
jasset_mgr,
saved_state,
)
}
#[no_mangle]
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,
)
{
activity: *mut ffi::GameActivity,
saved_state: *mut ::std::os::raw::c_void,
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 as raw::c_int, tag.as_ptr(), msg.as_ptr());
}
}
extern "Rust" {
pub fn android_main(app: AndroidApp);
}
@@ -519,60 +604,76 @@ 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);
#[allow(unused_unsafe)] // Otherwise rust 1.64 moans about using unsafe{} in unsafe functions
pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
abort_on_panic(|| {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
thread::spawn(move || {
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
let file = File::from_raw_fd(logpipe[0]);
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
}
}
}
});
let jvm = unsafe {
let jvm = (*(*native_app).activity).vm;
let activity: jobject = (*(*native_app).activity).javaGameActivity;
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
}
jvm
};
unsafe {
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap());
// 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);
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
detach_current_thread(jvm);
}
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();
}
})
}
+83
View File
@@ -0,0 +1,83 @@
use bitflags::bitflags;
use num_enum::{IntoPrimitive, TryFromPrimitive};
pub use crate::activity_impl::input::*;
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_ANY)
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum Source {
BluetoothStylus = 0x0000c002,
Dpad = 0x00000201,
/// Either a gamepad or a joystick
Gamepad = 0x00000401,
Hdmi = 0x02000001,
/// Either a gamepad or a joystick
Joystick = 0x01000010,
/// Pretty much any device with buttons. Query the keyboard type to determine
/// if it has alphabetic keys and can be used for text entry.
Keyboard = 0x00000101,
/// A pointing device, such as a mouse or trackpad
Mouse = 0x00002002,
/// A pointing device, such as a mouse or trackpad whose relative motions should be treated as navigation events
MouseRelative = 0x00020004,
/// An input device akin to a scroll wheel
RotaryEncoder = 0x00400000,
Sensor = 0x04000000,
Stylus = 0x00004002,
Touchpad = 0x00100008,
Touchscreen = 0x00001002,
TouchNavigation = 0x00200000,
Trackball = 0x00010004,
Unknown = 0,
}
bitflags! {
struct SourceFlags: u32 {
const CLASS_MASK = 0x000000ff;
const BUTTON = 0x00000001;
const POINTER = 0x00000002;
const TRACKBALL = 0x00000004;
const POSITION = 0x00000008;
const JOYSTICK = 0x00000010;
const NONE = 0;
}
}
/// An enum representing the class of a [`MotionEvent`] or [`KeyEvent`] source
///
/// See [the InputDevice docs](https://developer.android.com/reference/android/view/InputDevice#SOURCE_CLASS_MASK)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
None,
Button,
Pointer,
Trackball,
Position,
Joystick,
}
impl From<u32> for Class {
fn from(source: u32) -> Self {
let class = SourceFlags::from_bits_truncate(source);
match class {
SourceFlags::BUTTON => Class::Button,
SourceFlags::POINTER => Class::Pointer,
SourceFlags::TRACKBALL => Class::Trackball,
SourceFlags::POSITION => Class::Position,
SourceFlags::JOYSTICK => Class::Joystick,
_ => Class::None,
}
}
}
impl From<Source> for Class {
fn from(source: Source) -> Self {
let source: u32 = source.into();
source.into()
}
}
+474 -113
View File
@@ -1,43 +1,119 @@
use std::time::Duration;
use std::{os::unix::prelude::RawFd, sync::Arc};
use std::hash::Hash;
use std::ops::Deref;
//! 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.
//!
//! # 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
//!
//! [`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 libc::c_void;
use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
// TODO: import FdEvent and avoid depending on ndk Looper abstraction in case we want to
// support using epoll directly in the future.
use ndk::looper::FdEvent;
use ndk::native_window::NativeWindow;
use bitflags::bitflags;
#[cfg(not(target_os = "android"))]
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");
#[cfg(not(any(feature = "game-activity", feature = "native-activity")))]
compile_error!("Either \"game-activity\" or \"native-activity\" must be enabled as features");
compile_error!(
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
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)
which is not supported.
#[cfg(feature="native-activity")]
Since android-activity is responsible for the `android_main` entrypoint of your application
then there can only be a single implementation of android-activity linked with your application.
You can use `cargo tree` (e.g. via `cargo ndk -t arm64-v8a tree`) to identify why multiple
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."#
);
#[cfg(any(feature = "native-activity", doc))]
mod native_activity;
#[cfg(feature="native-activity")]
#[cfg(any(feature = "native-activity", doc))]
use native_activity as activity_impl;
#[cfg(feature="game-activity")]
#[cfg(feature = "game-activity")]
mod game_activity;
#[cfg(feature="game-activity")]
#[cfg(feature = "game-activity")]
use game_activity as activity_impl;
pub use activity_impl::input;
pub mod input;
// 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 config;
pub use config::ConfigurationRef;
mod util;
/// 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,
@@ -46,56 +122,66 @@ pub struct Rect {
pub bottom: i32,
}
// XXX: NativeWindow is a ref-counted object but the NativeWindow rust API
// doesn't currently implement Clone() in terms of acquiring a reference
// and Drop() in terms of releasing a reference. NativeWindowRef lets
// us expose a pointer to a NativeWindow more safely by ensuring it won't
// become invalid, even it it gets 'terminated'.
/// A reference to a `NativeWindow`, used for rendering
pub struct NativeWindowRef {
inner: NativeWindow
}
impl NativeWindowRef {
pub fn new(native_window: &NativeWindow) -> Self {
unsafe { ndk_sys::ANativeWindow_acquire(native_window.ptr().as_ptr()); }
Self { inner: native_window.clone() }
}
}
impl Drop for NativeWindowRef {
fn drop(&mut self) {
unsafe { ndk_sys::ANativeWindow_release(self.inner.ptr().as_ptr()) }
}
}
impl Deref for NativeWindowRef {
type Target = NativeWindow;
fn deref(&self) -> &Self::Target {
&self.inner
impl Rect {
/// An empty `Rect` with all components set to zero.
pub fn empty() -> Self {
Self {
left: 0,
top: 0,
right: 0,
bottom: 0,
}
}
}
pub type StateSaver<'a> = activity_impl::StateSaver<'a>;
pub type StateLoader<'a> = activity_impl::StateLoader<'a>;
impl From<Rect> for ndk_sys::ARect {
fn from(rect: Rect) -> Self {
Self {
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
}
}
}
impl From<ndk_sys::ARect> for Rect {
fn from(arect: ndk_sys::ARect) -> Self {
Self {
left: arect.left,
right: arect.right,
top: arect.top,
bottom: arect.bottom,
}
}
}
pub use activity_impl::StateLoader;
pub use activity_impl::StateSaver;
/// An application event delivered during [`AndroidApp::poll_events`]
#[non_exhaustive]
#[derive(Debug)]
pub enum MainEvent<'a> {
/**
* Unused. Reserved for future use when usage of AInputQueue will be
* supported.
*/
//InputChanged,
/// New input events are available via [`AndroidApp::input_events()`]
///
/// _Note: Even if more input is received this event will not be resent
/// until [`AndroidApp::input_events()`] 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
InputAvailable,
/// Command from main thread: a new [`NativeWindow`] is ready for use. Upon
/// receiving this command, [`native_window()`] will return the new window
/// receiving this command, [`AndroidApp::native_window()`] will return the new window
#[non_exhaustive]
InitWindow { },
InitWindow {},
/// Command from main thread: the existing [`NativeWindow`] needs to be
/// terminated. Upon receiving this command, [`native_window()`] still
/// terminated. Upon receiving this command, [`AndroidApp::native_window()`] still
/// returns the existing window; after returning from the [`AndroidApp::poll_events()`]
/// callback then [`native_window()`] will return `None`.
/// callback then [`AndroidApp::native_window()`] will return `None`.
#[non_exhaustive]
TerminateWindow {},
@@ -114,7 +200,8 @@ pub enum MainEvent<'a> {
/// 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
/// get the new content rect by calling [`AndroidApp::content_rect()`]
ContentRectChanged,
#[non_exhaustive]
ContentRectChanged {},
/// Command from main thread: the app's activity window has gained
/// input focus.
@@ -125,9 +212,10 @@ pub enum MainEvent<'a> {
LostFocus,
/// Command from main thread: the current device configuration has changed.
/// You can get a copy of the latest [Configuration] by calling
/// You can get a copy of the latest [`ndk::configuration::Configuration`] by calling
/// [`AndroidApp::config()`]
ConfigChanged,
#[non_exhaustive]
ConfigChanged {},
/// Command from main thread: the system is running low on memory.
/// Try to reduce your memory use.
@@ -163,26 +251,203 @@ pub enum MainEvent<'a> {
InsetsChanged {},
}
/// An event delivered during [`AndroidApp::poll_events`]
#[derive(Debug)]
#[non_exhaustive]
pub enum PollEvent<'a> {
Wake,
Timeout,
Main(MainEvent<'a>),
}
#[non_exhaustive]
FdEvent { ident: i32, fd: RawFd, events: FdEvent, data: *mut std::ffi::c_void },
Error
/// 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)
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
/// independently, or in combination with
/// [`Self::KEEP_SCREEN_ON`] and/or [`Self::SHOW_WHEN_LOCKED`]
const ALLOW_LOCK_WHILE_SCREEN_ON = 0x00000001;
/// Everything behind this window will be dimmed. */
const DIM_BEHIND = 0x00000002;
/// Blur everything behind this window.
#[deprecated = "Blurring is no longer supported"]
const BLUR_BEHIND = 0x00000004;
/// This window won't ever get key input focus, so the
/// user can not send key or other button events to it. Those will
/// instead go to whatever focusable window is behind it. This flag
/// will also enable [`Self::NOT_TOUCH_MODAL`] whether or not
/// that is explicitly set.
///
/// Setting this flag also implies that the window will not need to
/// interact with
/// a soft input method, so it will be Z-ordered and positioned
/// independently of any active input method (typically this means it
/// gets Z-ordered on top of the input method, so it can use the full
/// screen for its content and cover the input method if needed. You
/// can use [`Self::ALT_FOCUSABLE_IM`] to modify this
/// behavior.
const NOT_FOCUSABLE = 0x00000008;
/// This window can never receive touch events.
const NOT_TOUCHABLE = 0x00000010;
/// Even when this window is focusable (if
/// [`Self::NOT_FOCUSABLE`] is not set), allow any pointer
/// events outside of the window to be sent to the windows behind it.
/// Otherwise it will consume all pointer events itself, regardless of
/// whether they are inside of the window.
const NOT_TOUCH_MODAL = 0x00000020;
/// When set, if the device is asleep when the touch
/// screen is pressed, you will receive this first touch event. Usually
/// the first touch event is consumed by the system since the user can
/// not see what they are pressing on.
#[deprecated]
const TOUCHABLE_WHEN_WAKING = 0x00000040;
/// As long as this window is visible to the user, keep
/// the device's screen turned on and bright.
const KEEP_SCREEN_ON = 0x00000080;
/// Place the window within the entire screen, ignoring
/// decorations around the border (such as the status bar). The
/// window must correctly position its contents to take the screen
/// decoration into account.
const LAYOUT_IN_SCREEN = 0x00000100;
/// Allows the window to extend outside of the screen.
const LAYOUT_NO_LIMITS = 0x00000200;
/// Hide all screen decorations (such as the status
/// bar) while this window is displayed. This allows the window to
/// use the entire display space for itself -- the status bar will
/// be hidden when an app window with this flag set is on the top
/// layer. A fullscreen window will ignore a value of
/// [`Self::SOFT_INPUT_ADJUST_RESIZE`] the window will stay
/// fullscreen and will not resize.
const FULLSCREEN = 0x00000400;
/// Override [`Self::FULLSCREEN`] and force the
/// screen decorations (such as the status bar) to be shown.
const FORCE_NOT_FULLSCREEN = 0x00000800;
/// Turn on dithering when compositing this window to
/// the screen.
#[deprecated="This flag is no longer used"]
const DITHER = 0x00001000;
/// Treat the content of the window as secure, preventing
/// it from appearing in screenshots or from being viewed on non-secure
/// displays.
const SECURE = 0x00002000;
/// A special mode where the layout parameters are used
/// to perform scaling of the surface when it is composited to the
/// screen.
const SCALED = 0x00004000;
/// Intended for windows that will often be used when the user is
/// holding the screen against their face, it will aggressively
/// filter the event stream to prevent unintended presses in this
/// situation that may not be desired for a particular window, when
/// such an event stream is detected, the application will receive
/// a `AMOTION_EVENT_ACTION_CANCEL` to indicate this so
/// applications can handle this accordingly by taking no action on
/// the event until the finger is released.
const IGNORE_CHEEK_PRESSES = 0x00008000;
/// A special option only for use in combination with
/// [`Self::LAYOUT_IN_SCREEN`]. When requesting layout in
/// the screen your window may appear on top of or behind screen decorations
/// such as the status bar. By also including this flag, the window
/// manager will report the inset rectangle needed to ensure your
/// content is not covered by screen decorations.
const LAYOUT_INSET_DECOR = 0x00010000;
/// Invert the state of [`Self::NOT_FOCUSABLE`] with
/// respect to how this window interacts with the current method.
/// That is, if [`Self::NOT_FOCUSABLE`] is set and this flag is set,
/// then the window will behave as if it needs to interact with the
/// input method and thus be placed behind/away from it; if
/// [`Self::NOT_FOCUSABLE`] is not set and this flag is set,
/// then the window will behave as if it doesn't need to interact
/// with the input method and can be placed to use more space and
/// cover the input method.
const ALT_FOCUSABLE_IM = 0x00020000;
/// If you have set [`Self::NOT_TOUCH_MODAL`], you
/// can set this flag to receive a single special MotionEvent with
/// the action
/// `AMOTION_EVENT_ACTION_OUTSIDE` for
/// touches that occur outside of your window. Note that you will not
/// receive the full down/move/up gesture, only the location of the
/// first down as an `AMOTION_EVENT_ACTION_OUTSIDE`.
const WATCH_OUTSIDE_TOUCH = 0x00040000;
/// Special flag to let windows be shown when the screen
/// is locked. This will let application windows take precedence over
/// key guard or any other lock screens. Can be used with
/// [`Self::KEEP_SCREEN_ON`] to turn screen on and display
/// windows directly before showing the key guard window. Can be used with
/// [`Self::DISMISS_KEYGUARD`] to automatically fully
/// dismiss non-secure key guards. This flag only applies to the top-most
/// full-screen window.
const SHOW_WHEN_LOCKED = 0x00080000;
/// Ask that the system wallpaper be shown behind
/// your window. The window surface must be translucent to be able
/// to actually see the wallpaper behind it; this flag just ensures
/// that the wallpaper surface will be there if this window actually
/// has translucent regions.
const SHOW_WALLPAPER = 0x00100000;
/// When set as a window is being added or made
/// visible, once the window has been shown then the system will
/// poke the power manager's user activity (as if the user had woken
/// up the device) to turn the screen on.
const TURN_SCREEN_ON = 0x00200000;
/// When set the window will cause the key guard to
/// be dismissed, only if it is not a secure lock key guard. Because such
/// a key guard is not needed for security, it will never re-appear if
/// the user navigates to another window (in contrast to
/// [`Self::SHOW_WHEN_LOCKED`], which will only temporarily
/// hide both secure and non-secure key guards but ensure they reappear
/// when the user moves to another UI that doesn't hide them).
/// If the key guard is currently active and is secure (requires an
/// unlock pattern) then the user will still need to confirm it before
/// seeing this window, unless [`Self::SHOW_WHEN_LOCKED`] has
/// also been set.
const DISMISS_KEYGUARD = 0x00400000;
}
}
/// 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.
///
#[derive(Debug, Clone)]
pub struct AndroidApp {
pub(crate) inner: Arc<AndroidAppInner>
pub(crate) inner: Arc<RwLock<AndroidAppInner>>,
}
impl PartialEq for AndroidApp {
@@ -199,23 +464,60 @@ 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.native_activity()
}
/// Queries the current [`NativeWindow`] for the application.
///
/// This will only return `Some(window)` between
/// [`AndroidAppMainEvent::InitWindow`] and [`AndroidAppMainEvent::TerminateWindow`]
/// [`MainEvent::InitWindow`] and [`MainEvent::TerminateWindow`]
/// events.
pub fn native_window<'a>(&self) -> Option<NativeWindowRef> {
self.inner.native_window()
pub fn native_window(&self) -> Option<NativeWindow> {
self.inner.read().unwrap().native_window()
}
/// Calls [`ALooper_pollAll`] on the looper associated with this AndroidApp as well
/// as processing any events (such as lifecycle events) via the given `callback`.
/// 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
/// some events require pre- and post-processing either side of the callback. For correct
@@ -226,62 +528,114 @@ impl AndroidApp {
/// main thread. The [`MainEvent::SaveState`] event is also synchronized with the
/// Java main thread.
///
/// # Safety
/// This API must only be called from the application's 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)
where
F: FnMut(PollEvent),
{
self.inner.poll_events(timeout, callback);
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 [`poll_events()`].
///
/// Internally this uses [`ALooper_wake`] on the looper associated with this
/// [AndroidApp].
///
/// # Safety
/// This API can be used from any thread
/// events within [`AndroidApp::poll_events()`].
pub fn create_waker(&self) -> activity_impl::AndroidAppWaker {
self.inner.create_waker()
self.inner.read().unwrap().create_waker()
}
/// Returns a deep copy of this application's [`Configuration`]
pub fn config(&self) -> Configuration {
self.inner.config()
/// Returns a (cheaply clonable) reference to this application's [`ndk::configuration::Configuration`]
pub fn config(&self) -> ConfigurationRef {
self.inner.read().unwrap().config()
}
/// Queries the current content rectangle of the window; this is the area where the
/// window's content should be placed to be seen by the user.
///
/// # Safety
/// This API must only be called from the applications main thread
pub fn content_rect(&self) -> Rect {
self.inner.content_rect()
self.inner.read().unwrap().content_rect()
}
/// Queries the Asset Manager instance for the application.
///
/// Use this to access binary assets bundled inside your application's .apk file.
///
/// # Safety
/// This API must only be called from the applications main thread
pub fn asset_manager(&self) -> AssetManager {
self.inner.asset_manager()
self.inner.read().unwrap().asset_manager()
}
/// Change the window flags of the given activity.
///
/// Note that some flags must be set before the window decoration is created,
/// see
/// `<https://developer.android.com/reference/android/view/Window#setFlags(int,%20int)>`.
pub fn set_window_flags(
&self,
add_flags: WindowManagerFlags,
remove_flags: WindowManagerFlags,
) {
self.inner
.write()
.unwrap()
.set_window_flags(add_flags, remove_flags);
}
/// Enable additional input axis
///
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// and other axis should be enabled explicitly.
pub fn enable_motion_axis(&self, axis: input::Axis) {
self.inner.enable_motion_axis(axis);
self.inner.write().unwrap().enable_motion_axis(axis);
}
/// Disable input axis
///
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// and other axis should be enabled explicitly.
pub fn disable_motion_axis(&self, axis: input::Axis) {
self.inner.disable_motion_axis(axis);
self.inner.write().unwrap().disable_motion_axis(axis);
}
pub fn input_events<'b, F>(&self, callback: F)
where F: FnMut(&input::InputEvent)
/// Explicitly request that the current input method's soft input area be
/// shown to the user, if needed.
///
/// Call this if the user interacts with your view in such a way that they
/// have expressed they would like to start performing input into it.
pub fn show_soft_input(&self, show_implicit: bool) {
self.inner.read().unwrap().show_soft_input(show_implicit);
}
/// Request to hide the soft input window from the context of the window
/// that is currently accepting input.
///
/// This should be called as a result of the user doing some action that
/// fairly explicitly requests to have the input window hidden.
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
self.inner
.read()
.unwrap()
.hide_soft_input(hide_implicit_only);
}
/// Query and process all out-standing input event
///
/// `callback` should return [`InputStatus::Unhandled`] for any input events that aren't directly
/// handled by the application, or else [`InputStatus::Handled`]. Unhandled events may lead to a
/// fallback interpretation of the event.
///
/// Applications are 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._
///
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
pub fn input_events<F>(&self, callback: F)
where
F: FnMut(&input::InputEvent) -> InputStatus,
{
self.inner.input_events(callback);
self.inner.read().unwrap().input_events(callback)
}
/// The user-visible SDK version of the framework
@@ -290,7 +644,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");
}
@@ -298,16 +653,22 @@ impl AndroidApp {
/// Path to this application's internal data directory
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
self.inner.internal_data_path()
self.inner.read().unwrap().internal_data_path()
}
/// Path to this application's external data directory
pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
self.inner.external_data_path()
self.inner.read().unwrap().external_data_path()
}
/// Path to the directory containing the application's OBB files (if any).
pub fn obb_path(&self) -> Option<std::path::PathBuf> {
self.inner.obb_path()
self.inner.read().unwrap().obb_path()
}
}
}
#[test]
fn test_app_is_send_sync() {
fn needs_send_sync<T: Send + Sync>() {}
needs_send_sync::<AndroidApp>();
}
@@ -1,34 +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::{ALooper, AConfiguration, AInputQueue};
#[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,918 @@
//! 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::{
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()
}
}
#[derive(Debug)]
pub struct NativeActivityState {
pub msg_read: libc::c_int,
pub msg_write: libc::c_int,
pub config: super::ConfigurationRef,
pub saved_state: Vec<u8>,
pub input_queue: *mut ndk_sys::AInputQueue,
pub window: Option<NativeWindow>,
pub content_rect: ndk_sys::ARect,
pub activity_state: State,
pub destroy_requested: bool,
pub running: bool,
pub app_has_saved_state: bool,
pub destroyed: bool,
pub redraw_needed: bool,
pub pending_input_queue: *mut ndk_sys::AInputQueue,
pub pending_window: Option<NativeWindow>,
}
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();
guard.destroyed = true;
self.cond.notify_one();
}
}
}
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 as _)
};
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,
running: false,
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();
unsafe {
guard.write_cmd(AppCmd::Destroy);
while !guard.destroyed {
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() as _;
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.running = true;
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 ndk_sys::size_t,
) -> *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 as ndk_sys::size_t;
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]
#[allow(unused_unsafe)] // Otherwise rust 1.64 moans about using unsafe{} in unsafe functions
extern "C" fn ANativeActivity_onCreate(
activity: *mut ndk_sys::ANativeActivity,
saved_state: *const libc::c_void,
saved_state_size: libc::size_t,
) {
abort_on_panic(|| {
// Maybe make this stdout/stderr redirection an optional / opt-in feature?...
unsafe {
let mut logpipe: [RawFd; 2] = Default::default();
libc::pipe(logpipe.as_mut_ptr());
libc::dup2(logpipe[1], libc::STDOUT_FILENO);
libc::dup2(logpipe[1], libc::STDERR_FILENO);
std::thread::spawn(move || {
let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap();
let file = File::from_raw_fd(logpipe[0]);
let mut reader = BufReader::new(file);
let mut buffer = String::new();
loop {
buffer.clear();
if let Ok(len) = reader.read_line(&mut buffer) {
if len == 0 {
break;
} else if let Ok(msg) = CString::new(buffer.clone()) {
android_log(Level::Info, tag, &msg);
}
}
}
});
}
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 = unsafe {
let na = activity;
let jvm = (*na).vm;
let activity = (*na).clazz; // Completely bogus name; this is the _instance_ not class pointer
ndk_context::initialize_android_context(jvm.cast(), activity.cast());
// Since this is a newly spawned thread then the JVM hasn't been attached
// to the thread yet. Attach before calling the applications main function
// so they can safely make JNI calls
let mut jenv_out: *mut core::ffi::c_void = std::ptr::null_mut();
if let Some(attach_current_thread) = (*(*jvm)).AttachCurrentThread {
attach_current_thread(jvm, &mut jenv_out, std::ptr::null_mut());
}
jvm
};
let app = AndroidApp::new(rust_glue.clone());
rust_glue.notify_main_thread_running();
unsafe {
// We want to specifically catch any panic from the application's android_main
// so we can finish + destroy the Activity gracefully via the JVM
catch_unwind(|| {
// XXX: If we were in control of the Java Activity subclass then
// we could potentially run the android_main function via a Java native method
// springboard (e.g. call an Activity subclass method that calls a jni native
// method that then just calls android_main()) that would make sure there was
// a Java frame at the base of our call stack which would then be recognised
// when calling FindClass to lookup a suitable classLoader, instead of
// defaulting to the system loader. Without this then it's difficult for native
// code to look up non-standard Java classes.
android_main(app);
})
.unwrap_or_else(|panic| log_panic(panic));
// Let JVM know that our Activity can be destroyed before detaching from the JVM
//
// "Note that this method can be called from any thread; it will send a message
// to the main thread of the process where the Java finish call will take place"
ndk_sys::ANativeActivity_finish(activity);
if let Some(detach_current_thread) = (*(*jvm)).DetachCurrentThread {
detach_current_thread(jvm);
}
ndk_context::release_android_context();
}
});
// Wait for thread to start.
let mut guard = jvm_glue.mutex.lock().unwrap();
while !guard.running {
guard = jvm_glue.cond.wait(guard).unwrap();
}
})
}
@@ -0,0 +1,340 @@
use std::marker::PhantomData;
pub use ndk::event::{
Axis, ButtonState, EdgeFlags, KeyAction, KeyEventFlags, Keycode, MetaState, MotionAction,
MotionEventFlags, Pointer, PointersIter,
};
use crate::input::{Class, Source};
/// 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 not exactly match the ndk crate's
// `Source` enum.
let source =
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
source.try_into().unwrap_or(Source::Unknown)
}
/// Get the class of the event source.
///
#[inline]
pub fn class(&self) -> Class {
Class::from(self.source())
}
/// 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 {
self.ndk_event.action()
}
/// 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<'_> {
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<'_> {
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()
}
/// 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()
}
/// 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()
}
/// 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()
}
/* 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 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 not exactly match the ndk crate's
// `Source` enum.
let source =
unsafe { ndk_sys::AInputEvent_getSource(self.ndk_event.ptr().as_ptr()) as u32 };
source.try_into().unwrap_or(Source::Unknown)
}
/// Get the class of the event source.
///
#[inline]
pub fn class(&self) -> Class {
Class::from(self.source())
}
/// 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 {
self.ndk_event.action()
}
/// 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 {
self.ndk_event.key_code()
}
/// 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()
}
}
// 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>),
}
+268 -354
View File
@@ -1,292 +1,268 @@
#![cfg(feature="native-activity")]
#![cfg(any(feature = "native-activity", doc))]
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::os::raw;
use std::ptr;
use std::ptr::NonNull;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use std::{thread, ptr};
use std::os::unix::prelude::*;
use log::{Level, error, info, trace};
use libc::c_void;
use log::{error, trace};
use ndk::{asset::AssetManager, native_window::NativeWindow};
use ndk_sys::ALooper_wake;
use ndk_sys::{ALooper, ALooper_pollAll};
use crate::{
util, AndroidApp, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags,
};
use ndk::asset::AssetManager;
use ndk::configuration::Configuration;
use ndk::input_queue::InputQueue;
use ndk::looper::{FdEvent};
use ndk::native_window::NativeWindow;
pub mod input;
use crate::{MainEvent, Rect, PollEvent, AndroidApp, NativeWindowRef};
mod glue;
use self::glue::NativeActivityGlue;
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;
mod ffi;
pub mod input {
pub use ndk::event::{
InputEvent, Source, MetaState,
MotionEvent, Pointer, MotionAction, Axis, ButtonState, EdgeFlags, MotionEventFlags,
KeyEvent, KeyAction, Keycode, KeyEventFlags,
};
}
// 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.ptr.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 u64;
}
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.ptr.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()); }
unsafe {
ndk_sys::ALooper_wake(self.looper.as_ptr());
}
}
}
impl AndroidApp {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>) -> AndroidApp {
pub(crate) fn new(native_activity: NativeActivityGlue) -> Self {
let app = Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
native_activity,
looper: Looper {
ptr: ptr::null_mut(),
},
})),
};
// 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()
//
// Whenever we get a ConfigChanged notification we synchronize this
// config state with a deep copy.
let config = Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config));
{
let mut guard = app.inner.write().unwrap();
AndroidApp {
inner: Arc::new(AndroidAppInner {
ptr,
config: RwLock::new(config),
native_window: Default::default()
})
let main_fd = guard.native_activity.cmd_read_fd();
unsafe {
guard.looper.ptr = ndk_sys::ALooper_prepare(
ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int,
);
ndk_sys::ALooper_addFd(
guard.looper.ptr,
main_fd,
LOOPER_ID_MAIN,
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
None,
//&mut guard.cmd_poll_source as *mut _ as *mut _);
ptr::null_mut(),
);
}
}
app
}
}
#[derive(Debug)]
struct Looper {
pub ptr: *mut ndk_sys::ALooper,
}
unsafe impl Send for Looper {}
unsafe impl Sync for Looper {}
#[derive(Debug)]
pub(crate) struct AndroidAppInner {
ptr: NonNull<ffi::android_app>,
config: RwLock<Configuration>,
native_window: RwLock<Option<NativeWindow>>,
pub(crate) native_activity: NativeActivityGlue,
looper: Looper,
}
impl AndroidAppInner {
pub(crate) fn native_activity(&self) -> *const ndk_sys::ANativeActivity {
unsafe {
let app_ptr = self.ptr.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<NativeWindowRef> {
let guard = self.native_window.read().unwrap();
if let Some(ref window) = *guard {
Some(NativeWindowRef::new(window))
} else {
None
}
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)
where
F: FnMut(PollEvent),
{
trace!("poll_events");
unsafe {
let app_ptr = self.ptr;
let mut fd: i32 = 0;
let mut events: i32 = 0;
let mut source: *mut core::ffi::c_void = ptr::null_mut();
let timeout_milliseconds = if let Some(timeout) = timeout { timeout.as_millis() as i32 } else { -1 };
info!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
let id = ALooper_pollAll(timeout_milliseconds, &mut fd, &mut events, &mut source as *mut *mut core::ffi::c_void);
info!("pollAll id = {id}");
let timeout_milliseconds = if let Some(timeout) = timeout {
timeout.as_millis() as i32
} else {
-1
};
trace!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}");
assert!(
!ndk_sys::ALooper_forThread().is_null(),
"Application tried to poll events from non-main thread"
);
let id = ndk_sys::ALooper_pollAll(
timeout_milliseconds,
&mut fd,
&mut events,
&mut source as *mut *mut core::ffi::c_void,
);
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 => {
trace!("ALooper_pollAll returned POLL_ERROR");
callback(PollEvent::Error);
// Considering that this API is quite likely to be used in `android_main`
// it's rather unergonomic to require the call to unwrap a Result for each
// call to poll_events(). Alternatively we could maybe even just panic!()
// here, while it's hard to imagine practically being able to recover
//return Err(LooperError);
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(app_ptr.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 => Some(MainEvent::WindowResized {}),
ffi::APP_CMD_WINDOW_REDRAW_NEEDED => Some(MainEvent::RedrawNeeded {}),
ffi::APP_CMD_CONTENT_RECT_CHANGED => Some(MainEvent::ContentRectChanged),
ffi::APP_CMD_GAINED_FOCUS => Some(MainEvent::GainedFocus),
ffi::APP_CMD_LOST_FOCUS => Some(MainEvent::LostFocus),
ffi::APP_CMD_CONFIG_CHANGED => 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 } }),
ffi::APP_CMD_SAVE_STATE => 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::InitWindow => Some(MainEvent::InitWindow {}),
glue::AppCmd::TermWindow => Some(MainEvent::TerminateWindow {}),
glue::AppCmd::WindowResized => {
Some(MainEvent::WindowResized {})
}
glue::AppCmd::WindowRedrawNeeded => {
Some(MainEvent::RedrawNeeded {})
}
glue::AppCmd::ContentRectChanged => {
Some(MainEvent::ContentRectChanged {})
}
glue::AppCmd::GainedFocus => Some(MainEvent::GainedFocus),
glue::AppCmd::LostFocus => Some(MainEvent::LostFocus),
glue::AppCmd::ConfigChanged => {
Some(MainEvent::ConfigChanged {})
}
glue::AppCmd::LowMemory => Some(MainEvent::LowMemory),
glue::AppCmd::Start => Some(MainEvent::Start),
glue::AppCmd::Resume => Some(MainEvent::Resume {
loader: StateLoader { app: self },
}),
glue::AppCmd::SaveState => Some(MainEvent::SaveState {
saver: StateSaver { app: self },
}),
glue::AppCmd::Pause => Some(MainEvent::Pause),
glue::AppCmd::Stop => Some(MainEvent::Stop),
glue::AppCmd::Destroy => Some(MainEvent::Destroy),
};
trace!("Calling android_app_pre_exec_cmd({cmd_i})");
ffi::android_app_pre_exec_cmd(app_ptr.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.write().unwrap() =
Configuration::clone_from_ptr(NonNull::new_unchecked((*app_ptr.as_ptr()).config));
}
MainEvent::InitWindow { .. } => {
let win_ptr = (*app_ptr.as_ptr()).window;
*self.native_window.write().unwrap() =
Some(NativeWindow::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(app_ptr.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");
// For now we don't forward notifications of input events specifically, we just
// forward the notifications as a wake up, and assume the application main loop
// will unconditionally check events for each iteration of it's event loop
//
// (Specifically notifying when input events are received would be inconsistent
// with the current design of GameActivity input handling which we want to stay
// compatible with))
//
// XXX: Actually it was a bad idea to emit a Wake for input since applications
// are likely to _not_ consider that on its own a cause to redraw and it could
// end up spamming enough wake ups to interfere with other events that would
// trigger a redraw + input handling
//callback(PollEvent::Wake);
// To avoid spamming the application with event loop iterations notifying them of
// input events then we only send one `InputAvailable` per iteration of input
// handling. We re-attach the looper when the application calls
// `AndroidApp::input_events()`
self.native_activity.detach_input_queue_from_looper();
callback(PollEvent::Main(MainEvent::InputAvailable))
}
_ => {
let events = FdEvent::from_bits(events as u32)
.expect(&format!("Spurious ALooper_pollAll event flags {:#04x}", events as u32));
trace!("Custom ALooper event source: id = {id}, fd = {fd}, events = {events:?}, data = {source:?}");
callback(PollEvent::FdEvent{ ident: id, fd: fd as RawFd, events, data: source });
error!("Ignoring spurious ALooper event source: id = {id}, fd = {fd}, events = {events:?}, data = {source:?}");
}
}
}
@@ -299,37 +275,72 @@ 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.ptr.as_ptr();
AndroidAppWaker { looper: NonNull::new_unchecked((*app_ptr).looper) }
}
}
pub fn config(&self) -> Configuration {
self.config.read().unwrap().clone()
}
pub fn content_rect(&self) -> Rect {
unsafe {
let app_ptr = self.ptr.as_ptr();
Rect {
left: (*app_ptr).contentRect.left,
right: (*app_ptr).contentRect.right,
top: (*app_ptr).contentRect.top,
bottom: (*app_ptr).contentRect.bottom,
// From the application's pov we assume the looper pointer has a static
// lifetimes and we can safely assume it is never NULL.
AndroidAppWaker {
looper: NonNull::new_unchecked(self.looper.ptr),
}
}
}
pub fn config(&self) -> ConfigurationRef {
self.native_activity.config()
}
pub fn content_rect(&self) -> Rect {
self.native_activity.content_rect()
}
pub fn asset_manager(&self) -> AssetManager {
unsafe {
let app_ptr = self.ptr.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)
}
}
pub fn set_window_flags(
&self,
add_flags: WindowManagerFlags,
remove_flags: WindowManagerFlags,
) {
let na = self.native_activity();
let na_mut = na as *mut ndk_sys::ANativeActivity;
unsafe {
ndk_sys::ANativeActivity_setWindowFlags(
na_mut.cast(),
add_flags.bits(),
remove_flags.bits(),
);
}
}
// TODO: move into a trait
pub fn show_soft_input(&self, show_implicit: bool) {
let na = self.native_activity();
unsafe {
let flags = if show_implicit {
ndk_sys::ANATIVEACTIVITY_SHOW_SOFT_INPUT_IMPLICIT
} else {
0
};
ndk_sys::ANativeActivity_showSoftInput(na as *mut _, flags);
}
}
// TODO: move into a trait
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
let na = self.native_activity();
unsafe {
let flags = if hide_implicit_only {
ndk_sys::ANATIVEACTIVITY_HIDE_SOFT_INPUT_IMPLICIT_ONLY
} else {
0
};
ndk_sys::ANativeActivity_hideSoftInput(na as *mut _, flags);
}
}
pub fn enable_motion_axis(&self, _axis: input::Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
@@ -338,162 +349,65 @@ impl AndroidAppInner {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
pub fn input_events<'b, F>(&self, mut callback: F)
where F: FnMut(&input::InputEvent)
pub fn input_events<F>(&self, mut callback: F)
where
F: FnMut(&input::InputEvent) -> InputStatus,
{
let queue = unsafe {
let app_ptr = self.ptr.as_ptr();
if (*app_ptr).inputQueue == ptr::null_mut() {
return;
}
let queue = NonNull::new_unchecked((*app_ptr).inputQueue);
InputQueue::from_ptr(queue)
// Get the InputQueue for the NativeActivity (if there is one) and also ensure
// the queue is re-attached to our event Looper (so new input events will again
// trigger a wake up)
let queue = self
.native_activity
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
let queue = match queue {
Some(queue) => queue,
None => return,
};
info!("collect_events: START");
while let Some(event) = queue.get_event() {
info!("Got input event {event:?}");
if let Some(event) = queue.pre_dispatch(event) {
trace!("Pre dispatched input event {event:?}");
// Note: we basically ignore errors from get_event() currently. Looking
// at the source code for Android's InputQueue, the only error that
// can be returned here is 'WOULD_BLOCK', which we want to just treat as
// meaning the queue is empty.
//
// ref: https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/jni/android_view_InputQueue.cpp
//
while let Ok(Some(event)) = queue.get_event() {
if let Some(ndk_event) = queue.pre_dispatch(event) {
let event = match ndk_event {
ndk::event::InputEvent::MotionEvent(e) => {
input::InputEvent::MotionEvent(input::MotionEvent::new(e))
}
ndk::event::InputEvent::KeyEvent(e) => {
input::InputEvent::KeyEvent(input::KeyEvent::new(e))
}
};
let handled = callback(&event);
callback(&event);
// 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.
info!("Finishing input event {event:?}");
queue.finish_event(event, true);
let ndk_event = match event {
input::InputEvent::MotionEvent(e) => {
ndk::event::InputEvent::MotionEvent(e.into_ndk_event())
}
input::InputEvent::KeyEvent(e) => {
ndk::event::InputEvent::KeyEvent(e.into_ndk_event())
}
};
queue.finish_event(ndk_event, matches!(handled, InputStatus::Handled));
}
}
}
fn try_get_path_from_ptr(path: *const u8) -> Option<std::path::PathBuf> {
if path == ptr::null() { return None; }
let cstr = unsafe {
let cstr_slice = CStr::from_ptr(path);
cstr_slice.to_str().ok()?
};
if cstr.len() == 0 { return None; }
Some(std::path::PathBuf::from(cstr))
}
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
let na = self.native_activity();
unsafe { Self::try_get_path_from_ptr((*na).internalDataPath.cast()) }
unsafe { util::try_get_path_from_ptr((*na).internalDataPath) }
}
pub fn external_data_path(&self) -> Option<std::path::PathBuf> {
let na = self.native_activity();
unsafe { Self::try_get_path_from_ptr((*na).externalDataPath.cast()) }
unsafe { util::try_get_path_from_ptr((*na).externalDataPath) }
}
pub fn obb_path(&self) -> Option<std::path::PathBuf> {
let na = self.native_activity();
unsafe { Self::try_get_path_from_ptr((*na).obbPath.cast()) }
unsafe { util::try_get_path_from_ptr((*na).obbPath) }
}
}
// 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,
);
}
#[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 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);
}
}
}
});
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();
}
+70
View File
@@ -0,0 +1,70 @@
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.is_null() {
return None;
}
let cstr = unsafe {
let cstr_slice = CStr::from_ptr(path.cast());
cstr_slice.to_str().ok()?
};
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();
})
}
-22
View File
@@ -1,22 +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>
-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>
-53
View File
@@ -1,53 +0,0 @@
[package]
name = "agdk-egui"
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.26"
wgpu = "0.12.0"
pollster = "0.2"
egui = "0.18"
egui-wgpu = { version = "0.18", features = [ "winit" ] }
egui-winit = { version = "0.18", default-features = false }
egui_demo_lib = "0.18"
[patch.crates-io]
winit = { git = "https://github.com/rib/winit", branch = "android-activity" }
#winit = { path="../../../winit" }
egui = { git = "https://github.com/emilk/egui" }
egui-wgpu = { git = "https://github.com/emilk/egui" }
egui-winit = { git = "https://github.com/emilk/egui" }
egui_extras = { git = "https://github.com/emilk/egui" }
egui_demo_lib = { git = "https://github.com/emilk/egui" }
#egui = { path = "../../../egui/egui" }
#egui-wgpu = { path = "../../../egui/egui-wgpu" }
#egui-winit = { path = "../../../egui/egui-winit" }
#egui_extras = { path = "../../../egui/egui_extras" }
#egui_demo_lib = { path = "../../../egui/egui_demo_lib" }
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.9"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = [ "game-activity" ] }
[features]
default = []
desktop = []
[lib]
name="main"
crate_type=["cdylib"]
[[bin]]
path="src/lib.rs"
name="egui-test"
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.agdkegui/.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.agdkegui"
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.agdkegui">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="AGDK Egui"
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.agdkegui;
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.2-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()
}
}
include ':app'
-175
View File
@@ -1,175 +0,0 @@
use log::Level;
use winit::event_loop::{EventLoopWindowTarget, EventLoopBuilder, EventLoop};
#[cfg(target_os="android")]
use android_activity::AndroidApp;
#[cfg(target_os="android")]
use winit::platform::android::EventLoopBuilderExtAndroid;
use winit::{
event_loop::{ControlFlow},
};
use egui_wgpu::winit::Painter;
use egui_winit::State;
//use egui_winit_platform::{Platform, PlatformDescriptor};
use winit::event::Event::*;
const INITIAL_WIDTH: u32 = 1920;
const INITIAL_HEIGHT: u32 = 1080;
/// A custom event type for the winit app.
enum Event {
RequestRedraw,
}
/// Enable egui to request redraws via a custom Winit event...
#[derive(Clone)]
struct RepaintSignal(std::sync::Arc<std::sync::Mutex<winit::event_loop::EventLoopProxy<Event>>>);
fn create_window<T>(event_loop: &EventLoopWindowTarget<T>, state: &mut State, painter: &mut Painter) -> winit::window::Window {
let window = winit::window::WindowBuilder::new()
.with_decorations(true)
.with_resizable(true)
.with_transparent(false)
.with_title("egui winit + wgpu example")
.with_inner_size(winit::dpi::PhysicalSize {
width: INITIAL_WIDTH,
height: INITIAL_HEIGHT,
})
.build(&event_loop)
.unwrap();
unsafe { painter.set_window(Some(&window)) };
// NB: calling set_window will lazily initialize render state which
// means we will be able to query the maximum supported texture
// dimensions
if let Some(max_size) = painter.max_texture_side() {
state.set_max_texture_side(max_size);
}
let pixels_per_point = window.scale_factor() as f32;
state.set_pixels_per_point(pixels_per_point);
window.request_redraw();
window
}
fn _main(event_loop: EventLoop<Event>) {
let ctx = egui::Context::default();
let repaint_signal = RepaintSignal(std::sync::Arc::new(std::sync::Mutex::new(
event_loop.create_proxy()
)));
ctx.set_request_repaint_callback(move || {
repaint_signal.0.lock().unwrap().send_event(Event::RequestRedraw).ok();
});
let mut state = State::new(&event_loop);
let mut painter = Painter::new(
wgpu::Backends::all(),
wgpu::PowerPreference::LowPower,
wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::default(),
limits: wgpu::Limits::default()
},
wgpu::PresentMode::Fifo,
1);
let mut window: Option<winit::window::Window> = None;
let mut egui_demo_windows = egui_demo_lib::DemoWindows::default();
// On most platforms we can immediately create a winit window. On Android we manage
// window + surface state according to Resumed/Paused events.
#[cfg(not(target_os="android"))]
{
window = Some(create_window(&event_loop, &mut state, &mut painter));
}
event_loop.run(move |event, event_loop, control_flow| {
match event {
#[cfg(target_os="android")]
Resumed => {
match window {
None => {
window = Some(create_window(event_loop, &mut state, &mut painter));
}
Some(ref window) => {
unsafe { painter.set_window(Some(window)) };
window.request_redraw();
}
}
}
#[cfg(target_os="android")]
Suspended => {
window = None;
}
RedrawRequested(..) => {
if let Some(window) = window.as_ref() {
let raw_input = state.take_egui_input(window);
let full_output = ctx.run(raw_input, |ctx| {
egui_demo_windows.ui(ctx);
});
state.handle_platform_output(window, &ctx, full_output.platform_output);
painter.paint_and_update_textures(state.pixels_per_point(),
egui::Rgba::default(),
&ctx.tessellate(full_output.shapes),
&full_output.textures_delta);
if full_output.needs_repaint {
window.request_redraw();
}
}
}
MainEventsCleared | UserEvent(Event::RequestRedraw) => {
if let Some(window) = window.as_ref() {
window.request_redraw();
}
}
WindowEvent { event, .. } => {
if state.on_event(&ctx, &event) == false {
match event {
winit::event::WindowEvent::Resized(size) => {
painter.on_window_resized(size.width, size.height);
}
winit::event::WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
_ => {}
}
}
},
_ => (),
}
});
}
#[cfg(target_os="android")]
#[no_mangle]
fn android_main(app: AndroidApp) {
android_logger::init_once(
android_logger::Config::default().with_min_level(Level::Trace)
);
let event_loop = EventLoopBuilder::with_user_event()
.with_android_app(app)
.build();
_main(event_loop);
}
#[cfg(not(target_os="android"))]
fn main() {
env_logger::builder().filter_level(log::LevelFilter::Warn) // Default Log Level
.parse_default_env()
.init();
let event_loop = EventLoopBuilder::with_user_event().build();
_main(event_loop);
}
-1
View File
@@ -18,4 +18,3 @@ local.properties
# Added by cargo
/target
Cargo.lock
-1
View File
@@ -13,7 +13,6 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
+3 -1
View File
@@ -8,7 +8,9 @@ edition = "2021"
[dependencies]
log = "0.4"
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = "game-activity" }
android-activity = { path="../../android-activity", features = ["game-activity"] }
ndk-sys = "0.4"
ndk = "0.7"
[lib]
name="main"
+2
View File
@@ -3,6 +3,7 @@ plugins {
}
android {
ndkVersion "25.2.9519653"
compileSdk 31
defaultConfig {
@@ -32,6 +33,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
namespace 'co.realfit.agdkmainloop'
}
dependencies {
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="co.realfit.agdkmainloop">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
+2 -2
View File
@@ -1,7 +1,7 @@
// 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
id 'com.android.application' version '8.0.0' apply false
id 'com.android.library' version '8.0.0' apply false
}
task clean(type: Delete) {
+3 -1
View File
@@ -18,4 +18,6 @@ android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
@@ -1,6 +1,6 @@
#Mon May 02 15:39:12 BST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
+91 -55
View File
@@ -1,70 +1,106 @@
use android_activity::{AndroidApp, PollEvent, MainEvent};
use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent};
use log::info;
#[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));
let mut quit = false;
let mut redraw_pending = true;
let mut render_state: Option<()> = Default::default();
let mut native_window: Option<ndk::native_window::NativeWindow> = None;
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:#?}");
app.poll_events(
Some(std::time::Duration::from_secs(1)), /* 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::InitWindow { .. } => {
native_window = app.native_window();
redraw_pending = true;
}
MainEvent::TerminateWindow { .. } => {
native_window = None;
}
MainEvent::WindowResized { .. } => {
redraw_pending = true;
}
MainEvent::RedrawNeeded { .. } => {
redraw_pending = true;
}
MainEvent::InputAvailable { .. } => {
redraw_pending = true;
}
MainEvent::ConfigChanged { .. } => {
info!("Config Changed: {:#?}", app.config());
}
MainEvent::LowMemory => {}
MainEvent::Destroy => quit = true,
_ => { /* ... */ }
}
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...");
_ => {}
}
}
});
if redraw_pending {
if let Some(native_window) = &native_window {
redraw_pending = false;
// Handle input
app.input_events(|event| {
info!("Input Event: {event:?}");
InputStatus::Unhandled
});
info!("Render...");
dummy_render(native_window);
}
}
},
);
}
}
}
/// Post a NOP frame to the window
///
/// Since this is a bare minimum test app we don't depend
/// on any GPU graphics APIs but we do need to at least
/// convince Android that we're drawing something and are
/// responsive, otherwise it will stop delivering input
/// events to us.
fn dummy_render(native_window: &ndk::native_window::NativeWindow) {
unsafe {
let mut buf: ndk_sys::ANativeWindow_Buffer = std::mem::zeroed();
let mut rect: ndk_sys::ARect = std::mem::zeroed();
ndk_sys::ANativeWindow_lock(
native_window.ptr().as_ptr() as _,
&mut buf as _,
&mut rect as _,
);
// Note: we don't try and touch the buffer since that
// also requires us to handle various buffer formats
ndk_sys::ANativeWindow_unlockAndPost(native_window.ptr().as_ptr() as _);
}
}
-22
View File
@@ -1,22 +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>
-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>
-33
View File
@@ -1,33 +0,0 @@
[package]
name = "agdk-winit-wgpu"
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"
winit = { git = "https://github.com/rib/winit", branch = "android-activity"}
#winit = { path="../../../winit" }
wgpu = "0.12.0"
pollster = "0.2"
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.9"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11.0"
android-activity = { path="../../android-activity", features = [ "game-activity" ] }
[features]
default = []
desktop = []
[lib]
name="main"
crate_type=["cdylib"]
[[bin]]
path="src/lib.rs"
name="test-winit-wgpu"
required-features = [ "desktop" ]
-29
View File
@@ -1,29 +0,0 @@
This tests using `GameActivity` with winit and wgpu.
This is based on a re-worked winit backend here:
https://github.com/rib/winit/tree/android-activity
Although it would have been possible to handle the suspend/resume
lifecycle events with a simpler approach of destroying and
recreating all graphics state, this tries to represent how
lifecycle events could be handled in more complex applications,
such as within Bevy.
Considering that lifecycle events aren't supported consistently
on desktop platforms this test also aims to build and run
on desktop - for the sake of testing how more complex
applications (that need to be portable) can work. (enable
"desktop" feature to build binary)
```
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.agdkwinitwgpu/.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.agdkwinitwgpu"
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.agdkwinitwgpu">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="AGDK Winit Wgpu"
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.agdkwinitwgpu;
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>

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