HarmonyOSにおけるマルチデバイス連携の実装方法

概要

HarmonyOSでは、異なるデバイス間でアプリケーション連携を実現する様々な手法が提供されています。本稿では、UIAbilityの起動・ServiceExtensionAbilityへの接続・Callインターフェースを活用した3つの主要な連携方式について詳しく解説します。

1. デバイス間でのUIAbility起動とデータ返却

あるデバイスから別のデバイスのUIAbilityを起動し、処理結果を返す実装方法について説明します。

起動側の実装

import { BusinessError } from '@ohos.base';
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';

@Entry
@Component
struct PageName {
  private context = getContext(this) as common.UIAbilityContext;

  build() {
    Button('startAbilityForResult')
      .onClick(() => {
        let want: Want = {
          deviceId: this.getRemoteDeviceId(),
          bundleName: 'com.example.myapplication',
          abilityName: 'FuncAbility',
          moduleName: 'module1'
        };
        
        this.context.startAbilityForResult(want).then((data) => {
          if (data?.resultCode === 1001) {
            let info = data.want?.parameters?.info;
          }
        }).catch((error: BusinessError) => {
          console.info("startAbilityForResult err: " + JSON.stringify(error));
        });
      });

    Button('terminateSelfWithResult')
      .onClick(() => {
        const RESULT_CODE: number = 1001;
        this.context.terminateSelfWithResult(
          {
            resultCode: RESULT_CODE,
            want: {
              bundleName: 'com.example.myapplication',
              abilityName: 'FuncAbility',
              moduleName: 'module1'
            }
          },
          (err: BusinessError) => {
            console.info("terminateSelfWithResult err: " + JSON.stringify(err));
          }
        );
      });
  }

  private getRemoteDeviceId(): string {
    // 実際の実装では、デバイスDiscovery APIを使用
    return '';
  }
}

対象端での実装

import { BusinessError } from '@ohos.base';
import common from '@ohos.app.ability.common';

@Entry
@Component
struct TargetPage {
  private context = getContext(this) as common.UIAbilityContext;

  build() {
    Button('terminateSelfWithResult')
      .onClick(() => {
        const RESULT_CODE: number = 1001;
        this.context.terminateSelfWithResult(
          {
            resultCode: RESULT_CODE,
            want: {
              bundleName: 'com.example.myapplication',
              abilityName: 'FuncAbility',
              moduleName: 'module1'
            }
          },
          (err: BusinessError) => {
            console.info("terminateSelfWithResult err: " + JSON.stringify(err));
          }
        );
      });
  }
}

2. ServiceExtensionAbilityへの接続による連携

システムアプリケーションでは、connectServiceExtensionAbility()を使用してリモートデバイスのサービスに接続できます。例えば、スマートTVをディスプレイとして使い、タブレットをリモコンとして利用する分散型ゲームシナリオ 등에 활용됩니다。

API解説

API名説明
connectServiceExtensionAbility(want: Want, options: ConnectOptions): numberServiceExtensionAbilityへの接続を確立
disconnectServiceExtensionAbility(connection: number, callback: AsyncCallback<void>): void接続解除(コールバック形式)
disconnectServiceExtensionAbility(connection: number): Promise<void>接続解除(Promise形式)

実装手順

  1. ohos.permission.DISTRIBUTED_DATASYNC権限を申請
  2. 初回起動時にユーザーへの許可を求め
  3. バックグラウンドサービスを実装
  4. サービスへの接続を実装

接続の実装

import rpc from '@ohos.rpc';
import Want from '@ohos.app.ability.Want';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';

@Entry
@Component
struct PageName {
  private context = getContext(this) as common.UIAbilityContext;
  private connectionId: number = -1;

  build() {
    Button('connectServiceExtensionAbility')
      .onClick(() => {
        const REQUEST_CODE = 99;
        let want: Want = {
          deviceId: this.getRemoteDeviceId(),
          bundleName: 'com.example.myapplication',
          abilityName: 'ServiceExtAbility'
        };

        this.connectionId = this.context.connectServiceExtensionAbility(want, {
          onConnect: (elementName, remote) => {
            console.info('onConnect callback');
            if (remote === null) {
              console.info('onConnect remote is null');
              return;
            }
            
            let option = new rpc.MessageOption();
            let requestData = new rpc.MessageSequence();
            let responseData = new rpc.MessageSequence();
            
            requestData.writeInt(1);
            requestData.writeInt(99);

            remote.sendMessageRequest(REQUEST_CODE, requestData, responseData, option)
              .then((ret: rpc.RequestResult) => {
                let msg = responseData.readInt();
                console.info(`sendRequest ret:${ret} msg:${msg}`);
              })
              .catch((error: BusinessError) => {
                console.info('sendRequest failed');
              });
          },
          onDisconnect: (elementName) => {
            console.info('onDisconnect callback');
          },
          onFailed: (code) => {
            console.info('onFailed callback');
          }
        });
      });

    Button('disconnectServiceExtensionAbility')
      .onClick(() => {
        if (this.connectionId !== -1) {
          this.context.disconnectServiceExtensionAbility(this.connectionId)
            .then(() => {
              console.info('disconnectServiceExtensionAbility success');
              this.connectionId = -1;
            })
            .catch((error: BusinessError) => {
              console.error('disconnectServiceExtensionAbility failed');
            });
        }
      });
  }

  private getRemoteDeviceId(): string {
    return '';
  }
}

3. デバイス間Call呼び出しによる連携

Callインターフェースを使用した連携方式について説明します。デバイス内のCall呼び出しと同様の原理で動作します。

API解説

