鸿蒙动态权限申请完整规范流程和操作详解 原创 精华
好久没有写博客了,正好今天HarmonyOS发布会,看完激动人心的发布会之后,还是觉得需要写些东西。本来准备分享之前自己做的分布式流转的视频播放器的,但是分布式流转开发内容已经有好多博主发表过了,于是搜了下社区内容,发现动态权限申请这块的内容没人发布,并且发现有几篇博客的动态权限申请的代码过于简单存在漏洞。于是想着把这块内容整理整理发出来。
一、权限概述
已在config.json文件中声明的非敏感权限,非敏感权限不涉及用户的敏感数据或危险操作,会在应用安装时自动授予,该类权限的授权方式为系统授权(system_grant)。
敏感权限需要应用动态申请,通过运行时发送弹窗的方式请求用户授权,该类权限的授权方式为用户授权(user_grant)。
非敏感权限代码编写比较简单,这里就不做讲解。本文只讲解敏感权限如何编写代码,即动态权限申请流程。
二、敏感权限列表
敏感权限的申请需要按照动态申请流程向用户申请授权。
权限分类名称 | 权限名 | 说明 |
---|---|---|
位置 | ohos.permission.LOCATION | 允许应用在前台运行时获取位置信息。如果应用在后台运行时也要获取位置信息,则需要同时申请ohos.permission.LOCATION_IN_BACKGROUND权限。 |
ohos.permission.LOCATION_IN_BACKGROUND | 允许应用在后台运行时获取位置信息,需要同时申请ohos.permission.LOCATION权限。 | |
相机 | ohos.permission.CAMERA | 允许应用使用相机拍摄照片和录制视频。 |
麦克风 | ohos.permission.MICROPHONE | 允许应用使用麦克风进行录音。 |
日历 | ohos.permission.READ_CALENDAR | 允许应用读取日历信息。 |
ohos.permission.WRITE_CALENDAR | 允许应用在设备上添加、移除或修改日历活动。 | |
健身运动 | ohos.permission.ACTIVITY_MOTION | 允许应用读取用户当前的运动状态。 |
健康 | ohos.permission.READ_HEALTH_DATA | 允许应用读取用户的健康数据。 |
分布式数据管理 | ohos.permission.DISTRIBUTED_DATASYNC | 允许不同设备间的数据交换。 |
ohos.permission.DISTRIBUTED_DATA | 允许应用使用分布式数据的能力。 | |
媒体 | ohos.permission.MEDIA_LOCATION | 允许应用访问用户媒体文件中的地理位置信息。 |
ohos.permission.READ_MEDIA | 允许应用读取用户外部存储中的媒体文件信息。 | |
ohos.permission.WRITE_MEDIA | 允许应用读写用户外部存储中的媒体文件信息。 |
三、采用一个简单的相册案例演示动态权限申请开发流程
相册需要读取本机存储的权限,即ohos.permission.READ_USER_STORAGE,它属于敏感权限。
1、项目效果图以及操作场景展示
(1)首次安装app,用户需要读取相册数据时,会弹出对话框提醒用户授权
点击"允许"之后才能正常加载数据。
(2)如果点击禁止,并且没有勾选"禁止授权并且禁止后续再弹框提示",那么下次打开app时依然会进行弹框提示。
(3)如果点击禁止,并且勾选了"禁止授权并且禁止后续再弹框提示",那么后续也不会再继续弹框进行授权了,也就看不到数据。如果需要进行授权的话,需要用户自行去系统设置中手动更改权限。此时我们应该在页面上友好地使用toast提醒用户去系统设置中手动更改权限。
请记住我现在描述的3种操作场景,与后续编写代码紧密相关,有些开发者编写代码一行代码就搞定了动态授权操作,那样的代码只能满足我说的第一种使用场景,后面两种无法满足,使用起来非常不友好。
2、代码编写步骤
(1)配置config.json
首先在config.json的module中添加如下配置
"reqPermissions": [
{
"name": "ohos.permission.READ_USER_STORAGE",
"reason": "$string:permreason_storage",
"usedScene":
{
"ability": ["com.xdw.album.MainAbility"],
"when": "always"
}
}
]
权限申请格式采用数组格式,可支持同时申请多个权限,权限个数最多不能超过1024个。
reqPermissions权限申请字段说明如下表
键 | 值说明 | 类型 | 取值范围 | 默认值 | 规则约束 |
---|---|---|---|---|---|
name | 必须,填写需要使用的权限名称。 | 字符串 | 自定义 | 无 | 未填写时,解析失败。 |
reason | 可选,当申请的权限为user_grant权限时此字段必填。描述申请权限的原因。 | 字符串 | 显示文字长度不能超过256个字节。 | 空 | user_grant权限必填,否则不允许在应用市场上架。需做多语种适配。 |
usedScene | 可选,当申请的权限为user_grant权限时此字段必填。描述权限使用的场景和时机。场景类型有:ability、when(调用时机)。可配置多个ability。 | ability:字符串数组when:字符串 | ability:ability的名称when:inuse(使用时)、always(始终) | ability:空when:inuse | user_grant权限必填ability,可选填when。 |
(2)编写权限弹框触发代码
此步骤需要结合自己项目实际业务逻辑编写,本相册项目是在主页面打开的时候就触发了权限的申请,因此修改MainAbilitySlice代码,在onStart的时候就去进行校验,具体代码如下
if (verifySelfPermission("ohos.permission.READ_USER_STORAGE") != IBundleManager.PERMISSION_GRANTED) {
// 应用未被授予权限
if (canRequestPermission("ohos.permission.READ_USER_STORAGE")) {
// 是否可以申请弹框授权(首次申请或者用户未选择禁止且不再提示)
requestPermissionsFromUser(
new String[] { "ohos.permission.READ_USER_STORAGE" } , MY_PERMISSIONS_REQUEST_READ_USER_STORAGE);
} else {
// 显示应用需要权限的理由,提示用户进入设置授权
new ToastDialog(getContext()).setText("请进入系统设置进行授权").show();
}
} else {
// 权限已被授予
//加载显示系统相册中的照片
showPhotos();
}
这断代码还使用到了一个自定义的常量MY_PERMISSIONS_REQUEST_READ_USER_STORAGE,需要提前定义它,代码如下
public static final int MY_PERMISSIONS_REQUEST_READ_USER_STORAGE = 0; //自定义的一个权限请求识别码,用于处理权限回调
第一行首先调用系统方法verifySelfPermission校验权限是否已被授予,如果未授予则调用系统方法canRequestPermission查询该权限是否可以申请弹框授权,因为如果用户之前如果勾选了禁止授权并且禁止后续再弹框提示,那么就不能再进行弹框授权了,此时需要toast提示引导用户自行去系统设置中手动更改权限。如果可以申请弹框授权,则调用系统方法requestPermissionsFromUser进行弹框授权(应用上的弹框就是来自这个方法)。如果之前应用已经被授权过,则直接调用业务处理方法,这里自定义的业务处理方法是showPhotos,它的代码请见后面完整MainAbilitySlice代码。
此时还缺少一个在授权弹框上点击允许授权按钮之后的回调业务逻辑处理,该回调业务逻辑需要重写onRequestPermissionsFromUserResult方法,而该方法是Ability类的方法,而不是AbilitySlice类的方法。因此需要在MainAbility中重写该方法,然后在该重写方法中调用MainAbilitySlice对象中的showPhotos方法,这个就涉及到了MainAbility与MainAbilitySlice的通信。
关于MainAbility与MainAbilitySlice的通信的具体讲解请看我另外一篇博文,这里不在做详解。
(3)编写requestPermissionsFromUser的回调
该回调只能在Ability种进行编写,因此修改MainAbility的代码,核心代码如下:
@Override
public void onRequestPermissionsFromUserResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsFromUserResult(requestCode, permissions, grantResults);
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_USER_STORAGE: {
// 匹配requestPermissions的requestCode
if (grantResults.length > 0
&& grantResults[0] == IBundleManager.PERMISSION_GRANTED) {
// 权限被授予之后做相应业务逻辑的处理
mainAbilitySlice.showPhotos();
} else {
// 权限被拒绝
new ToastDialog(getContext()).setText("权限被拒绝").show();
}
return;
}
}
}
这里对requestCode进行了判断,它就是我们之前自定义的权限申请码,用来区分我们在多个地方进行权限申请的操作,能区分每次不同请求的回调。
四、常见操作误区
(1)只用一行简单代码进行动态权限申请,而没有提前校验权限和回调的过程
requestPermissionsFromUser(
new String[] { "ohos.permission.READ_USER_STORAGE" } , MY_PERMISSIONS_REQUEST_READ_USER_STORAGE);
这种情况就会出现万一有一次禁止了权限,后面就不会显示相册数据并且没人任何提示,影响用户体验。
(2)canRequestPermission代码逻辑没有编写
该逻辑代码不编写,就会出现用户点击了"禁止授权并且禁止后续再弹框提示",然后进入页面就不会显示相册数据并且没人任何提示,影响用户体验。
因此,为了加强用户体验,请不要省略上述动态权限申请的代码编写流程。
五、完整代码
MainAbilitySlice的代码如下:
package com.xdw.album.slice;
import com.xdw.album.MainAbility;
import com.xdw.album.ResourceTable;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.DataAbilityHelper;
import ohos.aafwk.ability.DataAbilityRemoteException;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Component;
import ohos.agp.components.Image;
import ohos.agp.components.TableLayout;
import ohos.agp.components.Text;
import ohos.agp.window.dialog.ToastDialog;
import ohos.bundle.IBundleManager;
import ohos.data.resultset.ResultSet;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import ohos.media.image.common.Size;
import ohos.media.photokit.metadata.AVStorage;
import ohos.utils.net.Uri;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.util.ArrayList;
public class MainAbilitySlice extends AbilitySlice {
private static final String TAG = "MainAbilitySlice";
private static final HiLogLabel LABEL = new HiLogLabel(HiLog.DEBUG, 0, "TAG");
public static final int MY_PERMISSIONS_REQUEST_READ_USER_STORAGE = 0; //自定义的一个权限请求识别码,用于处理权限回调
private TableLayout tlAlbum; //定义表格布局,用来加载图片控件
private Text textLoading, textNum; //定义正在加载文本,照片数量显示文本
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
MainAbility mainAbility = (MainAbility) getAbility();
mainAbility.setMainAbilitySlice(this);
initView();
if (verifySelfPermission("ohos.permission.READ_USER_STORAGE") != IBundleManager.PERMISSION_GRANTED) {
// 应用未被授予权限
if (canRequestPermission("ohos.permission.READ_USER_STORAGE")) {
// 是否可以申请弹框授权(首次申请或者用户未选择禁止且不再提示)
requestPermissionsFromUser(
new String[] { "ohos.permission.READ_USER_STORAGE" } , MY_PERMISSIONS_REQUEST_READ_USER_STORAGE);
} else {
// 显示应用需要权限的理由,提示用户进入设置授权
new ToastDialog(getContext()).setText("请进入系统设置进行授权").show();
}
} else {
// 权限已被授予
//加载显示系统相册中的照片
showPhotos();
}
}
@Override
public void onActive() {
super.onActive();
}
@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}
private void initView() {
//初始化相关UI组件
tlAlbum = (TableLayout) findComponentById(ResourceTable.Id_tl_album);
tlAlbum.setColumnCount(3); //表格设置成3列
textLoading = (Text) findComponentById(ResourceTable.Id_text_loading);
textNum = (Text) findComponentById(ResourceTable.Id_text_num);
}
//定义加载显示图片的方法
public void showPhotos() {
//先移除之前的表格布局中的所有组件
tlAlbum.removeAllComponents();
//定义一个数组,用来存放图片的id,它的size就是照片数量
ArrayList<Integer> img_ids = new ArrayList<Integer>();
//初始化DataAbilityHelper,用来获取系统共享数据
DataAbilityHelper helper = DataAbilityHelper.creator(getContext());
try {
//读取系统相册的数据
ResultSet result = helper.query(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, null, null);
//根据获取的数据觉得“正在加载”提示是否显示
if (result == null) {
textLoading.setVisibility(Component.VISIBLE);
} else {
textLoading.setVisibility(Component.HIDE);
}
//遍历获取的数据,来动态加载表格布局中的图片组件
while (result != null && result.goToNextRow()) {
//从获取的数据中读取图片的id
int mediaId = result.getInt(result.getColumnIndexForName(AVStorage.Images.Media.ID));
//生成uri,后面会根据uri获取文件
Uri uri = Uri.appendEncodedPathToUri(AVStorage.Images.Media.EXTERNAL_DATA_ABILITY_URI, "" + mediaId);
//获取文件信息
FileDescriptor filedesc = helper.openFile(uri, "r");
//定义一个图片编码参数选项用于设置相关编码参数
ImageSource.DecodingOptions decodingOpts = new ImageSource.DecodingOptions();
decodingOpts.desiredSize = new Size(300, 300);
//根据文件信息生成pixelMap对象,该对象是设置Image组件的关键api
ImageSource imageSource = ImageSource.create(filedesc, null);
PixelMap pixelMap = imageSource.createThumbnailPixelmap(decodingOpts, true);
//构造一个图片组件并且设置相关属性
Image img = new Image(MainAbilitySlice.this);
img.setId(mediaId);
img.setHeight(300);
img.setWidth(300);
img.setMarginTop(20);
img.setMarginLeft(20);
img.setPixelMap(pixelMap);
img.setScaleMode(Image.ScaleMode.ZOOM_CENTER);
//在表格布局中加载图片组件
tlAlbum.addComponent(img);
HiLog.info(LABEL, "uri=" + uri);
img_ids.add(mediaId);
}
} catch (DataAbilityRemoteException | FileNotFoundException e) {
e.printStackTrace();
}
//完成照片数量的刷新,如果没有照片,则在UI中显示“没有照片”的文本
if (img_ids.size() > 0) {
textLoading.setVisibility(Component.HIDE);
textNum.setVisibility(Component.VISIBLE);
textNum.setText("照片数量:" + img_ids.size());
} else {
textLoading.setVisibility(Component.VISIBLE);
textLoading.setText("没有照片");
textNum.setVisibility(Component.HIDE);
}
}
}
MainAbility的代码如下:
package com.xdw.album;
import com.xdw.album.slice.MainAbilitySlice;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.agp.window.dialog.ToastDialog;
import ohos.bundle.IBundleManager;
import static com.xdw.album.slice.MainAbilitySlice.MY_PERMISSIONS_REQUEST_READ_USER_STORAGE;
public class MainAbility extends Ability {
private MainAbilitySlice mainAbilitySlice;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(MainAbilitySlice.class.getName());
}
@Override
public void onRequestPermissionsFromUserResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsFromUserResult(requestCode, permissions, grantResults);
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_USER_STORAGE: {
// 匹配requestPermissions的requestCode
if (grantResults.length > 0
&& grantResults[0] == IBundleManager.PERMISSION_GRANTED) {
// 权限被授予之后做相应业务逻辑的处理
mainAbilitySlice.showPhotos();
} else {
// 权限被拒绝
new ToastDialog(getContext()).setText("权限被拒绝").show();
}
return;
}
}
}
public MainAbilitySlice getMainAbilitySlice() {
return mainAbilitySlice;
}
public void setMainAbilitySlice(MainAbilitySlice mainAbilitySlice) {
this.mainAbilitySlice = mainAbilitySlice;
}
}
布局文件ability_main.xml代码如下:
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:orientation="vertical">
<Text
ohos:id="$+id:text_loading"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:text="正在打开..."
ohos:text_alignment="center"
ohos:text_size="45fp"></Text>
<ScrollView
ohos:height="600vp"
ohos:width="match_parent"
ohos:left_padding="25vp"
>
<TableLayout
ohos:id="$+id:tl_album"
ohos:height="match_content"
ohos:width="match_parent"
>
</TableLayout>
</ScrollView>
<Text
ohos:id="$+id:text_num"
ohos:height="match_content"
ohos:width="match_content"
ohos:text_alignment="center"
ohos:text_size="20fp"></Text>
</DirectionalLayout>
为刚看完发布会就来发帖的老师点赞!!!老师辛苦了。
为什么点了始终允许,下次进来还是要请求权限呢?
这不是跟android一模一样吗
学习夏老师新作。
支持夏老师新作