作者:陈忠蔚

前言

现在随着个人设备越来越多,越来越需要多个设备之间相互感知和连接,设备和设备之间可以相互联动,形成互联互通的场景,而搭载HarmonyOS的设备恰好可以满足这一点 。下面通过开发一个HarmonyOS的多端分布式表白应用来实现设备之间的相互联动。

项目介绍

H5页面可以实现一些比较特殊的页面效果,所以选择在应用中集成H5页面。应用可以将页面直接投放到附近其他HarmonyOS设备上,实现多端设备分布式显示,同时应用可以跨端控制,更新应用页面,形成多设备协同的效果。

下面是效果展示:
表白效果展示.gif

多设备协同原理

HarmonyOS 给应用开发者提供了一套在多个设备不同应用之间进行任务流转的API接口,实现设备协同需要关注 流转任务管理服务分布式任务调度

流转任务管理服务:在流转发起端,接受用户应用程序注册,提供流转入口、状态显示、退出流转等管理能力。

分布式任务调度:提供远程服务启动、远程服务连接、远程迁移等能力,并通过不同能力组合,支撑用户应用程序完成跨端迁移或多端协同的业务体验。

分布式安全:提供E2E的加密通道,为用户应用程序提供安全的跨端传输机制,保证“正确的人,通过正确的设备,正确地使用数据”。

分布式软总线:使用基于手机、平板、智能穿戴、智慧屏等分布式设备的统一通信基座,为设备之间的互联互通提供统一的分布式通信能力。

任务流转流程:设备A和设备B登录相同的华为账号,在同一网络下。设备A向设备B发起协同,应用程序需先向系统的流转任务管理服务 注册回调,获取到设备B的 DeviceId 等设备信息,设备A初始化分布式任务调度,通过设备DeviceId指定设备发起协同,设备B接收到协同请求,初始化分布式任务调度,启动对应的应用程序,把流转的状态上报给流转任务管理服务,流转任务管理服务返回流转结果,完成一次设备A到设备B的任务流转。
图片1.png

理解分布式软总线

分布式:指的是一种运行方式,简单来说就是任务可以在一个设备上运行,也可以多个设备连接起来一起运行,在多个设备中没有一个绝对的中心。

总线:简单了解一下“总线”的概念,在计算机系统中,各个部件之间传送信息的通道叫总线,外部设备通过相应的接口与总线相连接,组成了整个计算机系统。

分布式软总线:所以不难理解分布式软总线其实就是实现多个设备之间的连接,传递消息,实现任务多端运行的一种技术。在HarmonyOS中,底层已经帮我们实现了设备之间的组网、发现和连接,所以并不需要关心设备怎么通信,只需要调用底层封装好的接口,实现多设备协同就可以了。

实现步骤

实现分布式多设备协同,需要实现跨端启动应用、后台PA服务、分布式数据同步的功能,具体实现流程如下
图片2.png

一、跨设备启动应用

多设备协同实现的前提,需要在多端安装相同的应用,而在现实使用环境中,在多个设备中安装一个相同的应用还是一个比较麻烦的事。而HarmonyOS的原子化服务则不需要用户手动安装,由系统程序框架后台安装后即可使用,在HarmonyOS的服务中心以服务卡片的形式展示。

应用由原子化服务平台(Huawei Ability Gallery)管理和分发,只需要上传到原子化服务平台(Huawei Ability Gallery)即可,在多设备协同中,当设备A的应用向设备B的应用发起多端协同,如果设备B上没有安装对应服务,HarmonyOS会自动下载相关原子化服务,和A端的应用一起进行多端协同。

跨设备启动应用,也就是设备A上的应用可以拉起设备B上的应用。因为原子化服务应用免安装的特性,所以不用关心应用在多设备上的安装,只需实现跨设备启动应用即可。

1. 创建原子化服务

以原子化服务的形式创建项目, 原子化服务的特点是支持免安装,没有应用图标,只在 HarmanoyOS 服务中心以卡片的形式展现,支持跨端迁移和多端协同。
在新建项目时选择Atomic Service,创建一个原子化服务,同时打开Show in service center 开关,自动创建服务卡片,去掉TV的勾选状态,目前服务卡片不支持TV设备。
图片3.png

2. 创建HarmonyOS IDL接口

选择项目的module目录,点击鼠标右键,选择New>Idl File,如下图:
图片4.png

IDL是HarmonyOS的接口描述语言,可以实现IPC跨进程间通信,接口的提供方是服务端,客户端绑定应用的服务来进行交互。

在IDL中定义服务端接口,代码如下。

