“碰一碰”说起来像一次物理动作,但对一个承接分享的业务页面来说,需要处理的远不止一张图片从甲设备到乙设备。用户可能已经打开接收页,又收到一次系统分享拉起;同一条分享通知可能被重复点击;图片临时 URI 在接收页关闭后可能无法再用。把每一次入口回调都当作新文件,接收箱就会出现重复素材;把暂时拿得到的 URI 当作长期保存成功,又会留下点不开的记录。

本篇把系统能力边界放在前面:官方白皮书介绍了手机间碰一碰分享与设备条件,Share Kit 官方 FAQ 则说明目标应用如何接收系统分享。两者不能简单画等号。 普通三方应用不能据此推断每一次物理碰一碰都会直达自身 UIAbility,也不能自行宣称拥有“精准识别碰触位置”的公开 API。本文实现的是“用户经系统分享通道将素材交给应用之后”的应用内收件隔离层;碰一碰的设备发现、协商、卡片确认与传输由系统完成,不在此模拟系统 API。

演示叫 NearDropVault,会话号 SHR-071,待收文件名 dock-note.png。安排一次已合法收到的分享输入,随后让相同的输入再触发一次应用入口。预期第一条状态是 IMPORTED,第二条是 DUPLICATE_IGNORED,接收箱保留 1 条正式记录。下面所有状态和日志都是构造演示,不代表已经做过两台设备的碰一碰实测。

一、明确分享数据进入应用的入口

系统分享目标需要声明自己支持的分享动作及统一数据类型;华为 Share Kit FAQ 给出的接收路径是:在 module.json5 的 skills 注册 ohos.want.action.sendData 和可处理的 UTD,入口 Ability 的 onCreate 或 onNewWant 获取 Want,再用 systemShare.getSharedData(want) 解出 SharedData,逐条读取 SharedRecord。其中 general.image 等 UTD 不是任意发挥的文件扩展名,而是系统用于匹配分享内容类型的合同。实际工程要按要支持的类型收紧声明,避免把无法处理的文件也吸引过来。

下面先只处理图片 URI 的接收和业务闸门,不试图用 Want.parameters 编造某个“碰一碰任务 ID”。SHR-071 是我们给演示测试自行分配的会话编号,而不是 Share Kit 系统下发字段。getSharedData 失败和没有 URI 的记录分别记录错误;涉及个人文件时不直接在 HiLog 打印完整路径。注意这里的 receiveUri 负责排队,真正的文件复制逻辑在下一段实现之前不应把界面标成成功。

import { UIAbility, Want } from '@kit.AbilityKit';
import { systemShare } from '@kit.ShareKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'NearDropVault';
export default class EntryAbility extends UIAbility {
  onCreate(want: Want): void { void this.readSharedData(want); }
  onNewWant(want: Want): void { void this.readSharedData(want); }

  private async readSharedData(want: Want): Promise<void> {
    try {
      const data: systemShare.SharedData = await systemShare.getSharedData(want);
      const records: systemShare.SharedRecord[] = data.getRecords();
      for (const record of records) {
        const uri: string = record.uri ?? '';
        if (!uri) continue;
        const result: string = await inboxGate.receiveUri(uri, async () => {
          return await importIntoSandbox(uri); // 应用侧持久化函数
        });
        hilog.info(0x0000, TAG, '%{public}s', `SHR-071 ${result}`);
      }
    } catch (error) {
      hilog.error(0x0000, TAG, '%{public}s', 'SHR-071 RECEIVE_FAILED');
    }
  }
}

onCreate 和 onNewWant 是两个不同的生命周期入口,不能因为界面只显示一个接收页就只写其中一个。二者共同进入单个处理函数,才能避免冷启动和热启动走两份逻辑。上面把日志限制为会话号和阶段,不输出文件名对应的真实沙箱路径。示例中 inboxGate 与 importIntoSandbox 需要由工程模块导出并初始化,这一段表达的是 Ability 的接入调用关系,不能拿它冒充拷入真机即可编译的完整工程文件。

