基于原生能力的各类通知管理
场景描述
1. Notification Kit提供通知发布功能,可以将应用产生的通知在客户端推送给用户。除了携带基本的文本图片信息外,还支持文件上传下载进度场景下的进度条通知,以及点击通知栏可以拉起目标应用的意图类型通知,Notification仅支持进程存在时发送通知。
2. Push Kit推送服务,是华为提供的消息推送平台,实现应用的实时消息推送,即使应用进程不在也能实时推送消息。
3. Live View Kit实况窗服务用于帮助用户聚焦正在进行的任务,方便快速查看和即时处理的通知形态多用于网约车、外卖等实时通知场景。
4. AVSession Kit提供音频视频播控服务,当创建AVSession对象,设置媒体信息并注册基础的播控回调后,播放时锁屏界面和状态栏会自动出现播放控制器。
方案描述
场景一:本地通知Notification
1. 基础类型通知。
除了基础类型通知外还包含长文本类型和多行文本类型,长文本和多行文本类型通知支持展开,通知对比如下:
方案:
发布携带图片的通知,支持横幅效果(横幅通知需在应用通知管理-提醒方式开启),通知3s(时间可自定义)后自动消除。
构造通知对象NotificationRequest:
a.选择文本类型通知,在normal下主要配置标题title和文本text即可。
b.通知携带图片需通过resourceManager.getMediaContent获取到media文件夹下图片资源后转为PixelMap,然后传递至largeIcon。
c.横幅通知,三方应用仅支持通过通知渠道SlotType设置,只有社交通信和服务提醒支持通知横幅效果且需在设置中开启横幅通知样式,各类SlotType详见通知渠道说明。
d.通知的定时发送与消除:Notification暂无定时发送能力,需依赖代理提醒实现,定时消除可通过autoDeletedTime设置通知消除时间。
e.点击通知拉起应用详见下文第三部分意图通知。
核心代码:
将media下资源图片转化为PixelMap对象。
let resourceManager = getContext().resourceManager;
let imageArray = await resourceManager.getMediaContent($r('app.media.icon').id);
let imageResource = image.createImageSource(imageArray.buffer);
let imagePixelMap = await imageResource.createPixelMap();
构造NotificationRequest对象。
let notificationRequest: notificationManager.NotificationRequest = {
id: 1,
largeIcon: imagePixelMap,
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, // 长文本类型通知
normal: {
title: '图片类型通知',
text: '3s后通知自动消除',
}
},
badgeNumber: 1, //通知角标
autoDeletedTime: Date.now() + 3000, //3s后自动消除
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION
};
发布通知。
notificationManager.publish(notificationRequest);
2. 进度条通知。
方案:
文件下载时实现实时进度通知,下载完成后点击通知预览下载文件。
a.构造NotificationRequest对象和进度条模板downloadTemplate,模板中包含三个字段标题title、下载文件名fileName、下载进度progress。
b.downloadFile下载pdf文件,使用downloadTask.on监听任务下载进度,根据“当前完成文件下载大小/文件总大小”得到下载进度,传入通知进度条模板中,实时更新应用下载进度。
c.应用下载完成后点击通知拉起应用预览下载文件,该能力实现详见下文第三部分意图通知。
核心代码:
构造NotificationRequest对象。
let notificationRequest: notificationManager.NotificationRequest = {
id: 5,
largeIcon: imagePixelMap,
wantAgent: wantAgent,
badgeNumber: 1,
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, //社交类型通知
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: 'test_title',
text: 'test_text',
}
},
//构造进度条模板,name字段当前需要固定配置为downloadTemplate
template: {
name: 'downloadTemplate',
data: { title: 'PDF文件下载', fileName: 'test.pdf下载完成:' + progress + '%', progressValue: progress }
},
}
下载文件并开启下载任务进度订阅。
let context = getContext() as common.UIAbilityContext;
let filesDir = context.filesDir;
request.downloadFile(context, {
url: 'https://xxxx/xxxx.pdf',
filePath: filesDir + '/test.pdf'
}).then((downloadTask: request.DownloadTask) => {
let progressCallback = (receivedSize: number, totalSize: number) => {
publishNotification(Math.round(100 * receivedSize / totalSize))
};
downloadTask.on('progress', progressCallback)
})
3. 意图通知。
方案:
以创建拉起应用的通知为例:
a.将动作类型actionType设为START_ABILITY拉起一个有页面的ability;如是触发公共事件通知,则设为SEND_COMMON_EVENT,并根据action设置事件名。由于WantAgentInfo中的actionType不支持同时配置两个,所以只能选择发布拉起应用的通知或者携带公共事件的通知,不能实现点击通知后既拉起应用还触发公共事件。
b.构造NotificationRequest对象,如果希望点击通知后通知不消失,则配置tapDismissed为false。
核心代码:
拉起应用场景。
let wantAgent: _WantAgent;
let wantAgentInfo: WantAgent.WantAgentInfo = {
wants: [
{
bundleName: 'com.example.mynotificationservice',
abilityName: 'EntryAbility',
} as Want
],
actionType: WantAgent.OperationType.START_ABILITIES,
requestCode: 0,
wantAgentFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
WantAgent.getWantAgent(wantAgentInfo).then((data) => {
wantAgent = data;
})
发布公共事件场景。
let wantAgentInfo: WantAgent.WantAgentInfo = {
wants: [
{
action:'eventName' //事件名
} as Want
],
actionType: WantAgent.OperationType.SEND_COMMON_EVENT,
requestCode: 0,
wantAgentFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
构建通知对象NotificationRequest。
let notificationRequest: notificationManager.NotificationRequest = {
content: {
notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '含行为意图的通知',
text: '点击后拉起应用',
},
},
id: 6,
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION, //社交类型通知
label: 'TEST',
wantAgent: wantAgent,
tapDismissed: false, //通知是否自动清除
};
发布通知。
notificationManager.publish(notificationRequest)
场景二:云端推送服务push Kit
开通推送服务
1.AGC上配置签名证书指纹,参考手动签名。
2.登录AppGallery Connect网站,开通推送服务并配置Client ID,详见文档开通推送服务与配置Client ID。
发送消息通知:
1.获取Push Token,Push Token标识了每台设备上每个应用**,调用getToken()接口**向Push Kit服务端请求Token,获取到Token后,使用Push Token来推送消息。
2.调用notificationManager.requestEnableNotification开启应用通知权限。
3.应用服务端调用Push Kit服务端的REST API推送通知消息,请求示例详见开发步骤3。
发送测试:
1.左侧导航栏选择“推送服务“,推送通知(V3 Beta)下点击”添加推送通知。
2.配置getToken获取到的Token,然后填写消息内容,完成后点击“提交”即可在设备收到推送消息。
场景三、实况窗服务
开通实况窗参考:开通实况窗权益,使用本地实况窗也需要应用的bundleName在AppGallery Connect上开通实况窗服务,具体实现demo详见实况窗服务-即时配送。本场景以打车出行为例实现。
方案:
以打车出行场景为例创建实况窗模板,根据event选择应用场景,包含出行打车、外卖配送、航班等,然后配置LiveViewData详细信息,主要设置卡片形态(即状态栏和锁屏页面展示的)参数PrimaryData;除了基本的标题文本内容外,出行打车场景通常选择进度模板LAYOUT_TYPE_PROGRESS展示司机当前距离乘客位置。
1.构建LiveView对象,卡片模板分为三个部分分别为固定区、辅助区、扩展区,锁屏状态下如果展示的是胶囊通知,则只会显示固定区和辅助区内容。打车出行场景下,可以定义进度类型扩展区ProgressLayout,通过progress实现车辆与乘客距离的实时展示,实况窗卡片模板详见通用卡片模板。
点击卡片拉起应用需通过clickAction点击事件实现,WantAgent参数配置可以参考场景一第三条意图通知。
2.startLiveView创建实况窗,乘客打车发布订单后,预估接驾距离、时间,使用startLiveView发布实况窗。
3.updateLiveView更新实况窗,当司机接单后,实时更新汽车颜色、型号、车牌,与乘客距离等信息,锁屏状态下可以通过liveViewLockScreenAbilityName实时展示锁屏沉浸实况窗页面(需用户长按实况窗才会进入沉浸式场景),由于实况窗开发涉及权限申请,本文仅以liveViewLockScreenPicture图片类型锁屏沉浸实况窗为例展示本地实现效果,加载锁屏沉浸实况窗页面依赖LiveViewLockScreenExtensionAbility,可以在onSessionCreate生命周期中加载应用指定页面。
"extensionAbilities": [
{
"name": "LiveViewLockScreenExtAbility",
"srcEntry": "./ets/liveviewlockscreenextability/LiveViewLockScreenExtAbility.ets",
"description": "$string:LiveViewLockScreenExtAbility_desc",
"icon": "$media:layered_image",
"label": "$string:LiveViewLockScreenExtAbility_label",
"type": "liveViewLockScreen",
}
]
4.stopLiveView结束实况窗,司机到达后更新最后实况窗,提醒乘客到指定地点上车。
核心代码:
构建实况窗对象。
private static async buildDefaultView(): Promise<liveViewManager.LiveView> {
let resourceManager = getContext().resourceManager;
let imageArray = await resourceManager.getMediaContent($r('app.media.icon').id);
let imageResource = image.createImageSource(imageArray.buffer);
let imagePixelMap = await imageResource.createPixelMap();
return {
id: 0,
event: 'PICK_UP', //出行打车
sequence: 1,
liveViewData: {
primary: {
title: await LiveViewController.resourceManager
.getStringValue($r("app.string.Delivery_default_primary_title")),
content: [
{
text: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_default_primary_content1")),
textColor: LiveViewController.contentColor
},
{
text: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_default_primary_content2")),
textColor: LiveViewController.contentColor
}
],
// liveViewLockScreenPicture: 'delivery_4x.png',
// liveViewLockScreenAbilityName: 'LiveViewLockScreenExtAbility',
// liveViewLockScreenAbilityParameters: dataInfo,
keepTime: 15,
clickAction: await ContextUtil.buildWantAgent(),
extensionData: {
type: liveViewManager.ExtensionType.EXTENSION_TYPE_ICON,
pic: 'icon_merchant.png',
clickAction: await ContextUtil.buildWantAgent()
},
layoutData: {
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
title: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_default_layout_title")),
content: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_default_layout_content")),
underlineColor: LiveViewController.underLineColor,
descPic: 'coffee.png'
}
},
capsule: {
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1,
icon: 'capsule_store.png',
backgroundColor: LiveViewController.capsuleColor,
title: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_default_capsule_title")),
content: '...'
}
}
}
}
发布实况窗。
public async startLiveView(): Promise<void> {
this.defaultView = await LiveViewController.buildDefaultView();
try {
const result = await liveViewManager.startLiveView(this.defaultView);
} catch (e) {
const err: BusinessError = e as BusinessError;
}
}
更新实况窗。
public async updateLiveView(): Promise<boolean> {
try {
// live view is disabled or default view is not init
if (!LiveViewController.isLiveViewEnabled() || !this.defaultView) {
return false;
}
switch (this.defaultView.sequence) {
case LiveViewStatus.WAITING_TAKE_ORDER: //司机接单,距离2公里,4分钟
this.defaultView.liveViewData.primary.title = Constants.TAXI_AWAIT_TITLE;
this.defaultView.liveViewData.primary.content = [
{
text: Constants.TAXI_AWAIT_TEXT1,
textColor: LiveViewController.contentColor
},
];
this.defaultView.liveViewData.primary.liveViewLockScreenPicture = 'icon_car1.jpg';
this.defaultView.liveViewData.primary.layoutData = {
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PROGRESS,
progress: 0,
lineType: liveViewManager.LineType.LINE_TYPE_DOTTED_LINE,
indicatorType: liveViewManager.IndicatorType.INDICATOR_TYPE_UP,
indicatorIcon: 'icon_taxi.png',
nodeIcons: [
'icon_order.png',
'icon_store_white.png',
'icon_finish.png'
]
};
this.defaultView.liveViewData.capsule = {
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1,
icon: 'capsule_store.png',
backgroundColor: LiveViewController.capsuleColor,
title: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_merchant_capsule_title"))
}
break;
case LiveViewStatus.WAITING_COUNTDOWN4: //司机接单,距离1.5公里,3分钟
break;
case LiveViewStatus.WAITING_COUNTDOWN3: //司机接单,距离1公里,2分钟
break;
case LiveViewStatus.WAITING_COUNTDOWN2: //司机接单,距离0.5公里,1分钟
break;
case LiveViewStatus.WAITING_COUNTDOWN1: //司机接单,已到达
break;
case LiveViewStatus.WAITING_ARRIVED:
await this.stopLiveView();
return false;
default:
await this.stopLiveView();
return false;
}
this.defaultView.sequence += 1;
const result = await liveViewManager.updateLiveView(this.defaultView);
return true;
} catch (e) {
const err: BusinessError = e as BusinessError;
Logger.error('Request updateLiveView error: %{public}s', err.message);
return false;
}
}
结束实况窗。
public async stopLiveView(): Promise<void> {
try {
// live view is disabled or default view is not init
if (!LiveViewController.isLiveViewEnabled() || !this.defaultView) {
Logger.warn('stopLiveView, live view is disabled.')
return;
}
Logger.info('stopLiveView, get active live view succeed.');
// stop live view
if (this.defaultView.sequence) {
this.defaultView.sequence += 1;
}
this.defaultView.liveViewData.primary.title = Constants.TAXI_ARRIVED_TITLE;
this.defaultView.liveViewData.primary.content = [
{
text: Constants.TAXI_ARRIVED_TEXT1,
textColor: LiveViewController.contentColor
},
];
this.defaultView.liveViewData.primary.layoutData = {
layoutType: liveViewManager.LayoutType.LAYOUT_TYPE_PICKUP,
title: '白色*问界M5',
content: '牌123456',
descPic: 'icon_car.png'
}
this.defaultView.liveViewData.capsule = {
type: liveViewManager.CapsuleType.CAPSULE_TYPE_TEXT,
status: 1,
icon: 'capsule_gps.png',
backgroundColor: LiveViewController.capsuleColor,
title: await LiveViewController.resourceManager.getStringValue($r("app.string.Delivery_delivered_capsule_title"))
}
Logger.info('Request stopLiveView req: %{public}s', JSON.stringify(this.defaultView));
const result = await liveViewManager.stopLiveView(this.defaultView);
Logger.info('Request stopLiveView result: %{public}s', JSON.stringify(result));
} catch (e) {
const err: BusinessError = e as BusinessError;
Logger.error('Request stopLiveView error: %{public}s', err.message);
}
}
场景四、锁屏界面播放器管理
方案:
调用avSession.createAVSession创建audio或video类型的AVSession,然后setAVMetadata设置相关媒体信息,setAVPlaybackState设置各种播放状态,当应用进入播放状态时,系统会自动发送通知,并在状态栏和锁屏界面展示,底层基于实况窗实现。具体步骤参考播控接入指南,demo详见媒体会话。
其它常见问题
1.notificationManager.requestEnableNotification未设置参数时,打开的通知权限弹窗不会跟随应用,如需权限弹窗跟随应用需设置模态弹窗,绑定Ability上下文。
2.发布携带表情通知,参考实现文档ArkUI是否支持emoji表情输入。
总结全面
必须顶起