实战鸿蒙,实现一款权限请求框架

一、申请权限的一般步骤

  • 判断是否有权限,如果有权限,直接进行下一步。
  • 如果没有权限,则开始申请权限。
  • 如果用户授权,进行下一步。
  • 如果用户拒绝授权,后面再次申请权限,系统为了不打扰用户,将不会出现系统的权限弹窗。在用户拒绝授权后,需要弹窗提示用户必须授权才能访问当前功能,并引导用户到系统设置中打开相应的权限。

每次申请权限的时候,都需要经过以上几个步骤,当申请的权限越来越多,大量的重复代码就出现了。为了减少重复代码,我封装了一个权限请求框架。

二、权限请求框架

  桃夭是鸿蒙系统上的一款权限请求框架,封装了权限请求逻辑,采用链式调用的方式请求权限,极大的简化了权限请求的代码,同时支持在UIUIAbilityUIExtensionAbility里面申请权限。需要注意的是,应用在UIExtensionAbility申请授权时,需要在onWindowStageCreate函数执行结束后或在onWindowStageCreate函数回调中申请权限。
  本项目基于开源鸿蒙4.1开发,最低兼容到API 11,请将DevEco Studio升级到最新版,DevEco Studio版本低于5.0.3.403可能无法编译。

三、桃夭名称来源

  桃夭一词出自古代第一部诗歌总集《诗经》中《诗经·桃夭》,“桃之夭夭,灼灼其华。”桃花怒放千万朵,色彩鲜艳红似火。

四、桃夭的使用方式

  下载

ohpm install @shijing/taoyao

  申请权限

TaoYao.with(this)
      .runtime()
      // 要申请的权限
      .permission(permissions)
      .onGranted(() => {
        // 权限申请成功
      })
      .onDenied(() => {
        // 权限申请失败
      })
      .request()

  申请权限变得如此之简单。

五、实现原理

5、1 如何支持在UIUIAbilityUIExtensionAbility里面申请权限。

  可以使用联合类型,也可以使用重载。这里通过重载的方式来实现在UIUIAbilityUIExtensionAbility里面申请权限。

 /**
   * 直接在UIExtensionAbility中申请权限
   *
   * @param uiAbility
   * @returns
   */
  static with(extensionAbility: UIExtensionAbility): IAccessControl;
  /**
   * 在UI中向用户申请授权
   *
   * @param context
   * @returns
   */
  static with(context: common.UIAbilityContext): IAccessControl;

  /**
   * 直接在UIAbility中申请权限
   *
   * @param uiAbility
   * @returns
   */
  static with(uiAbility: UIAbility): IAccessControl;

  static with(context: common.UIAbilityContext | UIAbility | UIExtensionAbility): IAccessControl {
    if (context instanceof UIAbility) {
      return new AccessControl(new UIAbilityOrigin(context))
    } else if (context instanceof UIExtensionAbility) {
      return new AccessControl(new UIExtensionAbilityOrigin(context))
    } else {
      return new AccessControl(new ContextOrigin(context))
    }
  }

  UIUIAbilityUIExtensionAbility里面最重要就是Context对象,申请权限的时候需要传入Context对象,我们需要从UIUIAbilityUIExtensionAbility里面获取Context对象。
  这里采用策略模式。创建接口OriginOrigin代表从哪申请权限,定义getContext方法,由子类实现该方法。

/**
 * 需要UI、UIAbility、UIExtensionAbility申请权限,同时获取Context对象。
 */
export interface Origin {

  /**
   * 获取context对象
   *
   * @returns
   */
  getContext(): Context

}

  ContextOrigin代表在在UI中申请权限,实现Origin接口,重写getContext方法。

/**
 * 在UI中申请权限
 */
export class ContextOrigin implements Origin {

  private context: common.UIAbilityContext

  constructor(context: common.UIAbilityContext) {
    this.context = context
  }

  getContext(): Context {
    return this.context
  }
}

  UIAbilityOrigin代表在在UIAbility中申请权限,同样实现Origin接口,重写getContext方法。

/**
 * 在UIAbility中申请权限
 */
export class UIAbilityOrigin implements Origin {

  private uiAbility: UIAbility

