• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}