鸿蒙截图工具开发实战 02:截屏权限 CUSTOM\_SCREEN\_CAPTURE——"检查"和"申请"为什么必须是两个函数

鸿蒙截图工具开发实战 02:截屏权限 CUSTOM_SCREEN_CAPTURE------"检查"和"申请"为什么必须是两个函数

系列第 2 篇。截取整块屏幕在任何平台都是敏感操作,HarmonyOS 给三方应用的通行证是 ohos.permission.CUSTOM_SCREEN_CAPTURE(user_grant 用户授权型,API 14+,仅平板/2in1 开放)。这篇讲这张通行证的完整用法:声明、申请、静默检查,以及一个托盘应用特有的死角------窗口不在前台时,你连"问用户要权限"的资格都没有

声明:module.json5

user_grant 权限第一步是在 module.json5 声明,reasonusedScene 是硬性要求(上架审核也会看):

json5 复制代码
"requestPermissions": [
  {
    "name": "ohos.permission.CUSTOM_SCREEN_CAPTURE",
    "reason": "$string:reason_screen_capture",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.PREPARE_APP_TERMINATE"
  }
]

reason 指向 string 资源,写清"干什么用"------这段文案会展示在授权弹窗里,也决定审核能不能过(第 24 篇上架时细说)。旁边的 PREPARE_APP_TERMINATE 是 system_grant,声明即生效,不弹窗。

申请:requestPermissionsFromUser 的前台前提

标准申请流程一行 API:

typescript 复制代码
const result = await abilityAccessCtrl.createAtManager()
  .requestPermissionsFromUser(context, ['ohos.permission.CUSTOM_SCREEN_CAPTURE']);
const granted = result.authResults.every(
  (r) => r === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);

文档不太强调、但实践里是第一优先级的前提:requestPermissionsFromUser 要求应用窗口在前台。授权弹窗要挂在你的窗口上,窗口都没有,弹窗挂哪儿?

这个前提对普通应用无感------用户点按钮触发申请,窗口自然在前台。但对托盘常驻应用是致命的:托盘态的定义就是窗口隐藏、进程活着,此时用户按 Ctrl+1 触发截屏,如果代码直接走 requestPermissionsFromUser,调用会失败,返回"未授权"。

第一版事故:明明授过权,按快捷键却弹出主窗口

第一版的截屏入口只有一个 ensureCapturePermission(检查+申请一把梭)。真机现象:托盘态按 Ctrl+1,屏幕没定格,反而"啪"弹出主窗口。链路是这样断的:

  1. 托盘态窗口隐藏 → requestPermissionsFromUser 失败;
  2. 代码把"申请失败"误读成"没授权";
  3. 走进"先拉起窗口引导授权"的兜底分支 → 主窗口弹出。

其实用户早就授过权了。问题出在用"申请"这个动作去回答"有没有"这个问题------授权状态查询根本不需要弹窗,也就不需要前台。

静默检查:checkAccessTokenSync

正确的状态查询是同步、静默、零前台要求的:

typescript 复制代码
const PERMISSION_CAPTURE = 'ohos.permission.CUSTOM_SCREEN_CAPTURE';

