diff --git a/platform/ohos/mail/src/bridge.rs b/platform/ohos/mail/src/bridge.rs index 1ef14ef8..92c8a21e 100644 --- a/platform/ohos/mail/src/bridge.rs +++ b/platform/ohos/mail/src/bridge.rs @@ -37,7 +37,7 @@ static NEXT_REQUEST: AtomicI64 = AtomicI64::new(1); /// One open picker: where its result goes, and the hold that keeps the Slint /// thread from presenting while the picker covers the window. struct Pending { - sender: oneshot::Sender>, + sender: oneshot::Sender>, _hold: PresentationHold, } @@ -105,17 +105,17 @@ pub(super) unsafe extern "C" fn set_bridge( } } -/// `submitBridgeResult(requestId, path, error)` implementation, exported for -/// the page's asynchronous flows. +/// `submitBridgeResult(requestId, path, name, error)` implementation, exported +/// for the page's asynchronous flows. pub(super) unsafe extern "C" fn submit_bridge_result( env: napi_env, info: ffi::napi_callback_info, ) -> napi_value { - // SAFETY: live `env`/`info` from the NAPI runtime; `argv` holds the three + // SAFETY: live `env`/`info` from the NAPI runtime; `argv` holds the four // expected arguments. unsafe { - let mut argc: usize = 3; - let mut argv: [napi_value; 3] = [core::ptr::null_mut(); 3]; + let mut argc: usize = 4; + let mut argv: [napi_value; 4] = [core::ptr::null_mut(); 4]; let rc = ffi::napi_get_cb_info( env, info, @@ -134,17 +134,22 @@ pub(super) unsafe extern "C" fn submit_bridge_result( } else { String::new() }; - let error = if argc > 2 { + let name = if argc > 2 { read_string(env, argv[2]).unwrap_or_default() } else { String::new() }; + let error = if argc > 3 { + read_string(env, argv[3]).unwrap_or_default() + } else { + String::new() + }; let request = pending().lock().ok().and_then(|mut map| map.remove(&id)); let accepted = match request { Some(Pending { sender, .. }) => { // Empty path + empty error means "the user cancelled". let outcome = if !path.is_empty() || error.is_empty() { - Ok(path) + Ok((path, name)) } else { Err(error) }; @@ -180,7 +185,7 @@ pub(super) fn open_url(url: &str) -> Result<(), String> { /// Registers a picker request. Its entry holds presentation until the page /// reports the result or the request is dropped, whichever thread awaits it. -fn register_picker() -> Result<(i64, oneshot::Receiver>), String> { +fn register_picker() -> Result<(i64, oneshot::Receiver>), String> { let id = NEXT_REQUEST.fetch_add(1, Ordering::SeqCst); let (sender, receiver) = oneshot::channel(); let request = Pending { @@ -194,14 +199,20 @@ fn register_picker() -> Result<(i64, oneshot::Receiver>), Ok((id, receiver)) } +/// One picked document: the private staged path and its original display name. +pub(super) struct PickedFile { + pub(super) path: String, + pub(super) name: String, +} + /// Run the system file picker for one document. `Ok(None)` = user cancelled. -pub(super) async fn pick_open() -> Result, String> { +pub(super) async fn pick_open() -> Result, String> { let (id, receiver) = register_picker()?; let guard = PendingGuard(id); call_closure(&PICK_OPEN, "pickOpen", id)?; - let path = await_result(receiver).await?; + let (path, name) = await_result(receiver).await?; drop(guard); - Ok((!path.is_empty()).then_some(path)) + Ok((!path.is_empty()).then_some(PickedFile { path, name })) } /// Ask the system file picker for a destination and copy `source_path` there. @@ -218,7 +229,7 @@ pub(super) async fn pick_save( name: suggested_name, }; call_closure(&PICK_SAVE, "pickSave", request)?; - let path = await_result(receiver).await?; + let (path, _name) = await_result(receiver).await?; drop(guard); Ok((!path.is_empty()).then_some(path)) } @@ -239,8 +250,8 @@ struct SaveRequest { } async fn await_result( - receiver: oneshot::Receiver>, -) -> Result { + receiver: oneshot::Receiver>, +) -> Result<(String, String), String> { match tokio::time::timeout(Duration::from_secs(600), receiver).await { Ok(Ok(result)) => result, Ok(Err(_)) => Err("the file picker was closed".to_owned()), diff --git a/platform/ohos/mail/src/documents.rs b/platform/ohos/mail/src/documents.rs index e5778583..c156ee11 100644 --- a/platform/ohos/mail/src/documents.rs +++ b/platform/ohos/mail/src/documents.rs @@ -16,19 +16,23 @@ pub(super) struct OhosDocuments; impl DocumentProvider for OhosDocuments { fn import(&self) -> DocumentFuture> { Box::pin(async { - let Some(path) = super::bridge::pick_open().await? else { + let Some(file) = super::bridge::pick_open().await? else { return Ok(None); }; // The page staged a copy in the app cache; deleting it once the - // core is done mirrors the Android provider. - let staged = PathBuf::from(path); - Ok(Some(ImportedDocument::temporary(staged, |path| { - if let Err(error) = std::fs::remove_file(&path) { - slint_ohos_backend::ohos_info!( - "mail: could not remove the imported copy: {error}" - ); - } - }))) + // core is done mirrors the Android provider. The display name is + // metadata only; the staged path remains the filesystem input. + let staged = PathBuf::from(file.path); + Ok(Some( + ImportedDocument::temporary(staged, |path| { + if let Err(error) = std::fs::remove_file(&path) { + slint_ohos_backend::ohos_info!( + "mail: could not remove the imported copy: {error}" + ); + } + }) + .with_name(file.name), + )) }) } diff --git a/platform/ohos/shell/page-templates/Index.mail.ets b/platform/ohos/shell/page-templates/Index.mail.ets index 84144384..bb2a1297 100644 --- a/platform/ohos/shell/page-templates/Index.mail.ets +++ b/platform/ohos/shell/page-templates/Index.mail.ets @@ -32,10 +32,15 @@ interface ImeCursor { height: number; } +interface StagedFile { + path: string; + name: string; +} + // The exports of the native XComponent module (`onLoad` hands them over). interface FlectarNativeModule { setBridge?: (bridge: FlectarBridge) => void; - submitBridgeResult?: (requestId: number, path: string, error: string) => boolean; + submitBridgeResult?: (requestId: number, path: string, name: string, error: string) => boolean; imeCommit?: (text: string) => void; imeDelete?: (count: number) => void; imeFunction?: (key: number) => void; @@ -207,8 +212,9 @@ struct Index { this.installClipboardListener(); } - private reportBridgeResult(requestId: number, path: string, error: string): boolean { - return this.nativeModule?.submitBridgeResult?.(requestId, path, error) ?? false; + private reportBridgeResult( + requestId: number, path: string, error: string, name: string = ''): boolean { + return this.nativeModule?.submitBridgeResult?.(requestId, path, name, error) ?? false; } // The Slint thread must never read the pasteboard: a cross-device clipboard @@ -284,10 +290,10 @@ struct Index { } const staged = await this.stageCopy(context, uris[0]); hilog.info(0x0000, LOG_TAG, 'picker imported a document (%{public}d picked)', uris.length); - if (!this.reportBridgeResult(requestId, staged, '')) { + if (!this.reportBridgeResult(requestId, staged.path, '', staged.name)) { // The native request timed out or was cancelled while the picker was // open; do not leave the staged copy behind. - fileIo.unlink(staged).catch((error: BusinessError): void => { + fileIo.unlink(staged.path).catch((error: BusinessError): void => { hilog.error(0x0000, LOG_TAG, 'could not remove the orphaned import: %{public}s', error.message); }); @@ -615,7 +621,7 @@ struct Index { * @throws { BusinessError } when the picked file cannot be opened or copied; * the caller reports it through the bridge result. */ - private async stageCopy(context: common.UIAbilityContext, uri: string): Promise { + private async stageCopy(context: common.UIAbilityContext, uri: string): Promise { const directory = `${context.cacheDir}/imports`; try { await fileIo.mkdir(directory, true); @@ -635,7 +641,18 @@ struct Index { // Never reuse the picked file name (or a predictable timestamp) in the // cache: it can leak the user's document name and collide with another // staged import. Keep only a short extension for content detection. - const raw = uri.split('/').pop() ?? ''; + // The display name is a separate field; decode the URI basename so a + // Chinese or spaced filename is not shown as `%E8%AE%BE...`. + let raw = uri.split('/').pop() ?? ''; + const query = raw.search(/[?#]/); + if (query >= 0) { + raw = raw.substring(0, query); + } + try { + raw = decodeURIComponent(raw); + } catch (error) { + // Keep the encoded name rather than failing the attachment import. + } const dot = raw.lastIndexOf('.'); const extension = dot > 0 ? raw.substring(dot).replace(/[^A-Za-z0-9.]/g, '').substring(0, 12) @@ -650,7 +667,7 @@ struct Index { } finally { await fileIo.close(target); } - return destination; + return { path: destination, name: raw }; } finally { await fileIo.close(source); }