目录

Pushing an In-App Call Message

Use Cases

In-app call messages empower your app with voice and video call capabilities. Push Kit processes an in-app call message differently when the device screen is locked or unlocked:

When a user answers a video call, the in-app call answer screen is displayed. After the call is connected, it can be ended proactively or passively.

  • For details about in-app call message problems, please refer to the FAQs.

  • It is recommended that the pushOptions.ttl of an in-app call message be set to 30 to 60 seconds.

Constraints

The in-app call message capability is available on phones and tablets. Support for kids' watches was added in API version 6.1.0(23) and later.

Applying for Required Permissions

You need to apply for the in-app call message permission to push in-app call messages to your users.

Frequency Control

During the debugging phase, the maximum number of test messages that can be pushed for each project every day is 1000. To send test messages, you need to set testMessage to true.

After formal release, the total number of messages that each app can push to a single device each day is subject to the device-based message frequency control policy. The message sending frequency is governed based on service scenarios and traffic. In cases where message sending is unnecessary or unreasonable, frequency limits will be applied.

Development Procedure

  1. Obtain a push token.

  2. Create a UIAbility component in your project, for example, VoIPUIAbility.ets (in the src/main/ets/entryability directory of the project), to process the main process of in-app call messages. Override the onCreate(), onWindowStageCreate(), and onDestroy() methods. The sample code is as follows:

    import { UIAbility } from '@kit.AbilityKit';
    import { pushService } from '@kit.PushKit';
    import { window } from '@kit.ArkUI';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { VoipCallService } from '../service/VoipCallService';
    import { BusinessError } from '@kit.BasicServicesKit';
    
    const DOMAIN = 0x0000;
    
    export default class VoIPUIAbility extends UIAbility {
      onCreate(): void {
        hilog.info(DOMAIN, 'testTag', 'VoIPUIAbility onCreate');
    
        try {
          pushService.receiveMessage('VoIP', this, (data) => {
            // Process the in-app call message data.
            try {
              VoipCallService.processVoIPMainMsg(data.data, this.context);
            } catch (error) {
              hilog.error(DOMAIN, 'testTag', 'Failed to process VoIP message: %{public}d %{public}s', error.code,
                error.message);
            }
          });
        } catch (e) {
          hilog.error(DOMAIN, 'testTag', `Failed to register VoIP, error: ${e.code}, ${e.message}.`);
        }
      }
    
      onWindowStageCreate(windowStage: window.WindowStage): void {
        hilog.info(DOMAIN, 'testTag', 'VoIPUIAbility onWindowStageCreate.');
    
        windowStage.loadContent('pages/CalleePage').catch((err: BusinessError) => {
          hilog.error(DOMAIN, 'testTag', `Failed to load content, error: ${err.code}, ${err.message}.`);
        });
      }
    
      onDestroy(): void {
        hilog.info(DOMAIN, 'testTag', 'VoIPUIAbility onDestroy');
      }
    }

    VoipCallService.ets (in the src/main/ets/service directory of the project) is used to process an in-app call message. The sample code is as follows:

    import { voipCall } from '@kit.CallServiceKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { common } from '@kit.AbilityKit';
    import { image } from '@kit.ImageKit';
    import { resourceManager } from '@kit.LocalizationKit';
    import { BusinessError } from '@kit.BasicServicesKit';
    
    export interface VoipScene {
      scene: string;
    }
    
    export interface Content {
      data: string;
      header: string;
      callId: string;
    }
    
    const DOMAIN = 0x0000;
    
    export class VoipCallService {
      private static callId: string | undefined;
    
      public static async processVoIPMainMsg(data: string,
        context: common.UIAbilityContext): Promise<void> {
        hilog.info(DOMAIN, 'testTag', `Process VoIP message: ${data}`);
    
        let content: Content;
        let scene: VoipScene;
        let callId: string;
    
        try {
          content = JSON.parse(data);
          scene = JSON.parse(content.data);
          callId = content.callId;
          if (!callId) {
            hilog.error(DOMAIN, 'testTag', 'CallId is null');
          }
          VoipCallService.callId = callId;
        } catch (e) {
          let err: BusinessError = e as BusinessError;
          hilog.error(DOMAIN, 'testTag', 'Failed to parse VoIP message data: %{public}d %{public}s', err.code, err.message);
          return;
        }
    
        try {
          // Subscribe to voipCallUiEvent.
          voipCall.on('voipCallUiEvent', async (event) => {
            hilog.info(DOMAIN, 'testTag', `Process voip call ui event: ${JSON.stringify(event)}.`);
    
            await VoipCallService.processVoipCallEvent(event.voipCallUiEvent);
          });
        } catch (err) {
          let e: BusinessError = err as BusinessError;
          hilog.error(DOMAIN, 'testTag', 'Failed to register event: %{public}d %{public}s', e.code, e.message);
        }
    
        const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
        let fileData: Uint8Array = new Uint8Array(0);
        try {
          // example.png indicates the user avatar. The value is the corresponding file name under /resources/rawfile.
          fileData = await resourceMgr.getRawFileContent('example.png');
        } catch (e) {
          hilog.error(DOMAIN, 'testTag', 'Failed to get raw file: %{public}d %{public}s', e.code, e.message);
        }
        const buffer = fileData.buffer;
        const imageSource: image.ImageSource = image.createImageSource(buffer);
        const pixelMap: image.PixelMap = await imageSource.createPixelMap();
        if (pixelMap) {
          pixelMap.getImageInfo((err, imageInfo) => {
            if (imageInfo) {
              hilog.info(DOMAIN, 'testTag',
                `User profile imageInfo: ${imageInfo.size.width} * ${imageInfo.size.height}.`);
            } else {
              hilog.error(DOMAIN, 'testTag', `Failed to obtain the image information.code is ${err.code}, message is ${err.message}`);
            }
          });
        }
    
        // Define the parameters for reporting an incoming call. Note that the voipCallType.scene field is defined by yourself to specify the call scenario type. Ensure that the options of this field defined on the device side are consistent with those used for pushing messages from the server side.
        let call: voipCall.VoipCallAttribute = {
          callId: callId,
          voipCallType: scene?.scene === 'video' ? voipCall.VoipCallType.VOIP_CALL_VIDEO :
            voipCall.VoipCallType.VOIP_CALL_VOICE,
          userName: 'push',
          userProfile: pixelMap,
          abilityName: 'VoIPUIAbility',
          voipCallState: voipCall.VoipCallState.VOIP_CALL_STATE_RINGING
        };
    
        try {
          // Report the incoming call.
          let error = await voipCall.reportIncomingCall(call);
          hilog.info(DOMAIN, 'testTag', `ReportIncomingCall result: ${error}.`);
        } catch (err) {
          let e: BusinessError = err as BusinessError;
          hilog.error(DOMAIN, 'testTag', 'Failed to report incoming call: %{public}d %{public}s', e.code, e.message);
        }
    
        // ...Trigger vibration and play a ringtone in the app.
      }
    
      public static async processVoipCallEvent(event: voipCall.VoipCallUiEvent) {
        try {
          switch (event) {
            case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_VOICE_ANSWER:
            case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_VIDEO_ANSWER:
              // Immediately report the answered state to Call Service Kit.
              await voipCall.reportCallStateChange(VoipCallService.callId,
                voipCall.VoipCallState.VOIP_CALL_STATE_ANSWERED);
    
              // Implement call answering in the app.
    
              // After the call is answered in the app, report the active state to Call Service Kit.
              await voipCall.reportCallStateChange(VoipCallService.callId,
                voipCall.VoipCallState.VOIP_CALL_STATE_ACTIVE);
              break;
            case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_REJECT:
            case voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_HANGUP:
              // ...Implement call hang-up in the app.
    
              // Report the call status to Call Service Kit.
              await voipCall.reportCallStateChange(VoipCallService.callId,
                voipCall.VoipCallState.VOIP_CALL_STATE_DISCONNECTED);
              break;
            default: {
              break;
            }
          }
        } catch (err) {
          let e: BusinessError = err as BusinessError;
          hilog.error(DOMAIN, 'testTag', 'Failed to report call state change: %{public}d %{public}s', e.code, e.message);
        }
      }
    
      public static close(): void {
        hilog.info(DOMAIN, 'testTag', 'Close VoIP');
    
        VoipCallService.processVoipCallEvent(voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_HANGUP);
        try {
          voipCall.off('voipCallUiEvent');
        } catch (err) {
          let e: BusinessError = err as BusinessError;
          hilog.error(DOMAIN, 'testTag', 'Failed to unregister event: %{public}d %{public}s', e.code, e.message);
        }
      }
    }

    You need to add the example.png file to the src/main/resources/rawfile directory of the project to specify the avatar displayed upon an incoming call.

    • UIAbility.onCreate is a synchronous API and does not support asynchronous callback. You need to register pushService.receiveMessage() at the lifecycle entry of the onCreate method and ensure that no asynchronous method awaits execution before the registration.
    • Receive the in-app call message in receiveMessage(). Your app client is advised to connect to the app server in advance so that the call can be directly connected when the user answers the call. In addition, your app needs to call voipCall.on to register the call status listening callback. After the user answers or rejects the call, the registered call status listening callback will return the call status.
    • When an in-app call is incoming, call the voipCall.reportIncomingCall API within 10 seconds to report the call status. When receiving the call status, the system displays the notification banner. The callId parameter passed in the voipCall.reportIncomingCall() API must be the same as the callId returned by receiveMessage().
    • If the in-app call connection fails to be established, call voipCall.reportIncomingCallError() to report the connection failure. If an app running in the foreground first receives an incoming in-app call through its own network connection and calls the voipCall.reportIncomingCall API to report the incoming call before it receives the in-app call message pushed by Push Kit, the app needs to call the voipCall.reportIncomingCallError() API to report the busy line.
    • An in-app call generally has three callback statuses: answered, rejected, and ended.
      • Answered: Your app needs to call the voipCall.reportCallStateChange API to report the call activation status after the connection is set up.
      • Rejected: After the connection to the server is disconnected, your app needs to call the voipCall.reportCallStateChange API to report the call rejection status.
      • Ended: If a carrier call arrives during an in-app call, the carrier call answering screen is launched. If the user taps the answer button, a callback is triggered indicating that the in-app call has ended. In this case, you need to disconnect your app client from the server in the callback method and call the voipCall.reportCallStateChange API to report the ended status.
    • For details about the in-app call callback statuses, please refer to Call Service Kit.
    • After reporting the incoming call, your app can call the vibrator.startVibration to trigger device vibration. For details about vibration, please refer to Sensor Service Kit. You can use the AVPlayer to play your app's ringtone. You are advised to set the audio stream to ringtone by setting usage to STREAM_USAGE_RINGTONE. When a call comes in, the ringtone starts to play and the music playback pauses. In addition, you can use AudioSession to manage the audio focus. This ensures that the audio focus is maintained throughout the call answering. For details, please refer to Audio Kit.
    • If your app is in the OVERHEATED state (Level = 4: device overheating or heavy load) during a voice or video call, please reduce the bitrate and frame rate, or disable the video stream and proceed with audio only. For details, please refer to the getLevel() API provided by Basic Services Kit.
  3. Add the video call answering screen CalleePage.ets to the src/main/ets/pages directory of the project. The sample code is as follows:

    import CallComponent from '../component/CallComponent';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    
    const DOMAIN = 0x0000;
    
    @Entry
    @Component
    struct CalleePage {
      @StorageLink('close') @Watch('close') end: boolean | undefined = undefined;
    
      aboutToAppear() {
        hilog.info(DOMAIN, 'testTag', 'CalleePage aboutToAppear');
    
        this.end = false;
      }
    
      private close() {
        if (this.end) {
          hilog.info(DOMAIN, 'testTag', 'CalleePage close');
    
          this.getUIContext().getRouter().back(); // This is only an example (going back). Set the routing based on your needs.
        }
      }
    
      aboutToDisappear() {
        hilog.info(DOMAIN, 'testTag', 'CalleePage aboutToDisappear');
      }
    
      build() {
        Column() {
          CallComponent({})
        }
      }
    }

    The sample code of CallComponent.ets (in the src/main/ets/component directory of the project) is as follows:

    import { VoipCallService } from '../service/VoipCallService';
    import { voipCall } from '@kit.CallServiceKit';
    
    @Component
    export default struct CallComponent {
      @StorageLink('close') end: boolean | undefined = undefined;
    
      build() {
        Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.SpaceBetween }) {
          Row() {
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
    
          Row({ space: 30 }) {
    
            Column() {
              Button()
                .width(80)
                .height(80)
                .backgroundColor(Color.Green)
                .onClick(() => {
                  VoipCallService.processVoipCallEvent(voipCall.VoipCallUiEvent.VOIP_CALL_EVENT_VIDEO_ANSWER);
                })
    
              Text('Answer').fontColor(Color.White).padding({ top: 5 })
            }
    
            Column() {
              Button()
                .width(80)
                .height(80)
                .backgroundColor(Color.Red)
                .onClick(() => {
                  this.end = true;
                  VoipCallService.close();
                })
    
              Text('Hang Up').fontColor(Color.White).padding({ top: 5 })
            }
    
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
        }
        .padding('30 10')
        .backgroundColor(Color.Black)
      }
    }

    Add page entries to src/main/resources/base/profile/main_pages.json at the project level. The sample code is as follows:

    {
      "src": [
        "pages/Index",
        "pages/CalleePage"
      ]
    }

    The page effect implemented by the sample code is for reference only.

  4. Configure actions for VoIPUIAbility in the abilities section of the src/main/module.json5 file of the project.

    "abilities": [
      // ...
      {
        "name": "VoIPUIAbility",
        "srcEntry": "./ets/entryability/VoIPUIAbility.ets",
        "launchType": "singleton",
        "description": "$string:module_desc",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": false,
        "skills": [
          // Keep the existing skill object unchanged.
          // Add an independent skill object and set actions to action.ohos.push.listener. This action can be defined for only one ability.
          {
            "actions": [
              "action.ohos.push.listener"
            ]
          }
        ]
      }
      // ...
    ]
    • actions: Set this parameter to action.ohos.push.listener. This action can be defined for only one ability. If the uris parameter also exists, it should be left empty.
  5. Call the REST API provided by Push Kit on your app server to send a message. For details, please refer to the function description for the API used to push scenario-specific messages.

