前面的话,最近将一款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
}
})
})
贴一下效果图吧