**interface** com.wealchen.multipoint.IMultiPointIdl {

  *//启动服务*
  void serviceStart([in] int code);

  *//发送消息*
  void sendMsg([in] int code,[in]int extras);

  *//停止服务*
  int serviceStop();
}

创建的IDL接口通过编译会在build > generated > source > Idl> 目录 debugrelease 下自动生成对应的接口类、桩类和代理类,如下图。

图片5.png

3. 实现后台接口服务

在HarmonyOS中,客户端绑定服务端后,获取到序列化的IRemoteObject对象,通过IRemoteObject对象实现客户端与服务端的通信,IRemoteObject在编译生成的桩类和代理类中已经完成了对象的创建和消息发送的实现,具体可查看上图中自动生成的代码。

创建一个Service后台服务MultiPointService,提供给客户端连接,并实现IDL中定义的接口,在接口实现中启动FA页面,接收客户端发送的消息,具体代码如下:

**public** **class** MultiPointService **extends** Ability {
  *// DESCRIPTOR 保持与 MultiPointIdlStub 和 MultiPointIdlProxy中的一致*
  **private** static final String DESCRIPTOR = "com.wealchen.multipoint.IMultiPointIdl";
  **private** static final String TAG = MultiPointService.class.getName();

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

  @Override
  **protected** IRemoteObject onConnect(Intent intent) {
    **return** **new** MultiPointRemoteObject(DESCRIPTOR);
  }

  **private** **class** MultiPointRemoteObject **extends** MultiPointIdlStub {
    **public** MultiPointRemoteObject(String descriptor) {
      **super**(descriptor);
    }

    @Override
    **public** void serviceStart(int _code) **throws** RemoteException {
      *//启动WebViewAbility页面*
      Intent intent = **new** Intent();
      Operation operation = **new** Intent.OperationBuilder().withBundleName(getBundleName())
          .withAbilityName(WebViewAbility.class.getName()).build();
      intent.setOperation(operation);
      intent.setParam(Constants.INTENT_STAR_PARAM, _code);
      startAbility(intent);
      LogUtil.debug(TAG, "serviceStart : start WebViewAbility " + _code);
    }

    @Override
    **public** void sendMsg(int _code, int _extras) **throws** RemoteException {
      LogUtil.debug(TAG, "sendMsg  code: " + _code + " extras: " + _extras);
    }

    @Override
    **public** int serviceStop() **throws** RemoteException {
      **return** 0;
    }
  }
}

4. 实现跨端启动应用

启动跨端应用需指定设备的DeviceId,通过设备管理器DeviceManager,可获取到当前同一网络下所有不是待机状态的设备,拿到DeviceId,DeviceName等设备信息,具体代码如下:

 *//获取同一网络下的设备*
    List<DeviceInfo> deviceInfos = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
    **if** (deviceInfos == **null** && deviceInfos.size() == 0) {
      **return**;
    }
    **for** (int i = 0; i < deviceInfos.size(); i++) {
      String devId = deviceInfos.get(i).getDeviceId();
      String devName = deviceInfos.get(i).getDeviceName();
    }

通过DeviceId可以与指定设备的后台服务MultiPointService连接,实现长期交互,系统提供了connectAbility方法,实现跨设备PA连接与断开连接的能力,通过AbilitySlice的connectAbility接口跨设备连接到后台服务MultiPointService,发送消息到指定设备,设备在接收到消息之后可以执行相应的任务,从而实现跨设备应用任务的调度,连接服务实现代码如下:

 *//启动远端设备的FA*
  **private** void startAbilityFa(String devicesId, String event, int localExtras) {
    Intent intent = **new** Intent();
    Operation operation =
        **new** Intent.OperationBuilder()
            .withDeviceId(devicesId)
            .withBundleName(getBundleName())
            .withAbilityName(MultiPointService.class.getName())
            .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
            .build();
    intent.setOperation(operation);
    boolean connectFlag = connectAbility(intent, **new** IAbilityConnection() {
      @Override
      **public** void onAbilityConnectDone(ElementName elementName, IRemoteObject remoteObject, int extra) {
        LogUtil.debug(TAG, "onAbilityConnectDone extra:" + extra);
        multiPointIdl = MultiPointIdlStub.asInterface(remoteObject);
        **try** {
          **if** (multiPointIdl != **null**) {
            **switch** (event) {
              **case** "serviceStart":
                multiPointIdl.serviceStart(0);
                **break**;
              **case** "sendMsg":
                multiPointIdl.sendMsg(1001, localExtras);
                **break**;
            }
          }
        } **catch** (RemoteException e) {
          LogUtil.error(TAG, "connect successful,but have remote exception");
        }
      }

      @Override
      **public** void onAbilityDisconnectDone(ElementName elementName, int extra) {
        LogUtil.debug(TAG, "extra " + extra + " elementName " + elementName.getAbilityName());
        disconnectAbility(**this**);
      }
    });
    **if** (connectFlag) {
      Toast.toast(**this**, "transmit successful!", TOAST_DURATION);
    } **else** {
      Toast.toast(**this**, "transmit failed!Please try again later.", TOAST_DURATION);
    }
  }

