最佳实践 - 鸿蒙全场景开发:桃夭权限框架助力媒体 / 定位场景化解决方案

前言

本文以 "全场景开发" 为核心视角,从桃夭框架的核心模块解析入手,深入拆解模块配置文件的权限声明逻辑、全局开关工具类的超级隐私模式适配机制、权限弹窗组件的用户体验优化思路,结合相机、麦克风、位置媒体与定位高频场景案例,呈现鸿蒙桃夭框架如何实现 "权限开发 + 用户体验 + 设备兼容" 。

鸿蒙权限框架:桃夭核心模块解析

模块配置文件:权限声明入口

1、模块配置文件 module.json5 鸿蒙应用权限声明,requestPermissions 数组,明确应用所需的各类权限网络、蓝牙、相机等,并通过 reason 说明权限使用缘由、usedScene 定义使用场景

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      },
      {
        "name": "ohos.permission.ACCESS_BLUETOOTH",
        "reason": "$string:access_bluetooth",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.MEDIA_LOCATION",
        "reason": "$string:media_location",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APP_TRACKING_CONSENT",
        "reason": "$string:app_tracking_consent",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.ACTIVITY_MOTION",
        "reason": "$string:activity_motion",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC",
        "reason": "$string:distributed_datasync",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.LOCATION_IN_BACKGROUND",
        "reason": "$string:location_in_background",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:approximately_location",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:microphone",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_CALENDAR",
        "reason": "$string:read_calendar",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_CALENDAR",
        "reason": "$string:write_calendar",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_HEALTH_DATA",
        "reason": "$string:read_health_data",
        "usedScene": {
          "abilities": [

          ],
          "when": "inuse"
        }
      },
    ],
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ]
  }
}

全局开关工具类:超级隐私模式把关人

2、GlobalSwitchUtil.ets 应用获取相机、麦克风、位置这三类敏感权限后,检测对应硬件功能是否因超级隐私模式被禁用,若禁用则引导用户开启,保障权限申请成功后功能能正常使用

img

img

img

import { Permissions } from "@kit.AbilityKit";
import { TaoYao } from "@shijing/taoyao";
import { promptAction } from '@kit.ArkUI';
export class GlobalSwitchUtil {

  /**
   *
   * 系统提供了超级隐私模式,在系统设置打开超级隐私模式后,相机、麦克风、位置将不可用。
   * 在获取相机权限、麦克风权限和位置权限后,如果超级隐私模式开启,需要引导用户关闭超级隐私模式。
   *
   * @param context
   * @param permissions
   */
  static requestGlobalSwitch(context: Context, permissions: Array<Permissions>) {
    const locationPermission = "ohos.permission.LOCATION" as Permissions
    const approximatelyPermission = "ohos.permission.APPROXIMATELY_LOCATION" as Permissions
    const backgroundPermission = "ohos.permission.LOCATION_IN_BACKGROUND" as Permissions
    const microphonePermission = "ohos.permission.MICROPHONE" as Permissions
    const cameraPermission = "ohos.permission.CAMERA" as Permissions
    if (permissions.indexOf(locationPermission) >= 0 || permissions.indexOf(approximatelyPermission) >= 0
      || permissions.indexOf(backgroundPermission) >= 0) {
      GlobalSwitchUtil.requestLocationGlobalSwitch(context)
    } else if (permissions.indexOf(microphonePermission) >= 0) {
      GlobalSwitchUtil.requestMicrophoneGlobalSwitch(context)
    } else if (permissions.indexOf(cameraPermission) >= 0) {
      GlobalSwitchUtil.requestCameraGlobalSwitch(context)
    }
  }

