1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { logger } from '../utils/Logger'; 17import { BusinessError } from '@ohos.base'; 18import common from '@ohos.app.ability.common'; // 导入依赖资源context模块 19import request from '@ohos.request'; // 导入上传下载模块 20import { getFileNameFromUrl } from '../utils/formatTime'; 21import { downloadFilesData } from '../model/dataType'; 22 23const TAG = 'Multiple_Files_Download'; 24const BYTE_CONVERSION: number = 1024; // 字节转换 25const INIT_PROGRESS: number = 0; // 进度条初始值 26const context = getContext(this) as common.UIAbilityContext; // 获取当前页面的上下文 27 28 29// 配置下载参数,这里以简单配置url为例 30function downloadConfig(downloadUrl: string): request.agent.Config { 31 // TODO 知识点:配置下载参数。一个下载任务需要配置对应一套下载参数request.agent.Config。本例中使用downloadConfig方法简单配置了下载文件的url,实际业务中请按实际情况按需配置。 32 const config: request.agent.Config = { 33 action: request.agent.Action.DOWNLOAD, // 配置任务选项,这里配置为下载任务 34 url: downloadUrl, // 配置下载任务url 35 overwrite: true, // 下载过程中路径已存在时的解决方案选择。true表示覆盖已存在的文件 36 method: 'GET', // HTTP标准方法。下载时,使用GET或POST。 37 saveas: './', // 这里'./'表示下载至应用当前缓存路径下。 38 mode: request.agent.Mode.BACKGROUND, // 任务模式设置后台任务。 39 gauge: true, // 后台任务的过程进度通知策略,仅应用于后台任务。true表示发出每个进度已完成或失败的通知。 40 retry: false, // 默认为true,如果没有网络或者网络不满足时,会自动暂停waiting,等网络满足时进行一次重试。设置为false时,没网直接走失败回调 41 }; 42 return config; 43} 44 45// 单个下载任务 46@Component 47export struct FileDownloadItem { 48 // 文件下载配置 49 @State downloadConfig: request.agent.Config = { action: request.agent.Action.DOWNLOAD, url: '' }; 50 // 下载文件名 51 @State fileName: string = ''; 52 // 下载任务状态 53 @State state: string = ''; 54 // 监听是否全部开始下载 55 @Link @Watch('onDownLoadUpdated') isStartAllDownload: boolean; 56 // 下载任务对象初始化。用于下载失败和下载过程中暂停和重新启动下载。 57 private downloadTask: request.agent.Task | undefined; 58 // 待下载任务数量 59 @Link downloadCount: number; 60 // 下载失败任务数量 61 @Link downloadFailCount: number; 62 // 下载状态图标显隐控制。下载中显示图标,下载完成或者下载失败隐藏图标 63 @State isShow: boolean = false; 64 // 是否正在下载标志位 65 @State downloading: boolean = false; 66 // 下载文件大小。类型字符串 67 @State sFileSize: string = '-'; 68 // 下载文件大小。类型数值 69 @State nFileSize: number = 0; 70 // 当前已下载数据量。类型字符串 71 @State sCurrentDownloadSize: string = '-'; 72 // 当前已下载数据量。类型数值 73 @State nCurrentDownloadSize: number = 0; 74 // 下载文件数据 75 @ObjectLink fileDataInfo: downloadFilesData; 76 // 下载历史列表 77 @Link historyArray: downloadFilesData[]; 78 // 下载列表 79 @Link downloadFileArray: downloadFilesData[]; 80 // 下载任务完成回调 81 private completedCallback = (progress: request.agent.Progress) => { 82 // 下载状态设置为下载完成 83 this.state = '下载完成'; 84 // 获取下载完成的时间 85 const downloadTime = new Date().getTime(); 86 // 下载完成时更改对应数据源中的下载状态与下载时间 87 this.getFileStatusAndTime(1, downloadTime); 88 89 if (this.sFileSize === '未知大小') { 90 // 如果下载url文件的服务器采用chunk分块传输文件数据,是获取不到下载文件总大小的。对于这种下载文件大小无法获取到的情况, 91 // 本例中下载进度条展示效果是初始未下载时进度为0,总进度为1,当文件下载完成时下载进度值改成1,表示下载完成,同步更新显示到进度条上。 92 this.nCurrentDownloadSize = 1; 93 } 94 95 // 文件下载完成,待下载任务数量减1 96 if (this.downloadCount > 0) { 97 this.downloadCount--; 98 } 99 100 // 隐藏下载状态图标 101 this.isShow = false; 102 } 103 // 下载任务失败回调。任务下载失败一般是由于网络不好,底层重试也失败后进入该下载失败回调。如果网络没问题,建议重新下载再试。 104 private failedCallback = (progress: request.agent.Progress) => { 105 this.state = '下载失败'; 106 107 // 下载失败时更改对应数据源中的下载状态 108 this.getFileStatusAndTime(2); 109 110 // 当所有任务下载失败时,"全部暂停"状态重置为"全部开始"。 111 this.downloadFailCount++; 112 if (this.downloadFailCount === this.downloadCount) { 113 this.isStartAllDownload = false; 114 } 115 if (this.downloadTask) { 116 // show用于获取下载任务相关信息。 117 request.agent.show(this.downloadTask.tid, (err: BusinessError, taskInfo: request.agent.TaskInfo) => { 118 if (err) { 119 logger.error(TAG, `Failed to show with error message: ${err.message}, error code: ${err.code}`); 120 return; 121 } 122 if (this.downloadTask) { 123 // 打印下载失败的任务id和任务状态 124 logger.error(TAG, 125 `Failed to download with error downloadTask tid: ${this.downloadTask.tid} state: ${taskInfo.progress.state}`); 126 // 隐藏下载状态图标 127 this.isShow = false; 128 } 129 }); 130 } 131 } 132 // 暂停任务回调 133 private pauseCallback = (progress: request.agent.Progress) => { 134 this.state = '已暂停'; 135 // 切换下载状态图标 136 this.downloading = false; 137 } 138 // 重新启动任务回调。如果下载url文件的服务器不支持分片传输,则文件将重新下载。如果服务器支持分片传输,则会基于之前暂停时的下载进度继续下载。 139 private resumeCallback = (progress: request.agent.Progress) => { 140 // 切换下载状态图标 141 this.downloading = true; 142 } 143 // 下载进度更新回调 144 private progressCallback = (progress: request.agent.Progress) => { 145 // 性能知识点: 如果注册了progress下载进度更新监听,不建议在progress下载进度更新回调中加日志打印,减少不必要的性能损耗。 146 this.state = '下载中'; 147 this.downloading = true; 148 // 显示下载状态图标 149 this.isShow = true; 150 if (this.downloadTask) { 151 // 第一次开始下载 152 if (this.sFileSize === '-') { 153 // 如果下载url文件的服务器采用chunk分块传输文件数据,是获取不到下载文件总大小的。传过来的值为-1,则在页面上显示'未知大小' 154 if (progress.sizes[0] === -1) { 155 this.sFileSize = '未知大小'; 156 // 文件大小无法获取的情况下,进度条的值设置为0,总进度设置为1 157 this.nCurrentDownloadSize = 0; 158 this.nFileSize = 1; 159 } else { 160 // 能获取文件大小时,按实际下载数据量更新进度 161 this.nFileSize = progress.sizes[0]; 162 this.sFileSize = (progress.sizes[0] / BYTE_CONVERSION).toFixed() + 'kb'; 163 this.nCurrentDownloadSize = progress.processed; 164 } 165 } else if (this.sFileSize === '未知大小') { 166 // 非首次下载(暂停过下载任务后重新启动下载时),文件大小未知情况时,下载时进度不做更新 167 logger.info(TAG, `When the file size is unknown, the download progress will not be updated`); 168 } else { 169 // 非首次下载(暂停过下载任务后重新启动下载时),文件大小能获取到的情况,更新下载进度 170 this.nCurrentDownloadSize = progress.processed; 171 } 172 // 用于显示已下载文件数据大小 173 this.sCurrentDownloadSize = (progress.processed / BYTE_CONVERSION).toFixed() + 'kb'; 174 } 175 } 176 177 aboutToAppear(): void { 178 // 初始化下载配置 179 this.downloadConfig = downloadConfig(this.fileDataInfo.url); 180 181 // 从下载链接获取文件名 182 this.fileName = getFileNameFromUrl(this.fileDataInfo.url); 183 } 184 185 // 文件下载成功时更改文件状态与时间;下载失败时更改文件的状态 186 getFileStatusAndTime(status: number, time?: number) { 187 this.fileDataInfo.fileStatus = status; 188 if (time) { 189 this.fileDataInfo.downloadTime = time; 190 // 下载成功加入到下载历史列表 191 this.historyArray.push(this.fileDataInfo); 192 // 下载列表删除下载成功的数据 193 this.downloadFileArray = this.downloadFileArray.filter((item: downloadFilesData) => { 194 return item.id !== this.fileDataInfo.id; 195 }); 196 } 197 } 198 199 // 监听是否开始下载/暂停下载 200 onDownLoadUpdated(): void { 201 if (this.isStartAllDownload) { 202 // 如果下载失败,则重新下载。下载失败原因一般是网络原因导致。 203 if (this.state === '下载失败') { 204 // 下载任务完成或者任务失败时,底层会自动销毁任务资源。所以如果需要重新下载,重新创建任务即可。这里只做了初始化task对象 205 this.downloadTask = undefined; 206 // 隐藏下载状态图标 207 this.isShow = false; 208 // 重置下载任务状态 209 this.state = ''; 210 } 211 // 下载 212 this.startDownload(); 213 } else { 214 if (this.downloadFailCount > 0 && this.downloadFailCount === this.downloadCount) { 215 // 如果是任务全部下载失败,重置isStartAllDownload为false的情况,重置downloadFailCount 216 this.downloadFailCount = 0; 217 } else { 218 // 暂停下载 219 this.pauseDownload(); 220 } 221 } 222 } 223 224 // 启动下载任务 225 startDownload(): void { 226 // 首次下载,创建任务 227 if (this.downloadTask === undefined) { 228 // TODO 知识点:创建下载任务,并注册下载任务相关监听。本例在每个FileDownloadItem中使用request.agent.create创建下载任务。然后在下载 229 // 任务创建成功后,注册各自下载任务相关监听。本例中注册了下载任务完成回调,下载任务失败回调,下载进度更新回调,暂停任务回调,重新启动任务回调。 230 request.agent.create(context, this.downloadConfig).then((task: request.agent.Task) => { 231 // 注册下载任务相关回调 232 task.on('completed', this.completedCallback); // 下载任务完成回调 233 task.on('failed', this.failedCallback); // 下载任务失败回调 234 task.on('pause', this.pauseCallback); // 暂停任务回调 235 task.on('resume', this.resumeCallback); // 重新启动任务回调 236 task.on('progress', this.progressCallback); // 下载进度更新回调 237 238 // TODO 知识点:启动下载任务。本例在每个FileDownloadItem中使用task.start方法启动各自的下载任务。 239 task.start((err: BusinessError) => { 240 if (err) { 241 logger.error(TAG, `Failed to task start with error message: ${err.message}, error code: ${err.code}`); 242 return; 243 } 244 this.downloadTask = task; 245 }) 246 }).catch((err: BusinessError) => { 247 logger.error(TAG, `Failed to task create with error message: ${err.message}, error code: ${err.code}`); 248 }); 249 } else { 250 // 任务已存在时,继续下载 251 this.resumeDownload(); 252 } 253 } 254 255 // 暂停下载任务 256 pauseDownload(): void { 257 if (this.downloadTask) { 258 // TODO 知识点:使用request.agent.show,根据任务id可查询任务的详细信息。本处用于查询下载任务状态 259 request.agent.show(this.downloadTask.tid, (err: BusinessError, taskInfo: request.agent.TaskInfo) => { 260 if (err) { 261 logger.error(TAG, `Failed to show with error message: ${err.message}, error code: ${err.code}`); 262 return; 263 } 264 // 判断当前下载任务状态是否满足暂停条件。 265 if (this.downloadTask && (taskInfo.progress.state === request.agent.State.WAITING || taskInfo.progress.state === 266 request.agent.State.RUNNING || taskInfo.progress.state === request.agent.State.RETRYING)) { 267 // TODO 知识点:使用task.pause可以暂停正在等待WAITING/正在运行RUNNING/正在重试RETRYING的后台下载任务。 268 this.downloadTask.pause().then(() => { 269 // 暂停任务成功 270 }).catch((err: BusinessError) => { 271 logger.error(TAG, `Failed to pause with error message: ${err.message}, error code: ${err.code}`); 272 }); 273 } else { 274 if (this.downloadTask) { 275 // 不满足暂停任务条件 276 logger.info(TAG, `Not meeting the pause task conditions,current task state: ${taskInfo.progress.state}`); 277 } 278 } 279 }); 280 } 281 } 282 283 // 重新启动下载任务 284 resumeDownload(): void { 285 if (this.downloadTask) { 286 // 查询任务状态 287 request.agent.show(this.downloadTask.tid, (err: BusinessError, taskInfo: request.agent.TaskInfo) => { 288 if (err) { 289 logger.error(TAG, `Failed to show with error message: ${err.message}, error code: ${err.code}`); 290 return; 291 } 292 // 判断如果任务是暂停状态,则重新启动下载任务 293 if (this.downloadTask && taskInfo.progress.state === request.agent.State.PAUSED) { 294 // TODO 知识点:使用task.resume可以重新启动任务,可恢复暂停的后台任务。 295 this.downloadTask.resume((err: BusinessError) => { 296 if (err) { 297 logger.error(TAG, `Failed to resume with error message: ${err.message}, error code: ${err.code}`); 298 return; 299 } 300 // 重新启动下载任务成功 301 }); 302 } 303 }); 304 } 305 } 306 307 build() { 308 RelativeContainer() { 309 Image($r('app.media.multiple_files_download_file')) 310 .height($r('app.integer.multiple_files_download_image_size_fifty')) 311 .width($r('app.integer.multiple_files_download_image_size_fifty')) 312 .id('fileImage') 313 314 Text(this.fileName) 315 .fontSize($r('app.integer.multiple_files_download_text_font_size_fourteen')) 316 .padding({ left: $r('app.integer.multiple_files_download_padding_twenty') }) 317 .alignRules({ 318 left: { anchor: 'fileImage', align: HorizontalAlign.End } 319 }) 320 .id('fileName') 321 322 Image(this.downloading ? $r('app.media.multiple_files_download_start') : 323 $r('app.media.multiple_files_download_stop')) 324 .visibility(this.isShow ? Visibility.Visible : Visibility.Hidden) 325 .height($r('app.integer.multiple_files_download_image_size_twenty_five')) 326 .width($r('app.integer.multiple_files_download_image_size_twenty_five')) 327 .margin({ top: $r('app.integer.multiple_files_download_margin_top_seven') }) 328 .alignRules({ 329 right: { anchor: '__container__', align: HorizontalAlign.End } 330 }) 331 .id('downloadImage') 332 .onClick(() => { 333 // 这里未做实际功能,仅做展示 334 AlertDialog.show({ 335 message: $r('app.string.multiple_files_download_function_only_display_purposes'), 336 alignment: DialogAlignment.Center 337 }); 338 }) 339 340 Text(this.sCurrentDownloadSize + '/' + this.sFileSize) 341 .fontSize($r('app.integer.multiple_files_download_text_font_size_twelve')) 342 .width($r('app.string.multiple_files_download_text_width')) 343 .fontColor($r('app.color.multiple_files_download_text_font_color')) 344 .margin({ top: $r('app.integer.multiple_files_download_margin_top_seven') }) 345 .padding({ left: $r('app.integer.multiple_files_download_padding_twenty') }) 346 .alignRules({ 347 top: { anchor: 'downloadImage', align: VerticalAlign.Center }, 348 left: { anchor: 'fileImage', align: HorizontalAlign.End } 349 }) 350 .id('downloadVal') 351 352 Text(this.state) 353 .fontSize($r('app.integer.multiple_files_download_text_font_size_twelve')) 354 .fontColor($r('app.color.multiple_files_download_text_font_color')) 355 .margin({ top: $r('app.integer.multiple_files_download_margin_top_seven') }) 356 .alignRules({ 357 top: { anchor: 'downloadImage', align: VerticalAlign.Center }, 358 left: { anchor: 'downloadVal', align: HorizontalAlign.End }, 359 right: { anchor: 'downloadImage', align: HorizontalAlign.Start } 360 }) 361 .id(this.fileName + 'state') 362 // 下载进度条,用于显示从下载进度更新回调中获取到的已下载数据大小 363 Progress({ value: INIT_PROGRESS, total: this.nFileSize, type: ProgressType.Capsule }) 364 .alignRules({ 365 top: { anchor: 'fileImage', align: VerticalAlign.Bottom } 366 }) 367 .value(this.nCurrentDownloadSize) 368 .height($r('app.integer.multiple_files_download_progress_height')) 369 .margin({ top: $r('app.integer.multiple_files_download_margin_top_five') }) 370 .id('progress') 371 } 372 .width($r('app.string.multiple_files_download_relative_container_width')) 373 .height($r('app.string.multiple_files_download_relative_container_height')) 374 } 375}