鸿蒙应用中下载文件完整逻辑(使用安全控件SaveButton或弹窗授权photoAccessHelper)

前面的话,最近将一款Android应用用Harmony ArkTS重写了一下,涉及到了文件上传和下载,先看了一下Harmony涉及到文件上传和下载的权限,因为业务场景是需要把网络图片下载到本地相册(下载),同时也需要本地相册的图片上传到服务器,这就需要ohos.permission.READ_IMAGEVIDEO和ohos.permission.WRITE_IMAGEVIDEO,但是这两个权限都是受限开放权限,搜了一下Harmony官方,9月份给出的条件是要走邮件申请,那就申请吧,回复的很快,被拒绝了,不过也给我了替代权限申请的方案,去官网看了看。

  • 保存图片到本地相册 可替代权限申请的方案是使用安全控件SaveButton弹窗授权photoAccessHelpe r的方式保存,developer.huawei.com/consumer/cn... 这两个解决方案都有一个好处,就是不用申请受限开放权限ohos.permission.WRITE_IMAGEVIDEO。

SaveButton的使用特点

就是和正常的权限请求效果差不多,第一次请求都有弹窗给用户,用户第一次拒绝后,再次点击SaveButton还是会有弹窗提示(和第一次一样的效果),如果用户同意后,再次点击SaveButton就没有弹窗提示,应该就是已经获取到权限了。

SaveButton的不足之处就是,在ArkUI效果很大的限制,没法自定义文字、外边距、内边距等等。 使用SaveButton可以先拿到相册的路径(首次点击SaveButton时有弹窗),然后再去下载。

弹窗授权photoAccessHelper的使用特点

和SaveButton正好相反,每次使用photoAccessHelper正如这个方案的名称一样(弹窗授权),不管上一次弹窗中用户同意还是禁止,都会再次弹窗。有个好处就是ArkUI效果上没有限制,都可以调用。

使用弹窗授权photoAccessHelper时,根据photoAccessHelper.showAssetsCreationDialog的参数,就需要先下载完成后,再有弹窗提示(每次都有),这种在用户体验上就不太友好,主要是流量和下载都完成了,才弹窗提示。

总结一下,就是下载图片到应用的沙箱中,只需要在src/main/module.json5中声明网络权限,不要任何其他权限声明和请求。但是把沙箱中的图片放到相册中就需要申请受限开放权限ohos.permission.WRITE_IMAGEVIDEO,这个时候就可以使用安全控件SaveButton或弹窗授权photoAccessHelper,就替代了权限的声明和请求。都是为了绕过WRITE_IMAGEVIDEO权限拿到相册的路径

对于文件下载的本地路径,只能是应用沙箱路径如下三个选择(这里说一下,需求是要把网络图片保存到本地相册相册啊,这里后面会使用Harmony SDK提供的PhotoAccessHelper中方法createAsset获取到相册的路径,虽然这个方法同样需要受限开放权限ohos.permission.WRITE_IMAGEVIDEO,但是上面的两个方案帮我们绕过或者申请了这个权限了) developer.huawei.com/consumer/cn...

ruby 复制代码
getContext().cacheDir //data/storage/el2/base/haps/entry/cache
getContext().tempDir //data/storage/el2/base/haps/entry/temp
getContext().filesDir //data/storage/el2/base/haps/entry/files

在模拟器中查看该路径

先总结一下下载的业务逻辑

使用SaveButton,先得到getContext().cacheDir路径和相册的路径,然后去下载图片到cacheDir下面,最后复制到相册中

使用弹窗授权photoAccessHelper,先得到getContext().cacheDir路径,然后去下载图片到cacheDir下面,最后使用photoAccessHelper.showAssetsCreationDialog得到相册的路径,把cacheDir中的图片复制到相册中。

上代码,工具类DownloadFile.ets

DownloadFile.ets 复制代码
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
import fs, { ReadOptions, WriteOptions } from '@ohos.file.fs';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { fileIo} from '@kit.CoreFileKit';

let TAG= "DownloadFile"
export class DownloadFile{

