目录

OneHop for Device Interaction

Overview

Since API version 26.0.0, the OneHop capability becomes available, enabling users to tap the top ends of devices together to quickly establish a connection and transfer data.

In everyday scenarios, users often need to quickly transfer data or collaborate between two devices. For example, they may want to exchange business cards at a party, invite friends to join a game, or share product information while shopping. Traditional methods, such as Bluetooth pairing or scanning QR codes to add friends, are often cumbersome and time-consuming.

OneHop provides a simple and intuitive solution: Users only need to tap two devices together to quickly establish a connection and transfer data. This eliminates complex pairing procedures, streamlines the process, and significantly enhances the overall user experience.

How to Implement

OneHop leverages the near field communication (NFC) capability of devices to initiate connection establishment through a top-end tap interaction. When two devices are tapped together, they automatically identify each other and establish a connection, which is then notified to your app through a callback. Your app can use the Connection object to send messages, and receive messages via a callback.

Constraints

[h2]Device Constraints

Wi-Fi and Bluetooth must be enabled on the involved devices.

[h2]Required Permissions

The ohos.permission.KNOCK_COLLABORATION permission is required. This capability is available only with this permission. For details about how to apply for the required permission, please refer to Requesting Restricted Permissions.

API Description

The table below lists the key APIs of OneHop. For details about the APIs, please refer to the API reference.

API Description
onServiceInteraction(config: CollaborationConfig, callbacks: ServiceInteractionCallback): void Registers the OneHop callback method.
offServiceInteraction(config: CollaborationConfig, callbacks?: ServiceInteractionCallback): void Unregisters the OneHop callback method.
Connection.sendMessage(data: ArrayBuffer): Promise<void> Sends information to the connected peer device. This API uses a promise to return the result.
Connection.disconnect(): Promise<void> Disconnects from the peer device. This API uses a promise to return the result.

Development Procedure

  1. Import the serviceInteraction module and other required utility modules.

    import { serviceInteraction } from '@kit.ServiceCollaborationKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { util } from '@kit.ArkTS';
  2. Create a CollaborationConfig object and set parameters such as the OneHop trigger mode and app window ID.

    const config: serviceInteraction.CollaborationConfig = {
      triggerType: serviceInteraction.TriggerType.KNOCK,
      windowId: 1, // In this example, the value 1 is passed. In actual development, you can obtain the window ID using this.getUIContext().getWindowId();.
      appLinking: 'https://example.com/app',
      timeOut: 10 // Optional. The default value is 20 seconds.
    };
  3. Register a callback method through the onServiceInteraction API to handle events such as device connection, disconnection, and message reception.

    const callback: serviceInteraction.ServiceInteractionCallback = {
      onDeviceConnect: (connection: serviceInteraction.Connection) => {
        // The devices are connected.
        hilog.info(0, 'MEMOMOCK', 'Devices connected, connectId: ' + connection.connectId);
      },
      onDeviceDisconnect: (connection: serviceInteraction.Connection, retCode: number) => {
        // The devices are disconnected.
        hilog.info(0, 'MEMOMOCK', 'Devices disconnected, connectId: ' + connection.connectId + ', retCode: ' + retCode);
      },
      onMessageReceive: (connection: serviceInteraction.Connection, data: ArrayBuffer) => {
        // The message from the peer device is received.
        hilog.info(0, 'MEMOMOCK', 'Message received, connectId: ' + connection.connectId);
      },
      onConnectFail: (retCode: number) => {
        // Connection failed.
        hilog.error(0, 'MEMOMOCK', 'Connection failed, retCode: ' + retCode);
      }
    };
    
    try {
      serviceInteraction.onServiceInteraction(config, callback);
      hilog.info(0, 'MEMOMOCK', 'Registration succeeded.');
    } catch (error) {
      hilog.error(0, 'MEMOMOCK', 'Registration failed. Message: ' + error.message);
    }
  4. Use the sendMessage method of the Connection object to send a message to the peer device.

    // Send a message.
    const message: string = 'Hello from device A';
    const textEncoder: util.TextEncoder = new util.TextEncoder();
    const uint8Array = new Uint8Array(textEncoder.encodeInto(message).length);
    textEncoder.encodeIntoUint8Array(message, uint8Array);
    
    try {
      connection.sendMessage(uint8Array.buffer).then(() => {
        hilog.info(0, 'MEMOMOCK', 'Message sent successfully.');
      }).catch((error: Error) => {
        hilog.error(0, 'MEMOMOCK', 'Failed to send the message: ' + error.message);
      });
    } catch (error) {
      hilog.error(0, 'MEMOMOCK', 'Sending failed. Message: ' + error.message);
    }
  5. Use the disconnect method of the Connection object to disconnect from the peer device.

    try {
      connection.disconnect().then(() => {
        hilog.info(0, 'MEMOMOCK', 'Disconnection succeeded.');
      }).catch((error: Error) => {
        hilog.error(0, 'MEMOMOCK', 'Disconnection failed: ' + error.message);
      });
    } catch (error) {
      hilog.error(0, 'MEMOMOCK', 'Disconnection failed. Message: ' + error.message);
    }
  6. Unregister the callback method through the offServiceInteraction API to stop receiving callback events.

    // Unregister the callback.
    try {
      serviceInteraction.offServiceInteraction(config);
      hilog.info(0, 'MEMOMOCK', 'Unregistration succeeded.');
    } catch (error) {
      hilog.error(0, 'MEMOMOCK', 'Unregistration failed. Message: ' + error.message);
    }

