鴻蒙APP權(quán)限管理

前言

系統(tǒng)提供了一種允許應用訪問系統(tǒng)資源(如:通訊錄等)和系統(tǒng)能力(如:訪問攝像頭、麥克風等)的通用權(quán)限訪問方式,來保護系統(tǒng)數(shù)據(jù)(包括用戶個人數(shù)據(jù))或功能,避免它們被不當或惡意使用。

權(quán)限組和子權(quán)限

為了盡可能減少系統(tǒng)彈出的權(quán)限彈窗數(shù)量,優(yōu)化交互體驗,系統(tǒng)將邏輯緊密相關(guān)的user_grant權(quán)限組合在一起,形成多個權(quán)限組。
當應用請求權(quán)限時,同一個權(quán)限組的權(quán)限將會在一個彈窗內(nèi)一起請求用戶授權(quán)。權(quán)限組中的某個權(quán)限,稱之為該權(quán)限組的子權(quán)限。

例如圖片和視頻讀取權(quán)限和寫入權(quán)限,可以放到一起合并申請,我們可以看到申請權(quán)限的 api 中權(quán)限的參數(shù)接收的是一個數(shù)組類型: requestPermissionsFromUser(context: Context, permissionList: Array<Permissions>, requestCallback: AsyncCallback<PermissionRequestResult>): void

具體哪些權(quán)限屬于同個權(quán)限組,可以參考應用權(quán)限組列表

在配置文件中聲明權(quán)限

在Entry 模塊的module.json5 中配置"requestPermissions",配置應用中需要的權(quán)限類型,示例中配置了相機權(quán)限、圖片和視頻的讀寫權(quán)限,其中圖片和視頻的讀和寫可以合并為一個權(quán)限組,詳見后面示例。

    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:permission_app_camera_reason",
        "usedScene": {
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:permission_app_read_image_video_reason",
        "usedScene": {
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:permission_app_write_image_video_reason",
        "usedScene": {
          "when": "inuse"
        }
      }
    ]

檢查權(quán)限是否已授權(quán)

