当前位置: 首页 > news >正文

第73篇 | HarmonyOS 近场分享隐私:保险箱照片为什么不能默认暴露

第73篇 | HarmonyOS 近场分享隐私:保险箱照片为什么不能默认暴露

第 73 篇把分享和隐私放在一起看。近场分享越自然,越需要明确边界:公开相册里的照片可以快速分享,但保险箱照片不能因为设备碰一碰或隔空抓取就默认暴露。双镜记忆相机通过visibility、保险箱解锁状态和页面上下文共同控制这条边界。

这一篇不会只说“要保护隐私”,而是回到项目代码看数据模型如何区分 public/private,照片如何移入和恢复,保险箱 UI 如何要求身份认证,以及解锁后哪些动作才允许继续。这样读者能理解隐私不是一句产品口号,而是一组工程状态。

本篇目标

  • 理解GalleryMoment.visibility如何划分公开相册和保险箱。
  • 掌握移入保险箱后为什么要立即锁定。
  • 理解近场分享只应该围绕当前可见上下文取记录。
  • 明确保险箱导出、分享和恢复都要在解锁状态下操作。

对应源码位置

  • superImage/entry/src/main/ets/services/GalleryRecordService.ets
  • superImage/entry/src/main/ets/pages/Index.ets

保险箱入口不是另一个相册皮肤

保险箱页面和普通相册最大的不同不是颜色,而是访问前提。普通相册用于整理公开记忆,保险箱用于保存不希望默认出现在分享、地图和浏览流里的照片。用户进入保险箱时,页面会先展示锁定态,要求本地身份认证。

这也是近场分享隐私的核心:不是把私密照片从项目里删除,而是让它们只在明确解锁、明确进入保险箱上下文时可见。只要仍在普通相册路径里,分享目标就应该来自公开记录。

保险箱页面要求先解锁再浏览私密照片

数据模型先区分 public 和 private

GalleryMoment里专门定义了GalleryMomentVisibility,每条照片记录都带有visibility字段。它和路径、地点、AI 描述、云同步修订号放在同一个模型里,说明公开/私密不是 UI 临时状态,而是记录本身的属性。

把隐私属性放进持久化模型很关键。应用重启、同步、排序、列表渲染时都能根据同一个字段判断记录归属。如果只靠页面临时数组保存私密状态,下一次加载就容易把保险箱照片重新混进公开相册。

GalleryMomentVisibility 把公开照片和私密照片写进数据模型

import { common } from '@kit.AbilityKit'; import { preferences } from '@kit.ArkData'; export type GalleryMomentStatus = 'pending' | 'ready'; export type GalleryMomentVisibility = 'public' | 'private'; export type GalleryWatermarkStyle = 'none' | 'time' | 'place' | 'dual'; export interface GalleryMoment { id: string; createdAt: number; updatedAt?: number; createdLabel: string; pairIndex: number; place: string; memoryTitle: string; latitude: number; longitude: number; backPath: string; frontPath: string; backUri: string; frontUri: string; aiStatus: GalleryMomentStatus; visibility: GalleryMomentVisibility; aiCaption: string; videoPrompt: string; watermarkStyle?: GalleryWatermarkStyle; watermarkText?: string; userNote?: string; aiPoem?: string; ownerKey?: string; syncDirty?: boolean; cloudRevision?: number; cloudBackAssetDataUrl?: string; cloudFrontAssetDataUrl?: string; }

修改可见性时要保留完整记录

updateRecordVisibility不是简单改一个字段,它会构造新的GalleryMoment,保留时间、地点、路径、AI 文案、水印、备注、云端资产和修订信息,同时把updatedAt更新为当前时间,并设置syncDirty

这样写可以保证移入保险箱不是“复制一份简化记录”,而是同一条记忆在公开/私密两个分层之间移动。保留云同步字段也很重要,否则私密状态改变后,端云一致性会失去依据。

updateRecordVisibility 保留记录完整字段并写回本地存储

private async updateRecordVisibility(recordId: string, visibility: 'public' | 'private'): Promise<void> { const nextRecords = this.galleryRecords.map((record: GalleryMoment) => { if (record.id !== recordId) { return record; } const nextRecord: GalleryMoment = { id: record.id, createdAt: record.createdAt, updatedAt: Date.now(), createdLabel: record.createdLabel, pairIndex: record.pairIndex, place: record.place, memoryTitle: record.memoryTitle, latitude: record.latitude, longitude: record.longitude, backPath: record.backPath, frontPath: record.frontPath, backUri: record.backUri, frontUri: record.frontUri, aiStatus: record.aiStatus, visibility: visibility, aiCaption: this.getRecordSmartCaption(record), videoPrompt: record.videoPrompt, watermarkStyle: this.getRecordWatermarkStyle(record), watermarkText: this.getRecordWatermarkText(record), userNote: this.getRecordUserNote(record), aiPoem: this.getRecordAiPoem(record), ownerKey: record.ownerKey, syncDirty: true, cloudRevision: record.cloudRevision ?? 0, cloudBackAssetDataUrl: record.cloudBackAssetDataUrl ?? '', cloudFrontAssetDataUrl: record.cloudFrontAssetDataUrl ?? '' }; return nextRecord; }); this.galleryRecords = nextRecords; this.syncRecordSelections(nextRecords); this.syncSelectedMapMemory(false); await this.syncMapMarkers(); await this.persistGalleryRecords(nextRecords); }

