问题现象

当申请ohos.permission.WRITE_IMAGEVIDEO权限被拒时,如何保存图片和视频到相册。

背景知识

ohos.permission.WRITE_IMAGEVIDEO是受限开放的权限,应用可以通过安全控件或授权弹窗的方式,将指定的媒体资源保存到相册中。

  • 安全控件(SaveButton):安全控件的保存控件,用户通过点击该保存按钮,可以临时获取存储权限,而不需要权限弹框授权确认。使用此控件需要UI样式合法,不合法会导致授权失败,可参考安全控件样式的约束与限制。
  • 授权弹窗(showAssetsCreationDialog):通过调用接口拉起保存确认弹窗,基于弹窗授权的方式获取的目标媒体文件uri。用户同意保存后,返回已创建并授予保存权限的uri列表,该列表永久生效,应用可使用该uri写入图片/视频。如果用户拒绝保存,将返回空列表。

解决方案

  • 方案一:安全控件SaveButton可以临时获取存储权限,而不需要权限弹框授权确认,最终把图片保存到相册。
    import photoAccessHelper from '@ohos.file.photoAccessHelper';
    import fs from '@ohos.file.fs';
    import { common } from '@kit.AbilityKit';
    import { UIContext } from '@kit.ArkUI';
    
    @Entry
    @Component
    struct Index4 {
      @State message: string = 'Hello World'
      uiContext: UIContext = this.getUIContext()
    
      build() {
        Row() {
          Column() {
            Image($r('app.media.icon'))
              .height(300)
              .width(300)
            SaveButton().onClick(async (_event: ClickEvent, result: SaveButtonOnClickResult) => {
              if (result === SaveButtonOnClickResult.SUCCESS) {
                try {
                  let context: Context = this.uiContext.getHostContext() as common.UIAbilityContext;
                  let helper = photoAccessHelper.getPhotoAccessHelper(context);
                  // onClick触发后一分钟内通过createAsset接口创建图片文件,一分钟后createAsset权限收回
                  let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
                  // 使用uri打开文件,可以持续写入内容,写入过程不受时间限制
                  let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
                  try {
                    context.resourceManager.getMediaContent($r('app.media.startIcon').id, 0)
                      .then(async value => {
                        let media = value.buffer;
                        // 写到媒体库文件中
                        await fs.write(file.fd, media);
                        await fs.close(file.fd);
                        this.uiContext.showAlertDialog({ message: '已保存至相册!' });
                      });
                  } catch (err) {
                    console.error(`error is ${err}`);
                  }
                } catch (error) {
                  console.error(`error is ${error}`);
                }
              } else {
                this.uiContext.showAlertDialog({ message: '设置权限失败' });
              }
            })
          }
          .width('100%')
        }
        .height('100%')
      }
    }

    方案二:调用showAssetsCreationDialog弹窗授权保存图片到相册。

    使用弹窗授权保存图片到相册,首先获取需要保存到媒体库的位于应用沙箱的图片/视频uri,然后调用showAssetsCreationDialog接口弹窗授权,通过fs.copyFileSync将图片保存到相册。

    import { fileIo as fs, fileUri } from '@kit.CoreFileKit'
    import { common } from '@kit.AbilityKit';
    import { photoAccessHelper } from '@kit.MediaLibraryKit';
    import { camera, cameraPicker as picker } from '@kit.CameraKit';
    
    @Entry
    @Component
    struct Index {
      @State message: string = 'SaveButton';
      uiContext: UIContext = this.getUIContext()
    
      async saveFile() {
        let mContext: Context = this.uiContext.getHostContext() as common.UIAbilityContext;
        let types: Array<picker.PickerMediaType> = [picker.PickerMediaType.PHOTO];
        let pickerProfile: picker.PickerProfile = {
          cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
          videoDuration: 15
        };
        let pickerResult: picker.PickerResult = await picker.pick(mContext,
          types, pickerProfile);
        if (pickerResult.resultCode === 0) {
          // 成功
          let finalUri = pickerResult.resultUri;
          // 保存图片到缓存目录
          let dirpath = (this.uiContext.getHostContext() as common.UIAbilityContext).tempDir + '/cameTem.jpg';
          let dirUri = fileUri.getUriFromPath(dirpath);
          let finalFile = fs.openSync(finalUri, fs.OpenMode.READ_ONLY)
          let dirFile = fs.openSync(dirpath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
          fs.copyFileSync(finalFile.fd, dirFile.fd)
          fs.closeSync(finalFile);
          fs.closeSync(dirFile);
          console.info('ImagePicker', 'Succeeded in copying. ');
          // 删除相册图片(需要申请ohos.permission.WRITE_IMAGEVIDEO权限)
          try {
            await photoAccessHelper.MediaAssetChangeRequest.deleteAssets(this.uiContext.getHostContext(),
              [pickerResult.resultUri])
            finalUri = dirUri;
          } catch (err) {
            console.error('ImagePicker', `deleteAssetsDemo failed with error: ${err.code}, ${err.message}`);
          }
          console.info('ShowAssetsCreationDialogDemo.');
    
          // 获取需要保存到媒体库的位于应用沙箱的图片/视频uri
          try {
            let srcFileUris: Array<string> = [dirUri];
            let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [{
              fileNameExtension: 'jpg',
              photoType: photoAccessHelper.PhotoType.IMAGE,
            }];
            let context = this.uiContext.getHostContext() as common.UIAbilityContext;
            let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
            let desFileUris: Array<string> =
              await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
            console.info('showAssetsCreationDialog success, data is ' + desFileUris);
            if (desFileUris.length > 0) {
              try {
                let srcFile = fs.openSync(srcFileUris[0], fs.OpenMode.READ_ONLY)
                let desFile = fs.openSync(desFileUris[0], fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
                fs.copyFileSync(srcFile.fd, desFile.fd)
                fs.closeSync(srcFile);
                fs.closeSync(desFile);
              } catch (e) {
                console.error(e)
              }
            }
          } catch (err) {
            console.error('showAssetsCreationDialog failed, errCode is ' + err.code + ', errMsg is ' + err.message);
          }
        }
      }
    
      build() {
        RelativeContainer() {
          Text(this.message)
            .id('SaveButton')
            .fontSize(50)
            .fontWeight(FontWeight.Bold)
            .alignRules({
              center: { anchor: '__container__', align: VerticalAlign.Center },
              middle: { anchor: '__container__', align: HorizontalAlign.Center }
            })
            .onClick(() => {
              this.saveFile();
            })
        }
        .height('100%')
        .width('100%')
      }
    }

    说明

    调用showAssetsCreationDialog接口时请确保module.json5文件中的abilities标签中配置了label和icon项,因为showAssetsCreationDialog依赖module.json5文件中的abilities标签中label和icon项,所以module.json5文件中的abilities标签必须存在label和icon项。

    module.json5的配置文件如下:

    // module.json5的部分代码
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:app_icon",
        // 以product的配置为主
        "label": "$string:app_name_test",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home",
              "wxentity.action.open"
            ]
          }
        ]
      }
    ]

Logo

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

更多推荐