HarmonyOS开发:Java应用


JAVA API

《Java API参考》

开发概述

通用开发历程

各类设备(手机/平板、智能穿戴、智慧屏等)通用的用户应用程序开发历程如下表所示。

任务 简介 相关资源
开发Ability Ability是HarmonyOS应用程序的重要组成部分,分为FA(Feature Ability)和PA(Particle Ability)两种类型:FA支持Page Ability:Page模板是FA唯一支持的模板,用于提供与用户交互的能力。PA支持Service Ability和Data Ability:Service模板用于提供后台运行任务的能力;Data模板用于对外部提供统一的数据访问抽象。进行HarmonyOS应用开发,首先要了解Ability如何使用。 Page AbilityService AbilityData Ability
开发UI FA需要提供UI用于与用户进行交互,HarmonyOS提供了Java UI和JS UI两种UI框架:Java UI提供了细粒度的UI编程接口,使应用开发更加灵活;JS UI提供了相对高层的UI描述,使应用开发更加简单。说明针对轻量级智能穿戴(Lite Wearable),现阶段只使用JS语言进行应用开发,详见轻量级智能穿戴开发 Java UI框架Java API参考JS UI框架JS API参考
开发业务功能 媒体:视频、音频、图像、相机等功能的开发。安全:权限、生物特征识别等功能的开发。AI:图像超分、语音识别、码生成等功能的开发。网络连接:NFC、蓝牙、WLAN等功能的开发。设备管理:传感器、控制类小器件、位置等功能的开发。数据管理:数据库、分布式数据/文件服务、数据搜索等功能的开发。线程:线程管理、线程间通信等功能的开发。IDL:声明系统服务和Ability对外提供的服务接口,并生成相关代码。 媒体开发指南安全开发指南AI开发指南网络与连接开发指南设备管理开发指南数据管理开发指南线程开发指南IDL接口使用指南

原子化服务开发历程

HarmonyOS除支持传统方式的需要安装的应用外,还支持提供特定功能的免安装的应用(即原子化服务),供用户在合适的场景、合适的设备上便捷使用。

原子化服务相对于传统方式的需要安装的应用更加轻量,同时提供更丰富的入口、更精准的分发。原子化服务的详细介绍请参见“原子化服务”。

其基本开发历程如下表所示。

任务 开发历程 相关内容
了解HarmonyOS 了解HarmonyOS的系统定位、技术特性、应用开发的基本概念和基础知识,熟悉HarmonyOS应用开发通用流程。 HarmonyOS概述开发基础知识快速入门
设计原子化服务 在设计阶段,需要满足原子化服务的设计规范,包括图标、卡片、分布式等规范。 原子化服务设计
掌握原子化服务约束 了解原子化服务之间的调用管控机制。 三方应用调用管控机制
开发原子化服务基础体验 了解原子化服务总体开发规则、如何开发服务卡片等基础体验。 原子化服务总体开发要求服务卡片开发指南
开发原子化服务分布式体验 了解如何开发流转、分享等分布式体验。 流转开发指南华为分享接入指南

Ability

Ability概述

Ability是应用所具备能力的抽象,也是应用程序的重要组成部分。一个应用可以具备多种能力(即可以包含多个Ability),HarmonyOS支持应用以Ability为单位进行部署。Ability可以分为FA(Feature Ability)和PA(Particle Ability)两种类型,每种类型为开发者提供了不同的模板,以便实现不同的业务功能。

  • FA支持Page Ability:Page模板是FA唯一支持的模板,用于提供与用户交互的能力。一个Page实例可以包含一组相关页面,每个页面用一个AbilitySlice实例表示。

  • PA支持

    Service Ability

    Data Ability

  • Service模板:用于提供后台运行任务的能力。
  • Data模板:用于对外部提供统一的数据访问抽象。

配置文件(config.json)中注册Ability时,可以通过配置Ability元素中的“type”属性来指定Ability模板类型,示例如下。

其中,“type”的取值可以为“page”、“service”或“data”,分别代表Page模板、Service模板、Data模板。为了便于表述,后文中我们将基于Page模板、Service模板、Data模板实现的Ability分别简称为Page、Service、Data。

{
    "module": {
        ...
        "abilities": [
            {
                ...
                "type": "page"
                ...
            }
        ]
        ...
    }
    ...
}

相关实例

针对Ability开发,有以下Codelabs可供参考:

  • 分布式亲子早教系统基于分布式能力,实现一个多屏互动、跨设备协同的亲子早教系统。

  • 分布式新闻客户端基于HarmonyOS应用中Service Ability和Page Ability的使用,实现跨设备FA拉起。

  • 跨设备视频播放基于分布式能力和IDL跨进程通信,实现视频跨设备播放、控制。

  • 分布式输入法基于分布式能力,将手机作为智慧屏的虚拟控制器,控制文字输入和遥控播放。

  • 分布式游戏手柄基于分布式能力,将手机作为智慧屏的虚拟手柄终端,组成全新的多人娱乐场景

Page Ability

Page与AbilitySlice

Page模板(以下简称“Page”)是FA唯一支持的模板,用于提供与用户交互的能力。一个Page可以由一个或多个AbilitySlice构成,AbilitySlice是指应用的单个页面及其控制逻辑的总和。

当一个Page由多个AbilitySlice共同构成时,这些AbilitySlice页面提供的业务能力应具有高度相关性。例如,新闻浏览功能可以通过一个Page来实现,其中包含了两个AbilitySlice:一个AbilitySlice用于展示新闻列表,另一个AbilitySlice用于展示新闻详情。Page和AbilitySlice的关系如图所示。

相比于桌面场景,移动场景下应用之间的交互更为频繁。通常,单个应用专注于某个方面的能力开发,当它需要其他能力辅助时,会调用其他应用提供的能力。例如,外卖应用提供了联系商家的业务功能入口,当用户在使用该功能时,会跳转到通话应用的拨号页面。与此类似,HarmonyOS支持不同Page之间的跳转,并可以指定跳转到目标Page中某个具体的AbilitySlice。

AbilitySlice路由配置

虽然一个Page可以包含多个AbilitySlice,但是Page进入前台时界面默认只展示一个AbilitySlice。默认展示的AbilitySlice是通过setMainRoute()方法来指定的。如果需要更改默认展示的AbilitySlice,可以通过addActionRoute()方法为此AbilitySlice配置一条路由规则。此时,当其他Page实例期望导航到此AbilitySlice时,可以在Intent中指定Action,详见不同Page间导航

setMainRoute()方法与addActionRoute()方法的使用示例如下:

public class MyAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // set the main route
        setMainRoute(MainSlice.class.getName());

        // set the action route
        addActionRoute("action.pay", PaySlice.class.getName());
        addActionRoute("action.scan", ScanSlice.class.getName());
    }
}

addActionRoute()方法中使用的动作命名,需要在应用配置文件(config.json)中注册:

{
    "module": {
        "abilities": [
            {
                "skills":[
                    {
                        "actions":[
                            "action.pay",
                            "action.scan"
                        ]
                    }
                ]
                ...
            }
        ]
        ...
    }
    ...
}

Page Ability生命周期

系统管理或用户操作等行为均会引起Page实例在其生命周期的不同状态之间进行转换。Ability类提供的回调机制能够让Page及时感知外界变化,从而正确地应对状态变化(比如释放资源),这有助于提升应用的性能和稳健性。

Page生命周期回调

Page生命周期的不同状态转换及其对应的回调,如图所示。

  • onStart()当系统首次创建Page实例时,触发该回调。对于一个Page实例,该回调在其生命周期过程中仅触发一次,Page在该逻辑后将进入INACTIVE状态。开发者必须重写该方法,并在此配置默认展示的AbilitySlice。
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(FooSlice.class.getName());
    }
  • onActive()Page会在进入INACTIVE状态后来到前台,然后系统调用此回调。Page在此之后进入ACTIVE状态,该状态是应用与用户交互的状态。Page将保持在此状态,除非某类事件发生导致Page失去焦点,比如用户点击返回键或导航到其他Page。当此类事件发生时,会触发Page回到INACTIVE状态,系统将调用onInactive()回调。此后,Page可能重新回到ACTIVE状态,系统将再次调用onActive()回调。因此,开发者通常需要成对实现onActive()和onInactive(),并在onActive()中获取在onInactive()中被释放的资源。

  • onInactive()当Page失去焦点时,系统将调用此回调,此后Page进入INACTIVE状态。开发者可以在此回调中实现Page失去焦点时应表现的恰当行为。

  • onBackground()如果Page不再对用户可见,系统将调用此回调通知开发者用户进行相应的资源释放,此后Page进入BACKGROUND状态。开发者应该在此回调中释放Page不可见时无用的资源,或在此回调中执行较为耗时的状态保存操作。

  • onForeground()处于BACKGROUND状态的Page仍然驻留在内存中,当重新回到前台时(比如用户重新导航到此Page),系统将先调用onForeground()回调通知开发者,而后Page的生命周期状态回到INACTIVE状态。开发者应当在此回调中重新申请在onBackground()中释放的资源,最后Page的生命周期状态进一步回到ACTIVE状态,系统将通过onActive()回调通知开发者用户。

  • onStop()

    系统将要销毁Page时,将会触发此回调函数,通知用户进行系统资源的释放。销毁Page的可能原因包括以下几个方面:

    • 用户通过系统管理能力关闭指定Page,例如使用任务管理器关闭Page。
  • 用户行为触发Page的terminateAbility()方法调用,例如使用应用的退出功能。

  • 配置变更导致系统暂时销毁Page并重建。

  • 系统出于资源管理目的,自动触发对处于BACKGROUND状态Page的销毁。

AbilitySlice生命周期

AbilitySlice作为Page的组成单元,其生命周期是依托于其所属Page生命周期的。AbilitySlice和Page具有相同的生命周期状态和同名的回调,当Page生命周期发生变化时,它的AbilitySlice也会发生相同的生命周期变化。此外,AbilitySlice还具有独立于Page的生命周期变化,这发生在同一Page中的AbilitySlice之间导航时,此时Page的生命周期状态不会改变。

AbilitySlice生命周期回调与Page的相应回调类似,因此不再赘述。由于AbilitySlice承载具体的页面,开发者必须重写AbilitySlice的onStart()回调,并在此方法中通过setUIContent()方法设置页面,如下所示:

    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);

        setUIContent(ResourceTable.Layout_main_layout);
    }

AbilitySlice实例创建和管理通常由应用负责,系统仅在特定情况下会创建AbilitySlice实例。例如,通过导航启动某个AbilitySlice时,是由系统负责实例化;但是在同一个Page中不同的AbilitySlice间导航时则由应用负责实例化。

Page与AbilitySlice生命周期关联

当AbilitySlice处于前台且具有焦点时,其生命周期状态随着所属Page的生命周期状态的变化而变化。当一个Page拥有多个AbilitySlice时,例如:MyAbility下有FooAbilitySlice和BarAbilitySlice,当前FooAbilitySlice处于前台并获得焦点,并即将导航到BarAbilitySlice,在此期间的生命周期状态变化顺序为:

  1. FooAbilitySlice从ACTIVE状态变为INACTIVE状态。
  2. BarAbilitySlice则从INITIAL状态首先变为INACTIVE状态,然后变为ACTIVE状态(假定此前BarAbilitySlice未曾启动)。
  3. FooAbilitySlice从INACTIVE状态变为BACKGROUND状态。

对应两个slice的生命周期方法回调顺序为:

FooAbilitySlice.onInactive() --> BarAbilitySlice.onStart() --> BarAbilitySlice.onActive() --> FooAbilitySlice.onBackground()

在整个流程中,MyAbility始终处于ACTIVE状态。但是,当Page被系统销毁时,其所有已实例化的AbilitySlice将联动销毁,而不仅是处于前台的AbilitySlice

AbilitySlice间导航

同一Page内导航

当发起导航的AbilitySlice和导航目标的AbilitySlice处于同一个Page时,您可以通过present()方法实现导航。如下代码片段展示通过点击按钮导航到其他AbilitySlice的方法:

@Override
protected void onStart(Intent intent) {

    ...
    Button button = ...;
    button.setClickedListener(listener -> present(new TargetSlice(), new Intent()));
    ...

}

如果开发者希望在用户从导航目标AbilitySlice返回时,能够获得其返回结果,则应当使用presentForResult()实现导航。用户从导航目标AbilitySlice返回时,系统将回调onResult()来接收和处理返回结果,开发者需要重写该方法。返回结果由导航目标AbilitySlice在其生命周期内通过setResult()进行设置。

@Override
protected void onStart(Intent intent) {

    ...
    Button button = ...;
    button.setClickedListener(listener -> presentForResult(new TargetSlice(), new Intent(), 0));
    ...

}

@Override
protected void onResult(int requestCode, Intent resultIntent) {
    if (requestCode == 0) {
        // Process resultIntent here.
    }
}

系统为每个Page维护了一个AbilitySlice实例的栈,每个进入前台的AbilitySlice实例均会入栈。当开发者在调用present()或presentForResult()时指定的AbilitySlice实例已经在栈中存在时,则栈中位于此实例之上的AbilitySlice均会出栈并终止其生命周期。前面的示例代码中,导航时指定的AbilitySlice实例均是新建的,即便重复执行此代码(此时作为导航目标的这些实例是同一个类),也不会导致任何AbilitySlice出栈。

不同Page间导航

AbilitySlice作为Page的内部单元,以Action的形式对外暴露,因此可以通过配置Intent的Action导航到目标AbilitySlice。Page间的导航可以使用startAbility()或startAbilityForResult()方法,获得返回结果的回调为onAbilityResult()。在Ability中调用setResult()可以设置返回结果。详细用法可参考根据Operation的其他属性启动应用中的示例

跨设备迁移

跨设备迁移(下文简称“迁移”)支持将Page在同一用户的不同设备间迁移,以便支持用户无缝切换的诉求。以Page从设备A迁移到设备B为例,迁移动作主要步骤如下:

  1. 设备A上的Page请求迁移。
  2. HarmonyOS处理迁移任务,并回调设备A上Page的保存数据方法,用于保存迁移必须的数据。
  3. HarmonyOS在设备B上启动同一个Page,并回调其恢复数据方法。

开发者可以参考以下详细步骤开发具有迁移功能的Page。

实现IAbilityContinuation接口

说明

一个应用可能包含多个Page,仅需要在支持迁移的Page中通过以下方法实现IAbilityContinuation接口。同时,此Page所包含的所有AbilitySlice也需要实现此接口。

  • onStartContinuation()Page请求迁移后,系统首先回调此方法,开发者可以在此回调中决策当前是否可以执行迁移,比如,弹框让用户确认是否开始迁移。

  • onSaveData()如果onStartContinuation()返回true,则系统回调此方法,开发者在此回调中保存必须传递到另外设备上以便恢复Page状态的数据。

  • onRestoreData()源侧设备上Page完成保存数据后,系统在目标侧设备上回调此方法,开发者在此回调中接受用于恢复Page状态的数据。注意,在目标侧设备上的Page会重新启动其生命周期,无论其启动模式如何配置。且系统回调此方法的时机在onStart()之前。

  • onCompleteContinuation()目标侧设备上恢复数据一旦完成,系统就会在源侧设备上回调Page的此方法,以便通知应用迁移流程已结束。开发者可以在此检查迁移结果是否成功,并在此处理迁移结束的动作,例如,应用可以在迁移完成后终止自身生命周期。

  • onRemoteTerminated()如果开发者使用continueAbilityReversibly()而不是continueAbility(),则此后可以在源侧设备上使用reverseContinueAbility()进行回迁。这种场景下,相当于同一个Page(的两个实例)同时在两个设备上运行,迁移完成后,如果目标侧设备上Page因任何原因终止,则源侧Page通过此回调接收终止通知。

请求迁移

实现IAbilityContinuation的Page可以在其生命周期内,调用continueAbility()或continueAbilityReversibly()请求迁移。两者的区别是,通过后者发起的迁移此后可以进行回迁。

try {
    continueAbility();
} catch (IllegalStateException e) {
    // Maybe another continuation in progress.
    ...
}

以Page从设备A迁移到设备B为例,详细的流程如下:

  1. 设备A上的Page请求迁移。
  2. 系统回调设备A上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onStartContinuation()方法,以确认当前是否可以立即迁移。
  3. 如果可以立即迁移,则系统回调设备A上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onSaveData()方法,以便保存迁移后恢复状态必须的数据。
  4. 如果保存数据成功,则系统在设备B上启动同一个Page,并恢复AbilitySlice栈,然后回调IAbilityContinuation.onRestoreData()方法,传递此前保存的数据;此后设备B上此Page从onStart()开始其生命周期回调。
  5. 系统回调设备A上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onCompleteContinuation()方法,通知数据恢复成功与否。