二、临时 URI 和本地正式文件不能混用

最容易出现的错误是拿到 record.uri 后马上把 URI 记到历史表,页面上立刻显示“导入成功”。这实际上只完成了“读取到指向分享内容的地址”。目标应用与分享发送方之间存在沙箱隔离,URI 的可访问范围与授权周期需要遵循系统约束;如果要在下次冷启动后独立打开,应该在权限有效期间把文件复制到自己的沙箱目录。复制出错就保持重试态,不要凭文件名生成一条看起来正常的素材。

为避免业务方法和底层 IO 强耦合,把闸门写成接受持久化回调的对象。这段代码只管理应用内的同一 URI 重入,并不会替代数据层的唯一索引。特别要注意失败是否能重试:如果第一次保存失败,不应永久占用去重集合;如果第一次还在写入中,第二次必须立即返回已处理提示,不能让两个写入同时覆盖目标文件。Set 只保留一次进程生命周期内的结果,跨进程和跨设备去重仍需要持久化业务 ID 或内容哈希。

type ReceiptResult = 'IMPORTED' | 'DUPLICATE_IGNORED' | 'RETRYABLE';
class InboxGate {
  private busy: Set<string> = new Set<string>();
  private completed: Set<string> = new Set<string>();

  async receiveUri(uri: string,
    persist: () => Promise<boolean>): Promise<ReceiptResult> {
    if (this.completed.has(uri) || this.busy.has(uri)) {
      return 'DUPLICATE_IGNORED';
    }
    this.busy.add(uri);
    try {
      const copied: boolean = await persist();
      if (!copied) return 'RETRYABLE';
      this.completed.add(uri);
      return 'IMPORTED';
    } catch (_) {
      return 'RETRYABLE';
    } finally {
      this.busy.delete(uri);
    }
  }
}
const inboxGate: InboxGate = new InboxGate();

用临时 URI 去重有一个明显边界:某些分享端可能每次提供不同 URI,甚至相同 URI 指向的数据会发生变化。因此它只能解决“同一次入口重复执行”的近程问题。要做正式的素材库,应先对复制后的数据核对文件大小、格式和必要的摘要,然后在应用数据库中建立以业务接收号为中心的约束。用户主动再次导入同一张图片也可能是合理需求,不能把同内容判断直接写死为永远拒绝。另一个重要取舍是不要把“数据迁移成功”与“缩略图解码完成”绑定:图像预览可以晚一点出现,原件保存和确认才是接收箱可靠性的起点。

对于真正的文件复制,华为 Core File Kit 官方资料明确提供 fileIo 的打开、复制、关闭等文件操作能力;是否能打开分享 URI 仍由系统授予的可访问权限决定。不要把发送方沙箱的绝对路径直接拼成接收方文件名,也不要绕过 URI 授权。getSharedData 返回的 SharedRecord URI 与用户文件访问机制需要按实际资源类型调试;图片、PDF、文本可能走完全不同的后处理路线。出于可核验性,本篇没有把某个未经端到端验证的 fileIo.copyFile 片段伪装成“所有分享 URI 都能复制”的通用实现,而是让 persist 明确作为真实工程必须完成的持久化边界。

三、看见第二次回调,不能看见第二份文件

接收页面应该表现为明确的状态,而不是弹出一条“成功”吐司就结束。演示中第一条输入经过合法解析、应用侧持久化回调报告成功,然后状态写为 IMPORTED;相同 URI 立即再进入一次业务方法,应得到 DUPLICATE_IGNORED。显示的“一条素材”来自保存成功的条目,而不是“碰一碰发生了两次”的计数。页面另设一块“本次入口事件”展示最后一次处理结果,避免用户把二次拉起当作新文件。