[h2]In-app Call Message

  1. To notify the callee of an incoming call, call the REST API provided by Push Kit on your app server to send an in-app call message. The following is a request example:

    // Request URL
    POST "https://push-api.cloud.huawei.com/v3/[projectId]/messages:send"
    
    // Request header
    Content-Type: application/json
    Authorization: Bearer eyJr*****OiIx---****.eyJh*****iJodHR--***.QRod*****4Gp---****
    push-type: 10
    
    // Request body
    {
      "pushOptions": {
        "ttl": 30
      },
      "payload": {
        "extraData": "{\"scene\": \"voice\"}"
      },
      "target": {
        "token": ["MAMzLg**********aZW"]
      }
    }
    • [projectId]: project ID. To obtain the value, sign in to AppGallery Connect, click Development and services, click the desired project, and go to Project settings.
    • Authorization: a character string in JWT format. Obtain the value by referring to Generating a JWT Based on a Service Account.
    • push-type: The value 10 indicates the in-app call message type.
    • token: push token. Obtain the value by referring to Obtaining a Push Token.
    • extraData: extra data carried, which is a character string. For details, please refer to the description of the extraData parameter in VoIPCallPayload (In-App Call Message). To obtain extraData, please refer to the relevant sample code.
    • ttl: message cache duration. You are advised to set the duration to 30 to 60 seconds. For details, please refer to pushOptions.ttl.
  • In-app call messages can only be used to wake up an app client during a voice/video call and notify users of the incoming call. Do not use this type of messages to end a call or communicate with the app client directly. Your app server should communicate with your app client through its own network connection. Compared with pushing messages, it is faster to use the existing network connection between the app server and app client for communication. If the connection to Push Kit is poor, push messages may fail to reach the app client.

  • You are advised to use Push Kit to send in-app call messages and then send call requests through the existing network connection between the app server and app client, regardless of whether the app client is running in the foreground. This ensures that calls can reach the app client properly.