在申請權(quán)限之前,先通過checkAccessToken()校驗權(quán)限是否已授權(quán)。

  /**
   * 檢查權(quán)限狀態(tài)
   * @param permission 權(quán)限
   */
  static async checkPermissionGrantStatus(permission: Permissions): Promise<boolean> {
    // 獲取應用程序的accessTokenID。
    let tokenId: number = 0;
    try {
      let bundleInfo: bundleManager.BundleInfo =
        await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
      tokenId = bundleInfo.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}`);
    }
    // 權(quán)限狀態(tài)
    let grantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
    // 校驗應用是否被授予權(quán)限。
    try {
      grantStatus = await abilityAccessCtrl.createAtManager().checkAccessToken(tokenId, permission);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
    }

    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  }

向用戶申請權(quán)限

向用戶申請權(quán)限可以通過requestPermissionsFromUser()實現(xiàn),系統(tǒng)不鼓勵頻繁彈窗打擾用戶。如果用戶拒絕授權(quán),將無法再次彈窗。但我們可能有這么一個場景,需要在非首次請求用戶權(quán)限獲取到未授權(quán)狀態(tài)時,引導用戶到系統(tǒng)應用設(shè)置去進行開啟權(quán)限,如何判斷呢?requestPermissionsFromUser的回調(diào)中在 api12+增加了dialogShownResults屬性,用于判斷是否進行了彈窗,因此我們請求權(quán)限的方法除了回調(diào)授權(quán)狀態(tài),還回調(diào)了是否有彈窗。

  /**
   * 請求權(quán)限
   * @param permissions 權(quán)限列表
   */
  static requestPermissions(context: Context, permissions: Permissions[]): Promise<[boolean, boolean]> {
    return new Promise<[boolean, boolean]>((resolve, reject) => {
      abilityAccessCtrl.createAtManager().requestPermissionsFromUser(context, permissions)
        .then((result) => {
          let isGrant = true
          result.authResults.forEach((authResult) => {
            if (authResult !== 0) {
              isGrant = false
            }
          })
          let isDialogShown = false
          if (result.dialogShownResults) {
            isDialogShown = result.dialogShownResults.includes(true)
          }
          resolve([isGrant, isDialogShown])
        })
        .catch((err: BusinessError) => {
          reject()
        })
    })
  }

引導用戶跳轉(zhuǎn)系統(tǒng)應用設(shè)置

當請求用戶授權(quán)首次彈窗后,無法再次彈窗,但用戶未來可能希望更改授權(quán)狀態(tài),應用需引導用戶在系統(tǒng)設(shè)置中手動授予權(quán)限,可以從應用中直接跳轉(zhuǎn)到系統(tǒng)應用設(shè)置中。

/**
   * 跳轉(zhuǎn)系統(tǒng)應用設(shè)置
   * @param context
   */
  static async jumpToPermission(context: common.UIAbilityContext): Promise<void> {
    let bundleInfo: bundleManager.BundleInfo =
      await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let want: Want = {
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry',
      parameters: {
        pushParams: bundleInfo.name
      }
    };
    context.startAbility(want).then(() => {
      console.info('TAG', 'jump to permission success.');
    }).catch((error: BusinessError) => {
      console.error('TAG', `jump to permission failed. Code: ${error.code}, message is ${error.message}`);
    });
  }

完整代碼

ObservedArray.ets

/**
 * 觀察數(shù)組
 */
export class ObservedArray<T> extends Array<T> {
  constructor(...args: T[]) {
    super(...args)
  }
}

UserPermissionModel.ets

import { Permissions } from '@kit.AbilityKit'
import { UserPermissionHandle } from './UserPermissionHandle'

/**
 * 用戶權(quán)限模型
 */
@Observed
export class UserPermissionModel {
  name: string
  permissions: Permissions[]
  isGrant: boolean = false

  constructor(name: string, permission: Permissions[]) {
    this.name = name
    this.permissions = permission
  }

  // 檢查權(quán)限
  async checkPermissionGrantStatus() {
    try {
      let isGrant = true
      for (let i = 0; i < this.permissions.length; i++) {
        let permission = this.permissions[i]
        let value = await UserPermissionHandle.checkPermissionGrantStatus(permission)
        if (!value) {
          isGrant = false // 其中一個未授權(quán),權(quán)限組判定為未授權(quán)
        }
      }
      this.isGrant = isGrant
    } catch (err) {
      this.isGrant = false
    }
  }

  // 請求權(quán)限
  async requestPermissions(context: Context) {
    return await UserPermissionHandle.requestPermissions(context, this.permissions)
  }
}

UserPermissionHandle.ets

import abilityAccessCtrl, { PermissionRequestResult, Permissions } from "@ohos.abilityAccessCtrl";
import bundleManager from "@ohos.bundle.bundleManager";
import { BusinessError } from "@kit.BasicServicesKit";
import { common, Context, Want } from "@kit.AbilityKit";

/**
 * 用戶權(quán)限處理類
 */
export class UserPermissionHandle {

  /**
   * 檢查權(quán)限狀態(tài)
   * @param permission 權(quán)限
   */
  static async checkPermissionGrantStatus(permission: Permissions): Promise<boolean> {
    // 獲取應用程序的accessTokenID。
    let tokenId: number = 0;
    try {
      let bundleInfo: bundleManager.BundleInfo =
        await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
      tokenId = bundleInfo.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}`);
    }
    // 權(quán)限狀態(tài)
    let grantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
    // 校驗應用是否被授予權(quán)限。
    try {
      grantStatus = await abilityAccessCtrl.createAtManager().checkAccessToken(tokenId, permission);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
    }

    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  }

  /**
   * 請求權(quán)限
   * @param permissions 權(quán)限列表
   */
  static requestPermissions(context: Context, permissions: Permissions[]): Promise<[boolean, boolean]> {
    return new Promise<[boolean, boolean]>((resolve, reject) => {
      abilityAccessCtrl.createAtManager().requestPermissionsFromUser(context, permissions)
        .then((result) => {
          let isGrant = true
          result.authResults.forEach((authResult) => {
            if (authResult !== 0) {
              isGrant = false
            }
          })
          let isDialogShown = false
          if (result.dialogShownResults) {
            isDialogShown = result.dialogShownResults.includes(true)
          }
          resolve([isGrant, isDialogShown])
        })
        .catch((err: BusinessError) => {
          reject()
        })
    })
  }

  /**
   * 跳轉(zhuǎn)系統(tǒng)應用設(shè)置
   * @param context
   */
  static async jumpToPermission(context: common.UIAbilityContext): Promise<void> {
    let bundleInfo: bundleManager.BundleInfo =
      await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let want: Want = {
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry',
      parameters: {
        pushParams: bundleInfo.name
      }
    };
    context.startAbility(want).then(() => {
      console.info('TAG', 'jump to permission success.');
    }).catch((error: BusinessError) => {
      console.error('TAG', `jump to permission failed. Code: ${error.code}, message is ${error.message}`);
    });
  }
}