二、多端设备协同

多设备协同可以实现对跨端设备的控制,使用HarmonyOS的分布式数据服务,不同设备之间的数据可以实时更新并显示在界面上。

1. 分布式数据服务介绍

分布式数据服务是HarmonyOS为应用程序提供不同设备间同步数据的能力,通过使用分布式数据接口,应用程序将数据保存到分布式数据库中,不同设备之间可以通过分布式数据服务互相访问,支持数据在相同帐号的多端设备之间相互同步。

HarmonyOS的分布式数据库是一种NoSQL类型数据库,其数据以键值对key-value的形式进行组织、索引和存储,分布式数据服务包含五部分:

服务接口:提供专门的数据库创建、数据访问、数据订阅等接口给应用程序调用,接口支持KV数据模型,支持常用的数据类型,同时确保接口的兼容性、易用性和可发布性。

服务组件:负责服务内元数据管理、权限管理、加密管理、备份和恢复管理以及多用户管理等、同时负责初始化底层分布式DB的存储组件、同步组件和通信适配层。

存储组件:负责数据的访问、数据的缩减、事务、快照、数据库加密,以及数据合并和冲突解决等特性。

同步组件:连结了存储组件与通信组件,其目标是保持在线设备间的数据库数据一致性,包括将本地产生的未同步数据同步给其他设备,接收来自其他设备发送过来的数据,并合并到本地设备中。

通信适配层:负责调用底层公共通信层的接口完成通信管道的创建、连接,接收设备上下线消息,维护已连接和断开设备列表的元数据,同时将设备上下线信息发送给上层同步组件,同步组件维护连接的设备列表,同步数据时根据该列表,调用通信适配层的接口将数据封装并发送给连接的设备。
图片6.png

2. 创建分布式数据库

创建分布式数据库管理器,设置是否开启加密,自动同步功能,定义数据存储和查询方法,使用手动同步数据的方法实现数据的同步,详细代码代码如下:

**public** **class** MyKvStoreManager {
  **private** static final String TAG = MyKvStoreManager.class.getName();
  **private** static MyKvStoreManager instance = **null**;
  **private** static SingleKvStore singleKvStore = **null**;
  **private** static KvManager kvManager;

  **private** MyKvStoreManager(Context context) {
    createKVManager(context);
  }
  */**
  ** 创建分布式数据管理器*
  ** \*/*
  **public** static void createKVManager(Context context) {
    KvManagerConfig config = **new** KvManagerConfig(context);
    kvManager = KvManagerFactory.getInstance().createKvManager(config);
    **try** {
      Options options = **new** Options();
      options.setCreateIfMissing(**true**)
          .setAutoSync(**false**)
          .setEncrypt(**false**)
          .setKvStoreType(KvStoreType.SINGLE_VERSION);
      String storeId = "remoteData";
      singleKvStore = kvManager.getKvStore(options, storeId);
    } **catch** (KvStoreException e) {
      LogUtil.error(TAG, "getKvStore:" + e.getKvStoreErrorCode());
    }
  }

  */**
  ** 存储数据*
  ** \*/*
  **public** static void putKvStore(String key, String value) {
    **if** (singleKvStore == **null**) {
      **return**;
    }
    **try** {
      singleKvStore.putString(key, value);
    } **catch** (KvStoreException e) {
      LogUtil.debug(TAG, "putString:" + e.getKvStoreErrorCode());
    }
  }

  */**
  ** 查询数据*
  ** \*/*
  **public** static String getKvStore(String key) {
    String valueString = "";
    **try** {
      valueString = singleKvStore.getString(key);
    } **catch** (KvStoreException e) {
      LogUtil.debug(TAG, "getString:" + e.getKvStoreErrorCode());
    }
    **return** valueString;
  }

  */**
  ** 手动同步数据*
  ** \*/*
  **public** static void syncData() {
    List<DeviceInfo> deviceInfoList = kvManager.getConnectedDevicesInfo(DeviceFilterStrategy.NO_FILTER);
    List<String> deviceIdList = **new** ArrayList<>();
    **for** (DeviceInfo deviceInfo : deviceInfoList) {
      deviceIdList.add(deviceInfo.getId());
      LogUtil.debug(TAG,"syncData getId: "+deviceInfo.getId());
    }
    **if** (deviceIdList.isEmpty()){
      **return**;
    }
    singleKvStore.sync(deviceIdList, SyncMode.PUSH_PULL);
  }
   */**
  ** 订阅数据同步结果*
  ** \*/*
  **public** static void dataChangeObserver(KvStoreObserver storeObserver) {
    singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_REMOTE, storeObserver);
  }
}

