diff --git a/crates/ability/Cargo.toml b/crates/ability/Cargo.toml index c0290c5..fe99fac 100644 --- a/crates/ability/Cargo.toml +++ b/crates/ability/Cargo.toml @@ -11,6 +11,9 @@ drag_and_drop = [] webview = ["dep:ohos-web-binding", "dep:http"] [dependencies] +# common dependencies +futures-channel = "0.3" + # for napi binding napi-ohos = { workspace = true, default-features = false, features = ["napi8"] } napi-derive-ohos = { workspace = true } diff --git a/crates/ability/src/app.rs b/crates/ability/src/app.rs index 6c675e7..d42abba 100644 --- a/crates/ability/src/app.rs +++ b/crates/ability/src/app.rs @@ -1,14 +1,18 @@ use std::{ + cell::Cell, cell::RefCell, fmt::Debug, + rc::Rc, sync::{ atomic::{AtomicBool, AtomicI64}, Arc, Mutex, RwLock, }, }; +use futures_channel::oneshot; use napi_ohos::{ - bindgen_prelude::{Function, JsObjectValue}, + bindgen_prelude::{CallbackContext, Function, JsObjectValue, Unknown}, + threadsafe_function::ThreadsafeFunctionCallMode, Error, Result, }; use ohos_arkui_binding::XComponent; @@ -17,8 +21,9 @@ use ohos_ime_binding::IME; use ohos_xcomponent_binding::RawWindow; use crate::{ - get_helper, get_main_thread_env, AbilityError, Configuration, Event, OpenHarmonyWaker, Rect, - WAKER, + get_helper, get_main_thread_env, get_permission_request_tsfn, unknown_to_permission_promise, + AbilityError, Configuration, Event, OpenHarmonyWaker, PermissionRequest, PermissionRequestCode, + PermissionRequestOutput, Rect, WAKER, }; static ID: AtomicI64 = AtomicI64::new(0); @@ -268,6 +273,85 @@ impl OpenHarmonyApp { self.inner.read().unwrap().exit(code).unwrap(); } + /// Request one or more runtime permissions through ArkTS helper. + /// Returns each requested permission and the corresponding request result code. + /// ! Don't call this function from main thread with block_on. + pub async fn request_permission