[h2]Missed Call Notification

  1. To send a missed call notification to the callee, call the corresponding REST API on your app server to send a notification message. The following uses a notification message as an example:

    // Request URL
    POST "https://push-api.cloud.huawei.com/v3/[projectId]/messages:send"
       
    // Request header
    Content-Type: application/json
    Authorization: Bearer eyJr*****OiIx---****.eyJh*****iJodHR--***.QRod*****4Gp---****
    push-type: 0
       
    // Request body
    {
      "pushOptions": {
        "ttl":86400
      },
      "payload": {
        "notification": {
          "category": "MISS_CALL",
          "title": "Notification title",
          "body": "Notification body",
          "clickAction": {
            "actionType": 0
          },
          "appMessageId": "12345"
        }
       },
       "target": {
         "token": ["MAMzLg**********aZW"]
       }
     }
    • push-type: The value 0 indicates the notification message type.
    • category: message category used for self-classification. Set this parameter to MISS_CALL. For details, please refer to the parameter description. Before pushing messages, ensure that you have applied for the required notification message self-classification permission.
    • appMessageId: unique ID of an app message. When the calling party hangs up, the called party's VoIP app running in the foreground can call Notification Kit to send a missed call notification; if the VoIP app is running in the background, the app can use Push Kit to send the missed call notification. Whether an app is running in the foreground or background sometimes cannot be accurately determined, and two missed call notifications may be sent in this case. You are advised to use the same appMessageId for Notification Kit and Push Kit to send missed call notifications. The notifications will then be automatically deduplicated by ID.
    • For details about other parameters, please refer to AlertPayload (Notification Message).