  static requestLocationGlobalSwitch(context: Context) {
    if (!TaoYao.isLocationEnabled()) {
      // 开启了超级隐私模式或者未打开定位开关,拉起全局开关弹窗引导用户关闭超级隐私模式或者打开定位开关
      TaoYao.requestLocationGlobalSwitch(context).then((isOpen => {
        if (isOpen) {
          GlobalSwitchUtil.toast("定位开关已打开")
        } else {
          GlobalSwitchUtil.toast("定位开关已关闭")
        }
      }))
    } else {
      GlobalSwitchUtil.toast("定位开关已打开")
    }
  }

  static requestMicrophoneGlobalSwitch(context: Context) {
    TaoYao.isMicrophoneMute().then(isMute => {
      if (isMute) {
        TaoYao.requestMicrophoneGlobalSwitch(context).then((isOpen => {
          if (isOpen) {
            GlobalSwitchUtil.toast("麦克风已开启")
          } else {
            GlobalSwitchUtil.toast("麦克风已静音")
          }
        }))
      } else {
        GlobalSwitchUtil.toast("麦克风开启")
      }
    })
  }

  static requestCameraGlobalSwitch(context: Context) {
    if (TaoYao.isCameraMuted(context)) {
      TaoYao.requestCameraGlobalSwitch(context).then(isOpen => {
        if (isOpen) {
          GlobalSwitchUtil.toast("相机已开启")
        } else {
          GlobalSwitchUtil.toast("相机关闭")
        }
      })
    } else {
      GlobalSwitchUtil.toast("相机已开启")
    }
  }

  private static toast(text: string) {
    promptAction.showToast({ message: text })
  }
}

权限弹窗组件:用户体验桥梁

3、PermissionDialog.ets 自定义弹窗组件,核心作用是在权限申请被拒绝后,向用户展示权限用途说明并并引导用户前往系统设置授权,是权限申请流程中提升用户体验的关键环节

img

img

img

import { ArrayUtils, TaoYao } from '@shijing/taoyao/Index'
import { common, Permissions } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { promptAction } from '@kit.ArkUI'
import { GlobalSwitchUtil } from '../utils/GlobalSwitchUtil'

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

  private title: string = '权限设置'
  private subtitle?: Resource | string
  private left: string = '取消'
  private right: string = '去设置'
  private permissions = new Array<Permissions>()
  private message = ''
  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')
    } else {
      this.subtitle = this.message
    }
  }

  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()
            if (ArrayUtils.isEmpty(this.permissions)) {
              // 通知权限只能跳转到系统设置页面,系统权限设置弹窗不支持通知权限
              TaoYao.goToSettingPage(this.context)
            } else {
              this.showSystemPermissionDialog()
            }

          })
      }
      .margin({top: 30})
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .width('100%')
    .borderRadius(20)
    .backgroundColor('#ffffff')
    .padding({left: 24, right: 24, top: 30, bottom: 28})
  }

  private showSystemPermissionDialog() {
    TaoYao
      .showSystemPermissionDialog(this.context, this.permissions)
      .onGranted(() => {
        // 直接拉起系统权限设置弹窗后,用户授权
        this.toast('直接拉起系统权限设置弹窗后,有权限了')
        /*
         * 系统提供了超级隐私模式,在系统设置打开超级隐私模式后,相机、麦克风、位置将不可用。
         * 在获取相机权限、麦克风权限、位置权限后,如果开启了超级隐私模式,需要引导用户关闭超级隐私模式。
         */
        GlobalSwitchUtil.requestGlobalSwitch(this.context, this.permissions)
      })
      .onDenied(() => {
        // 直接拉起系统权限设置弹窗后,用户未授权
        this.toast('直接拉起系统权限设置弹窗后,没权限')
      })
      .onFailed(() => {
        /*
         * 拉起系统设置弹窗失败,无法直接判断用户是否在系统设置页面授权,可以在onPageShow方法里面判断是否有权限
         * 目前发现ohos.permission.READ_HEALTH_DATA健康数据权限无法直接拉起系统设置弹窗,只能跳转到系统设置页面
         */
      })
  }
  private toast(text: string = "有权限了") {
    promptAction.showToast({ message: text })
  }
}

