天天看點

HarmonyOS - 實作多裝置協同

作者:陳忠蔚

前言

現在随着個人裝置越來越多,越來越需要多個裝置之間互相感覺和連接配接,裝置和裝置之間可以互相關聯,形成互聯互通的場景,而搭載HarmonyOS的裝置恰好可以滿足這一點 。下面通過開發一個HarmonyOS的多端分布式表白應用來實作裝置之間的互相關聯。

項目介紹

H5頁面可以實作一些比較特殊的頁面效果,是以選擇在應用中內建H5頁面。應用可以将頁面直接投放到附近其他HarmonyOS裝置上,實作多端裝置分布式顯示,同時應用可以跨端控制,更新應用頁面,形成多裝置協同的效果。

下面是效果展示:

HarmonyOS - 實作多裝置協同

多裝置協同原理

HarmonyOS 給應用開發者提供了一套在多個裝置不同應用之間進行任務流轉的API接口,實作裝置協同需要關注 流轉任務管理服務 和 分布式任務排程。

• 流轉任務管理服務:在流轉發起端,接受使用者應用程式注冊,提供流轉入口、狀态顯示、退出流轉等管理能力。

• 分布式任務排程:提供遠端服務啟動、遠端服務連接配接、遠端遷移等能力,并通過不同能力組合,支撐使用者應用程式完成跨端遷移或多端協同的業務體驗。

• 分布式安全:提供E2E的加密通道,為使用者應用程式提供安全的跨端傳輸機制,保證“正确的人,通過正确的裝置,正确地使用資料”。

• 分布式軟總線:使用基于手機、平闆、智能穿戴、智慧屏等分布式裝置的統一通信基座,為裝置之間的互聯互通提供統一的分布式通信能力。

• 任務流轉流程:裝置A和裝置B登入相同的華為賬号,在同一網絡下。裝置A向裝置B發起協同,應用程式需先向系統的流轉任務管理服務 注冊回調,擷取到裝置B的 DeviceId 等裝置資訊,裝置A初始化分布式任務排程,通過裝置DeviceId指定裝置發起協同,裝置B接收到協同請求,初始化分布式任務排程,啟動對應的應用程式,把流轉的狀态上報給流轉任務管理服務,流轉任務管理服務傳回流轉結果,完成一次裝置A到裝置B的任務流轉。

HarmonyOS - 實作多裝置協同

了解分布式軟總線

• 分布式:指的是一種運作方式,簡單來說就是任務可以在一個裝置上運作,也可以多個裝置連接配接起來一起運作,在多個裝置中沒有一個絕對的中心。

• 總線:簡單了解一下“總線”的概念,在計算機系統中,各個部件之間傳送資訊的通道叫總線,外部裝置通過相應的接口與總線相連接配接,組成了整個計算機系統。

• 分布式軟總線:是以不難了解分布式軟總線其實就是實作多個裝置之間的連接配接,傳遞消息,實作任務多端運作的一種技術。在HarmonyOS中,底層已經幫我們實作了裝置之間的組網、發現和連接配接,是以并不需要關心裝置怎麼通信,隻需要調用底層封裝好的接口,實作多裝置協同就可以了。

實作步驟

實作分布式多裝置協同,需要實作跨端啟動應用、背景PA服務、分布式資料同步的功能,具體實作流程如下

HarmonyOS - 實作多裝置協同

一、跨裝置啟動應用

多裝置協同實作的前提,需要在多端安裝相同的應用,而在現實使用環境中,在多個裝置中安裝一個相同的應用還是一個比較麻煩的事。而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裝置。

HarmonyOS - 實作多裝置協同

2. 建立HarmonyOS IDL接口

選擇項目的module目錄,點選滑鼠右鍵,選擇New>Idl File,如下圖:

HarmonyOS - 實作多裝置協同

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> 目錄 debug 和release 下自動生成對應的接口類、樁類和代理類,如下圖。

HarmonyOS - 實作多裝置協同

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的存儲元件、同步元件和通信适配層。

• 存儲元件:負責資料的通路、資料的縮減、事務、快照、資料庫加密,以及資料合并和沖突解決等特性。

• 同步元件:連結了存儲元件與通信元件,其目标是保持線上裝置間的資料庫資料一緻性,包括将本地産生的未同步資料同步給其他裝置,接收來自其他裝置發送過來的資料,并合并到本地裝置中。

• 通信适配層:負責調用底層公共通信層的接口完成通信管道的建立、連接配接,接收裝置上下線消息,維護已連接配接和斷開裝置清單的中繼資料,同時将裝置上下線資訊發送給上層同步元件,同步元件維護連接配接的裝置清單,同步資料時根據該清單,調用通信适配層的接口将資料封裝并發送給連接配接的裝置。

HarmonyOS - 實作多裝置協同

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

繼續閱讀