Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/ability/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
90 changes: 87 additions & 3 deletions crates/ability/src/app.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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<P>(&self, permission: P) -> Result<Vec<PermissionRequestCode>>
where
P: Into<PermissionRequest>,
{
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::<Result<PermissionRequestOutput>>();
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<Unknown>| {
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;
Expand Down
2 changes: 2 additions & 0 deletions crates/ability/src/helper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
133 changes: 133 additions & 0 deletions crates/ability/src/helper/permission.rs
Original file line number Diff line number Diff line change
@@ -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<String, Vec<String>>;
pub type PermissionRequestOutput = Either<i32, Vec<i32>>;

type PermissionRequestCall<'a> = Function<'a, PermissionRequestInput, Unknown<'a>>;

type PermissionThreadsafeFunction = ThreadsafeFunction<
PermissionRequestInput,
Unknown<'static>,
PermissionRequestInput,
Status,
false,
>;

type PermissionRequestTsfn = LazyLock<RwLock<Option<Arc<PermissionThreadsafeFunction>>>>;

pub(crate) static PERMISSION_REQUEST_TSFN: PermissionRequestTsfn =
LazyLock::new(|| RwLock::new(None));

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PermissionRequest {
Single(String),
Multiple(Vec<String>),
}

impl PermissionRequest {
pub fn permissions(&self) -> Vec<String> {
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<String> 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<Vec<String>> for PermissionRequest {
fn from(value: Vec<String>) -> Self {
Self::Multiple(value)
}
}

impl From<Vec<&str>> 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<Arc<PermissionThreadsafeFunction>> {
let permission_request_callback: Function<'_, PermissionRequestInput, Unknown<'_>> = env
.create_function_from_closure("permission_request_callback", move |ctx| {
let permission = ctx.first_arg::<PermissionRequestInput>()?;

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::<PermissionRequestCall<'_>>("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::<false>()
.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<Arc<PermissionThreadsafeFunction>> {
(*PERMISSION_REQUEST_TSFN)
.read()
.ok()
.and_then(|guard| guard.as_ref().map(Arc::clone))
}

pub fn unknown_to_permission_promise(
value: Unknown<'static>,
) -> Result<PromiseRaw<'static, PermissionRequestOutput>> {
// Safety: ArkTS helper.requestPermission always returns a Promise<number | number[]>.
unsafe { value.cast::<PromiseRaw<'static, PermissionRequestOutput>>() }
}
7 changes: 5 additions & 2 deletions crates/ability/src/render/xcomponent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()))?;
Expand Down
4 changes: 4 additions & 0 deletions package/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 0.4.0-beta.2
- Support requestPermission method

---
# 0.4.0-beta.1
- Fix gesture for XComponent

Expand Down
2 changes: 1 addition & 1 deletion package/oh-package.json5
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
1 change: 1 addition & 0 deletions rust_ability/ability_rust/src/main/ets/ability/type.ets
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,5 @@ export interface Module {
export interface ArkHelper {
exit: (code: number) => void;
createWebview: (data: WebViewInitData) => Object;
requestPermission: (permission: string | string[]) => Promise<number | number[]>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,6 +18,10 @@ export struct DefaultXComponent {
private nativeModule: ESObject;
private helper: ArkHelper = {
exit,
requestPermission: async (permission: string | string[]): Promise<number | number[]> => {
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
return await requestPermission(context, permission);
},
createWebview: (data: NativeWebViewInitData) => {
const initScripts: ScriptItem[] = (data?.initializationScripts || []).map((i) => {
return {
Expand Down
2 changes: 2 additions & 0 deletions rust_ability/ability_rust/src/main/ets/helper/index.ets
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export * from "./os";
export * from "./random";

export * from "./object";

export * from "./permission";
54 changes: 54 additions & 0 deletions rust_ability/ability_rust/src/main/ets/helper/permission.ets
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Permission request helper for OpenHarmony.
* Returns Promise<number | number[]> 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<number | number[]> {
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<Permissions> = permissionList as Array<Permissions>;
const resultCodes: Array<number> = new Array(permissionList.length).fill(REQUEST_FAILED);

try {
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(context, requestPermissions);
const authResults: Array<number> = 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];
}
Loading