mirror of
https://github.com/flectar/mail.git
synced 2026-11-12 02:05:59 +00:00
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.
This commit is contained in:
1 parent
81d76c3427
commit
0784cf06d3
10 files changed
+166
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<fn()> = 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<String> {
|
||||
// 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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -260,6 +260,8 @@ fn run_mail() -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync
|
||||
Arc::new(documents::OhosDocuments);
|
||||
// Soft-keyboard control for Slint's editable fields.
|
||||
slint_ohos_backend::ime::set_hooks(bridge::show_ime, bridge::hide_ime);
|
||||
// A denied paste asks for the pasteboard ACL permission once (see clipboard.rs).
|
||||
slint_ohos_backend::clipboard::set_permission_hook(bridge::request_pasteboard);
|
||||
let mut platform =
|
||||
flectar_mail::PlatformContext::app_private(internal, cache, documents, credentials, oauth);
|
||||
platform.external_urls = flectar_mail::ExternalUrlOpener::new(bridge::open_url);
|
||||
|
||||
@@ -256,9 +256,10 @@ cp -r "$RES_OUT/resources" "$STAGE/resources"
|
||||
# requestPermissions: without ohos.permission.INTERNET every socket operation
|
||||
# (loopback OAuth listener, IMAP/SMTP) fails with EPERM, which surfaces as
|
||||
# "OAuth failed: io error: Operation not permitted (os error 1)".
|
||||
# READ_PASTEBOARD is deliberately absent: this device's installer rejects it
|
||||
# for normal apps (ACL-only, "grant request permissions failed"), so clipboard
|
||||
# reads stay unavailable; clipboard writes need no permission.
|
||||
# READ_PASTEBOARD is declared by the module: pasting needs it, but the hap
|
||||
# only installs with an ACL-enabled profile that lists the permission
|
||||
# (otherwise the installer reports "grant request permissions failed").
|
||||
# Copying needs no permission.
|
||||
# SDK coordinates: the resource compiler only emits the V2 `resources.index`
|
||||
# the device can load for HarmonyOS targets; DevEco-built haps carry these
|
||||
# exact values.
|
||||
@@ -286,6 +287,14 @@ module.setdefault("libIsolation", True)
|
||||
permissions = module.setdefault("requestPermissions", [])
|
||||
if not any(entry.get("name") == "ohos.permission.INTERNET" for entry in permissions):
|
||||
permissions.append({"name": "ohos.permission.INTERNET"})
|
||||
# The packing tool resolves `$string:` reasons through the resource table; the
|
||||
# permission dialog needs the numeric reasonId.
|
||||
for entry in permissions:
|
||||
reason = entry.get("reason")
|
||||
if isinstance(reason, str) and reason.startswith("$string:"):
|
||||
key = "string:" + reason[len("$string:"):]
|
||||
if key in ids:
|
||||
entry["reasonId"] = ids[key]
|
||||
app = manifest["app"]
|
||||
app["bundleName"] = bundle_name
|
||||
app["compileSdkVersion"] = "6.1.0.105"
|
||||
|
||||
@@ -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.
|
||||
/**
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "pasteboard_permission_reason",
|
||||
"value": "用于将剪贴板中的文本粘贴到邮件正文、主题和收件人输入框。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user