API名説明
startAbilityByCall(want: Want): Promise<Caller>UIAbilityを起動し、Caller通信インターフェースを取得
on(method: string, callback: CalleeCallBack): voidCalleeにメソッドコールバックを登録
off(method: string): voidCalleeのメソッドコールバックを解除
call(method: string, data: rpc.Parcelable): Promise<void>Calleeにデータを送信
callWithResult(method: string, data: rpc.Parcelable): Promise<rpc.MessageSequence>Calleeにデータを送信し、結果を受け取る
release(): voidCallerインターフェースを解放

実装手順

  1. 権限申請(ohos.permission.DISTRIBUTED_DATASYNC)
  2. ユーザーへの許可要求
  3. 被呼び出し側UIAbilityの実装
  4. UIAbility起動モードの設定(singleton)

被呼び出し側の実装

import rpc from '@ohos.rpc';
import Want from '@ohos.app.ability.Want';
import UIAbility from '@ohos.app.ability.UIAbility';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';

const TAG: string = '[CalleeAbility]';
const MSG_SEND_METHOD: string = 'CallSendMsg';

class MyParcelable {
  num: number = 0;
  str: string = "";

  constructor(num: number, str: string) {
    this.num = num;
    this.str = str;
  }

  marshalling(messageSequence: rpc.MessageSequence): boolean {
    messageSequence.writeInt(this.num);
    messageSequence.writeString(this.str);
    return true;
  }

  unmarshalling(messageSequence: rpc.MessageSequence): boolean {
    this.num = messageSequence.readInt();
    this.str = messageSequence.readString();
    return true;
  }
}

function sendMsgCallback(data: rpc.MessageSequence): MyParcelable {
  console.info('CalleeSortFunc called');
  
  let receivedData: MyParcelable = new MyParcelable(0, '');
  data.readParcelable(receivedData);
  console.info(`receiveData[${receivedData.num}, ${receivedData.str}]`);
  
  return new MyParcelable(Number(receivedData.num) + 1, `send ${receivedData.str} succeed`);
}

export default class CalleeAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    try {
      this.callee.on(MSG_SEND_METHOD, sendMsgCallback);
    } catch (error) {
      console.info(`${MSG_SEND_METHOD} register failed with error ${JSON.stringify(error)}`);
    }
  }

  onDestroy() {
    try {
      this.callee.off(MSG_SEND_METHOD);
    } catch (error) {
      console.error(TAG, `${MSG_SEND_METHOD} unregister failed with error ${JSON.stringify(error)}`);
    }
  }
}

呼び出し側の実装

import UIAbility, { Caller } from '@ohos.app.ability.UIAbility';
import { BusinessError } from '@ohos.base';

const MSG_SEND_METHOD: string = 'CallSendMsg';

export default class EntryAbility extends UIAbility {
  private remoteCaller: Caller | undefined;

  async onButtonGetRemoteCaller() {
    let context = this.context;
    
    context.startAbilityByCall({
      deviceId: this.getRemoteDeviceId(),
      bundleName: 'com.samples.CallApplication',
      abilityName: 'CalleeAbility'
    }).then((data) => {
      if (data != null) {
        this.remoteCaller = data;
        console.info('get remote caller success');
        
        this.remoteCaller.onRelease((msg) => {
          console.info(`remote caller onRelease is called ${msg}`);
        });
        
        try {
          this.remoteCaller.onRemoteStateChange((str) => {
            console.info('Remote state changed ' + str);
          });
        } catch (error) {
          console.info(`Caller.onRemoteStateChange catch error`);
        }
      }
    }).catch((error: BusinessError) => {
      console.error(`get remote caller failed with ${error}`);
    });
  }

  async onButtonCall() {
    try {
      let msg: MyParcelable = new MyParcelable(1, 'origin_Msg');
      if (this.remoteCaller) {
        await this.remoteCaller.call(MSG_SEND_METHOD, msg);
      }
    } catch (error) {
      console.info(`caller call failed with ${error}`);
    }
  }

  async onButtonCallWithResult(originMsg: string) {
    try {
      let msg: MyParcelable = new MyParcelable(1, originMsg);
      if (this.remoteCaller) {
        const data = await this.remoteCaller.callWithResult(MSG_SEND_METHOD, msg);
        console.info('caller callWithResult succeed');
        
        let result: MyParcelable = new MyParcelable(0, '');
        data.readParcelable(result);
        console.info(`caller result is [${result.num}, ${result.str}]`);
      }
    } catch (error) {
      console.info(`caller callWithResult failed with ${error}`);
    }
  }

  releaseCall() {
    try {
      if (this.remoteCaller) {
        this.remoteCaller.release();
        this.remoteCaller = undefined;
      }
      console.info('caller release succeed');
    } catch (error) {
      console.info(`caller release failed with ${error}`);
    }
  }

  private getRemoteDeviceId(): string {
    return '';
  }
}

モジュール設定

// module.json5
{
  "abilities": [{
    "name": ".CalleeAbility",
    "srcEntry": "./ets/CalleeAbility/CalleeAbility.ts",
    "launchType": "singleton",
    "description": "$string:CalleeAbility_desc",
    "icon": "$media:icon",
    "label": "$string:CalleeAbility_label",
    "exported": true
  }]
}

まとめ

HarmonyOSでは、UIAbilityの起動・ServiceExtensionAbilityへの接続・Call呼び出しという3つの主要な方式により、異なるデバイス間でのシームレスな連携を実現できます。アプリケーションの要件に応じて、適切な方式を選択してください。

タグ: HarmonyOS 鸿蒙 多端协同 分布式开发 UIAbility

7月30日 07:45 投稿