下面是纯 UI 的演示组件,使用已存在的 @State 机制渲染构造结果。它没有假装已经接入系统设备间传输。正式接入时应该让 Ability 收件完成后通过应用状态容器或数据库观察机制通知此页,而不是在真正的文件接收线程中直接操作页面组件。接收箱(1)、IMPORTED 和 DUPLICATE_IGNORED 分别承担计数、文件状态与入口事件状态,不要混成一个布尔量。

@Entry
@Component
struct ReceiveInbox {
  @State sessionId: string = 'SHR-071';
  @State fileName: string = 'dock-note.png';
  @State filePhase: string = 'IMPORTED';
  @State lastEntry: string = 'DUPLICATE_IGNORED';
  @State importCount: number = 1;

  build() {
    Column({ space: 14 }) {
      Text('接收箱').fontSize(25).fontWeight(FontWeight.Bold)
      Text(this.sessionId)
      Text(`已导入 ${this.importCount} 项`)
      Row() {
        Text(this.fileName).layoutWeight(1)
        Text(this.filePhase)
      }
      Text(`最近入口:${this.lastEntry}`)
        .fontSize(13).fontColor('#526581')
      Button('查看已导入素材')
    }
    .padding(20).width('100%').height('100%')
  }
}

页面里的按钮只描述预期操作,不意味着已经实现文件打开。要把它做成可用按钮,需要在确认持久化路径后绑定实际的预览组件和异常提示。状态图建议在右上角保留 SHR-071,底部标明“演示数据”,避免把人工造出的任务号误认为系统凭证。开发界面中的 HiLog 只展示 IMPORTED 与 DUPLICATE_IGNORED 两种决策,不能伪造设备间接触坐标、通信距离、物理信号强度或者所谓官方回调耗时。

检查路径也应分两组。第一组验证纯应用逻辑:给 InboxGate 一个会延迟完成的 persist,在第一次 Promise 未结算前重复调用,确认第二次不会执行保存;让首次返回 false,再触发应允许重试;已经保存成功后再调应忽略。第二组才是系统集成:在声明 UTD 的目标应用上,从支持的发送方执行真实系统分享,分别验证冷启动、热启动、取消分享、文件 URI 失效和沙箱复制失败。只有第二组真正跑过,才有资格说“这个设备上接收分享可用”。

示意图显示本地处理后的稳定页面:SHR-071、dock-note.png、已导入 1 项,文件状态 IMPORTED,最近入口 DUPLICATE_IGNORED。这张图展示业务判断结果,不代表真实碰一碰成功,也不展示任何系统私有硬件数据。

四、接入现场时真正需要核对的限制

华为官方关于系统分享的 FAQ 明确指出,目标应用需要注册可接收的数据类型,网络图片不能直接作为远端 URL 交给系统分享,必要时应先下载成合法本地资源;大量共享记录还存在描述信息与数量方面的约束。更关键的是,碰一碰不能绕过确认卡片直接强制发送内容。任何声称只靠应用端 ArkTS 就能取消系统确认、精准读取接触位置、强制指定接收设备的代码,都会触碰官方能力边界。

因此这套收件层的价值是可解释的业务隔离,不是“破解”系统近场能力。它适合被系统分享唤起后的图片笔记、文件整理和收藏场景;不适合被当作任意设备发现、跨设备自动传输、后台静默收件的方案。迁移到正式工程时,先确定目标设备与 HarmonyOS SDK 的实际支持范围,再检查 skills、UTD 和 URI 权限,最后把内容摘要和接收序号写进应用持久层。这三步没有做完,就应该在文章里保留“示例”两个字,而不是把生成画面写成产品已经上线。

资料与验证边界: 华为官方《系统分享与数据接收常见问题》(2026-10-14):https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/share-kit-faq-0001 ;华为鸿蒙生态解决方案白皮书《碰一碰分享》:https://developer.huawei.com/consumer/cn/doc/guidebook/solution-0000002784570841 ;官方 Ability 使用 onCreate/onNewWant 示例:https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/screentimeguard-app-redirection 。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