From 61a1bfdbec5e80e98a44baf7b8d49e9f2868be80 Mon Sep 17 00:00:00 2001 From: richerfu Date: Mon, 2 Feb 2026 23:14:05 +0800 Subject: [PATCH 1/3] feat: add requestPermission method --- crates/ability/src/helper/mod.rs | 2 + crates/ability/src/helper/permission.rs | 71 +++++++++++++ crates/ability/src/render/xcomponent.rs | 5 +- .../src/main/ets/ability/type.ets | 1 + .../main/ets/components/DefaultXComponent.ets | 6 ++ .../src/main/ets/helper/index.ets | 2 + .../src/main/ets/helper/permission.ets | 99 +++++++++++++++++++ 7 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 crates/ability/src/helper/permission.rs create mode 100644 rust_ability/ability_rust/src/main/ets/helper/permission.ets diff --git a/crates/ability/src/helper/mod.rs b/crates/ability/src/helper/mod.rs index dbc6127..9e90b02 100644 --- a/crates/ability/src/helper/mod.rs +++ b/crates/ability/src/helper/mod.rs @@ -5,9 +5,11 @@ use napi_ohos::{bindgen_prelude::ObjectRef, Env}; #[cfg(feature = "webview")] mod webview; mod window_info; +mod permission; #[cfg(feature = "webview")] pub use webview::*; +pub use permission::*; thread_local! { static HELPER: Rc>> = Rc::new(RefCell::new(None)); diff --git a/crates/ability/src/helper/permission.rs b/crates/ability/src/helper/permission.rs new file mode 100644 index 0000000..eb65126 --- /dev/null +++ b/crates/ability/src/helper/permission.rs @@ -0,0 +1,71 @@ +use std::sync::{Arc, LazyLock, RwLock}; + +use napi_ohos::{ + bindgen_prelude::Function, threadsafe_function::ThreadsafeFunction, Env, Result, Status, +}; + +use crate::get_main_thread_env; + +type PermissionRequestTsfn = + LazyLock>>>>; + +pub(crate) static PERMISSION_REQUEST_TSFN: PermissionRequestTsfn = + LazyLock::new(|| RwLock::new(None)); + +/// Create permission request threadsafe function +/// This function creates a threadsafe function that can be called from C layer +/// to request permissions from ArkTS layer +pub fn create_permission_request_tsfn( + env: &Env, +) -> Result>> { + // Create a function that will be called from ArkTS when permission is requested + let permission_request_callback: Function<'_, String, ()> = + env.create_function_from_closure("permission_request_callback", move |ctx| { + let permission = ctx.first_arg::()?; + + // Get the helper object from thread-local storage + if let Some(env_ref) = get_main_thread_env().borrow().as_ref() { + let helper = unsafe { crate::get_helper() }; + if let Some(helper_ref) = helper.borrow().as_ref() { + if let Ok(helper_value) = helper_ref.get_value(env_ref) { + if let Ok(request_permission_fn) = helper_value + .get_named_property::>("requestPermission") + { + // Call the ArkTS requestPermission function + if let Err(e) = request_permission_fn.call(permission.clone()) { + eprintln!("Failed to call requestPermission: {:?}", e); + } + } + } + } + } + + Ok(()) + })?; + + 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(|_| { + napi_ohos::Error::from_reason("Failed to write PERMISSION_REQUEST_TSFN") + })?; + guard.replace(tsfn_arc.clone()); + } + + Ok(tsfn_arc) +} + +/// Get the permission request threadsafe function +/// This can be called from C layer to get the threadsafe function +pub fn get_permission_request_tsfn( +) -> Option>> { + (*PERMISSION_REQUEST_TSFN) + .read() + .ok() + .and_then(|guard| guard.as_ref().map(Arc::clone)) +} diff --git a/crates/ability/src/render/xcomponent.rs b/crates/ability/src/render/xcomponent.rs index 5f98691..63f01ae 100644 --- a/crates/ability/src/render/xcomponent.rs +++ b/crates/ability/src/render/xcomponent.rs @@ -5,7 +5,7 @@ use ohos_ime_binding::IME; use crate::{ input, set_helper, set_main_thread_env, Event, InputEvent, IntervalInfo, OpenHarmonyApp, Rect, - Size, + Size, create_permission_request_tsfn, }; /// create lifecycle object and return to arkts @@ -17,6 +17,9 @@ pub fn render( ) -> Result { 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 = 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..9b4baf9 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) => 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..5427018 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): Promise => { + const context = this.getContext() as common.UIAbilityContext; + return await requestPermission(context, permission, this.nativeModule); + }, 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..5781ddd --- /dev/null +++ b/rust_ability/ability_rust/src/main/ets/helper/permission.ets @@ -0,0 +1,99 @@ +/* + * Permission request helper for OpenHarmony + * This module provides permission request functionality that can be called from native code + */ + +import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'; +import common from '@ohos.app.ability.common'; +import hilog from '@ohos.hilog'; + +const TAG = 'PermissionHelper'; +const APPROVAL: number = 0; + +// Permission mapping +const PermissionsMap: Map = new Map(); +PermissionsMap.set('ohos.permission.WRITE_MEDIA', 'ohos.permission.WRITE_MEDIA'); +PermissionsMap.set('ohos.permission.MICROPHONE', 'ohos.permission.MICROPHONE'); +PermissionsMap.set('ohos.permission.CAMERA', 'ohos.permission.CAMERA'); +PermissionsMap.set('ohos.permission.READ_MEDIA', 'ohos.permission.READ_MEDIA'); +PermissionsMap.set('ohos.permission.INTERNET', 'ohos.permission.INTERNET'); +PermissionsMap.set('ohos.permission.LOCATION', 'ohos.permission.LOCATION'); +PermissionsMap.set('ohos.permission.ACCESS_FINE_LOCATION', 'ohos.permission.ACCESS_FINE_LOCATION'); +PermissionsMap.set('ohos.permission.ACCESS_COARSE_LOCATION', 'ohos.permission.ACCESS_COARSE_LOCATION'); + +/** + * Request permission from user + * This is an async function that should be called from native code via threadsafe function + * + * @param context - UIAbilityContext for requesting permission + * @param permission - Permission string (e.g., 'ohos.permission.MICROPHONE') + * @param nativeModule - Native module that exports permissionResult function + * @returns Promise - true if granted, false if denied + */ +export async function requestPermission( + context: common.UIAbilityContext, + permission: string, + nativeModule: ESObject | null +): Promise { + if (!context) { + hilog.error(0x0000, TAG, 'requestPermission: context is null'); + if (nativeModule && typeof nativeModule.permissionResult === 'function') { + nativeModule.permissionResult(false); + } + return false; + } + + if (!permission) { + hilog.error(0x0000, TAG, 'requestPermission: permission is null'); + if (nativeModule && typeof nativeModule.permissionResult === 'function') { + nativeModule.permissionResult(false); + } + return false; + } + + const permissionValue = PermissionsMap.get(permission); + if (!permissionValue) { + hilog.warn(0x0000, TAG, `requestPermission: Unknown permission ${permission}, assuming granted`); + if (nativeModule && typeof nativeModule.permissionResult === 'function') { + nativeModule.permissionResult(true); + } + return true; + } + + const permissionsList: Array = [permissionValue]; + + hilog.info(0x0000, TAG, `requestPermission: Requesting permission ${permission}`); + + try { + // Create AtManager instance + const atManager = abilityAccessCtrl.createAtManager(); + + // Request permission asynchronously + const result = await atManager.requestPermissionsFromUser(context, permissionsList); + const grantStatus: Array = result.authResults; + + // Check if permission is granted + let granted = false; + for (let i = 0; i < grantStatus.length; i++) { + if (grantStatus[i] === APPROVAL) { + granted = true; + break; + } + } + + hilog.info(0x0000, TAG, `requestPermission: Permission ${permission} ${granted ? 'granted' : 'denied'}`); + + // Notify native code of the result + if (nativeModule && typeof nativeModule.permissionResult === 'function') { + nativeModule.permissionResult(granted); + } + + return granted; + } catch (err) { + hilog.error(0x0000, TAG, `requestPermission: Failed to request permission ${permission}, error: ${JSON.stringify(err)}`); + if (nativeModule && typeof nativeModule.permissionResult === 'function') { + nativeModule.permissionResult(false); + } + return false; + } +} From 2e1a952f3b85e780a26fdd340afb5b857f1f8683 Mon Sep 17 00:00:00 2001 From: richerfu Date: Thu, 12 Feb 2026 09:15:04 +0800 Subject: [PATCH 2/3] feat: add requestPermission method --- crates/ability/Cargo.toml | 3 + crates/ability/src/app.rs | 90 +++++++++++- crates/ability/src/helper/mod.rs | 4 +- crates/ability/src/helper/permission.rs | 130 +++++++++++++----- crates/ability/src/render/xcomponent.rs | 6 +- .../src/main/ets/ability/type.ets | 2 +- .../main/ets/components/DefaultXComponent.ets | 8 +- .../src/main/ets/helper/permission.ets | 107 +++++--------- rust_example/xcomponent_example/Cargo.toml | 3 +- rust_example/xcomponent_example/src/lib.rs | 68 ++++++++- .../main/ets/entryability/EntryAbility.ets | 8 +- .../entry/src/main/ets/pages/Index.ets | 29 +--- .../entry/src/main/module.json5 | 12 ++ 13 files changed, 318 insertions(+), 152 deletions(-) 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 9e90b02..b447561 100644 --- a/crates/ability/src/helper/mod.rs +++ b/crates/ability/src/helper/mod.rs @@ -2,14 +2,14 @@ use std::{cell::RefCell, rc::Rc}; use napi_ohos::{bindgen_prelude::ObjectRef, Env}; +mod permission; #[cfg(feature = "webview")] mod webview; mod window_info; -mod permission; +pub use permission::*; #[cfg(feature = "webview")] pub use webview::*; -pub use permission::*; thread_local! { static HELPER: Rc>> = Rc::new(RefCell::new(None)); diff --git a/crates/ability/src/helper/permission.rs b/crates/ability/src/helper/permission.rs index eb65126..1f89e6a 100644 --- a/crates/ability/src/helper/permission.rs +++ b/crates/ability/src/helper/permission.rs @@ -1,46 +1,104 @@ use std::sync::{Arc, LazyLock, RwLock}; use napi_ohos::{ - bindgen_prelude::Function, threadsafe_function::ThreadsafeFunction, Env, Result, Status, + bindgen_prelude::{Function, JsObjectValue, PromiseRaw, Unknown}, + threadsafe_function::ThreadsafeFunction, + Either, Env, Error, Result, Status, }; use crate::get_main_thread_env; -type PermissionRequestTsfn = - LazyLock>>>>; +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)); -/// Create permission request threadsafe function -/// This function creates a threadsafe function that can be called from C layer -/// to request permissions from ArkTS layer -pub fn create_permission_request_tsfn( - env: &Env, -) -> Result>> { - // Create a function that will be called from ArkTS when permission is requested - let permission_request_callback: Function<'_, String, ()> = - env.create_function_from_closure("permission_request_callback", move |ctx| { - let permission = ctx.first_arg::()?; - - // Get the helper object from thread-local storage +#[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() }; - if let Some(helper_ref) = helper.borrow().as_ref() { - if let Ok(helper_value) = helper_ref.get_value(env_ref) { - if let Ok(request_permission_fn) = helper_value - .get_named_property::>("requestPermission") - { - // Call the ArkTS requestPermission function - if let Err(e) = request_permission_fn.call(permission.clone()) { - eprintln!("Failed to call requestPermission: {:?}", e); - } - } - } + 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); } } - Ok(()) + Err(Error::from_reason( + "Failed to call helper.requestPermission from main thread", + )) })?; let tsfn = permission_request_callback @@ -51,21 +109,25 @@ pub fn create_permission_request_tsfn( let tsfn_arc = Arc::new(tsfn); { - let mut guard = (*PERMISSION_REQUEST_TSFN).write().map_err(|_| { - napi_ohos::Error::from_reason("Failed to write PERMISSION_REQUEST_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) } -/// Get the permission request threadsafe function -/// This can be called from C layer to get the threadsafe function -pub fn get_permission_request_tsfn( -) -> Option>> { +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 63f01ae..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, + create_permission_request_tsfn, input, set_helper, set_main_thread_env, Event, InputEvent, + IntervalInfo, OpenHarmonyApp, Rect, Size, }; /// create lifecycle object and return to arkts @@ -17,7 +17,7 @@ pub fn render( ) -> Result { set_helper(helper); set_main_thread_env(*env); - + // Initialize permission request threadsafe function let _ = create_permission_request_tsfn(env); 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 9b4baf9..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,5 +66,5 @@ export interface Module { export interface ArkHelper { exit: (code: number) => void; createWebview: (data: WebViewInitData) => Object; - requestPermission: (permission: string) => Promise; + 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 5427018..71ea587 100644 --- a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets +++ b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets @@ -3,7 +3,7 @@ import { ArkHelper, WebViewInitData as NativeWebViewInitData } from "../ability/ import { exit } from "../helper"; import { Loadable } from "../helper/loadable"; import { requestPermission } from "../helper/permission"; -import common from '@ohos.app.ability.common'; +import common from "@ohos.app.ability.common"; import { RustWebviewNodeController, WebviewStyle, @@ -18,9 +18,9 @@ export struct DefaultXComponent { private nativeModule: ESObject; private helper: ArkHelper = { exit, - requestPermission: async (permission: string): Promise => { - const context = this.getContext() as common.UIAbilityContext; - return await requestPermission(context, permission, this.nativeModule); + 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) => { diff --git a/rust_ability/ability_rust/src/main/ets/helper/permission.ets b/rust_ability/ability_rust/src/main/ets/helper/permission.ets index 5781ddd..a703589 100644 --- a/rust_ability/ability_rust/src/main/ets/helper/permission.ets +++ b/rust_ability/ability_rust/src/main/ets/helper/permission.ets @@ -1,99 +1,54 @@ /* - * Permission request helper for OpenHarmony - * This module provides permission request functionality that can be called from native code + * 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'; +import abilityAccessCtrl, { Permissions } from "@ohos.abilityAccessCtrl"; +import common from "@ohos.app.ability.common"; +import hilog from "@ohos.hilog"; -const TAG = 'PermissionHelper'; -const APPROVAL: number = 0; +const TAG = "PermissionHelper"; +const REQUEST_FAILED: number = -1; -// Permission mapping -const PermissionsMap: Map = new Map(); -PermissionsMap.set('ohos.permission.WRITE_MEDIA', 'ohos.permission.WRITE_MEDIA'); -PermissionsMap.set('ohos.permission.MICROPHONE', 'ohos.permission.MICROPHONE'); -PermissionsMap.set('ohos.permission.CAMERA', 'ohos.permission.CAMERA'); -PermissionsMap.set('ohos.permission.READ_MEDIA', 'ohos.permission.READ_MEDIA'); -PermissionsMap.set('ohos.permission.INTERNET', 'ohos.permission.INTERNET'); -PermissionsMap.set('ohos.permission.LOCATION', 'ohos.permission.LOCATION'); -PermissionsMap.set('ohos.permission.ACCESS_FINE_LOCATION', 'ohos.permission.ACCESS_FINE_LOCATION'); -PermissionsMap.set('ohos.permission.ACCESS_COARSE_LOCATION', 'ohos.permission.ACCESS_COARSE_LOCATION'); +function normalizePermission(permission: string | string[]): string[] { + return Array.isArray(permission) ? permission : [permission]; +} /** - * Request permission from user - * This is an async function that should be called from native code via threadsafe function - * - * @param context - UIAbilityContext for requesting permission - * @param permission - Permission string (e.g., 'ohos.permission.MICROPHONE') - * @param nativeModule - Native module that exports permissionResult function - * @returns Promise - true if granted, false if denied + * 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, - nativeModule: ESObject | null -): Promise { - if (!context) { - hilog.error(0x0000, TAG, 'requestPermission: context is null'); - if (nativeModule && typeof nativeModule.permissionResult === 'function') { - nativeModule.permissionResult(false); - } - return false; - } + permission: string | string[], +): Promise { + const isArrayInput = Array.isArray(permission); - if (!permission) { - hilog.error(0x0000, TAG, 'requestPermission: permission is null'); - if (nativeModule && typeof nativeModule.permissionResult === 'function') { - nativeModule.permissionResult(false); - } - return false; + if (!context) { + hilog.error(0x0000, TAG, "requestPermission: context is null"); + return isArrayInput ? [] : REQUEST_FAILED; } - const permissionValue = PermissionsMap.get(permission); - if (!permissionValue) { - hilog.warn(0x0000, TAG, `requestPermission: Unknown permission ${permission}, assuming granted`); - if (nativeModule && typeof nativeModule.permissionResult === 'function') { - nativeModule.permissionResult(true); - } - return true; + 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 permissionsList: Array = [permissionValue]; - - hilog.info(0x0000, TAG, `requestPermission: Requesting permission ${permission}`); + const requestPermissions: Array = permissionList as Array; + const resultCodes: Array = new Array(permissionList.length).fill(REQUEST_FAILED); try { - // Create AtManager instance const atManager = abilityAccessCtrl.createAtManager(); + const result = await atManager.requestPermissionsFromUser(context, requestPermissions); + const authResults: Array = result.authResults || []; - // Request permission asynchronously - const result = await atManager.requestPermissionsFromUser(context, permissionsList); - const grantStatus: Array = result.authResults; - - // Check if permission is granted - let granted = false; - for (let i = 0; i < grantStatus.length; i++) { - if (grantStatus[i] === APPROVAL) { - granted = true; - break; - } - } - - hilog.info(0x0000, TAG, `requestPermission: Permission ${permission} ${granted ? 'granted' : 'denied'}`); - - // Notify native code of the result - if (nativeModule && typeof nativeModule.permissionResult === 'function') { - nativeModule.permissionResult(granted); + for (let i = 0; i < resultCodes.length; i++) { + resultCodes[i] = authResults[i] ?? REQUEST_FAILED; } - - return granted; } catch (err) { - hilog.error(0x0000, TAG, `requestPermission: Failed to request permission ${permission}, error: ${JSON.stringify(err)}`); - if (nativeModule && typeof nativeModule.permissionResult === 'function') { - nativeModule.permissionResult(false); - } - return false; + 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": {} + } ] } } From 8f90d68bf0d763588341e59ccad314fed023a730 Mon Sep 17 00:00:00 2001 From: richerfu Date: Thu, 12 Feb 2026 09:26:46 +0800 Subject: [PATCH 3/3] 0.4.0-beta.2 --- package/CHANGELOG.md | 4 ++++ package/oh-package.json5 | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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": [