  constructor(uiAbility: UIAbility) {
    this.uiAbility = uiAbility
  }

  getContext(): Context {
    return this.uiAbility.context
  }
}

  UIExtensionAbilityOrigin代表在在UIExtensionAbility中申请权限,同样实现Origin接口,重写getContext方法。

/**
 * 在UIExtensionAbility中申请权限
 */
export class UIExtensionAbilityOrigin implements Origin {

  private uiExtensionAbility: UIExtensionAbility

  constructor(uiExtensionAbility: UIExtensionAbility) {
    this.uiExtensionAbility = uiExtensionAbility
  }

  getContext(): Context {
    return this.uiExtensionAbility.context
  }

}

5、2 检测申请的权限是否在module.json5文件中声明

  申请的权限必须在module.json5文件中声明,否则桃夭会直接抛异常。如何检测申请的权限是否在配置文件中声明?
  如下代码,通过bundleManager对象获取应用信息,之后就可以获取应用在配置文件中声明的权限了。如果要申请的权限没有module.json5文件中声明,那就会抛异常。

/**
   * 检查要申请的权限是否在module.json5文件中声明
   *
   * @param permissions 要申请的权限
   */
  private checkCommonConfig(permissions: Array<Permissions>) {
    const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
    // 同步获取在module.json5文件中声明的所有权限
    const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags)
    const reqPermissionDetails = bundleInfo.reqPermissionDetails
    if (ArrayUtils.isEmpty(reqPermissionDetails)) {
      throw new Error('请在module.json5文件中声明权限')
    }
    const reqPermissions = new ArrayList<string>()
    reqPermissionDetails.forEach(reqPermissionDetail => {
      reqPermissions.add(reqPermissionDetail.name)
    })
    permissions.forEach((permission) => {
      if (!reqPermissions.has(permission)) {
        // 要申请的权限没有module.json5文件中声明
        throw new Error(`请在module.json5文件中声明${permission}权限`)
      }
    })
  }

5、3 检测其它配置

对于位置权限,有三种情况:
  第一:申请模糊位置权限,大部分情况下,不会申请模糊位置权限,更多的是第二种情况。
  第二:申请精确位置权限。
  第三:申请后台位置权限。
针对位置权限,我们需要额外的配置下。
  如果用户申请精确位置权限,那就要先申请粗略位置权限。
  如果用户申请后台位置权限,那就先申请模糊位置权限和精确位置权限。当同意这两个权限后,弹窗提示用户到系统设置中打开相应的权限,用户在设置界面中的选择“始终允许”应用访问位置信息权限,应用就获取了后台位置权限。

 /**
   * 检查权限的其它配置
   *
   * @param permissions
   */
  checkOtherConfig(permissions: Array<Permissions>) {
    const locationPermissionIndex = permissions.indexOf(this.LOCATION_PERMISSION)
    const locationBackgroundIndex = permissions.indexOf(this.LOCATION_IN_BACKGROUND)
    if (locationPermissionIndex >= 0 && locationBackgroundIndex < 0) {
      /*
       * 对于位置权限,有两种情况
       * 第一:申请模糊位置权限,大部分情况下,不会申请模糊位置权限,更多的是第二种情况。
       * 第二:申请精确位置权限,需要先申请模糊位置权限。
       */
      permissions = []
      permissions.push(this.APPROXIMATELY_LOCATION)
      permissions.push(this.LOCATION_PERMISSION)
    }
    if (locationBackgroundIndex >= 0) {
      // 申请后台位置权限,需要先申请模糊位置权限和精确位置权限。当用户点击弹窗授予前台位置权限后,应用通过弹窗、提示窗等形式告知用户前往设置界面授予后台位置权限。
      permissions = []
      permissions.push(this.APPROXIMATELY_LOCATION)
      permissions.push(this.LOCATION_PERMISSION)
      permissions.push(this.LOCATION_IN_BACKGROUND)
    }
    this.setNewPermission(permissions)
  }

