From 1ce044b0541e1aa7ac081ac53b7d44447b599496 Mon Sep 17 00:00:00 2001 From: David Ackerman Date: Fri, 22 Jul 2022 17:25:16 +0200 Subject: [PATCH 1/2] Add basic ios platform implementation --- Cargo.toml | 14 +++++++--- README.md | 8 +++--- examples/macroquad-permissions/Cargo.toml | 4 ++- examples/macroquad-permissions/src/main.rs | 31 +++++++++++++++++++--- platform/android/src/error.rs | 18 +++---------- platform/ios/Cargo.toml | 15 +++++++++++ platform/ios/README.md | 15 +++++++++++ platform/ios/src/error.rs | 12 +++++++++ platform/ios/src/lib.rs | 7 +++++ platform/ios/src/permission.rs | 6 +++++ platform/ios/src/types/mod.rs | 3 +++ platform/ios/src/types/permission.rs | 7 +++++ src/error.rs | 18 +++++++++++++ src/lib.rs | 9 +++++-- src/permission.rs | 28 ++++++++++++++----- 15 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 platform/ios/Cargo.toml create mode 100644 platform/ios/README.md create mode 100644 platform/ios/src/error.rs create mode 100644 platform/ios/src/lib.rs create mode 100644 platform/ios/src/permission.rs create mode 100644 platform/ios/src/types/mod.rs create mode 100644 platform/ios/src/types/permission.rs create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 02b7d2be..65c80c5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,25 +6,31 @@ authors = ["DodoRare Team "] description = "Cross-Platform Rust Toolkit for Games ๐Ÿน" repository = "https://github.com/dodorare/crossbow" license = "Apache-2.0" -keywords = ["build", "android", "apple", "ios", "tools"] +keywords = ["build", "android", "ios", "tools"] readme = "README.md" exclude = [".github/**/*"] [dependencies] -# Platform-specific dependencies -crossbow-android = { path = "platform/android", version = "0.1.6", optional = true } +thiserror = "1.0" +displaydoc = "0.2" +anyhow = "1.0" [target.'cfg(target_os = "android")'.dependencies] +crossbow-android = { path = "platform/android", version = "0.1.6", optional = true } ndk-glue = "0.6.2" +[target.'cfg(target_os = "ios")'.dependencies] +crossbow-ios = { path = "platform/ios", version = "0.1.6", optional = true } + [patch.crates-io] winit = { git = "https://github.com/rust-windowing/winit", rev = "f93f2c158bf527ed56ab2b6f5272214f0c1d9f7d" } bevy = { git = "https://github.com/dodorare/bevy", rev = "d51104020004d34db1aa3a9a5795834d7814ae55" } miniquad = { git = "https://github.com/not-fl3/miniquad", rev = "d67ffe6950cf73df307e2d23aaa4726f14399985" } [features] -default = ["android"] +default = ["android", "ios"] android = ["crossbow-android"] +ios = ["crossbow-ios"] [workspace] members = [ diff --git a/README.md b/README.md index 7a38f8cd..65f383bf 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The `crossbow` project aims to provide a complete toolkit for cross-platform gam > There are already [cargo-apk](https://github.com/rust-windowing/android-ndk-rs/tree/master/cargo-apk), [cargo-mobile](https://github.com/BrainiumLLC/cargo-mobile), [cargo-xcode](https://gitlab.com/kornelski/cargo-xcode), etc. - why do I need another packaging tool? -Project `crossbow` is not only a packaging tool for Android and iOS - it's a toolkit. With `crossbundle-tools` you can customize and create new commands; with `crossbundle` you can create native **.apk/.aab** without any *Java* or setup *Gradle* project with fancy **Crossbow Android plugins** (iOS in near future); with `crossbow-android` you can write your own Android plugins in *Java/Kotlin*. +Project `crossbow` is not only a packaging tool for Android and iOS - it's a toolkit. With `crossbundle-tools` you can customize and create new commands; with `crossbundle` you can create native **.apk/.aab** without any *Java* or setup *Gradle* project with fancy **Crossbow Android plugins** (**iOS** in near future); with `crossbow-android` you can write your own Android plugins in *Java/Kotlin*. ## Design Goals @@ -41,14 +41,16 @@ Crossbundle crates: | Name | Description | Status | | ---- | ----------- | ------ | -| [crossbundle](./crossbundle/cli) | Command-line tool for building applications. | โœ… | +| [crossbundle](./crossbundle/cli) | Command-line tool for building and running applications. | โœ… | | [crossbundle-tools](./crossbundle/tools) | Toolkit used in `crossbundle` to build/pack/sign bundles. | โœ… | Crossbow Plugins: | Name | Description | Status | | ---- | ----------- | ------ | -| [crossbow-admob](./crossbow/admob) | Google AdMob Plugin for Android. | ๐Ÿ†— | +| [crossbow-android](./platform/android) | Crossbow Android Platform implementation. | ๐Ÿ†— | +| [crossbow-ios](./platform/ios) | Crossbow iOS Platform implementation. | ๐Ÿ›  | +| [crossbow-admob](./crossbow/admob) | Google AdMob Plugin for Android (iOS in future). | ๐Ÿ†— | | [crossbow-play-billing](./crossbow/play-billing) | Google Play Billing for Android. | ๐Ÿ“ | | [crossbow-play-games-sdk](./crossbow/play-games-sdk) | Google Play Games Sdk for Android. | ๐Ÿ“ | diff --git a/examples/macroquad-permissions/Cargo.toml b/examples/macroquad-permissions/Cargo.toml index ee50bee9..dc90475c 100644 --- a/examples/macroquad-permissions/Cargo.toml +++ b/examples/macroquad-permissions/Cargo.toml @@ -6,13 +6,15 @@ edition = "2021" [dependencies] crossbow = { version = "0.1.6", path = "../../" } -crossbow-admob = { version = "0.1.6", path = "../../plugins/admob" } log = "0.4" anyhow = "1.0" macroquad = "0.3.7" async-executor = "1.4.1" futures-lite = "1.12.0" +[target.'cfg(target_os = "android")'.dependencies] +crossbow-admob = { version = "0.1.6", path = "../../plugins/admob" } + [package.metadata.android] app_name = "Macroquad_Permissions" target_sdk_version = 30 diff --git a/examples/macroquad-permissions/src/main.rs b/examples/macroquad-permissions/src/main.rs index ebdbf7d5..b15da825 100644 --- a/examples/macroquad-permissions/src/main.rs +++ b/examples/macroquad-permissions/src/main.rs @@ -1,4 +1,8 @@ +#[cfg(target_os = "android")] use crossbow::android::{plugin, types::*}; +#[cfg(target_os = "ios")] +use crossbow::ios::types::*; +#[cfg(any(target_os = "android", target_os = "ios"))] use crossbow::request_permission; use macroquad::prelude::*; use macroquad::ui::{hash, root_ui, Skin}; @@ -39,13 +43,18 @@ async fn main() -> anyhow::Result<()> { } }; + #[cfg(target_os = "android")] let (_, vm) = crossbow::android::create_java_vm().unwrap(); + #[cfg(target_os = "android")] let jnienv = vm.attach_current_thread_as_daemon().unwrap(); + #[cfg(target_os = "android")] let admob_singleton = plugin::get_jni_singleton("AdMob").expect("Crossbow Error: AdMob is not registered"); + #[cfg(target_os = "android")] let admob = crossbow_admob::AdMobPlugin::from_jnienv(admob_singleton.clone(), jnienv).unwrap(); + #[cfg(target_os = "android")] let mut label = "Signal: ".to_owned(); let window_skin = skin.clone(); loop { @@ -54,13 +63,26 @@ async fn main() -> anyhow::Result<()> { root_ui().push_skin(&window_skin); root_ui().window(hash!(), vec2(0.0, 250.0), vec2(500.0, 500.0), |ui| { ui.label(vec2(15.0, 0.0), "AdMob"); + #[cfg(target_os = "android")] ui.label(vec2(15.0, 50.0), &label); - if ui.button(vec2(-15.0, 150.0), "Ask camera permission") { - request_permission(AndroidPermission::Camera).unwrap(); + + #[cfg(any(target_os = "android", target_os = "ios"))] + if ui.button(vec2(-15.0, 150.0), "Camera permission") { + #[cfg(target_os = "android")] + let permission = AndroidPermission::Camera; + #[cfg(target_os = "ios")] + let permission = IosPermission::Camera; + request_permission(permission).unwrap(); } - if ui.button(vec2(-15.0, 300.0), "Ask storage permission") { - request_permission(AndroidPermission::ReadExternalStorage).unwrap(); + #[cfg(any(target_os = "android", target_os = "ios"))] + if ui.button(vec2(-15.0, 300.0), "Storage permission") { + #[cfg(target_os = "android")] + let permission = AndroidPermission::ReadExternalStorage; + #[cfg(target_os = "ios")] + let permission = IosPermission::Camera; + request_permission(permission).unwrap(); } + #[cfg(target_os = "android")] if ui.button(vec2(-15.0, 450.0), "Show ad") { if !admob.get_is_initialized().unwrap() { println!("calling initialize()"); @@ -102,6 +124,7 @@ async fn main() -> anyhow::Result<()> { }); root_ui().pop_skin(); + #[cfg(target_os = "android")] if let Ok(signal) = admob_singleton.get_receiver().try_recv() { println!("signal: {:?}", signal); label = format!( diff --git a/platform/android/src/error.rs b/platform/android/src/error.rs index 3ccf7fdd..206e2e89 100644 --- a/platform/android/src/error.rs +++ b/platform/android/src/error.rs @@ -1,7 +1,7 @@ use displaydoc::Display; use thiserror::Error; -/// `Result` type that used in `crossbow-permissions`. +/// Result type wrapper with AndroidError. pub type Result = std::result::Result; /// Permissions error type. @@ -16,19 +16,7 @@ pub enum AndroidError { /// Wrong JNI Rust Type WrongJniRustType, /// Rust Jni library error - Jni(jni::errors::Error), + Jni(#[from] jni::errors::Error), /// Anyhow library errors - Anyhow(anyhow::Error), -} - -impl From for AndroidError { - fn from(error: jni::errors::Error) -> Self { - Self::Jni(error) - } -} - -impl From for AndroidError { - fn from(error: anyhow::Error) -> Self { - Self::Anyhow(error) - } + Anyhow(#[from] anyhow::Error), } diff --git a/platform/ios/Cargo.toml b/platform/ios/Cargo.toml new file mode 100644 index 00000000..60bfe536 --- /dev/null +++ b/platform/ios/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "crossbow-ios" +version = "0.1.6" +edition = "2021" +authors = ["DodoRare Team "] +description = "Cross-Platform Rust Toolkit for Games ๐Ÿน" +repository = "https://github.com/dodorare/crossbow" +license = "Apache-2.0" +keywords = ["crossbow", "ios", "port"] +readme = "README.md" + +[dependencies] +thiserror = "1.0" +displaydoc = "0.2" +anyhow = "1.0" diff --git a/platform/ios/README.md b/platform/ios/README.md new file mode 100644 index 00000000..cc879e40 --- /dev/null +++ b/platform/ios/README.md @@ -0,0 +1,15 @@ +# Crossbow iOS Platform + +## About + +This folder contains the Objective-C and Rust code for the iOS platform port, using [Cargo](https://crates.io/) as a build system. Used by the [crossbow](../../). + +You will probably want to use the top level [crossbow](../../) crate in your game. + +## WARNING + +Crossbow iOS is still in the very early stages of development. APIs can and will change. Essential features are missing, and documentation is sparse. Please don't build any serious projects with Crossbow iOS unless you are prepared to be broken by API changes constantly. + +## Thanks and inspiration + +This project was inspired by the [Godot iOS](https://github.com/godotengine/godot/tree/master/platform/ios) platform implementation, give them a star if you like ;) diff --git a/platform/ios/src/error.rs b/platform/ios/src/error.rs new file mode 100644 index 00000000..aad5a0c9 --- /dev/null +++ b/platform/ios/src/error.rs @@ -0,0 +1,12 @@ +use displaydoc::Display; +use thiserror::Error; + +/// Result type wrapper with IosError. +pub type Result = std::result::Result; + +/// Permissions error type. +#[derive(Display, Debug, Error)] +pub enum IosError { + /// Anyhow library errors + Anyhow(#[from] anyhow::Error), +} diff --git a/platform/ios/src/lib.rs b/platform/ios/src/lib.rs new file mode 100644 index 00000000..83e5c8b5 --- /dev/null +++ b/platform/ios/src/lib.rs @@ -0,0 +1,7 @@ +pub mod types; +pub mod error; +pub mod permission; + +pub fn init() { + println!("init"); +} diff --git a/platform/ios/src/permission.rs b/platform/ios/src/permission.rs new file mode 100644 index 00000000..b7824e06 --- /dev/null +++ b/platform/ios/src/permission.rs @@ -0,0 +1,6 @@ +use crate::{error::*, types::*}; + +pub fn request_permission(_permission: &IosPermission) -> Result { + panic!("iOS permissions not supported yet"); + // Ok(false) +} diff --git a/platform/ios/src/types/mod.rs b/platform/ios/src/types/mod.rs new file mode 100644 index 00000000..a4271798 --- /dev/null +++ b/platform/ios/src/types/mod.rs @@ -0,0 +1,3 @@ +mod permission; + +pub use permission::*; diff --git a/platform/ios/src/types/permission.rs b/platform/ios/src/types/permission.rs new file mode 100644 index 00000000..217c7518 --- /dev/null +++ b/platform/ios/src/types/permission.rs @@ -0,0 +1,7 @@ +/// iOS Permissions +/// +/// See for more details: https://developer.android.com/reference/android/Manifest.permission +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IosPermission { + Camera, +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..cf558851 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,18 @@ +use displaydoc::Display; +use thiserror::Error; + +/// Result type wrapper with CrossbowError. +pub type Result = std::result::Result; + +/// Permissions error type. +#[derive(Display, Debug, Error)] +pub enum CrossbowError { + /// Ios errors + #[cfg(all(target_os = "android", feature = "android"))] + AndroidError(#[from] crate::android::error::AndroidError), + /// Ios errors + #[cfg(all(target_os = "ios", feature = "ios"))] + IosError(#[from] crate::ios::error::IosError), + /// Anyhow library errors + Anyhow(#[from] anyhow::Error), +} diff --git a/src/lib.rs b/src/lib.rs index ad4fcd18..0e687dec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,13 @@ -#[cfg(target_os = "android")] +#[cfg(all(target_os = "android", feature = "android"))] pub use ndk_glue; -#[cfg(feature = "android")] +#[cfg(all(target_os = "android", feature = "android"))] pub use crossbow_android as android; +#[cfg(all(target_os = "ios", feature = "ios"))] +pub use crossbow_ios as ios; + +pub mod error; mod permission; + pub use permission::*; diff --git a/src/permission.rs b/src/permission.rs index 8930411a..0475f157 100644 --- a/src/permission.rs +++ b/src/permission.rs @@ -1,22 +1,36 @@ -use crossbow_android::{error::Result, permission, types::AndroidPermission}; +use crate::error::*; +#[cfg(all(target_os = "android", feature = "android"))] +use crossbow_android::{permission, types::AndroidPermission}; +#[cfg(all(target_os = "ios", feature = "ios"))] +use crossbow_ios::{permission, types::IosPermission}; pub enum Permission { + #[cfg(all(target_os = "android", feature = "android"))] Android(AndroidPermission), - Apple, + #[cfg(all(target_os = "ios", feature = "ios"))] + Ios(IosPermission), } pub fn request_permission>(permission: T) -> Result { + #[allow(unreachable_code)] match permission.into() { - Permission::Android(x) => permission::request_permission(&x), - Permission::Apple => { - println!("iOS permissions not supported yet"); - Ok(false) - } + #[cfg(all(target_os = "android", feature = "android"))] + Permission::Android(x) => Ok(permission::request_permission(&x)?), + #[cfg(all(target_os = "ios", feature = "ios"))] + Permission::Ios(x) => Ok(permission::request_permission(&x)?), } } +#[cfg(all(target_os = "android", feature = "android"))] impl From for Permission { fn from(x: AndroidPermission) -> Self { Permission::Android(x) } } + +#[cfg(all(target_os = "ios", feature = "ios"))] +impl From for Permission { + fn from(x: IosPermission) -> Self { + Permission::Ios(x) + } +} From 0e56c7141a94a6da05a1e3f17091383f7d948c05 Mon Sep 17 00:00:00 2001 From: David Ackerman Date: Sat, 23 Jul 2022 00:24:25 +0200 Subject: [PATCH 2/2] Add working Camera and PhotoLibrary permission request on iOS --- .github/pull_request_template.md | 9 +- .github/scripts/publish_crates.sh | 1 + .github/workflows/publish.yml | 1 + README.md | 6 +- crossbundle/cli/src/commands/build/apple.rs | 2 +- .../cli/src/commands/build/build_context.rs | 2 + crossbundle/cli/src/error.rs | 16 +-- crossbundle/cli/src/types/android_config.rs | 7 ++ crossbundle/cli/src/types/apple_config.rs | 10 +- .../tools/src/commands/apple/save_plist.rs | 4 +- crossbundle/tools/src/error.rs | 22 ++-- crossbundle/tools/tests/apple_full.rs | 2 +- examples/macroquad-permissions/Cargo.toml | 14 ++- examples/macroquad-permissions/src/main.rs | 26 +++- platform/android/src/error.rs | 10 +- platform/ios/Cargo.toml | 7 ++ platform/ios/build.rs | 6 + platform/ios/src/error.rs | 2 +- platform/ios/src/lib.rs | 2 +- platform/ios/src/permission.rs | 54 ++++++++- platform/ios/src/types/permission.rs | 113 +++++++++++++++++- 21 files changed, 264 insertions(+), 52 deletions(-) create mode 100644 platform/ios/build.rs diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6efcc0fd..8c580422 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,12 +9,11 @@ ## Changelog -> This section is optional. If this was a trivial fix, or has no externally-visible impact, you can delete this section. +> This section is optional. If this was a trivial fix or has no externally-visible impact, you can delete this section. - What changed as a result of this PR? -- If applicable, organize changes under "Added", "Changed", or "Fixed" sub-headings -- Stick to one or two sentences. If more detail is needed for a particular change, consider adding it to the "Solution" section - - If you can't summarize the work, your change may be unreasonably large / unrelated. Consider splitting your PR to make it easier to review and merge! +- If applicable, organize changes under "Added", "Changed", or "Fixed" sub-headings. +- Stick to one or two sentences. If more detail is needed for a particular change, consider adding it to the "Solution" section. ## Migration Guide @@ -22,4 +21,4 @@ - If this PR is a breaking change (relative to the last release of Crossbow), describe how a user might need to migrate their code to support these changes - Simply adding new functionality is not a breaking change. -- Fixing behavior that was definitely a bug, rather than a questionable design choice is not a breaking change. +- Fixing behavior that was definitely a bug rather than a questionable design choice is not a breaking change. diff --git a/.github/scripts/publish_crates.sh b/.github/scripts/publish_crates.sh index 0f06e910..5c7b3328 100755 --- a/.github/scripts/publish_crates.sh +++ b/.github/scripts/publish_crates.sh @@ -4,6 +4,7 @@ # Legends say that the order of the crates should be kept. crates=( platform/android + platform/ios plugins/admob crossbundle/tools crossbundle/cli diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e9044fbd..bc420024 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,6 +16,7 @@ jobs: build-and-publish-release: name: Build and publish Github release runs-on: ${{ matrix.os }} + if: false strategy: fail-fast: false matrix: diff --git a/README.md b/README.md index 65f383bf..94514793 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,20 @@ ## What is Crossbow? -The `crossbow` project aims to provide a complete toolkit for cross-platform game development in Rust - from project creation to publishing. In addition, the project simplifies the creation, packaging, and signing of Android and iOS applications. We want to make most of our tools - engine agnostic to help rust game developers integrate them into their engines or games. +The `crossbow` project aims to provide a complete toolkit for cross-platform game development in *Rust* - from project creation to publishing. In addition, the project simplifies the creation, packaging, and signing of **Android** and **iOS** applications. We want to make most of our tools - engine agnostic to help rust game developers integrate them into their engines or games. ## Why Crossbow? > There are already [cargo-apk](https://github.com/rust-windowing/android-ndk-rs/tree/master/cargo-apk), [cargo-mobile](https://github.com/BrainiumLLC/cargo-mobile), [cargo-xcode](https://gitlab.com/kornelski/cargo-xcode), etc. - why do I need another packaging tool? -Project `crossbow` is not only a packaging tool for Android and iOS - it's a toolkit. With `crossbundle-tools` you can customize and create new commands; with `crossbundle` you can create native **.apk/.aab** without any *Java* or setup *Gradle* project with fancy **Crossbow Android plugins** (**iOS** in near future); with `crossbow-android` you can write your own Android plugins in *Java/Kotlin*. +Project `crossbow` is not only a packaging tool for **Android** and iOS - it's a toolkit. With `crossbundle-tools` you can customize and create new commands; with `crossbundle` you can create native **.apk/.aab** without any *Java* or setup *Gradle* project with fancy **Crossbow Android plugins** (**iOS** in near future); with `crossbow-android` you can write your own Android plugins in *Java/Kotlin*. ## Design Goals * **Customizable**: Create new commands with available tools. * **Simple**: Easy to start but flexible for strong devs. * **Capable**: It's possible to build plain **.apk/.aab** or **.app/.ipa**; or with help of *Gradle/XCode*. -* **Rust**: Don't leave your *Rust* code - almost everything can be configured from **Cargo.toml**. +* **Rust**: Don't leave your *Rust* code - almost everything can be configured from `Cargo.toml`. ## ๐Ÿ›  Installation diff --git a/crossbundle/cli/src/commands/build/apple.rs b/crossbundle/cli/src/commands/build/apple.rs index 7bb98b92..1f09c705 100644 --- a/crossbundle/cli/src/commands/build/apple.rs +++ b/crossbundle/cli/src/commands/build/apple.rs @@ -120,7 +120,7 @@ impl AppleBuildCommand { config.status("Copying binary to app folder")?; std::fs::copy(&bin_path, &app_path.join(&name)).unwrap(); config.status_message("Generating", "Info.plist")?; - apple::save_apple_plist(&app_path, properties, false).unwrap(); + apple::save_info_plist(&app_path, properties, false).unwrap(); if self.identity.is_some() { config.status("Starting code signing process")?; apple::copy_profile( diff --git a/crossbundle/cli/src/commands/build/build_context.rs b/crossbundle/cli/src/commands/build/build_context.rs index a51be757..f5396ef8 100644 --- a/crossbundle/cli/src/commands/build/build_context.rs +++ b/crossbundle/cli/src/commands/build/build_context.rs @@ -162,6 +162,8 @@ impl BuildContext { pub fn gen_info_plist(&self, package_name: &str) -> Result { if let Some(info_plist_path) = &self.apple_config.info_plist_path { Ok(apple::read_info_plist(info_plist_path)?) + } else if let Some(info_plist) = &self.apple_config.info_plist { + Ok(info_plist.clone()) } else { Ok(apple::gen_minimal_info_plist( package_name, diff --git a/crossbundle/cli/src/error.rs b/crossbundle/cli/src/error.rs index 7557363e..2248fc1d 100644 --- a/crossbundle/cli/src/error.rs +++ b/crossbundle/cli/src/error.rs @@ -14,25 +14,25 @@ pub enum Error { TeamIdentifierNotProvided, /// Invalid cargo metadata values InvalidCargoMetadata, - /// Invalid metadata in manifest + /// Invalid metadata in manifest: {0:?} InvalidMetadata(anyhow::Error), - /// IO error + /// IO error: {0:?} Io(#[from] std::io::Error), - /// Clap error + /// Clap error: {0:?} Clap(#[from] clap::Error), - /// Anyhow error + /// Anyhow error: {0:?} AnyhowError(#[from] anyhow::Error), - /// Crossbundle Tools error + /// Crossbundle Tools error: {0:?} CrossbundleTools(#[from] crossbundle_tools::error::Error), - /// AndroidManifest error + /// AndroidManifest error: {0:?} AndroidManifest(#[from] android_manifest::error::Error), - /// FsExtra error + /// FsExtra error: {0:?} FsExtra(#[from] fs_extra::error::Error), /// Path {0:?} doesn't exist PathNotFound(std::path::PathBuf), /// Home dir not found HomeDirNotFound, - /// Failed to download jar file + /// Failed to download jar file: {0:?} DownloadFailed(ureq::Error), /// Failed to create jar file in specified path `{path}` cause of `{cause}` JarFileCreationFailed { diff --git a/crossbundle/cli/src/types/android_config.rs b/crossbundle/cli/src/types/android_config.rs index 8d73d765..e31238e8 100644 --- a/crossbundle/cli/src/types/android_config.rs +++ b/crossbundle/cli/src/types/android_config.rs @@ -9,12 +9,19 @@ pub const MIN_SDK_VERSION: u32 = 19; #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct AndroidConfig { + /// Application name. pub app_name: Option, + /// Application version name. pub version_name: Option, + /// Application version code. pub version_code: Option, + /// Minimum SDK version supported. pub min_sdk_version: Option, + /// Target SDK version. pub target_sdk_version: Option, + /// Maximum SDK version supported. pub max_sdk_version: Option, + /// Icon name in resources. pub icon: Option, /// Path to AndroidManifest.xml file. diff --git a/crossbundle/cli/src/types/apple_config.rs b/crossbundle/cli/src/types/apple_config.rs index 18432aad..eed1a589 100644 --- a/crossbundle/cli/src/types/apple_config.rs +++ b/crossbundle/cli/src/types/apple_config.rs @@ -1,15 +1,23 @@ -use crossbundle_tools::types::*; +use crossbundle_tools::types::{apple_bundle::prelude::*, AppleTarget}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct AppleConfig { + /// Application Name. pub app_name: Option, + /// Application version name. pub version_name: Option, + /// Application version code. pub version_code: Option, + /// Icon name in resources. pub icon: Option, + + /// Path to Info.plist file. pub info_plist_path: Option, + /// Apple Info.plist configuration. + pub info_plist: Option, /// Apple build targets. pub build_targets: Option>, /// Apple resources directory path relatively to project path. diff --git a/crossbundle/tools/src/commands/apple/save_plist.rs b/crossbundle/tools/src/commands/apple/save_plist.rs index 8781e775..bfa5d069 100644 --- a/crossbundle/tools/src/commands/apple/save_plist.rs +++ b/crossbundle/tools/src/commands/apple/save_plist.rs @@ -4,7 +4,7 @@ use std::fs::File; use std::path::Path; /// Saves given InfoPlist in new `Info.plist` file. -pub fn save_apple_plist(out_dir: &Path, properties: &InfoPlist, binary: bool) -> Result<()> { +pub fn save_info_plist(out_dir: &Path, properties: &InfoPlist, binary: bool) -> Result<()> { // Create Info.plist file let file_path = out_dir.join("Info.plist"); let file = File::create(file_path)?; @@ -104,7 +104,7 @@ mod tests { }, ..Default::default() }; - save_apple_plist(dir.path(), &properties, false).unwrap(); + save_info_plist(dir.path(), &properties, false).unwrap(); let file_path = dir.path().join("Info.plist"); let result = std::fs::read_to_string(&file_path).unwrap(); assert_eq!(result, PLIST_TEST_EXAMPLE.replace(" ", "\t")); diff --git a/crossbundle/tools/src/error.rs b/crossbundle/tools/src/error.rs index 837722d9..dff163f0 100644 --- a/crossbundle/tools/src/error.rs +++ b/crossbundle/tools/src/error.rs @@ -42,9 +42,9 @@ pub enum AndroidError { FailedToFindAndroidManifest(String), /// Unable to find NDK file UnableToFindNDKFile, - /// AndroidTools error + /// AndroidTools error: {0:?} AndroidTools(#[from] android_tools::error::Error), - /// AndroidManifest error + /// AndroidManifest error: {0:?} AndroidManifest(#[from] android_manifest::error::Error), } @@ -61,7 +61,7 @@ pub enum AppleError { ZipCommandFailed, /// Codesign allocate not found CodesignAllocateNotFound, - /// Simctl error + /// Simctl error: {0:?} Simctl(simctl::Error), /// Target dir does not exists TargetNotFound, @@ -71,7 +71,7 @@ pub enum AppleError { AssetsNotFound, /// Failed to find Info.plist in path: {0} FailedToFindInfoPlist(String), - /// Plist data error + /// Plist data error: {0:?} Plist(#[from] plist::Error), } @@ -115,19 +115,19 @@ pub enum Error { /// Failed to choose shell string color. /// Argument for --color must be auto, always, or never, but found `{}` FailedToChooseShellStringColor(String), - /// IO error + /// IO error: {0:?} Io(#[from] std::io::Error), - /// FS Extra error + /// FS Extra error: {0:?} FsExtra(#[from] fs_extra::error::Error), - /// Zip error + /// Zip error: {0:?} Zip(#[from] zip::result::ZipError), - /// Android error + /// Android error: {0:?} Android(#[from] AndroidError), - /// Apple error + /// Apple error: {0:?} Apple(#[from] AppleError), - /// Anyhow error + /// Anyhow error: {0:?} AnyhowError(#[from] anyhow::Error), - /// Other error + /// Other error: {0:?} OtherError(#[from] Box), } diff --git a/crossbundle/tools/tests/apple_full.rs b/crossbundle/tools/tests/apple_full.rs index 992081cc..325982f1 100644 --- a/crossbundle/tools/tests/apple_full.rs +++ b/crossbundle/tools/tests/apple_full.rs @@ -93,7 +93,7 @@ fn test_apple_full() { // Generate Info.plist let properties = get_minimal_info_plist(&name); - save_apple_plist(&app_dir, &properties, false).unwrap(); + save_info_plist(&app_dir, &properties, false).unwrap(); // Sign bundle codesign(&app_dir, true, None, None).unwrap(); diff --git a/examples/macroquad-permissions/Cargo.toml b/examples/macroquad-permissions/Cargo.toml index dc90475c..9cc6f1d1 100644 --- a/examples/macroquad-permissions/Cargo.toml +++ b/examples/macroquad-permissions/Cargo.toml @@ -47,10 +47,20 @@ name = "android.permission.CAMERA" [package.metadata.apple] app_name = "Macroquad_Permissions" -target_sdk_version = 30 version_code = 1 -icon = "ic_launcher" +icon = "icon" build_targets = ["aarch64-apple-ios", "x86_64-apple-ios"] assets = "assets" res = "res/apple" + +[package.metadata.apple.info_plist] +CFBundleIdentifier = "dodorare.macroquad-permissions" +CFBundleVersion = "1.0" +CFBundleShortVersionString = "1.0" +CFBundleExecutable = "macroquad-permissions" +UILaunchStoryboardName = "LaunchScreen" +CFBundleName = "Macroquad_Permissions" +# Permissions +NSCameraUsageDescription = "Macroquad_Permissions requires access to your phone's camera." +NSPhotoLibraryUsageDescription = "Macroquad_Permissions requires access to your phone's photo library." diff --git a/examples/macroquad-permissions/src/main.rs b/examples/macroquad-permissions/src/main.rs index b15da825..43ba232b 100644 --- a/examples/macroquad-permissions/src/main.rs +++ b/examples/macroquad-permissions/src/main.rs @@ -2,7 +2,7 @@ use crossbow::android::{plugin, types::*}; #[cfg(target_os = "ios")] use crossbow::ios::types::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(target_os = "android")] use crossbow::request_permission; use macroquad::prelude::*; use macroquad::ui::{hash, root_ui, Skin}; @@ -70,17 +70,33 @@ async fn main() -> anyhow::Result<()> { if ui.button(vec2(-15.0, 150.0), "Camera permission") { #[cfg(target_os = "android")] let permission = AndroidPermission::Camera; - #[cfg(target_os = "ios")] - let permission = IosPermission::Camera; + #[cfg(target_os = "android")] request_permission(permission).unwrap(); + + #[cfg(target_os = "ios")] + let media = MediaType::Video; + #[cfg(target_os = "ios")] + crossbow::ios::permission::request_capture_device_permission(&media, |res| { + println!("Permission result: {:?}", res); + }); } #[cfg(any(target_os = "android", target_os = "ios"))] if ui.button(vec2(-15.0, 300.0), "Storage permission") { #[cfg(target_os = "android")] let permission = AndroidPermission::ReadExternalStorage; - #[cfg(target_os = "ios")] - let permission = IosPermission::Camera; + #[cfg(target_os = "android")] request_permission(permission).unwrap(); + + #[cfg(target_os = "ios")] + let access = AccessLevel::AddOnly; + #[cfg(target_os = "ios")] + crossbow::ios::permission::request_photo_library_permission(&access, |res| { + println!("Permission result: {:?}", res); + }); + + // let result = std::cell::Cell::new(AuthorizationStatus::NotDetermined); + // result.into_inner() + // println!("AuthorizationStatus: {:?}", result); } #[cfg(target_os = "android")] if ui.button(vec2(-15.0, 450.0), "Show ad") { diff --git a/platform/android/src/error.rs b/platform/android/src/error.rs index 206e2e89..9c858ded 100644 --- a/platform/android/src/error.rs +++ b/platform/android/src/error.rs @@ -7,16 +7,16 @@ pub type Result = std::result::Result; /// Permissions error type. #[derive(Display, Debug, Error)] pub enum AndroidError { - /// Signal Sender with provided singleton name not available + /// Signal Sender with `{0}` singleton name not available SignalSenderNotAvailable(String), - /// Singleton with provided name not found or haven't registered + /// Singleton with `{0}` name not found or haven't registered SingletonNotRegistered(String), - /// Unsupported JNI Rust Type + /// Unsupported JNI Rust Type: {0} UnsupportedJniRustType(String), /// Wrong JNI Rust Type WrongJniRustType, - /// Rust Jni library error + /// Rust Jni library error: {0:?} Jni(#[from] jni::errors::Error), - /// Anyhow library errors + /// Anyhow library errors: {0:?} Anyhow(#[from] anyhow::Error), } diff --git a/platform/ios/Cargo.toml b/platform/ios/Cargo.toml index 60bfe536..c9e8b05c 100644 --- a/platform/ios/Cargo.toml +++ b/platform/ios/Cargo.toml @@ -8,8 +8,15 @@ repository = "https://github.com/dodorare/crossbow" license = "Apache-2.0" keywords = ["crossbow", "ios", "port"] readme = "README.md" +build = "build.rs" [dependencies] thiserror = "1.0" displaydoc = "0.2" anyhow = "1.0" + +objc = "0.2" +block = "0.1" +cocoa-foundation = "0.1" +core-foundation = "0.9" +libc = "0.2" diff --git a/platform/ios/build.rs b/platform/ios/build.rs new file mode 100644 index 00000000..803b7a67 --- /dev/null +++ b/platform/ios/build.rs @@ -0,0 +1,6 @@ +//! Emits linker flags depending on platforms and features. + +fn main() { + println!("cargo:rustc-link-lib=framework=AVFoundation"); + println!("cargo:rustc-link-lib=framework=Photos"); +} diff --git a/platform/ios/src/error.rs b/platform/ios/src/error.rs index aad5a0c9..09692996 100644 --- a/platform/ios/src/error.rs +++ b/platform/ios/src/error.rs @@ -7,6 +7,6 @@ pub type Result = std::result::Result; /// Permissions error type. #[derive(Display, Debug, Error)] pub enum IosError { - /// Anyhow library errors + /// Anyhow library errors: {0:?} Anyhow(#[from] anyhow::Error), } diff --git a/platform/ios/src/lib.rs b/platform/ios/src/lib.rs index 83e5c8b5..293c1233 100644 --- a/platform/ios/src/lib.rs +++ b/platform/ios/src/lib.rs @@ -1,6 +1,6 @@ -pub mod types; pub mod error; pub mod permission; +pub mod types; pub fn init() { println!("init"); diff --git a/platform/ios/src/permission.rs b/platform/ios/src/permission.rs index b7824e06..ad15fa89 100644 --- a/platform/ios/src/permission.rs +++ b/platform/ios/src/permission.rs @@ -1,6 +1,54 @@ use crate::{error::*, types::*}; +use cocoa_foundation::{base::id, foundation::NSUInteger}; +use objc::{class, msg_send, sel, sel_impl}; -pub fn request_permission(_permission: &IosPermission) -> Result { - panic!("iOS permissions not supported yet"); - // Ok(false) +pub fn request_permission(permission: &IosPermission) -> Result { + match permission { + // IosPermission::PhotoLibrary(level) => { + // request_photo_library_permission(level); + // Ok(res == AuthorizationStatus::Limited || res == AuthorizationStatus::Authorized) + // } + _ => Ok(false), + } } + +pub fn request_capture_device_permission(media: &MediaType, handler: F) +where + F: Fn(bool) + Send + Sync + 'static, +{ + let block = block::ConcreteBlock::new(move |success: bool| handler(success)); + let opt: id = media.into(); + unsafe { + let _: () = msg_send![class!(AVCaptureDevice), requestAccessForMediaType:opt completionHandler:block.copy()]; + } +} + +pub fn request_photo_library_permission(level: &AccessLevel, handler: F) +where + F: Fn(AuthorizationStatus) + Send + Sync + 'static, +{ + let block = block::ConcreteBlock::new(move |res: NSUInteger| { + handler(AuthorizationStatus::from(res)); + }); + let opt: NSUInteger = level.into(); + unsafe { + let _: () = msg_send![class!(PHPhotoLibrary), requestAuthorizationForAccessLevel:opt handler:block.copy()]; + } +} + +// , error: id +// let mut opts: NSUInteger = 0; +// for opt in options { +// let o: NSUInteger = opt.into(); +// opts = opts << o; +// } +// let localized_description: id = msg_send![error, localizedDescription]; +// let bytes = localized_description.UTF8String() as *const u8; +// let e = std::str::from_utf8(std::slice::from_raw_parts( +// bytes, +// localized_description.len(), +// )) +// .unwrap(); +// if e != "" { +// println!("Error: {:?}", e); +// } diff --git a/platform/ios/src/types/permission.rs b/platform/ios/src/types/permission.rs index 217c7518..dba490cc 100644 --- a/platform/ios/src/types/permission.rs +++ b/platform/ios/src/types/permission.rs @@ -1,7 +1,114 @@ +use cocoa_foundation::{base::id, foundation::NSUInteger}; + /// iOS Permissions -/// -/// See for more details: https://developer.android.com/reference/android/Manifest.permission #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IosPermission { - Camera, + /// AVCaptureDevice. + /// + /// Hardware or virtual capture device like a camera or microphone. + /// + /// More details: https://developer.apple.com/documentation/avfoundation/avcapturedevice + CaptureDevice(MediaType), + /// PHPhotoLibrary. + /// + /// Access and changes to the userโ€™s photo library. + /// + /// More details: https://developer.apple.com/documentation/photokit/phphotolibrary + PhotoLibrary(AccessLevel), +} + +/// AVMediaType. +/// +/// An identifier for various media types. +/// +/// More details: https://developer.apple.com/documentation/avfoundation/avmediatypeaudio +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum MediaType { + /// AVMediaTypeAudio. + /// + /// The media contains audio media. + Audio, + /// AVMediaTypeVideo. + /// + /// The media contains video. + Video, +} + +#[link(name = "AVFoundation", kind = "framework")] +extern "C" { + pub static AVMediaTypeVideo: id; + pub static AVMediaTypeAudio: id; +} + +impl From for id { + fn from(val: MediaType) -> Self { + match val { + MediaType::Audio => unsafe { AVMediaTypeAudio }, + MediaType::Video => unsafe { AVMediaTypeVideo }, + } + } +} + +impl From<&MediaType> for id { + fn from(val: &MediaType) -> Self { + match val { + MediaType::Audio => unsafe { AVMediaTypeAudio }, + MediaType::Video => unsafe { AVMediaTypeVideo }, + } + } +} + +/// PHAccessLevel. +/// +/// The appโ€™s level of access to the userโ€™s photo library. +/// +/// More details: https://developer.apple.com/documentation/photokit/phaccesslevel +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum AccessLevel { + AddOnly, + ReadWrite, +} + +impl From for NSUInteger { + fn from(level: AccessLevel) -> Self { + match level { + AccessLevel::AddOnly => 1 << 0, + AccessLevel::ReadWrite => 1 << 1, + } + } +} + +impl From<&AccessLevel> for NSUInteger { + fn from(level: &AccessLevel) -> Self { + match level { + AccessLevel::AddOnly => 1 << 0, + AccessLevel::ReadWrite => 1 << 1, + } + } +} + +/// PHAuthorizationStatus. +/// +/// Information about your appโ€™s authorization to access the userโ€™s photo library. +/// +/// More details: https://developer.apple.com/documentation/photokit/phauthorizationstatus +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum AuthorizationStatus { + NotDetermined, + Restricted, + Denied, + Authorized, + Limited, +} + +impl From for AuthorizationStatus { + fn from(integer: NSUInteger) -> Self { + match integer { + 0 => Self::NotDetermined, + 1 => Self::Restricted, + 2 => Self::Denied, + 3 => Self::Authorized, + _ => Self::Limited, + } + } }