请求回迁

使用continueAbilityReversibly()请求迁移并完成后,源侧设备上已迁移的Page可以发起回迁,以便使用户活动重新回到此设备。

try {
    reverseContinueAbility();
} catch (IllegalStateException e) {
    // Maybe another continuation in progress.
    ...
}

以Page从设备A迁移到设备B后并请求回迁为例,详细的流程如下:

  1. 设备A上的Page请求回迁。
  2. 系统回调设备B上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onStartContinuation()方法,以确认当前是否可以立即迁移。
  3. 如果可以立即迁移,则系统回调设备B上Page及其AbilitySlice栈中所有AbilitySlice实例的IAbilityContinuation.onSaveData()方法,以便保存回迁后恢复状态必须的数据。
  4. 如果保存数据成功,则系统在设备A上Page恢复AbilitySlice栈,然后回调IAbilityContinuation.onRestoreData()方法,传递此前保存的数据。
  5. 如果数据恢复成功,则系统终止设备B上Page的生命周期。

相关实例

针对Page Ability开发,有以下示例工程可供参考:

  • PageAbility本示例演示了同一Page Ability内和不同Page间的AbilitySlice导航,以及Page Ability的跨端迁移。

针对跨设备迁移开发,有以下Codelabs可供参考:

  • 分布式邮件编辑基于跨设备迁移和分布式文件能力,实现邮件的跨设备编辑和附件的调用

Service Ability

基于Service模板的Ability(以下简称“Service”)主要用于后台运行任务(如执行音乐播放、文件下载等),但不提供用户交互界面。Service可由其他应用或Ability启动,即使用户切换到其他应用,Service仍将在后台继续运行。

Service是单实例的。在一个设备上,相同的Service只会存在一个实例。如果多个Ability共用这个实例,只有当与Service绑定的所有Ability都退出后,Service才能够退出。由于Service是在主线程里执行的,因此,如果在Service里面的操作时间过长,开发者必须在Service里创建新的线程来处理(详见线程间通信),防止造成主线程阻塞,应用程序无响应。

创建Service

介绍如何创建一个Service。

  1. 创建Ability的子类,实现Service相关的生命周期方法。Service也是一种Ability,Ability为Service提供了以下生命周期方法,用户可以重写这些方法,来添加其他Ability请求与Service Ability交互时的处理方法。
  • onStart()该方法在创建Service的时候调用,用于Service的初始化。在Service的整个生命周期只会调用一次,调用时传入的Intent应为空。

  • onCommand()在Service创建完成之后调用,该方法在客户端每次启动该Service时都会调用,用户可以在该方法中做一些调用统计、初始化类的操作。

  • onConnect()在Ability和Service连接时调用,该方法返回IRemoteObject对象,用户可以在该回调函数中生成对应Service的IPC通信通道,以便Ability与Service交互。Ability可以多次连接同一个Service,系统会缓存该Service的IPC通信对象,只有第一个客户端连接Service时,系统才会调用Service的onConnect方法来生成IRemoteObject对象,而后系统会将同一个RemoteObject对象传递至其他连接同一个Service的所有客户端,而无需再次调用onConnect方法。

  • onDisconnect()在Ability与绑定的Service断开连接时调用。

  • onStop()在Service销毁时调用。Service应通过实现此方法来清理任何资源,如关闭线程、注册的侦听器等。

创建Service的代码示例如下:

public class ServiceAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
    }

    @Override
    public void onCommand(Intent intent, boolean restart, int startId) {
        super.onCommand(intent, restart, startId);
    }

    @Override
    public IRemoteObject onConnect(Intent intent) {
        return super.onConnect(intent);
    }

    @Override
    public void onDisconnect(Intent intent) {
        super.onDisconnect(intent);
    }

    @Override
    public void onStop() {
        super.onStop();
    }
}
  1. 注册Service。Service也需要在应用配置文件中进行注册,注册类型type需要设置为service。
{
    "module": {
        "abilities": [         
            {    
                "name": ".ServiceAbility",
                "type": "service",
                "visible": true
                ...
            }
        ]
        ...
    }
    ...
}

启动Service

介绍通过startAbility()启动Service以及对应的停止方法。

  • 启动ServiceAbility为开发者提供了startAbility()方法来启动另外一个Ability。因为Service也是Ability的一种,开发者同样可以通过将Intent传递给该方法来启动Service。不仅支持启动本地Service,还支持启动远程Service。

开发者可以通过构造包含DeviceId、BundleName与AbilityName的Operation对象来设置目标Service信息。这三个参数的含义如下:

  • DeviceId:表示设备ID。如果是本地设备,则可以直接留空;如果是远程设备,可以通过ohos.distributedschedule.interwork.DeviceManager提供的getDeviceList获取设备列表,详见《API参考》。

  • BundleName:表示包名称。

  • AbilityName:表示待启动的Ability名称。

启动本地设备Service的代码示例如下:

Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("")
        .withBundleName("com.domainname.hiworld.himusic")
        .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
        .build();
intent.setOperation(operation);
startAbility(intent);

启动远程设备Service的代码示例如下:

Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("deviceId")
        .withBundleName("com.domainname.hiworld.himusic")
        .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
        .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 设置支持分布式调度系统多设备启动的标识
        .build();
intent.setOperation(operation);
startAbility(intent);

执行上述代码后,Ability将通过startAbility() 方法来启动Service。

  • 如果Service尚未运行,则系统会先调用onStart()来初始化Service,再回调Service的onCommand()方法来启动Service。

  • 如果Service正在运行,则系统会直接回调Service的onCommand()方法来启动Service。

  • 停止ServiceService一旦创建就会一直保持在后台运行,除非必须回收内存资源,否则系统不会停止或销毁Service。开发者可以在Service中通过terminateAbility()停止本Service或在其他Ability调用stopAbility()来停止Service。

停止Service同样支持停止本地设备Service和停止远程设备Service,使用方法与启动Service一样。一旦调用停止Service的方法,系统便会尽快销毁Service

连接Service

如果Service需要与Page Ability或其他应用的Service Ability进行交互,则须创建用于连接的Connection。Service支持其他Ability通过connectAbility()方法与其进行连接。

在使用connectAbility()处理回调时,需要传入目标Service的Intent与IAbilityConnection的实例。IAbilityConnection提供了两个方法供开发者实现:onAbilityConnectDone()是用来处理连接Service成功的回调,onAbilityDisconnectDone()是用来处理Service异常死亡的回调。

创建连接Service回调实例的代码示例如下:

// 创建连接Service回调实例
private IAbilityConnection connection = new IAbilityConnection() {
    // 连接到Service的回调
    @Override
    public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) {
        // Client侧需要定义与Service侧相同的IRemoteObject实现类。开发者获取服务端传过来IRemoteObject对象,并从中解析出服务端传过来的信息。
    }

    // Service异常死亡的回调
    @Override
    public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
    }
};

连接Service的代码示例如下:

// 连接Service
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("deviceId")
        .withBundleName("com.domainname.hiworld.himusic")
        .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
        .build();
intent.setOperation(operation);
connectAbility(intent, connection);

同时,Service侧也需要在onConnect()时返回IRemoteObject,从而定义与Service进行通信的接口。onConnect()需要返回一个IRemoteObject对象,HarmonyOS提供了IRemoteObject的默认实现,用户可以通过继承LocalRemoteObject来创建自定义的实现类。Service侧把自身的实例返回给调用侧的代码示例如下:

// 创建自定义IRemoteObject实现类
private class MyRemoteObject extends LocalRemoteObject {
    MyRemoteObject(){
    }
}

// 把IRemoteObject返回给客户端
@Override
protected IRemoteObject onConnect(Intent intent) {
    return new MyRemoteObject();
}
相关实例

针对Service Ability开发,有以下示例工程可供参考:

  • ServiceAbility本示例演示了Service Ability的启动、停止、连接、断开连接等操作,支持对跨设备的Service Ability进行操作

Service Ability生命周期

与Page类似,Service也拥有生命周期,如图所示。根据调用方法的不同,其生命周期有以下两种路径:

  • 启动Service该Service在其他Ability调用startAbility()时创建,然后保持运行。其他Ability通过调用stopAbility()来停止Service,Service停止后,系统会将其销毁。

  • 连接Service该Service在其他Ability调用connectAbility()时创建,客户端可通过调用disconnectAbility()断开连接。多个客户端可以绑定到相同Service,而且当所有绑定全部取消后,系统即会销毁该Service。

前台Service

一般情况下,Service都是在后台运行的,后台Service的优先级都是比较低的,当资源不足时,系统有可能回收正在运行的后台Service。

在一些场景下(如播放音乐),用户希望应用能够一直保持运行,此时就需要使用前台Service。前台Service会始终保持正在运行的图标在系统状态栏显示。

使用前台Service并不复杂,开发者只需在Service创建的方法里,调用keepBackgroundRunning()将Service与通知绑定。调用keepBackgroundRunning()方法前需要在配置文件中声明ohos.permission.KEEP_BACKGROUND_RUNNING权限,同时还需要在配置文件中添加对应的backgroundModes参数。在onStop()方法中调用cancelBackgroundRunning()方法可停止前台Service。

使用前台Service的onStart()代码示例如下:

// 创建通知,其中1005为notificationId
NotificationRequest request = new NotificationRequest(1005);
NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
content.setTitle("title").setText("text");
NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
request.setContent(notificationContent);

// 绑定通知,1005为创建通知时传入的notificationId
keepBackgroundRunning(1005, request);

在配置文件中,“module > abilities”字段下对当前Service做如下配置:

{    
    "name": ".ServiceAbility",
    "type": "service",
    "visible": true,
    "backgroundModes": ["dataTransfer", "location"]
}

Data Ability

使用Data模板的Ability(以下简称“Data”)有助于应用管理其自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。Data既可用于同设备不同应用的数据共享,也支持跨设备不同应用的数据共享。

数据的存放形式多样,可以是数据库,也可以是磁盘上的文件。Data对外提供对数据的增、删、改、查,以及打开文件等接口,这些接口的具体实现由开发者提供。

URI介绍

Data的提供方和使用方都通过URI(Uniform Resource Identifier)来标识一个具体的数据,例如数据库中的某个表或磁盘上的某个文件。HarmonyOS的URI仍基于URI通用标准,格式如下:

  • scheme:协议方案名,固定为“dataability”,代表Data Ability所使用的协议类型。

  • authority:设备ID。如果为跨设备场景,则为目标设备的ID;如果为本地设备场景,则不需要填写。

  • path:资源的路径信息,代表特定资源的位置信息。

  • query:查询参数。

  • fragment:可以用于指示要访问的子资源。

URI示例:

  • 跨设备场景:dataability://device_id/com.domainname.dataability.persondata/person/10

  • 本地设备:dataability:///com.domainname.dataability.persondata/person/10

创建Data

使用Data模板的Ability形式仍然是Ability,因此,开发者需要为应用添加一个或多个Ability的子类,来提供程序与其他应用之间的接口。Data为结构化数据和文件提供了不同API接口供用户使用,因此,开发者需要首先确定好使用何种类型的数据。本章节主要讲述了创建Data的基本步骤和需要使用的接口。

Data提供方可以自定义数据的增、删、改、查,以及文件打开等功能,并对外提供这些接口。

确定数据存储方式

确定数据的存储方式,Data支持以下两种数据形式:

  • 文件数据:如文本、图片、音乐等。

  • 结构化数据:如数据库等。

实现UserDataAbility

UserDataAbility用于接收其他应用发送的请求,提供外部程序访问的入口,从而实现应用间的数据访问。

实现UserDataAbility,需要在“Project”窗口当前工程的主目录(“entry > src > main > java > com.xxx.xxx”)选择“File > New > Ability > Empty Data Ability”,设置“Data Name”后完成UserDataAbility的创建。

Data提供了文件存储和数据库存储两组接口供用户使用。

文件存储

开发者需要在Data中重写FileDescriptor openFile(Uri uri, String mode)方法来操作文件:uri为客户端传入的请求目标路径;mode为开发者对文件的操作选项,可选方式包含“r”(读), “w”(写), “rw”(读写)等。

ohos.rpc.MessageParcel类提供了一个静态方法,用于获取MessageParcel实例。开发者可通过获取到的MessageParcel实例,使用dupFileDescriptor()函数复制待操作文件流的文件描述符,并将其返回,供远端应用访问文件。

示例:根据传入的uri打开对应的文件

private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log");

@Override
public FileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    // 创建messageParcel
    MessageParcel messageParcel = MessageParcel.obtain();
    File file = new File(uri.getDecodedPathList().get(0)); //get(0)是获取URI完整字段中查询参数字段。
    if (mode == null || !"rw".equals(mode)) {
        file.setReadOnly();
    }
    FileInputStream fileIs = new FileInputStream(file);
    FileDescriptor fd = null;
    try {
        fd = fileIs.getFD();
    } catch (IOException e) {
        HiLog.info(LABEL_LOG, "failed to getFD");
    }

    // 绑定文件描述符
    return messageParcel.dupFileDescriptor(fd);
}

数据库存储

  1. 初始化数据库连接。系统会在应用启动时调用onStart()方法创建Data实例。在此方法中,开发者应该创建数据库连接,并获取连接对象,以便后续和数据库进行操作。为了避免影响应用启动速度,开发者应当尽可能将非必要的耗时任务推迟到使用时执行,而不是在此方法中执行所有初始化。

示例:初始化的时候连接数据库

private static final String DATABASE_NAME = "UserDataAbility.db";
private static final String DATABASE_NAME_ALIAS = "UserDataAbility";
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log");
private OrmContext ormContext = null;

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    DatabaseHelper manager = new DatabaseHelper(this);
    ormContext = manager.getOrmContext(DATABASE_NAME_ALIAS, DATABASE_NAME, BookStore.class);
}
  1. 编写数据库操作方法。Ability定义了6个方法供用户处理对数据库表数据的增删改查。这6个方法在Ability中已默认实现,开发者可按需重写。
方法 描述
ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) 查询数据库
int insert(Uri uri, ValuesBucket value) 向数据库中插入单条数据
int batchInsert(Uri uri, ValuesBucket[] values) 向数据库中插入多条数据
int delete(Uri uri, DataAbilityPredicates predicates) 删除一条或多条数据
int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) 更新数据库
DataAbilityResult[] executeBatch(ArrayList operations) 批量操作数据库

这些方法的使用说明如下:

  • query()该方法接收三个参数,分别是查询的目标路径,查询的列名,以及查询条件,查询条件由类DataAbilityPredicates构建。根据传入的列名和查询条件查询用户表的代码示例如下:
public ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) {
    if (ormContext == null) {
        HiLog.error(LABEL_LOG, "failed to query, ormContext is null");
        return null;
    }

    // 查询数据库
    OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
    ResultSet resultSet = ormContext.query(ormPredicates, columns);
    if (resultSet == null) {
        HiLog.info(LABEL_LOG, "resultSet is null");
    }

    // 返回结果
    return resultSet;
}

insert()该方法接收两个参数,分别是插入的目标路径和插入的数据值。其中,插入的数据由ValuesBucket封装,服务端可以从该参数中解析出对应的属性,然后插入到数据库中。此方法返回一个int类型的值用于标识结果。接收到传过来的用户信息并把它保存到数据库中的代码示例如下:

public int insert(Uri uri, ValuesBucket value) {
    // 参数校验
    if (ormContext == null) {
        HiLog.error(LABEL_LOG, "failed to insert, ormContext is null");
        return -1;
    }

    // 构造插入数据
    User user = new User();
    user.setUserId(value.getInteger("userId"));
    user.setFirstName(value.getString("firstName"));
    user.setLastName(value.getString("lastName"));
    user.setAge(value.getInteger("age"));
    user.setBalance(value.getDouble("balance"));

    // 插入数据库
    boolean isSuccessful = ormContext.insert(user);
    if (!isSuccessful) {
        HiLog.error(LABEL_LOG, "failed to insert");
        return -1;
    }
    isSuccessful = ormContext.flush();
    if (!isSuccessful) {
        HiLog.error(LABEL_LOG, "failed to insert flush");
        return -1;
    }
    DataAbilityHelper.creator(this, uri).notifyChange(uri);
    int id = Math.toIntExact(user.getRowId());
    return id;
}
  • batchInsert()该方法为批量插入方法,接收一个ValuesBucket数组用于单次插入一组对象。它的作用是提高插入多条重复数据的效率。该方法系统已实现,开发者可以直接调用。

  • delete()该方法用来执行删除操作。删除条件由类DataAbilityPredicates构建,服务端在接收到该参数之后可以从中解析出要删除的数据,然后到数据库中执行。根据传入的条件删除用户表数据的代码示例如下:

