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": {}
+ }
]
}
}