移入保险箱后立即锁定

moveRecordToVault调用可见性更新后,会把vaultSelectedId指向这条记录,同时把vaultUnlocked改为 false。这个细节非常重要:用户从公开详情页把照片移入保险箱后,不能让旧详情继续展示私密内容。

恢复照片时则反向操作:关闭保险箱查看器,把记录改回 public,并把相册选中项指回这条记录。移入和恢复都要同步地图标记、选择状态和本地持久化,隐私边界才不会只在一个页面上成立。

moveRecordToVault 会在移入后立即锁定保险箱

private async moveRecordToVault(recordId: string): Promise<void> { await this.updateRecordVisibility(recordId, 'private'); this.vaultSelectedId = recordId; this.vaultUnlocked = false; this.galleryNoticeText = '已移入保险箱'; this.vaultStatusText = '保险箱已锁定'; this.cloudSyncStatusText = this.cloudSyncIdentity ? '保险箱变更会自动同步' : '登录华为账号后同步保险箱'; if (this.galleryViewMode === 'detail') { this.closeGalleryRecordDetail(); } } private async restoreRecordFromVault(recordId: string): Promise<void> { if (this.vaultSelectedId === recordId) { this.closeVaultRecordViewer(); } await this.updateRecordVisibility(recordId, 'public'); this.gallerySelectedId = recordId; if (this.getVaultRecords().length === 0) { this.vaultUnlocked = false; this.vaultStatusText = ''; } else { this.vaultStatusText = ''; } this.galleryNoticeText = ''; this.cloudSyncStatusText = this.cloudSyncIdentity ? '保险箱变更会自动同步' : '登录华为账号后同步保险箱'; }

解锁后才显示恢复、导出和分享

保险箱 UI 分成空状态、锁定态和解锁态。未解锁时展示身份认证按钮,不直接渲染私密照片网格和操作按钮。也就是说,私密照片的入口先被锁定分支拦住,用户完成认证前只能看到解锁路径。

对近场分享隐私来说,这个 UI 状态很重要。附近分享回调可以根据当前上下文取记录,但私密记录只有在保险箱已解锁并且用户处在保险箱页面时才应该成为“当前可见内容”。这就是产品体验和代码状态共同形成的边界。

