一茧彤、申請權(quán)限的一般步驟
- 判斷是否有權(quán)限骡显,如果有權(quán)限,直接進(jìn)行下一步。
- 如果沒有權(quán)限惫谤,則開始申請權(quán)限壁顶。
- 如果用戶授權(quán),進(jìn)行下一步溜歪。
- 如果用戶拒絕授權(quán)博助,后面再次申請權(quán)限,系統(tǒng)為了不打擾用戶痹愚,將不會出現(xiàn)系統(tǒng)的權(quán)限彈窗富岳。在用戶拒絕授權(quán)后,需要彈窗提示用戶必須授權(quán)才能訪問當(dāng)前功能拯腮,并引導(dǎo)用戶到系統(tǒng)設(shè)置中打開相應(yīng)的權(quán)限窖式。
每次申請權(quán)限的時候,都需要經(jīng)過以上幾個步驟动壤,當(dāng)申請的權(quán)限越來越多萝喘,大量的重復(fù)代碼就出現(xiàn)了。為了減少重復(fù)代碼琼懊,我封裝了一個權(quán)限請求框架阁簸。
二、權(quán)限請求框架
??桃夭是鴻蒙系統(tǒng)上的一款權(quán)限請求框架哼丈,封裝了權(quán)限請求邏輯启妹,采用鏈?zhǔn)秸{(diào)用的方式請求權(quán)限,極大的簡化了權(quán)限請求的代碼醉旦,同時支持在UI
饶米、UIAbility
、UIExtensionAbility
里面申請權(quán)限车胡。需要注意的是檬输,應(yīng)用在UIExtensionAbility
申請授權(quán)時,需要在onWindowStageCreate函數(shù)執(zhí)行結(jié)束后或在onWindowStageCreate
函數(shù)回調(diào)中申請權(quán)限匈棘。
??本項目基于開源鴻蒙4.1開發(fā)丧慈,最低兼容到API 11,請將DevEco Studio升級到最新版主卫,DevEco Studio版本低于5.0.3.403可能無法編譯逃默。
三、桃夭名稱來源
??桃夭一詞出自古代第一部詩歌總集《詩經(jīng)》中《詩經(jīng)·桃夭》队秩,“桃之夭夭笑旺,灼灼其華♀勺剩”桃花怒放千萬朵筒主,色彩鮮艷紅似火关噪。
四、桃夭的使用方式
??下載
ohpm install @shijing/taoyao
??申請權(quán)限
TaoYao.with(this)
.runtime()
// 要申請的權(quán)限
.permission(permissions)
.onGranted(() => {
// 權(quán)限申請成功
})
.onDenied(() => {
// 權(quán)限申請失敗
})
.request()
??申請權(quán)限變得如此之簡單乌妙。
五使兔、實現(xiàn)原理
5、1 如何支持在UI
藤韵、UIAbility
虐沥、UIExtensionAbility
里面申請權(quán)限。
??可以使用聯(lián)合類型泽艘,也可以使用重載欲险。這里通過重載的方式來實現(xiàn)在UI
、UIAbility
匹涮、UIExtensionAbility
里面申請權(quán)限天试。
/**
* 直接在UIExtensionAbility中申請權(quán)限
*
* @param uiAbility
* @returns
*/
static with(extensionAbility: UIExtensionAbility): IAccessControl;
/**
* 在UI中向用戶申請授權(quán)
*
* @param context
* @returns
*/
static with(context: common.UIAbilityContext): IAccessControl;
/**
* 直接在UIAbility中申請權(quán)限
*
* @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
對象喜每,申請權(quán)限的時候需要傳入Context
對象,我們需要從UI
雳攘、UIAbility
带兜、UIExtensionAbility
里面獲取Context
對象。
??這里采用策略模式吨灭。創(chuàng)建接口Origin
刚照,Origin
代表從哪申請權(quán)限,定義getContext
方法沃于,由子類實現(xiàn)該方法涩咖。
/**
* 需要UI、UIAbility繁莹、UIExtensionAbility申請權(quán)限,同時獲取Context對象特幔。
*/
export interface Origin {
/**
* 獲取context對象
*
* @returns
*/
getContext(): Context
}
??ContextOrigin
代表在在UI中申請權(quán)限咨演,實現(xiàn)Origin
接口,重寫getContext
方法蚯斯。
/**
* 在UI中申請權(quán)限
*/
export class ContextOrigin implements Origin {
private context: common.UIAbilityContext
constructor(context: common.UIAbilityContext) {
this.context = context
}
getContext(): Context {
return this.context
}
}
??UIAbilityOrigin
代表在在UIAbility
中申請權(quán)限薄风,同樣實現(xiàn)Origin
接口,重寫getContext
方法拍嵌。
/**
* 在UIAbility中申請權(quán)限
*/
export class UIAbilityOrigin implements Origin {
private uiAbility: UIAbility
constructor(uiAbility: UIAbility) {
this.uiAbility = uiAbility
}
getContext(): Context {
return this.uiAbility.context
}
}
??UIExtensionAbilityOrigin
代表在在UIExtensionAbility
中申請權(quán)限遭赂,同樣實現(xiàn)Origin
接口,重寫getContext
方法横辆。
/**
* 在UIExtensionAbility中申請權(quán)限
*/
export class UIExtensionAbilityOrigin implements Origin {
private uiExtensionAbility: UIExtensionAbility
constructor(uiExtensionAbility: UIExtensionAbility) {
this.uiExtensionAbility = uiExtensionAbility
}
getContext(): Context {
return this.uiExtensionAbility.context
}
}
5撇他、2 檢測申請的權(quán)限是否在module.json5文件中聲明
??申請的權(quán)限必須在module.json5文件中聲明,否則桃夭會直接拋異常。如何檢測申請的權(quán)限是否在配置文件中聲明困肩?
??如下代碼划纽,通過bundleManager
對象獲取應(yīng)用信息,之后就可以獲取應(yīng)用在配置文件中聲明的權(quán)限了锌畸。如果要申請的權(quán)限沒有module.json5文件中聲明勇劣,那就會拋異常。
/**
* 檢查要申請的權(quán)限是否在module.json5文件中聲明
*
* @param permissions 要申請的權(quán)限
*/
private checkCommonConfig(permissions: Array<Permissions>) {
const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
// 同步獲取在module.json5文件中聲明的所有權(quán)限
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleFlags)
const reqPermissionDetails = bundleInfo.reqPermissionDetails
if (ArrayUtils.isEmpty(reqPermissionDetails)) {
throw new Error('請在module.json5文件中聲明權(quán)限')
}
const reqPermissions = new ArrayList<string>()
reqPermissionDetails.forEach(reqPermissionDetail => {
reqPermissions.add(reqPermissionDetail.name)
})
permissions.forEach((permission) => {
if (!reqPermissions.has(permission)) {
// 要申請的權(quán)限沒有module.json5文件中聲明
throw new Error(`請在module.json5文件中聲明${permission}權(quán)限`)
}
})
}
5潭枣、3 檢測其它配置
對于位置權(quán)限比默,有三種情況:
??第一:申請模糊位置權(quán)限,大部分情況下盆犁,不會申請模糊位置權(quán)限命咐,更多的是第二種情況。
??第二:申請精確位置權(quán)限蚣抗。
??第三:申請后臺位置權(quán)限侈百。
針對位置權(quán)限,我們需要額外的配置下翰铡。
??如果用戶申請精確位置權(quán)限钝域,那就要先申請粗略位置權(quán)限。
??如果用戶申請后臺位置權(quán)限锭魔,那就先申請模糊位置權(quán)限和精確位置權(quán)限例证。當(dāng)同意這兩個權(quán)限后,彈窗提示用戶到系統(tǒng)設(shè)置中打開相應(yīng)的權(quán)限迷捧,用戶在設(shè)置界面中的選擇“始終允許”應(yīng)用訪問位置信息權(quán)限织咧,應(yīng)用就獲取了后臺位置權(quán)限。
/**
* 檢查權(quán)限的其它配置
*
* @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) {
/*
* 對于位置權(quán)限漠秋,有兩種情況
* 第一:申請模糊位置權(quán)限笙蒙,大部分情況下,不會申請模糊位置權(quán)限庆锦,更多的是第二種情況捅位。
* 第二:申請精確位置權(quán)限,需要先申請模糊位置權(quán)限搂抒。
*/
permissions = []
permissions.push(this.APPROXIMATELY_LOCATION)
permissions.push(this.LOCATION_PERMISSION)
}
if (locationBackgroundIndex >= 0) {
// 申請后臺位置權(quán)限艇搀,需要先申請模糊位置權(quán)限和精確位置權(quán)限。當(dāng)用戶點擊彈窗授予前臺位置權(quán)限后求晶,應(yīng)用通過彈窗焰雕、提示窗等形式告知用戶前往設(shè)置界面授予后臺位置權(quán)限。
permissions = []
permissions.push(this.APPROXIMATELY_LOCATION)
permissions.push(this.LOCATION_PERMISSION)
permissions.push(this.LOCATION_IN_BACKGROUND)
}
this.setNewPermission(permissions)
}
5芳杏、4 判斷是否有權(quán)限
??當(dāng)所有的檢測都通過后矩屁,就可以判斷是否有權(quán)限了辟宗。調(diào)用checkAccessToken()
方法來校驗當(dāng)前是否已經(jīng)授權(quán)。如果已經(jīng)授權(quán)档插,則回調(diào)告知調(diào)用者已經(jīng)有權(quán)限慢蜓,否則需要進(jìn)行下一步操作,即向用戶申請授權(quán)郭膛。
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;
// 獲取應(yīng)用程序的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}`);
}
// 校驗應(yīng)用是否被授予權(quán)限
grantStatus = atManager.checkAccessTokenSync(tokenId, permission);
if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return false
}
}
return true
}
5晨抡、5 申請權(quán)限
??調(diào)用requestPermissionsFromUser()
,如果用戶授權(quán)则剃,則調(diào)用mOnGranted
耘柱。如果用戶拒絕授權(quán),提示用戶必須授權(quán)才能訪問當(dāng)前頁面的功能棍现,并引導(dǎo)用戶到系統(tǒng)設(shè)置中打開相應(yīng)的權(quán)限调煎。
/**
* 申請權(quán)限
*
* @param permissions
*/
requestPermission(permissions: Permissions[]) {
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser會判斷權(quán)限的授權(quán)狀態(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) {
// 用戶授權(quán),可以繼續(xù)訪問目標(biāo)操作
this.mOnGranted?.(this.originPermissions)
} else {
// 用戶拒絕授權(quán)己肮,提示用戶必須授權(quán)才能訪問當(dāng)前頁面的功能士袄,并引導(dǎo)用戶到系統(tǒng)設(shè)置中打開相應(yīng)的權(quán)限
this.mOnDenied?.(this.originPermissions)
return;
}
}
// 授權(quán)成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
5、6 系統(tǒng)設(shè)置彈窗
??用戶拒絕授權(quán)谎僻,提示用戶必須授權(quán)才能訪問當(dāng)前頁面的功能娄柳,并引導(dǎo)用戶到系統(tǒng)設(shè)置中打開相應(yīng)的權(quán)限。但在跳轉(zhuǎn)系統(tǒng)設(shè)置之前艘绍,需要彈窗提示用戶赤拒,這里提供一個默認(rèn)的彈窗。如果這個彈窗不滿足你的要求诱鞠,你可以改掉挎挖。當(dāng)用戶在彈窗里面點擊取消,則隱藏彈窗航夺。當(dāng)用戶在彈窗里面點擊去設(shè)置蕉朵,則跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面。
import { TaoYao } from '@shijing/taoyao/Index'
import { common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
/**
* 跳轉(zhuǎn)系統(tǒng)設(shè)置之前阳掐,需要先彈窗
*/
@CustomDialog
export struct PermissionDialog {
private title: string = '權(quán)限設(shè)置'
private subtitle?: Resource
private left: string = '取消'
private right: string = '去設(shè)置'
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 跳轉(zhuǎn)到設(shè)置頁面
??使用下面的代碼即可跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面。構(gòu)建一個want
對象锚烦,指定bundleName
、abilityName
帝雇、uri
涮俄、parameters
等參數(shù),調(diào)用startAbility
尸闸。
function openPermissionsInSystemSettings(context: common.UIAbilityContext): void {
let wantInfo: Want = {
bundleName: 'com.huawei.hmos.settings', // 系統(tǒng)設(shè)置的包名
abilityName: 'com.huawei.hmos.settings.MainAbility', // 系統(tǒng)設(shè)置權(quán)限頁面的類名
uri: 'application_info_entry',
parameters: {
pushParams: 'com.example.myapplication' // 應(yīng)用的包名彻亲,也就是打開指定應(yīng)用的詳情頁面
}
}
context.startAbility(wantInfo).then(() => {
// ...
}).catch((err: BusinessError) => {
// ...
})
??目前只有華為手機(jī)使用了開源鴻蒙系統(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
是一個默認(rèn)創(chuàng)建Want
對象的子類。
/**
* 默認(rèn)獲取的want參數(shù)
*/
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 // 打開指定應(yīng)用的詳情頁面
}
}
return wantInfo
}
}
??對于華為手機(jī)啤月,我們就繼承DefaultSettingWant
煮仇,直接使用默認(rèn)創(chuàng)建的Want
對象。
/**
* 獲取華為手機(jī)上的want參數(shù)
*/
export class HuaWeiSettingWant extends DefaultSettingWant {
}
??如下代碼顽冶,先創(chuàng)建SettingWant
對象欺抗,通過deviceInfo.brand
判斷品牌,如果是華為手機(jī)强重,則創(chuàng)建HuaWeiSettingWant
绞呈。調(diào)用getWant
獲取到Want
對象,調(diào)用startAbility
跳轉(zhuǎn)到系統(tǒng)設(shè)置间景。
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中跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面
this.startAbilityFromUIExtensionAbility(want)
} else {
// 在UI或者UIAbility中跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面
this.startAbilityFromUIAbility(want)
}
}
/**
* 在UIExtensionAbility中跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面
*
* @param want
*/
private startAbilityFromUIExtensionAbility(want: Want) {
(this.origin.getContext() as common.UIExtensionContext).startAbility(want).then(() => {
// 跳轉(zhuǎn)成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
})
}
/**
* 在UI或者UIAbility中跳轉(zhuǎn)到系統(tǒng)設(shè)置頁面
*
* @param want
*/
private startAbilityFromUIAbility(want: Want) {
this.getContext().startAbility(want).then(() => {
// 跳轉(zhuǎn)成功
}).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
}
六佃声、源碼
??更多具體的代碼,請下載源碼倘要。