Using Transient Tasks in HarmonyOS Next: A DownloadCenter Sample

Photo by Stanislav on Unsplash

Introduction

In HarmonyOS Next, managing transient background tasks is crucial for delivering smooth user experiences without draining system resources or violating OS lifecycle constraints.

This article focuses on demonstrating how to use Transient Tasks with a simulated file download scenario. By simulating download progress, we learn how to keep tasks alive in the background, update the notification UI in real-time, and manage the lifecycle using the official BackgroundTasksKit API.

What are Transient Tasks?

In HarmonyOS Next, a transient task is a temporary background operation that the system allows to continue execution even if the app enters the background.

DownloadCenter App

App Preview

DownloadCenter is a simulated download manager in a HarmonyOS Next application that allows users to start, pause, resume, or cancel file downloads while displaying real-time progress. It leverages the BackgroundTasksKit to request transient background tasks, ensuring that the download process continues even when the app moves to the background. Additionally, it updates the user with live notifications using NotificationKit, providing a responsive and lifecycle-aware download experience.

HarmonyOS NEXT Kits Used in DownloadCenter and Implementation Overview

DownloadCenter uses several HarmonyOS NEXT kits:

  • AbilityKit: Used to manage the UIAbility context and access ability information such as bundle name and ability name for creating WantAgents.
  • NotificationKit: Handles publishing, canceling, and checking support for custom notification templates like download progress notifications.
  • BackgroundTasksKit: Manages transient background tasks via requestSuspendDelay and cancelSuspendDelay, enabling background execution during simulated downloads.
  • BasicServicesKit: Provides error handling through the BusinessError class used in background task management.

Now, let’s dive into the core implementation and explore how the DownloadCenter is built step-by-step to simulate a seamless download experience with background task support.

Required Permissions

To execute transient tasks while running in the background, the following permission must be added to module.json5:

"requestPermissions": [
{
"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"
}
]

Preparing Permissions and Notifications

When the UI is about to appear, the app requests notification permission, creates a WantAgent for handling notification clicks, and checks if the custom download notification template is supported.

aboutToAppear() {
openNotificationPermission(this.context);
createWantAgent(bundleName, abilityName).then(agent => {
this.wantAgentObj = agent;
});
notificationManager.isSupportTemplate('downloadTemplate').then(support => {
this.isSupport = support;
});
}

Managing Download States

The app controls the download process with methods to start, pause, resume, and cancel downloads. These methods update the download status and progress, and handle UI updates and background task requests.

async start() {
this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING;
this.requestSuspend();
this.download();
}
async pause() {
this.downloadStatus = DOWNLOAD_STATUS.PAUSE;
clearInterval(this.interval);
this.cancelSuspend();
}

Handling Background Task Lifecycles

To keep the download running when the app is in the background, requestSuspend() requests a transient task from the system to delay suspension, and cancelSuspend() cancels this transient task request if needed. If the transient task is about to timeout, the provided callback gracefully cancels the download.

requestSuspend() {
const info = backgroundTaskManager.requestSuspendDelay('File downloading', () => {
this.cancel(); // Cancel if timeout
});
this.requestId = info.requestId;
}
cancelSuspend() {
if (this.requestId !== -1) {
backgroundTaskManager.cancelSuspendDelay(this.requestId);
this.requestId = -1;
}
}

Simulating Download and Updating Notifications

The download() method simulates download progress using setInterval, updates the progress state, and publishes a notification with the current progress until completion.

download() {
this.interval = setInterval(async () => {
if (this.downloadProgress >= CommonConstants.PROGRESS_TOTAL) {
clearInterval(this.interval);
this.downloadStatus = DOWNLOAD_STATUS.FINISHED;
this.cancelSuspend();
} else {
this.downloadProgress += CommonConstants.PROGRESS_SPEED;
}
publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj);
}, CommonConstants.UPDATE_FREQUENCY);
}

Now, let’s take a quick look at the complete code implementation.

