HarmonyOS NEXT體驗官#實戰(zhàn)鴻蒙,實現(xiàn)一款權限請求框架
一、申請權限的一般步驟
- 判斷是否有權限,如果有權限,直接進行下一步。
- 如果沒有權限,則開始申請權限。
- 如果用戶授權,進行下一步。
- 如果用戶拒絕授權,后面再次申請權限,系統(tǒng)為了不打擾用戶,將不會出現(xiàn)系統(tǒng)的權限彈窗。在用戶拒絕授權后,需要彈窗提示用戶必須授權才能訪問當前功能,并引導用戶到系統(tǒng)設置中打開相應的權限。
每次申請權限的時候,都需要經過以上幾個步驟,當申請的權限越來越多,大量的重復代碼就出現(xiàn)了。為了減少重復代碼,我封裝了一個權限請求框架。
二、權限請求框架
桃夭是鴻蒙系統(tǒng)上的一款權限請求框架,封裝了權限請求邏輯,采用鏈式調用的方式請求權限,極大的簡化了權限請求的代碼,同時支持在UI、UIAbility、UIExtensionAbility里面申請權限。需要注意的是,應用在UIExtensionAbility申請授權時,需要在onWindowStageCreate函數執(zhí)行結束后或在onWindowStageCreate函數回調中申請權限。
本項目基于開源鴻蒙4.1開發(fā),最低兼容到API 11,請將DevEco Studio升級到最新版,DevEco Studio版本低于5.0.3.403可能無法編譯。
三、桃夭名稱來源
桃夭一詞出自古代第一部詩歌總集《詩經》中《詩經·桃夭》,“桃之夭夭,灼灼其華?!碧一ㄅ徘f朵,色彩鮮艷紅似火。
四、桃夭的使用方式
下載
ohpm install @shijing/taoyao
申請權限
TaoYao.with(this)
.runtime()
// 要申請的權限
.permission(permissions)
.onGranted(() => {
// 權限申請成功
})
.onDenied(() => {
// 權限申請失敗
})
.request()
申請權限變得如此之簡單。
五、實現(xiàn)原理
1.如何支持在UI、UIAbility、UIExtensionAbility里面申請權限。
可以使用聯(lián)合類型,也可以使用重載。這里通過重載的方式來實現(xiàn)在UI、UIAbility、UIExtensionAbility里面申請權限。
/**
* 直接在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))
}
}
UI、UIAbility、UIExtensionAbility里面最重要就是Context對象,申請權限的時候需要傳入Context對象,我們需要從UI、UIAbility、UIExtensionAbility里面獲取Context對象。這里采用策略模式。創(chuàng)建接口Origin,Origin代表從哪申請權限,定義getContext方法,由子類實現(xiàn)該方法。
/**
* 需要UI、UIAbility、UIExtensionAbility申請權限,同時獲取Context對象。
*/
export interface Origin {
/**
* 獲取context對象
*
* @returns
*/
getContext(): Context
}
ContextOrigin代表在在UI中申請權限,實現(xiàn)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中申請權限,同樣實現(xiàn)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中申請權限,同樣實現(xiàn)Origin接口,重寫getContext方法。
/**
* 在UIExtensionAbility中申請權限
*/
export class UIExtensionAbilityOrigin implements Origin {
private uiExtensionAbility: UIExtensionAbility
constructor(uiExtensionAbility: UIExtensionAbility) {
this.uiExtensionAbility = uiExtensionAbility
}
getContext(): Context {
return this.uiExtensionAbility.context
}
}
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}權限`)
}
})
}
3.檢測其它配置
對于位置權限,有三種情況:
第一:申請模糊位置權限,大部分情況下,不會申請模糊位置權限,更多的是第二種情況。
第二:申請精確位置權限。
第三:申請后臺位置權限。
針對位置權限,我們需要額外的配置下。
如果用戶申請精確位置權限,那就要先申請粗略位置權限。
如果用戶申請后臺位置權限,那就先申請模糊位置權限和精確位置權限。當同意這兩個權限后,彈窗提示用戶到系統(tǒng)設置中打開相應的權限,用戶在設置界面中的選擇“始終允許”應用訪問位置信息權限,應用就獲取了后臺位置權限。
/**
* 檢查權限的其它配置
*
* @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)
}
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.申請權限
調用requestPermissionsFromUser(),如果用戶授權,則調用mOnGranted。如果用戶拒絕授權,提示用戶必須授權才能訪問當前頁面的功能,并引導用戶到系統(tǒng)設置中打開相應的權限。
/**
* 申請權限
*
* @param permissions
*/
requestPermission(permissions: Permissions[]) {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser會判斷權限的授權狀態(tài)來決定是否喚起彈窗
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) {
// 用戶授權,可以繼續(xù)訪問目標操作
this.mOnGranted?.(this.originPermissions)
} else {
// 用戶拒絕授權,提示用戶必須授權才能訪問當前頁面的功能,并引導用戶到系統(tǒng)設置中打開相應的權限
this.mOnDenied?.(this.originPermissions)
return;
}
}
// 授權成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
6.系統(tǒng)設置彈窗
用戶拒絕授權,提示用戶必須授權才能訪問當前頁面的功能,并引導用戶到系統(tǒng)設置中打開相應的權限。但在跳轉系統(tǒng)設置之前,需要彈窗提示用戶,這里提供一個默認的彈窗。如果這個彈窗不滿足你的要求,你可以改掉。當用戶在彈窗里面點擊取消,則隱藏彈窗。當用戶在彈窗里面點擊去設置,則跳轉到系統(tǒng)設置頁面。
import { TaoYao } from '@shijing/taoyao/Index'
import { common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
/**
* 跳轉系統(tǒng)設置之前,需要先彈窗
*/
@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})
}
}
7.跳轉到設置頁面
使用下面的代碼即可跳轉到系統(tǒng)設置頁面。構建一個want對象,指定bundleName 、abilityName 、uri 、parameters 等參數,調用startAbility 。
function openPermissionsInSystemSettings(context: common.UIAbilityContext): void {
let wantInfo: Want = {
bundleName: 'com.huawei.hmos.settings', // 系統(tǒng)設置的包名
abilityName: 'com.huawei.hmos.settings.MainAbility', // 系統(tǒng)設置權限頁面的類名
uri: 'application_info_entry',
parameters: {
pushParams: 'com.example.myapplication' // 應用的包名,也就是打開指定應用的詳情頁面
}
}
context.startAbility(wantInfo).then(() => {
// ...
}).catch((err: BusinessError) => {
// ...
})
目前只有華為手機使用了開源鴻蒙系統(tǒng),不排除后續(xù)會有其它的廠商使用開源鴻蒙系統(tǒng),到時want對象的bundleName、abilityName、uri可能會不一樣。在這種情況下,上面的代碼就會有兼容性問題。這就需要針對不同的品牌,創(chuàng)建不同的want對象。這里采用策略模式。如下代碼,創(chuàng)建SettingWant接口,定義getWant方法,由子類實現(xiàn)該方法,也就是由子類來創(chuàng)建want對象。
export interface SettingWant {
/**
* 獲取want對象
*
* @param bundleName
* @returns
*/
getWant(bundleName: string): Want
}
新建DefaultSettingWant類,DefaultSettingWant是一個默認創(chuàng)建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,直接使用默認創(chuàng)建的Want對象。
/**
* 獲取華為手機上的want參數
*/
export class HuaWeiSettingWant extends DefaultSettingWant {
}
如下代碼,先創(chuàng)建SettingWant 對象,通過deviceInfo.brand判斷品牌,如果是華為手機,則創(chuàng)建HuaWeiSettingWant 。調用getWant獲取到Want對象,調用startAbility跳轉到系統(tǒng)設置。
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中跳轉到系統(tǒng)設置頁面
this.startAbilityFromUIExtensionAbility(want)
} else {
// 在UI或者UIAbility中跳轉到系統(tǒng)設置頁面
this.startAbilityFromUIAbility(want)
}
}
/**
* 在UIExtensionAbility中跳轉到系統(tǒng)設置頁面
*
* @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中跳轉到系統(tǒng)設置頁面
*
* @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
}
六、源碼
更多具體的代碼,請下載源碼或者查看OpenHarmony三方庫中心倉。