diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2e8ca98..f1bcf3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,75 +1,113 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -env: - CARGO_TERM_COLOR: always - RUSTFLAGS: --deny warnings - RUSTDOCFLAGS: --deny warnings - -jobs: - # Check formatting. - format: - name: Format - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly - with: - components: rustfmt - - - name: Check formatting - working-directory: bevy_ios_safearea - run: cargo fmt --all -- --check - - # Run Clippy lints. - clippy: - name: Clippy - runs-on: macos-latest - timeout-minutes: 30 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - - name: Populate target directory from cache - uses: Leafwing-Studios/cargo-cache@v2 - with: - manifest-path: bevy_ios_safearea/Cargo.toml - - - name: Run Clippy lints - working-directory: bevy_ios_safearea - run: cargo clippy --workspace --all-features --all-targets -- --deny warnings - - # Check documentation. - doc: - name: Docs - runs-on: macos-latest - timeout-minutes: 30 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly - - - name: Populate target directory from cache - uses: Leafwing-Studios/cargo-cache@v2 - with: - manifest-path: bevy_ios_safearea/Cargo.toml - - - name: Check documentation - working-directory: bevy_ios_safearea - run: cargo doc --workspace --all-features --document-private-items --no-deps +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: --deny warnings + RUSTDOCFLAGS: --deny warnings + +jobs: + # Check formatting. + format: + name: Format + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + + - name: Check formatting + working-directory: bevy_ios_safearea + run: cargo fmt --all -- --check + + # Run Clippy lints. + clippy: + name: Clippy + runs-on: macos-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Populate target directory from cache + uses: Leafwing-Studios/cargo-cache@v2 + with: + manifest-path: bevy_ios_safearea/Cargo.toml + + - name: Run Clippy lints + working-directory: bevy_ios_safearea + run: cargo clippy --workspace --all-features --all-targets -- --deny warnings + + android_build: + name: Android Build + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Install Dependencies + run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev lld llvm + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: "17" + distribution: "temurin" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Add Android targets + run: rustup target add aarch64-linux-android + - uses: taiki-e/install-action@v2 + with: + tool: cargo-ndk + + - name: Populate target directory from cache + uses: Leafwing-Studios/cargo-cache@v2 + with: + manifest-path: bevy_ios_safearea/Cargo.toml + + - name: Build for Android- game activity + working-directory: bevy_ios_safearea + run: cargo ndk build --workspace --features android-game-activity --target aarch64-linux-android + + - name: Build for Android- native activity + working-directory: bevy_ios_safearea + run: cargo ndk build --workspace --features android-native-activity --target aarch64-linux-android + + # Check documentation. + doc: + name: Docs + runs-on: macos-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + + - name: Populate target directory from cache + uses: Leafwing-Studios/cargo-cache@v2 + with: + manifest-path: bevy_ios_safearea/Cargo.toml + + - name: Check documentation + working-directory: bevy_ios_safearea + run: cargo doc --workspace --all-features --document-private-items --no-deps diff --git a/bevy_ios_safearea/Cargo.toml b/bevy_ios_safearea/Cargo.toml index c125ba3..e3ccb16 100644 --- a/bevy_ios_safearea/Cargo.toml +++ b/bevy_ios_safearea/Cargo.toml @@ -16,16 +16,32 @@ targets = [ "x86_64-apple-darwin", # iOS "aarch64-apple-ios", + # android + "aarch64-linux-android", + "armv7-linux-androideabi" ] +[features] +default = [] +android-native-activity = ["bevy_winit/android-native-activity"] +android-game-activity = ["bevy_winit/android-game-activity"] + [dependencies] -bevy = { version = "0.16", default-features = false, features = [ - "bevy_window", - "bevy_winit", - "bevy_log", -] } +bevy_ecs = {version = "0.16"} +bevy_app = {version = "0.16"} +bevy_reflect = {version = "0.16"} + +[target.'cfg(target_os = "ios")'.dependencies] +bevy_window = {version = "0.16"} +bevy_winit = {version = "0.16"} +bevy_log = {version = "0.16"} winit = { version = "0.30", default-features = false, features = ["rwh_06"] } +[target.'cfg(target_os = "android")'.dependencies] +bevy_window = {version = "0.16"} +bevy_log = {version = "0.16"} +bevy_winit = {version = "0.16"} +jni = "0.21" [lints.rust] missing_docs = "warn" diff --git a/bevy_ios_safearea/src/android.rs b/bevy_ios_safearea/src/android.rs new file mode 100644 index 0000000..e4b5632 --- /dev/null +++ b/bevy_ios_safearea/src/android.rs @@ -0,0 +1,71 @@ +use crate::IosSafeAreaResource; + +/// Get the safe area insets for Android. +pub(crate) fn try_get_safe_area() -> Option { + use jni::{ + objects::JObject, + sys::{_jobject, JNIInvokeInterface_}, + }; + + let android_app = bevy_window::ANDROID_APP.get()?; + let vm = unsafe { + jni::JavaVM::from_raw(android_app.vm_as_ptr() as *mut *const JNIInvokeInterface_).ok()? + }; + let activity = unsafe { JObject::from_raw(android_app.activity_as_ptr() as *mut _jobject) }; + let mut env = vm.attach_current_thread().ok()?; + + // Get the Window from the Activity + let window = env + .call_method(&activity, "getWindow", "()Landroid/view/Window;", &[]) + .ok()?; + + let window_obj = window.l().ok()?; + + // Get the DecorView from the Window + let decor_view = env + .call_method(&window_obj, "getDecorView", "()Landroid/view/View;", &[]) + .ok()?; + + let decor_view_obj = decor_view.l().ok()?; + + // Get the root window insets + let root_insets = env + .call_method( + &decor_view_obj, + "getRootWindowInsets", + "()Landroid/view/WindowInsets;", + &[], + ) + .ok()?; + + let insets_obj = root_insets.l().ok()?; + + // Get system window insets (for API level 20+) + let top = env + .call_method(&insets_obj, "getSystemWindowInsetTop", "()I", &[]) + .ok()? + .i() + .ok()?; + let bottom = env + .call_method(&insets_obj, "getSystemWindowInsetBottom", "()I", &[]) + .ok()? + .i() + .ok()?; + let left = env + .call_method(&insets_obj, "getSystemWindowInsetLeft", "()I", &[]) + .ok()? + .i() + .ok()?; + let right = env + .call_method(&insets_obj, "getSystemWindowInsetRight", "()I", &[]) + .ok()? + .i() + .ok()?; + + Some(IosSafeAreaResource { + top: top as f32, + bottom: bottom as f32, + left: left as f32, + right: right as f32, + }) +} diff --git a/bevy_ios_safearea/src/lib.rs b/bevy_ios_safearea/src/lib.rs index 320e194..7ac84d3 100644 --- a/bevy_ios_safearea/src/lib.rs +++ b/bevy_ios_safearea/src/lib.rs @@ -1,7 +1,12 @@ #![doc = include_str!("../README.md")] +#[cfg(all( + target_os = "android", + any(feature = "android-native-activity", feature = "android-game-activity") +))] +mod android; #[cfg(target_os = "ios")] mod native; mod plugin; -pub use plugin::{IosSafeArea, IosSafeAreaPlugin, IosSafeAreaResource}; +pub use plugin::{IosSafeArea, IosSafeAreaPlugin, IosSafeAreaResource, SafeAreaExt}; diff --git a/bevy_ios_safearea/src/plugin.rs b/bevy_ios_safearea/src/plugin.rs index e3eef9c..d5cfbc9 100644 --- a/bevy_ios_safearea/src/plugin.rs +++ b/bevy_ios_safearea/src/plugin.rs @@ -1,5 +1,10 @@ -use bevy::{ecs::system::SystemParam, prelude::*}; - +use bevy_app::{App, Plugin}; +use bevy_ecs::{ + reflect::ReflectResource, + resource::Resource, + system::{Res, SystemParam}, +}; +use bevy_reflect::Reflect; /// Resource providing iOS device safe area insets. /// It is created and added only when there are insets on the running device. /// It is recommended to access it from systems by using [`IosSafeArea`] SystemParam. @@ -12,8 +17,9 @@ use bevy::{ecs::system::SystemParam, prelude::*}; /// fn bevy_system(safe_area: IosSafeArea) { /// let safe_area_top = safe_area.top(); /// } -// ``` -#[derive(Resource, Clone, Debug, Default)] +/// ``` +#[derive(Resource, Clone, Debug, Default, Reflect)] +#[reflect(Resource)] pub struct IosSafeAreaResource { /// The inset from the top of the screen. /// @@ -61,6 +67,19 @@ impl IosSafeArea<'_> { } } +/// Helper trait for updating insets. +pub trait SafeAreaExt { + /// Force refresh of insets. + fn update_safe_area(&mut self) -> &mut Self; +} + +impl SafeAreaExt for bevy_ecs::system::Commands<'_, '_> { + fn update_safe_area(&mut self) -> &mut Self { + #[cfg(any(target_os = "android", target_os = "ios"))] + self.run_system_cached(init); + self + } +} /// Plugin to query iOS device safe area insets. /// /// # Example @@ -75,23 +94,61 @@ impl IosSafeArea<'_> { pub struct IosSafeAreaPlugin; impl Plugin for IosSafeAreaPlugin { - #[cfg_attr(not(target_os = "ios"), allow(unused_variables))] fn build(&self, app: &mut App) { + app.register_type::(); #[cfg(target_os = "ios")] - app.add_systems(Startup, init); + { + app.add_systems(bevy_app::Startup, init); + } + #[cfg(target_os = "android")] + { + use bevy_ecs::schedule::IntoScheduleConfigs; + app.add_systems(bevy_app::Update, init.run_if(on_application_running)); + } + } +} + +#[cfg(target_os = "android")] +fn on_application_running( + mut app_lifecycle_reader: bevy_ecs::event::EventReader, +) -> bool { + app_lifecycle_reader + .read() + .any(|e| matches!(e, bevy_window::AppLifecycle::Running)) +} + +#[cfg(target_os = "android")] +fn init(mut commands: bevy_ecs::system::Commands) { + let insets = if cfg!(any( + feature = "android-native-activity", + feature = "android-game-activity" + )) { + bevy_log::debug!("safe area updating"); + crate::android::try_get_safe_area() + } else { + bevy_log::debug!("No feature for android is enabled. No insets read."); + None + }; + if let Some(insets) = insets { + bevy_log::debug!("safe area updated: {:?}", &insets); + commands.insert_resource(insets); + } else { + bevy_log::debug!("safe area- no insets got"); } } #[cfg(target_os = "ios")] fn init( - windows: NonSend, - window: Single>, - mut commands: Commands, + windows: bevy_ecs::system::NonSend, + window: bevy_ecs::system::Single< + bevy_ecs::entity::Entity, + bevy_ecs::query::With, + >, + mut commands: bevy_ecs::system::Commands, ) { - use bevy::log::tracing; use winit::raw_window_handle::HasWindowHandle; - tracing::debug!("safe area updating"); + bevy_log::debug!("safe area updating"); let raw_window = windows.get_window(*window).expect("invalid window handle"); if let Ok(handle) = raw_window.window_handle() { @@ -114,7 +171,7 @@ fn init( right, }; - tracing::debug!("safe area updated: {:?}", safe_area); + bevy_log::debug!("safe area updated: {:?}", safe_area); commands.insert_resource(safe_area); }