  /**
   * 下载图片到相册
   * @param context
   * @param imagePath 网络文件绝对路径
   * @param callback
   */
  public static downloadImageToPhoto(context:common.UIAbilityContext,imagePath:string,callback:DownloadCallback) {
    console.info(`${TAG} 下载图片到相册 downloadImagToPhoto imagePath: ${imagePath}`);
    if (!imagePath.startsWith("http://")&&!imagePath.startsWith("https://")) {
      callback.onError()
      return
    }
    //1.获取下载到的应用沙箱目录
    //https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-request-V5#downloadconfig
    let localCacheDir:string = DownloadFile.getLocalCacheDir()
    let imageName = `${Date.now().toString()}`
    let downloadToFilePath=localCacheDir + `/${imageName}.jpg`

    //2.1相册,需求是要求保存到相册目录
    let helper = photoAccessHelper.getPhotoAccessHelper(context);
    let options: photoAccessHelper.CreateOptions = {
      title: imageName
    }
    //2.2 文件下载到应用沙箱沙箱目录后 再保存到最终的相册路径地址 destPath
    helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg',options).then((destPath)=>{
      console.info(`${TAG} 相册 photoAccessHelper uri: ${destPath}`);
      if (destPath) {
        if (callback.onStart) {
          callback.onStart()
        }
        request.downloadFile(context, {
          url: imagePath,
          filePath: downloadToFilePath
        }).then((downloadTask: request.DownloadTask) => {
          downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
            console.info(`${TAG} download progress receivedSize=${receivedSize},totalSize=${totalSize}`);
            callback.inProgress(receivedSize,totalSize)
          })
          downloadTask.on('complete', async () => {
            console.info(`${TAG} 下载到应用沙箱目录完成 download complete`);
            //3.将下载到应用沙箱目录中的文件 写到相册中destPath
            let result  = await DownloadFile.saveToDestFilePath(downloadToFilePath,destPath)
            if (result) {
              callback.onSuccess(destPath)
            }else {
              callback.onError()
            }
          })
        }).catch((err: BusinessError) => {
          console.info(`${TAG} Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
          callback.onError()
        });
      }else {
        console.info(`${TAG} 获取相册路径地址 destPath失败`);
        callback.onError()
      }
    }).catch((err:Error)=>{
      console.info(`${TAG} Invoke downloadTask failed, err is ${JSON.stringify(err)}`);
      callback.onError()
    })
  }

  /**
   * 拷贝文件 从沙箱目录中
   * @param srcFilePath 沙箱目录中文件
   * @param destFilePath
   * @returns
   */
  static async saveToDestFilePath(srcFilePath:string,destFilePath:string){
    console.info(`${TAG} saveToDestFilePath srcFilePath= ${srcFilePath},destFilePath= ${destFilePath}`);
    try {
      let bufSize = 4096;
      let readSize = 0;
      let srcFile  = fs.openSync(srcFilePath, fs.OpenMode.READ_WRITE);
      console.info(`${TAG} saveToDestFilePath srcFile: ${srcFile.path} size=`+ DownloadFile.bytesToMB(fs.statSync(srcFile.path).size));
      let destFile = fs.openSync(destFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      console.info(`${TAG} saveToDestFilePath destFile: ${destFile.path}`);
      let buf = new ArrayBuffer(bufSize);
      let readOptions: ReadOptions = {
        offset: readSize,
        length: bufSize
      };

      let startTime = Date.now()
      let readLen = fs.readSync(srcFile.fd, buf, readOptions);
      console.info(`${TAG} readSync readLen: ${readLen}`);
      while (readLen > 0) {
        readSize += readLen;
        let writeOptions: WriteOptions = {
          length: readLen
        };
        fs.writeSync(destFile.fd, buf, writeOptions);
        readOptions.offset = readSize;
        readLen = fs.readSync(srcFile.fd, buf, readOptions);
      }
      //fs.unlinkSync(srcFile.path)
      fs.closeSync(srcFile);
      fs.closeSync(destFile);
      console.info(`${TAG} close successfully time=${Date.now()-startTime}`);
      return true
    } catch (err) {
      console.info(`${TAG} saveToDestFilePath failed with err:: ${err.code}, ${err.message}`);
      return false
    }
  }

  static showAssetsCreationDialog(context:common.UIAbilityContext,imagePath:string,callback:DownloadCallback){
    console.info(`${TAG} CreationDialog 下载图片到相册 downloadImagToPhoto imagePath: ${imagePath}`);
    let localCacheDir:string = DownloadFile.getLocalCacheDir()
    let imageName = `${Date.now().toString()}`
    let downloadToFilePath=localCacheDir + `/${imageName}.jpg`
    request.downloadFile(context, {
      url: imagePath,
      filePath: downloadToFilePath
    }).then((downloadTask: request.DownloadTask) => {
      downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
        console.info(`${TAG} CreationDialog download progress receivedSize=${receivedSize},totalSize=${totalSize}`);
        callback.inProgress(receivedSize,totalSize)
      })
      downloadTask.on('complete', async () => {
        console.info(`${TAG} CreationDialog 下载到应用沙箱目录完成 download complete`);
        //3.将下载到应用沙箱目录中的文件 写到相册中destPath
        try {
          // 指定待保存到媒体库的位于应用沙箱的图片uri
          let srcFileUris: Array<string> = [
            downloadToFilePath
          ];
          // 指定待保存照片的创建选项,包括文件后缀和照片类型,标题和照片子类型可选
          let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
            {
              title: imageName, // 可选
              fileNameExtension: 'jpg',
              photoType: photoAccessHelper.PhotoType.IMAGE,
              subtype: photoAccessHelper.PhotoSubtype.DEFAULT, // 可选
            }
          ];
          // 基于弹窗授权的方式获取媒体库的目标uri
          let helper = photoAccessHelper.getPhotoAccessHelper(context);
          let desFileUris: Array<string> = await helper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
          let destPath = desFileUris[0]
          //3.将下载到应用沙箱目录中的文件 写到相册中destPath
          let result  = await DownloadFile.saveToDestFilePath(downloadToFilePath,destPath)
          if (result) {
            callback.onSuccess(destPath)
          }else {
            callback.onError()
          }
        } catch (err) {
          console.info(`${TAG} failed to create asset by dialog successfully errCode is: ${err.code}, ${err.message}`);
          callback.onError()
        }
      })
    }).catch((err: BusinessError) => {
      console.info(`${TAG} Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
      callback.onError()
    });
  }