[h2]Debugging and Verification

After the development is complete, perform the following steps to verify the function:

  1. Prepare two devices and ensure that Wi-Fi and Bluetooth are enabled on both devices.
  2. Run your app on both devices.
  3. Gently tap the top ends of the two devices together and check whether the connection callback is triggered.
  4. Check whether the connection status is correctly displayed.
  5. Send a message on one device and check whether the other device receives the message.
  6. Disconnect the devices and check whether the disconnection callback is properly triggered.
  7. Check the logs to ensure that all operations are performed properly.

[h2]Complete Sample Code

The following sample code demonstrates how to use the OneHop function to implement interaction between two devices, including message sending and receiving.

import { serviceInteraction } from '@kit.ServiceCollaborationKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { util } from '@kit.ArkTS';

/**
 * Connection information encapsulation.
 */
interface ConnectionInfo {
  connection: serviceInteraction.Connection;
  deviceName: string;
}

/**
 * Core process:
 * 1. Listener registration: Call onServiceInteraction only once to register the callback method.
 * 2. Callback handling: Each time the devices tap each other, a callback is triggered, and a new Connection object is returned.
 * 3. Multi-connection handling: Use connectionList to manage multiple connections, and set a callback for each connection.
 */
@Entry
@Component
struct LuckyCardExchangePage {
  @State connectionStatus: string = 'Not connected'
  @State connectionList: ConnectionInfo[] = [];
  @State windowId: number = 0;
  private appLinking: string = 'https://example.com/luckycard/exchange';
  @State sendMessage: string = '';
  @State receivedMessage: string = '';

  aboutToAppear() {
    this.windowId = this.getUIContext().getWindowId() as number;
    this.registerServiceInteraction();
  }

  aboutToDisappear() {
    this.unregisterServiceInteraction();
    this.disconnectAll();
  }

  /**
   * Register a OneHop event listener.
   */
  private registerServiceInteraction(): void {
    const config: serviceInteraction.CollaborationConfig = {
      triggerType: serviceInteraction.TriggerType.KNOCK,
      windowId: this.windowId,
      appLinking: this.appLinking
    };

    const callback: serviceInteraction.ServiceInteractionCallback = {
      onDeviceConnect: (connection: serviceInteraction.Connection) => {
        this.addConnection(connection);
      },
      onDeviceDisconnect: (connection: serviceInteraction.Connection, retCode: number) => {
        this.onDisconnected(connection, retCode);
      },
      onMessageReceive: (connection: serviceInteraction.Connection, data: ArrayBuffer) => {
        this.onMessageReceived(connection, data);
      },
      onConnectFail: (retCode: number) => {
        hilog.info(0, 'MEMOMOCK', 'Connection failed. Error code: ' + retCode);
      },
    };

    try {
      serviceInteraction.onServiceInteraction(config, callback);
    } catch (error) {
      hilog.error(0, 'MEMOMOCK', 'Registration failed. Message: ' + error.message);
    }
    hilog.info(0, 'MEMOMOCK', 'Service interaction listener registered.');
  }

  /**
   * Unregisters the listener.
   */
  private unregisterServiceInteraction(): void {
    const config: serviceInteraction.CollaborationConfig = {
      triggerType: serviceInteraction.TriggerType.KNOCK,
      windowId: this.windowId,
      appLinking: this.appLinking
    };

    try {
      serviceInteraction.offServiceInteraction(config);
    } catch (error) {
      hilog.error(0, 'MEMOMOCK', 'Unregistration failed. Message: ' + error.message);
    }
    hilog.info(0, 'MEMOMOCK', 'Service interaction listener canceled.');
  }

