From 0784cf06d30d2011a67b3f315affc39c7c4f4587 Mon Sep 17 00:00:00 2001 From: springmin Date: Mon, 19 Oct 2026 13:36:37 +0800 Subject: [PATCH] ohos: request the pasteboard ACL permission for paste Reading the system pasteboard requires ohos.permission.READ_PASTEBOARD (system_basic, user_grant). Declare it with a localized reason, resolve the reasonId while packing the hap (the packing tool normally does this), and let the page ask for the permission when a paste is denied; a toast explains the denial. The hap now only installs with an ACL-enabled signing profile that lists the permission; the README documents the requirement. --- platform/ohos/README.md | 6 +++ platform/ohos/backend/src/clipboard.rs | 16 +++++++ platform/ohos/mail/src/bridge.rs | 14 ++++++ platform/ohos/mail/src/lib.rs | 2 + platform/ohos/scripts/build-hap.sh | 15 ++++-- .../shell/entry/src/main/ets/pages/Index.ets | 47 ++++++++++++++++++- .../ohos/shell/entry/src/main/module.json5 | 14 ++++-- .../main/resources/base/element/string.json | 6 ++- .../main/resources/zh_CN/element/string.json | 8 ++++ .../ohos/shell/page-templates/Index.mail.ets | 47 ++++++++++++++++++- 10 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 platform/ohos/shell/entry/src/main/resources/zh_CN/element/string.json diff --git a/platform/ohos/README.md b/platform/ohos/README.md index 67473f7c..48415509 100644 --- a/platform/ohos/README.md +++ b/platform/ohos/README.md @@ -156,6 +156,12 @@ handled (and asserted) in the script, do not "simplify" them away: from the sync engine is published through `@kit.NotificationKit` when the in-app notifications setting is on. A refusal is final at the system level; the user can re-enable notifications in the system settings. +- **Clipboard**: copying needs no permission, but pasting reads the system + pasteboard and therefore requires `ohos.permission.READ_PASTEBOARD`, an ACL + (restricted) permission. The module declares it and the page requests it on + the first denied paste. The signing profile must list the permission in + `acls.allowed-acls`; a hap built without such a profile fails to install + with "grant request permissions failed". - **Data**: the application keeps its data under the standard bundle data path. Import/export goes through the system document picker via the host `DocumentProvider`, and credentials live in the system Asset Store, never diff --git a/platform/ohos/backend/src/clipboard.rs b/platform/ohos/backend/src/clipboard.rs index 9cd88a47..2bf92006 100644 --- a/platform/ohos/backend/src/clipboard.rs +++ b/platform/ohos/backend/src/clipboard.rs @@ -7,6 +7,16 @@ use core::ffi::{CStr, c_int}; use std::ffi::CString; +use std::sync::OnceLock; + +/// Hook installed by the shell to ask for `ohos.permission.READ_PASTEBOARD` +/// after a denied read. Tests and hosts without a bridge leave it unset. +static PERMISSION_HOOK: OnceLock = OnceLock::new(); + +/// Install the hook that requests the pasteboard permission from the user. +pub fn set_permission_hook(hook: fn()) { + let _ = PERMISSION_HOOK.set(hook); +} use crate::ffi; @@ -28,6 +38,12 @@ pub fn read_text() -> Option { // denial (e.g. 201: missing READ_PASTEBOARD). if status != 0 { crate::log::error(&format!("clipboard: read failed (status {status})")); + // 201 is "permission denied": let the shell ask the user once. + if status == 201 + && let Some(hook) = PERMISSION_HOOK.get() + { + (hook)(); + } } } else { let plain = ffi::OH_UdsPlainText_Create(); diff --git a/platform/ohos/mail/src/bridge.rs b/platform/ohos/mail/src/bridge.rs index 8c22598c..9dabc35d 100644 --- a/platform/ohos/mail/src/bridge.rs +++ b/platform/ohos/mail/src/bridge.rs @@ -28,6 +28,7 @@ static PICK_SAVE: AtomicUsize = AtomicUsize::new(0); static SHOW_IME: AtomicUsize = AtomicUsize::new(0); static HIDE_IME: AtomicUsize = AtomicUsize::new(0); static NOTIFY: AtomicUsize = AtomicUsize::new(0); +static REQUEST_PASTEBOARD: AtomicUsize = AtomicUsize::new(0); /// Request ids for the callback-based picker flows. static NEXT_REQUEST: AtomicI64 = AtomicI64::new(1); @@ -80,6 +81,13 @@ pub(super) unsafe extern "C" fn set_bridge( install_property(env, argv[0], b"showIme\0", &SHOW_IME, call_zero_arg); install_property(env, argv[0], b"hideIme\0", &HIDE_IME, call_zero_arg); install_property(env, argv[0], b"notify\0", &NOTIFY, call_notify); + install_property( + env, + argv[0], + b"requestPasteboard\0", + &REQUEST_PASTEBOARD, + call_zero_arg, + ); undefined(env) } } @@ -209,6 +217,12 @@ pub(super) fn hide_ime() { let _ = call_closure(&HIDE_IME, "hideIme", ()); } +/// Ask the page to request the pasteboard permission (called from the +/// clipboard fallback on a denied read). +pub(super) fn request_pasteboard() { + let _ = call_closure(&REQUEST_PASTEBOARD, "requestPasteboard", ()); +} + /// Publish a system notification through the ArkTS hook. pub(super) fn notify(title: &str, body: &str) { let request = NotifyRequest { diff --git a/platform/ohos/mail/src/lib.rs b/platform/ohos/mail/src/lib.rs index ee3a244e..6df1427b 100644 --- a/platform/ohos/mail/src/lib.rs +++ b/platform/ohos/mail/src/lib.rs @@ -260,6 +260,8 @@ fn run_mail() -> std::result::Result<(), Box void; hideIme: () => void; notify: (title: string, body: string) => void; + requestPasteboard: () => void; } // The exports of the native XComponent module (`onLoad` hands them over). @@ -33,6 +34,7 @@ interface FlectarNativeModule { struct Index { private nativeModule: FlectarNativeModule | undefined = undefined; private imeAttached: boolean = false; + private pasteboardPermissionRequested: boolean = false; aboutToAppear(): void { this.probeResources(); @@ -120,6 +122,9 @@ struct Index { notify: (title: string, body: string): void => { this.notify(title, body); }, + requestPasteboard: (): void => { + this.requestPasteboardPermission(); + }, }); } @@ -300,6 +305,46 @@ struct Index { } } + // The clipboard is readable only with the ACL permission + // ohos.permission.READ_PASTEBOARD. Native code asks for it after the first + // denied paste; granting takes effect for the next paste. + private requestPasteboardPermission(): void { + if (this.pasteboardPermissionRequested) { + return; + } + this.pasteboardPermissionRequested = true; + try { + const context = this.getUIContext().getHostContext() as common.UIAbilityContext; + const manager = abilityAccessCtrl.createAtManager(); + const permissions: Permissions[] = ['ohos.permission.READ_PASTEBOARD']; + manager.requestPermissionsFromUser(context, permissions) + .then((result: PermissionRequestResult): void => { + const granted = result.authResults.length > 0 && result.authResults[0] === 0; + hilog.info(0x0000, NOTIFICATION_TAG, `pasteboard permission granted: ${granted}`); + if (!granted) { + this.showPasteboardNotice(); + } + }) + .catch((error: BusinessError): void => { + hilog.error(0x0000, NOTIFICATION_TAG, + 'pasteboard permission request failed: %{public}d', error.code); + }); + } catch (error) { + hilog.error(0x0000, NOTIFICATION_TAG, 'pasteboard permission request failed'); + } + } + + private showPasteboardNotice(): void { + try { + this.getUIContext().getPromptAction().showToast({ + message: 'Clipboard access is off; enable it in Settings to paste.', + duration: 3000, + }); + } catch (error) { + hilog.error(0x0000, NOTIFICATION_TAG, 'pasteboard notice failed'); + } + } + // Copy a picked (URI-granted) file into the app cache so native code never // touches provider URIs. /** diff --git a/platform/ohos/shell/entry/src/main/module.json5 b/platform/ohos/shell/entry/src/main/module.json5 index a58235e7..07527e3c 100644 --- a/platform/ohos/shell/entry/src/main/module.json5 +++ b/platform/ohos/shell/entry/src/main/module.json5 @@ -24,8 +24,16 @@ skills: [{ entities: ['entity.system.home'], actions: ['ohos.want.action.home'] }], }, ], - // Socket access for IMAP/SMTP/OAuth — the only permission the port needs. - // The packaging step in scripts/build-hap.sh re-asserts this list (deduped). - requestPermissions: [{ name: 'ohos.permission.INTERNET' }], + // Socket access for IMAP/SMTP/OAuth, plus the ACL permission that lets + // the app read the clipboard for paste. The packaging step in + // scripts/build-hap.sh re-asserts this list (deduped). + requestPermissions: [ + { name: 'ohos.permission.INTERNET' }, + { + name: 'ohos.permission.READ_PASTEBOARD', + reason: '$string:pasteboard_permission_reason', + usedScene: { abilities: ['EntryAbility'], when: 'inuse' }, + }, + ], }, } diff --git a/platform/ohos/shell/entry/src/main/resources/base/element/string.json b/platform/ohos/shell/entry/src/main/resources/base/element/string.json index 03484e37..85a7be22 100644 --- a/platform/ohos/shell/entry/src/main/resources/base/element/string.json +++ b/platform/ohos/shell/entry/src/main/resources/base/element/string.json @@ -2,6 +2,10 @@ "string": [ { "name": "module_desc", "value": "Flectar Mail for HarmonyOS" }, { "name": "EntryAbility_desc", "value": "Flectar Mail for HarmonyOS" }, - { "name": "EntryAbility_label", "value": "Flectar Mail for HarmonyOS" } + { "name": "EntryAbility_label", "value": "Flectar Mail for HarmonyOS" }, + { + "name": "pasteboard_permission_reason", + "value": "Used to paste the copied text into the message body, subject and recipient fields." + } ] } diff --git a/platform/ohos/shell/entry/src/main/resources/zh_CN/element/string.json b/platform/ohos/shell/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 00000000..a8413621 --- /dev/null +++ b/platform/ohos/shell/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "pasteboard_permission_reason", + "value": "用于将剪贴板中的文本粘贴到邮件正文、主题和收件人输入框。" + } + ] +} diff --git a/platform/ohos/shell/page-templates/Index.mail.ets b/platform/ohos/shell/page-templates/Index.mail.ets index 2f4ef6f8..29d8c8ff 100644 --- a/platform/ohos/shell/page-templates/Index.mail.ets +++ b/platform/ohos/shell/page-templates/Index.mail.ets @@ -1,4 +1,4 @@ -import { common, Want } from '@kit.AbilityKit'; +import { abilityAccessCtrl, common, PermissionRequestResult, Permissions, Want } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { fileIo, picker } from '@kit.CoreFileKit'; import { inputMethod } from '@kit.IMEKit'; @@ -17,6 +17,7 @@ interface FlectarBridge { showIme: () => void; hideIme: () => void; notify: (title: string, body: string) => void; + requestPasteboard: () => void; } // The exports of the native XComponent module (`onLoad` hands them over). @@ -33,6 +34,7 @@ interface FlectarNativeModule { struct Index { private nativeModule: FlectarNativeModule | undefined = undefined; private imeAttached: boolean = false; + private pasteboardPermissionRequested: boolean = false; aboutToAppear(): void { this.probeResources(); @@ -120,6 +122,9 @@ struct Index { notify: (title: string, body: string): void => { this.notify(title, body); }, + requestPasteboard: (): void => { + this.requestPasteboardPermission(); + }, }); } @@ -300,6 +305,46 @@ struct Index { } } + // The clipboard is readable only with the ACL permission + // ohos.permission.READ_PASTEBOARD. Native code asks for it after the first + // denied paste; granting takes effect for the next paste. + private requestPasteboardPermission(): void { + if (this.pasteboardPermissionRequested) { + return; + } + this.pasteboardPermissionRequested = true; + try { + const context = this.getUIContext().getHostContext() as common.UIAbilityContext; + const manager = abilityAccessCtrl.createAtManager(); + const permissions: Permissions[] = ['ohos.permission.READ_PASTEBOARD']; + manager.requestPermissionsFromUser(context, permissions) + .then((result: PermissionRequestResult): void => { + const granted = result.authResults.length > 0 && result.authResults[0] === 0; + hilog.info(0x0000, NOTIFICATION_TAG, `pasteboard permission granted: ${granted}`); + if (!granted) { + this.showPasteboardNotice(); + } + }) + .catch((error: BusinessError): void => { + hilog.error(0x0000, NOTIFICATION_TAG, + 'pasteboard permission request failed: %{public}d', error.code); + }); + } catch (error) { + hilog.error(0x0000, NOTIFICATION_TAG, 'pasteboard permission request failed'); + } + } + + private showPasteboardNotice(): void { + try { + this.getUIContext().getPromptAction().showToast({ + message: 'Clipboard access is off; enable it in Settings to paste.', + duration: 3000, + }); + } catch (error) { + hilog.error(0x0000, NOTIFICATION_TAG, 'pasteboard notice failed'); + } + } + // Copy a picked (URI-granted) file into the app cache so native code never // touches provider URIs. /**