public int delete(Uri uri, DataAbilityPredicates predicates) {
    if (ormContext == null) {
        HiLog.error(LABEL_LOG, "failed to delete, ormContext is null");
        return -1;
    }

    OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
    int value = ormContext.delete(ormPredicates);
    DataAbilityHelper.creator(this, uri).notifyChange(uri);
    return value;
}
  • update()此方法用来执行更新操作。用户可以在ValuesBucket参数中指定要更新的数据,在DataAbilityPredicates中构建更新的条件等。更新用户表的数据的代码示例如下:
public int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) {
    if (ormContext == null) {
       HiLog.error(LABEL_LOG, "failed to update, ormContext is null");
       return -1;
   }

   OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
   int index = ormContext.update(ormPredicates, value);
   HiLog.info(LABEL_LOG, "UserDataAbility update value:" + index);
   DataAbilityHelper.creator(this, uri).notifyChange(uri);
   return index;
}
  • executeBatch()此方法用来批量执行操作。DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,用户可自行设置自己要执行的数据库操作。该方法系统已实现,开发者可以直接调用。

说明

上述代码示例中,初始化了数据库类BookStore.class,并通过实体类User.class对该数据库的表User进行增删改查操作。

关于对象关系映射数据库的具体逻辑,以及示例中BookStore.class与User.class的逻辑关系,可参考“对象关系映射数据库开发指导”。

注册UserDataAbility

和Service类似,开发者必须在配置文件中注册Data。

配置文件中该字段在创建Data Ability时会自动创建,name与创建的Data Ability一致。

需要关注以下属性:

  • type: 类型设置为data

  • uri: 对外提供的访问路径,全局唯一

  • permissions: 访问该data ability时需要申请的访问权限

说明

如果权限非系统权限,需要在配置文件中进行自定义。请参考权限开发指导中关于“自定义权限”的相关说明。

{
    "name": ".UserDataAbility",
     "type": "data",
     "visible": true,
     "uri": "dataability://com.example.myapplication5.DataAbilityTest",
     "permissions": [
        "com.example.myapplication5.DataAbility.DATA"
     ]
}

访问Data

开发者可以通过DataAbilityHelper类来访问当前应用或其他应用提供的共享数据。DataAbilityHelper作为客户端,与提供方的Data进行通信。Data接收到请求后,执行相应的处理,并返回结果。DataAbilityHelper提供了一系列与Data Ability对应的方法。

下面介绍DataAbilityHelper具体的使用步骤。

声明使用权限

如果待访问的Data声明了访问需要权限,则访问此Data需要在配置文件中声明需要此权限。声明请参考权限申请字段说明

"reqPermissions": [
    {
        "name": "com.example.myapplication5.DataAbility.DATA"
    },
    // 访问文件还需要添加访问存储读写权限
    {
        "name": "ohos.permission.READ_USER_STORAGE"
    },
    {
        "name": "ohos.permission.WRITE_USER_STORAGE"
    }
]

创建DataAbilityHelper

DataAbilityHelper为开发者提供了creator()方法来创建DataAbilityHelper实例。该方法为静态方法,有多个重载。最常见的方法是通过传入一个context对象来创建DataAbilityHelper对象。

获取helper对象示例:

  1. DataAbilityHelper helper = DataAbilityHelper.creator(this);

访问Data Ability

DataAbilityHelper为开发者提供了一系列的接口来访问不同类型的数据(文件、数据库等)。

  • 访问文件DataAbilityHelper为开发者提供了FileDescriptor openFile(Uri uri, String mode)方法来操作文件。此方法需要传入两个参数,其中uri用来确定目标资源路径,mode用来指定打开文件的方式,可选方式包含“r”(读), “w”(写), “rw”(读写),“wt”(覆盖写),“wa”(追加写),“rwt”(覆盖写且可读)。

该方法返回一个目标文件的FD(文件描述符),把文件描述符封装成流,开发者就可以对文件流进行自定义处理。

访问文件示例:

// 读取文件描述符
FileDescriptor fd = helper.openFile(uri, "r");
FileInputStream fis = new FileInputStream(fd);

// 使用文件描述符封装成的文件流,进行文件操作
  • 访问数据库DataAbilityHelper为开发者提供了增、删、改、查以及批量处理等方法来操作数据库。

说明

对数据库的操作方法,详见数据管理中各数据库类型的开发指南。

方法 描述
ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) 查询数据库
int insert(Uri uri, ValuesBucket value) 向数据库中插入单条数据
int batchInsert(Uri uri, ValuesBucket[] values) 向数据库中插入多条数据
int delete(Uri uri, DataAbilityPredicates predicates) 删除一条或多条数据
int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) 更新数据库
DataAbilityResult[] executeBatch(ArrayList operations) 批量操作数据库

这些方法的使用说明如下:

  • query()查询方法,其中uri为目标资源路径,columns为想要查询的字段。开发者的查询条件可以通过DataAbilityPredicates来构建。查询用户表中id在101-103之间的用户,并把结果打印出来,代码示例如下:
DataAbilityHelper helper = DataAbilityHelper.creator(this);

// 构造查询条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.between("userId", 101, 103);

// 进行查询
ResultSet resultSet = helper.query(uri, columns, predicates);

// 处理结果
resultSet.goToFirstRow();
do {
    // 在此处理ResultSet中的记录;
} while(resultSet.goToNextRow());
  • insert()新增方法,其中uri为目标资源路径,ValuesBucket为要新增的对象。插入一条用户信息的代码示例如下:
DataAbilityHelper helper = DataAbilityHelper.creator(this);

// 构造插入数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString("name", "Tom");
valuesBucket.putInteger("age", 12);
helper.insert(uri, valuesBucket);
  • batchInsert()批量插入方法,和insert()类似。批量插入用户信息的代码示例如下:
DataAbilityHelper helper = DataAbilityHelper.creator(this);

// 构造插入数据
ValuesBucket[] values = new ValuesBucket[2];
values[0] = new ValuesBucket();
values[0].putString("name", "Tom");
values[0].putInteger("age", 12);
values[1] = new ValuesBucket();
values[1].putString("name", "Tom1");
values[1].putInteger("age", 16);
helper.batchInsert(uri, values);
  • delete()删除方法,其中删除条件可以通过DataAbilityPredicates来构建。删除用户表中id在101-103之间的用户,代码示例如下:
DataAbilityHelper helper = DataAbilityHelper.creator(this);

// 构造删除条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.between("userId", 101, 103);
helper.delete(uri, predicates);
  • update()更新方法,更新数据由ValuesBucket传入,更新条件由DataAbilityPredicates来构建。更新id为102的用户,代码示例如下:
DataAbilityHelper helper = DataAbilityHelper.creator(this);

// 构造更新条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.equalTo("userId", 102);

// 构造更新数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString("name", "Tom");
valuesBucket.putInteger("age", 12);
helper.update(uri, valuesBucket, predicates);
  • executeBatch()此方法用来执行批量操作。DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,开发者可自行设置自己要执行的数据库操作。插入多条数据的代码示例如下:
DataAbilityHelper helper = DataAbilityHelper.creator(abilityObj, insertUri);

// 构造批量操作
ValuesBucket value1 = initSingleValue();
DataAbilityOperation opt1 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value1).build();
ValuesBucket value2 = initSingleValue2();
DataAbilityOperation opt2 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value2).build();
ArrayList<DataAbilityOperation> operations = new ArrayList<DataAbilityOperation>();
operations.add(opt1);
operations.add(opt2);
DataAbilityResult[] result = helper.executeBatch(insertUri, operations);

相关实例

针对Data Ability开发,有以下示例工程可供参考:

  • DataAbility本示例演示了如何使用Data Ability对数据库进行增、删、改、查,以及读取文本文件。

针对Data Ability开发,有以下Codelabs可供参考:

  • 关系型数据库基于Data Ability的关系型数据库和数据管理能力,实现数据库相关应用服务的快速开发。

Intent

基本概念

Intent是对象之间传递信息的载体。例如,当一个Ability需要启动另一个Ability时,或者一个AbilitySlice需要导航到另一个AbilitySlice时,可以通过Intent指定启动的目标同时携带相关数据。Intent的构成元素包括Operation与Parameters,具体描述参见表。

属性 子属性 描述
Operation Action 表示动作,通常使用系统预置Action,应用也可以自定义Action。例如IntentConstants.ACTION_HOME表示返回桌面动作。
Entity 表示类别,通常使用系统预置Entity,应用也可以自定义Entity。例如Intent.ENTITY_HOME表示在桌面显示图标。
Uri 表示Uri描述。如果在Intent中指定了Uri,则Intent将匹配指定的Uri信息,包括scheme, schemeSpecificPart, authority和path信息。
Flags 表示处理Intent的方式。例如Intent.FLAG_ABILITY_CONTINUATION标记在本地的一个Ability是否可以迁移到远端设备继续运行。
BundleName 表示包描述。如果在Intent中同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。
AbilityName 表示待启动的Ability名称。如果在Intent中同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。
DeviceId 表示运行指定Ability的设备ID。
Parameters - Parameters是一种支持自定义的数据结构,开发者可以通过Parameters传递某些请求所需的额外信息。

当Intent用于发起请求时,根据指定元素的不同,分为两种类型:

  • 如果同时指定了BundleName与AbilityName,则根据Ability的全称(例如“com.demoapp.FooAbility”)来直接启动应用。

  • 如果未同时指定BundleName和AbilityName,则根据Operation中的其他属性来启动应用。

说明

Intent设置属性时,必须先使用Operation来设置属性。如果需要新增或修改属性,必须在设置Operation后再执行操作。

关于Intent最简单的使用方法,可参见快速入门的示例代码。其中“实现页面跳转”重点描述了使用Intent实现两个页面跳转关系的操作。

根据Ability的全称启动应用

通过构造包含BundleName与AbilityName的Operation对象,可以启动一个Ability、并导航到该Ability。示例代码如下:

Intent intent = new Intent();

// 通过Intent中的OperationBuilder类构造operation对象,指定设备标识(空串表示当前设备)、应用包名、Ability名称
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("")
        .withBundleName("com.demoapp")
        .withAbilityName("com.demoapp.FooAbility")
        .build();

// 把operation设置到intent中
intent.setOperation(operation);
startAbility(intent);

作为处理请求的对象,会在相应的回调方法中接收请求方传递的Intent对象。以导航到另一个Ability为例,导航的目标Ability可以在其onStart()回调的参数中获得Intent对象。

根据Operation的其他属性启动应用

有些场景下,开发者需要在应用中使用其他应用提供的某种能力,而不感知提供该能力的具体是哪一个应用。例如开发者需要通过浏览器打开一个链接,而不关心用户最终选择哪一个浏览器应用,则可以通过Operation的其他属性(除BundleName与AbilityName之外的属性)描述需要的能力。如果设备上存在多个应用提供同种能力,系统则弹出候选列表,由用户选择由哪个应用处理请求。以下示例展示使用Intent跨Ability查询天气信息。

请求方

在Ability中构造Intent以及包含Action的Operation对象,并调用startAbilityForResult()方法发起请求。然后重写onAbilityResult()回调方法,对请求结果进行处理。

private void queryWeather() {
    Intent intent = new Intent();
    Operation operation = new Intent.OperationBuilder()
            .withAction(Intent.ACTION_QUERY_WEATHER)
            .build();
    intent.setOperation(operation);
    startAbilityForResult(intent, REQ_CODE_QUERY_WEATHER);
}

@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultData) {
    switch (requestCode) {
        case REQ_CODE_QUERY_WEATHER:
            // Do something with result.
            ...
            return;
        default:
            ...
    }
}

处理方

  1. 作为处理请求的对象,首先需要在配置文件中声明对外提供的能力,以便系统据此找到自身并作为候选的请求处理者。
{
    "module": {
        ...
        "abilities": [
            {
                ...
                "skills":[
                    {
                        "actions":[
                            "ability.intent.QUERY_WEATHER"
                        ]
                    }
                ]
                ...
            }
        ]
        ...
    }
    ...
}
  1. 在Ability中配置路由以便支持以此action导航到对应的AbilitySlice。
@Override
protected void onStart(Intent intent) {
    ...
    addActionRoute(Intent.ACTION_QUERY_WEATHER, DemoSlice.class.getName());
    ...
}
  1. 在Ability中处理请求,并调用setResult()方法暂存返回结果。
@Override
protected void onActive() {
    ...
    Intent resultIntent = new Intent();
    setResult(0, resultIntent);   //0为当前Ability销毁后返回的resultCode。
    ...
}

相关实例

针对Intent开发,有以下示例工程可供参考:

  • Intent本示例演示了如何根据Ability的全称启动应用和根据Operation的其他属性启动应用。

  • AbilityIntent本示例通过Ability的全称和Operation的其他属性两种方式实现界面跳转以及数据传递

分布式任务调度

在HarmonyOS中,分布式任务调度平台对搭载HarmonyOS的多设备构筑的“超级虚拟终端”提供统一的组件管理能力,为应用定义统一的能力基线、接口形式、数据结构、服务描述语言,屏蔽硬件差异;支持远程启动、远程调用、业务无缝迁移等分布式任务。

分布式任务调度平台在底层实现Ability(分布式任务调度的基本组件)跨设备的启动/关闭、连接及断开连接以及迁移等能力,实现跨设备的组件管理:

  • 启动和关闭:向开发者提供管理远程Ability的能力,即支持启动Page模板的Ability,以及启动、关闭Service和Data模板的Ability。

  • 连接和断开连接:向开发者提供跨设备控制服务(Service和Data模板的Ability)的能力,开发者可以通过与远程服务连接及断开连接实现获取或注销跨设备管理服务的对象,达到和本地一致的服务调度。

  • 迁移能力:向开发者提供跨设备业务的无缝迁移能力,开发者可以通过调用Page模板Ability的迁移接口,将本地业务无缝迁移到指定设备中,打通设备间壁垒。

约束与限制

  • 开发者需要在Intent中设置支持分布式的标记(例如:Intent.FLAG_ABILITYSLICE_MULTI_DEVICE表示该应用支持分布式调度),否则将无法获得分布式能力。

  • 开发者通过在config.json中的reqPermissions字段里添加多设备协同访问的权限申请:三方应用使用{“name”: “ohos.permission.DISTRIBUTED_DATASYNC”}。

  • PA(Particle Ability,Service和Data模板的Ability)的调用支持连接及断开连接、启动及关闭这四类行为,在进行调度时:

    • 开发者必须在Intent中指定PA对应的bundleName和abilityName。
  • 当开发者需要跨设备启动、关闭或连接PA时,需要在Intent中指定对端设备的deviceId。开发者可通过如设备管理类DeviceManager提供的getDeviceList获取指定条件下匿名化处理的设备列表,实现对指定设备PA的启动/关闭以及连接管理。

  • FA(Feature Ability,Page模板的Ability)的调用支持启动和迁移行为,在进行调度时:

    • 当启动FA时,需要开发者在Intent中指定对端设备的deviceId、bundleName和abilityName。
  • FA的迁移实现相同bundleName和abilityName的FA跨设备迁移,因此需要指定迁移设备的deviceId。

  • DevEco Studio远程模拟设备的功能无法调测分布式任务调度,需要在真机环境下进行测试

场景介绍

开发者在应用中集成分布式调度能力,通过调用指定能力的分布式接口,实现跨设备能力调度。根据Ability模板及意图的不同,分布式任务调度向开发者提供以下六种能力:启动远程FA、启动远程PA、关闭远程PA、连接远程PA、断开连接远程PA和FA跨设备迁移。下面以设备A(本地设备)和设备B(远端设备)为例,进行场景介绍:

  1. 设备A启动设备B的FA:在设备A上通过本地应用提供的启动按钮,启动设备B上对应的FA。例如:设备A控制设备B打开相册,只需开发者在启动FA时指定打开相册的意图即可。
  2. 设备A启动设备B的PA:在设备A上通过本地应用提供的启动按钮,启动设备B上指定的PA。例如:开发者在启动远程服务时通过意图指定音乐播放服务,即可实现设备A启动设备B音乐播放的能力。
  3. 设备A关闭设备B的PA:在设备A上通过本地应用提供的关闭按钮,关闭设备B上指定的PA。类似启动的过程,开发者在关闭远程服务时通过意图指定音乐播放服务,即可实现关闭设备B上该服务的能力。
  4. 设备A连接设备B的PA:在设备A上通过本地应用提供的连接按钮,连接设备B上指定的PA。连接后,通过其他功能相关按钮实现控制对端PA的能力。通过连接关系,开发者可以实现跨设备的同步服务调度,实现如大型计算任务互助等价值场景。
  5. 设备A与设备B的PA断开连接:在设备A上通过本地应用提供断开连接的按钮,将之前已连接的PA断开连接。
  6. 设备A的FA迁移至设备B:设备A上通过本地应用提供的迁移按钮,将设备A的业务无缝迁移到设备B中。通过业务迁移能力,打通设备A和设备B间的壁垒,实现如文档跨设备编辑、视频从客厅到房间跨设备接续播放等场景。