5、4 判断是否有权限

  当所有的检测都通过后,就可以判断是否有权限了。调用checkAccessToken()方法来校验当前是否已经授权。如果已经授权,则回调告知调用者已经有权限,否则需要进行下一步操作,即向用户申请授权。

  hasPermission(permissions: Array<Permissions>): boolean {
    for (let i = 0; i < permissions.length; i++) {
      const permission = permissions[i]
      let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
      let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
      // 获取应用程序的accessTokenID
      let tokenId: number = 0;
      try {
        let bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
        let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
        tokenId = appInfo.accessTokenId;
      } catch (error) {
        const err: BusinessError = error as BusinessError;
        console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
      }
      // 校验应用是否被授予权限
      grantStatus = atManager.checkAccessTokenSync(tokenId, permission);
      if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        return false
      }
    }
    return true
  }

5、5 申请权限

  调用requestPermissionsFromUser(),如果用户授权,则调用mOnGranted。如果用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限。

 /**
   * 申请权限
   *
   * @param permissions
   */
  requestPermission(permissions: Permissions[]) {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    atManager.requestPermissionsFromUser(this.origin.getContext(), permissions).then((data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] === 0) {
          // 用户授权,可以继续访问目标操作
          this.mOnGranted?.(this.originPermissions)
        } else {
          // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
          this.mOnDenied?.(this.originPermissions)
          return;
        }
      }
      // 授权成功
    }).catch((err: BusinessError) => {
      console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    })
  }

5、6 系统设置弹窗

  用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限。但在跳转系统设置之前,需要弹窗提示用户,这里提供一个默认的弹窗。如果这个弹窗不满足你的要求,你可以改掉。当用户在弹窗里面点击取消,则隐藏弹窗。当用户在弹窗里面点击去设置,则跳转到系统设置页面。