(&self, permission: P) -> Result> + where + P: Into, + { + let request = permission.into(); + let requested_permissions = request.permissions(); + let input = request.into_input(); + + let permission_tsfn = get_permission_request_tsfn().ok_or_else(|| { + Error::from_reason("requestPermission threadsafe function is not initialized") + })?; + + let (tx, rx) = oneshot::channel::>(); + let status = permission_tsfn.call_with_return_value( + input, + ThreadsafeFunctionCallMode::NonBlocking, + move |result, _| { + match result { + Ok(value) => { + let tx_cell = Rc::new(Cell::new(Some(tx))); + let tx_in_catch = tx_cell.clone(); + let promise = unknown_to_permission_promise(value)?; + promise + .then(move |ctx| { + if let Some(sender) = tx_cell.replace(None) { + let _ = sender.send(Ok(ctx.value)); + } + Ok(()) + })? + .catch(move |ctx: CallbackContext| { + if let Some(sender) = tx_in_catch.replace(None) { + let _ = sender.send(Err(ctx.value.into())); + } + Ok(()) + })?; + } + Err(err) => { + let _ = tx.send(Err(err)); + } + } + + Ok(()) + }, + ); + + if status != napi_ohos::Status::Ok { + return Err(Error::from_reason(format!( + "call requestPermission failed with status: {:?}", + status + ))); + } + + let output = rx + .await + .map_err(|_| Error::from_reason("requestPermission callback receiver dropped"))??; + + let codes = match output { + napi_ohos::Either::A(code) => vec![code], + napi_ohos::Either::B(codes) => codes, + }; + + if requested_permissions.len() != codes.len() { + return Err(Error::from_reason(format!( + "requestPermission result length mismatch: requested {}, got {}", + requested_permissions.len(), + codes.len() + ))); + } + + Ok(requested_permissions + .into_iter() + .zip(codes.into_iter()) + .map(|(permission, code)| PermissionRequestCode { permission, code }) + .collect()) + } + pub fn run_loop<'a, F: FnMut(Event) + 'a>(&self, mut event_handle: F) { if HAS_EVENT.load(std::sync::atomic::Ordering::SeqCst) { return; diff --git a/crates/ability/src/helper/mod.rs b/crates/ability/src/helper/mod.rs index dbc6127..b447561 100644 --- a/crates/ability/src/helper/mod.rs +++ b/crates/ability/src/helper/mod.rs @@ -2,10 +2,12 @@ use std::{cell::RefCell, rc::Rc}; use napi_ohos::{bindgen_prelude::ObjectRef, Env}; +mod permission; #[cfg(feature = "webview")] mod webview; mod window_info; +pub use permission::*; #[cfg(feature = "webview")] pub use webview::*; diff --git a/crates/ability/src/helper/permission.rs b/crates/ability/src/helper/permission.rs new file mode 100644 index 0000000..1f89e6a --- /dev/null +++ b/crates/ability/src/helper/permission.rs @@ -0,0 +1,133 @@ +use std::sync::{Arc, LazyLock, RwLock}; + +use napi_ohos::{ + bindgen_prelude::{Function, JsObjectValue, PromiseRaw, Unknown}, + threadsafe_function::ThreadsafeFunction, + Either, Env, Error, Result, Status, +}; + +use crate::get_main_thread_env; + +pub type PermissionRequestInput = Either>; +pub type PermissionRequestOutput = Either>; + +type PermissionRequestCall<'a> = Function<'a, PermissionRequestInput, Unknown<'a>>; + +type PermissionThreadsafeFunction = ThreadsafeFunction< + PermissionRequestInput, + Unknown<'static>, + PermissionRequestInput, + Status, + false, +>; + +type PermissionRequestTsfn = LazyLock>>>; + +pub(crate) static PERMISSION_REQUEST_TSFN: PermissionRequestTsfn = + LazyLock::new(|| RwLock::new(None)); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PermissionRequest { + Single(String), + Multiple(Vec), +} + +impl PermissionRequest { + pub fn permissions(&self) -> Vec { + match self { + Self::Single(permission) => vec![permission.clone()], + Self::Multiple(permissions) => permissions.clone(), + } + } + + pub fn into_input(self) -> PermissionRequestInput { + match self { + Self::Single(permission) => Either::A(permission), + Self::Multiple(permissions) => Either::B(permissions), + } + } +} + +impl From for PermissionRequest { + fn from(value: String) -> Self { + Self::Single(value) + } +} + +impl From<&str> for PermissionRequest { + fn from(value: &str) -> Self { + Self::Single(value.to_string()) + } +} + +impl From> for PermissionRequest { + fn from(value: Vec) -> Self { + Self::Multiple(value) + } +} + +impl From> for PermissionRequest { + fn from(value: Vec<&str>) -> Self { + Self::Multiple(value.into_iter().map(str::to_string).collect()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PermissionRequestCode { + pub permission: String, + pub code: i32, +} + +/// Create permission request threadsafe function. +/// The callback proxies to ArkTS helper.requestPermission(permission) and returns its Promise object. +pub fn create_permission_request_tsfn(env: &Env) -> Result> { + let permission_request_callback: Function<'_, PermissionRequestInput, Unknown<'_>> = env + .create_function_from_closure("permission_request_callback", move |ctx| { + let permission = ctx.first_arg::()?; + + if let Some(env_ref) = get_main_thread_env().borrow().as_ref() { + let helper = unsafe { crate::get_helper() }; + let helper_borrow = helper.borrow(); + if let Some(helper_ref) = helper_borrow.as_ref() { + let helper_obj = helper_ref.get_value(env_ref)?; + let request_permission_fn = helper_obj + .get_named_property::>("requestPermission")?; + return request_permission_fn.call(permission); + } + } + + Err(Error::from_reason( + "Failed to call helper.requestPermission from main thread", + )) + })?; + + let tsfn = permission_request_callback + .build_threadsafe_function() + .callee_handled::() + .build()?; + + let tsfn_arc = Arc::new(tsfn); + + { + let mut guard = (*PERMISSION_REQUEST_TSFN) + .write() + .map_err(|_| Error::from_reason("Failed to write PERMISSION_REQUEST_TSFN"))?; + guard.replace(tsfn_arc.clone()); + } + + Ok(tsfn_arc) +} + +pub fn get_permission_request_tsfn() -> Option> { + (*PERMISSION_REQUEST_TSFN) + .read() + .ok() + .and_then(|guard| guard.as_ref().map(Arc::clone)) +} + +pub fn unknown_to_permission_promise( + value: Unknown<'static>, +) -> Result> { + // Safety: ArkTS helper.requestPermission always returns a Promise. + unsafe { value.cast::>() } +} diff --git a/crates/ability/src/render/xcomponent.rs b/crates/ability/src/render/xcomponent.rs index 5f98691..eafffb6 100644 --- a/crates/ability/src/render/xcomponent.rs +++ b/crates/ability/src/render/xcomponent.rs @@ -4,8 +4,8 @@ use ohos_arkui_binding::{ArkUIHandle, RootNode, XComponent}; use ohos_ime_binding::IME; use crate::{ - input, set_helper, set_main_thread_env, Event, InputEvent, IntervalInfo, OpenHarmonyApp, Rect, - Size, + create_permission_request_tsfn, input, set_helper, set_main_thread_env, Event, InputEvent, + IntervalInfo, OpenHarmonyApp, Rect, Size, }; /// create lifecycle object and return to arkts @@ -18,6 +18,9 @@ pub fn render( set_helper(helper); set_main_thread_env(*env); + // Initialize permission request threadsafe function + let _ = create_permission_request_tsfn(env); + let mut root = RootNode::new(slot); let xcomponent_native = XComponent::new().map_err(|e| Error::from_reason(e.reason.to_string()))?; diff --git a/package/CHANGELOG.md b/package/CHANGELOG.md index d376c68..ad7bb65 100644 --- a/package/CHANGELOG.md +++ b/package/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.4.0-beta.2 +- Support requestPermission method + +--- # 0.4.0-beta.1 - Fix gesture for XComponent diff --git a/package/oh-package.json5 b/package/oh-package.json5 index 63b9410..57b31af 100644 --- a/package/oh-package.json5 +++ b/package/oh-package.json5 @@ -4,7 +4,7 @@ "name": "@ohos-rs/ability", "description": "Adaptor for OpenHarmony/HarmonyNext Application with Rust", "main": "index.ets", - "version": "0.4.0-beta.1", + "version": "0.4.0-beta.2", "repository": "https://github.com/harmony-contrib/openharmony-ability.git", "dependencies": {}, "keywords": [ diff --git a/rust_ability/ability_rust/src/main/ets/ability/type.ets b/rust_ability/ability_rust/src/main/ets/ability/type.ets index 0498890..b533065 100644 --- a/rust_ability/ability_rust/src/main/ets/ability/type.ets +++ b/rust_ability/ability_rust/src/main/ets/ability/type.ets @@ -66,4 +66,5 @@ export interface Module { export interface ArkHelper { exit: (code: number) => void; createWebview: (data: WebViewInitData) => Object; + requestPermission: (permission: string | string[]) => Promise; } diff --git a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets index 42fefe4..71ea587 100644 --- a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets +++ b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets @@ -2,6 +2,8 @@ import { NodeContent } from "@kit.ArkUI"; import { ArkHelper, WebViewInitData as NativeWebViewInitData } from "../ability/type"; import { exit } from "../helper"; import { Loadable } from "../helper/loadable"; +import { requestPermission } from "../helper/permission"; +import common from "@ohos.app.ability.common"; import { RustWebviewNodeController, WebviewStyle, @@ -16,6 +18,10 @@ export struct DefaultXComponent { private nativeModule: ESObject; private helper: ArkHelper = { exit, + requestPermission: async (permission: string | string[]): Promise => { + const context = this.getUIContext().getHostContext() as common.UIAbilityContext; + return await requestPermission(context, permission); + }, createWebview: (data: NativeWebViewInitData) => { const initScripts: ScriptItem[] = (data?.initializationScripts || []).map((i) => { return { diff --git a/rust_ability/ability_rust/src/main/ets/helper/index.ets b/rust_ability/ability_rust/src/main/ets/helper/index.ets index 941336f..a19ba1f 100644 --- a/rust_ability/ability_rust/src/main/ets/helper/index.ets +++ b/rust_ability/ability_rust/src/main/ets/helper/index.ets @@ -3,3 +3,5 @@ export * from "./os"; export * from "./random"; export * from "./object"; + +export * from "./permission"; diff --git a/rust_ability/ability_rust/src/main/ets/helper/permission.ets b/rust_ability/ability_rust/src/main/ets/helper/permission.ets new file mode 100644 index 0000000..a703589 --- /dev/null +++ b/rust_ability/ability_rust/src/main/ets/helper/permission.ets @@ -0,0 +1,54 @@ +/* + * Permission request helper for OpenHarmony. + * Returns Promise for single/multiple permission requests. + */ + +import abilityAccessCtrl, { Permissions } from "@ohos.abilityAccessCtrl"; +import common from "@ohos.app.ability.common"; +import hilog from "@ohos.hilog"; + +const TAG = "PermissionHelper"; +const REQUEST_FAILED: number = -1; + +function normalizePermission(permission: string | string[]): string[] { + return Array.isArray(permission) ? permission : [permission]; +} + +/** + * Request permission(s) and return auth result code(s). + * Single input -> single code; array input -> code array with same order. + */ +export async function requestPermission( + context: common.UIAbilityContext, + permission: string | string[], +): Promise { + const isArrayInput = Array.isArray(permission); + + if (!context) { + hilog.error(0x0000, TAG, "requestPermission: context is null"); + return isArrayInput ? [] : REQUEST_FAILED; + } + + const permissionList = normalizePermission(permission).filter((item: string) => !!item); + if (permissionList.length === 0) { + hilog.error(0x0000, TAG, "requestPermission: permission is empty"); + return isArrayInput ? [] : REQUEST_FAILED; + } + + const requestPermissions: Array = permissionList as Array; + const resultCodes: Array = new Array(permissionList.length).fill(REQUEST_FAILED); + + try { + const atManager = abilityAccessCtrl.createAtManager(); + const result = await atManager.requestPermissionsFromUser(context, requestPermissions); + const authResults: Array = result.authResults || []; + + for (let i = 0; i < resultCodes.length; i++) { + resultCodes[i] = authResults[i] ?? REQUEST_FAILED; + } + } catch (err) { + hilog.error(0x0000, TAG, `requestPermission: failed, error: ${JSON.stringify(err)}`); + } + + return isArrayInput ? resultCodes : resultCodes[0]; +} diff --git a/rust_example/xcomponent_example/Cargo.toml b/rust_example/xcomponent_example/Cargo.toml index 0f01d47..9d40576 100755 --- a/rust_example/xcomponent_example/Cargo.toml +++ b/rust_example/xcomponent_example/Cargo.toml @@ -10,12 +10,13 @@ publish = false crate-type = ["cdylib"] [dependencies] -napi-ohos = { workspace = true } +napi-ohos = { workspace = true, features = ["tokio_rt"] } napi-derive-ohos = { workspace = true } openharmony-ability = { workspace = true } openharmony-ability-derive = { workspace = true } ohos-hilog-binding = { version = "*" } +futures-executor = "0.3" [build-dependencies] napi-build-ohos = { workspace = true } diff --git a/rust_example/xcomponent_example/src/lib.rs b/rust_example/xcomponent_example/src/lib.rs index 36d2906..a75e422 100755 --- a/rust_example/xcomponent_example/src/lib.rs +++ b/rust_example/xcomponent_example/src/lib.rs @@ -1,17 +1,79 @@ -use std::sync::{LazyLock, RwLock}; +#![allow(dead_code)] +use std::sync::{ + atomic::{AtomicBool, Ordering}, + LazyLock, RwLock, +}; + +use napi_ohos::{Error, Result}; use ohos_hilog_binding::hilog_info; use openharmony_ability::{Event, InputEvent, OpenHarmonyApp}; use openharmony_ability_derive::ability; -#[allow(dead_code)] static INNER_APP: LazyLock>> = LazyLock::new(|| RwLock::new(None)); +static PERMISSION_REQUESTED: AtomicBool = AtomicBool::new(false); +static MAIN_THREAD_DEMO_REQUESTED: AtomicBool = AtomicBool::new(false); + +#[napi_derive_ohos::napi] +pub async fn demo_request_permission_from_main_thread() -> Result> { + if MAIN_THREAD_DEMO_REQUESTED.swap(true, Ordering::SeqCst) { + hilog_info!("main-thread demo request already triggered"); + return Ok(vec![]); + } + + let app = INNER_APP + .read() + .unwrap() + .as_ref() + .cloned() + .ok_or_else(|| Error::from_reason("OpenHarmony app not initialized"))?; + + let results = app.request_permission("ohos.permission.MICROPHONE").await?; + let mut codes = Vec::with_capacity(results.len()); + for item in results { + hilog_info!(format!( + "main-thread demo permission result => permission: {}, code: {}", + item.permission, item.code + ) + .as_str()); + codes.push(item.code); + } + + Ok(codes) +} #[ability] fn openharmony_app(app: OpenHarmonyApp) { INNER_APP.write().unwrap().replace(app.clone()); + let permission_app = app.clone(); - app.run_loop(|types| match types { + app.run_loop(move |types| match types { + Event::SurfaceCreate => { + hilog_info!("ohos-rs macro surface_create"); + if !PERMISSION_REQUESTED.swap(true, Ordering::SeqCst) { + let app_for_permission = permission_app.clone(); + std::thread::spawn(move || { + let permissions = vec!["ohos.permission.CAMERA"]; + let result = futures_executor::block_on( + app_for_permission.request_permission(permissions), + ); + match result { + Ok(results) => { + for item in results { + hilog_info!(format!( + "permission request result => permission: {}, code: {}", + item.permission, item.code + ) + .as_str()); + } + } + Err(err) => { + hilog_info!(format!("permission request failed: {}", err).as_str()); + } + } + }); + } + } Event::Input(k) => match k { InputEvent::ImeEvent(s) => { hilog_info!(format!("ohos-rs macro input_text: {:?}", s).as_str()); diff --git a/xcomponent_example/entry/src/main/ets/entryability/EntryAbility.ets b/xcomponent_example/entry/src/main/ets/entryability/EntryAbility.ets index 39d8f3f..09b710c 100644 --- a/xcomponent_example/entry/src/main/ets/entryability/EntryAbility.ets +++ b/xcomponent_example/entry/src/main/ets/entryability/EntryAbility.ets @@ -4,8 +4,8 @@ import { AbilityConstant } from "@kit.AbilityKit"; import window from "@ohos.window"; export default class EntryAbility extends RustAbility { - public moduleName: string = "hello_openharmony"; - public defaultPage: boolean = true; + public moduleName: string = "xcomponent_example"; + public defaultPage: boolean = false; async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { super.onCreate(want, launchParam); @@ -13,7 +13,9 @@ export default class EntryAbility extends RustAbility { async onWindowStageCreate(windowStage: window.WindowStage): Promise { const window = windowStage.getMainWindowSync(); - await window.setWindowLayoutFullScreen(true); + await window.setWindowLayoutFullScreen(false); super.onWindowStageCreate(windowStage); + + await windowStage.loadContent("pages/Index"); } } diff --git a/xcomponent_example/entry/src/main/ets/pages/Index.ets b/xcomponent_example/entry/src/main/ets/pages/Index.ets index 1af0cbb..3536940 100644 --- a/xcomponent_example/entry/src/main/ets/pages/Index.ets +++ b/xcomponent_example/entry/src/main/ets/pages/Index.ets @@ -1,36 +1,21 @@ import { DefaultXComponent } from "@ohos-rs/ability"; -import { - ItemRestriction, - SegmentButton, - SegmentButtonOptions, - SegmentButtonTextItem, -} from "@kit.ArkUI"; -import { changeRender } from "libwgpu_in_app.so"; +import { demoRequestPermissionFromMainThread } from "libxcomponent_example.so"; @Entry @Component struct Index { - @State tabOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({ - buttons: [ - { text: "boids" }, - { text: "MSAA line" }, - { text: "cube" }, - { text: "water" }, - { text: "shadow" }, - ] as ItemRestriction, - backgroundBlurStyle: BlurStyle.BACKGROUND_THICK, - }); - @State @Watch("handleChange") tabSelectedIndexes: number[] = [0]; + private permissionDemoCalled: boolean = false; - handleChange() { - console.log(`changeIndex: ${this.tabSelectedIndexes}`); - changeRender(this.tabSelectedIndexes[0]); + async handleClick() { + const re: number[] = await demoRequestPermissionFromMainThread(); + console.log(`${re}`); } build() { Row() { Column() { - SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes }) + Button() + .onClick(() => this.handleClick()) DefaultXComponent() }.width("100%") }.height("100%"); diff --git a/xcomponent_example/entry/src/main/module.json5 b/xcomponent_example/entry/src/main/module.json5 index 83ca116..9578c76 100644 --- a/xcomponent_example/entry/src/main/module.json5 +++ b/xcomponent_example/entry/src/main/module.json5 @@ -47,6 +47,18 @@ } ] } + ], + "requestPermissions": [ + { + "name": "ohos.permission.CAMERA", + "reason": "$string:module_desc", + "usedScene": {} + }, + { + "name": "ohos.permission.MICROPHONE", + "reason": "$string:page_show", + "usedScene": {} + } ] } }