接口说明

分布式调度平台提供的连接和断开连接PA、启动远程FA、启动和关闭PA以及迁移FA的能力,是实现更多价值性场景的基础。

连接远程PA

connectAbility(Intent intent, IAbilityConnection conn)接口提供连接指定设备上PA的能力,Intent中指定待连接PA的设备deviceId、bundleName和abilityName。当连接成功后,通过在conn定义的onAbilityConnectDone回调中获取对端PA的服务代理,两者的连接关系则由conn维护。具体的参数定义如下表所示:

参数名 类型 说明
intent ohos.aafwk.content.Intent 开发者需在intent对应的Operation中指定待连接PA的设备deviceId、bundleName和abilityName。
conn ohos.aafwk.ability.IAbilityConnection 当连接成功或失败时,作为连接关系的回调接口。该接口提供连接完成和断开连接完成时的处理逻辑,开发者可根据具体的场景进行定义。

启动远程FA/PA

startAbility(Intent intent)接口提供启动指定设备上FA和PA的能力,Intent中指定待启动FA/PA的设备deviceId、bundleName和abilityName。具体参数定义如下表所示:

参数名 类型 说明
intent ohos.aafwk.content.Intent 当开发者需要调用该接口启动远程PA时,需要指定待启动PA的设备deviceId、bundleName和abilityName。若不指定设备deviceId,则无法跨设备调用PA。类似地,在启动FA时,也需要开发者指定启动FA的设备deviceId、bundleName和abilityName。

分布式调度平台还会提供与上述功能相对应的断开远程PA的连接和关闭远程PA的接口,相关的参数与连接、启动的接口类似。

  • 断开远程PA连接:disconnectAbility(IAbilityConnection conn)。

  • 关闭远程PA:stopAbility(Intent intent)。

迁移FA

continueAbility(String deviceId)接口提供将本地FA迁移到指定设备上的能力,需要开发者在调用时指定目标设备的deviceId。具体参数定义如下表所示:

说明

Ability和AbilitySlice类均需要实现IAbilityContinuation及其方法,才可以实现FA迁移。

参数名 类型 说明
deviceId String 当开发者需要调用该接口将本地FA迁移时,需要指定目标设备的deviceId。

开发步骤

  1. 导入功能依赖的包。
// 以下依赖包含分布式调度平台开放的接口
// 用于:连接/断开连接远程PA、启动远程FA、通过连接关系实现对PA的控制
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.ability.IAbilityConnection;
import ohos.aafwk.content.Intent;
import ohos.aafwk.content.Operation;
import ohos.bundle.ElementName;
// 为了实现迁移能力,需要引入传递迁移所需数据的包以及实现迁移能力的接口
import ohos.aafwk.ability.IAbilityContinuation;
import ohos.aafwk.content.IntentParams;
// 为了实现跨设备指令及数据通信,需要使用RPC接口
import ohos.rpc.IRemoteObject;
import ohos.rpc.IRemoteBroker;
import ohos.rpc.MessageParcel;
import ohos.rpc.MessageOption;
import ohos.rpc.RemoteException;
import ohos.rpc.RemoteObject;
//(可选)多设备场景下涉及设备选择,为此需要引入组网设备发现的能力
import ohos.distributedschedule.interwork.DeviceInfo;
import ohos.distributedschedule.interwork.DeviceManager;
// (可选)设计界面相关的包函数,对FA界面及按钮进行绘制
import ohos.agp.components.Button;
import ohos.agp.components.Component;
import ohos.agp.components.Component.ClickedListener;
import ohos.agp.components.ComponentContainer.LayoutConfig;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.components.PositionLayout;
  1. (可选)编写一个基本的FA用于使用分布式能力。
// 调用AbilitySlice模板实现一个用于控制基础功能的FA,AbilitySlice的代码示例如下:
public class SampleSlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // 开发者可以自行进行界面设计
        // 为按钮设置统一的背景色
        // 例如通过PositionLayout可以实现简单界面
        PositionLayout layout = new PositionLayout(this);
        LayoutConfig config = new LayoutConfig(LayoutConfig.MATCH_PARENT, LayoutConfig.MATCH_PARENT);
        layout.setLayoutConfig(config);
        ShapeElement buttonBg = new ShapeElement();
        buttonBg.setRgbColor(new RgbColor(0, 125, 255));
        addComponents(layout, buttonBg, config);
        super.setUIContent(layout);
    }

    @Override
    public void onInactive() {
        super.onInactive();
    }

    @Override
    public void onActive() {
        super.onActive();
    }

    @Override
    public void onBackground() {
        super.onBackground();
    }

    @Override
    public void onForeground(Intent intent) {
        super.onForeground(intent);
    }

    @Override
    public void onStop() {
        super.onStop();
    }
}

说明

此步骤展示了一个简单FA的实现过程,实际开发中请开发者根据需要进行设计。

  1. 使用分布式能力要求开发者在Ability对应的config.json中声明多设备协同访问的权限:三方应用使用{“name”: “ohos.permission.DISTRIBUTED_DATASYNC”}。一个三方应用部署的示例如下:
{
    "reqPermissions": [
        {
            "name": "ohos.permission.DISTRIBUTED_DATASYNC"
        }
    ]
}

此外,对于三方应用还要求在实现Ability的代码中显式声明需要使用的权限,如下所示:

public class SampleSlice extends AbilitySlice {
    @Override
    public void onStart(Intent intent) {
        // 开发者显示声明需要使用的权限
        requestPermissionsFromUser(new String[]{"ohos.permission.DISTRIBUTED_DATASYNC"}, 0);
        super.onStart(intent);        
    }
}
  1. (可选)为不同的能力设置相应的控制按钮。
// 建议开发者按照自己的界面进行按钮设计

// 开发者可以自行实现如下createButton的方法,新建一个显示文字text,背景色为buttonBg以及大小尺寸位置符合config设置的按钮,用来与用户交互
// private Button createButton(String text, ShapeElement buttonBg, LayoutConfig config)
// 按照顺序在PositionLayout中依次添加按钮的示例
private void addComponents(PositionLayout linear, ShapeElement buttonBg, LayoutConfig config) {
    // 构建远程启动FA的按钮
    btnStartRemoteFA = createButton("StartRemoteFA", buttonBg, config);
    btnStartRemoteFA.setClickedListener(mStartRemoteFAListener);
    linear.addComponent(btnStartRemoteFA);
    // 构建远程启动PA的按钮
    btnStartRemotePA = createButton("StartRemotePA", buttonBg, config);
    btnStartRemotePA.setClickedListener(mStartRemotePAListener);
    linear.addComponent(btnStartRemotePA);
    // 构建远程关闭PA的按钮
    btnStopRemotePA = createButton("StopRemotePA", buttonBg, config);
    btnStopRemotePA.setClickedListener(mStopRemotePAListener);
    linear.addComponent(btnStopRemotePA);
    // 构建连接远程PA的按钮
    btnConnectRemotePA = createButton("ConnectRemotePA", buttonBg, config);
    btnConnectRemotePA.setClickedListener(mConnectRemotePAListener);
    linear.addComponent(btnConnectRemotePA);
    // 构建控制连接PA的按钮
    btnControlRemotePA = createButton("ControlRemotePA", buttonBg, config);
    btnControlRemotePA.setClickedListener(mControlPAListener);
    linear.addComponent(btnControlRemotePA);
    // 构建与远程PA断开连接的按钮
    btnDisconnectRemotePA = createButton("DisconnectRemotePA", buttonBg, config);
    btnDisconnectRemotePA.setClickedListener(mDisconnectRemotePAListener);
    linear.addComponent(btnDisconnectRemotePA);
    // 构建迁移FA的按钮
    btnContinueRemoteFA = createButton("ContinueRemoteFA", buttonBg, config);
    btnContinueRemoteFA.setClickedListener(mContinueAbilityListener);
    linear.addComponent(btnContinueRemoteFA);
}

说明

此处只展示了基于按钮控制的能力调度方法,实际开发中请开发者根据需要选择能力调度方式。代码示例中未体现按钮如位置、样式等具体的设置方法,详请参考JAVA UI框架

  1. 通过设备管理DeviceManager提供的getDeviceList接口获取设备列表,用于指定目标设备。
// ISelectResult是一个自定义接口,用来处理指定设备deviceId后执行的行为
 interface ISelectResult {
     void onSelectResult(String deviceId);
 }

// 获得设备列表,开发者可在得到的在线设备列表中选择目标设备执行操作
private void scheduleRemoteAbility(ISelectResult listener) {
    // 调用DeviceManager的getDeviceList接口,通过FLAG_GET_ONLINE_DEVICE标记获得在线设备列表
    List<DeviceInfo> onlineDevices = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
    // 判断组网设备是否为空
    if (onlineDevices.isEmpty()) {
        listener.onSelectResult(null);
        return;
    }
    int numDevices = onlineDevices.size();
    List<String> deviceIds = new ArrayList<>(numDevices);
    onlineDevices.forEach((device) -> {
        deviceIds.add(device.getDeviceId());
    });
    // 以选择首个设备作为目标设备为例
    // 开发者也可按照具体场景,通过别的方式进行设备选择
    String selectDeviceId = deviceIds.get(0);
    listener.onSelectResult(selectDeviceId);    
}

上述实例中涉及对在线组网设备的查询,该项能力需要开发者在对应的config.json中声明获取设备列表及设备信息的权限,如下所示:

{
    "reqPermissions": [
        {
            "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"
        }, 
        {
            "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO"
        }, 
        {
            "name": "ohos.permission.GET_BUNDLE_INFO"
        }
    ]
}
  1. 为启动远程FA的按钮设置点击回调,实现启动远程FA的能力。
// 启动一个指定bundleName和abilityName的FA
private ClickedListener mStartRemoteFAListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        // 启动远程PA
        scheduleRemoteAbility(new ISelectResult() {
            @Override
            void onSelectResult(String deviceId) {
                if (deviceId != null) {
                    // 通过scheduleRemoteAbility指定目标设备deviceId
                    // 指定待启动FA的bundleName和abilityName
                    // 例如:bundleName = "com.helloworld"
                    //       abilityName = "com.helloworld.SampleFeatureAbility"
                    // 设置分布式标记,表明当前涉及分布式能力
                    Operation operation = new Intent.OperationBuilder()
                            .withDeviceId(deviceId)
                            .withBundleName(bundleName)
                            .withAbilityName(abilityName)
                            .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                            .build();
                    Intent intent = new Intent();
                    intent.setOperation(operation);
                    // 通过AbilitySlice包含的startAbility接口实现跨设备启动FA
                    startAbility(intent);
                }
            }
        });
    }
};
  1. 为启动和关闭PA定义回调,实现启动和关闭PA的能力。对于PA的启动、关闭、连接等操作都需要开发者提供目标设备的deviceId。开发者可以通过DeviceManager相关接口得到当前组网下的设备列表,并以弹窗的形式供用户选择,也可以按照实际需要实现其他个性化的处理方式。在点击事件回调函数中,需要开发者指定得到deviceId后的处理逻辑,即实现类似上例中listener.onSelectResult(String deviceId)的方法,代码示例如下:
// 启动远程PA
private ClickedListener mStartRemotePAListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        // 启动远程PA
        scheduleRemoteAbility(new ISelectResult() {
            @Override
            void onSelectResult(String deviceId) {
                if (deviceId != null) {
                    // bundleName和abilityName与待启动PA对应
                    // 例如:bundleName = "com.helloworld"
                    //       abilityName = "com.helloworld.SampleParticleAbility"
                    Operation operation = new Intent.OperationBuilder()
                            .withDeviceId(deviceId)
                            .withBundleName(bundleName)
                            .withAbilityName(abilityName)
                            .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                            .build();
                    Intent intentToStartPA = new Intent();
                    intentToStartPA.setOperation(operation);
                    startAbility(intentToStartPA);
                }
            }
        });
    }
};

// 关闭远程PA,和启动类似开发者需要指定待关闭PA对应的bundleName和abilityName
private ClickedListener mStopRemotePAListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        scheduleRemoteAbility(new ISelectResult() {
            @Override
            void onSelectResult(String deviceId) {
                if (deviceId != null) {
                    // bundleName和abilityName与待关闭PA对应
                    // 例如:bundleName = "com.helloworld"
                    //       abilityName = "com.helloworld.SampleParticleAbility"
                    Operation operation = new Intent.OperationBuilder()
                            .withDeviceId(deviceId)
                            .withBundleName(bundleName)
                            .withAbilityName(abilityName)
                            .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                            .build();
                    Intent intentToStopPA = new Intent();
                    intentToStopPA.setOperation(operation);
                    stopAbility(intentToStopPA);
                }
            }
        });
    }
};

说明

启动和关闭的行为类似,开发者只需在Intent中指定待调度PA的deviceId、bundleName和abilityName,并以operation的形式封装到Intent内。通过AbilitySlice(Ability)包含的startAbility()和stopAbility()接口即可实现相应功能。

  1. 设备A连接设备B侧的PA,利用连接关系调用该PA执行特定任务,以及断开连接。
// 当连接完成时,用来提供管理已连接PA的能力
private MyRemoteProxy mProxy = null;
// 用于管理连接关系
private IAbilityConnection mConn = new IAbilityConnection() {
    @Override
    public void onAbilityConnectDone(ElementName element, IRemoteObject remote, int resultCode) {
        // 跨设备PA连接完成后,会返回一个序列化的IRemoteObject对象
        // 通过该对象得到控制远端服务的代理
        mProxy = new MyRemoteProxy(remote);
        btnConnectRemotePA.setText("connectRemoteAbility done");
    }

    @Override
    public void onAbilityDisconnectDone(ElementName element, int resultCode) {
        // 当已连接的远端PA关闭时,会触发该回调
        // 支持开发者按照返回的错误信息进行PA生命周期管理
        disconnectAbility(mConn);
    }
};

仅通过启动/关闭两种方式对PA进行调度无法应对需长期交互的场景,因此,分布式任务调度平台向开发者提供了跨设备PA连接及断开连接的能力。为了对已连接PA进行管理,开发者需要实现一个满足IAbilityConnection接口的连接状态检测实例,通过该实例可以对连接及断开连接完成时设置具体的处理逻辑,例如:获取控制对端PA的代理等。进一步为了使用该代理跨设备调度PA,开发者需要在本地及对端分别实现对外接口一致的代理。一个具备加法能力的代理示例如下:

// 以连接提供加法计算能力的PA为例。为了提供跨设备连接能力,需要在本地发起连接侧和对端被连接侧分别实现代理
// 发起连接侧的代理示例如下:
public class MyRemoteProxy implements IRemoteBroker {
    private static final int ERR_OK = 0;
    private static final int COMMAND_PLUS = IRemoteObject.MIN_TRANSACTION_ID;
    private final IRemoteObject remote;

    public MyRemoteProxy(IRemoteObject remote) {
        this.remote = remote;
    }

    @Override
    public IRemoteObject asObject() {
        return remote;
    }

    public int plus(int a, int b) throws RemoteException {
        MessageParcel data = MessageParcel.obtain();
        MessageParcel reply = MessageParcel.obtain();
        // option不同的取值,决定采用同步或异步方式跨设备控制PA
        // 本例需要同步获取对端PA执行加法的结果,因此采用同步的方式,即MessageOption.TF_SYNC
        // 具体MessageOption的设置,可参考相关API文档
        MessageOption option = new MessageOption(MessageOption.TF_SYNC);
        data.writeInt(a);
        data.writeInt(b);

        try {
            remote.sendRequest(COMMAND_PLUS, data, reply, option);
            int errCode = reply.readInt();
            if (errCode != ERR_OK) {
                throw new RemoteException();
            }
            int result = reply.readInt();
            return result;
        }
finally {
            data.reclaim();
            reply.reclaim();
        }
    }
}

此外,对端待连接的PA需要实现对应的客户端,代码示例如下所示:

// 以计算加法为例,对端实现的客户端如下
public class MyRemote extends RemoteObject implements IRemoteBroker{
    private static final int ERR_OK = 0;
    private static final int ERROR = -1;
    private static final int COMMAND_PLUS = IRemoteObject.MIN_TRANSACTION_ID;