  /**
   * 获取应用沙箱存储目录 统一一下,方便APP设置页面删除
   * @returns
   */
  static getLocalCacheDir():string {
    let cache_dir_name = "appfiles";
    let cache_dir = getContext().cacheDir+"/"+cache_dir_name;
    console.info(`createCacheDir cache_dir: ${cache_dir}`);
    let existAppCacheDir = fileIo.accessSync(cache_dir,fileIo.AccessModeType.EXIST)
    if (!existAppCacheDir) {
      fileIo.mkdirSync(cache_dir, true);
    }
    return cache_dir
  }

  public static bytesToMB(bytes:number){
    if(bytes>=1000){
      bytes=bytes/1000.0;
      if(bytes>=1000){
        bytes=bytes/1000.0;
        if(bytes>=1000){
          bytes=bytes/1000.0;
          return bytes+" GB";
        }else{
          return bytes+" MB";
        }
      }else{
        return bytes+" KB";
      }
    }else{
      return bytes+" Byte";
    }
  }
}

/**
 * 下载文件的返回结果
 */
interface DownloadCallback {
  onError: () => void;
  onStart?: () => void;
  inProgress: (progress: number, total: number) => void;
  onSuccess: (file: string) => void;
}

调用处代码逻辑

ets 复制代码
SaveButton(this.saveButtonOptions)
        .width(100)
        .height(50)
        .onClick(async (event, result: SaveButtonOnClickResult) => {
          if (result == SaveButtonOnClickResult.SUCCESS) {
            DownloadFile.downloadImageToPhoto(getContext(this) as common.UIAbilityContext, this.fileUri, {
              onError: () => {
              },
              inProgress: (progress, total) => {
              },
              onSuccess: (file) => {
                this.fileDownLocalUri = file
              }
            })
          }
        })
      Text("保存图片")
        .backgroundColor(Color.Black)
        .fontSize(18)
        .textAlign(TextAlign.Center)
        .fontColor(Color.White)
        .width("90")
        .height(45)
        .margin({ top: 40 })
        .fontWeight(FontWeight.Bold)
        .onClick(()=>{
          DownloadFile.showAssetsCreationDialog(getContext(this) as common.UIAbilityContext, this.fileUri, {
            onError: () => {
            },
            inProgress: (progress, total) => {
            },
            onSuccess: (file) => {
              this.fileDownLocalUri = file
            }
          })
        })

贴一下效果图吧

相关推荐
觉醒法师3 小时前
HarmonyOS开发 - 本地持久化之实现LocalStorage支持多实例
前端·javascript·华为·typescript·harmonyos
东林知识库6 小时前
2024年10月HarmonyOS应用开发者基础认证全新题库
学习·华为·harmonyos
ChinaDragonDreamer6 小时前
HarmonyOS:@Watch装饰器:状态变量更改通知
开发语言·harmonyos·鸿蒙
Lei活在当下11 小时前
【初探鸿蒙01】鸿蒙生态用开发白皮书V3.0解读
harmonyos
SameX12 小时前
实现多子类型输入法:如何在 HarmonyOS中加载不同的输入模式
harmonyos
SuperHeroWu714 小时前
【HarmonyOS】判断应用是否已安装
华为·微信·harmonyos·qq·微博·应用是否安装·canopenlink
SoraLuna14 小时前
「Mac畅玩鸿蒙与硬件7」鸿蒙开发环境配置篇7 - 使用命令行工具和本地模拟器管理项目
macos·华为·harmonyos
SuperHeroWu71 天前
【HarmonyOS】鸿蒙应用OAID广告标识ID设置设备唯一标识
华为·harmonyos·oaid·广告标识·跟踪权限
雪芽蓝域zzs1 天前
HarmonyOS 组件样式@Style 、 @Extend、自定义扩展(AttributeModifier、AttributeUpdater)
深度学习·harmonyos
佛山芃程科技1 天前
鸿蒙NEXT+Flutter开发7-存储应用设置项
harmonyos