  /**
   * Add a new connection.
   */
  private addConnection(connection: serviceInteraction.Connection): void {
    // Check whether a connection with the same connection ID already exists.
    const exists = this.connectionList.some(
      item => item.connection.connectId === connection.connectId
    );

    if (exists) {
      hilog.info(0, 'MEMOMOCK', 'Connection already exists. connectId: ' + connection.connectId);
      return;
    }

    // Create a connection.
    const connectionInfo: ConnectionInfo = {
      connection: connection,
      deviceName: 'Device' + (this.connectionList.length + 1)
    };
    // Add the connection to the connection list.
    this.connectionList.push(connectionInfo);
    this.updateConnectionStatus();

    hilog.info(0, 'MEMOMOCK', 'New connection established. connectId: ' + connection.connectId +
      ', current connections: ' + this.connectionList.length);
  }

  /**
   * Handle the disconnection.
   */
  private onDisconnected(connection: serviceInteraction.Connection, retCode: number): void {
    const index = this.connectionList.findIndex(
      item => item.connection.connectId === connection.connectId
    );

    if (index > -1) {
      this.connectionList.splice(index, 1);
      hilog.info(0, 'MEMOMOCK', 'Disconnected. connectId: ' + connection.connectId + ', retCode: ' + retCode);
    }

    this.updateConnectionStatus();
  }

  /**
   * Handle message receiving.
   */
  private onMessageReceived(connection: serviceInteraction.Connection, data: ArrayBuffer): void {
    const connectionInfo = this.connectionList.find(
      item => item.connection === connection
    );

    const deviceName = connectionInfo ? connectionInfo.deviceName : 'Unknown device'
    hilog.info(0, 'MEMOMOCK', 'Message received. Device: ' + deviceName + ', connectId: ' + connection.connectId);

    // Decode the message content.
    const decoder = util.TextDecoder.create('utf-8');
    const str = decoder.decodeToString(new Uint8Array(data));
    hilog.info(0, 'MEMOMOCK', 'data: ' + data + ', str: ' + str);

    // Save the received message.
    this.receivedMessage = str;
  }

  /**
   * Send a message.
   */
  private sendToAllDevices(): void {
    if (this.connectionList.length === 0) {
      hilog.warn(0, 'MEMOMOCK', 'No connected devices.');
      return;
    }

    this.connectionList.forEach((connection) => {
      let textEncoder: util.TextEncoder = new util.TextEncoder();
      let uint8Array = new Uint8Array(textEncoder.encodeInto(this.sendMessage).length);
      textEncoder.encodeIntoUint8Array(this.sendMessage, uint8Array);
      try {
        connection.connection.sendMessage(uint8Array.buffer);
      } catch (error) {
        hilog.error(0, 'MEMOMOCK', 'Sending failed. Message: ' + error.message);
      }
    });
  }

  /**
   * Disconnect all connections.
   */
  private disconnectAll(): void {
    this.connectionList.forEach((connection) => {
      try {
        connection.connection.disconnect();
      } catch (error) {
        hilog.error(0, 'MEMOMOCK', 'Disconnection failed. Message: ' + error.message);
      }
    });
    this.connectionList = [];
    this.updateConnectionStatus();
  }

  /**
   * Update the connection status.
   */
  private updateConnectionStatus(): void {
    this.connectionStatus = this.connectionList.length > 0
      ? 'Connected to ' + this.connectionList.length + ' devices'
      : 'Not connected';
  }

  build() {
    Column() {
      Text ('OneHop for collaboration')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      Text('Connection status: ' + this.connectionStatus)
        .fontSize(16)
        .margin({ bottom: 20 })

      if (this.connectionList.length > 0) {
        Text('Connected devices:')
          .fontSize(14)
          .margin({ bottom: 10 })

        ForEach(this.connectionList, (item: ConnectionInfo) => {
          Text(item.deviceName + ' (ID: ' + item.connection.connectId + ')')
            .fontSize(14)
            .margin({ bottom: 5 })
        })
      }

      Text('Instructions: Tap two devices together to establish a connection.')
        .fontSize(12)
        .fontColor(Color.Gray)
        .margin({ top: 20 })

      if (this.connectionList.length > 0) {
        TextInput()
          .margin({ top: 20, bottom: 20 })
          .onChange((value) => {
            this.sendMessage = value;
          })

        Button('Send a message to the connected device')
          .onClick(() => {
            this.sendToAllDevices();
          })

        Button('Disconnect')
          .margin({ top: 10 })
          .onClick(() => {
            this.disconnectAll();
          })
      }

      if (this.receivedMessage != '' && this.connectionList.length > 0) {
        TextArea({
          text: this.receivedMessage
        })
          .margin({ top: 20, bottom: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}