鸿蒙截图工具开发实战 02:截屏权限 CUSTOM_SCREEN_CAPTURE------"检查"和"申请"为什么必须是两个函数
系列第 2 篇。截取整块屏幕在任何平台都是敏感操作,HarmonyOS 给三方应用的通行证是
ohos.permission.CUSTOM_SCREEN_CAPTURE(user_grant 用户授权型,API 14+,仅平板/2in1 开放)。这篇讲这张通行证的完整用法:声明、申请、静默检查,以及一个托盘应用特有的死角------窗口不在前台时,你连"问用户要权限"的资格都没有。
声明:module.json5
user_grant 权限第一步是在 module.json5 声明,reason 和 usedScene 是硬性要求(上架审核也会看):
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,屏幕没定格,反而"啪"弹出主窗口。链路是这样断的:
- 托盘态窗口隐藏 → requestPermissionsFromUser 失败;
- 代码把"申请失败"误读成"没授权";
- 走进"先拉起窗口引导授权"的兜底分支 → 主窗口弹出。
其实用户早就授过权了。问题出在用"申请"这个动作去回答"有没有"这个问题------授权状态查询根本不需要弹窗,也就不需要前台。
静默检查: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,以及"不能截到自己"这个约束逼出来的时序设计。