HarmonyOs应用权限申请,system_grant和user_grant区别。本文附头像上传申 ...

立聪堂德州十三局店  论坛元老 | 2024-9-25 03:52:20 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1025|帖子 1025|积分 3075

HarmonyOs应用权限申请,system_grant和user_grant区别。本文附头像上传申请user-grant权限代码示例

   system_grant(体系授权)

  system_grant指的是体系授权范例,在该范例的权限许可下,应用被允许访问的数据不会涉及到用户或设备的敏感信息,应用被允许执行的操作对体系大概其他应用产生的影响可控
  user_grant(用户授权)

  user_grant指的是用户授权范例,在该范例的权限许可下,应用被允许访问的数据将会涉及到用户或设备的敏感信息,应用被允许执行的操作大概对体系大概其他应用产生严重的影响。
  权限APL等级
  根据权限对于差别等级应用有差别的开放范围,权限范例对应分为以下三个等级,等级依次提高。
  APL级别说明开放范围normal允许应用访问超出默认规则外的寻常体系资源,如设置Wi-Fi信息、调用相机拍摄等。这些体系资源的开放(包括数据和功能)对用户隐私以及其他应用带来的风险低。APL等级为normal及以上的应用。system_basic允许应用访问操作体系底子服务(体系提供大概预置的底子功能)相关的资源,如体系设置、身份认证等。这些体系资源的开放对用户隐私以及其他应用带来的风险较高。- APL等级为system_basic及以上的应用。- 部分权限对normal级别的应用受限开放,这部分权限在本引导中形貌为“受限开放权限”。system_core涉及开放操作体系核心资源的访问操作。这部分体系资源是体系最核心的底层服务,如果遭受粉碎,操作体系将无法正常运行。- APL等级为system_core的应用。- 仅对体系应用开放。  

  • 每次执行需要目标权限的操作时,应用都必须检查自己是否已经具有该权限。
    如需检查用户是否已向您的应用授予特定权限,可以利用checkAccessToken()函数,此方法会返回Promise<[GrantStatus]>:PERMISSION_GRANTED或PERMISSION_DENIED。
  • 每次访问受目标权限掩护的接口之前,都需要利用requestPermissionsFromUser()接口哀求相应的权限。
    用户大概在动态授予权限后通过体系设置来取消应用的权限,因此不能将之前授予的授权状态恒久化。
  • user_grant权限授权要基于用户可知可控的原则,需要应用在运行时主动调用体系动态申请权限的接口,体系弹框由用户授权,用户连合应用运行场景的上下文,识别出应用申请相应敏感权限的公道性,从而做出正确的选择。
  • 体系不鼓励频繁弹窗打扰用户,如果用户拒绝授权,将无法再次拉起弹窗,需要应用引导用户在体系应用“设置”的界面中手动授予权限。
  • API12新增可以二次向用户申请授权requestPermissionOnSetting()
  • 体系权限弹窗不可被遮挡。
    体系权限弹窗不可被其他组件/控件遮挡,弹窗信息需要完整展示,以便用户识别并完成授权动作。
    如果体系权限弹窗与其他组件/控件同时同位置展示,体系权限弹窗将默认覆盖其他组件/控件。
  代码示例:点击头像申请访问相机和相册媒体的权限,拒绝后二次点击半模态弹窗按钮向用户进行二次权限申请:
实现思路:
1.点击头像时利用checkAccessToken()检查当前是否已拥有权限,如果有则直接利用,如果没有则利用requestPermissionsFromUser()进行向用户扣问权限授权
2.如果用户全部拒绝,下一次利用时则利用requestPermissionOnSetting()进行二次用户扣问拉起弹窗(也可以引导用户去设置界面手动授权,在requestPermissionsFromUser()拒绝策略中即可实现)
//校验权限工具类
  1. import { abilityAccessCtrl,Context, bundleManager, common, Permissions } from '@kit.AbilityKit';
  2. import { BusinessError } from '@kit.BasicServicesKit';
  3. /**
  4. * 校验当前是否已经授权(已封装成工具类,如使用三层架构则可放入common中)
  5. * @param permission
  6. * @returns
  7. */
  8. export async function checkPermissionGrant(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  9.   let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  10.   let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
  11.   // 获取应用程序的accessTokenID
  12.   let tokenId: number = 0;
  13.   try {
  14.     let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
  15.     let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
  16.     tokenId = appInfo.accessTokenId;
  17.   } catch (error) {
  18.     const err: BusinessError = error as BusinessError;
  19.     console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
  20.   }
  21.   // 校验应用是否被授予权限
  22.   try {
  23.     grantStatus = await atManager.checkAccessToken(tokenId, permission);
  24.   } catch (error) {
  25.     const err: BusinessError = error as BusinessError;
  26.     console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
  27.   }
  28.   return grantStatus;
  29. }
  30. /**
  31. *向用户二次询问拉起弹窗授权
  32. */
  33. export function reqPermissionTwice(permissions: Array<Permissions>, context: common.UIAbilityContext): Promise<Array<abilityAccessCtrl.GrantStatus>> {
  34.   let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  35.   return atManager.requestPermissionOnSetting(context, permissions)
  36. }