保险箱未解锁时只显示认证入口和导入入口

} else if (!this.vaultUnlocked || !this.getFeaturedVaultRecord()) { Column({ space: 18 }) { Stack({ alignContent: Alignment.Center }) { Circle() .width(118) .height(118) .fill('#263542') .stroke('#E9B65E') .strokeWidth(1) Circle() .width(82) .height(82) .fill('#050809') .stroke('#FFB86B') .strokeWidth(2) Text('锁') .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor('#FFF1D2') } .width(128) .height(128) .shadow({ radius: 30, color: '#6619B8C7', offsetX: 0, offsetY: 0 }) Text('打开保险箱查看私密照片') .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor($r('app.color.ml_on_surface')) .textAlign(TextAlign.Center) Text('查看私密内容需要验证身份') .fontSize(13) .lineHeight(20) .fontColor($r('app.color.ml_on_surface_variant')) .textAlign(TextAlign.Center) Button(this.vaultAuthBusy ? '认证中...' : '解锁保险箱') .height(48) .width('100%') .enabled(!this.vaultAuthBusy) .fontSize(15) .fontWeight(FontWeight.Medium) .fontColor(this.getWarmActionTextColor()) .backgroundColor(this.getWarmActionBackgroundColor()) .borderRadius(24) .onClick(() => { void this.unlockVaultWithFace(); }) Button(this.mediaImportBusy ? '导入中...' : '导入系统相册') .height(42) .width('100%') .enabled(!this.mediaImportBusy && !this.vaultAuthBusy) .fontSize(14) .fontWeight(FontWeight.Medium) .fontColor(this.getSecondaryActionTextColor()) .backgroundColor(this.getSecondaryActionBackgroundColor()) .borderRadius(18) .onClick(() => { void this.importSystemAlbumPhotos('vault'); }) Row({ space: 12 }) { Text('人脸识别') .fontSize(12) .fontColor($r('app.color.ml_on_surface')) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.getDarkChipBackgroundColor()) .borderRadius(16) .onClick(() => { void this.unlockVaultWithFace(); }) Text('指纹识别') .fontSize(12) .fontColor($r('app.color.ml_on_surface')) .padding({ left: 14, right: 14, top: 8, bottom: 8 }) .backgroundColor(this.getDarkChipBackgroundColor()) .borderRadius(16) .onClick(() => { void this.unlockVaultWithFingerprint(); }) } } .width('100%') .padding({ left: 24, right: 24, top: 30, bottom: 24 }) .backgroundColor($r('app.color.ml_panel_glass')) .borderRadius(34) .border({ width: 1, color: '#5519B8C7' }) .alignItems(HorizontalAlign.Center) } else {

工程检查清单

  • 私密状态必须写入记录模型,而不是只存在页面变量。
  • 移入保险箱后要关闭公开详情并锁定保险箱。
  • 恢复公开相册时要同步选择状态和持久化数据。
  • 保险箱的导出、分享、查看入口必须依赖解锁状态。

今日练习

  1. 把一条公开记录改成 private,观察普通相册列表是否消失。
  2. 移入保险箱后立刻返回相册,确认旧详情页不再显示私密照片。
  3. 在保险箱锁定状态下尝试打开详情,验证openVaultRecordViewer是否直接返回。

训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。

http://www.zskr.cn/news/1492705.html

相关文章:

  • 大麦网抢票脚本终极指南:5分钟掌握Python自动化抢票技巧
  • 深度拆解:从 Lease 租约机制到流水线复制,分布式文件系统的底层高可用架构
  • 用PyTorch手把手搭建ResNet34:从Residual Block到完整模型,附代码逐行解析
  • 小程序毕设项目:nodejs基于微信小程序的设备报修系统 (源码+文档,讲解、调试运行,定制等)
  • 南阳法穆兰+卡地亚手表专业回收,26年精选回收店铺排行榜推荐 - 莘州文化
  • 手把手教你用MATLAB scatter3美化论文图表:从默认空心点到期刊级三维散点图实战
  • HarmonyOS ArkTS 中的枚举:enum 完全使用指南与最佳实践
  • 【论文复现】基于行波理论的输电线路故障诊断方法研究附Simulink仿真
  • YOLOv11涨点改进| TIP 2025 |独家特征融合改进篇| 引入DFAM双特征聚合模块,通过局部纹理先验强化边缘、轮廓信息,助力小目标检测、RGB-D目标检测、多模态融合目标检测有效涨点
  • 制造业电子数据交换EDI软件落地价值|详细解答
  • 大模型+Skills=MCP?深度解析智能体核心组件,告别概念混乱!
  • Java开发工程师全景解读:岗位职责·城市薪资·发展前景·高考志愿填报指南(2026版)
  • Keyboard Chatter Blocker:3分钟搞定键盘连击问题,让你的机械键盘重获新生!
  • 攀枝花帝舵+江诗丹顿手表专业回收,26年精选回收店铺排行榜推荐 - 莘州文化
  • 从攻击者视角看DVWA XSS:Cookie窃取背后的原理、危害与防御思考
  • 四角色多智能体讨论系统:用LangGraph构建结构化AI协作流程
  • 莲湖区家政公司分析:靠谱家政电话与家电空调维修参考 - 资讯速览
  • 5分钟掌握VRM插件:Blender虚拟角色制作终极指南
  • 3DS游戏格式转换:技术深度解析与实战指南
  • 小程序毕设选题推荐:基于springboot+vue的微信小程序的个人运动健康管理平台的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】
  • Awoo Installer架构深度解析:Nintendo Switch游戏安装引擎的设计哲学与性能优化
  • 鸡西美度天梭+宝玑手表专业回收,26年精选回收店铺排行榜推荐 - 莘州文化
  • 3分钟让Figma说中文:设计师必备的界面本地化解决方案
  • Go语言(Golang)开发工程师全景解析:岗位职责·语言优势与使用场景·各城市薪资·发展前景·高考志愿填报(2026版)
  • Cursor大规模代码重构实战:AST语义驱动的自动化迁移方案
  • 【Unity教程】使用vuforia创建简单的AR实例
  • 旧物交换网站源码包:SpringBoot后端+Vue前端,含数据库脚本、部署文档与操作视频
  • 2026如何提升营销岗位的职场能力和核心竞争力
  • 如何在3分钟内为Microsoft Word添加APA第7版参考文献格式?
  • 计算机毕业设计之django基于python网络安全攻防学习平台