/** 静默检查截屏权限是否已授予(不弹窗、不要求窗口在前台;设置页状态显示也用) */
export function isCapturePermissionGranted(): boolean {
  try {
    const info = bundleManager.getBundleInfoForSelfSync(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    const status = abilityAccessCtrl.createAtManager()
      .checkAccessTokenSync(info.appInfo.accessTokenId, PERMISSION_CAPTURE);
    return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  } catch (err) {
    hilog.error(DOMAIN, TAG, 'check permission failed: %{public}s', JSON.stringify(err));
    return false;
  }
}

两步:getBundleInfoForSelfSync 拿到自己的 accessTokenId,再 checkAccessTokenSync 查授权状态。注意 BundleFlag 必须带 GET_BUNDLE_INFO_WITH_APPLICATION,否则 appInfo 是空的。

在此之上,"申请"函数改成先查后问

typescript 复制代码
export async function ensureCapturePermission(context: common.UIAbilityContext): Promise<boolean> {
  if (isCapturePermissionGranted()) {
    return true;   // 已授权直接放行,托盘态不触碰弹窗 API
  }
  const atMgr = abilityAccessCtrl.createAtManager();
  const result = await atMgr.requestPermissionsFromUser(context, [PERMISSION_CAPTURE]);
  return result.authResults.every((r) => r === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
}

真没授权怎么办:先 showAbility 再问一次

静默检查解决了"已授权被误判"的问题。还剩一个分支:托盘态触发截屏、确实没授权。这时必须弹窗,弹窗必须有前台窗口------那就先把窗口拉起来,再申请第二次。截屏入口的完整权限段:

typescript 复制代码
let granted = await ensureCapturePermission(context);
if (!granted && isPC()) {
  // 托盘态窗口隐藏时授权弹窗可能不展示:先拉起窗口再申请一次
  try {
    await context.showAbility();
  } catch (err) {
    hilog.warn(DOMAIN, TAG, 'showAbility before permission failed: %{public}s', JSON.stringify(err));
  }
  granted = await ensureCapturePermission(context);
}
if (!granted) {
  this.toast('未获得截屏权限');
  return;
}

第二次调用里 ensureCapturePermission 会再走一遍静默检查------万无一失,也不会重复弹窗。

首次运行:把授权做成引导,而不是拦截

user_grant 权限的授权弹窗,系统只会自动弹一次;用户拒绝后再调 requestPermissionsFromUser 是静默失败的。所以首次运行的授权机会很珍贵。我的做法:首启强制显示主窗口(跳过"启动即进托盘"),进设置页后延迟申请:

typescript 复制代码
if (SettingsStore.firstRun()) {
  SettingsStore.setFirstRunDone();
  // 稍延后申请:requestPermissionsFromUser 要求窗口在前台,避开窗口刚加载的瞬间
  setTimeout(() => {
    const context = getContext(this) as common.UIAbilityContext;
    ensureCapturePermission(context).then((granted: boolean) => {
      this.capturePermGranted = granted;
      if (!granted) {
        this.toast('未获得截屏权限,可在「通用」中重新授权');
      }
    });
  }, 600);
}

又是时机问题:窗口 loadContent 刚完成的瞬间,前台状态可能还没就绪,setTimeout 600ms 避开这个窗口期。

被拒绝后的补救入口放在设置页「通用」板块------一行权限状态 + 「去授权」按钮,状态行本身可点击刷新(用户从系统设置授权回来后点一下就变绿):

typescript 复制代码
@Local private capturePermGranted: boolean = isCapturePermissionGranted();
// ...
.onClick(() => {
  // 点状态可手动刷新(从系统设置授权回来后)
  this.capturePermGranted = isCapturePermissionGranted();
})

小结

  • CUSTOM_SCREEN_CAPTURE 是 user_grant + 仅平板/2in1,声明时 reason/usedScene 缺一不可;
  • 检查权限用 checkAccessTokenSync(静默、同步、无前台要求),申请权限才用 requestPermissionsFromUser(弹窗、必须前台)------两个动作两个函数,托盘应用尤其不能混;
  • 授权弹窗只自动弹一次,首启是黄金窗口;拒绝后要留手动补救入口;
  • 托盘态真没授权时,先 showAbility() 拉起窗口再申请第二次。

权限到手,下一篇讲按下 Ctrl+1 之后的事:screenshot.capture() 怎么拿到整屏 PixelMap,以及"不能截到自己"这个约束逼出来的时序设计。

相关推荐
nullregedit1 小时前
HarmonyOS 弦乐调音器开发实战 02:AudioCapturer 与 NSDF 怎样完成实时音高检测
harmonyos·arkts·数字信号处理·audiocapturer·乐器调音
如此风景2 小时前
HarmonyOS应用开发-Navigation 路由表详解
harmonyos
云端漫步19872 小时前
HarmonyOS NEXT AI 智能生活助手:AI 待办事项生成
人工智能·华为·生活·harmonyos
烛衔溟3 小时前
HarmonyOS 网络连接 —— HTTP 请求、Axios 与 Socket 通信
http·华为·harmonyos
懿路向前4 小时前
【HarmonyOS学习笔记】2026-08-04 | 端插件新装饰器与CreateRecord全链路验证
笔记·学习·harmonyos
云端漫步19874 小时前
HarmonyOS NEXT AI 智能生活助手:AI 翻译助手
人工智能·华为·生活·harmonyos
世人万千丶12 小时前
鸿蒙日志体系高级应用:HiLog分级输出/隐私脱敏/远程日志采集/线上问题精准溯源方案
学习·harmonyos·鸿蒙
程序员黑豆18 小时前
鸿蒙应用开发:AttributeModifier 使用教程
前端·harmonyos
HarmonyOS_SDK18 小时前
基于人体骨骼点识别与跟踪,实现低时延体感游戏
harmonyos