Index.ets

import { ObservedArray } from '../common/ObservedArray';
import { UserPermissionHandle } from '../common/UserPermissionHandle';
import { UserPermissionModel } from '../common/UserPermissionModel';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Index {

  @State permissionModels: ObservedArray<UserPermissionModel> = new ObservedArray<UserPermissionModel>(
    new UserPermissionModel('相機權(quán)限', ['ohos.permission.CAMERA']),
    new UserPermissionModel('相冊權(quán)限', ['ohos.permission.READ_IMAGEVIDEO', 'ohos.permission.WRITE_IMAGEVIDEO']),
    new UserPermissionModel('麥克風權(quán)限', ['ohos.permission.MICROPHONE']),
    new UserPermissionModel('位置信息權(quán)限', ['ohos.permission.APPROXIMATELY_LOCATION', 'ohos.permission.LOCATION']),
  )

  onPageShow(): void {
    this.permissionModels.forEach((model) => {
      model.checkPermissionGrantStatus()
    })
  }

  build() {
    Navigation() {
      Column() {
        List() {
          ForEach(this.permissionModels, (model: UserPermissionModel) => {
            ListItemView({
              model: model,
              onClickToggle: async () => {
                let context = this.getUIContext().getHostContext() as common.UIAbilityContext
                if (!context) {
                  return
                }
                if (!model.isGrant) {
                  let result = await UserPermissionHandle.requestPermissions(context, model.permissions)
                  let isGrant = result[0]
                  let isDialogShown = result[1]
                  model.isGrant = isGrant
                  if (!isGrant && !isDialogShown) {
                    // 未授權(quán),且無彈窗,則跳轉(zhuǎn)系統(tǒng)應用設(shè)置
                    UserPermissionHandle.jumpToPermission(context)
                  }
                } else {
                  UserPermissionHandle.jumpToPermission(context)
                }
              }
            })
              .padding({
                left: 20,
                right: 20
              })
          })
        }
        .width('100%')
        .layoutWeight(1)
        .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .title('權(quán)限管理')
  }
}

@Component
struct ListItemView {

  @ObjectLink model: UserPermissionModel

  onClickToggle?: () => void

  build() {
    Row() {
      Text(this.model.name)
      Stack() {
        Toggle({ type: ToggleType.Switch, isOn: this.model.isGrant })
          .margin({ left: 10 })
          .onTouchIntercept((event) => {
            return HitTestMode.None
          })
      }
      .onClick(() => {
        this.onClickToggle?.()
      })
    }
    .width('100%')
    .height(50)
    .justifyContent(FlexAlign.SpaceBetween)
  }
}
圖片和視頻讀取&圖片和視頻寫入兩者合并為一個權(quán)限組一起申請

參考資料

聲明權(quán)限
向用戶申請授權(quán)
應用權(quán)限組列表

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容