HarmonyOS鸿蒙Next中关于长时任务更新通知报202错误

HarmonyOS鸿蒙Next中关于长时任务更新通知报202错误 按照系统文档给出的案例,如下图所示:

cke_743.png

启动长时任务成功,但是在通过notificationId更新进度时,报202错误,Not system application to call the interface,已经按文档说的申请了"name": “ohos.permission.KEEP_BACKGROUND_RUNNING”,

是需要申请其他权限如"ohos.permission.NOTIFICATION_CONTROLLER",或者还是需要申请实况窗的权限才能操作?请高手指导,不胜感激!


更多关于HarmonyOS鸿蒙Next中关于长时任务更新通知报202错误的实战教程也可以访问 https://www.itying.com/category-93-b0.html

17 回复

尊敬的开发者您好,当前仅支持数据传输类型的长时任务使用实况窗,您这边的蓝牙相关场景建议使用普通通知。
数据传输类型的长时任务代码可参考:

【解决方案】

以下载网络视频到应用本地为例,具体步骤如下:

  1. 添加后台运行权限ohos.permission.KEEP_BACKGROUND_RUNNING和网络权限ohos.permission.INTERNET

  2. 声明数据传输后台模式类型。在module.json5文件中为需要使用长时任务的UIAbility声明相应的长时任务类型,配置文件中填写长时任务类型的配置项。示例如下:

"backgroundModes": [
  // 配置长时任务类型为数据传输
  "dataTransfer"
]
  1. 在UI页面申请数据传输长时任务,并且通知worker线程下载视频。

  2. 在worker中使用request.downloadFile接口下载视频。

完整代码示例如下:

Index.ets文件:

import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { common, WantAgent, wantAgent } from '@kit.AbilityKit';
import { MessageEvents, worker } from '@kit.ArkTS';

const workerInstance = new worker.ThreadWorker('entry/ets/workers/Worker.ets');

@Entry
@Component
struct Index {
  context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
  private notificationId: number = 0; // 保存通知id
  private filePath: string = '';
  private fileCount: number = 2;
  @State fileNameArr: Array<string> = new Array(this.fileCount);
  private fileUrl: string = 'https://xxx/xxx.mp4';

  aboutToAppear(): void {
    workerInstance.onmessage = (e: MessageEvents): void => {
      if (e.data.code == 100) {
        this.updateProcess(e.data.percent, e.data.currentName, e.data.completeCount);
      }
      if (e.data.code == 200) {
        this.stopContinuousTask();
      }
    };
  }