import { common, wantAgent } from '@kit.AbilityKit';
import { notificationManager } from '@kit.NotificationKit';
import { createWantAgent, publishNotification, openNotificationPermission } from '../common/utils/NotificationUtil';
import { getStringByRes } from '../common/utils/ResourseUtil';
import Logger from '../common/utils/Logger';
import CommonConstants, { DOWNLOAD_STATUS } from '../common/constants/CommonConstants';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
private requestId: number = -1;
@State downloadStatus: number = DOWNLOAD_STATUS.INITIAL;
@State downloadProgress: number = 0;
private notificationTitle: string = '';
private wantAgentObj: object = {} as wantAgent.WantAgentInfo;
private interval: number = -1;
private isSupport: boolean = true;
aboutToAppear() {
openNotificationPermission(this.context);
const bundleName = this.context.abilityInfo.bundleName;
const abilityName = this.context.abilityInfo.name;
createWantAgent(bundleName, abilityName).then(agent => {
this.wantAgentObj = agent;
});
notificationManager.isSupportTemplate('downloadTemplate').then(support => {
this.isSupport = support;
});
}
onBackPress() {
this.cancel();
}
build() {
Column() {
Column() {
Row() {
Image($r('app.media.ic_image'))
.objectFit(ImageFit.Fill)
.width(24)
.height(24)
Text(CommonConstants.DOWNLOAD_FILE)
.fontSize(12)
.textAlign(TextAlign.Center)
.fontColor(Color.Black)
.margin({ left: 8 })
}
.width('100%')
Progress({
value: this.downloadProgress,
total: CommonConstants.PROGRESS_TOTAL
}).width('100%')
Row() {
if (this.downloadStatus === DOWNLOAD_STATUS.INITIAL) {
this.customButton($r('app.string.button_download'), (): Promise<void> => this.start())
} else if (this.downloadStatus === DOWNLOAD_STATUS.DOWNLOADING) {
Row() {
this.cancelButton()
this.customButton($r('app.string.button_pause'), (): Promise<void> => this.pause())
}
} else if (this.downloadStatus === DOWNLOAD_STATUS.PAUSE) {
Row() {
this.cancelButton()
this.customButton($r('app.string.button_resume'), (): Promise<void> => this.resume())
}
} else {
Text('Download completed!')
.fontSize(12)
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('85%')
.height(108)
.backgroundColor(Color.White)
.borderRadius(16)
.justifyContent(FlexAlign.SpaceBetween)
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor(Color.Grey)
.justifyContent(FlexAlign.Center)
}
// Background Operations
async start() {
this.notificationTitle = await getStringByRes($r('app.string.notification_title_download'), this);
this.downloadProgress = 0;
this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING;
this.requestSuspend();
this.download();
}
async resume() {
this.notificationTitle = await getStringByRes($r('app.string.notification_title_download'), this);
this.downloadStatus = DOWNLOAD_STATUS.DOWNLOADING;
this.requestSuspend();
this.download();
}
async pause() {
this.notificationTitle = await getStringByRes($r('app.string.notification_title_pause'), this);
this.downloadStatus = DOWNLOAD_STATUS.PAUSE;
clearInterval(this.interval);
this.cancelSuspend();
if (this.isSupport) {
publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj);
}
}
async cancel() {
this.downloadProgress = 0;
this.downloadStatus = DOWNLOAD_STATUS.INITIAL;
clearInterval(this.interval);
this.cancelSuspend();
notificationManager.cancel(CommonConstants.NOTIFICATION_ID);
}
requestSuspend() {
const reason = 'File downloading';
try {
const info = backgroundTaskManager.requestSuspendDelay(reason, () => {
Logger.warn('⏱ Transient task about to timeout.');
this.cancel();
});
this.requestId = info.requestId;
Logger.info(`Transient task started with ID: ${this.requestId}`);
} catch (err) {
Logger.error(`requestSuspendDelay failed: ${(err as BusinessError).message}`);
}
}
cancelSuspend() {
try {
if (this.requestId !== -1) {
backgroundTaskManager.cancelSuspendDelay(this.requestId);
Logger.info(`Transient task canceled (ID: ${this.requestId})`);
this.requestId = -1;
}
} catch (err) {
Logger.error(`cancelSuspendDelay failed: ${(err as BusinessError).message}`);
}
}
download() {
this.interval = setInterval(async () => {
if (this.downloadProgress >= CommonConstants.PROGRESS_TOTAL) {
clearInterval(this.interval);
this.notificationTitle = await getStringByRes($r('app.string.notification_title_finish'), this);
this.downloadStatus = DOWNLOAD_STATUS.FINISHED;
this.cancelSuspend();
Logger.info('Download finished.');
} else {
this.downloadProgress += CommonConstants.PROGRESS_SPEED;
Logger.info(`Downloading... progress: ${this.downloadProgress}`);
}
if (this.isSupport) {
publishNotification(this.downloadProgress, this.notificationTitle, this.wantAgentObj);
}
}, CommonConstants.UPDATE_FREQUENCY);
}
@Builder
customButton(textResource: Resource, click: Function = () => {
}) {
Button(textResource)
.fontSize(8)
.backgroundColor($r('app.color.button_color'))
.buttonsStyle()
.onClick(() => {
click();
})
}
@Builder
cancelButton() {
Button($r('app.string.button_cancel'))
.buttonsStyle()
.backgroundColor($r('app.color.cancel_button_color'))
.fontColor($r('app.color.button_color'))
.margin({ right: 8 })
.onClick(() => {
this.cancel();
})
}
}
@Extend(Button)
function buttonsStyle() {
.constraintSize({ minWidth: 64 })
.height(24)
.borderRadius(14)
.fontSize(8)
}

Conclusion

This implementation demonstrates how to manage transient background tasks effectively to keep downloads running smoothly even when the app is in the background. By integrating notification updates and lifecycle management, it ensures a seamless user experience. Overall, it highlights the power of HarmonyOS BackgroundTasksKit in handling real-time background operations.

Learn more Using Transient Tasks in HarmonyOS Next: A DownloadCenter Sample

Leave a Reply