    public MyRemote() {
        super("MyService_Remote");
    }

    @Override
    public IRemoteObject asObject() {
        return this;
    }

    @Override
    public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
        if (code != COMMAND_PLUS) {
            reply.writeInt(ERROR);
            return false;
        }
        int value1 = data.readInt();
        int value2 = data.readInt();
        int sum = value1 + value2;
        reply.writeInt(ERR_OK);
        reply.writeInt(sum);
        return true;
    }
}

对端除了要实现如上所述的客户端外,待连接的PA还需要作如下修改:

// 为了返回给连接方可调用的代理,需要在该PA中实例化客户端,例如作为该PA的成员变量
private MyRemote remote = new MyRemote();
// 当该PA接收到连接请求时,即将该客户端转化为代理返回给连接发起侧
@Override
protected IRemoteObject onConnect(Intent intent) {
    super.onConnect(intent);
    return remote.asObject();
}

完成上述步骤后,可以通过点击事件实现连接、利用连接关系控制PA以及断开连接等行为,代码示例如下:

// 连接远程PA
private ClickedListener mConnectRemotePAListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        scheduleRemoteAbility(new ISelectResult() {
            @Override
            void onSelectResult(String deviceId) {
                if (deviceId != null) {
                    Intent connectPAIntent = new Intent();
                    // bundleName和abilityName与待连接的PA一一对应
                    // 例如:bundleName = "com.helloworld"
                    //       abilityName = "com.helloworld.SampleParticleAbility"
                    Operation operation = new Intent.OperationBuilder()
                            .withDeviceId(deviceId)
                            .withBundleName(bundleName)
                            .withAbilityName(abilityName)
                            .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                            .build();
                    connectPAIntent.setOperation(operation);
                    connectAbility(connectPAIntent, mConn);
                }
            }
        });
    }
};
// 控制已连接PA执行加法
private ClickedListener mControlPAListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        if (mProxy != null) {
            int ret = -1;
            try {
                ret = mProxy.plus(10, 20);
            } catch (RemoteException e) {
                HiLog.error(LABEL, "ControlRemotePA error");
            }
            btnControlRemotePA.setText("ControlRemotePA result = " + ret);
        }
    }
};
// 与远程PA断开连接
private ClickedListener mDisconnectRemotePAListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        // 按钮复位
        btnConnectRemotePA.setText("ConnectRemotePA");
        btnControlRemotePA.setText("ControlRemotePA");
        disconnectAbility(mConn);
    }
};

说明

通过连接/断开连接远程PA,与跨设备PA建立长期的管理关系。例如在本例中,通过连接关系得到远程PA的控制代理后,实现跨设备计算加法并将结果返回到本地显示。在实际开发中,开发者可以根据需要实现多种分布式场景,例如:跨设备位置/电量等信息的采集、跨设备计算资源互助等。

  1. 设备A将运行时的FA迁移到设备B,实现业务在设备间无缝迁移。
// 跨设备迁移FA
// 本地FA设置当前运行任务
private ClickedListener mContinueAbilityListener = new ClickedListener() {
    @Override
    public void onClick(Component arg0) {
        // 用户选择设备后实现业务迁移
        scheduleRemoteAbility(new ISelectResult() {
            @Override
            public void onSelectResult(String deviceId) {
                continueAbility(deviceId);
            }
        });
    }
};

FA的迁移还涉及到状态数据的传递,需要继承IAbilityContinuation接口,供开发者实现迁移过程中特定事件的管理能力,代码示例如下:

public class SampleSlice extends AbilitySlice implements IAbilityContinuation {
    @Override
    public boolean onSaveData(IntentParams saveData) {
        String exampleData = String.valueOf(System.currentTimeMillis());
        saveData.setParam("continueParam", exampleData);
        return true;
    }

    @Override
    public boolean onRestoreData(IntentParams restoreData) {
        // 远端FA迁移传来的状态数据,开发者可以按照特定的场景对这些数据进行处理
        Object data = restoreData.getParam("continueParam");
        return true;
    }

    @Override
    public void onCompleteContinuation(int result) {
        btnContinueRemoteFA.setText("ContinueAbility Done");
    }
}

通过自定义迁移事件相关的行为,最终实现对Ability的迁移。具体的定义可以参考相关的API文档,此处主要以较为常用的两个事件,包括迁移发起端完成迁移的回调onCompleteContinuation(int result)以及接收到远端迁移行为传递数据的回调onRestoreData(IntentParams restoreData)。其他还包括迁移到远端设备的FA关闭的回调onRemoteTerminated()、用于本地迁移发起时保存状态数据的回调onSaveData(IntentParams saveData)和本地发起迁移的回调onStartContinuation()。按照实际应用自定义特定场景对应的回调,可以完成多种场景下FA的迁移任务。

说明

  • FA迁移可以打通设备间的壁垒,有助于不同能力的设备进行互助。前文以一个简单的例子介绍如何通过分布式任务调度提供的能力,实现FA跨设备的迁移(包括FA启动及状态数据的同步)。

  • FA迁移过程中,远端FA首先接收到发起端FA传输的数据,再执行启动,即onRestoreData()发生在onStart()之前,详见API参考。

相关实例

针对分布式任务调度,有以下示例工程可供参考:

  • DistributedScheduler本示例演示了分布式任务调度的六种场景:启动远程FA,启动远程PA,关闭远程PA,连接远程PA,断开连接远程PA, 和FA跨端迁移。

针对分布式任务调度,有以下Codelabs可供参考:

公共事件与通知

armonyOS通过CES(Common Event Service,公共事件服务)为应用程序提供订阅、发布、退订公共事件的能力,通过ANS(Advanced Notification Service,即通知增强服务)系统服务来为应用程序提供发布通知的能力。

  • 公共事件可分为系统公共事件和自定义公共事件。

    • 系统公共事件:系统将收集到的事件信息,根据系统策略发送给订阅该事件的用户程序。 例如:用户可感知亮灭屏事件,系统关键服务发布的系统事件(例如:USB插拔,网络连接,系统升级等)。
  • 自定义公共事件:应用自定义一些公共事件用来处理业务逻辑。

  • 通知提供应用的即时消息或通信消息,用户可以直接删除或点击通知触发进一步的操作。

  • IntentAgent封装了一个指定行为的Intent,可以通过IntentAgent启动Ability和发布公共事件。

应用如果需要接收公共事件,需要订阅相应的事件。

约束与限制

公共事件的约束与限制

  • 目前公共事件仅支持动态订阅。部分系统事件需要具有指定的权限,具体的权限见API参考。

  • 目前公共事件订阅不支持多用户。

  • ThreadMode表示线程模型,目前仅支持HANDLER模式,即在当前UI线程上执行回调函数。

  • deviceId用来指定订阅本地公共事件还是远端公共事件。deviceId为null、空字符串或本地设备deviceId时,表示订阅本地公共事件,否则表示订阅远端公共事件。

通知的约束与限制

  • 通知目前支持六种样式:普通文本、长文本、图片、社交、多行文本和媒体样式。创建通知时必须包含一种样式。

  • 通知支持快捷回复。

IntentAgent的限制

使用IntentAgent启动Ability时,Intent必须指定Ability的包名和类名

场景介绍

每个应用都可以订阅自己感兴趣的公共事件,订阅成功后且公共事件发布后,系统会把其发送给应用。这些公共事件可能来自系统、其他应用和应用自身。HarmonyOS提供了一套完整的API,支持用户订阅、发布和接收公共事件。发布公共事件需要借助CommonEventData对象,接收公共事件需要继承CommonEventSubscriber类并实现onReceiveEvent回调函数。

接口说明

公共事件相关基础类包含CommonEventDataCommonEventPublishInfoCommonEventSubscribeInfoCommonEventSubscriberCommonEventManager。基础类之间的关系如下图所示:

  • CommonEventData

    CommonEventData封装公共事件相关信息。用于在发布、分发和接收时处理数据。在构造CommonEventData对象时,相关参数需要注意以下事项:

    • code为有序公共事件的结果码,data为有序公共事件的结果数据,仅用于有序公共事件场景。
  • intent不允许为空,否则发布公共事件失败。

接口名 描述
CommonEventData() 创建公共事件数据。
CommonEventData(Intent intent) 创建公共事件数据指定Intent。
CommonEventData(Intent intent, int code, String data) 创建公共事件数据,指定Intent、code和data。
getIntent() 获取公共事件Intent。
setCode(int code) 设置有序公共事件的结果码。
getCode() 获取有序公共事件的结果码。
setData(String data) 设置有序公共事件的详细结果数据。
getData() 获取有序公共事件的详细结果数据。
  • CommonEventPublishInfo

    CommonEventPublishInfo封装公共事件发布相关属性、限制等信息,包括公共事件类型(有序或粘性)、接收者权限等。

    • 有序公共事件:主要场景是多个订阅者有依赖关系或者对处理顺序有要求,例如:高优先级订阅者可修改公共事件内容或处理结果,包括终止公共事件处理;或者低优先级订阅者依赖高优先级的处理结果等。有序公共事件的订阅者可以通过CommonEventSubscribeInfo.setPriority()方法指定优先级,缺省为0,优先级范围[-1000, 1000],值越大优先级越高。
  • 粘性公共事件:指公共事件的订阅动作是在公共事件发布之后进行,订阅者也能收到的公共事件类型。主要场景是由公共事件服务记录某些系统状态,如蓝牙、WLAN、充电等事件和状态。不使用粘性公共事件机制时,应用可以通过直接访问系统服务获取该状态;在状态变化时,系统服务、硬件需要提供类似observer等方式通知应用。发布粘性公共事件可以通过setSticky()方法设置, 发布粘性公共事件需要申请如下权限。声明请参考权限开发指导表1 reqPermissions权限申请字段说明

"reqPermissions": [
  {
    "name": "ohos.permission.COMMONEVENT_STICKY",
    "reason": "Obtain the required permission",
    "usedScene": {
      "ability": [
        ".MainAbility"
      ],
      "when": "inuse"
    }
  },
  {
    ...
  }
]
接口名 描述
CommonEventPublishInfo() 创建公共事件信息。
CommonEventPublishInfo(CommonEventPublishInfo publishInfo) 拷贝一个公共事件信息。
setSticky(boolean sticky) 设置公共事件的粘性属性。
setOrdered(boolean ordered) 设置公共事件的有序属性。
setSubscriberPermissions(String[] subscriberPermissions) 设置公共事件订阅者的权限,多参数仅第一个生效。
  • CommonEventSubscribeInfoCommonEventSubscribeInfo封装公共事件订阅相关信息,比如优先级、线程模式、事件范围等。

线程模式(ThreadMode):设置订阅者的回调方法执行的线程模式。ThreadMode有HANDLER,POST,ASYNC,BACKGROUND四种模式,目前只支持HANDLER模式。

  • HANDLER:在Ability的主线程上执行。

  • POST:在事件分发线程执行。

  • ASYNC:在一个新创建的异步线程执行。

  • BACKGROUND:在后台线程执行。

接口名 描述
CommonEventSubscribeInfo(MatchingSkills matchingSkills) 创建公共事件订阅器指定matchingSkills。
CommonEventSubscribeInfo(CommonEventSubscribeInfo) 拷贝公共事件订阅器对象。
setPriority(int priority) 设置优先级,用于有序公共事件。
setThreadMode(ThreadMode threadMode) 指定订阅者的回调函数运行在哪个线程上。
setPermission(String permission) 设置发布者必须具备的权限。
setDeviceId(String deviceId) 指定订阅哪台设备的公共事件。
  • CommonEventSubscriber

    CommonEventSubscriber封装公共事件订阅者及相关参数。

    • CommonEventSubscriber.AsyncCommonEventResult类处理有序公共事件异步执行,详见API参考。
  • 目前只能通过调用CommonEventManager的subscribeCommonEvent()进行订阅。

接口名 描述
CommonEventSubscriber(CommonEventSubscribeInfo subscribeInfo) 构造公共事件订阅者实例。
onReceiveEvent(CommonEventData data) 由开发者实现, 在接收到公共事件时被调用。
AsyncCommonEventResult goAsyncCommonEvent() 设置有序公共事件异步执行。
setCodeAndData(int code, String data) 设置有序公共事件的异步结果。
setData(String data) 设置有序公共事件的异步结果数据。
setCode(int code) 设置有序公共事件的异步结果码。
getData() 获取有序公共事件的异步结果数据。
getCode() 获取有序公共事件的异步结果码。
abortCommonEvent() 取消当前的公共事件,仅对有序公共事件有效,取消后,公共事件不再向下一个订阅者传递。
getAbortCommonEvent() 获取当前有序公共事件是否取消的状态。
clearAbortCommonEvent() 清除当前有序公共事件abort状态。
isOrderedCommonEvent() 查询当前公共事件的是否为有序公共事件。
isStickyCommonEvent() 查询当前公共事件是否为粘性公共事件。
  • CommonEventManagerCommonEventManager是为应用提供订阅、退订和发布公共事件的静态接口类。
方法 描述
publishCommonEvent(CommonEventData eventData) 发布公共事件。
publishCommonEvent(CommonEventData event, CommonEventPublishInfo publishInfo) 发布公共事件指定发布信息。
publishCommonEvent(CommonEventData event, CommonEventPublishInfo publishInfo, CommonEventSubscriber resultSubscriber) 发布有序公共事件,指定发布信息和最后一个接收者。
subscribeCommonEvent(CommonEventSubscriber subscriber) 订阅公共事件。
unsubscribeCommonEvent(CommonEventSubscriber subscriber) 退订公共事件。

发布公共事件

开发者可以发布四种公共事件:无序的公共事件、带权限的公共事件、有序的公共事件、粘性的公共事件。

发布无序的公共事件:构造CommonEventData对象,设置Intent,通过构造operation对象把需要发布的公共事件信息传入intent对象。然后调用 CommonEventManager.publishCommonEvent(CommonEventData) 接口发布公共事件。

try {
    Intent intent = new Intent();   
    Operation operation = new Intent.OperationBuilder()
            .withAction("com.my.test")
            .build();
    intent.setOperation(operation);
    CommonEventData eventData = new CommonEventData(intent);
    CommonEventManager.publishCommonEvent(eventData); 
    HiLog.info(LABEL_LOG, "Publish succeeded"); 
} catch (RemoteException e) {
    HiLog.error(LABEL_LOG, "Exception occurred during publishCommonEvent invocation."); 
}

发布携带权限的公共事件:构造CommonEventPublishInfo对象,设置订阅者的权限。

说明

非系统已定义的权限,需要先在config.json中自定义,才可以申请使用。详见权限定义字段说明

"reqPermissions": [
  {
    "name": "com.example.MyApplication.permission",
    "reason": "Obtain the required permission",
    "usedScene": {
      "ability": [
        ".MainAbility"
      ],
      "when": "inuse"
    }
  }, 
  {
    ...
  }
]
  • 发布带权限的公共事件示例代码如下:
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withAction("com.my.test")
        .build();
intent.setOperation(operation);
CommonEventData eventData = new CommonEventData(intent);
CommonEventPublishInfo publishInfo = new CommonEventPublishInfo();
String[] permissions = {"com.example.MyApplication.permission"};
publishInfo.setSubscriberPermissions(permissions); // 设置权限
try {   
    CommonEventManager.publishCommonEvent(eventData, publishInfo); 
    HiLog.info(LABEL_LOG, "Publish succeeded"); 
} catch (RemoteException e) {
    HiLog.error(LABEL_LOG, "Exception occurred during publishCommonEvent invocation."); 
}

发布有序的公共事件:构造CommonEventPublishInfo对象,通过setOrdered(true)指定公共事件属性为有序公共事件,也可以指定一个最后的公共事件接收者。

CommonEventSubscriber resultSubscriber = new MyCommonEventSubscriber();
CommonEventPublishInfo publishInfo = new CommonEventPublishInfo();
publishInfo.setOrdered(true); // 设置属性为有序公共事件
try {   
    CommonEventManager.publishCommonEvent(eventData, publishInfo, resultSubscriber); // 指定resultSubscriber为有序公共事件最后一个接收者。
} catch (RemoteException e) {
    HiLog.error(LABEL_LOG, "Exception occurred during publishCommonEvent invocation."); 
}
发布粘性公共事件:构造CommonEventPublishInfo对象,通过setSticky(true)指定公共事件属性为粘性公共事件。
  1. 发布者首先在config.json中申请发布粘性公共事件所需的权限,各字段含义详见权限申请字段说明
{
    "reqPermissions": [{
        "name": "ohos.permission.COMMONEVENT_STICKY",
        "reason": "Obtain the required permission",
        "usedScene": {
           "ability": [
            ".MainAbility"
           ],
           "when": "inuse"
        }
    }, {
    ...
    }]
}
  1. 发布粘性公共事件。