  build() {
    Column() {
      Button('开始下载')
        .type(ButtonType.Capsule)
        .margin({ top: 10 })
        .backgroundColor('#0A59F7')
        .width(300)
        .height(40)
        .onClick(() => {
          this.filePath = this.context.filesDir;
          let fileUrlArrTemp: Array<string> = new Array(this.fileCount);
          let fileNameArrTemp: Array<string> = new Array(this.fileCount);
          for (let index = 0; index < this.fileCount; index++) {
            this.fileNameArr[index] = `trailer${index}.mp4`;
            fileUrlArrTemp[index] = this.fileUrl;
            fileNameArrTemp[index] = `trailer${index + 1}.mp4`;
          }
          workerInstance.postMessage({
            code: 200,
            context: this.context,
            fileUrlArr: fileUrlArrTemp,
            filePath: this.filePath,
            fileNameArr: fileNameArrTemp
          });
          this.startContinuousTask();
        })
    }
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.SpaceAround)
  }

  startContinuousTask() {
    let wantAgentInfo: wantAgent.WantAgentInfo = {
      // 点击通知后,将要执行的动作列表
      // 添加需要被拉起应用的bundleName和abilityName
      wants: [
        {
          bundleName: this.context.abilityInfo.bundleName,
          abilityName: 'MainAbility'
        }
      ],
      // 指定点击通知栏消息后的动作是拉起ability
      actionType: wantAgent.OperationType.START_ABILITY,
      // 使用者自定义的一个私有值
      requestCode: 0,
      // 点击通知后,动作执行属性
      actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
    };

    try {
      // 通过wantAgent模块下getWantAgent方法获取WantAgent对象
      wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj: WantAgent) => {
        try {
          let list: Array<string> = ['dataTransfer'];
          backgroundTaskManager.startBackgroundRunning(this.context, list, wantAgentObj)
            .then((res: backgroundTaskManager.ContinuousTaskNotification) => {
              console.info('Operation startBackgroundRunning succeeded');
              // 保存通知id,用于更新进度条
              this.notificationId = res.notificationId;
            })
            .catch((error: BusinessError) => {
              console.error(`Failed to Operation startBackgroundRunning. code is ${error.code} message is ${error.message}`);
            });
        } catch (error) {
          console.error(`Failed to Operation startBackgroundRunning. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
        }
      });
    } catch (error) {
      console.error(`Failed to Operation getWantAgent. code is ${(error as BusinessError).code} message is ${(error as BusinessError).message}`);
    }
  }

  // 停止长时任务
  stopContinuousTask() {
    backgroundTaskManager.stopBackgroundRunning(this.context).then(() => {
      console.info(`Succeeded in operationing stopBackgroundRunning.`);
    }).catch((err: BusinessError) => {
      console.error(`Failed to operation stopBackgroundRunning. Code is ${err.code}, message is ${err.message}`);
    });
  }

  // 应用更新进度
  updateProcess(process: Number, fileName: string, completeCount: number) {
    // 应用定义下载类通知模版
    let downLoadTemplate: notificationManager.NotificationTemplate = {
      name: 'downloadTemplate', // 当前只支持downloadTemplate,保持不变
      data: {
        title: `已下载${completeCount}个/共${this.fileNameArr.length}个`, // 必填
        fileName: `正在下载:${fileName}`, // 必填
        progressValue: process, // 应用更新进度值,自定义
      }
    };
    let request: notificationManager.NotificationRequest = {
      content: {
        // 系统实况类型,保持不变
        notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW,
        systemLiveView: {
          typeCode: 8, // 上传下载类型需要填写 8,当前仅支持此类型。保持不变
          title: `保存视频到相册`, // 应用自定义
          text: `正在下载:${fileName}`, // 应用自定义
        }
      },
      id: this.notificationId, // 必须是申请长时任务返回的id,否则应用更新通知失败
      notificationSlotType: notificationManager.SlotType.LIVE_VIEW, // 实况窗类型,保持不变
      template: downLoadTemplate // 应用需要设置的模版名称
    };

    try {
      notificationManager.publish(request).then(() => {
        console.info(`publish success, id= ${this.id}`);
      }).catch((err: BusinessError) => {
        console.error(`publish fail. code is ${(err as BusinessError).code} message is ${(err as BusinessError).message}`);
      });
    } catch (err) {
      console.error(`publish fail. code is ${(err as BusinessError).code} message is ${(err as BusinessError).message}`);
    }
  }
}

Worker.ets文件:

import { MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
import { common } from '@kit.AbilityKit';
import { request } from '@kit.BasicServicesKit';

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
let completeCount: number = 0;
let isDownLoading: boolean = false;
let IntervalID: number = 0;

/**
 * Defines the event handler to be called when the worker thread receives a message sent by the host thread.
 * The event handler is executed in the worker thread.
 *
 * @param event message data
 */
workerPort.onmessage = (event: MessageEvents) => {
  completeCount = 0;
  let context: common.UIAbilityContext = event.data.context;
  let fileUrlArr: string = event.data.fileUrlArr;
  let filePath: string = event.data.filePath;
  let fileNameArr: Array<string> = event.data.fileNameArr;
  let totalCount: number = fileNameArr.length;
  console.info('------ start ------------');
  // 开启定时器,定时扫描当前是否下载完成,或者是否正在下载
  IntervalID = setInterval(() => {
    if (completeCount < totalCount) {
      if (isDownLoading === false) {
        downLoadFile(context, fileUrlArr[completeCount], filePath, fileNameArr[completeCount]);
      }
    } else {
      console.info('------ end ------------');
      workerPort.postMessage({ code: 200, data: 20 });
      clearInterval(IntervalID);
    }
  }, 500);
};

/**
 * Defines the event handler to be called when the worker receives a message that cannot be deserialized.
 * The event handler is executed in the worker thread.
 *
 * @param event message data
 */
workerPort.onmessageerror = (event: MessageEvents) => {
  console.log(`MessageEvents => ${JSON.stringify(event)}`);
};

/**
 * Defines the event handler to be called when an exception occurs during worker execution.
 * The event handler is executed in the worker thread.
 *
 * @param event error message
 */
workerPort.onerror = () => {
};

function downLoadFile(context: common.UIAbilityContext, fileUrl: string, filePath: string, fileName: string) {
  console.info(`download fileUrl: ${fileUrl}, fileName: ${fileName}, filePath: ${filePath}`);
  try {
    // 需要手动将url替换为真实服务器的HTTP协议地址
    isDownLoading = true;
    request.downloadFile(context, {
      url: fileUrl,
      filePath: context.filesDir + `/${fileName}`
    }, (err: BusinessError, data: request.DownloadTask) => {
      if (err) {
        console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
        return;
      }
      let downloadTask: request.DownloadTask = data;
      downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
        console.info(`download receivedSize: ${receivedSize}, totalSize: ${totalSize}`);
        let percent = Math.ceil((receivedSize / totalSize) * 100); // 计算进度值
        workerPort.postMessage({
          code: 100,
          percent: percent, // 当前正在下载的文件进度
          currentName: fileName, // 当前正在下载的文件名
          completeCount: completeCount // 下载完成的个数
        });
      });

      downloadTask.on('complete', () => {
        completeCount++;
        console.info('download complete');
        isDownLoading = false;
      });
    });
  } catch (err) {
    console.error(`Failed to request the download. Code: ${err.code}, message: ${err.message}`);
  }
}

更多关于HarmonyOS鸿蒙Next中关于长时任务更新通知报202错误的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


202 先按通用错误码排查:它表示“非系统应用调用系统 API”。普通应用更新自己发布的通知,不要走通知控制类系统接口;官方“更新通知”示例是复用同一个 NotificationRequest.id,修改内容后再次 notificationManager.publish。长时任务只负责 startBackgroundRunning,返回的 notificationId 只是标识该任务通知。 依据: https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/update-notification https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/continuous-task https://developer.huawei.com/consumer/cn/doc/harmonyos-references/errorcode-universal

是的,这个结论更准确:当前能走“长时任务通知就地更新进度/实况窗”这条链路的,核心前提是数据传输类长时任务,也就是 dataTransfer 场景。

bluetoothInteraction 这类长时任务可以用于后台蓝牙交互和保活通知,但不能直接复用长时任务返回的 notificationId 去发布 SYSTEM_LIVE_VIEW / LIVE_VIEW 进度通知,所以会报 202。官方示例如果没有明确写出只适用于 dataTransfer,确实容易让人误解。

如果业务本质是蓝牙健康数据采集,建议还是按 bluetoothInteraction 保留长时任务本身,进度展示用普通通知或应用内页面;只有上传/下载等数据传输任务,才按 dataTransfer + 对应实况窗能力去接。

看你这段代码,202 基本可以定位在 updateTaskInfo() 里的 notificationManager.publish(request),不是 startBackgroundRunning() 本身。

触发点是这两个配置:

notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW notificationSlotType: notificationManager.SlotType.LIVE_VIEW

这走的是系统实况窗/Live View 通知类型,普通三方应用不能把长时任务返回的 notificationId 拿来 publish 一个 SYSTEM_LIVE_VIEW 通知,所以会报 Not system application。也就是说 notificationId 是长时任务通知的标识,但不代表应用可以通过 Notification Kit 用系统实况窗类型重新发布/改写它。

你的场景是蓝牙采集健康数据,startBackgroundRunning(context, [‘bluetoothInteraction’], agent) 这个方向本身更接近蓝牙交互长时任务;但它对应的是后台保活和长时任务通知,不等于开放一个可任意更新进度模板的系统通知。想展示进度有三个选择:

  1. 保留 bluetoothInteraction 长时任务通知,另外用普通通知发布业务进度,使用你自己的 notification id,不要使用 SYSTEM_LIVE_VIEW/LIVE_VIEW。
  2. 如果业务确实是上传/下载类数据传输,按 API 21+ 的 MODE_DATA_TRANSFER + SUBMODE_LIVE_VIEW_NOTIFICATION 方向接入,并确认实况窗能力/场景准入;这和蓝牙采集本身不是一类场景。
  3. 如果只是页面内展示采集进度,优先放在应用页面或前台服务 UI,不要强依赖改写系统生成的长时任务通知。

所以你现在这段代码的最小修改是:不要在 publish() 里使用 NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW 和 SlotType.LIVE_VIEW,也不要复用长时任务返回的 notificationId 去发布实况窗通知。

是不是在后台调用startAbility了。仅这段代码不会202的。

可以贴出完整代码。

我做了个简单页面,页面代码就不贴了,就是个按钮启动,在页面的对应的viewmodel里,发起长时任务及更新长时任务,相关代码如下:

testTask = async (context: UIContext): Promise<void> => {
    const taskContext = context.getHostContext() as common.UIAbilityContext
    const taskInfo = await MyTask.startTask(taskContext)
    Logger.info('===testTask', JSON.stringify(taskInfo))
    let count = 0
    const total = 10
    let timer: number | undefined
    timer = setInterval(() => {
        if (count === total) {
            MyTask.stopTask(taskContext)
            clearInterval(timer)
            Logger.info('===update', '结束')
        } else {
            MyTask.updateTaskInfo(count, total, '两小时', taskInfo.notificationId)
            Logger.info('===update', `计数:${count}`)
        }
        count++
    }, 1000)
}

长时任务的相关操作,我封装到一个类里了MyTask,代码如下:

export class MyTask {
    /*
    * 启动长程采集任务
    * */
    static startTask =
        async (context: common.UIAbilityContext): Promise<backgroundTaskManager.ContinuousTaskNotification> => {
            const wantAgentInfo: wantAgent.WantAgentInfo = {
                wants: [
                    {
                        bundleName: context.abilityInfo.bundleName,
                        abilityName: context.abilityInfo.name,
                    }
                ],
                requestCode: 0,
                actionType: wantAgent.OperationType.START_ABILITY,
                wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG],
            }
            try {
                //获取意图实例
                const agent = await wantAgent.getWantAgent(wantAgentInfo).catch((err: Error) => {
                    throw err
                })
                const res = backgroundTaskManager.startBackgroundRunning(context, ['bluetoothInteraction'], agent)
                    .catch((err: Error) => {
                        throw err
                    })
                return res
            } catch (err) {
                return Promise.reject(err.message)
            }
        }

    /*
    * 停止长程任务
    * */
    static stopTask = async (context: common.UIAbilityContext) => {
        await backgroundTaskManager.stopBackgroundRunning(context).catch((err: Error) => {
            return Promise.reject(err.message)
        })
    }

    /*
    * 更新长程任务信息
    * */
    static updateTaskInfo = (collectedTime: number, totalTime: number, timeTitle: string, notificationId: number) => {
        const template: notificationManager.NotificationTemplate = {
            name: 'downloadTemplate', // 当前只支持downloadTemplate,保持不变
            data: {
                title: '数据采集', // 必填
                fileName: '设定时长:' + timeTitle, // 必填
                progressValue: `${(collectedTime * 100 / totalTime).toFixed(2)} %`// 应用更新进度值,自定义
            }
        }
        const request: notificationManager.NotificationRequest = {
            content: {
                // 系统实况类型,保持不变
                notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW,
                systemLiveView: {
                    typeCode: 8, // 上传下载类型需要填写 8,当前仅支持此类型。保持不变
                    title: "test", // 应用自定义
                    text: "test", // 应用自定义
                }
            },
            id: notificationId, // 必须是申请长时任务返回的id,否则应用更新通知失败
            notificationSlotType: notificationManager.SlotType.LIVE_VIEW, // 实况窗类型,保持不变
            template: template // 应用需要设置的模版名称
        }
        Logger.info('===publish', `id:${notificationId}`)
        notificationManager.publish(request).then(() => {
            Logger.info('===publish', `time:${collectedTime}`)
        }).catch((err: Error) => {
            Logger.error('===publish', JSON.stringify(err) + err.message + err.name)
            throw err
        })
    }
}

这个调用
cke_260.png

不是这个原因,是官方目前只支持dataTransfer类型的就地更新进度,我把长时任务类型从bluetoothInteraction转成dataTransfer,就ok了,官方文档举例也不说明适用范围,真的是很坑啊。

这个 202 基本不是权限声明的问题,而是接口权限级别的问题。

你现在能成功 startBackgroundRunning(),说明:

  • ohos.permission.KEEP_BACKGROUND_RUNNING 已经生效了;
  • 长时任务本身也已经启动成功;
  • 问题不在后台任务权限。

真正的问题在于:

notificationManager.publish(request) 里用了:

notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW

还有:

notificationSlotType: notificationManager.SlotType.LIVE_VIEW

这两个属于“系统实况窗 / 系统级 Live View”能力。

而:

NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW

并不是普通三方应用开放的接口。

所以更新时系统直接报:

202 Not system application to call the interface

意思就是:

“你不是系统应用,不能调用这个系统级通知接口”。

你现在这个场景其实是:

  • 启动长时任务:成功(普通应用可用)
  • 更新系统级 Live View:失败(仅系统应用可用)

这是两个权限体系。

你提到的:

ohos.permission.NOTIFICATION_CONTROLLER

这个也是 system_basic/system_core 级权限。

普通应用申请了也不会生效。

包括很多“实况窗”相关能力,目前也不是完全对三方开放。

所以结论是:

  1. 不是 KEEP_BACKGROUND_RUNNING 的问题
  2. 不是 notificationId 的问题
  3. 不是代码逻辑问题
  4. 是你用了 SYSTEM_LIVE_VIEW 导致的系统级限制

你现在有两个方案:

方案1(推荐):

改成普通前台服务通知。

不要使用:

NOTIFICATION_CONTENT_SYSTEM_LIVE_VIEW
LIVE_VIEW

改普通 Notification。

这样三方应用可以正常更新进度。

方案2:

接入真正的“实况窗/卡片”能力。

但目前很多能力:

  • 需要白名单
  • 需要系统签名
  • 或仅部分场景开放

并不是普通应用随便就能用。

所以现在大多数应用实际上都是:

  • 长时任务

  • 普通进度通知

而不是系统级 Live View。

关键是,这是华为官方文档的示例,如果是这样,官方文档也太儿戏了吧

可以使用“实况窗”能力试试

之前就是用的长时任务加普通通知,但是显得很傻,有个后台任务的通知在那里,然后下面再加个普通通知,之后看到官方文档能在长时任务返回的通知id上更新信息,觉得华为是不是真的解决这个问题了,结果试了后报错,如果不开发给大家用,就不要在官方文档里展示这个示例吧,太失望了。

错误202 “Not system application to call the interface” 不是权限问题,而是API使用方式错了。普通应用只能通过通知发布/更新机制,不能直接操控系统通知控制器

先查询系统是否支持进度条模板类通知

notificationManager.isSupportTemplate('downloadTemplate').then((data: boolean) => {
let isSupportTemplate: boolean = data; // isSupportTemplate的值为true表示支持downloadTemplate模板类通知,false表示不支持
hilog.info(DOMAIN_NUMBER, TAG,
`Succeeded in supporting download template notification. data is ${isSupportTemplate}`);
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG,
`Failed to support download template notification. Code is ${err.code}, message is ${err.message}`);
});

这个 202 一般不是 KEEP_BACKGROUND_RUNNING 没配好,而是调用到了系统应用才允许的接口。NOTIFICATION_CONTROLLER 属于通知控制类权限,三方应用通常不能靠在 module.json5 里声明来获得,也不是用来更新自己长时任务通知的常规方案。

如果你是 API 21+ 的长时任务,建议把“通知 id”和“长时任务 id”区分开:更新长时任务应走 backgroundTaskManager 的 updateBackgroundRunning,并在 ContinuousTaskRequest 里传已存在的 continuousTaskId,且 backgroundTaskModes/backgroundTaskSubmodes 要和申请时匹配。文档里也说明 updateBackgroundRunning 时 continuousTaskId 必须存在,否则更新失败。

另外,如果场景是后台下载/上传这类数据传输,进度更新对应的是 MODE_DATA_TRANSFER + SUBMODE_LIVE_VIEW_NOTIFICATION,进度长时间不更新还会被系统取消;普通文本通知不能直接当作数据传输进度通知来更新。实况窗也不是单纯加一个权限就能替代普通通知,需要按 Live View/实况窗能力和场景接入。

建议按这个顺序排查:1)确认没有调用管理/控制其他通知的系统接口;2)确认用 continuousTaskId 更新长时任务,而不是混用 notificationId;3)确认长时任务主类型/子类型是开放给当前应用的组合;4)确认通知授权已开启。

HarmonyOS Next 中长时任务更新通知报202错误,通常因NotificationRequestcontenttemplateId参数无效,或通知ID与任务绑定不一致。检查通知ID是否与startLongTimeTask传入的ID匹配,并确保NotificationRequest内容不包含非法字符或缺失必需字段。

报 202 错误是因为直接调用了系统级通知发布接口(如 Notification.publish),HarmonyOS NEXT 中该接口仅限系统应用。长时任务的进度更新应使用长时任务实例提供的公开方法,不需要 NOTIFICATION_CONTROLLER 等系统权限。

在调用 startBackgroundRunning 成功后,你会获得一个 ContinuousTask 对象,使用它的 updateNotification 即可更新通知内容与进度,例如:

let continuousTask: continuousTask.ContinuousTask;
// 启动长时任务后获得 continuousTask 实例
let request: notification.NotificationRequest = {
  // 构造包含进度等更新的通知
};
continuousTask.updateNotification(request);

这样即不会触发权限错误,已申请的 KEEP_BACKGROUND_RUNNING 足够。实况窗是另一机制,此处无需申请。

回到顶部