前言
应用向用户动态申请授权,是指在用户使用应用的过程中,应用方会根据应用场景和业务向用户动态地请求相应的权限。例如,当应用需要访问用户的相机或麦克风时,会向用户弹出一个授权请求框,询问用户是否允许应用访问这些设备,而用户可以选择允许或拒绝此次授权请求。这种方式可以提高应用的安全性,在一定程度上保护用户的隐私信息安全。
Nothing theoretical can be clearer than the code. 本期笔者将以一个Demo为例,与读者们共同探讨在ArkUI的框架中如何实现动态申请授权的功能
正文
通常情况下,一些提供基础功能的权限可以通过静态的方式获取(即直接将开发者需要的权限在模块级别的module.json5文件中声明),如联网权限。而对于一些能够为应用提供用户的隐私数据的敏感权限,则需要以动态的方式可视化地向用户申请。本期的Demo以申请获取大致位置权限(即"ohos.permission.APPROXIMATELY_LOCATION")为例,实现动态申请权限的功能。
新建工程
打开DevEco Studio(开发工具的版本必须支持API9),创建一个新的project,相关勾选如下
在module.json5中添加相应的权限
成功创建工程后,在工程文件目录中打开目录:entry/src/main/module.json5, 添加两个权限——定位权限( "ohos.permission.LOCATION")和获取大致位置的权限("ohos.permission.APPROXIMATELY_LOCATION" )。
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
//添加模块所需的相关权限
"requestPermissions": [
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "inuse"
}
},
],
......
}
}
事实上,当应用同时获取以上两种权限后,应用可以获取设备的精准位置,精准度在米级别。
集成功能模块
要实现向用户动态申请授权的功能,我们需要制作两个功能模块,分别是检查是否已获得所需权限的功能和向用户发起权限申请的功能。为了达到公共调用和功能模块化,我们需要将这两个功能模块集成到两个不同的TypeScript文件中,并将可调用接口导出。
在ets文件夹下新建目录,并将其命名为Service。
在Service目录下新建两个TypeScript文件(右键Service目录,选择新建,再选择TypeScript),分别命名为Detector与Applicant。
在编辑器中打开Detector.ts,加入以下代码以集成检查应用是否已获得所需权限的功能,各代码块的具体功能已写注解
//导入程序访问控制管理模块
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
//导入包管理模块
import bundleManager from '@ohos.bundle.bundleManager';
///定义待检测的权限列表
const permissionsList: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION'] //权限数据的列表
//模块的日志标签
const TAG = '------[Detector] '
const APPROVAL:number = 0
//默认导出的模块接口
export default async function Check_Access(){
//创建AtManager实例
let atManager = abilityAccessCtrl.createAtManager()
//定义局部变量grantStatus
let grantStatus:abilityAccessCtrl.GrantStatus
//定义局部变量tokenId
let tokenId:number
try{
//等待包管理模块获取本模块所在的包的BundleInfo
let bundleInfo:bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
//获取上述BundleInfo中携带的ApplicationInfo
let appInfo:bundleManager.ApplicationInfo = bundleInfo.appInfo
//获上述ApplicationInfo携带的accessTokenId
tokenId = appInfo.accessTokenId
}catch (err){
console.error(TAG+`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`)
}
try{
//利用AManager实例检查是否已获得所需权限
grantStatus = await atManager.checkAccessToken(tokenId,permissionsList[0])
}catch (err){
console.error(TAG+`checkAccessToken failed, code is ${err.code}, message is ${err.message}`)
}
//根据不同的检查结果做不同的输出
if(grantStatus == APPROVAL){
console.info(TAG+'Accessible')
return true
}else {
console.error(TAG+'Inaccessible')
return false
}
}
因为笔者已给代码添加了注释,所以笔者就不对这个模块做太多啰嗦的分析了。这个模块的大致执行逻辑是,通过调用系统能力@ohos.bundle.bundleManager获取本包的accessTokenId,再调用系统能力@ohos.abilityAccessCtrl,在传入accessTokenId和权限列表的条件下检查module是否已获取权限列表中的每个权限。当atManager的异步方法checkAccessToken返回结果为0时,表示所需权限均已被提供。
在编辑器中打开Applicant.ts,加入以下代码以集成向用户发起权限申请的功能,各代码块的具体功能已写注解。
//导入程序访问控制管理模块
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'
//导入common
import common from '@ohos.app.ability.common';
//定义待动态申请的权限列表
const permissionsList: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']
const APPROVAL:number = 0
//模块的日志标签
const TAG = '------[Applicant] '
//默认导出的模块接口
export default async function Request_Permission_From_Users(context:common.UIAbilityContext){
//预定义函数执行结果的状态
let isFinished:boolean = false
//创建AtManager实例
let atManager = abilityAccessCtrl.createAtManager()
//等待程序访问控制模块完成权限请求的异步操作,完成后根据返回结果执行then()或catch()
await atManager.requestPermissionsFromUser(context, permissionsList).then((result)=>{
//将API返回的数据存储到变量grantStatus中
let grantStatus: Array<number> = result.authResults
//判断用户是否提供所有相关权限
for(let i = 0 ; i < grantStatus.length ; i++){
if(grantStatus[i] === APPROVAL){ //用户提供所有权限, ===指的是全等
console.info(TAG+'Succeed! Obtain all the permissions')
isFinished = true //将函数执行结果的状态设置为true
}else{ //用户未提供所有权限
console.error(TAG+'User denies providing the permissions')
}
}
}).catch((err)=>{
console.error(TAG+`Request permission failed, code is ${err.code}, message is ${err.message}`)
})
return isFinished
}
这个模块的大致执行逻辑是,在atManager的异步方法requestPermissionsFromUser
中传入所需的上下文对象和待申请权限列表,并等待其异步过程的结束。在requestPermissionsFromUser的异步过程中,系统会弹出选择框,询问用户是否提供权限。当用户点击选择框中的允许或禁止时,权限会被授予或否,异步过程随即结束(此处指的是申请单个权限的场景),requestPermissionsFromUser将申请结果以number型数组的形式输出。当此数组中的每个元素的值都为0时,表示所有权限都成功获得,否则,用户拒绝授权了至少一个权限。
Detector.ts和Applicant.ts均默认导出了接口,想要在其他地方调用它们的功能,只需导入接口即可。
编辑页面UI
添加图片资源
在工程文件目录中打开目录:src/main/resources/rawfile, 添加两张任意的图片(可以在IDE中将待添加的图片资源直接粘贴至rawfile目录下,也可以在文件资源管理器中通过文件路径打开rawfile目录并添加图片资源),分别命名为image1和image2。当然,图片的格式没有要求,只要在之后的步骤中能被正确引用即可。
设计页面UI
打开Index.ts,删除原有的Text组件,新增两个Button组件和一个Image组件(相关属性设置如下),并声明一个用@state修饰的布尔变量ifAccessible。
@Entry
@Component
struct Index {
//将应用是否获取权限这条信息用布尔型变量ifAccessible储存
@State ifAccessible:boolean = false
build() {
Row() {
Column() {
//添加两个Button组件和一个Image组件
//条件渲染Image组件
if(this.ifAccessible){
Image($rawfile('image2.png'))
.height(200)
.width(200)
}else{
Image($rawfile('image1.png'))
.height(200)
.width(200)
}
//第一个Button组件
Button('检查应用是否获得权限')
.fontSize(20)
.width('70%')
.margin({
top:40
})
.backgroundColor(Color.Pink)
//第二个Button组件
Button('向用户动态申请权限')
.fontSize(20)
.width('70%')
.margin({
top:20
})
.backgroundColor(Color.Pink)
}
.width('100%')
}
.height('100%')
}
}
预览器效果如下:
添加自定义弹窗
首先,在组件Index之外用struct声明一个新的自定义组件dialog,并用装饰器@CustomDialog对其进行修饰,使dialog拥有成为自定义弹窗的能力。接着,我们对弹窗显示的内容进行自定义设置,在build函数中加入自定义UI声明。其中,CustomDialogController类型的成员变量controller和字符串类型成员变量message皆必不可少,后者可用于对dialog的外部传参。
之后,我们在组件Index中new两个弹窗控制器( CustomDialogController类的实例),并分别将它们赋予两个私有成员变量(dialogController_Accessible和dialogController_Inaccessible)以供调用。当然,我们是用两个message不同的dialog组件来构造这两个弹窗控制器的,所以弹窗控制器dialogController_Accessible和弹窗控制器dialogController_Inaccessible可用于打开和关闭它们所对应的dialog。
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
......
//new两个弹窗控制器
private dialogController_Accessible : CustomDialogController = new CustomDialogController({
builder:dialog({
message:'暂未获取权限'
})
})
private dialogController_Inaccessible : CustomDialogController = new CustomDialogController({
builder:dialog({
message:'已获取权限'
})
})
build() {
......
}
}
//自定义弹窗
@CustomDialog
struct dialog{
controller:CustomDialogController
@State message:string = ''
build(){
Column() {
Text(this.message)
.fontSize(20)
.height(40)
.fontColor(Color.White)
}
.width('100%')
.backgroundColor(Color.Gray)
}
}
编写回调方法
首先,我们从先前步骤中已集成的功能模块Detector导入异步函数Check_Access,并自定义异步方法detect。在detect的方法体中,我们通过关键字await等待异步过程,这意味者原本的异步任务变成了一个等价的延时同步任务,保证了detect中操作语句的顺序执行。由于关键字await只能在异步方法或异步函数中出现,所以detect必须是异步方法。根据Check_Access的不同返回结果(成功或失败),系统会生成不同内容的弹窗与用户交互。detect()编写完后,我们在用于检查应用是否获得权限的Button组件的onclick事件中加入此异步方法。
import Check_Access from 'ets/Service/Detector' //从模块Detector中导入异步函数Check_Access
@Entry
@Component
struct Index {
.....
//编写异步方法detect,调用之前已写好的模块文件Detector
async detect(){
let res = await Check_Access()
this.ifAccessible = res
if(res){
this.dialogController_Accessible.open()
}else{
this.dialogController_Inaccessible.open()
}
}
......
build() {
Row() {
Column() {
......
Button('检查应用是否获得权限')
.fontSize(20)
.width('70%')
.margin({
top:40
})
.backgroundColor(Color.Pink)
//设置onclick回调,并调用异步函数detect()
.onClick(()=>{
this.detect()
})
......
}
.width('100%')
}
.height('100%')
}
}
......
接着,我们从先前步骤中已集成的功能模块Applicant中导入异步函数Request_Permission_From_Users,并导入common。随后,我们通过getContext方法获取上下文对象,并将其转化为UIAbilityContext类型,存入私有成员变量context中。之后,自定义异步方法apply,在方法体中将成员变量context传入异步函数Request_Permission_From_Users中。这样以后,我们便可在用于向用户动态申请权限的Button组件的onclick事件中加入自定义方法apply了。
......
import Request_Permission_From_Users from 'ets/Service/Applicant'
import common from '@ohos.app.ability.common'
@Entry
@Component
struct Index {
......
//获取上下文对象, 储存在成员变量context中
private context = getContext(this) as common.UIAbilityContext
//编写异步方法,调用之前已写好的模块文件Applicant
async apply(){
let res = await Request_Permission_From_Users(this.context)
this.ifAccessible = res
if(res){
this.dialogController_Accessible.open()
}else{
this.dialogController_Inaccessible.open()
}
}
......
build() {
Row() {
Column() {
......
Button('向用户动态申请权限')
.fontSize(20)
.width('70%')
.margin({
top:20
})
.backgroundColor(Color.Pink)
//设置onclick回调,并调用异步函数apply()
.onClick(()=>{
this.apply()
})
}
.width('100%')
}
.height('100%')
}
}
......
至此,本期的Demo完成了。
真机&模拟机调试
Demo完成之后,我们需要用模拟器或真机来运行以查看效果。
事实上,变量ifAccessible的值应该保存在本地的数据库里,这样每次重新创建此Demo的页面实例时,才能正确地显示对应图像,读者们可以自行修复这个bug。
当然,我们也可以打开日志栏,通过日志信息观察功能模块的运行。打开编辑器下方的Hilog,勾选'show only js log',并在标签筛选栏中输入0FEFE,过滤后的日志信息如下。
文章相关附件可以点击下面的原文链接前往下载:
https://ost.51cto.com/resource/3052