CommonEventPublishInfo publishInfo = new CommonEventPublishInfo();
publishInfo.setSticky(true); // 设置属性为粘性公共事件
try {   
    CommonEventManager.publishCommonEvent(eventData, publishInfo); 
} catch (RemoteException e) {
    HiLog.error(LABEL, "Exception occurred during publishCommonEvent invocation."); 
}

订阅公共事件

  1. 创建CommonEventSubscriber派生类,在onReceiveEvent()回调函数中处理公共事件。

说明

此处不能执行耗时操作,否则会阻塞UI线程,产生用户点击没有反应等异常。

class MyCommonEventSubscriber extends CommonEventSubscriber { 
    MyCommonEventSubscriber(CommonEventSubscribeInfo info) { 
        super(info);   
    }
    @Override 
    public void onReceiveEvent(CommonEventData commonEventData) {
    } 
}
  1. 构造MyCommonEventSubscriber对象,调用CommonEventManager.subscribeCommonEvent()接口进行订阅。
String event = "com.my.test";
MatchingSkills matchingSkills = new MatchingSkills();
matchingSkills.addEvent(event); // 自定义事件
matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON); // 亮屏事件
CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
MyCommonEventSubscriber subscriber = new MyCommonEventSubscriber(subscribeInfo);
try {
    CommonEventManager.subscribeCommonEvent(subscriber); 
} catch (RemoteException e) {
    HiLog.error(LABEL, "Exception occurred during subscribeCommonEvent invocation."); 
}

如果订阅拥有指定权限应用发布的公共事件,发布者需要在config.json中申请权限,各字段含义详见权限申请字段说明

"reqPermissions": [
    {
        "name": "ohos.abilitydemo.permission.PROVIDER",
        "reason": "Obtain the required permission",
        "usedScene": {
            "ability": ["com.hmi.ivi.systemsetting.MainAbility"],
            "when": "inuse"
        }
    }
]

如果订阅的公共事件是有序的,可以调用setPriority()指定优先级。

String event = "com.my.test";
MatchingSkills matchingSkills = new MatchingSkills();
matchingSkills.addEvent(event); // 自定义事件

CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
subscribeInfo.setPriority(100); // 设置优先级,优先级取值范围[-1000,1000],值默认为0。
MyCommonEventSubscriber subscriber = new MyCommonEventSubscriber(subscribeInfo);
try {
     CommonEventManager.subscribeCommonEvent(subscriber); 
} catch (RemoteException e) {
     HiLog.error(LABEL, "Exception occurred during subscribeCommonEvent invocation."); 
}
  1. 针对在onReceiveEvent中不能执行耗时操作的限制,可以使用CommonEventSubscriber的goAsyncCommonEvent()来实现异步操作,函数返回后仍保持该公共事件活跃,且执行完成后必须调用AsyncCommonEventResult .finishCommonEvent()来结束。
EventRunner runner = EventRunner.create(); // EventRunner创建新线程,将耗时的操作放到新的线程上执行
MyEventHandler myHandler = new MyEventHandler(runner); // MyEventHandler为EventHandler的派生类,在不同线程间分发和处理事件和Runnable任务
@Override
public void onReceiveEvent(CommonEventData commonEventData){
    final AsyncCommonEventResult result = goAsyncCommonEvent();

    Runnable task = new Runnable() {
        @Override
        public void run() {
            ........         // 待执行的操作,由开发者定义
            result.finishCommonEvent(); // 调用finish结束异步操作
        }
    };
    myHandler.postTask(task);
} 

退订公共事件

在Ability的onStop()中调用CommonEventManager.unsubscribeCommonEvent()方法来退订公共事件。调用后,之前订阅的所有公共事件均被退订。

try { 
    CommonEventManager.unsubscribeCommonEvent(subscriber);
 } catch (RemoteException e) {
    HiLog.error(LABEL, "Exception occurred during unsubscribeCommonEvent invocation.");
 }

相关实例

针对公共事件开发,有以下示例工程可供参考:

  • CommonEvent本示例演示了公共事件的订阅、发布和退订。

针对公共事件,有以下Codelabs可供参考:

  • 分布式亲子早教系统基于分布式能力,实现一个多屏互动、跨设备协同的亲子早教系统。

  • 分布式输入法基于分布式能力,将手机作为智慧屏的虚拟控制器,控制文字输入和遥控播放

通知开发指导

场景介绍

HarmonyOS提供了通知功能,即在一个应用的UI界面之外显示的消息,主要用来提醒用户有来自该应用中的信息。当应用向系统发出通知时,它将先以图标的形式显示在通知栏中,用户可以下拉通知栏查看通知的详细信息。常见的使用场景:

  • 显示接收到短消息、即时消息等。

  • 显示应用的推送消息,如广告、版本更新等。

  • 显示当前正在进行的事件,如播放音乐、导航、下载等。

接口说明

通知相关基础类包含NotificationSlotNotificationRequestNotificationHelper。基础类之间的关系如下所示:

  • NotificationSlotNotificationSlot可以对提示音、振动、锁屏显示和重要级别等进行设置。一个应用可以创建一个或多个NotificationSlot,在发布通知时,通过绑定不同的NotificationSlot,实现不同用途。

说明

NotificationSlot需要先通过NotificationHelper的addNotificationSlot(NotificationSlot)方法发布后,通知才能绑定使用;所有绑定该NotificationSlot的通知在发布后都具备相应的特性,对象在创建后,将无法更改这些设置,对于是否启动相应设置,用户有最终控制权。

不指定NotificationSlot时,当前通知会使用默认的NotificationSlot,默认的NotificationSlot优先级为LEVEL_DEFAULT。

接口名 描述
NotificationSlot(String id, String name, int level) 构造NotificationSlot。
setLevel(int level) 设置NotificationSlot的级别。
setName(String name) 设置NotificationSlot的命名。
setDescription(String description) 设置NotificationSlot的描述信息。
enableBypassDnd(boolean bypassDnd) 设置是否绕过系统的免打扰模式。
setEnableVibration(boolean vibration) 设置收到通知时是否使能振动。
setLockscreenVisibleness(int visibleness) 设置在锁屏场景下,收到通知后是否显示,以及显示的效果。
setEnableLight(boolean isLightEnabled) 设置收到通知时是否开启呼吸灯,前提是当前硬件支持呼吸灯。
setLedLightColor(int color) 设置收到通知时的呼吸灯颜色。
setSlotGroup(String groupId) 绑定当前NotificationSlot到一个NotificationSlot组。

NotificationSlot的级别目前支持如下几种, 由低到高:

  • LEVEL_NONE: 表示通知不发布。

  • LEVEL_MIN:表示通知可以发布,但是不显示在通知栏,不自动弹出,无提示音;该级别不适用于前台服务的场景。

  • LEVEL_LOW:表示通知可以发布且显示在通知栏,不自动弹出,无提示音。

  • LEVEL_DEFAULT:表示通知发布后可在通知栏显示,不自动弹出,触发提示音。

  • LEVEL_HIGH:表示通知发布后可在通知栏显示,自动弹出,触发提示音。

  • NotificationRequestNotificationRequest用于设置具体的通知对象,包括设置通知的属性,如:通知的分发时间、小图标、大图标、自动删除等参数,以及设置具体的通知类型,如普通文本、长文本等。

接口名 描述
NotificationRequest() 构建一个通知。
NotificationRequest(int notificationId) 构建一个通知,指定通知的id。通知的Id在应用内容具有唯一性,如果不指定,默认为0。
setNotificationId(int notificationId) 设置当前通知id。
setAutoDeletedTime(long time) 设置通知自动取消的时间戳。
setContent(NotificationRequest.NotificationContent content) 设置通知的具体类型。
setDeliveryTime(long deliveryTime) 设置通知分发的时间戳。
setSlotId(String slotId) 设置通知的NotificationSlot id。
setTapDismissed(boolean tapDismissed) 设置通知在用户点击后是否自动取消。
setLittleIcon(PixelMap smallIcon) 设置通知的小图标,在通知左上角显示。
setBigIcon(PixelMap bigIcon) 设置通知的大图标,在通知的右边显示。
setGroupValue(String groupValue) 设置分组通知,相同分组的通知在通知栏显示时,将会折叠在一组应用中显示。
addActionButton(NotificationActionButton actionButton) 设置通知添加通知ActionButton。
setIntentAgent(IntentAgent agent) 设置通知承载指定的IntentAgent,在通知中实现即将触发的事件。

具体的通知类型:目前支持六种类型,包括普通文本NotificationNormalContent、长文本NotificationLongTextContent、图片NotificationPictureContent、多行NotificationMultiLineContent、社交NotificationConversationalContent、媒体NotificationMediaContent。

类名 接口名 描述
NotificationNormalContent setTitle(String title) 设置通知标题。
NotificationNormalContent setText(String text) 设置通知内容。
NotificationNormalContent setAdditionalText(String additionalText) 设置通知次要内容,是对通知内容的补充。
NotificationPictureContent setBriefText(String briefText) 设置通知概要内容,是对通知内容的总结。
NotificationPictureContent setExpandedTitle(String expandedTitle) 设置附加图片的通知展开时的标题。
NotificationPictureContent setBigPicture(PixelMap bigPicture) 设置通知的图片内容,附加在setText(String text)下方。
NotificationLongTextContent setLongText(String longText) 设置通知的长文本。
NotificationConversationalContent setConversationTitle(String conversationTitle) 设置社交通知的标题。
NotificationConversationalContent addConversationalMessage(ConversationalMessage message) 通知添加一条消息。
NotificationMultiLineContent addSingleLine(String line) 在当前通知中添加一行文本。
NotificationMediaContent setAVToken(AVToken avToken) 将媒体通知绑定指定的AVToken。
NotificationMediaContent setShownActions(int[] actions) 设置媒体通知待展示的按钮。

说明

通知发布后,通知的设置不可修改。如果下次发布通知使用相同的id,就会更新之前发布的通知。

  • NotificationHelperNotificationHelper封装了发布、更新、删除通知等静态方法。
接口名 描述
publishNotification(NotificationRequest request) 发布一条通知。
publishNotification(String tag, NotificationRequest request) 发布一条带TAG的通知。
cancelNotification(int notificationId) 取消指定的通知。
cancelNotification(String tag, int notificationId) 取消指定的带TAG的通知。
cancelAllNotifications() 取消之前发布的所有通知。
addNotificationSlot(NotificationSlot slot) 创建一个NotificationSlot。
getNotificationSlot(String slotId) 获取NotificationSlot。
removeNotificationSlot(String slotId) 删除一个NotificationSlot。
getActiveNotifications() 获取当前应用发的活跃通知。
getActiveNotificationNums() 获取系统中当前应用发的活跃通知的数量。
setNotificationBadgeNum(int num) 设置通知的角标。
setNotificationBadgeNum() 设置当前应用中活跃状态通知的数量在角标显示。

开发步骤

通知的开发指导分为创建NotificationSlot、发布通知和取消通知等开发场景。

创建NotificationSlot

NotificationSlot可以设置公共通知的震动,锁屏模式,重要级别等,并通过调用NotificationHelper.addNotificationSlot()发布NotificationSlot对象。

NotificationSlot slot = new NotificationSlot("slot_001", "slot_default", NotificationSlot.LEVEL_MIN); // 创建notificationSlot对象
slot.setDescription("NotificationSlotDescription");
slot.setEnableVibration(true); // 设置振动提醒
slot.setLockscreenVisibleness(NotificationRequest.VISIBLENESS_TYPE_PUBLIC);// 设置锁屏模式
slot.setEnableLight(true); // 设置开启呼吸灯提醒
slot.setLedLightColor(Color.RED.getValue());// 设置呼吸灯的提醒颜色
try {
   NotificationHelper.addNotificationSlot(slot);
} catch (RemoteException ex) {
   HiLog.error(LABEL, "Exception occurred during addNotificationSlot invocation.");
}

发布通知

  1. 构建NotificationRequest对象,应用发布通知前,通过NotificationRequest的setSlotId()方法与NotificationSlot绑定,使该通知在发布后都具备该对象的特征。
int notificationId = 1;
NotificationRequest request = new NotificationRequest(notificationId);
request.setSlotId(slot.getId());
  1. 调用setContent()设置通知的内容。
String title = "title";
String text = "There is a normal notification content.";
NotificationNormalContent content = new NotificationNormalContent();
content.setTitle(title)
       .setText(text);
NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
request.setContent(notificationContent); // 设置通知的内容
  1. 调用publishNotification()发布通知。
try {
   NotificationHelper.publishNotification(request);
} catch (RemoteException ex) {
   HiLog.error(LABEL, "Exception occurred during publishNotification invocation.");
}

取消通知

取消通知分为取消指定单条通知和取消所有通知,应用只能取消自己发布的通知。

  • 调用cancelNotification()取消指定的单条通知。
int notificationId = 1;
try {
    NotificationHelper.cancelNotification(notificationId);
} catch (RemoteException ex) {
    HiLog.error(LABEL, "Exception occurred during cancelNotification invocation.");
}
  • 调用cancelAllNotifications()取消所有通知。
try {
    NotificationHelper.cancelAllNotifications();
} catch (RemoteException ex) {
    HiLog.error(LABEL, "Exception occurred during cancelAllNotifications invocation.");
}

相关实例

针对通知开发,有以下示例工程可供参考:

  • Notification通知功能,即在一个应用的UI界面之外显示的消息,主要用来提醒用户有来自该应用中的信息。本示例演示了如何发布通知和取消通知

IntentAgent开发指导

场景介绍

IntentAgent封装了一个指定行为的Intent,可以通过triggerIntentAgent接口主动触发,也可以与通知绑定被动触发。具体的行为包括:启动Ability和发布公共事件。例如:收到通知后,在点击通知后跳转到一个新的Ability,不点击则不会触发。

接口说明

IntentAgent相关基础类包括IntentAgentHelperIntentAgentInfoIntentAgentConstantTriggerInfo,基础类之间的关系如下图所示:

  • IntentAgentHelperIntentAgentHelper封装了获取、激发、取消IntentAgent等静态方法。
接口名 描述
getIntentAgent(Context context, IntentAgentInfo paramsInfo) 获取一个IntentAgent实例。
triggerIntentAgent(Context context, IntentAgent agent, IntentAgent.Oncompleted onCompleted, EventHandler handler, TriggerInfo paramsInfo) 主动激发一个IntentAgent实例。
cancel(IntentAgent agent) 取消一个IntentAgent实例。
judgeEquality(IntentAgent agent, IntentAgent otherAgent) 判断两个IntentAgent实例是否相等。
getHashCode(IntentAgent agent) 获取一个IntentAgent实例的哈希码。
getBundleName(IntentAgent agent) 获取一个IntentAgent实例的包名。
getUid(IntentAgent agent) 获取一个IntentAgent实例的用户ID。
  • IntentAgentInfo

    IntentAgentInfo类封装了获取一个IntentAgent实例所需的数据。使用构造函数IntentAgentInfo(int requestCode, OperationType operationType, List flags, List intents, IntentParams extraInfo)获取IntentAgentInfo对象。

    • requestCode:使用者定义的一个私有值。
  • operationType:为IntentAgentConstant.OperationType枚举中的值。

  • flags:为IntentAgentConstant.Flags枚举中的值。

  • intents:将被执行的意图列表。operationType的值为START_ABILITY,START_SERVICE和SEND_COMMON_EVENT时,intents列表只允许包含一个Intent;operationType的值为START_ABILITIES时,intents列表允许包含多个Intent

  • extraInfo:表明如何启动一个有页面的ability,可以为null,只在operationType的值为START_ABILITY和START_ABILITIES时有意义。

  • IntentAgentConstantIntentAgentConstant类中包含OperationType和Flags两个枚举类:

类名 枚举值
IntentAgentConstant.OperationType UNKNOWN_TYPE:不识别的类型。START_ABILITY:开启一个有页面的Ability。START_ABILITIES:开启多个有页面的Ability。START_SERVICE:开启一个无页面的ability。SEND_COMMON_EVENT:发送一个公共事件。
IntentAgentConstant.Flags ONE_TIME_FLAG:IntentAgent仅能使用一次。只在operationType的值为START_ABILITY,START_SERVICE和SEND_COMMON_EVENT时有意义。NO_BUILD_FLAG:如果描述IntentAgent对象不存在,则不创建它,直接返回null。只在operationType的值为START_ABILITY,START_SERVICE和SEND_COMMON_EVENT时有意义。CANCEL_PRESENT_FLAG:在生成一个新的IntentAgent对象前取消已存在的一个IntentAgent对象。只在operationType的值为START_ABILITY,START_SERVICE和SEND_COMMON_EVENT时有意义。UPDATE_PRESENT_FLAG:使用新的IntentAgent的额外数据替换已存在的IntentAgent中的额外数据。只在operationType的值为START_ABILITY,START_SERVICE和SEND_COMMON_EVENT时有意义。CONSTANT_FLAG:IntentAgent是不可变的。REPLACE_ELEMENT:当前Intent中的element属性可被IntentAgentHelper.triggerIntentAgent()中Intent的element属性取代。REPLACE_ACTION: 当前Intent中的action属性可被IntentAgentHelper.triggerIntentAgent()中Intent的action属性取代。REPLACE_URI:当前Intent中的uri属性可被IntentAgentHelper.triggerIntentAgent()中Intent的uri属性取代。REPLACE_ENTITIES:当前Intent中的entities属性可被IntentAgentHelper.triggerIntentAgent()中Intent的entities属性取代。REPLACE_BUNDLE:当前Intent中的bundleName属性可被IntentAgentHelper.triggerIntentAgent()中Intent的bundleName属性取代。
  • TriggerInfo

    TriggerInfo类封装了主动激发一个IntentAgent实例所需的数据,使用构造函数TriggerInfo(String permission, IntentParams extraInfo, Intent intent, int code)获取TriggerInfo对象。

    • permission:IntentAgent的接收者的权限名称,只在operationType的值为SEND_COMMON_EVENT时,该参数才有意义。
  • extraInfo:激发IntentAgent时用户自定义的额外数据。

  • intent:额外的Intent。如果IntentAgentInfo成员变量flags包含CONSTANT_FLAG,则忽略该参数;如果flags包含REPLACE_ELEMENT,REPLACE_ACTION,REPLACE_URI,REPLACE_ENTITIES或REPLACE_BUNDLE,则使用额外Intent的element,action,uri,entities或bundleName属性替换原始Intent中对应的属性。如果intent是空,则不替换原始Intent的属性。

  • code:提供给IntentAgent目标的结果码。

开发步骤

获取IntentAgent的代码示例如下:

// 指定要启动的Ability的BundleName和AbilityName字段
// 将Operation对象设置到Intent中
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("")
        .withBundleName("com.testintentagent")
        .withAbilityName("com.testintentagent.entry.IntentAgentAbility")
        .build();
intent.setOperation(operation);
List<Intent> intentList = new ArrayList<>();
intentList.add(intent);
// 定义请求码
int requestCode = 200;
// 设置flags
List<IntentAgentConstant.Flags> flags = new ArrayList<>();
flags.add(IntentAgentConstant.Flags.UPDATE_PRESENT_FLAG);
// 指定启动一个有页面的Ability
IntentAgentInfo paramsInfo = new IntentAgentInfo(requestCode, IntentAgentConstant.OperationType.START_ABILITY, flags, intentList, null);
// 获取IntentAgent实例
IntentAgent agent = IntentAgentHelper.getIntentAgent(this, paramsInfo);
通知中添加IntentAgent的代码示例如下:
int notificationId = 1;
NotificationRequest request = new NotificationRequest(notificationId);
String title = "title";
String text = "There is a normal notification content.";
NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
content.setTitle(title)
       .setText(text);
NotificationContent notificationContent = new NotificationContent(content);
request.setContent(notificationContent); // 设置通知的内容
request.setIntentAgent(agent); // 设置通知的IntentAgent
主动激发IntentAgent的代码示例如下:
int code = 100;
IntentAgentHelper.triggerIntentAgent(this, agent, null, null, new TriggerInfo(null, null, null, code));

相关实例

针对IntentAgent开发指导,有以下示例工程可供参考:

  • IntentAgent本示例演示了如何通过IntentAgent启动Ability和发布公共事件。

服务卡片

服务卡片(以下简称“卡片”)是FA的一种界面展示形式,将FA的重要信息或操作前置到卡片,以达到服务直达,减少体验层级目的。

卡片常用于嵌入到其他应用(当前只支持系统应用)中作为其界面的一部分显示,并支持拉起页面,发送消息等基础的交互功能。卡片使用方负责显示卡片。

示例如下图所示。

基本概念

  • 卡片使用方显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。

  • 卡片管理服务用于管理系统中所添加卡片的常驻代理服务,包括卡片对象的管理与使用,以及卡片周期性刷新等。

  • 卡片提供方提供卡片显示内容的HarmonyOS应用或原子化服务,控制卡片的显示内容、控件布局以及控件点击事件。

说明

卡片使用方和提供方不要求常驻运行,在需要添加/删除/请求更新卡片时,卡片管理服务会拉起卡片提供方获取卡片信息。

运作机制

卡片管理服务包含以下模块:

  • 周期性刷新:在卡片添加后,根据卡片的刷新策略启动定时任务周期性触发卡片的刷新。

  • 卡片缓存管理:在卡片添加到卡片管理服务后,对卡片的视图信息进行缓存,以便下次获取卡片时可以直接返回缓存数据,降低时延。

  • 卡片生命周期管理:对于卡片切换到后台或者被遮挡时,暂停卡片的刷新;以及卡片的升级/卸载场景下对卡片数据的更新和清理。

  • 卡片使用方对象管理:对卡片使用方的RPC对象进行管理,用于使用方请求进行校验以及对卡片更新后的回调处理。

  • 通信适配层:负责与卡片使用方和提供方进行RPC通信。

卡片提供方包含以下模块:

  • 卡片服务:由卡片提供方开发者实现,开发者实现onCreateForm、onUpdateForm和onDeleteForm处理创建卡片、更新卡片以及删除卡片等请求,提供相应的卡片服务。

  • 卡片提供方实例管理模块:由卡片提供方开发者实现,负责对卡片管理服务分配的卡片实例进行持久化管理。

  • 通信适配层:由HarmonyOS SDK提供,负责与卡片管理服务通信,用于将卡片的更新数据主动推送到卡片管理服务

场景介绍

卡片提供方控制卡片实际显示的内容、控件布局以及控件点击事件。开发者可以通过集成以下接口来提供卡片服务。

接口说明

HarmonyOS中的服务卡片为卡片提供方开发者提供以下接口能力。

类名 接口名 描述
Ability ProviderFormInfo onCreateForm(Intent intent) 卡片提供方接收创建卡片通知接口。
void onUpdateForm(long formId) 卡片提供方接收更新卡片通知接口。
void onDeleteForm(long formId) 卡片提供方接收删除卡片通知接口。
void onTriggerFormEvent(long formId, String message) 卡片提供方处理卡片事件接口(JS卡片使用)。
boolean updateForm(long formId, ComponentProvider component) 卡片提供方主动更新卡片(Java卡片使用)。
boolean updateForm(long formId, FormBindingData formBindingData) 卡片提供方主动更新卡片(JS卡片使用),仅更新formBindingData中携带的信息,卡片中其余信息保持不变。
void onCastTempForm(long formId) 卡片提供方接收临时卡片转常态卡片通知。
void onEventNotify(Map<Long, Integer> formEvents) 卡片提供方接收到事件通知,其中Ability.FORM_VISIBLE表示卡片可见通知,Ability.FORM_INVISIBLE表示卡片不可见通知。
ProviderFormInfo ProviderFormInfo(int resId, Context context) Java卡片返回对象构造函数。
ProviderFormInfo() JS卡片返回对象构造函数。
void mergeActions(ComponentProvider componentProviderActions) 在提供方侧调用该接口,将开发者在ComponentProvider中设置的actions配置数据合并到当前对象中。
void setJsBindingData(FormBindingData data) 设置JS卡片的内容信息(JS卡片使用)。

其中,onEventNotify仅系统应用才会回调,其他接口回调时机如下图:

说明

卡片管理服务不负责保持卡片的活跃状态(设置了定时更新的除外),当使用方作出相应的请求时,管理服务会拉起提供方并回调相应接口。

Java卡片与JS卡片选型指导

Java/JS卡片场景能力差异如下表所示:

场景 Java卡片 JS卡片 支持的版本
实时刷新(类似时钟) Java使用ComponentProvider做实时刷新代价比较大 JS可以做到端侧刷新,但是需要定制化组件 HarmonyOS 2.0及以上
开发方式 Java UI在卡片提供方需要同时对数据和组件进行处理,生成ComponentProvider远端渲染 JS卡片在使用方加载渲染,提供方只要处理数据、组件和逻辑分离 HarmonyOS 2.0及以上
组件支持 Text、Image、DirectionalLayout、PositionLayout、DependentLayout div、list、list-item、swiper、stack、image、text、span、progress、button(定制:chart 、clock、calendar) HarmonyOS 2.0及以上
卡片内动效 不支持 暂不开放 HarmonyOS 2.0及以上
阴影模糊 不支持 支持 HarmonyOS 2.0及以上
动态适应布局 不支持 支持 HarmonyOS 2.0及以上
自定义卡片跳转页面 不支持 支持 HarmonyOS 2.0及以上

综上所述,JS卡片比JAVA卡片支持的控件和能力都更丰富:

Java卡片:适合作为一个直达入口,没有复杂的页面和事件。

JS卡片:适合有复杂界面的卡片。

约束与限制

对于同一个Page ability,在config.json中最多支持配置16张卡片

Java卡片开发指导

  1. 使用DevEco Studio创建卡片工程。卡片应用是一款特殊的元能力服务,其配置文件config.json中声明以下几项,系统能够识别该应用为一款卡片应用,并与系统进行绑定。

config.json文件”abilities”配置forms模块细节如下,各属性详情可见表。

"forms": [
  {
    "name": "Form_Java",
    "description": "form_description",
    "type": "Java",
    "colorMode": "auto",
    "isDefault": true,
    "updateEnabled": true,
    "scheduledUpateTime": "10:30",
    "updateDuration": 1,
    "defaultDimension": "2*2",
    "formVisibleNotify": true,
    "supportDimensions": [
      "1*2",
      "2*2",
      "2*4",
      "4*4"
    ],
    "landscapeLayouts": [
      "$layout:form_ability_layout_1_2",
      "$layout:form_ability_layout_2_2",
      "$layout:form_ability_layout_2_4",
      "$layout:form_ability_layout_4_4"
    ],
    "portraitLayouts": [
      "$layout:form_ability_layout_1_2",
      "$layout:form_ability_layout_2_2",
      "$layout:form_ability_layout_2_4",
      "$layout:form_ability_layout_4_4"
    ],
    "formConfigAbility": "ability://SecondFormAbility",
    "metaData": {
      "customizeData": [
        {
          "name": "originWidgetName",
          "value": "com.huawei.weather.testWidget"
        }
      ]
    }
  }
]

说明

“forms”模块中的name为卡片名,即在onCreateForm中根据AbilitySlice.PARAM_FORM_NAME_KEY可取到的值。

除此之外,在卡片所在的”abilities”中还需要配置”visible”: true和”formsEnabled”: true。

属性名称 子属性名称 含义 数据类型 是否可缺省
name - 表示卡片的类名。字符串最大长度为127字节。 字符串
description - 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为255字节。 字符串 可缺省,缺省为空。
isDefault - 表示该卡片是否为默认卡片,每个Ability有且只有一个默认卡片。true:默认卡片。false:非默认卡片。 布尔值
type - 表示卡片的类型。取值范围如下:Java:Java卡片。JS:JS卡片。 字符串
colorMode - 表示卡片的主题样式,取值范围如下:auto:自适应。dark:深色主题。light:浅色主题。 字符串 可缺省,缺省值为“auto”。
supportDimensions - 表示卡片支持的外观规格,取值范围:12:表示1行2列的二宫格。22:表示2行2列的四宫格。24:表示2行4列的八宫格。44:表示4行4列的十六宫格。 字符串数组
defaultDimension - 表示卡片的默认外观规格,取值必须在该卡片supportDimensions配置的列表中。 字符串
landscapeLayouts - 表示卡片外观规格对应的横向布局文件,与supportDimensions中的规格一一对应。仅当卡片类型为Java卡片时,需要配置该标签。 字符串数组
portraitLayouts - 表示卡片外观规格对应的竖向布局文件,与supportDimensions中的规格一一对应。仅当卡片类型为Java卡片时,需要配置该标签。 字符串数组
updateEnabled - 表示卡片是否支持周期性刷新,取值范围:true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,优先选择定时刷新。false:表示不支持周期性刷新。 布尔类型
scheduledUpdateTime - 表示卡片的定点刷新的时刻,采用24小时制,精确到分钟。 字符串 可缺省,缺省值为“0:0”。
updateDuration - 表示卡片定时刷新的更新周期,单位为30分钟,取值为自然数。当取值为0时,表示该参数不生效。当取值为正整数N时,表示刷新周期为30*N分钟。 数值 可缺省,缺省值为“0”。
formConfigAbility - 表示卡片的配置跳转链接,采用URI格式。 字符串 可缺省,缺省值为空。
jsComponentName - 表示JS卡片的Component名称。字符串最大长度为127字节。仅当卡片类型为JS卡片时,需要配置该标签。 字符串
metaData - 表示卡片的自定义信息,包含customizeData数组标签。 对象 可缺省,缺省值为空。
customizeData - 表示自定义的卡片信息。 对象数组 可缺省,缺省值为空。
name 表示数据项的键名称。字符串最大长度为255字节。 字符串 可缺省,缺省值为空。
value 表示数据项的值。字符串最大长度为255字节。 字符串 可缺省,缺省值为空。
  1. 创建一个FormAbility,覆写卡片相关回调函数。
  • onCreateForm(Intent intent)

  • onUpdateForm(long formId)

  • onDeleteForm(long formId)

  • onCastTempForm(long formId)

  • onEventNotify(Map<Long, Integer> formEvents)

在onCreateForm(Intent intent)中,当卡片使用方请求获取卡片时,卡片提供方会被拉起并调用onCreateForm(Intent intent)回调,intent中会带有卡片ID,卡片名称,临时卡片标记和卡片外观规格信息,分别通过AbilitySlice.PARAM_FORM_IDENTITY_KEY、AbilitySlice.PARAM_FORM_NAME_KEY、AbilitySlice.PARAM_FORM_TEMORARY_KEY和AbilitySlice.PARAM_FORM_DIMENSION_KEY按需获取。

提供方可以通过AbilitySlice.PARAM_FORM_CUSTOMIZE_KEY获取卡片使用方设置的自定义数据。

public class FormAbility extends Ability {
    ......
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        ......
    }
    @Override
    protected ProviderFormInfo onCreateForm(Intent intent) {
        long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_IDENTITY_KEY, 0);
        String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
        int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
        boolean tempFlag = intent.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);

        // 获取自定义数据
        IntentParams intentParams = intent.getParam(AbilitySlice.PARAM_FORM_CUSTOMIZE_KEY);

        HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);
        // 开发者需要根据卡片的名称以及外观规格获取对应的xml布局并构造卡片对象,此处ResourceTable.Layout_form_ability_layout_2_2仅为示例
        ProviderFormInfo formInfo = new ProviderFormInfo(ResourceTable.Layout_form_ability_layout_2_2, this);
        // 获取卡片信息
        String formData = getInitFormData(formName, specificationId);
        ComponentProvider componentProvider = new ComponentProvider();
        componentProvider.setText(ResourceTable.Id_title, "formData-" + formData);
        formInfo.mergeActions(componentProvider);
        ......
        HiLog.info(LABEL_LOG, "onCreateForm finish.......");
        return formInfo;
    }

    @Override
    protected void onDeleteForm(long formId) {
        super.onDeleteForm(formId);
        // 删除卡片实例数据,需要由开发者实现
        deleteFormInfo(formId);
        ......
    }

    @Override
    // 若卡片支持定时更新/定点更新/卡片使用方主动请求更新功能,则提供方需要覆写该方法以支持数据更新
    protected void onUpdateForm(long formId) {
        super.onUpdateForm(formId);
        // 更新卡片信息,由开发者实现
        ......
    }

    @Override
    protected void onCastTempForm(long formId) {
        // 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理,将数据持久化。
        super.onCastTempForm (formId);
        ......
    }

    @Override
    protected void onEventNotify(Map<Long, Integer> formEvents) {
        // 使用方发起可见或者不可见通知触发,提供方需要做相应的处理,比如卡片可见时刷新卡片,仅系统应用能收到该回调。
        super.onEventNotify(formEvents);
        ......
    }
}
  1. 卡片信息持久化。因大部分卡片提供方都不是常驻服务,只有在需要使用时才会被拉起获取卡片信息。且卡片框架支持对卡片进行多实例管理,卡片ID对应实例ID,因此若卡片提供方支持对卡片数据进行配置,则需要提供方对卡片的业务数据按照卡片ID进行持久化管理,以便在后续获取、更新以及拉起时能获取到正确的卡片业务数据。

