diff --git a/.gitignore b/.gitignore index 294fb025..f38dbaf9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,9 +18,6 @@ out/ # Uncomment the following line in case you need and you don't have the release build type files in your app # release/ -# Gradle files -.gradle/ - # Local configuration file (sdk path, etc) local.properties @@ -393,9 +390,6 @@ $RECYCLE.BIN/ # Generated files -# Gradle files -.gradle - # Signing files .signing/ @@ -406,11 +400,10 @@ $RECYCLE.BIN/ # Log Files # Android Studio -/*/build/ -/*/local.properties -/*/out -/*/*/build -/*/*/production +build/ +local.properties +out +production *.ipr *.swp @@ -481,3 +474,26 @@ fabric.properties # Temporary files *.tmp + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath diff --git a/Cargo.toml b/Cargo.toml index 057eca04..eddba5e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ exclude = [".github/**/*"] crossbundle-derive = { path = "crossbundle/derive", version = "0.1.3" } crossbundle-tools = { path = "crossbundle/tools", version = "0.1.3", optional = true } -crossbow-ads = { path = "plugins/ads", version = "0.1.3", optional = true } -crossbow-permissions = { path = "plugins/permissions", version = "0.1.3", optional = true } +# Platform-specific dependencies +crossbow-android = { path = "platform/android", version = "0.1.3", optional = true } [target.'cfg(target_os = "android")'.dependencies] ndk-glue = "0.6.2" @@ -28,10 +28,12 @@ miniquad = { git = "https://github.com/not-fl3/miniquad", rev = "d67ffe6950cf73d [features] default = [] +android = ["crossbow-android"] [workspace] members = [ "plugins/*", "crossbundle/*", + "platform/*", "examples/*", ] diff --git a/README.md b/README.md index 06bd6300..4e758cdb 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,10 @@ Crate structure: | Name | Description | Status | | ---- | ----------- | ------ | | [crossbundle](./crossbundle/cli/README.md) | Command-line tool for building applications | ✅ | -| [crossbundle-install](https://github.com/dodorare/crossbow/blob/improve-documentation/docs/crossbundle-install-command.md) | Crossbundle install command to install necessary packages | ✅ | +| [crossbundle-install](./docs/crossbundle-install-command.md) | Crossbundle install command to install necessary packages | ✅ | | [crossbundle-tools](./crossbundle/tools/README.md) | Toolkit used in `crossbundle` to build/pack/sign bundles | ✅ | | [crossbundle-derive](./crossbundle/derive/README.md) | Derive macros for projects built with `crossbow` | ✅ | | [crossbow-ads](./crossbow/ads/README.md) | Plugin for advertisements | 🛠 | -| [crossbow-permissions](./crossbow/permissions/README.md) | Plugin for runtime permissions | 🛠 | | [android-tools-rs](https://github.com/dodorare/android-tools-rs) | Android-related tools for building and developing application | ✅ | | [android-manifest-rs](https://github.com/dodorare/android-manifest-rs) | [AndroidManifest](https://developer.android.com/guide/topics/manifest/manifest-intro) serializer and deserializer for Rust | ✅ | | [apple-bundle-rs](https://github.com/dodorare/apple-bundle-rs) | [AppleBundleResources](https://developer.apple.com/documentation/bundleresources) serializer and deserializer for Rust | ✅ | diff --git a/crossbundle/cli/Cargo.toml b/crossbundle/cli/Cargo.toml index 73c65d69..633db22a 100644 --- a/crossbundle/cli/Cargo.toml +++ b/crossbundle/cli/Cargo.toml @@ -23,20 +23,22 @@ path = "src/main.rs" [dependencies] crossbundle-tools = { path = "../tools", version = "0.1.3" } -clap = { version = "3.1", features = ["derive"] } +android-tools = "0.2.8" +clap = { version = "3.2.8", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } -dunce = "1.0" + thiserror = "1.0" colored = "2.0" -displaydoc = "0.1" +displaydoc = "0.2" pretty_env_logger = "0.4" log = "0.4" + fs_extra = "1.2" -android-tools = "0.2.7" dirs = "4.0.0" +dunce = "1.0" ureq = "2.4.0" -cargo = "0.61.1" -cargo-util = "0.1.1" +cargo = "0.63.1" +cargo-util = "0.2.0" [features] default = ["android", "ios"] diff --git a/crossbundle/cli/README.md b/crossbundle/cli/README.md index 4e572531..344e0bab 100644 --- a/crossbundle/cli/README.md +++ b/crossbundle/cli/README.md @@ -72,8 +72,8 @@ use_info_plist = true # Path to Info.plist file info_plist_path = "path/to/Info.plist" -# Android package name to place in AndroidManifest.xml. -android_package_name = "com.example.Example" +# Android package to place in AndroidManifest.xml. +android_package = "com.example.ExampleProject" # Android resources directory path relatively to project path. android_res = "res/android" # Android assets directory path relatively to project path. diff --git a/crossbundle/cli/src/cargo_manifest.rs b/crossbundle/cli/src/cargo_manifest.rs index 9be59205..d4925539 100644 --- a/crossbundle/cli/src/cargo_manifest.rs +++ b/crossbundle/cli/src/cargo_manifest.rs @@ -1,5 +1,7 @@ use crossbundle_tools::types::{ - android_manifest::{Service, UsesFeature, UsesPermission, UsesPermissionSdk23}, + android_manifest::{ + MetaData, Queries, Service, UsesFeature, UsesPermission, UsesPermissionSdk23, + }, *, }; use serde::{Deserialize, Serialize}; @@ -24,7 +26,7 @@ pub struct Metadata { pub info_plist_path: Option, /// Android package name to place in AndroidManifest.xml. - pub android_package_name: Option, + pub android_package: Option, /// Android resources directory path relatively to project path. pub android_res: Option, /// Android assets directory path relatively to project path. @@ -43,6 +45,10 @@ pub struct Metadata { /// Android service to place in AndroidManifest.xml. pub android_service: Option>, + /// Android application meta_data to place in AndroidManifest.xml. + pub android_meta_data: Option>, + /// Android queries to place in AndroidManifest.xml. + pub android_queries: Option, /// Apple build targets. pub apple_build_targets: Option>, diff --git a/crossbundle/cli/src/commands/build/android.rs b/crossbundle/cli/src/commands/build/android.rs index c7201c24..f9504f4b 100644 --- a/crossbundle/cli/src/commands/build/android.rs +++ b/crossbundle/cli/src/commands/build/android.rs @@ -18,12 +18,21 @@ pub struct AndroidBuildCommand { #[clap(flatten)] pub shared: SharedBuildCommand, /// Build for the given android architecture. - /// Supported targets are: `armv7-linux-androideabi`, `aarch64-linux-android`, `i686-linux-android`, `x86_64-linux-android` + /// Supported targets are: `armv7-linux-androideabi`, `aarch64-linux-android`, + /// `i686-linux-android`, `x86_64-linux-android` #[clap(long, default_value = "aarch64-linux-android")] pub target: Vec, /// Generating aab. By default crossbow generating apk #[clap(long)] pub aab: bool, + /// Compile rust code as a dynamic library [default: crossbow-android] + #[clap(long, default_missing_value = "crossbow_android")] + pub lib: Option, + /// Compile rust code as a dynamic library, generate Gradle project and build + /// generate apk/aab. If value provided - should be in Path format and it will + /// be used as a path to export Gradle project [default: :target] + #[clap(long, default_missing_value = ":target")] + pub gradle: Option, /// Path to the signing key #[clap(long, requires_all = &["sign-key-pass", "sign-key-alias"])] pub sign_key_path: Option, @@ -46,12 +55,113 @@ impl AndroidBuildCommand { let context = BuildContext::new(config, self.shared.target_dir.clone())?; if self.aab { self.execute_aab(config, &context)?; + } else if let Some(lib_name) = &self.lib { + self.build_rust_lib(config, &context, lib_name, None)?; + } else if let Some(export_path) = &self.gradle { + self.build_gradle(config, &context, export_path)?; } else { self.execute_apk(config, &context)?; } Ok(()) } + /// Compile rust code as a dynamic library, generate Gradle project and build generate apk/aab + pub fn build_gradle( + &self, + config: &Config, + context: &BuildContext, + export_path: &str, + ) -> crate::error::Result { + let profile = self.shared.profile(); + let example = self.shared.example.as_ref(); + let (_, target_dir, package_name) = Self::needed_project_dirs(example, context)?; + + config.status_message("Starting gradle build process", &package_name)?; + let android_build_dir = if export_path == ":target" { + target_dir.join("android").join(&package_name) + } else { + std::fs::create_dir_all(export_path)?; + dunce::canonicalize(export_path)? + }; + + config.status("Generating gradle project")?; + let gradle_project_path = android::gen_gradle_project( + &android_build_dir, + context.android_res(), + context.android_assets(), + )?; + + // Get AndroidManifest.xml from file or generate from Cargo.toml + let (sdk, _, _) = Self::android_toolchain(context)?; + let (_android_manifest, _manifest_path) = Self::android_manifest( + config, + context, + &sdk, + &package_name, + profile, + &gradle_project_path, + true, + )?; + + let lib_name = "crossbow_android"; + self.build_rust_lib(config, context, lib_name, Some(android_build_dir))?; + + config.status_message( + "Gradle project generated", + gradle_project_path.to_str().unwrap(), + )?; + Ok(gradle_project_path) + } + + /// Compile rust code as a dynamic library + pub fn build_rust_lib( + &self, + config: &Config, + context: &BuildContext, + lib_name: &str, + export_path: Option, + ) -> crate::error::Result<()> { + let profile = self.shared.profile(); + let example = self.shared.example.as_ref(); + let (project_path, target_dir, package_name) = Self::needed_project_dirs(example, context)?; + config.status_message("Starting lib build process", &package_name)?; + let (_sdk, ndk, target_sdk_version) = Self::android_toolchain(context)?; + + let android_build_dir = if let Some(export_path) = export_path { + export_path + } else { + target_dir.join("android").join(&package_name) + }; + + config.status_message("Compiling", "lib")?; + let build_targets = context.android_build_targets(&self.target); + let compiled_libs = self.build_target( + build_targets, + lib_name, + &ndk, + &project_path, + profile, + target_sdk_version, + &target_dir, + config, + )?; + + for (compiled_lib, build_target) in compiled_libs { + config.status_message( + "Moving library to target/android/ directory", + compiled_lib.file_name().unwrap().to_str().unwrap(), + )?; + let abi = build_target.android_abi(); + let out_dir = android_build_dir.join("libs").join(profile).join(abi); + if !out_dir.exists() { + std::fs::create_dir_all(&out_dir)?; + } + let file_name = compiled_lib.file_name().unwrap().to_owned(); + std::fs::copy(compiled_lib, &out_dir.join(&file_name))?; + } + Ok(()) + } + /// Builds APK with aapt tool and signs it with apksigner pub fn execute_apk( &self, @@ -61,18 +171,25 @@ impl AndroidBuildCommand { let profile = self.shared.profile(); let example = self.shared.example.as_ref(); let (project_path, target_dir, package_name) = Self::needed_project_dirs(example, context)?; - config.status_message("Starting build process", &package_name)?; + config.status_message("Starting apk build process", &package_name)?; let (sdk, ndk, target_sdk_version) = Self::android_toolchain(context)?; + let android_build_dir = target_dir.join("android").join(&package_name); + let native_build_dir = android_build_dir.join("native"); + let outputs_build_dir = android_build_dir.join("outputs"); + if !outputs_build_dir.exists() { + std::fs::create_dir_all(&outputs_build_dir)?; + } + // Get AndroidManifest.xml from file or generate from Cargo.toml - let android_build_dir = target_dir.join("android").join(&profile); let (android_manifest, manifest_path) = Self::android_manifest( config, context, &sdk, - package_name.to_string(), + &package_name, profile, - &android_build_dir.clone(), + &native_build_dir, + false, )?; config.status_message("Compiling", "lib")?; @@ -99,7 +216,7 @@ impl AndroidBuildCommand { let unaligned_apk_path = android::gen_unaligned_apk( &sdk, &project_path, - &android_build_dir, + &native_build_dir, &manifest_path, context.android_assets(), context.android_res(), @@ -132,7 +249,7 @@ impl AndroidBuildCommand { &sdk, &unaligned_apk_path, &package_label, - &android_build_dir, + &outputs_build_dir, )?; config.status_message("Generating", "debug signing key")?; @@ -157,18 +274,25 @@ impl AndroidBuildCommand { let profile = self.shared.profile(); let example = self.shared.example.as_ref(); let (project_path, target_dir, package_name) = Self::needed_project_dirs(example, context)?; - config.status_message("Starting build process", &package_name)?; + config.status_message("Starting aab build process", &package_name)?; let (sdk, ndk, target_sdk_version) = Self::android_toolchain(context)?; + let android_build_dir = target_dir.join("android").join(&package_name); + let native_build_dir = android_build_dir.join("native"); + let outputs_build_dir = android_build_dir.join("outputs"); + if !outputs_build_dir.exists() { + std::fs::create_dir_all(&outputs_build_dir)?; + } + // Get AndroidManifest.xml from file or generate from Cargo.toml - let android_build_dir = target_dir.join("android").join(&profile); let (android_manifest, manifest_path) = Self::android_manifest( config, context, &sdk, - package_name.to_string(), + &package_name, profile, - &android_build_dir.clone(), + &native_build_dir, + false, )?; config.status_message("Compiling", "lib")?; @@ -185,7 +309,7 @@ impl AndroidBuildCommand { )?; config.status_message("Generating", "proto format APK file")?; - let compiled_res_path = android_build_dir.join("compiled_res"); + let compiled_res_path = native_build_dir.join("compiled_res"); if !compiled_res_path.exists() { std::fs::create_dir_all(&compiled_res_path)?; } @@ -193,7 +317,7 @@ impl AndroidBuildCommand { let compiled_res = if let Some(res) = context.android_res() { let aapt2_compile = sdk.aapt2()?.compile_incremental( dunce::simplified(&res), - &dunce::simplified(&compiled_res_path).to_owned(), + dunce::simplified(&compiled_res_path), ); let compiled_res = aapt2_compile.run()?; Some(compiled_res) @@ -201,7 +325,7 @@ impl AndroidBuildCommand { None }; - let apk_path = android_build_dir.join(format!("{}_module.apk", package_name)); + let apk_path = native_build_dir.join(format!("{}_module.apk", package_name)); let mut aapt2_link = sdk.aapt2()? .link_compiled_res(compiled_res, &apk_path, &manifest_path); @@ -213,7 +337,7 @@ impl AndroidBuildCommand { aapt2_link.run()?; config.status("Extracting apk files")?; - let output_dir = android_build_dir.join("extracted_apk_files"); + let output_dir = native_build_dir.join("extracted_apk_files"); let extracted_apk_path = android::extract_archive(&apk_path, &output_dir)?; config.status("Adding libs")?; @@ -232,14 +356,15 @@ impl AndroidBuildCommand { .unwrap_or(MIN_SDK_VERSION), &extracted_apk_path, &target_dir, + &package_name, )?; } config.status("Generating ZIP module from extracted files")?; let gen_zip_modules = - android::gen_zip_modules(&android_build_dir, &package_name, &extracted_apk_path)?; + android::gen_zip_modules(&native_build_dir, &package_name, &extracted_apk_path)?; - for entry in std::fs::read_dir(&android_build_dir)? { + for entry in std::fs::read_dir(&native_build_dir)? { let entry = entry?; let path = entry.path(); if path.ends_with(format!("{}_unsigned.aab", package_name)) { @@ -248,11 +373,8 @@ impl AndroidBuildCommand { } config.status("Generating aab from modules")?; - let aab_path = android::gen_aab_from_modules( - &package_name, - &[gen_zip_modules.clone()], - &android_build_dir, - )?; + let aab_path = + android::gen_aab_from_modules(&package_name, &[gen_zip_modules], &outputs_build_dir)?; config.status_message("Generating", "debug signing key")?; let key = Self::find_keystore( @@ -272,9 +394,15 @@ impl AndroidBuildCommand { let signed_aab = android_build_dir.join(format!("{}_signed.aab", package_name)); std::fs::rename(&aab_path, &signed_aab)?; + let output_aab = signed_aab.file_name().unwrap().to_str().unwrap(); + println!("output_aab {:?}", output_aab); + println!("outputs_build_dir {:?}", outputs_build_dir); + let aab_output_path = outputs_build_dir.join(output_aab); + let mut options = fs_extra::file::CopyOptions::new(); + options.overwrite = true; + fs_extra::file::move_file(&signed_aab, &outputs_build_dir.join(output_aab), &options)?; config.status("Build finished successfully")?; - - Ok((android_manifest, sdk, signed_aab, package_name, key)) + Ok((android_manifest, sdk, aab_output_path, package_name, key)) } /// Specifies project path and target directory needed to build application @@ -307,14 +435,15 @@ impl AndroidBuildCommand { config: &Config, context: &BuildContext, sdk: &AndroidSdk, - package_name: String, + package_name: &str, profile: Profile, - android_build_dir: &PathBuf, + android_build_dir: &Path, + gradle: bool, ) -> crate::error::Result<(AndroidManifest, PathBuf)> { config.status_message("Generating", "AndroidManifest.xml")?; let android_manifest = - context.gen_android_manifest(&sdk, &package_name, profile.is_debug())?; - let manifest_path = android::save_android_manifest(&android_build_dir, &android_manifest)?; + context.gen_android_manifest(sdk, package_name, profile.is_debug(), gradle)?; + let manifest_path = android::save_android_manifest(android_build_dir, &android_manifest)?; Ok((android_manifest, manifest_path)) } @@ -324,11 +453,11 @@ impl AndroidBuildCommand { sign_key_pass: Option, sign_key_alias: Option, ) -> crate::error::Result { - let key = if let Some(key_path) = sign_key_path.clone() { + let key = if let Some(key_path) = sign_key_path { let aab_key = AabKey { key_path, - key_pass: sign_key_pass.clone().unwrap().to_string(), - key_alias: sign_key_alias.clone().unwrap().to_string(), + key_pass: sign_key_pass.unwrap(), + key_alias: sign_key_alias.unwrap(), }; if aab_key.key_path.exists() { aab_key @@ -368,7 +497,7 @@ impl AndroidBuildCommand { ) -> crate::error::Result> { let mut libs = Vec::new(); for build_target in build_targets { - let lib_name = format!("lib{}.so", package_name.replace("-", "_")); + let lib_name = format!("lib{}.so", package_name.replace('-', "_")); let rust_triple = build_target.rust_triple(); config.status_message("Compiling for architecture", rust_triple)?; @@ -379,9 +508,9 @@ impl AndroidBuildCommand { // Compile rust code for android depending on application wrapper rust_compile( - &ndk, + ndk, build_target, - &project_path, + project_path, profile, self.shared.features.clone(), self.shared.all_features, diff --git a/crossbundle/cli/src/commands/build/apple.rs b/crossbundle/cli/src/commands/build/apple.rs index 8fd8c247..4c5ad1ac 100644 --- a/crossbundle/cli/src/commands/build/apple.rs +++ b/crossbundle/cli/src/commands/build/apple.rs @@ -89,7 +89,7 @@ impl AppleBuildCommand { apple::compile_rust_for_ios( target, build_target, - &project_path, + project_path, profile, self.shared.features.clone(), self.shared.all_features, @@ -104,8 +104,8 @@ impl AppleBuildCommand { .join(rust_triple) .join(&profile); let app_path = apple::gen_apple_app_folder( - &apple_target_dir, - &name, + apple_target_dir, + name, context.apple_res().as_ref().map(|r| project_path.join(r)), context .apple_assets() @@ -126,7 +126,7 @@ impl AppleBuildCommand { config.status_message("Generating", "xcent file")?; let xcent_path = apple::gen_xcent( &app_path, - &name, + name, self.team_identifier .as_ref() .ok_or(Error::TeamIdentifierNotProvided)?, @@ -140,7 +140,7 @@ impl AppleBuildCommand { config.status("Code signing process finished")?; } config.status("Generating ipa file")?; - apple::gen_apple_ipa(&apple_target_dir, &app_path, &name)?; + apple::gen_apple_ipa(apple_target_dir, &app_path, name)?; config.status("Build finished successfully")?; Ok(app_path) } diff --git a/crossbundle/cli/src/commands/build/build_context.rs b/crossbundle/cli/src/commands/build/build_context.rs index f0041813..f02d7aa7 100644 --- a/crossbundle/cli/src/commands/build/build_context.rs +++ b/crossbundle/cli/src/commands/build/build_context.rs @@ -51,9 +51,6 @@ impl BuildContext { /// Get package name from cargo manifest pub fn package_name(&self) -> String { - if let Some(package_name) = self.metadata.android_package_name.clone() { - return package_name; - }; self.manifest.summary().name().to_string() } @@ -95,23 +92,33 @@ impl BuildContext { self.metadata.android_assets.clone() } - /// Get android manifest from the path in cargo manifest or generate it with the given configuration + /// Get android package id from cargo manifest + pub fn android_package(&self, package_name: &str) -> String { + self.metadata + .android_package + .clone() + .unwrap_or(format!("com.rust.{}", package_name)) + .replace('-', "_") + } + + /// Get android manifest from the path in cargo manifest or generate it with the given configuration pub fn gen_android_manifest( &self, sdk: &AndroidSdk, package_name: &str, debuggable: bool, + gradle: bool, ) -> Result { let android_manifest = GenAndroidManifest { - app_id: Some(package_name.to_string()), + app_id: Some(self.android_package(package_name)), package_name: package_name.to_string(), app_name: self.metadata.app_name.clone(), version_name: self .metadata .version_name .clone() - .unwrap_or(self.package_version()), - version_code: self.metadata.version_code.clone().unwrap_or(1), + .unwrap_or_else(|| self.package_version()), + version_code: self.metadata.version_code.unwrap_or(1), min_sdk_version: self.metadata.min_sdk_version, target_sdk_version: self .metadata @@ -124,6 +131,8 @@ impl BuildContext { permissions: self.metadata.android_permissions.clone(), features: self.metadata.android_features.clone(), service: self.metadata.android_service.clone(), + meta_data: self.metadata.android_meta_data.clone(), + queries: self.metadata.android_queries.clone(), }; if self.metadata.use_android_manifest { let path = self @@ -133,7 +142,7 @@ impl BuildContext { .unwrap_or_else(|| self.project_path.join("AndroidManifest.xml")); Ok(android::read_android_manifest(&path)?) } else if !self.metadata.use_android_manifest { - let manifest = GenAndroidManifest::gen_android_manifest(&android_manifest); + let manifest = android_manifest.gen_android_manifest(gradle); Ok(manifest) } else { let target_sdk_version = sdk.default_platform(); @@ -148,7 +157,7 @@ impl BuildContext { } /// Get info plist from the path in cargo manifest or generate it with the given configuration - pub fn gen_info_plist(&self, package_name: &String) -> Result { + pub fn gen_info_plist(&self, package_name: &str) -> Result { if self.metadata.use_info_plist { let path = self .metadata @@ -163,7 +172,7 @@ impl BuildContext { self.metadata .version_name .clone() - .unwrap_or(self.package_version()), + .unwrap_or_else(|| self.package_version()), )) } else { Ok(apple::gen_minimal_info_plist( diff --git a/crossbundle/cli/src/commands/install/bundletool.rs b/crossbundle/cli/src/commands/install/bundletool.rs index b3bb9491..49f2b9b1 100644 --- a/crossbundle/cli/src/commands/install/bundletool.rs +++ b/crossbundle/cli/src/commands/install/bundletool.rs @@ -21,12 +21,12 @@ impl BundletoolInstallCommand { /// Download and install bundletool to provided or default path pub fn install(&self, config: &Config) -> crate::error::Result<()> { config.status("Installing bundletool")?; - if self.force == false { + if !self.force { for bundletool in std::fs::read_dir(default_file_path(self.file_name())?.parent().unwrap())? { let installed_bundletool = bundletool?.path(); - if installed_bundletool.ends_with(Self::file_name(&self)) { + if installed_bundletool.ends_with(self.file_name()) { config.status("You have installed budletool on your system already")?; return Ok(()); } diff --git a/crossbundle/cli/src/commands/install/command_line_tools.rs b/crossbundle/cli/src/commands/install/command_line_tools.rs index f66027e6..9ca7857d 100644 --- a/crossbundle/cli/src/commands/install/command_line_tools.rs +++ b/crossbundle/cli/src/commands/install/command_line_tools.rs @@ -28,7 +28,7 @@ impl CommandLineToolsInstallCommand { .parse::() .ok() .unwrap() - .join(format!("{}", self.file_name())); + .join(self.file_name()); let file_path = default_file_path(self.file_name())?; @@ -36,7 +36,7 @@ impl CommandLineToolsInstallCommand { "Downloading command line tools zip archive into", &file_path.parent().unwrap().to_str().unwrap(), )?; - Self::download_and_save_file(&self, command_line_tools_download_url, &file_path)?; + self.download_and_save_file(command_line_tools_download_url, &file_path)?; let sdk_path = AndroidSdk::sdk_install_path()?; println!("sdk_path {:?}", sdk_path); @@ -73,7 +73,7 @@ impl CommandLineToolsInstallCommand { ) -> crate::error::Result<()> { for sdkmanager in std::fs::read_dir(file_path.parent().unwrap())? { let zip_path = sdkmanager?.path(); - if zip_path.ends_with(Self::file_name(&self)) { + if zip_path.ends_with(self.file_name()) { return Ok(()); } } diff --git a/crossbundle/cli/src/commands/install/mod.rs b/crossbundle/cli/src/commands/install/mod.rs index 00911c27..f0c78e65 100644 --- a/crossbundle/cli/src/commands/install/mod.rs +++ b/crossbundle/cli/src/commands/install/mod.rs @@ -20,8 +20,8 @@ const OS_TAG: &str = "mac"; #[cfg(target_os = "linux")] const OS_TAG: &str = "linux"; -const COMMAND_LINE_TOOLS_DOWNLOAD_URL: &'static str = "https://dl.google.com/android/repository/"; -const BUNDLETOOL_JAR_FILE_DOWNLOAD_URL: &'static str = +const COMMAND_LINE_TOOLS_DOWNLOAD_URL: &str = "https://dl.google.com/android/repository/"; +const BUNDLETOOL_JAR_FILE_DOWNLOAD_URL: &str = "https://github.com/google/bundletool/releases/download"; #[derive(Parser, Clone, Debug)] @@ -71,7 +71,7 @@ pub fn download_to_file( /// Using default file path related on $HOME path for all installed commands pub fn default_file_path(file_name: String) -> crate::error::Result { let default_file_path = dirs::home_dir() - .ok_or_else(|| crate::error::Error::HomeDirNotFound)? + .ok_or(crate::error::Error::HomeDirNotFound)? .join(file_name); Ok(default_file_path) } diff --git a/crossbundle/cli/src/commands/run/android.rs b/crossbundle/cli/src/commands/run/android.rs index 6097f1b8..785d25a2 100644 --- a/crossbundle/cli/src/commands/run/android.rs +++ b/crossbundle/cli/src/commands/run/android.rs @@ -1,6 +1,8 @@ use crate::commands::build::{android::AndroidBuildCommand, BuildContext}; use crate::error::Result; +use android_tools::error::CommandExt; use clap::Parser; +use crossbundle_tools::commands::android::gradle_init; use crossbundle_tools::tools::{BuildApks, InstallApks}; use crossbundle_tools::{commands::android, utils::Config}; @@ -14,7 +16,17 @@ impl AndroidRunCommand { /// Deployes and runs application in AAB or APK format on your device or emulator pub fn run(&self, config: &Config) -> Result<()> { let context = BuildContext::new(config, self.build_command.shared.target_dir.clone())?; - if self.build_command.aab { + if let Some(export_path) = &self.build_command.gradle { + let gradle_project_path = + self.build_command + .build_gradle(config, &context, export_path)?; + let mut gradle = gradle_init()?; + gradle + .arg("installDebug") + .arg("-p") + .arg(dunce::simplified(&gradle_project_path)); + gradle.output_err(true)?; + } else if self.build_command.aab { let (android_manifest, sdk, aab_path, package_name, key) = self.build_command.execute_aab(config, &context)?; config.status("Generating apks")?; @@ -34,6 +46,8 @@ impl AndroidRunCommand { config.status("Starting APK file")?; android::start_apk(&sdk, &android_manifest.package)?; config.status("Run finished successfully")?; + } else if self.build_command.lib.is_some() { + config.status("Can not run dynamic library")?; } else { let (android_manifest, sdk, apk_path) = self.build_command.execute_apk(config, &context)?; diff --git a/crossbundle/cli/src/commands/run/apple.rs b/crossbundle/cli/src/commands/run/apple.rs index 68f4dfe6..57c728a2 100644 --- a/crossbundle/cli/src/commands/run/apple.rs +++ b/crossbundle/cli/src/commands/run/apple.rs @@ -54,14 +54,10 @@ impl AppleRunCommand { } fn get_app_path(&self, app_paths: &[PathBuf]) -> Result { - if self.device { + if self.device || cfg!(target_arch = "aarch64") { Self::get_app_path_by_target(app_paths, AppleTarget::Aarch64AppleIos) } else { - if cfg!(target_arch = "aarch64") { - Self::get_app_path_by_target(app_paths, AppleTarget::Aarch64AppleIos) - } else { - Self::get_app_path_by_target(app_paths, AppleTarget::X86_64AppleIos) - } + Self::get_app_path_by_target(app_paths, AppleTarget::X86_64AppleIos) } } diff --git a/crossbundle/cli/src/error.rs b/crossbundle/cli/src/error.rs index 50ff546f..58da5ad6 100644 --- a/crossbundle/cli/src/error.rs +++ b/crossbundle/cli/src/error.rs @@ -26,6 +26,8 @@ pub enum Error { CrossbundleTools(#[from] crossbundle_tools::error::Error), /// AndroidManifest error AndroidManifest(#[from] android_manifest::error::Error), + /// FsExtra error + FsExtra(#[from] fs_extra::error::Error), /// Path {0:?} doesn't exist PathNotFound(std::path::PathBuf), /// Home dir not found @@ -47,6 +49,6 @@ pub enum Error { // TODO: Fix this. Is there a better casting for it? impl From for Error { fn from(error: crossbundle_tools::tools::AndroidToolsError) -> Self { - Error::CrossbundleTools(error.into()).into() + Error::CrossbundleTools(error.into()) } } diff --git a/crossbundle/tools/Cargo.toml b/crossbundle/tools/Cargo.toml index af9892c1..408beea3 100644 --- a/crossbundle/tools/Cargo.toml +++ b/crossbundle/tools/Cargo.toml @@ -17,9 +17,9 @@ dunce = "1.0" fs_extra = "1.2" dirs = "4.0.0" simctl = { version = "0.1.1", package = "creator-simctl" } -android-manifest = "0.1.5" +android-manifest = "0.1.8" apple-bundle = "0.1.1" -android-tools = "0.2.7" +android-tools = "0.2.8" thiserror = "1.0" anyhow = "1.0" displaydoc = "0.2" @@ -31,8 +31,9 @@ log = "0.4" zip = "0.5.13" zip-extensions = "0.6" tempfile = "3.2" -cargo = "0.61.1" -cargo-util = "0.1.1" +cargo = "0.63.1" +cargo-util = "0.2.0" +rust-embed = { version = "6.4.0", features = ["include-exclude"] } [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/crossbundle/tools/src/commands/android/add_libs_into_aapt2.rs b/crossbundle/tools/src/commands/android/add_libs_into_aapt2.rs index 16c193ac..d8442b93 100644 --- a/crossbundle/tools/src/commands/android/add_libs_into_aapt2.rs +++ b/crossbundle/tools/src/commands/android/add_libs_into_aapt2.rs @@ -15,6 +15,7 @@ pub fn add_libs_into_aapt2( min_sdk_version: u32, build_dir: &Path, target_dir: &Path, + package_name: &str, ) -> Result { // Get list of android system libs (https://developer.android.com/ndk/guides/stable_apis) let mut system_libs = Vec::new(); @@ -45,21 +46,30 @@ pub fn add_libs_into_aapt2( // Add all needed libs into apk archive let abi = build_target.android_abi(); let out_dir = build_dir.join("lib").join(abi); + let project_dir = target_dir + .join("android") + .join(&package_name) + .join("libs") + .join(abi); for (_lib_name, lib_path) in needed_libs { - add_lib_aapt2(&lib_path, &out_dir)?; + add_lib_aapt2(&lib_path, &out_dir, &project_dir)?; } Ok(out_dir) } /// Copy lib into `out_dir` then add this lib into apk file -pub fn add_lib_aapt2(lib_path: &Path, out_dir: &Path) -> Result<()> { +pub fn add_lib_aapt2(lib_path: &Path, out_dir: &Path, project_dir: &Path) -> Result<()> { if !lib_path.exists() { return Err(Error::PathNotFound(lib_path.to_owned())); } std::fs::create_dir_all(&out_dir)?; + if !project_dir.exists() { + std::fs::create_dir_all(&project_dir)?; + } let filename = lib_path.file_name().unwrap(); let mut options = fs_extra::file::CopyOptions::new(); options.overwrite = true; fs_extra::file::copy(&lib_path, out_dir.join(&filename), &options)?; + fs_extra::file::copy(&lib_path, project_dir.join(&filename), &options)?; Ok(()) } diff --git a/crossbundle/tools/src/commands/android/add_libs_into_apk.rs b/crossbundle/tools/src/commands/android/add_libs_into_apk.rs index d7dbbca3..e0579dee 100644 --- a/crossbundle/tools/src/commands/android/add_libs_into_apk.rs +++ b/crossbundle/tools/src/commands/android/add_libs_into_apk.rs @@ -45,9 +45,8 @@ pub fn add_libs_into_apk( &dylibs_paths, &mut needed_libs, )?; - // Add all needed libs into apk archive let abi = build_target.android_abi(); - let out_dir = build_dir.join("lib").join(abi); + let out_dir = build_dir.join("libs").join(abi); for (_lib_name, lib_path) in needed_libs { aapt_add_lib(sdk, apk_path, &lib_path, &out_dir, abi)?; } @@ -70,11 +69,20 @@ fn aapt_add_lib( std::fs::copy(lib_path, &out_dir.join(&file_name))?; // `aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]` // Add specified files to Zip-compatible archive - let mut aapt = sdk.build_tool(bin!("aapt"), Some(apk_path.parent().unwrap()))?; - aapt.arg("add") - .arg(apk_path) - .arg(format!("lib/{}/{}", abi, file_name.to_str().unwrap())); - aapt.output_err(true)?; + let apk_dir = apk_path.parent().unwrap(); + let add_lib = apk_dir + .parent() + .unwrap() + .join("libs") + .join(abi) + .join(file_name.to_str().unwrap()); + // FIXME: Is this should just let it do nothing if apk file is not found? + // Perhaps throw an error if apk file is not found. + if apk_path.exists() { + let mut aapt = sdk.build_tool(bin!("aapt"), Some(apk_dir))?; + aapt.arg("add").arg(apk_path).arg(add_lib); + aapt.output_err(true)?; + } Ok(()) } @@ -193,21 +201,3 @@ pub fn get_libs_in_dir(dir: &Path) -> std::io::Result> { }; Ok(libs) } - -// fs::read_dir(version_specific_libraries_path)? -// .filter_map(|entry| { -// entry -// .map(|entry| { -// if entry.path().is_file() { -// if let Some(file_name) = entry.file_name().to_str() { -// if file_name.ends_with(".so") { -// return Some(file_name.into()); -// } -// } -// } -// None -// }) -// .transpose() -// }) -// .collect::>() -// .map_err(|err| err.into()) diff --git a/crossbundle/tools/src/commands/android/gen_aab_from_modules.rs b/crossbundle/tools/src/commands/android/generate/gen_aab_from_modules.rs similarity index 100% rename from crossbundle/tools/src/commands/android/gen_aab_from_modules.rs rename to crossbundle/tools/src/commands/android/generate/gen_aab_from_modules.rs diff --git a/crossbundle/tools/src/commands/android/generate/gen_gradle_project.rs b/crossbundle/tools/src/commands/android/generate/gen_gradle_project.rs new file mode 100644 index 00000000..b36920a5 --- /dev/null +++ b/crossbundle/tools/src/commands/android/generate/gen_gradle_project.rs @@ -0,0 +1,118 @@ +use rust_embed::RustEmbed; +use std::{ + fs::File, + io::Write, + path::{Path, PathBuf}, +}; + +#[derive(RustEmbed)] +#[folder = "../../platform/android/java/app"] +#[include = "src/*"] +#[include = "*.xml"] +#[include = "*.gradle"] +#[exclude = "build/"] +#[exclude = "libs/"] +struct CrossbowAppTemplate; + +pub fn gen_gradle_project( + android_build_dir: &Path, + resources_dir: Option, + assets_dir: Option, +) -> crate::error::Result { + let gradle_project_path = android_build_dir.join("gradle"); + + for file_name in CrossbowAppTemplate::iter() { + let file_path = gradle_project_path.join(file_name.as_ref()); + if let Some(path) = file_path.parent() { + std::fs::create_dir_all(path)?; + } + let mut build_gradle = File::create(file_path)?; + let file = CrossbowAppTemplate::get(file_name.as_ref()).unwrap(); + write!( + build_gradle, + "{}", + std::str::from_utf8(file.data.as_ref()).unwrap() + )?; + } + + let mut gradle_properties = File::create(gradle_project_path.join("gradle.properties"))?; + write!(gradle_properties, "{}", crossbow_app_gradle_properties())?; + + let mut settings_gradle = File::create(gradle_project_path.join("settings.gradle"))?; + write!(settings_gradle, "{}", crossbow_app_settings_gradle())?; + + let mut options = fs_extra::dir::CopyOptions::new(); + options.overwrite = true; + options.content_only = true; + // Copy resources to gradle folder if provided + if let Some(resources_dir) = resources_dir { + fs_extra::dir::copy(resources_dir, &gradle_project_path.join("res"), &options)?; + } + // Copy assets to gradle folder if provided + if let Some(assets_dir) = assets_dir { + fs_extra::dir::copy(assets_dir, &gradle_project_path.join("assets"), &options)?; + } + + Ok(gradle_project_path) +} + +fn crossbow_app_gradle_properties() -> String { + r#" +# 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 +android.enableJetifier=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 +"# + .to_string() +} + +fn crossbow_app_settings_gradle() -> String { + r#" +include ":crossbow" +project(":crossbow").projectDir = new File("../../../../platform/android/java/") +include ":crossbow:lib" +"# + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_crossbow_app_template() { + for file in CrossbowAppTemplate::iter() { + println!("{}", file.as_ref()); + } + assert!( + CrossbowAppTemplate::get("src/com/crossbow/game/CrossbowApp.kt").is_some(), + "CrossbowApp.kt should exist" + ); + assert!( + CrossbowAppTemplate::get("libs/debug/arm64-v8a/libcrossbow_android.so").is_none(), + "libcrossbow_android.so shouldn't exist" + ); + } +} diff --git a/crossbundle/tools/src/commands/android/gen_key.rs b/crossbundle/tools/src/commands/android/generate/gen_key.rs similarity index 100% rename from crossbundle/tools/src/commands/android/gen_key.rs rename to crossbundle/tools/src/commands/android/generate/gen_key.rs diff --git a/crossbundle/tools/src/commands/android/gen_manifest.rs b/crossbundle/tools/src/commands/android/generate/gen_manifest.rs similarity index 83% rename from crossbundle/tools/src/commands/android/gen_manifest.rs rename to crossbundle/tools/src/commands/android/generate/gen_manifest.rs index 4be0300d..a5b72621 100644 --- a/crossbundle/tools/src/commands/android/gen_manifest.rs +++ b/crossbundle/tools/src/commands/android/generate/gen_manifest.rs @@ -16,17 +16,20 @@ pub struct GenAndroidManifest { pub permissions: Option>, pub features: Option>, pub service: Option>, + pub meta_data: Option>, + pub queries: Option, } impl GenAndroidManifest { /// Generates [`AndroidManifest`](android_manifest::AndroidManifest) with /// given changes - pub fn gen_android_manifest(&self) -> AndroidManifest { + pub fn gen_android_manifest(&self, gradle: bool) -> AndroidManifest { AndroidManifest { package: self .app_id .clone() - .unwrap_or(format!("com.rust.{}", self.package_name.replace('-', "_"))), + .unwrap_or(format!("com.rust.{}", self.package_name)) + .replace('-', "_"), version_name: Some(self.version_name.clone()), version_code: Some(self.version_code), uses_sdk: Some(UsesSdk { @@ -37,8 +40,9 @@ impl GenAndroidManifest { uses_permission_sdk_23: self.permissions_sdk_23.clone().unwrap_or_default(), uses_permission: self.permissions.clone().unwrap_or_default(), uses_feature: self.features.clone().unwrap_or_default(), + queries: self.queries.clone(), application: Application { - has_code: Some(false), + has_code: Some(gradle), label: Some(StringResourceOrString::string( self.app_name .as_ref() @@ -54,14 +58,13 @@ impl GenAndroidManifest { Some("android".to_string()), )), service: self.service.clone().unwrap_or_default(), + meta_data: self.meta_data.clone().unwrap_or_default(), activity: vec![Activity { - name: "android.app.NativeActivity".to_string(), + name: match gradle { + true => "com.crossbow.game.CrossbowApp".to_string(), + false => "android.app.NativeActivity".to_string(), + }, resizeable_activity: Some(true), - label: Some(StringResourceOrString::string( - self.app_name - .as_ref() - .unwrap_or(&self.package_name.to_owned()), - )), config_changes: vec![ ConfigChanges::Orientation, ConfigChanges::KeyboardHidden, @@ -70,7 +73,10 @@ impl GenAndroidManifest { .into(), meta_data: vec![MetaData { name: Some("android.app.lib_name".to_string()), - value: Some(self.package_name.replace('-', "_")), + value: Some(match gradle { + true => "crossbow_android".to_string(), + false => self.package_name.replace('-', "_"), + }), ..Default::default() }], intent_filter: vec![IntentFilter { @@ -96,7 +102,8 @@ impl GenAndroidManifest { package: self .app_id .clone() - .unwrap_or(format!("com.rust.{}", self.package_name.replace('-', "_"))), + .unwrap_or(format!("com.rust.{}", self.package_name)) + .replace('-', "_"), version_name: Some(self.version_name.clone()), version_code: Some(self.version_code), application: Application { diff --git a/crossbundle/tools/src/commands/android/gen_minimal_unsigned_aab.rs b/crossbundle/tools/src/commands/android/generate/gen_minimal_unsigned_aab.rs similarity index 88% rename from crossbundle/tools/src/commands/android/gen_minimal_unsigned_aab.rs rename to crossbundle/tools/src/commands/android/generate/gen_minimal_unsigned_aab.rs index 5c4d366b..e788a385 100644 --- a/crossbundle/tools/src/commands/android/gen_minimal_unsigned_aab.rs +++ b/crossbundle/tools/src/commands/android/generate/gen_minimal_unsigned_aab.rs @@ -17,7 +17,7 @@ pub fn gen_minimal_unsigned_aab( }; let android_manifest = manifest.gen_min_android_manifest(); - let manifest_path = super::save_android_manifest(aab_build_dir, &android_manifest)?; + let manifest_path = super::super::save_android_manifest(aab_build_dir, &android_manifest)?; let apk_path = aab_build_dir.join(format!("{}_module.apk", package_name)); if !aab_build_dir.exists() { std::fs::create_dir_all(&aab_build_dir)?; @@ -34,14 +34,14 @@ pub fn gen_minimal_unsigned_aab( aapt2_link.run()?; let output_dir = aab_build_dir.join("extracted_apk_files"); - let extracted_apk_path = super::extract_archive(&apk_path, &output_dir)?; + let extracted_apk_path = super::super::extract_archive(&apk_path, &output_dir)?; let gen_zip_modules = super::gen_zip_modules(aab_build_dir, package_name, &extracted_apk_path)?; let aab_path = super::gen_aab_from_modules(package_name, &[gen_zip_modules.clone()], aab_build_dir)?; - super::remove(vec![gen_zip_modules, extracted_apk_path])?; + super::super::remove(vec![gen_zip_modules, extracted_apk_path])?; Ok(aab_path) } diff --git a/crossbundle/tools/src/commands/android/gen_unaligned_apk.rs b/crossbundle/tools/src/commands/android/generate/gen_unaligned_apk.rs similarity index 100% rename from crossbundle/tools/src/commands/android/gen_unaligned_apk.rs rename to crossbundle/tools/src/commands/android/generate/gen_unaligned_apk.rs diff --git a/crossbundle/tools/src/commands/android/gen_zip_modules.rs b/crossbundle/tools/src/commands/android/generate/gen_zip_modules.rs similarity index 100% rename from crossbundle/tools/src/commands/android/gen_zip_modules.rs rename to crossbundle/tools/src/commands/android/generate/gen_zip_modules.rs diff --git a/crossbundle/tools/src/commands/android/generate/mod.rs b/crossbundle/tools/src/commands/android/generate/mod.rs new file mode 100644 index 00000000..e4d30d9c --- /dev/null +++ b/crossbundle/tools/src/commands/android/generate/mod.rs @@ -0,0 +1,15 @@ +mod gen_aab_from_modules; +mod gen_gradle_project; +mod gen_key; +mod gen_manifest; +mod gen_minimal_unsigned_aab; +mod gen_unaligned_apk; +mod gen_zip_modules; + +pub use gen_aab_from_modules::*; +pub use gen_gradle_project::*; +pub use gen_key::*; +pub use gen_manifest::*; +pub use gen_minimal_unsigned_aab::*; +pub use gen_unaligned_apk::*; +pub use gen_zip_modules::*; diff --git a/crossbundle/tools/src/commands/android/gradle_command.rs b/crossbundle/tools/src/commands/android/gradle_command.rs new file mode 100644 index 00000000..f01cd6e2 --- /dev/null +++ b/crossbundle/tools/src/commands/android/gradle_command.rs @@ -0,0 +1,26 @@ +use crate::error::{AndroidError, Result}; +use std::process::Command; + +/// Find gradle executable file in and initialize it +pub fn gradle_init() -> Result { + if let Ok(gradle) = which::which(bat!("gradle")) { + return Ok(Command::new(gradle)); + } + let gradle = std::env::var("GRADLE_HOME").ok(); + let gradle_path = std::path::PathBuf::from(gradle.ok_or(AndroidError::AndroidSdkNotFound)?); + let gradle_executable = gradle_path.join("bin").join(bat!("gradle")); + Ok(Command::new(gradle_executable)) +} + +#[cfg(test)] +mod tests { + use crate::error::CommandExt; + + use super::gradle_init; + #[test] + fn test_gradle_exe() { + let mut gradle = gradle_init().unwrap(); + gradle.arg("-h"); + gradle.output_err(true).unwrap(); + } +} diff --git a/crossbundle/tools/src/commands/android/helper_functions.rs b/crossbundle/tools/src/commands/android/helper_functions.rs index 12f6c4d1..80a7632c 100644 --- a/crossbundle/tools/src/commands/android/helper_functions.rs +++ b/crossbundle/tools/src/commands/android/helper_functions.rs @@ -3,31 +3,12 @@ use crate::error::*; /// Helper function to delete files pub fn remove(target: Vec) -> Result<()> { target.iter().for_each(|content| { - if content.is_file() { + if content.exists() && content.is_file() { std::fs::remove_file(&content).unwrap(); } - if content.is_dir() { + if content.exists() && content.is_dir() { std::fs::remove_dir_all(&content).unwrap(); } }); Ok(()) } - -/// Helper function to parse the file to slice u8 from assets folder. -pub fn parse_the_file_to_slice_u8( - file_name: &str, - app_name: &str, - additional_dir: Option, -) -> Result> { - let current_dir = std::env::current_dir()?; - let mut buf = current_dir; - buf.push("examples"); - buf.push(app_name); - buf.push("assets"); - if let Some(add) = additional_dir { - buf.push(add); - } - buf.push(file_name); - let bytes = std::fs::read(buf)?; - Ok(bytes) -} diff --git a/crossbundle/tools/src/commands/android/mod.rs b/crossbundle/tools/src/commands/android/mod.rs index 5b758188..0a0c94ef 100644 --- a/crossbundle/tools/src/commands/android/mod.rs +++ b/crossbundle/tools/src/commands/android/mod.rs @@ -7,12 +7,8 @@ mod align_apk; mod attach_logger; mod detect_abi; mod extract_apk; -mod gen_aab_from_modules; -mod gen_key; -mod gen_manifest; -mod gen_minimal_unsigned_aab; -mod gen_unaligned_apk; -mod gen_zip_modules; +mod generate; +mod gradle_command; mod helper_functions; mod install_apk; mod read_manifest; @@ -28,12 +24,8 @@ pub use align_apk::*; pub use attach_logger::*; pub use detect_abi::*; pub use extract_apk::*; -pub use gen_aab_from_modules::*; -pub use gen_key::*; -pub use gen_manifest::*; -pub use gen_minimal_unsigned_aab::*; -pub use gen_unaligned_apk::*; -pub use gen_zip_modules::*; +pub use generate::*; +pub use gradle_command::*; pub use helper_functions::*; pub use install_apk::*; pub use read_manifest::*; diff --git a/crossbundle/tools/src/commands/android/rust_compile/rust_compiler.rs b/crossbundle/tools/src/commands/android/rust_compile/rust_compiler.rs index 0eecfd62..6b14be1f 100644 --- a/crossbundle/tools/src/commands/android/rust_compile/rust_compiler.rs +++ b/crossbundle/tools/src/commands/android/rust_compile/rust_compiler.rs @@ -96,7 +96,7 @@ impl cargo::core::compiler::Executor for SharedLibraryExecutor { && (target.kind() == &cargo::core::manifest::TargetKind::Bin || target.kind() == &cargo::core::manifest::TargetKind::ExampleBin) { - let mut new_args = cmd.get_args().to_owned(); + let mut new_args = cmd.get_args().cloned().collect::>(); let extra_code = match self.app_wrapper { ApplicationWrapper::Sokol => consts::SOKOL_EXTRA_CODE, @@ -222,7 +222,7 @@ impl cargo::core::compiler::Executor for SharedLibraryExecutor { target.name() ))); } else if mode == cargo::core::compiler::CompileMode::Build { - let mut new_args = cmd.get_args().to_owned(); + let mut new_args = cmd.get_args().cloned().collect::>(); // Change crate-type from cdylib to rlib let mut iter = new_args.iter_mut().rev().peekable(); diff --git a/crossbundle/tools/src/commands/android/sign_apk.rs b/crossbundle/tools/src/commands/android/sign_apk.rs index 22b929c9..af9aee4b 100644 --- a/crossbundle/tools/src/commands/android/sign_apk.rs +++ b/crossbundle/tools/src/commands/android/sign_apk.rs @@ -5,7 +5,7 @@ use std::path::Path; /// Signs APK with given key. /// Uses `apksigner` build tool -pub fn sign_apk(sdk: &AndroidSdk, apk_path: &Path, key: AabKey) -> Result<()> { +pub fn sign_apk(sdk: &AndroidSdk, apk_path: &Path, key: AabKey) -> Result { let mut apksigner = sdk.build_tool(bat!("apksigner"), None)?; apksigner .arg("sign") @@ -15,5 +15,6 @@ pub fn sign_apk(sdk: &AndroidSdk, apk_path: &Path, key: AabKey) -> Result<()> { .arg(format!("pass:{}", &key.key_pass)) .arg(apk_path); apksigner.output_err(true)?; - Ok(()) + let apk_path = apk_path.to_path_buf(); + Ok(apk_path) } diff --git a/crossbundle/tools/src/error.rs b/crossbundle/tools/src/error.rs index dda2d6ec..2809d607 100644 --- a/crossbundle/tools/src/error.rs +++ b/crossbundle/tools/src/error.rs @@ -9,13 +9,15 @@ use thiserror::Error; /// `Result` type that used in `crossbundle-tools`. pub type Result = std::result::Result; -/// Android specific error type +/// Android specific error type. #[derive(Display, Debug, Error)] pub enum AndroidError { /// Android SDK is not found AndroidSdkNotFound, /// Android NDK is not found AndroidNdkNotFound, + /// Gradle is not found + GradleNotFound, /// Android SDK has no build tools BuildToolsNotFound, /// Android SDK has no platforms installed diff --git a/crossbundle/tools/tests/aab_full.rs b/crossbundle/tools/tests/aab_full.rs index 89d1afc1..55a1742f 100644 --- a/crossbundle/tools/tests/aab_full.rs +++ b/crossbundle/tools/tests/aab_full.rs @@ -110,6 +110,7 @@ fn test_aab_full() { target_sdk_version, &extracted_apk_path, &target_dir, + &package_name, ) .unwrap(); assert!(lib.exists()); diff --git a/crossbundle/tools/tests/add_libs.rs b/crossbundle/tools/tests/add_libs.rs index d67b39c8..7f9232d2 100644 --- a/crossbundle/tools/tests/add_libs.rs +++ b/crossbundle/tools/tests/add_libs.rs @@ -61,6 +61,7 @@ fn add_bevy_libs() { target_sdk_version, &out_dir, &project_path.join("target"), + &bevy_package_name, ) .unwrap(); assert!(lib.exists()); @@ -129,10 +130,10 @@ fn add_quad_libs() { target_sdk_version, &out_dir, &project_path.join("target"), + &quad_package_name, ) .unwrap(); assert!(lib.exists()); - // std::thread::sleep(std::time::Duration::from_secs(11111)); println!("library saved in {:?}", lib); // Check the size of the library to ensure it is not corrupted diff --git a/crossbundle/tools/tests/android_full.rs b/crossbundle/tools/tests/android_full.rs index 433659dd..e62f3f59 100644 --- a/crossbundle/tools/tests/android_full.rs +++ b/crossbundle/tools/tests/android_full.rs @@ -49,12 +49,14 @@ fn test_android_full() { .join(build_target.rust_triple()) .join(profile.as_ref()); let compiled_lib = out_dir.join(format!("lib{}.so", quad_package_name)); - // Check the size of the library to ensure it is not corrupted - if compiled_lib.exists() { - let size = std::fs::metadata(&compiled_lib).unwrap().len(); - println!("library size is {:?}", size); + if !out_dir.exists() { + std::fs::create_dir_all(&out_dir).unwrap(); } - assert!(compiled_lib.exists()); + let android_build_dir = project_path + .join("target") + .join("android") + .join(&quad_package_name); + let native_build_dir = android_build_dir.join("native"); // Gen android manifest let target_dir = project_path.join("target"); @@ -72,7 +74,7 @@ fn test_android_full() { let unaligned_apk_path = android::gen_unaligned_apk( &sdk, &project_path, - &apk_build_dir, + &native_build_dir, &manifest_path, None, None, @@ -91,7 +93,7 @@ fn test_android_full() { build_target, profile, 29, - &apk_build_dir, + &android_build_dir, &target_dir, ) .unwrap(); diff --git a/crossbundle/tools/tests/gen_gradle_project.rs b/crossbundle/tools/tests/gen_gradle_project.rs new file mode 100644 index 00000000..38e0bb60 --- /dev/null +++ b/crossbundle/tools/tests/gen_gradle_project.rs @@ -0,0 +1,99 @@ +use crossbundle_tools::{ + commands::{ + android::{self, rust_compile, GenAndroidManifest}, + gen_minimal_project, + }, + tools::{AndroidNdk, AndroidSdk}, + types::{AndroidTarget, ApplicationWrapper, IntoRustTriple, Profile}, +}; + +#[test] +pub fn test_gen_gradle_project() { + // Creates temporary directory + let tempdir = tempfile::tempdir().unwrap(); + let project_path = tempdir.path(); + + // Assigns configuration for project + let package_name = gen_minimal_project(&project_path, true).unwrap(); + + // Assign needed configuration to compile rust for android with bevy + let sdk = AndroidSdk::from_env().unwrap(); + let ndk = AndroidNdk::from_env(Some(sdk.sdk_path())).unwrap(); + let build_target = AndroidTarget::Aarch64LinuxAndroid; + let profile = Profile::Release; + let target_sdk_version = 30; + let version_code = 1_u32; + let lib_name = format!("lib{}.so", package_name.replace("-", "_")); + let app_wrapper = ApplicationWrapper::Sokol; + + let android_build_dir = project_path + .join("target") + .join("android") + .join(&package_name); + std::fs::create_dir_all(&android_build_dir).unwrap(); + + // Generate gradle project + let gradle_project_path = android::gen_gradle_project(&android_build_dir, None, None).unwrap(); + + // Generate manifest + let manifest = GenAndroidManifest { + package_name: package_name.to_string(), + version_code, + ..Default::default() + }; + let android_manifest = manifest.gen_min_android_manifest(); + let manifest_path = + android::save_android_manifest(&gradle_project_path, &android_manifest).unwrap(); + assert!(manifest_path.exists()); + + // Compile rust code for android with bevy engine + rust_compile( + &ndk, + build_target, + project_path, + profile, + vec![], + false, + false, + target_sdk_version, + &lib_name, + app_wrapper, + ) + .unwrap(); + println!("rust was compiled for example"); + + // Specifies needed directories to manage library location + let mut libs = Vec::new(); + let out_dir = project_path + .join("target") + .join(build_target.rust_triple()) + .join(profile.as_ref()); + let compiled_lib = out_dir.join(lib_name); + assert!(compiled_lib.exists()); + libs.push((compiled_lib, build_target)); + + // Adds libs into ./target/aarch64-linux-android/debug/ + for (compiled_lib, build_target) in libs { + let lib = android::add_libs_into_aapt2( + &ndk, + &compiled_lib, + build_target, + profile, + target_sdk_version, + &out_dir, + &project_path.join("target"), + &package_name, + ) + .unwrap(); + assert!(lib.exists()); + println!("library saved in {:?}", lib); + + std::thread::sleep(std::time::Duration::from_secs(11111)); + // Check the size of the library to ensure it is not corrupted + for entry in std::fs::read_dir(&lib).unwrap() { + let library = entry.unwrap().path(); + let size = std::fs::metadata(&library).unwrap().len(); + println!("library size is {:?}", size); + } + } +} diff --git a/docs/main-project-configuration.md b/docs/main-project-configuration.md index 76cdfd0d..a89fda9d 100644 --- a/docs/main-project-configuration.md +++ b/docs/main-project-configuration.md @@ -67,7 +67,6 @@ and then place `AndroidManifest.xml` or/and `Info.plist` near `Cargo.toml` - "] edition = "2021" [dependencies] -crossbow = { version = "0.1.3", path = "../../" } +crossbow = { version = "0.1.3", path = "../../", features = ["android"] } log = "0.4" anyhow = "1.0" macroquad = "0.3.7" diff --git a/examples/macroquad-3d/src/main.rs b/examples/macroquad-3d/src/main.rs index 9d3aaef6..e7080b2d 100644 --- a/examples/macroquad-3d/src/main.rs +++ b/examples/macroquad-3d/src/main.rs @@ -2,6 +2,8 @@ use macroquad::prelude::*; #[macroquad::main("Macroquad 3D")] async fn main() -> anyhow::Result<()> { + crossbow::crossbow_android::init(); + let rust_logo = load_texture("bob/rust.png").await?; let ferris = load_texture("bob/ferris.png").await?; diff --git a/examples/macroquad-permissions/Cargo.toml b/examples/macroquad-permissions/Cargo.toml index 74270fa1..ba5b374c 100644 --- a/examples/macroquad-permissions/Cargo.toml +++ b/examples/macroquad-permissions/Cargo.toml @@ -5,7 +5,7 @@ authors = ["DodoRare Team "] edition = "2021" [dependencies] -crossbow = { version = "0.1.3", path = "../../", features = ["crossbow-permissions"] } +crossbow = { version = "0.1.3", path = "../../", features = ["android"] } log = "0.4" anyhow = "1.0" macroquad = "0.3.7" diff --git a/examples/macroquad-permissions/src/main.rs b/examples/macroquad-permissions/src/main.rs index e98fc711..466aa4eb 100644 --- a/examples/macroquad-permissions/src/main.rs +++ b/examples/macroquad-permissions/src/main.rs @@ -1,11 +1,12 @@ -#[cfg(target_os = "android")] -use crossbow::crossbow_permissions::prelude::*; +use crossbow::crossbow_android::permission::prelude::*; use macroquad::prelude::*; use macroquad::ui::{hash, root_ui, Skin}; #[macroquad::main("Macroquad UI")] async fn main() -> anyhow::Result<()> { + crossbow::crossbow_android::init(); + let skin = { let label_style = root_ui() .style_builder() @@ -46,21 +47,15 @@ async fn main() -> anyhow::Result<()> { let window_skin = skin.clone(); loop { - clear_background(MAROON); + clear_background(WHITE); root_ui().push_skin(&window_skin); root_ui().window(hash!(), vec2(0.0, 250.0), vec2(500.0, 500.0), |ui| { if ui.button(vec2(-15.0, 150.0), "Ask camera permission") { - #[cfg(target_os = "android")] - request_permission(Permission::AndroidPermission(AndroidPermission::Camera)) - .unwrap(); + request_permission(AndroidPermission::Camera).unwrap(); } if ui.button(vec2(-15.0, 300.0), "Ask storage permission") { - #[cfg(target_os = "android")] - request_permission(Permission::AndroidPermission( - AndroidPermission::ReadExternalStorage, - )) - .unwrap(); + request_permission(AndroidPermission::ReadExternalStorage).unwrap(); } }); root_ui().pop_skin(); diff --git a/plugins/permissions/Cargo.toml b/platform/android/Cargo.toml similarity index 57% rename from plugins/permissions/Cargo.toml rename to platform/android/Cargo.toml index cdd3507d..31781837 100644 --- a/plugins/permissions/Cargo.toml +++ b/platform/android/Cargo.toml @@ -1,25 +1,24 @@ [package] -name = "crossbow-permissions" +name = "crossbow-android" version = "0.1.3" edition = "2021" authors = ["DodoRare Team "] description = "Cross-Platform Rust Toolkit for Games 🏹" repository = "https://github.com/dodorare/crossbow" license = "Apache-2.0" -keywords = ["permissions", "android", "ios"] +keywords = ["crossbow", "android", "port"] readme = "README.md" [dependencies] thiserror = "1.0" -displaydoc = "0.1" +displaydoc = "0.2" anyhow = "1.0" - -[target.'cfg(target_os = "android")'.dependencies] -ndk-context = "0.1" -ndk-glue = "0.6.2" jni = "0.19" -ndk = "0.6" +ndk-context = "0.1" +# ndk-glue = "0.6.2" +# ndk = "0.6" -[features] -default = ["full-permissions"] -full-permissions = [] +[package.metadata] +app_name = "crossbow_android" +target_sdk_version = 30 +android_build_targets = ["aarch64-linux-android"] diff --git a/platform/android/README.md b/platform/android/README.md new file mode 100644 index 00000000..eff71539 --- /dev/null +++ b/platform/android/README.md @@ -0,0 +1,3 @@ +# Crossbow Android Platform port + +This folder contains the Java and Rust (JNI) code for the Android platform port, using [Gradle](https://gradle.org/) and [Cargo](https://crates.io/) as a build systems. diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml new file mode 100644 index 00000000..733fdf6d --- /dev/null +++ b/platform/android/java/app/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle new file mode 100644 index 00000000..8e46476b --- /dev/null +++ b/platform/android/java/app/build.gradle @@ -0,0 +1,110 @@ +buildscript { + apply from: "config.gradle" + repositories { + google() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath libraries.androidGradlePlugin + classpath libraries.kotlinGradlePlugin + } +} + +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply from: "config.gradle" + +allprojects { + repositories { + google() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + + // // Godot user plugins custom maven repos + // String[] mavenRepos = getGodotPluginsMavenRepos() + // if (mavenRepos != null && mavenRepos.size() > 0) { + // for (String repoUrl : mavenRepos) { + // maven { + // url repoUrl + // } + // } + // } + } +} + +dependencies { + implementation libraries.kotlinStdLib + implementation libraries.androidxAppcompat + + if (rootProject.findProject(":lib")) { + implementation project(":lib") + } else if (rootProject.findProject(":crossbow:lib")) { + implementation project(":crossbow:lib") + } else { + // Custom build mode. In this scenario this project is the only one around and the Crossbow + // library is available through the pre-generated crossbow-lib.*.aar android archive files. + debugImplementation fileTree(dir: "libs/debug", include: ["*.jar", "*.aar"]) + releaseImplementation fileTree(dir: "libs/release", include: ["*.jar", "*.aar"]) + } + + // // Godot user plugins remote dependencies + // String[] remoteDeps = getGodotPluginsRemoteBinaries() + // if (remoteDeps != null && remoteDeps.size() > 0) { + // for (String dep : remoteDeps) { + // implementation dep + // } + // } + // // Godot user plugins local dependencies + // String[] pluginsBinaries = getGodotPluginsLocalBinaries() + // if (pluginsBinaries != null && pluginsBinaries.size() > 0) { + // implementation files(pluginsBinaries) + // } +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + kotlinOptions { + jvmTarget = versions.javaVersion + } + + defaultConfig { + // The default ignore pattern for the "assets" directory includes hidden files and directories which are used by Crossbow projects. + aaptOptions { + ignoreAssetsPattern "!.svn:!.git:!.gitignore:!.ds_store:!*.scc:_*:!CVS:!thumbs.db:!picasa.ini:!*~" + } + + // Feel free to modify the application id to your own. + applicationId getExportPackageName() + versionCode getExportVersionCode() + versionName getExportVersionName() + minSdkVersion getExportMinSdkVersion() + targetSdkVersion getExportTargetSdkVersion() + + missingDimensionStrategy "products", "template" + } + + lintOptions { + abortOnError false + disable "MissingTranslation", "UnusedResources" + } + + sourceSets { + main { + manifest.srcFile "AndroidManifest.xml" + java.srcDirs = ["src"] + assets.srcDirs = ["assets"] + res.srcDirs = ["res"] + } + debug.jniLibs.srcDirs = ["../libs/debug"] + release.jniLibs.srcDirs = ["../libs/release"] + } +} diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle new file mode 100644 index 00000000..918dd6ac --- /dev/null +++ b/platform/android/java/app/config.gradle @@ -0,0 +1,76 @@ +ext.versions = [ + crossbowLibrary : "0.1.3", + androidGradlePlugin: "7.0.0", + compileSdk : 31, + minSdk : 19, + targetSdk : 30, + buildTools : "30.0.3", + kotlinVersion : "1.6.21", + fragmentVersion : "1.3.6", + appcompatVersion : "1.4.0", + nexusPublishVersion: "1.1.0", + javaVersion : 11, + ndkVersion : "23.1.7779620" +] + +ext.libraries = [ + androidGradlePlugin: "com.android.tools.build:gradle:$versions.androidGradlePlugin", + kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlinVersion", + kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib:$versions.kotlinVersion", + androidxFragment : "androidx.fragment:fragment:$versions.fragmentVersion", + androidxAppcompat : "androidx.appcompat:appcompat:$versions.appcompatVersion", +] + +ext.getExportPackageName = { -> + // Retrieve the app id from the project property set by the Crossbow build command. + String appId = project.hasProperty("export_package_name") ? project.property("export_package_name") : "" + // Check if the app id is valid, otherwise use the default. + if (appId == null || appId.isEmpty()) { + appId = "com.crossbow.game" + } + return appId +} + +ext.getExportVersionCode = { -> + String versionCode = project.hasProperty("export_version_code") ? project.property("export_version_code") : "" + if (versionCode == null || versionCode.isEmpty()) { + versionCode = "1" + } + try { + return Integer.parseInt(versionCode) + } catch (NumberFormatException ignored) { + return 1 + } +} + +ext.getExportVersionName = { -> + String versionName = project.hasProperty("export_version_name") ? project.property("export_version_name") : "" + if (versionName == null || versionName.isEmpty()) { + versionName = "1.0" + } + return versionName +} + +ext.getExportMinSdkVersion = { -> + String minSdkVersion = project.hasProperty("export_version_min_sdk") ? project.property("export_version_min_sdk") : "" + if (minSdkVersion == null || minSdkVersion.isEmpty()) { + minSdkVersion = "$versions.minSdk" + } + try { + return Integer.parseInt(minSdkVersion) + } catch (NumberFormatException ignored) { + return versions.minSdk + } +} + +ext.getExportTargetSdkVersion = { -> + String targetSdkVersion = project.hasProperty("export_version_target_sdk") ? project.property("export_version_target_sdk") : "" + if (targetSdkVersion == null || targetSdkVersion.isEmpty()) { + targetSdkVersion = "$versions.targetSdk" + } + try { + return Integer.parseInt(targetSdkVersion) + } catch (NumberFormatException ignored) { + return versions.targetSdk + } +} diff --git a/platform/android/java/app/src/com/crossbow/game/CrossbowApp.kt b/platform/android/java/app/src/com/crossbow/game/CrossbowApp.kt new file mode 100644 index 00000000..6fb8aa35 --- /dev/null +++ b/platform/android/java/app/src/com/crossbow/game/CrossbowApp.kt @@ -0,0 +1,10 @@ +package com.crossbow.game + +import android.os.Bundle +import com.dodorare.crossbow.CrossbowNativeActivity + +open class CrossbowApp : CrossbowNativeActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle new file mode 100644 index 00000000..cac9fb95 --- /dev/null +++ b/platform/android/java/build.gradle @@ -0,0 +1,21 @@ +buildscript { + apply from: "app/config.gradle" + repositories { + google() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath libraries.androidGradlePlugin + classpath libraries.kotlinGradlePlugin + } +} + +apply from: "app/config.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} diff --git a/platform/android/java/gradle.properties b/platform/android/java/gradle.properties new file mode 100644 index 00000000..92516dbc --- /dev/null +++ b/platform/android/java/gradle.properties @@ -0,0 +1,26 @@ +# 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 +android.enableJetifier=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 diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.jar b/platform/android/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..41d9927a Binary files /dev/null and b/platform/android/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/platform/android/java/gradle/wrapper/gradle-wrapper.properties b/platform/android/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b1159fc5 --- /dev/null +++ b/platform/android/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/platform/android/java/gradlew b/platform/android/java/gradlew new file mode 100755 index 00000000..1b6c7873 --- /dev/null +++ b/platform/android/java/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${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 "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/platform/android/java/gradlew.bat b/platform/android/java/gradlew.bat new file mode 100644 index 00000000..ac1b06f9 --- /dev/null +++ b/platform/android/java/gradlew.bat @@ -0,0 +1,89 @@ +@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 diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml new file mode 100644 index 00000000..7924814e --- /dev/null +++ b/platform/android/java/lib/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle new file mode 100644 index 00000000..ecc59a5b --- /dev/null +++ b/platform/android/java/lib/build.gradle @@ -0,0 +1,50 @@ +plugins { + id "com.android.library" + id "org.jetbrains.kotlin.android" +} + +dependencies { + implementation libraries.kotlinStdLib + implementation libraries.androidxFragment + implementation libraries.androidxAppcompat + // implementation "androidx.games:games-activity:1.1.0" +} + +android { + namespace = "com.dodorare.crossbow" + + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + ndkVersion versions.ndkVersion + + compileOptions { + sourceCompatibility versions.javaVersion + targetCompatibility versions.javaVersion + } + + kotlinOptions { + jvmTarget = versions.javaVersion + } + + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + manifestPlaceholders = [crossbowLibraryVersion: versions.crossbowLibrary, minSdkVersion: versions.minSdk, targetSdkVersion: versions.targetSdk] + } + + lintOptions { + abortOnError false + disable "MissingTranslation", "UnusedResources" + } + + sourceSets { + main { + manifest.srcFile "AndroidManifest.xml" + java.srcDirs = ["src"] + assets.srcDirs = ["assets"] + res.srcDirs = ["res"] + } + debug.jniLibs.srcDirs = ["libs/debug"] + release.jniLibs.srcDirs = ["libs/release"] + } +} diff --git a/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowGameActivity.kt b/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowGameActivity.kt new file mode 100644 index 00000000..e4f9e62f --- /dev/null +++ b/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowGameActivity.kt @@ -0,0 +1,17 @@ +package com.dodorare.crossbow + +// import android.app.Activity +// import com.google.androidgamesdk.GameActivity + +// open class CrossbowGameActivity : GameActivity() { +// companion object { +// init { +// // This is necessary when any of the following happens: +// // - crossbow_android library is not configured to the following line in the manifest: +// // +// // - GameActivity derived class calls to the native code before calling +// // the super.onCreate() function. +// System.loadLibrary("crossbow_android") +// } +// } +// } diff --git a/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowLib.kt b/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowLib.kt new file mode 100644 index 00000000..fbbcae98 --- /dev/null +++ b/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowLib.kt @@ -0,0 +1,18 @@ +package com.dodorare.crossbow + +import android.app.Activity + +class CrossbowLib { + companion object { + init { + // This is necessary when any of the following happens: + // - crossbow_android library is not configured to the following line in the manifest: + // + // - GameActivity derived class calls to the native code before calling + // the super.onCreate() function. + System.loadLibrary("crossbow_android") + } + } + + external fun requestPermissionResult(p_permission: String, p_result: Boolean) +} diff --git a/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowNativeActivity.kt b/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowNativeActivity.kt new file mode 100644 index 00000000..bdf9715f --- /dev/null +++ b/platform/android/java/lib/src/com/dodorare/crossbow/CrossbowNativeActivity.kt @@ -0,0 +1,46 @@ +package com.dodorare.crossbow + +import android.app.Activity +import android.app.NativeActivity +import android.util.Log +import android.os.Bundle +import android.content.pm.PackageManager +import androidx.core.app.ActivityCompat + +open class CrossbowNativeActivity : NativeActivity(), ActivityCompat.OnRequestPermissionsResultCallback { + companion object { + init { + // This is necessary when any of the following happens: + // - crossbow_android library is not configured to the following line in the manifest: + // + // - GameActivity derived class calls to the native code before calling + // the super.onCreate() function. + System.loadLibrary("crossbow_android") + } + } + private var crossbowInstance: CrossbowLib? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + Log.v(TAG, "Creating new CrossbowLib instance") + crossbowInstance = CrossbowLib() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + // TODO: Replace with https://tedblob.com/onrequestpermissionsresult-deprecated-android-java/ + + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // for (CrossbowPlugin plugin : pluginRegistry.getAllPlugins()) { + // plugin.onMainRequestPermissionsResult(requestCode, permissions, grantResults) + // } + + for (i in permissions.indices) { + crossbowInstance?.requestPermissionResult(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED) + } + } +} diff --git a/platform/android/java/lib/src/com/dodorare/crossbow/Extensions.kt b/platform/android/java/lib/src/com/dodorare/crossbow/Extensions.kt new file mode 100644 index 00000000..c21999f5 --- /dev/null +++ b/platform/android/java/lib/src/com/dodorare/crossbow/Extensions.kt @@ -0,0 +1,7 @@ +package com.dodorare.crossbow + +val Any.TAG: String + get() { + val tag = javaClass.simpleName + return if (tag.length <= 23) tag else tag.substring(0, 23) + } diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle new file mode 100644 index 00000000..1cfad785 --- /dev/null +++ b/platform/android/java/settings.gradle @@ -0,0 +1,5 @@ +// Configure the root project. +rootProject.name = "Crossbow" + +include ":lib" +include ":app" diff --git a/platform/android/src/lib.rs b/platform/android/src/lib.rs new file mode 100644 index 00000000..de2b9bad --- /dev/null +++ b/platform/android/src/lib.rs @@ -0,0 +1,22 @@ +pub mod permission; + +use jni::{ + objects::{JClass, JString}, + sys::jboolean, + JNIEnv, +}; + +#[no_mangle] +#[allow(non_snake_case)] +unsafe extern "C" fn Java_com_dodorare_crossbow_CrossbowLib_requestPermissionResult( + _env: JNIEnv, + _class: JClass, + _p_permission: JString, + p_result: jboolean, +) { + println!("p_result: {:?}", p_result); +} + +pub fn init() { + println!("init"); +} diff --git a/plugins/permissions/src/error.rs b/platform/android/src/permission/error.rs similarity index 72% rename from plugins/permissions/src/error.rs rename to platform/android/src/permission/error.rs index 8e62f85b..20278a97 100644 --- a/plugins/permissions/src/error.rs +++ b/platform/android/src/permission/error.rs @@ -4,22 +4,18 @@ use thiserror::Error; /// `Result` type that used in `crossbow-permissions`. pub type Result = std::result::Result; -/// `Crossbow-permissions` error type. +/// Permissions error type. #[derive(Display, Debug, Error)] pub enum PermissionError { - /// Requesting permission on the wrong platform - PermissionWrongPlatform, /// Rust Jni library error - #[cfg(target_os = "android")] Jni(jni::errors::Error), /// Anyhow library errors Anyhow(anyhow::Error), } -#[cfg(target_os = "android")] impl From for PermissionError { fn from(error: jni::errors::Error) -> Self { - PermissionError::Jni(error.into()) + PermissionError::Jni(error) } } diff --git a/platform/android/src/permission/mod.rs b/platform/android/src/permission/mod.rs new file mode 100644 index 00000000..91db2559 --- /dev/null +++ b/platform/android/src/permission/mod.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod request_permission; +pub mod types; + +pub mod prelude { + pub use super::request_permission::*; + pub use super::types::android::*; +} diff --git a/platform/android/src/permission/request_permission.rs b/platform/android/src/permission/request_permission.rs new file mode 100644 index 00000000..38398397 --- /dev/null +++ b/platform/android/src/permission/request_permission.rs @@ -0,0 +1,121 @@ +use crate::permission::{error::*, types::android::*}; +use jni::signature as Signature; + +/// Create a java VM for executing Java calls +fn create_java_vm() -> Result<(ndk_context::AndroidContext, jni::JavaVM)> { + let ctx = ndk_context::android_context(); + let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?; + Ok((ctx, vm)) +} + +/// Find declared permissions in AndroidManifest.xml and return it as JValue type +fn get_permission_from_manifest<'a>( + permission: AndroidPermission, + java_env: &jni::AttachGuard<'a>, +) -> Result> { + // Find the android manifest class and get the permission + let class_manifest_permission = java_env.find_class(ANDROID_MANIFEST_PERMISSION)?; + let field_permission = java_env.get_static_field_id( + class_manifest_permission, + permission.to_string(), + MANIFEST_PERMISSION_SIGNATURE, + )?; + + // Convert the permission to the JValue type + let string_permission = java_env + .get_static_field_unchecked( + class_manifest_permission, + field_permission, + Signature::JavaType::Object(JAVA_STRING_SIGNATURE.to_owned()), + )? + .to_owned(); + Ok(string_permission) +} + +/// Get `PERMISSION_GRANTED` and `PERMISSION_DENIED` status +pub fn permission_status<'a>( + java_env: &jni::AttachGuard<'a>, +) -> Result<(jni::objects::JValue<'a>, jni::objects::JValue<'a>)> { + let class_package_manager = java_env.find_class(ANDROID_PACKAGE_MANAGER)?; + let field_permission_granted = java_env.get_static_field_id( + class_package_manager, + PERMISSIONS_GRANTED, + PRIMITIVE_INT_SIGNATURE, + )?; + + let field_permission_denied = java_env.get_static_field_id( + class_package_manager, + PERMISSION_DENIED, + PRIMITIVE_INT_SIGNATURE, + )?; + + let permission_denied = java_env.get_static_field_unchecked( + class_package_manager, + field_permission_denied, + Signature::JavaType::Primitive(Signature::Primitive::Int), + )?; + + let permission_granted = java_env.get_static_field_unchecked( + class_package_manager, + field_permission_granted, + Signature::JavaType::Primitive(Signature::Primitive::Int), + )?; + + Ok((permission_granted, permission_denied)) +} + +/// Provides checking permission status in the application and will request permission if it is denied. +pub fn request_permission(permission: AndroidPermission) -> Result { + let (ctx, vm) = create_java_vm()?; + let java_env = vm.attach_current_thread()?; + + let string_permission = get_permission_from_manifest(permission, &java_env)?; + + let (_permission_granted, permission_denied) = permission_status(&java_env)?; + + // Determine whether you have been granted a particular permission. + let class_context = java_env.find_class(ANDROID_CONTEXT)?; + let method_check_self_permission = java_env.get_method_id( + class_context, + CHECK_SELF_PERMISSION_METHOD, + CHECK_SELF_PERMISSION_SIGNATURE, + )?; + + let ret = java_env.call_method_unchecked( + ctx.context().cast(), + method_check_self_permission, + Signature::JavaType::Primitive(Signature::Primitive::Int), + &[string_permission], + )?; + + if ret.i()? == permission_denied.i()? { + let array_permissions = java_env.new_object_array( + ARRAY_LENGTH, + java_env.find_class(JAVA_STRING_SIGNATURE)?, + java_env.new_string(String::new())?, + )?; + + let string_permission = get_permission_from_manifest(permission, &java_env)?; + + java_env.set_object_array_element( + array_permissions, + OBJECT_INDEX, + string_permission.l()?, + )?; + let class_activity = java_env.find_class(ANDROID_ACTIVITY)?; + let method_request_permissions = java_env.get_method_id( + class_activity, + REQUEST_PERMISSIONS_METHOD, + REQUEST_PERMISSIONS_SIGNATURE, + )?; + + java_env.call_method_unchecked( + ctx.context().cast(), + method_request_permissions, + Signature::JavaType::Primitive(Signature::Primitive::Void), + &[array_permissions.into(), jni::objects::JValue::Int(0)], + )?; + } + + Ok(true) +} diff --git a/plugins/permissions/src/types/android_permission.rs b/platform/android/src/permission/types/android_permission.rs similarity index 100% rename from plugins/permissions/src/types/android_permission.rs rename to platform/android/src/permission/types/android_permission.rs diff --git a/plugins/permissions/src/types/android_permission_group.rs b/platform/android/src/permission/types/android_permission_group.rs similarity index 100% rename from plugins/permissions/src/types/android_permission_group.rs rename to platform/android/src/permission/types/android_permission_group.rs diff --git a/plugins/permissions/src/types/consts.rs b/platform/android/src/permission/types/consts.rs similarity index 100% rename from plugins/permissions/src/types/consts.rs rename to platform/android/src/permission/types/consts.rs diff --git a/plugins/permissions/src/types/mod.rs b/platform/android/src/permission/types/mod.rs similarity index 100% rename from plugins/permissions/src/types/mod.rs rename to platform/android/src/permission/types/mod.rs diff --git a/plugins/permissions/README.md b/plugins/permissions/README.md deleted file mode 100644 index e3bc25f6..00000000 --- a/plugins/permissions/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Crossbow Permissions - -The main purpose of this plugin is to give cross-platform access to permissions. \ No newline at end of file diff --git a/plugins/permissions/src/android/mod.rs b/plugins/permissions/src/android/mod.rs deleted file mode 100644 index cd375bb5..00000000 --- a/plugins/permissions/src/android/mod.rs +++ /dev/null @@ -1,69 +0,0 @@ -mod request_permission; - -pub use request_permission::*; - -use crate::types::android::*; -use jni::signature as Signature; - -/// Create a java VM for executing Java calls -fn create_java_vm() -> crate::error::Result<(ndk_context::AndroidContext, jni::JavaVM)> { - let ctx = ndk_context::android_context(); - let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }?; - Ok((ctx, vm)) -} - -/// Find declared permissions in AndroidManifest.xml and return it as JValue type -fn get_permission_from_manifest<'a>( - permission: AndroidPermission, - java_env: &jni::AttachGuard<'a>, -) -> crate::error::Result> { - // Find the android manifest class and get the permission - let class_manifest_permission = java_env.find_class(ANDROID_MANIFEST_PERMISSION)?; - let field_permission = java_env.get_static_field_id( - class_manifest_permission, - permission.to_string(), - MANIFEST_PERMISSION_SIGNATURE, - )?; - - // Convert the permission to the JValue type - let string_permission = java_env - .get_static_field_unchecked( - class_manifest_permission, - field_permission, - Signature::JavaType::Object(JAVA_STRING_SIGNATURE.to_owned()), - )? - .to_owned(); - Ok(string_permission) -} - -/// Get `PERMISSION_GRANTED` and `PERMISSION_DENIED` status -pub fn permission_status<'a>( - java_env: &jni::AttachGuard<'a>, -) -> crate::error::Result<(jni::objects::JValue<'a>, jni::objects::JValue<'a>)> { - let class_package_manager = java_env.find_class(ANDROID_PACKAGE_MANAGER)?; - let field_permission_granted = java_env.get_static_field_id( - class_package_manager, - PERMISSIONS_GRANTED, - PRIMITIVE_INT_SIGNATURE, - )?; - - let field_permission_denied = java_env.get_static_field_id( - class_package_manager, - PERMISSION_DENIED, - PRIMITIVE_INT_SIGNATURE, - )?; - - let permission_denied = java_env.get_static_field_unchecked( - class_package_manager, - field_permission_denied, - Signature::JavaType::Primitive(Signature::Primitive::Int), - )?; - - let permission_granted = java_env.get_static_field_unchecked( - class_package_manager, - field_permission_granted, - Signature::JavaType::Primitive(Signature::Primitive::Int), - )?; - - Ok((permission_granted, permission_denied)) -} diff --git a/plugins/permissions/src/android/request_permission.rs b/plugins/permissions/src/android/request_permission.rs deleted file mode 100644 index 2be537e9..00000000 --- a/plugins/permissions/src/android/request_permission.rs +++ /dev/null @@ -1,59 +0,0 @@ -use super::*; -use crate::{error::*, types::android::*}; -use jni::signature as Signature; - -/// Provides checking permission status in the application and will request permission if it is denied. -pub fn request_permission(permission: AndroidPermission) -> Result { - let (ctx, vm) = create_java_vm()?; - let java_env = vm.attach_current_thread()?; - - let string_permission = get_permission_from_manifest(permission, &java_env)?; - - let (_permission_granted, permission_denied) = permission_status(&java_env)?; - - // Determine whether you have been granted a particular permission. - let class_context = java_env.find_class(ANDROID_CONTEXT)?; - let method_check_self_permission = java_env.get_method_id( - class_context, - CHECK_SELF_PERMISSION_METHOD, - CHECK_SELF_PERMISSION_SIGNATURE, - )?; - - let ret = java_env.call_method_unchecked( - ctx.context().cast(), - method_check_self_permission, - Signature::JavaType::Primitive(Signature::Primitive::Int), - &[string_permission], - )?; - - if ret.i()? == permission_denied.i()? { - let array_permissions = java_env.new_object_array( - ARRAY_LENGTH.into(), - java_env.find_class(JAVA_STRING_SIGNATURE)?, - java_env.new_string(String::new())?, - )?; - - let string_permission = get_permission_from_manifest(permission, &java_env)?; - - java_env.set_object_array_element( - array_permissions, - OBJECT_INDEX.into(), - string_permission.l()?, - )?; - let class_activity = java_env.find_class(ANDROID_ACTIVITY)?; - let method_request_permissions = java_env.get_method_id( - class_activity, - REQUEST_PERMISSIONS_METHOD, - REQUEST_PERMISSIONS_SIGNATURE, - )?; - - java_env.call_method_unchecked( - ctx.context().cast(), - method_request_permissions, - Signature::JavaType::Primitive(Signature::Primitive::Void), - &[array_permissions.into(), jni::objects::JValue::Int(0)], - )?; - } - - Ok(true) -} diff --git a/plugins/permissions/src/lib.rs b/plugins/permissions/src/lib.rs deleted file mode 100644 index feccc7fe..00000000 --- a/plugins/permissions/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -#[cfg(target_os = "android")] -pub mod android; -pub mod error; -pub mod types; - -pub enum Permission { - AndroidPermission(types::android::AndroidPermission), - ApplePermission, -} - -pub fn request_permission(permission: Permission) -> error::Result { - match permission { - Permission::AndroidPermission(_p) => { - #[cfg(target_os = "android")] - return android::request_permission(_p); - - #[cfg(not(target_os = "android"))] - Err(error::PermissionError::PermissionWrongPlatform) - } - Permission::ApplePermission => Ok(false), - } -} - -pub mod prelude { - #[cfg(target_os = "android")] - pub use super::android::*; - pub use super::types::android::*; - - pub use super::{request_permission, Permission}; -} diff --git a/src/lib.rs b/src/lib.rs index 7fe92839..0adb08af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,8 @@ pub use crossbow_ads; #[cfg(feature = "crossbundle-tools")] pub use crossbundle_tools; -#[cfg(feature = "crossbow-permissions")] -pub use crossbow_permissions; - #[cfg(target_os = "android")] pub use ndk_glue; + +#[cfg(feature = "android")] +pub use crossbow_android;