复制代码
//主页面(其中涉及一些工具类未提供,不影响本次示例功能实现,结构会有影响。项目代码还在开发中,之后会开源)
  1. import {AppStorageEnum,NavigationBar,HdUser,hdHttp,cameraCapture,HdLoadingDialo,logger,authStore,reqPermissionTwice,checkPermissionGrant} from "@ss/basic"
  2. import { fileIo, picker } from '@kit.CoreFileKit'
  3. import { BusinessError, request } from '@kit.BasicServicesKit'
  4. import { promptAction } from '@kit.ArkUI'
  5. import { router } from '@kit.ArkUI'
  6. import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'
  7. @Component
  8. export struct MineIndexView {
  9.   pageInfos: NavPathStack = new NavPathStack()
  10.   @StorageProp(AppStorageEnum.TOP_AVOID_HEIGHT)
  11.   avoidTopHeight: number = 0
  12.   @StorageProp(AppStorageEnum.LOGIN_USER)
  13.   loginUser: HdUser = {} as HdUser
  14.   @State isShowSheet: boolean = false
  15.   dialog: CustomDialogController = new CustomDialogController({
  16.     builder: HdLoadingDialog({ message: '更新中...' }),
  17.     customStyle: true,
  18.     alignment: DialogAlignment.Center
  19.   })
  20.   permissions: Array<Permissions> = ['ohos.permission.CAMERA', 'ohos.permission.WRITE_MEDIA'];
  21.   isGrantFirst: boolean = false
  22.   aboutToAppear(): void {
  23.     console.log(JSON.stringify(this.loginUser))
  24.     // this.checkPermissions()
  25.   }
  26.   async checkPermissions(permission: Permissions): Promise<void> {
  27.     //通过checkAccessToken校验本次usergrant是否已授权
  28.     let grantStatus: abilityAccessCtrl.GrantStatus = await checkPermissionGrant(permission);
  29.     //根据返回结果处理业务
  30.     const currentPermission: Array<Permissions> = [permission]
  31.     if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { //授权
  32.       logger.info("已授权")
  33.       if (currentPermission[0] == this.permissions[0]) {
  34.         this.updateAvatarForCamera()
  35.       } else {
  36.         this.updateAvatarForPhotos()
  37.       }
  38.     } else { //未授权
  39.       logger.info("未授权")
  40.       if (this.isGrantFirst) { //因为二次询问授权之前必须调用一次requestPermissionsFromUser
  41.         //二次向用户申请授权api12拥有
  42.         reqPermissionTwice(currentPermission, getContext(this) as common.UIAbilityContext)
  43.           .then((data: Array<abilityAccessCtrl.GrantStatus>) => {
  44.             if (data[0] == abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
  45.               if (currentPermission[0] == this.permissions[0]) {
  46.                 this.updateAvatarForCamera()
  47.               } else {
  48.                 this.updateAvatarForPhotos()
  49.               }
  50.             }
  51.           })
  52.           .catch((err: BusinessError) => {
  53.             console.error('data:' + JSON.stringify(err));
  54.           });
  55.       }
  56.     }
  57.   }
  58.   //询问用户是否授权,只会调起一次弹窗
  59.   reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
  60.     let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  61.     // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
  62.     atManager.requestPermissionsFromUser(context, permissions).then((data) => {
  63.       let grantStatus: Array<number> = data.authResults;
  64.       let length: number = grantStatus.length;
  65.       let count: number = 0
  66.       for (let i = 0; i < length; i++) {
  67.         count = count + 1
  68.         if (grantStatus[i] === 0) {
  69.           // 用户授权,可以继续访问目标操作
  70.           this.isShowSheet = true
  71.         } else {
  72.           // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
  73.           if(count == length)
  74.             this.isShowSheet = false
  75.         }
  76.       }
  77.       // 授权成功
  78.     }).catch((err: BusinessError) => {
  79.       console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
  80.     })
  81.   }
  82.   /**
  83.    * 唤起相机上传图片
  84.    */
  85.   async updateAvatarForCamera() {
  86.     //调用相机唤起工具类
  87.     let URI = await cameraCapture(getContext(this) as common.UIAbilityContext)
  88.     if (URI != undefined) {
  89.       // 2. 存储到沙箱中(fileIo在API9之后就废弃了,使用fs替换)
  90.       const context = getContext(this)
  91.       const fileType = 'jpg'
  92.       const fileName = Date.now() + '.' + fileType
  93.       const copyFilePath = context.cacheDir + '/' + fileName
  94.       const file = fileIo.openSync(URI, fileIo.OpenMode.READ_ONLY)
  95.       fileIo.copyFileSync(file.fd, copyFilePath)
  96.       // 3. 准备请求配置
  97.       const config: request.UploadConfig = {
  98.         url: hdHttp.baseURL + 'userInfo/avatar',
  99.         method: 'POST',
  100.         header: {
  101.           'Accept': '*/*',
  102.           'Authorization': `Bearer ${this.loginUser.token}`,
  103.           'Content-Type': 'multipart/form-data'
  104.         },
  105.         files: [
  106.           {
  107.             name: 'file', //multipart提交时,表单项目的名称,缺省为file
  108.             uri: `internal://cache/` + fileName, //仅支持"internal"协议类型,"internal://cache/"为应用的私有目录,是必填字段
  109.             type: fileType, //文件的内容类型,默认根据文件名或路径的后缀获取。
  110.             filename: fileName //multipart提交时,请求头中的文件名。
  111.           }
  112.         ],
  113.         data: []
  114.       }
  115.       // 4. 开始上传
  116.       this.dialog.open() //自定义加载弹窗
  117.       request.uploadFile(context, config, (err, data) => {
  118.         if (err) {
  119.           return logger.error('UPLOAD', err.message)
  120.         }
  121.         data.on('complete', () => {
  122.           // 5. 更新头像
  123.           hdHttp.get<HdUser>('userInfo').then(res => {
  124.             this.loginUser.avatar = res.data.avatar
  125.             authStore.setUser(this.loginUser)
  126.             promptAction.showToast({ message: '更新头像成功' }) //提示弹窗
  127.             this.dialog.close()
  128.           })
  129.         })
  130.       })
  131.     }
  132.   }
  133.   /**
  134.    * 唤起相册上传图片
  135.    */
  136.   updateAvatarForPhotos() {
  137.     const photoSelectOptions = new picker.PhotoSelectOptions()
  138.     photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE
  139.     photoSelectOptions.maxSelectNumber = 1
  140.     const photoViewPicker = new picker.PhotoViewPicker()
  141.     photoViewPicker.select(photoSelectOptions).then(result => {
  142.       // 1. 得到文件路径
  143.       const URI = result.photoUris[0]
  144.       // 2. 存储到沙箱中(fileIo在API9之后就废弃了,使用fs替换)
  145.       const context = getContext(this)
  146.       const fileType = 'jpg'
  147.       const fileName = Date.now() + '.' + fileType
  148.       const copyFilePath = context.cacheDir + '/' + fileName
  149.       const file = fileIo.openSync(URI, fileIo.OpenMode.READ_ONLY)
  150.       fileIo.copyFileSync(file.fd, copyFilePath)
  151.       // 3. 准备请求配置
  152.       const config: request.UploadConfig = {
  153.         url: hdHttp.baseURL + 'userInfo/avatar',
  154.         method: 'POST',
  155.         header: {
  156.           'Accept': '*/*',
  157.           'Authorization': `Bearer ${this.loginUser.token}`,
  158.           'Content-Type': 'multipart/form-data'
  159.         },
  160.         files: [
  161.           {
  162.             name: 'file', //multipart提交时,表单项目的名称,缺省为file
  163.             uri: `internal://cache/` + fileName, //仅支持"internal"协议类型,"internal://cache/"为应用的私有目录,是必填字段
  164.             type: fileType, //文件的内容类型,默认根据文件名或路径的后缀获取。
  165.             filename: fileName //multipart提交时,请求头中的文件名。
  166.           }
  167.         ],
  168.         data: []
  169.       }
  170.       // 4. 开始上传
  171.       this.dialog.open() //自定义加载弹窗
  172.       request.uploadFile(context, config, (err, data) => {
  173.         if (err) {
  174.           return logger.error('UPLOAD', err.message)
  175.         }
  176.         data.on('complete', () => {
  177.           // 5. 更新头像
  178.           hdHttp.get<HdUser>('userInfo').then(res => {
  179.             this.loginUser.avatar = res.data.avatar
  180.             authStore.setUser(this.loginUser)
  181.             promptAction.showToast({ message: '更新头像成功' }) //提示弹窗
  182.             this.dialog.close()
  183.           })
  184.         })
  185.       })
  186.     })
  187.   }
  188.   @Builder
  189.   builderMenuBack() {
  190.     Button({ type: ButtonType.Circle }) { //设置按钮的类型未圆形按钮
  191.       Image($r("app.media.left"))
  192.     }.onClick(() => {
  193.       router.back()
  194.     })
  195.     .width(25)
  196.     .height(25)
  197.     .backgroundColor('rgba(255, 255, 255, 0)')
  198.   }
  199.   @Builder
  200.   builderMenuMail() {
  201.     Button({ type: ButtonType.Circle }) { //设置按钮的类型未圆形按钮
  202.       Image($r("app.media.mail"))
  203.     }.onClick(() => {
  204.       this.pageInfos.pushPathByName("MailCenterView", false);
  205.     })
  206.     .width(25)
  207.     .height(25)
  208.     .backgroundColor('rgba(255, 255, 255, 0)')
  209.   }
  210.   /**
  211.    * 半模态对话框
  212.    */
  213.   @Builder
  214.   buildAvatarSheet() {
  215.     Column() {
  216.       Text("拍照")
  217.         .width('100%')
  218.         .height('50%')
  219.         .lineHeight('45%')
  220.         .textAlign(TextAlign.Center)
  221.         .backgroundColor('#e5e7ed')
  222.         .onClick(() => {
  223.           this.isShowSheet = !this.isShowSheet
  224.           this.checkPermissions(this.permissions[0])
  225.         })
  226.       Divider().color(Color.White)
  227.       Text("从相册选择")
  228.         .width('100%')
  229.         .height('50%')
  230.         .lineHeight('45%')
  231.         .textAlign(TextAlign.Center)
  232.         .backgroundColor('#e5e7ed')
  233.         .onClick(() => {
  234.         //并非多次一举,再调用reqPermissionsFromUser之前如果打开半模态化转场会阻塞主线程(原因不知)
  235.           if(!this.isGrantFirst) {
  236.                 this.isGrantFirst = true
  237.                 this.reqPermissionsFromUser(this.permissions, getContext(this) as common.UIAbilityContext)
  238.               }else{
  239.                 this.isShowSheet = !this.isShowSheet
  240.               }
  241.         })
  242.     }
  243.     .width('100%')
  244.     .height(150)
  245.   }
  246.   build() {
  247.     Navigation(this.pageInfos) {
  248.       Column() {
  249.         NavigationBar({
  250.           buildButtonBackParam: () => {
  251.             this.builderMenuBack()
  252.           },
  253.           buildMenuOneParam: () => {
  254.             this.builderMenuMail()
  255.           }
  256.         })
  257.         Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center, direction: FlexDirection.Column }) {
  258.           Image($rawfile('kyrie.jpg'))
  259.             .id("mine")
  260.             .width(90)
  261.             .height(90)
  262.             .clip(true)
  263.             .borderRadius(45)
  264.             .margin({ top: 45 })
  265.             .geometryTransition("avatar")
  266.             .transition(TransitionEffect.opacity(0.99))
  267.             .onClick(() => {
  268.               this.isShowSheet = !this.isShowSheet
  269.               if(!this.isGrantFirst)
  270.                 this.reqPermissionsFromUser(this.permissions, getContext(this) as common.UIAbilityContext)
  271.             })
  272.           Text(this.loginUser.username)
  273.             .fontWeight(700)
  274.             .fontSize(16)
  275.             .margin({ top: 8 })
  276.           Button({ type: ButtonType.Capsule, stateEffect: false }) {
  277.             Text("终身大会员")
  278.               .fontColor("#7f8085")
  279.               .fontSize(12)
  280.           }
  281.           .width(150)
  282.           .height(32)
  283.           .margin({ top: 8 })
  284.           .backgroundColor("#dbdce1")
  285.         }
  286.         .bindSheet(this.isShowSheet, this.buildAvatarSheet(), {
  287.           height: 150,
  288.           dragBar: false,
  289.           showClose: false,
  290.           onDisappear: () => {
  291.             this.isShowSheet = false
  292.           }
  293.         })
  294.         .width("90%")
  295.         .height(200)
  296.       }
  297.       .width('100%')
  298.       .transition(TransitionEffect.asymmetric(
  299.         TransitionEffect.opacity(0.99),
  300.         TransitionEffect.OPACITY
  301.       ))
  302.       .height('100%')
  303.     }
  304.     .backgroundColor("#e4e5ea")
  305.   }
  306. }
复制代码
效果图:
1.第一次点击头像

2.假如全部拒绝,第二次访问继续扣问(详细业务权限)


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立聪堂德州十三局店

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表