diff --git a/feature_capabilities/src/main/ets/providers/ShareProvider.ets b/feature_capabilities/src/main/ets/providers/ShareProvider.ets index 68161f7..bc3f18b 100644 --- a/feature_capabilities/src/main/ets/providers/ShareProvider.ets +++ b/feature_capabilities/src/main/ets/providers/ShareProvider.ets @@ -1,24 +1,21 @@ /** * 分享能力(契约 §8.1/§10.1)。三端统一「剪贴板+指引窗 → 系统分享」,**不依赖任何分享 SDK**。 * - * H5 调 friendsSharetypeUrlToptitleDescript(data=sharetypeBean JSON): - * - sharefriend=="2" → 直接微信指引窗(朋友圈语义,回传 type=2)。 - * - sharefriend=="1" → 弹自定义面板(微信/QQ/抖音/取消) → 选定渠道。 - * 选定渠道后按内容类型组装并写剪贴板,再弹 ShareGuideDialog: - * - 文本(type1/4/其他):剪贴板 PLAIN_TEXT(title\ndescription,无 url); - * - 图片(type2 截图 / type3 图片链接下载,低优先):剪贴板 PIXELMAP(尽力)。 - * 指引窗动作:打开App(openLink scheme,best-effort) / 用系统分享(systemText|systemImage,可靠) / 取消。 - * 仅微信乐观回传 sharesuccess:打开/系统分享→{success:2,type};取消→{success:3,type}。QQ/抖音不回传。 + * H5 调 friendsSharetypeUrlToptitleDescript(data=sharetypeBean JSON),按内容类型分流: + * - 图片(type=="2" 截图 / type=="3" 图片链接下载):截图(WebSlot componentSnapshot)/下载落盘后 + * **直接拉系统分享面板(systemImage)** 把图片交给目标 App;不弹自有面板/指引/不写剪贴板。乐观回传 sharesuccess。 + * - 文本(其余 type):sharefriend=="1" 弹自定义面板(微信/QQ/抖音)→选定渠道;=="2" 直接微信。 + * 选定后写剪贴板 PLAIN_TEXT(title\ndescription,无 url) + 弹 ShareGuideDialog: + * 打开App(openLink scheme,best-effort) / 用系统分享(systemText) / 取消。仅微信乐观回传 sharesuccess。 * - * 注:图片"复制剪贴板再去对方App粘贴"能否成功取决于对方是否读 PIXELMAP(鸿蒙不可保证), - * 故指引窗"用系统分享"(systemImage) 为可靠兜底;拉起 App 仅打开、不预填(需用户自行粘贴)。 + * 注:图片"复制剪贴板再去对方App粘贴"鸿蒙下不可靠(对方是否读 PIXELMAP 无保证,§R-19), + * 故图片一律走系统分享(对应原工程微信 SDK shareImage 的去SDK等价);拉起 App 仅打开不预填(需自行粘贴)。 */ import { systemShare } from '@kit.ShareKit'; import { uniformTypeDescriptor as utd } from '@kit.ArkData'; -import { fileUri, fileIo } from '@kit.CoreFileKit'; +import { fileUri } from '@kit.CoreFileKit'; import { common, bundleManager } from '@kit.AbilityKit'; import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; -import { image } from '@kit.ImageKit'; import { util } from '@kit.ArkTS'; import { BridgeController } from 'feature_bridge'; import { InboundHandlers, OutboundHandlers, SharetypeBean, ShareSuccessResp } from 'contracts'; @@ -101,9 +98,14 @@ export class ShareProvider implements CapabilityProvider { this.log.i(`onShare bean sharefriend=${bean.sharefriend} type=${bean.type} ` + `sharetype=${bean.sharetype} url=${bean.webpageUrl} ` + `titleLen=${bean.title.length} descLen=${bean.description.length}`); + if (bean.type === '2' || bean.type === '3') { + // 图片:截图/下载 → 直接拉系统分享面板(剪贴板对图片不可靠 §R-19),不走面板/指引。 + this.startImageShare(bean); + return; + } if (bean.sharefriend === '2') { - // 朋友圈语义:不弹面板,直接微信指引窗(契约 §10.1)。 - this.startShare(bean, 'wechat'); + // 文本·朋友圈语义:不弹面板,直接微信指引窗(契约 §10.1)。 + this.startTextShare(bean, 'wechat'); return; } this.seq += 1; @@ -116,7 +118,7 @@ export class ShareProvider implements CapabilityProvider { private onPlatformChosen(bean: SharetypeBean, payload?: Object): void { const platform: string = payload !== undefined ? (payload as SharePanelResult).platform : 'cancel'; if (platform === 'wechat' || platform === 'qq' || platform === 'douyin') { - this.startShare(bean, platform); + this.startTextShare(bean, platform); } // cancel:不分享、不回传。 } @@ -142,7 +144,7 @@ export class ShareProvider implements CapabilityProvider { this.reportResult(3, up.type === '1' ? 1 : 2); return; } - this.onImageReady(bean, 'wechat', filePath); + this.deliverImage(ctx.uiAbilityContext, filePath, bean); } // —— 组装 + 弹指引窗 —— @@ -152,79 +154,64 @@ export class ShareProvider implements CapabilityProvider { return (v === undefined || v === null) ? '' : `${v}`; } - private startShare(bean: SharetypeBean, platform: string): void { - if (bean.type === '2') { - this.log.i(`startShare -> canvas image (platform=${platform})`); - this.startCanvasImageShare(bean, platform); - } else if (bean.type === '3') { - this.log.i(`startShare -> link image (platform=${platform})`); - this.startLinkImageShare(bean, platform); - } else { - this.log.i(`startShare -> text (platform=${platform}, type='${bean.type}')`); - this.startTextShare(bean, platform); - } - } - private startTextShare(bean: SharetypeBean, platform: string): void { this.writeClipboardText(buildShareText(bean.title, bean.description)); - this.guide(bean, platform, 'text', ''); + this.guide(bean, platform); } - /** type2:截当前 Web canvas → 落盘 → 图片指引窗(捕获失败降级文本指引,非"图片转文本"语义)。 */ - private startCanvasImageShare(bean: SharetypeBean, platform: string): void { + /** + * 图片分享(type=2 截图 / type=3 链接下载):落盘后**直接拉系统分享面板**把图片交给目标 App。 + * 剪贴板粘贴对图片不可靠(§R-19),故图片不走剪贴板/指引/自有面板。乐观回传 sharesuccess。 + */ + private startImageShare(bean: SharetypeBean): void { const ctx = this.ctx; if (ctx === undefined) { return; } + const uiCtx: common.UIAbilityContext = ctx.uiAbilityContext; + if (bean.type === '3') { + const url: string = bean.webpageUrl; + if (url === '' || !(url.startsWith('http://') || url.startsWith('https://'))) { + this.log.w('link image bad url; abort image share'); + return; + } + this.seq += 1; + const ext: string = url.toLowerCase().indexOf('.png') >= 0 ? '.png' : '.jpg'; + const savePath: string = `${uiCtx.filesDir}/share/link_${this.seq}${ext}`; + Downloader.download(url, savePath).then(() => { + this.deliverImage(uiCtx, savePath, bean); + }).catch((e: Error) => { + this.log.w(`link image download failed: ${e.message}`); + }); + return; + } + // type=2:截当前激活 Web 的渲染像素(WebSlot 用 componentSnapshot)。 const cap = ctx.captureCanvas; if (cap === undefined) { - this.startTextShare(bean, platform); + this.log.w('captureCanvas unavailable; abort image share'); return; } - // sharetype 不是 canvasId(原工程为"闲聊"二级字段),截图始终取默认首个 ,故传空串。 cap('').then((dataUrl: string) => { this.log.i(`canvas captured dataUrlLen=${dataUrl.length}`); - const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(ctx.uiAbilityContext, dataUrl) : ''; + const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(uiCtx, dataUrl) : ''; if (filePath !== '') { - this.onImageReady(bean, platform, filePath); + this.deliverImage(uiCtx, filePath, bean); } else { - this.log.w('canvas capture empty (页面可能无 ); fallback to text guide'); - this.startTextShare(bean, platform); + this.log.w('canvas capture empty; abort image share'); } }).catch((e: Error) => { - this.log.w(`captureCanvas failed: ${e.message}; fallback to text guide`); - this.startTextShare(bean, platform); + this.log.w(`captureCanvas failed: ${e.message}`); }); } - /** type3(低优先,需确认 H5 是否使用):从 webpageUrl 下载图片 → 落盘 → 图片指引窗(失败降级文本)。 */ - private startLinkImageShare(bean: SharetypeBean, platform: string): void { - const ctx = this.ctx; - if (ctx === undefined) { - return; - } - const url: string = bean.webpageUrl; - if (url === '' || !(url.startsWith('http://') || url.startsWith('https://'))) { - this.startTextShare(bean, platform); - return; - } - this.seq += 1; - const ext: string = url.toLowerCase().indexOf('.png') >= 0 ? '.png' : '.jpg'; - const savePath: string = `${ctx.uiAbilityContext.filesDir}/share/link_${this.seq}${ext}`; - Downloader.download(url, savePath).then(() => { - this.onImageReady(bean, platform, savePath); - }).catch((e: Error) => { - this.log.w(`link image download failed: ${e.message}; fallback to text guide`); - this.startTextShare(bean, platform); - }); + /** 图片落盘后:拉系统分享面板把图片交给用户选择的 App + 乐观回传 sharesuccess。 */ + private deliverImage(uiCtx: common.UIAbilityContext, filePath: string, bean: SharetypeBean): void { + this.systemImage(uiCtx, filePath, bean); + this.reportResult(2, bean.sharefriend === '1' ? 1 : 2); } - private onImageReady(bean: SharetypeBean, platform: string, filePath: string): void { - this.writeClipboardImage(filePath); - this.guide(bean, platform, 'image', filePath); - } - - private guide(bean: SharetypeBean, platform: string, kind: string, filePath: string): void { + /** 文本分享指引窗(仅文本类用;图片走系统分享,不经此)。 */ + private guide(bean: SharetypeBean, platform: string): void { // 退订上一个可能悬挂的指引监听(用户上次未操作就再次触发分享)。 if (this.guideCancelFn !== undefined) { this.guideCancelFn(); @@ -234,13 +221,13 @@ export class ShareProvider implements CapabilityProvider { const resultEvent: string = `share.guide.${this.instanceId}.${this.seq}`; this.guideCancelFn = EventBus.once(resultEvent, (p) => { this.guideCancelFn = undefined; - this.onGuideAction(bean, platform, kind, filePath, p); + this.onGuideAction(bean, platform, p); }); - const req: ShareGuideRequest = { platform, contentKind: kind, resultEvent }; + const req: ShareGuideRequest = { platform, contentKind: 'text', resultEvent }; EventBus.emit(ShareEvents.SHOW_GUIDE, req); } - private onGuideAction(bean: SharetypeBean, platform: string, kind: string, filePath: string, payload?: Object): void { + private onGuideAction(bean: SharetypeBean, platform: string, payload?: Object): void { const ctx = this.ctx; if (ctx === undefined) { return; @@ -255,11 +242,7 @@ export class ShareProvider implements CapabilityProvider { this.reportResult(2, reportType); } } else if (action === 'system') { - if (kind === 'image' && filePath !== '') { - this.systemImage(uiCtx, filePath, bean); - } else { - this.systemText(uiCtx, bean); - } + this.systemText(uiCtx, bean); if (platform === 'wechat') { this.reportResult(2, reportType); } @@ -314,38 +297,7 @@ export class ShareProvider implements CapabilityProvider { } } - /** 图片写剪贴板(尽力;对方能否粘贴 PIXELMAP 不可保证,systemImage 才是可靠交付)。 */ - private writeClipboardImage(filePath: string): void { - let file: fileIo.File; - try { - file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY); - } catch (e) { - this.log.w(`clipboard image open failed: ${(e as Error).message}`); - return; - } - const source: image.ImageSource = image.createImageSource(file.fd); - source.createPixelMap().then((pm: image.PixelMap) => { - let data: pasteboard.PasteData; - try { - data = pasteboard.createData(pasteboard.MIMETYPE_PIXELMAP, pm); - } catch (e) { - this.log.w(`clipboard image init failed: ${(e as Error).message}`); - pm.release(); - return; - } - pasteboard.getSystemPasteboard().setData(data) - .then(() => this.log.i('clipboard image set ok')) - .catch((e: BusinessError) => this.log.w(`clipboard image failed: ${e.code} ${e.message}`)) - .finally(() => pm.release()); - }).catch((e: BusinessError) => { - this.log.w(`createPixelMap failed: ${e.code} ${e.message}`); - }).finally(() => { - source.release(); - fileIo.closeSync(file); - }); - } - - // —— 系统分享(可靠兜底;文本不下发 url)—— + // —— 系统分享(图片可靠交付 / 文本兜底;文本不下发 url)—— private systemText(uiCtx: common.UIAbilityContext, bean: SharetypeBean): void { const text: string = buildShareText(bean.title, bean.description);