同时,需要适配onDeleteForm(int formId)卡片删除通知接口,在其中实现卡片实例数据的删除。和JS卡片相同,需要注意卡片使用方在请求卡片时传递给提供方应用的Intent数据中存在临时标记字段,表示此次请求的卡片是否为临时卡片,由于临时卡片的数据不会持久化的特殊性,某些场景比如卡片服务框架死亡重启,此时临时卡片数据在卡片服务框架中已经删除,且对应的卡片ID不会通知到提供方,所以卡片提供方需要自己负责清理长时间未删除的临时卡片数据。同时对应的卡片使用方可能会将之前请求的临时卡片转换为常态卡片。如果转换成功,卡片提供方也需要对对应的临时卡片ID进行处理,把卡片提供方记录的临时卡片数据转换为常态卡片数据,防止提供方在清理长时间未删除的临时卡片时,把已经转换为常态卡片的临时卡片信息删除,导致卡片信息丢失。

@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
    long formId = intent.getLongParam(AbilitySlice.PARAM_FORM_ID_KEY, -1L);
    String formName = intent.getStringParam(AbilitySlice.PARAM_FORM_NAME_KEY);
    int specificationId = intent.getIntParam(AbilitySlice.PARAM_FORM_DIMENSION_KEY, 0);
    boolean tempFlag = params.getBooleanParam(AbilitySlice.PARAM_FORM_TEMPORARY_KEY, false);
    HiLog.info(LABEL_LOG, "onCreateForm: " + formId + " " + formName + " " + specificationId);

    .......
    // 将创建的卡片信息持久化,以便在下次获取/更新该卡片实例时进行使用,该方法需要由开发者实现。
    storeFormInfo(formId, formName, specificationId, formData);
    ......
    HiLog.info(LABEL_LOG, "onCreateForm finish.......");
    return formInfo;
}
@Override
protected void onDeleteForm(long formId) {
    super.onDeleteForm(formId);
    // 由开发人员自行实现,删除卡片实例数据
    deleteFormInfo(formId);
    ......
}
@Override
protected void onCastTempForm(long formId) {
    // 使用方将临时卡片转换为常态卡片触发,提供方需要做相应的处理
    super.onCastTempForm (formId);
    ......
}
  1. 卡片数据更新。当需要卡片提供方更新数据时(如触发了定时更新,定点更新,或者卡片使用方主动请求更新),卡片提供方获取最新数据,并调用updateForm接口更新卡片。示例如下:
@Override
protected void onUpdateForm(long formId) {
    super.onUpdateForm(formId);
    ComponentProvider componentProvider = new ComponentProvider(ResourceTable.Layout_form_ability_layout_2_2, this);
    // 获取卡片实例需要更新的卡片数据,需要由开发者实现
    String formData = getUpdateFormData(formId);
    componentProvider.setText(ResourceTable.Id_title, "update formData-" + formData);
    updateForm(formId, componentProvider);
    ......
}

卡片使用方点击拉起卡片页面,会在onStart(Intent intent)中携带formId(通过AbilitySlice.PARAM_FORM_IDENTITY_KEY获取),若需要在AbilitySlice中更新,也可以使用updateForm接口进行更新,示例如下:

public class FormAbilitySlice extends AbilitySlice {
    ......
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        ......
        Button button = new Button(this);
        button.setText("Update form data");
        button.setClickedListener(component -> {
            ......
            if (intent.hasParameter(AbilitySlice.PARAM_FORM_IDENTITY_KEY)) {
                int formId = intent.getIntParam(AbilitySlice.PARAM_FORM_ID_KEY, -1);
                ComponentProvider componentProvider = new ComponentProvider(ResourceTable.Layout_form_ability_layout_2_2, context);
                String formData = getUpdateFormData(formId);
                componentProvider.setText(ResourceTable.Id_modifylayout, "update formData-" + formData);
                getAbility().updateForm(formId, componentProvider);
            }
        });
        ......
    }
}
  1. Java卡片控制事件。Java卡片当前通过IntentAgent能力支持对卡片控制设置事件,例如可以使用START_ABILITY、START_SERVICE这两类能力,在点击整张卡片时,跳转到提供卡片的ability。

示例如下:

@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
    ......
    ProviderFormInfo formInfo = new ProviderFormInfo(ResourceTable.Layout_form_ability_layout_2_2, this);
    ComponentProvider componentProvider = new ComponentProvider();
    // 针对title控件设置事件
    componentProvider.setIntentAgent(ResourceTable.Id_title, startAbilityIntentAgent());
    formInfo.mergeActions(componentProvider);
    ......
    return formInfo;
}

// 设置触发的事件为系统预置的HarmonyOS betaApp应用
private IntentAgent startAbilityIntentAgent() {
    Intent intent = new Intent();
    Operation operation = new Intent.OperationBuilder()
            .withDeviceId("")
            .withBundleName("com.huawei.ohos.betaapp.link")
            .withAbilityName("com.huawei.ohos.betaapp.link.MainAbility")
            .build();
    intent.setOperation(operation);
    List<Intent> intentList = new ArrayList<>();
    intentList.add(intent);
    List<Flags> flags = new ArrayList<>();
    flags.add(Flags.UPDATE_PRESENT_FLAG);
    IntentAgentInfo paramsInfo = new IntentAgentInfo(200, IntentAgentConstant.OperationType.START_ABILITY, flags, intentList, null);
    IntentAgent intentAgent = IntentAgentHelper.getIntentAgent(this, paramsInfo);
    return intentAgent;
}
  1. 开发Java卡片布局。在使用DevEco Studio创建模块时会生成对应的Java UI xml布局文件,具体规则请参考《XML创建布局》,需要注意设置ohos:remote=”true”。

以下是天气卡片xml布局示例,供参考:

<?xml version="1.0" encoding="utf-8"?>
<DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos"
                 ohos:width="match_parent"
                 ohos:height="match_parent"
                 ohos:id="$+id:background"
                 ohos:orientation="vertical"
                 ohos:background_element="$media:weather"
                 ohos:remote="true">
        <Text
                ohos:id="$+id:title"
                ohos:text="天气1"
                ohos:text_size="39px"
                ohos:text_color="#b0c4de"
                ohos:top_margin="42px"
                ohos:left_margin="20px"
                ohos:width="match_content"
                ohos:height="match_content"/>
        <Text
                ohos:id="$+id:temperature"
                ohos:text="35°"
                ohos:text_size="100px"
                ohos:text_color="#b0c4de"
                ohos:top_margin="25px"
                ohos:left_margin="20px"
                ohos:below="$id:title"
                ohos:width="match_content"
                ohos:height="match_content"/>
        <Text
                ohos:id="$+id:location"
                ohos:text="上海"
                ohos:text_size="39px"
                ohos:text_color="#b0c4de"
                ohos:top_margin="24px"
                ohos:left_margin="20px"
                ohos:below="$id:temperature"
                ohos:width="match_content"
                ohos:height="match_content"/>
        <Text
                ohos:id="$+id:textView4"
                ohos:text="9月4号 星期五"
                ohos:text_size="39px"
                ohos:text_color="#b0c4de"
                ohos:top_margin="10px"
                ohos:left_margin="20px"
                ohos:below="$id:location"
                ohos:width="match_content"
                ohos:height="match_content"/>

        <Text
                ohos:id="$+id:textView5"
                ohos:text="多云"
                ohos:text_size="39px"
                ohos:text_color="#b0c4de"
                ohos:top_margin="10px"
                ohos:left_margin="150px"
                ohos:below="$id:location"
                ohos:end_of="$id:textView4"
                ohos:align_parent_end="true"
                ohos:width="match_content"
                ohos:height="match_content"/>

        <Image
                ohos:id="$+id:imageView"
                ohos:width="160px"
                ohos:height="150px"
                ohos:top_margin="20px"
                ohos:left_margin="150px"
                ohos:below="$id:title"
                ohos:end_of="$id:temperature"
                ohos:image_src="$media:clouds"/>
</DependentLayout>

剪贴板

用户通过系统剪贴板服务,可实现应用之间的简单数据传递。例如:在应用A中复制的数据,可以在应用B中粘贴,反之亦可。

  • HarmonyOS提供系统剪贴板服务的操作接口,支持用户程序从系统剪贴板中读取、写入和查询剪贴板数据,以及添加、移除系统剪贴板数据变化的回调。

  • HarmonyOS提供剪贴板数据的对象定义,包含内容对象和属性对象

场景介绍

同一设备的应用程序A、B之间可以借助系统剪贴板服务完成简单数据的传递,即应用程序A向剪贴板服务写入数据后,应用程序B可以从中读取出数据。

在使用剪贴板服务时,需要注意以下几点:

  • 只有在前台获取到焦点的应用才有读取系统剪贴板的权限(系统默认输入法应用除外)。

  • 写入到剪贴板服务中的剪贴板数据不会随应用程序结束而销毁。

  • 对同一用户而言,写入剪贴板服务的数据会被下一次写入的剪贴板数据所覆盖。

  • 在同一设备内,剪贴板单次传递内容不应超过500KB。

接口说明

SystemPasteboard提供系统剪贴板操作的相关接口,比如复制、粘贴、配置回调等。PasteData是剪贴板服务操作的数据对象,一个PasteData由若干个内容节点(PasteData.Record)和一个属性集合对象(PasteData.DataProperty)组成。Record是存放剪贴板数据内容信息的最小单位,每个Record都有其特定的MIME类型,如纯文本、HTML、URI、Intent。剪贴板数据的属性信息存在放PasteData.DataProperty中,包括标签、时间戳等。

SystemPasteboard

SystemPasteboard提供系统剪贴板服务的操作接口,比如复制、粘贴、配置回调等。

接口名 描述
getSystemPasteboard(Context context) 获取系统剪切板服务的对象实例。
getPasteData() 读取当前系统剪贴板中的数据。
hasPasteData() 判断当前系统剪贴板中是否有内容。
setPasteData(PasteData data) 将剪贴板数据写入到系统剪贴板。
clear() 清空系统剪贴板数据。
addPasteDataChangedListener(IPasteDataChangedListener listener) 用户程序添加系统剪贴板数据变化的回调,当系统剪贴板数据发生变化时,会触发用户程序的回调实现。
removePasteDataChangedListener(IPasteDataChangedListener listener) 用户程序移除系统剪贴板数据变化的回调。

PasteData

PasteData是剪贴板服务操作的数据对象,其中内容节点定义为PasteData.Record,属性集合定义为PasteData.DataProperty。

接口名 描述
PasteData() 构造器,创建一个空内容数据对象。
creatPlainTextData(CharSequence text) 构建一个包含纯文本内容节点的数据对象。
creatHtmlData(String htmlText) 构建一个包含HTML内容节点的数据对象。
creatUriData(Uri uri) 构建一个包含URI内容节点的数据对象。
creatIntentData(Intent intent) 构建一个包含Intent内容节点的数据对象。
getPrimaryMimeType() 获取数据对象中首个内容节点的MIME类型,如果没有查询到内容,将返回一个空字符串。
getPrimaryText() 获取数据对象中首个内容节点的纯文本内容,如果没有查询到内容,将返回一个空对象。
addTextRecord(CharSequence text) 向数据对象中添加一个纯文本内容节点,该方法会自动更新数据属性中的MIME类型集合,最多只能添加128个内容节点。
addRecord(Record record) 向数据对象中添加一个内容节点,该方法会自动更新数据属性中的MIME类型集合,最多只能添加128个内容节点。
getRecordCount() 获取数据对象中内容节点的数量。
getRecordAt(int index) 获取数据对象在指定下标处的内容节点,如果操作失败会返回空对象。
removeRecordAt(int index) 移除数据对象在指定下标处的内容节点,如果操作成功会返回true,操作失败会返回false。
getMimeTypes() 获取数据对象中上所有内容节点的MIME类型列表,当内容节点为空时,返回列表为空对象。
getProperty() 获取该数据对象的属性集合成员。
常量名 描述
MIMETYPE_TEXT_PLAIN= “text/plain” 纯文本的MIME类型定义。
MIMETYPE_TEXT_HTML= “text/html” HTML的MIME类型定义。
MIMETYPE_TEXT_URI= “text/uri” URI的MIME类型定义。
MIMETYPE_TEXT_INTENT= “text/ohos.intent” Intent的MIME类型定义。
MAX_RECORD_NUM=128 单个PasteData中所能包含的Record的数量上限。

PasteData.Record

一个PasteData中包含若干个特定MIME类型的PasteData.Record,每个Record是存放剪贴板数据内容信息的最小单位。

接口名 描述
createPlainTextRecord(CharSequence text) 构造一个MIME类型为纯文本的内容节点。
createHtmlTextRecord(String htmlText) 构造一个MIME类型为HTML的内容节点。
createUriRecord(Uri uri) 构造一个MIME类型为URI的内容节点。
createIntentRecord(Intent intent) 构造一个MIME类型为Intent的内容节点。
getPlainText() 获取该内容节点中的文本内容,如果没有内容将返回空对象。
getHtmlText() 获取该内容节点中的HTML内容,如果没有内容将返回空对象。
getUri() 获取该内容节点中的URI内容,如果没有内容将返回空对象。
getIntent() 获取该内容节点中的Intent内容,如果没有内容将返回空对象。
getMimeType() 获取该内容节点的MIME类型。
convertToText(Context context) 将该内容节点的内容转为文本形式。

PasteData.DataProperty

每个PasteData中都有一个PasteData.DataProperty成员,其中存放着该数据对象的属性集合,例如自定义标签、MIME类型集合列表等。

接口名 描述
getMimeTypes() 获取所属数据对象的MIME类型集合列表,当内容节点为空时,返回列表为空对象。
hasMimeType(String mimeType) 判断所属数据对象中是否包含特定MIME类型的内容。
getTimestamp() 获取所属数据对象被写入系统剪贴板时的时间戳,如果该数据对象尚未被写入,则返回0。
setTag(CharSequence tag) 设置自定义标签。
getTag() 获取自定义标签。
setAdditions(PacMap extraProps) 设置一些附加键值对信息。
getAdditions() 获取附加键值对信息。

IPasteDataChangedListener

IPasteDataChangedListener是定义剪贴板数据变化回调的接口类,开发者需要实现此接口来编码触发回调时的处理逻辑。

接口名 描述
onChanged() 当系统剪贴板数据发生变化时的回调接口。

开发步骤

  1. 应用A获取系统剪贴板服务。
SystemPasteboard pasteboard = SystemPasteboard.getSystemPasteboard(appContext);
  1. 应用A向系统剪贴板中写入一条纯文本数据。
if (pasteboard != null) {
    pasteboard.setPasteData(PasteData.creatPlainTextData("Hello, world!"));
}
  1. 应用B从系统剪贴板读取数据,将数据对象中的首个文本类型(纯文本/HTML)内容信息在控件中显示,忽略其他类型内容。
PasteData pasteData = pasteboard.getPasteData();
if (pasteData == null) {
    return;
}
DataProperty dataProperty = pasteData.getProperty();
boolean hasHtml = dataProperty.hasMimeType(PasteData.MIMETYPE_TEXT_HTML);
boolean hasText = dataProperty.hasMimeType(PasteData.MIMETYPE_TEXT_PLAIN);
if (hasHtml || hasText) {
    Text text = (Text) findComponentById(ResourceTable.Id_text);
    for (int i = 0; i < pasteData.getRecordCount(); i++) {
        PasteData.Record record = pasteData.getRecordAt(i);
        String mimeType = record.getMimeType();
        if (mimeType.equals(PasteData.MIMETYPE_TEXT_HTML)) {
            text.setText(record.getHtmlText());
            break;
        } else if (mimeType.equals(PasteData.MIMETYPE_TEXT_PLAIN)) {
            text.setText(record.getPlainText().toString());
            break;
        } else {
            // skip records of other Mime type 
        }
    }
}
  1. 应用C注册添加系统剪贴板数据变化回调,当系统剪贴板数据发生变化时触发处理逻辑。
IPasteDataChangedListener listener = new IPasteDataChangedListener() {
    @Override
    public void onChanged() {
        PasteData pasteData = pasteboard.getPasteData();
        if (pasteData == null) {
            return;
        }
        // Operations to handle data change on the system pasteboard
    }
};
pasteboard.addPasteDataChangedListener(listener);

相关实例

针对剪贴板开发指导,有以下示例工程可供参考:

  • Pasteboard本示例演示了应用之间的数据剪贴

线程

UI

媒体

安全

AI

网络连接

设备管理

数据管理

日志管理

原子化服务

流转


文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录