鸿蒙应用中下载文件完整逻辑(使用安全控件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
            }
          })
        })

贴一下效果图吧

相关推荐
SameX3 小时前
HarmonyOS Next 安全生态构建与展望
前端·harmonyos
SameX3 小时前
HarmonyOS Next 打造智能家居安全系统实战
harmonyos
Random_index11 小时前
#Uniapp篇:支持纯血鸿蒙&发布&适配&UIUI
uni-app·harmonyos
鸿蒙自习室14 小时前
鸿蒙多线程开发——线程间数据通信对象02
ui·harmonyos·鸿蒙
SuperHeroWu716 小时前
【HarmonyOS】鸿蒙应用接入微博分享
华为·harmonyos·鸿蒙·微博·微博分享·微博sdk集成·sdk集成
zhangjr057519 小时前
【HarmonyOS Next】鸿蒙实用装饰器一览(一)
前端·harmonyos·arkts
诗歌难吟4641 天前
初识ArkUI
harmonyos
SameX1 天前
HarmonyOS Next 设备安全特性深度剖析学习
harmonyos
郭梧悠1 天前
HarmonyOS(57) UI性能优化
ui·性能优化·harmonyos
郝晨妤2 天前
鸿蒙原生应用开发元服务 元服务是什么?和App的关系?(保姆级步骤)
android·ios·华为od·华为·华为云·harmonyos·鸿蒙