主入口页面:权限与功能集成中枢

4、 Index.ets 应用主入口页面,通过数据源驱动权限申请弹窗(dataSource+ PermissionDialog),集成摄像头/文档/音视频等系统文件选择器(cameraPicker/documentPicker),并与上下文(UIAbilityContext)联动实现系统能力调用

img

import { hilog } from '@kit.PerformanceAnalysisKit';
import { common, Permissions } from '@kit.AbilityKit';
import {AudioBuilder,CameraBuilder, CameraSelector, ContactBuilder,DocumentBuilder,MediaBuilder,MediaMimeType,TaoYao,UseCase } from '@shijing/taoyao/Index';
import { PermissionDetail } from '../model/PermissionDetail';
import { PermissionViewModel } from '../viewmodel/PermissionViewModel';
import DataSource from '../viewmodel/DataSource';
import { PermissionDialog } from '../view/PermissionDialog';
import { promptAction } from '@kit.ArkUI';
import { GlobalSwitchUtil } from '../utils/GlobalSwitchUtil';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World'
  private dataSource = new DataSource<PermissionDetail>(PermissionViewModel.getReqPermissionDetails())
  private context = getContext(this) as common.UIAbilityContext
  private dialogController = new CustomDialogController({
    builder: PermissionDialog(),
  })

  private toast(text: string = "有权限了") {
    promptAction.showToast({ message: text })
  }

  private showPermissionDialog(permission: Array<Permissions>) {
    this.dialogController = new CustomDialogController({
      builder: PermissionDialog({permissions: permission}),
    })
    this.dialogController.open()
  }

  private notification(reason: string) {
    TaoYao.with(this.context)
      .notification()
      .permission()
      .onGranted(() => {
        this.toast()
      })
      .onDenied(() => {
        this.dialogController = new CustomDialogController({
          builder: PermissionDialog({message: reason}),
        })
        this.dialogController.open()
      })
      .request()
  }

  build() {
    Column() {
      Grid() {
        LazyForEach(this.dataSource, (permissionDetail: PermissionDetail) => {
          GridItem() {
            Button(permissionDetail.chineseName)
              .width('100%')
              .height(50)
              .fontSize(16)
              .onClick(() => {
                if (permissionDetail.name === "shijing.taoyao.NOTIFICATION") {
                  // 申请通知权限
                  this.notification(permissionDetail.reason)
                  TaoYao.isNotificationEnabled().then((enable: boolean) => {
                    console.log(`yunfei是否有通知权限:${enable}`)
                  })
                  TaoYao.isDistributedEnabled().then((enable: boolean) => {
                    console.log(`yunfei是否支持分布式通知:${enable}`)
                  })
                  return
                }
                if (permissionDetail.name === "contact_picker") {
                  // 联系人选择器,通过联系人选择器获取联系人,不需要申请通讯录权限
                  this.contactPicker()
                  return
                }
                if (permissionDetail.name === "camera_picker") {
                  // 相机选择器,拉起系统相机不需要申请相机权限
                  this.cameraPicker()
                  return
                }
                if (permissionDetail.name === "media_picker") {
                  // 图片、视频选择器,拉起系统图库不需要申请权限
                  this.mediaPicker()
                  return
                }
                if (permissionDetail.name === "document_picker") {
                  // 文档选择器,拉起文档选择器不需要申请权限
                  this.documentPicker()
                  return
                }
                if (permissionDetail.name === "audio_picker") {
                  // 音频选择器,拉起音频选择器不需要申请权限
                  this.audioPicker()
                  return
                }
                const name = permissionDetail.name as Permissions
                const permissions: Array<Permissions> = [name]
                TaoYao.with(this.context)
                  .runtime()
                  .permission(permissions)
                  .onGranted(permissions => {
                    // 权限申请成功
                    this.toast()
                    /*
                     * 系统提供了超级隐私模式,在系统设置打开超级隐私模式后,相机、麦克风、位置将不可用。
                     * 在获取相机权限、麦克风权限、位置权限后,如果开启了超级隐私模式,需要引导用户关闭超级隐私模式。
                     */
                    GlobalSwitchUtil.requestGlobalSwitch(this.context, permissions)
                  })
                  .onDenied(permissions => {
                    /*
                     * 1、由于安全隐私要求,应用不能通过系统弹窗的形式被授予后台位置权限,应用如果需要使用后台位置权限,会先申请模糊位置权限和精确位置权限,
                     * 然后弹窗提示用户到系统设置中打开相应的权限,用户在设置界面中的选择“始终允许”应用访问位置信息权限,应用就获取了后台位置权限。
                     * 2、权限申请失败,当用户拒绝授权时,将无法再次拉起系统的权限弹窗,此时只能弹窗提示用户到系统设置中打开相应的权限
                     */
                    this.showPermissionDialog(permissions)
                  })
                  // 开始申请权限
                  .request()
              })
          }
        }, (permissionDetail: PermissionDetail) => permissionDetail.name)
      }
      .columnsTemplate('1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  /**
   * 联系人选择器,通过联系人选择器获取联系人,不需要申请通讯录权限。
   * 如果申请通讯录权限,应用就能获取到所有的联系人,联系人选择器只允许应用获取到用户选择的联系人
   */
  contactPicker() {
    TaoYao.with(this.context)
      .contact()
      .onSuccess((data) => {
        // 联系人列表
        if (data.length > 0) {
          // 联系人名称
          console.log("yunfei", data[0].name?.fullName)
          // 联系人号码
          console.log("yunfei", data[0]?.phoneNumbers?.[0]?.phoneNumber)
        }
      })
      .onError((err) => {
        console.log(err.message)
      })
      .selectContacts(new ContactBuilder()
        // 可以选择多个联系人
        .setMultiSelect(true))
  }

  /**
   * 相机选择器,拉起系统相机不需要申请相机权限
   */
  cameraPicker() {
    TaoYao.with(this.context)
      .camera()
      .onSuccess((uri) => {
        // 拍照或者录像的文件沙箱路径
        console.log(uri)
      })
      .onError((err) => {
        console.log(err.stack)
      })
      .openSystemCamera(new CameraBuilder()
        // 后置相机,默认使用后摄
        .setCameraSelector(CameraSelector.CAMERA_POSITION_BACK)
        // 可以只要拍照,只要录像,默认拍照和录像都有
        .setUseCase([UseCase.PHOTO, UseCase.VIDEO])
        // 文件保存路径,可以不设置
        //.setSaveUri("")
        // 录制视频最大时长,可以不设置
        //.setVideoMaxDuration()
      )
  }

  /**
   * 图片、视频选择器,拉起系统图库不需要申请存储权限,只能获取选中的图片、视频
   */
  mediaPicker() {
    TaoYao.with(this.context)
      .media()
      .onSuccess((uris) => {
        uris.forEach((uri) => {
          console.log("yunfei", uri)
        })
      })
      .onError((err) => {
        console.log(err.stack)
      })
      .select(new MediaBuilder()
        // 选择媒体文件的最大数目
        .setMaxSelectNumber(10)
        // 可选择的媒体文件类型,图片类型、视频类型、图片和视频类型、动态照片类型
        .setMediaMineType(MediaMimeType.IMAGE_VIDEO_TYPE)
      )
  }

  /**
   * 文档选择器,拉起文档选择器不需要申请权限,只能获取选中的文档
   */
  documentPicker() {
    TaoYao.with(this.context)
      .document()
      .onSuccess((uris) => {
        uris.forEach((uri) => {
          console.log("yunfei", uri)
        })
      })
      .onError((err) => {
        console.log(err.stack)
      })
      .select(new DocumentBuilder()
        // 选择媒体文件的最大数目
        .setMaxSelectNumber(10)
        // 指定选择的文件或者目录路径(可选)
        //.setDefaultFilePathUri("")
        // 选择是否对指定文件或目录授权,true为授权,当为true时,defaultFilePathUri为必选参数,拉起文管授权界面;false为非授权,拉起常规文管界面(可选)
        //.setAuthMode(false)
        // 选择文件的后缀类型['后缀类型描述|后缀类型'](可选) 若选择项存在多个后缀名,则每一个后缀名之间用英文逗号进行分隔(可选),后缀类型名不能超过100,选择所有文件:'所有文件(*.*)|.*';
        // 例如:['图片(.png, .jpg)|.png,.jpg', '文档|.txt', '视频|.mp4', '.pdf']
        .setFileSuffixFilters(['文档|.docx'])
      )
  }

  /**
   * 音频选择器,拉起音频选择器不需要申请权限,只能获取选中的文档
   */
  audioPicker() {
    TaoYao.with(this.context)
      .audio()
      .onSuccess((uris) => {
        uris.forEach((uri) => {
          console.log("yunfei", uri)
        })
      })
      .onError((err) => {
        console.log(err.stack)
      })
      // 目前音频选择器不支持参数配置,默认可以选择所有类型的用户文件。
      .select(new AudioBuilder())
  }
}

相机权限:系统相机能力的 "轻量调用"

用户点击 "相机权限" 相关触发逻辑对应页面中相机权限或相机选择器按钮交互,会调用 cameraPicker 方法,借助桃夭框架无需直接申请相机权限的情况下,拉起系统相机功能,通过 CameraBuilder 可配置相机为后置 CAMERA_POSITION_BACK、设置使用场景拍照和录像,UseCase.PHOTO与 UseCase.VIDEO,拍照或录像完成后,成功回调 onSuccess 中能获取到拍摄文件的沙箱路径 uri,便于后续对拍摄内容进行处理,过程中出现错误,错误回调 onError 会输出错误堆栈信息

// 相机选择器方法,拉起系统相机不需要申请相机权限相关调用逻辑
cameraPicker() {
  TaoYao.with(this.context)
    .camera()
    .onSuccess((uri) => {
      // 拍照或者录像的文件沙箱路径
      console.log(uri)
    })
    .onError((err) => {
      console.log(err.stack)
    })
    .openSystemCamera(new CameraBuilder()
      // 后置相机,默认使用后摄
      .setCameraSelector(CameraSelector.CAMERA_POSITION_BACK)
      // 可以只要拍照,只要录像,默认拍照和录像都有
      .setUseCase([UseCase.PHOTO, UseCase.VIDEO])
    )
}

img

img

img

麦克风权限:隐私与功能的 "平衡术"

涉及麦克风权限时,通过 TaoYao 的 runtime().permission(permissions).request() 发起申请,若权限授予 onGranted,会调用GlobalSwitchUtil.requestGlobalSwitch 检测系统超级隐私模式,因为该模式开启后麦克风会不可用,若检测到需引导用户关闭。同时,requestMicrophoneGlobalSwitch 方法会专门检测麦克风是否因超级隐私模式被静音,若静音则引导用户开启,保障麦克风权限获取后功能能正常使用;若权限被拒绝 onDenied,则展示权限弹窗引导用户去系统设置授权。

// 权限申请核心逻辑中与麦克风权限相关部分,以及超级隐私模式检测
const name = permissionDetail.name as Permissions
const permissions: Array<Permissions> = [name]
TaoYao.with(this.context)
  .runtime()
  .permission(permissions)
  .onGranted(permissions => {
    this.toast()
    /*
     * 系统提供了超级隐私模式,在系统设置打开超级隐私模式后,相机、麦克风、位置将不可用。
     * 在获取相机权限、麦克风权限、位置权限后,如果开启了超级隐私模式,需要引导用户关闭超级隐私模式。
     */
    GlobalSwitchUtil.requestGlobalSwitch(this.context, permissions)
  })
  .onDenied(permissions => {
    this.showPermissionDialog(permissions)
  })
  .request()

// 麦克风超级隐私模式检测相关方法
static requestMicrophoneGlobalSwitch(context: Context) {
  TaoYao.isMicrophoneMute().then(isMute => {
    if (isMute) {
      TaoYao.requestMicrophoneGlobalSwitch(context).then((isOpen => {
        if (isOpen) {
          GlobalSwitchUtil.toast("麦克风已开启")
        } else {
          GlobalSwitchUtil.toast("麦克风已静音")
        }
      }))
    } else {
      GlobalSwitchUtil.toast("麦克风开启")
    }
  })
}

img

img

img

位置权限:超级隐私模式的 "特殊适配"

位置权限包括模糊位置、后台定位等申请时,同样通过 TaoYao 的权限申请流程,权限授予后,GlobalSwitchUtil.requestGlobalSwitch 会检测超级隐私模式,因为该模式下位置功能会被禁用,requestLocationGlobalSwitch方法会检查定位功能是否开启,若因超级隐私模式或手动关闭导致未开启,会拉起系统弹窗引导用户打开定位开关;若已开启则直接提示,确保位置权限获取后能正常使用定位相关功能,若权限被拒则引导用户去系统设置授权

// 权限申请核心逻辑中与位置权限相关部分,以及超级隐私模式检测
const name = permissionDetail.name as Permissions
const permissions: Array<Permissions> = [name]
TaoYao.with(this.context)
  .runtime()
  .permission(permissions)
  .onGranted(permissions => {
    this.toast()
    GlobalSwitchUtil.requestGlobalSwitch(this.context, permissions)
  })
  .onDenied(permissions => {
    this.showPermissionDialog(permissions)
  })
  .request()

// 位置超级隐私模式检测相关方法
static requestLocationGlobalSwitch(context: Context) {
  if (!TaoYao.isLocationEnabled()) {
    TaoYao.requestLocationGlobalSwitch(context).then((isOpen => {
      if (isOpen) {
        GlobalSwitchUtil.toast("定位开关已打开")
      } else {
        GlobalSwitchUtil.toast("定位开关已关闭")
      }
    }))
  } else {
    GlobalSwitchUtil.toast("定位开关已打开")
  }
}

img

img

img

项目总结:桃夭权限框架的实践价值与优势

桃夭权限框架最实在的就是省了不少事,不用再为手机、平板等设备的权限差异反复改代码,也不用重复写权限申请、异常处理的逻辑,框架里的配置文件、工具类、弹窗组件直接就能用,拿来就适配多设备场景,像做媒体拍摄、定位这类功能时,能自动处理超级隐私模式的坑,比如相机、定位开了权限却用不了的情况,会主动引导用户解决,连用户弹窗的话术都配好了,不用再纠结怎么跟用户解释权限用途

全场景多设备无缝适配:统一封装跨终端权限逻辑,无需针对手机、平板等设备单独适配,轻松支撑全场景应用权限管理需求

场景化能力直达:深度整合媒体、定位等核心场景权限处理,从申请流程到超级隐私模式兼容一键搞定,快速落地业务功能

稳定性与扩展性双保障:基于鸿蒙原生能力构建,适配系统迭代与政策变更,同时模块化设计支持灵活扩展,降低长期维护成本

总结

img

鸿蒙桃夭权限框架能够帮助开发者解决鸿蒙权限开发核心痛点:多设备不用重复适配,权限申请、异常处理有现成模块,媒体、定位场景还能自动兼容超级隐私模式,连用户引导弹窗都配好了,省出时间能多琢磨业务逻辑。

👉想解锁更多干货?点击立即加入鸿蒙知识共建交流群https://work.weixin.qq.com/gm/afdd8c7246e72c0e94abdbd21bc9c5c1

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