import { TaoYao } from '@shijing/taoyao/Index'
import { common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'

/**
 * 跳转系统设置之前,需要先弹窗
 */
@CustomDialog
export struct PermissionDialog {

  private title: string = '权限设置'
  private subtitle?: Resource
  private left: string = '取消'
  private right: string = '去设置'
  private permissions = new Array<Permissions>()
  private context = getContext(this) as common.UIAbilityContext
  controller: CustomDialogController

  aboutToAppear(): void {
    if (this.permissions.indexOf(('ohos.permission.ACCESS_BLUETOOTH' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.access_bluetooth')
    } else if (this.permissions.indexOf(('ohos.permission.MEDIA_LOCATION' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.media_location')
    } else if (this.permissions.indexOf(('ohos.permission.APP_TRACKING_CONSENT' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.app_tracking_consent')
    } else if (this.permissions.indexOf(('ohos.permission.ACTIVITY_MOTION' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.activity_motion')
    } else if (this.permissions.indexOf(('ohos.permission.CAMERA' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.camera')
    } else if (this.permissions.indexOf(('ohos.permission.DISTRIBUTED_DATASYNC' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.distributed_datasync')
    } else if (this.permissions.indexOf(('ohos.permission.LOCATION_IN_BACKGROUND' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.location_in_background')
    } else if (this.permissions.indexOf(('ohos.permission.LOCATION' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.location')
    } else if (this.permissions.indexOf(('ohos.permission.APPROXIMATELY_LOCATION' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.approximately_location')
    } else if (this.permissions.indexOf(('ohos.permission.MICROPHONE' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.microphone')
    } else if (this.permissions.indexOf(('ohos.permission.READ_CALENDAR' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.read_calendar')
    } else if (this.permissions.indexOf(('ohos.permission.WRITE_CALENDAR' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.write_calendar')
    } else if (this.permissions.indexOf(('ohos.permission.READ_HEALTH_DATA' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.read_health_data')
    } else if (this.permissions.indexOf(('ohos.permission.READ_MEDIA' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.read_media')
    } else if (this.permissions.indexOf(('ohos.permission.WRITE_MEDIA' as Permissions)) >= 0) {
      this.subtitle = $r('app.string.write_media')
    }
  }

  build() {
    Column() {
      Text(this.title)
        .fontSize(20)
        .fontColor('#151724')
      Text(this.subtitle)
        .fontColor('#151724')
        .fontSize(15)
        .margin({top: 30})
      Row() {
        Button(this.left)
          .fontColor('#585a5c')
          .borderRadius(24)
          .backgroundColor('#eeeeee')
          .width('40%')
          .height(48)
          .margin({right: 20})
          .onClick(() => {
            this.controller.close()
          })
        Button(this.right)
          .fontColor('#ffffff')
          .borderRadius(24)
          .backgroundColor('#4b54fa')
          .width('40%')
          .height(48)
          .onClick(() => {
            this.controller.close()
            TaoYao.goToSettingPage(this.context)
          })
      }
      .margin({top: 30})
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .borderRadius(20)
    .backgroundColor('#ffffff')
    .padding({left: 24, right: 24, top: 30, bottom: 28})
  }
}

5、7 跳转到设置页面

  使用下面的代码即可跳转到系统设置页面。构建一个want对象,指定bundleNameabilityNameuriparameters等参数,调用startAbility

function openPermissionsInSystemSettings(context: common.UIAbilityContext): void {
  let wantInfo: Want = {
    bundleName: 'com.huawei.hmos.settings', // 系统设置的包名
    abilityName: 'com.huawei.hmos.settings.MainAbility', // 系统设置权限页面的类名
    uri: 'application_info_entry',
    parameters: {
      pushParams: 'com.example.myapplication' // 应用的包名,也就是打开指定应用的详情页面
    }
  }
  context.startAbility(wantInfo).then(() => {
    // ...
  }).catch((err: BusinessError) => {
    // ...
  })

  目前只有华为手机使用了开源鸿蒙系统,不排除后续会有其它的厂商使用开源鸿蒙系统,到时want对象的bundleNameabilityNameuri可能会不一样。在这种情况下,上面的代码就会有兼容性问题。这就需要针对不同的品牌,创建不同的want对象。这里采用策略模式。如下代码,创建SettingWant接口,定义getWant方法,由子类实现该方法,也就是由子类来创建want对象。

export interface SettingWant {

  /**
   * 获取want对象
   *
   * @param bundleName
   * @returns
   */
  getWant(bundleName: string): Want
}

  新建DefaultSettingWant类,DefaultSettingWant是一个默认创建Want对象的子类。

/**
 * 默认获取的want参数
 */
export class DefaultSettingWant implements SettingWant {

  getWant(bundleName: string): Want {
    let wantInfo: Want = {
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry',
      parameters: {
        pushParams: bundleName // 打开指定应用的详情页面
      }
    }
    return wantInfo
  }

}

  对于华为手机,我们就继承DefaultSettingWant,直接使用默认创建的Want对象。

/**
 * 获取华为手机上的want参数
 */
export class HuaWeiSettingWant extends DefaultSettingWant {
}

  如下代码,先创建SettingWant对象,通过deviceInfo.brand判断品牌,如果是华为手机,则创建HuaWeiSettingWant。调用getWant获取到Want对象,调用startAbility跳转到系统设置。

gToSettingPage(): void {
    const bundleName = this.getContext().abilityInfo.bundleName
    let settingWant: SettingWant
    if (deviceInfo.brand === "HUAWEI") {
      settingWant = new HuaWeiSettingWant()
    } else {
      settingWant = new DefaultSettingWant()
    }
    const want = settingWant.getWant(bundleName)
    if (this.origin instanceof UIExtensionAbilityOrigin) {
      // 在UIExtensionAbility中跳转到系统设置页面
      this.startAbilityFromUIExtensionAbility(want)
    } else {
      // 在UI或者UIAbility中跳转到系统设置页面
      this.startAbilityFromUIAbility(want)
    }
  }

 /**
   * 在UIExtensionAbility中跳转到系统设置页面
   *
   * @param want
   */
  private startAbilityFromUIExtensionAbility(want: Want) {
    (this.origin.getContext() as common.UIExtensionContext).startAbility(want).then(() => {
      // 跳转成功
    }).catch((err: BusinessError) => {
      console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    })
  }

  /**
   * 在UI或者UIAbility中跳转到系统设置页面
   *
   * @param want
   */
  private startAbilityFromUIAbility(want: Want) {
    this.getContext().startAbility(want).then(() => {
      // 跳转成功
    }).catch((err: BusinessError) => {
      console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    })
  }

  getContext(): common.UIAbilityContext {
    return (this.origin.getContext()) as common.UIAbilityContext
  }

六、源码

  更多具体的代码,请下载源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容