3. 订阅分布式数据变化

订阅分布式数据变化客户端应用需要实现KvStoreObserver接口,KvStoreObserver接口回调中返回数据更新的结果,根据返回结果应用执行相应的任务,或者更新界面,从而实现多端设备的同步,注意:回调方法中不允许更新UI组件等阻塞动作。

更新UI可以通过getMainTaskDispatcher().asyncDispatch()切换到UI主线程更新UI的,详细代码如下:

MyKvStoreManager.dataChangeObserver(**new** KvStoreObserver() {
  @Override
  **public** void onChange(ChangeNotification changeNotification) {
    LogUtil.debug(TAG, "dataChangeObserver");
    List<Entry> updateEntries = changeNotification.getUpdateEntries();
    **for** (int i = 0; i < updateEntries.size(); i++) {
      String key = updateEntries.get(i).getKey();
      String value = updateEntries.get(i).getValue().getString();
      LogUtil.debug(TAG, "dataChangeObserver key:" + key + " value:" + value);
      getMainTaskDispatcher().asyncDispatch(**new** Runnable() {
        @Override
        **public** void run() {
          **switch** (key) {
            **case** JS_ADD_NAME:
              *//添加名字*
              setJsAddName(value);
              **break**;
            **case** JS_DEL_NAME:
              *//删除名字*
              setJsDelName(value);
              **break**;
            **default**:
              updateListView(key, value);
              **break**;
          }
        }
      });

    }
  }
});

三、WebView与JavaScript的交互

1. 在WebView调用H5页面的JavaScript方法

应用的页面是WebView 加载的H5页面,关于如何加载H5页面,可以参考:HarmonyOS - JavaUI 框架之使用WebView加载本地H5页面

更新H5页面时,WebView需要调用JavaScript,通过WebView.executeJs()传入H5页面中对应的方法名称,实现应用调用页面内的JavaScript方法,实现的代码如下:

 **private** void setJsAddName(String addName) {
    LogUtil.debug(TAG, "addName:" + addName);
    String add_js = String.format("javascript:addName('%s')", addName);
    webView.executeJs(add_js, **new** AsyncCallback<String>() {
      @Override
      **public** void onReceive(String msg) {
        *// 在此确认返回结果*
        LogUtil.debug(TAG, "executeJs onReceive:" + msg);
      }
    });
  }

H5页面定义的方法:

**function** addName(name){
    **var** tpl = document.getElementById('name').innerText
    **var** str = name +'\n'
    document.getElementById('name').innerText = tpl.concat(str)
    **return** name
}

2. H5页面调用应用中的方法

WebView组件通过注入回调对象到页面内容,实现在H5页面中调用应用中的方法。

H5页面调用应用中的方法:

**function** callToApp() {
  **if** (window.showDeviceList && window.showDeviceList.call) {
    **var** result = showDeviceList.call("showDeviceList");
  }
}

应用中定义的方法:

webView.addJsCallback("showDeviceList", **new** JsCallback() {
  @Override
  **public** String onCallback(String s) {
    getMainTaskDispatcher().asyncDispatch(**new** Runnable() {
      @Override
      **public** void run() {
        **switch** (s) {
          **case** "showDeviceList":
            **if** (!bottomDialog.isShowing()) {
              bottomDialog.show();
            }
            **break**;
        }
      }
    });
    **return** method;
  }
});

总结

应用实现了包括原子化服务、任务流转、跨端启动应用、分布式数据服务、多端设备协同、WebView组件与JavaScript交互的功能,其中包含了HarmonyOS系统才具有的功能。以上只是简单介绍了HarmonyOS特有功能的实现,当然HarmonyOS的特性不止这些,更多的功能和实现还需要开发者去探索。

更多原创内容请关注:中软国际 HarmonyOS 技术团队

入门到精通、技巧到案例,系统化分享HarmonyOS开发技术,欢迎投稿和订阅,让我们一起携手前行共建鸿蒙生态。

附件链接:https://ost.51cto.com/resource/2073

想了解更多关于开源的内容,请访问:

51CTO 开源基础软件社区

https://ost.51cto.com/#bkwz