In a world where connectivity through audio and video calls is essential, if you're planning to create a video-calling app with call-trigger functionality, you've come to the right place.

In this tutorial, we will build a comprehensive video calling app for Android that enables smooth call handling and high-quality video communication. We’ll utilize the Telecom Framework to manage call functionality, Firebase for real-time data synchronization, and VideoSDK to deliver clear, reliable video conferencing.

Take a moment to watch the video demonstration and review the complete code for the sample app to see exactly what you'll be building in this blog.

Understanding the Telecom Framework

Before we get started, let’s take a closer look at the Telecom Framework. This framework manages both audio and video calls on Android devices, supporting traditional SIM-based calls as well as VoIP calls via the ConnectionService API.
The major components that Telecom manages are ConnectionService and InCallService:

  • ConnectionService handles the technical aspects of call connections, managing states, and audio/video routing.
  • InCallService manages the user interface, allowing users to see and interact with ongoing calls.

Understanding how the app will function internally before building it will make the development process smoother.

App Functionality Overview

To understand how the app functions, consider the following scenario: John wants to call his friend Max. John opens the app, enters Max's caller ID, and presses "Call." Max sees an incoming call UI on his device, with options to accept or reject the call. If he accepts, a video call is established between them using VideoSDK.

Steps in the Process:

  1. User Action: John enters Max's Caller ID and initiates the call.
  2. Database and Notification: The app maps the ID in the Firebase database and sends a notification to Max's device.
  3. Incoming Call UI: Max’s device receives the notification, triggering the incoming call UI using the Telecom Framework.
  4. Call Connection: If Max accepts, the video call begins using VideoSDK.

Here is a pictorial representation of the flow for better understanding.

Video SDK Image

Now that we have established the flow of the app and how it functions, let's get started with development!

Core Functionality of the App

The app relies on several key libraries to manage video calling and notifications:

  • Android Telecom Framework: Manages call routing and interaction with the system UI for incoming and outgoing calls.
  • Retrofit: Used for sending and receiving API requests, including call initiation and status updates.
  • Firebase Cloud Messaging (FCM): Handles push notifications to trigger call events.
  • Firebase Realtime Database: Stores user tokens and caller IDs for establishing video calls.
  • VideoSDK: Manages the actual video conferencing features.

Prerequisites

  • Android Studio (for Android app development)
  • Firebase Project (for notifications and database management)
  • VideoSDK Account (for video conferencing functionality)
  • Node.js and Firebase Tools (for backend development)

Make sure you have a basic understanding of Android development, Retrofit, and Firebase Cloud Messaging.

Now that we've covered the prerequisites, let's dive into building the app

Android App Setup

Add Dependencies

  1. In your settings.gradle, add Jetpack and Maven repositories:
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://www.jitpack.io' }
        maven { url "https://maven.aliyun.com/repository/jcenter" }
    }
}
  1. In your build.gradle file, add the following dependencies:
   //VideoSdk
    implementation 'live.videosdk:rtc-android-sdk:0.1.35'
    implementation 'com.nabinbhandari.android:permissions:3.8'
    implementation 'com.amitshekhar.android:android-networking:1.0.2'

    //Firebase
    implementation 'com.google.firebase:firebase-messaging:23.0.0'
    implementation platform('com.google.firebase:firebase-bom:33.4.0')
    implementation 'com.google.firebase:firebase-analytics'

    //Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'

Set Permissions in AndroidManifest.xml

Ensure the following permissions are configured:

    <uses-feature
        android:name="android.hardware.camera"
        android:required="false" />
    <uses-feature
        android:name="android.hardware.telephony"
        android:required="false" />

    <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />


How to Configure Firebase for Notifications and Realtime Database

[a] Firebase Setup for Notifications & Realtime Database

Step 1: Add Firebase to Your Android App

  • Go to the Firebase Console and create a new project.
  • Download the google-services.json file and place it in your project’s app/ directory

Step 2: Add Firebase Dependencies

  • In your build.gradle files, add the necessary Firebase dependencies:
// Project-level build.gradle
classpath 'com.google.gms:google-services:4.3.10'

// App-level build.gradle
apply plugin: 'com.google.gms.google-services'

implementation 'com.google.firebase:firebase-messaging:23.0.0'
implementation 'com.google.firebase:firebase-database:20.0.0'

Step 3: Enable Firebase Messaging and Real-time Database

  • Enable Firebase Cloud Messaging (FCM) and Realtime Database in the Firebase console under your project settings.

Step 4: Firebase Service Configuration

Ensure your app is registered in Firebase, and implement a FirebaseMessagingService to handle notifications, which we will do later.

[b] Firebase Server-Side Setup (serviceAccount.json file)

To set up Firebase Admin SDK for your server, follow these steps:

  1. Go to the Firebase Console and select your project.
  2. In Project Settings, navigate to the Service Accounts tab.
  3. Click on Generate a new private key to download your service account JSON file. This file will be named something like <your-project>-firebase-adminsdk-<unique-id>.json.

Project Structure

Root/
├── app/
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/
│   │   │   │   ├── FirebaseDatabase/
│   │   │   │   │   └── DatabaseUtils.java
│   │   │   │   ├── Meeting/
│   │   │   │   │   ├── MeetingActivity.java
│   │   │   │   │   └── ParticipantAdapter.java
│   │   │   │   ├── Network/
│   │   │   │   │   ├── ApiClient.java
│   │   │   │   │   ├── ApiService.java
│   │   │   │   │   ├── NetworkCallhandler.java
│   │   │   │   │   └── NetworkUtils.java
│   │   │   │   ├── Services/
│   │   │   │   │   ├── CallConnectionService.java
│   │   │   │   │   ├── MyFirebaseMessagingService.java
│   │   │   │   │   └── MyInCallService.java
│   │   │   │   ├── MainActivity.java
│   │   │   │   ├── MainApplication.java
│   │   │   │   └── MeetingIdCallBack.java
│   │   ├── res/
│   │   │   └── layout/
│   │   │       ├── activity_main.xml
│   │   │       ├── activity_meeting.xml
│   │   │       └── item_remote_peer.xml
├── build.gradle
└── settings.gradle

Let's get started with the basic UI of the call-initiating screen.

UI Development

The MainActivity layout provides the initial screen where users can initiate video calls. The key components include:

  • Caller ID Input: An EditText for entering the unique caller ID to initiate a call.
  • Call Button: A button to trigger the call.
  • Unique ID Display: Displays a unique ID for each user.
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:background="?android:attr/windowBackground">

    <com.google.android.material.appbar.MaterialToolbar
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:title="VideoSDK CallKit Example"
        app:titleTextColor="@color/white"
        android:background="?attr/colorPrimaryDark"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:orientation="vertical"
        android:gravity="center"
        android:padding="24dp"
        android:layout_weight="1">

   <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:drawable/editbox_background_normal"
        android:orientation="vertical"
        android:layout_marginBottom="50dp"
        android:backgroundTint="#E7E1E1">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Your Caller ID"
            android:textSize="23sp"
            android:textColor="@color/black"
            android:paddingLeft="12dp"
            android:paddingRight="12dp"
            android:fontFamily="sans-serif-medium"
            android:gravity="center"
            android:elevation="4dp"
            android:layout_margin="8dp"
            android:clipToOutline="true"
            />

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@color/black"
            />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center"
            >
            <TextView
                android:id="@+id/txt_callId"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Call Id"
                android:textSize="32sp"
                android:textColor="@color/black"
                android:paddingLeft="12dp"
                android:paddingRight="12dp"
                android:fontFamily="sans-serif-medium"
                android:gravity="center"
                android:layout_margin="8dp"
                android:textIsSelectable="true"
                android:clipToOutline="true"
                />

            <ImageView
                android:id="@+id/copyIcon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/baseline_content_copy_24"
                android:padding="8dp"
                />
        </LinearLayout>
    </LinearLayout>


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:drawable/editbox_background_normal"
        android:orientation="vertical"
        android:gravity="center"
        android:backgroundTint="#E7E1E1"
        android:padding="14dp"
        >
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Enter call ID of user you want to call"
            android:textSize="23sp"
            android:textColor="@color/black"
            android:fontFamily="sans-serif-medium"
            android:elevation="4dp"
            android:layout_marginBottom="20dp"
            />

        <EditText
            android:inputType="number"
            android:id="@+id/caller_id_input"
            android:layout_width="match_parent"
            android:layout_height="64dp"
            android:hint="Enter Caller ID"
            android:layout_marginBottom="10dp"
            android:background="@android:drawable/editbox_background_normal"
            android:textSize="18sp"
            android:textColor="@android:color/black"
            android:gravity="center"/>

        <Button
            android:id="@+id/call_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Call"
            android:padding="16dp"
            android:layout_marginTop="20dp"
            android:textSize="18sp"
            android:textColor="@android:color/white"
            />

    </LinearLayout>

    </LinearLayout>
</LinearLayout>

activity_main.xml

This is how the UI will look:

Video SDK Image

With the UI for calling in place let's start with the actual calling development.

Firebase Messaging for Call Initiation

  1. To initiate the call process, we first need to secure user permission to manage notifications and calls.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
        {
            checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 133) {
            boolean allPermissionsGranted = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    allPermissionsGranted = false;
                    break;
                }
            }

            if (allPermissionsGranted) {
                registerPhoneAccount();
            } else {
                Toast.makeText(this, "Permissions are required for call management", Toast.LENGTH_LONG).show();
            }
        }
    }

    private static final int PERMISSION_REQ_ID = 22;

    private static final String[] REQUESTED_PERMISSIONS = new String[]{
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.POST_NOTIFICATIONS
    };

    private boolean checkSelfPermission(String permission, int requestCode) {
        if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode);
            return false;
        }
        return true;
    }
}
  1. Once permission is granted, the next step is to identify each user and retrieve their messaging token, enabling us to send notifications effectively.
Don't worry if you encounter an error due to a missing file. Please continue following the steps, as the required file will be provided later in the guide.
public class MainActivity extends AppCompatActivity {

    private EditText callerIdInput;
    private TextView myId;
    ImageView copyIcon;
    String myCallId = String.valueOf(10000000 + new Random().nextInt(90000000));

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
        {
            checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID);
        }
        myId = findViewById(R.id.txt_callId);
        callerIdInput = findViewById(R.id.caller_id_input);

        copyIcon = findViewById(R.id.copyIcon);
        Button callButton = findViewById(R.id.call_button);
        myId.setText(myCallId);

        ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
        ClipData clipData = ClipData.newPlainText("copied text", myCallId);

        copyIcon.setOnClickListener(v -> {
            clipboardManager.setPrimaryClip(clipData);
            Toast.makeText(this, "Copied to clipboard", Toast.LENGTH_SHORT).show();
        });

        NetworkUtils initMeeting = new NetworkUtils();

        initMeeting.createMeeting(new MeetingIdCallBack() {
            @Override
            @Async
            public void onMeetingIdReceived(String meetingId, String token) {
                MainApplication.setMeetingId(meetingId);
            }
        });

        NetworkCallHandler.myCallId = myCallId;
        DatabaseUtils.myCallId = myCallId;

        //Firebase Notification
        NotificationChannel channel = new NotificationChannel("notification_channel", "notification_channel", NotificationManager.IMPORTANCE_DEFAULT);
        NotificationManager manager = getSystemService(NotificationManager.class);
        manager.createNotificationChannel(channel);
        FirebaseMessaging.getInstance().subscribeToTopic("general")
                .addOnCompleteListener(new OnCompleteListener<Void>() {
                    @Override
                    public void onComplete(@NonNull Task<Void> task) {
                        String msg = "Subscribed Successfully";
                        if (!task.isSuccessful()) {
                            msg = "Subscription failed";
                        }
                        Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
                    }
                });

       //Firebase Database Actions
        DatabaseUtils databaseUtils = new DatabaseUtils();

        DatabaseReference databaseReference = FirebaseDatabase.getInstance().getReference();
        FirebaseMessaging.getInstance().getToken().addOnCompleteListener( task -> {
            NetworkCallHandler.FcmToken = task.getResult();
            DatabaseUtils.FcmToken= task.getResult();
            databaseUtils.sendUserDataToFirebase(databaseReference);
        });

        callButton.setOnClickListener(v -> {
            String callerNumber = callerIdInput.getText().toString();

            if (callerNumber.length() == 8) {
                databaseUtils.retrieveUserData(databaseReference, callerNumber);
            } else {
                Toast.makeText(MainActivity.this, "Please input the correct caller ID", Toast.LENGTH_SHORT).show();
            }
        });
    }

Check if the FirebaseMessaging token is already present in the database. If it exists, update the callee ID; otherwise, create a new entry in the database.

public class DatabaseUtils {

    String calleeInfoToken ;
    public static String FcmToken ;
    public static String myCallId;

    public void sendUserDataToFirebase(DatabaseReference databaseReference) {

        DatabaseReference usersRef = databaseReference.child("User");

        usersRef.orderByChild("token").equalTo(FcmToken).addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                if (dataSnapshot.exists()) {
                    // Token exists, update the callerId
                    for (DataSnapshot userSnapshot : dataSnapshot.getChildren()) {
                        userSnapshot.getRef().child("callerId").setValue(myCallId)
                                .addOnSuccessListener(aVoid -> {
                                    Log.d("FirebaseData", "CallerId successfully updated.");
                                })
                                .addOnFailureListener(e -> {
                                    Log.e("FirebaseError", "Failed to update callerId.", e);
                                });
                    }
                } else {
                    // Token doesn't exist, create new entry
                    String userId = usersRef.push().getKey();
                    Map<String, Object> map = new HashMap<>();
                    map.put("callerId", myCallId);
                    map.put("token", FcmToken);

                    if (userId != null) {
                        usersRef.child(userId).setValue(map)
                                .addOnSuccessListener(aVoid -> {
                                    Log.d("FirebaseData", "Data successfully saved.");
                                })
                                .addOnFailureListener(e -> {
                                    Log.e("FirebaseError", "Failed to save data.", e);
                                });
                    }
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {
                Log.e("FirebaseError", "Error checking for existing token", databaseError.toException());
            }
        });
    }
}

When the call is initiated, first verify if the caller ID exists in the Firebase database. If it does, proceed to invoke the notification method.

public class DatabaseUtils {

 //...
    public void retrieveUserData(DatabaseReference databaseReference, String callerNumber) {
    
        NetworkCallHandler callHandler = new NetworkCallHandler();
        databaseReference.child("User").orderByChild("callerId").equalTo(callerNumber).addListenerForSingleValueEvent(new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot snapshot) {
                if (snapshot.exists()) {
                    for (DataSnapshot data : snapshot.getChildren()) {
                        String token = data.child("token").getValue(String.class);
                        if (token != null) {
                            calleeInfoToken = token;
                            NetworkCallHandler.calleeInfoToken = token;
                            callHandler.initiateCall();
                            break;
                        }
                    }
                } else {
                    Log.d("TAG", "retrieveUserData: No matching callerId found");
                }
            }
            @Override
            public void onCancelled(@NonNull DatabaseError error) {
                Log.e("FirebaseError", "Failed to read data from Firebase", error.toException());
            }
        });
    }
}

We'll configure the VideoSDK token and Meeting ID as soon as the home screen loads, ensuring they're ready when the user initiates a call.

public class NetworkUtils {
    
String sampleToken = MainApplication.getToken();

public void createMeeting(MeetingIdCallBack callBack) {
    // we will make an API call to VideoSDK Server to get a roomId
    AndroidNetworking.post("https://api.videosdk.live/v2/rooms")
            .addHeaders("Authorization", sampleToken) //we will pass the token in the Headers
            .build()
            .getAsJSONObject(new JSONObjectRequestListener() {
                @Override
                public void onResponse(JSONObject response) {
                    try {
                        // response will contain `meetingID`
                        final String meetingId = response.getString("roomId");
                        callBack.onMeetingIdReceived(meetingId,sampleToken);
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void onError(ANError anError) {
                    anError.printStackTrace();
                }
            });
}
}

The MeetingIdCallBack interface allows us to receive the meetingId and token in our MainActivity.

public interface MeetingIdCallBack {
    void onMeetingIdReceived(String meetingId,String token);
}

using onMeetingIdReceived callback method to get meetingId and token.

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      //...
          NetworkUtils networkUtils = new NetworkUtils();
          networkUtils.createMeeting(new MeetingIdCallBack() {
          @Override
          public void onMeetingIdReceived(String meetingId, String token) {
              MainApplication.setMeetingId(meetingId);
          }
      });
}
  1. The next step is to initiate the call.

For this, we’ll set up an Express server with two APIs as Firebase functions—one to trigger notifications on the other device and another to update the call status (accepted or rejected).

Start by importing and initializing the required packages in  server.js .

we will also need to initialize Firebase Admin SDK.

const functions = require("firebase-functions");
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
var admin = require("firebase-admin");
const { v4: uuidv4 } = require("uuid");

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(morgan("dev"));

// Path to your service account key file for Firebase Admin SDK
var serviceAccount = require("add_path_here");

// Initialize Firebase Admin SDK
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "database_url" // Replace with your database URL
});

// Home Route
app.get("/", (req, res) => {
  res.send("Hello World!");
});

// Start the Express server
app.listen(9000, () => {
  console.log(`API server listening at http://localhost:9000`);
});

// Export app as a Firebase Cloud Function
exports.app = functions.https.onRequest(app);
  • The first API we need is initiate-call, which will be used to send a notification to the receiving user and start the call by sending details like caller information and VideoSDK room details.
// Initiate call notification (for Android)
app.post("/initiate-call", (req, res) => {
  const { calleeInfo, callerInfo, videoSDKInfo } = req.body;

  var FCMtoken = calleeInfo.token;
    const info = JSON.stringify({
      callerInfo,
      videoSDKInfo,
      type: "CALL_INITIATED",
    });
    var message = {
      data: {
        info,
      },
      android: {
        priority: "high",
      },
      token: FCMtoken,
    };
    
  // Send the FCM message using firebase-admin
  admin.messaging().send(message)
    .then((response) => {
      console.log("Successfully sent FCM message:", response);
      res.status(200).send(response);
    })
    .catch((error) => {
      console.log("Error sending FCM message:", error);
      res.status(400).send("Error sending FCM message: " + error);
    });
});
  • The second API we need is update-call, which updates the status of the incoming call (such as accepted, rejected, etc.) and sends a notification to the caller.
// Update call notification (for Android)
app.post("/update-call", (req, res) => {
  const { callerInfo, type } = req.body;
  const info = JSON.stringify({
    callerInfo,
    type,
  });

  var message = {
    data: {
      info,
    },
    token: callerInfo.token,
  };
  var message = {
    data: { info },
    token: callerInfo.token, // Token for the target device
    android: {
      priority: "high",
      notification: {
        title: "Call Updated",
        body: "Your call has been updated by " + callerInfo.name,
      },
    },
  };

  // Send the update message through firebase-admin
  admin.messaging().send(message)
    .then((response) => {
      console.log("Successfully updated call:", response);
      res.status(200).send(response);
    })
    .catch((error) => {
      console.log("Error updating call:", error);
      res.status(400).send("Error updating call: " + error);
    });
});

4. Now that the APIs are created, we will trigger them from the app.

Here the FCM_SERVER_URL needs to be updated with the URL of your Firebase functions.

You can either deploy the server or run the server in a local environment using npm run server.js

If you're running it on a local device, you need to use the device's IP address.

public class ApiClient {
    private static final String BASE_URL = "FCM_SERVER_URL";
    private static Retrofit retrofit = null;

    public static Retrofit getClient() {
        if (retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    // ScalarsConverterFactory handles plain text responses
                    .addConverterFactory(ScalarsConverterFactory.create())
                    // GsonConverterFactory handles JSON responses
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }
}

Now, let's define endpoints for API Calls.

public interface ApiService {

    @POST("/initiate-call")
    Call<String> initiateCall(@Body Map<String, Object> callRequestBody);

    @POST("/update-call")
    Call<String> updateCall(@Body Map<String,Object> callUpdateBody);
}

Initiates a call by sending caller, callee, and VideoSDK information to the server
and handles the server response and logs success or failure accordingly.

public  class NetworkCallHandler {

    static ApiService apiService = ApiClient.getClient().create(ApiService.class);
    public static String myCallId;
    public static String FcmToken;
    public static String calleeInfoToken;

    public void initiateCall() {
        ApiService apiService = ApiClient.getClient().create(ApiService.class);

        Map<String,String> callerInfo = new HashMap<>();
        Map<String,String> calleeInfo = new HashMap<>();
        Map <String,String> videoSDKInfo= new HashMap<>();

        //callerInfo
        callerInfo.put("callerId",myCallId);
        callerInfo.put("token",FcmToken);

        //calleeInfo
        calleeInfo.put("token",calleeInfoToken);

        //videoSDKInfo
        videoSDKInfo.put("meetingId", MainApplication.getMeetingId());
        videoSDKInfo.put("token",MainApplication.getToken());

        Map<String,Object> callRequestBody = new HashMap<>();
        callRequestBody.put("callerInfo",callerInfo);
        callRequestBody.put("calleeInfo",calleeInfo);
        callRequestBody.put("videoSDKInfo",videoSDKInfo);

        Call<String> call = apiService.initiateCall(callRequestBody);
        call.enqueue(new Callback<String>() {
            @Override
            public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
                if (response.isSuccessful()) {
                    Log.d("API", "Call initiated: " + response.body());
                } else {
                    Log.e("API", "Failed to initiate call: " + response.message());
                }
            }

            @Override
            public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
                Log.e("API", "API call failed: " + t.getMessage());
            }
        });
    }
}

5. The notification sent is now configured. Now we need to invoke the call when you receive the notification.

You can extract all the information from the notification body, which will help us to create a meeting.

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String TAG = "FCMService";
    private static final String CHANNEL_ID = "notification_channel";

    String callerID;
    String meetingId ;
    String token ;
    public static String FCMtoken;

    @Override
    public void onNewToken(@NonNull String token) {
        super.onNewToken(token);
    }

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {

        // Handle incoming message (call request)
        Map<String, String> data = remoteMessage.getData();

        if (!data.isEmpty()) {
            try {
                JSONObject object = new JSONObject(data.get("info"));
                JSONObject callerInfo = object.getJSONObject("callerInfo");
                callerID = callerInfo.getString("callerId");
                FCMtoken  =  callerInfo.getString("token");
                if (object.has("videoSDKInfo")) {
                    JSONObject videoSdkInfo = object.getJSONObject("videoSDKInfo");
                    meetingId = videoSdkInfo.getString("meetingId");
                    token = videoSdkInfo.getString("token");
                    handleIncomingCall(callerID);
                }
                String type = (String) object.get("type");

                if(type.equals("ACCEPTED")){
                    startMeeting();
                } else if (type.equals("REJECTED")) {
                    showIncomingCallNotification(callerID);
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            Toast toast = Toast.makeText(getApplicationContext(), "CALL REJECTED FROM CALLER ID: " + callerID, Toast.LENGTH_SHORT);
                            toast.show();
                        }
                    });
                }
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        } else {
            Log.d(TAG, "onMessageReceived: No data found in the notification payload.");
        }
    }

    private void startMeeting() {
        Intent intent = new Intent(getApplicationContext(), MeetingActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("meetingId", MainApplication.getMeetingId());
        intent.putExtra("token", MainApplication.getToken());
        startActivity(intent);
    }

    private void handleIncomingCall(String callerId) {

        // Create a bundle to pass call details
        Bundle extras = new Bundle();
        Uri uri = Uri.fromParts("tel", callerId, null);
        extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri);
        extras.putString("meetingId", meetingId);
        extras.putString("token", token);
        extras.putString("callerID",callerId);

        try {
            MainApplication.telecomManager.addNewIncomingCall(MainApplication.phoneAccountHandle, extras);
        } catch (Throwable cause) {
            Log.e("handleIncomingCall", "error in addNewIncomingCall ", cause.getCause());
        }
    }
    
    private void showIncomingCallNotification(String callerId) {
        createNotificationChannel();

        Intent intent = new Intent(this, MainActivity.class);

        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE);

        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.drawable.baseline_call_24)
                .setContentTitle("Call REJECTED ")
                .setContentText("Call from " + callerId)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setFullScreenIntent(pendingIntent, true)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .setCategory(NotificationCompat.CATEGORY_CALL)
                .build();

        NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        notificationManager.notify(1, notification);
    }

    private void createNotificationChannel() {
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }
}

Add this MyFirebaseMessagingService to AndroidManifest.xml

<service
  android:name=".Services.MyFirebaseMessagingService"
  android:enabled="true"
  android:exported="true"
  android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
  <intent-filter>
      <action android:name="com.google.firebase.MESSAGING_EVENT" />
      <action android:name="com.google.android.c2dm.intent.RECEIVE" />
  </intent-filter>
</service>

Now when the notification is received, this should trigger a call.

To achieve this, we need to register TelecomManager with a PhoneAccountHandle and CallConnectionService to manage and handle calls.

Initialize the TelecomManager and PhoneAccountHandle in the MainApplication class to make them accessible throughout the application.

Also, to initiate the call with the VideoSDK, you need to add the VideoSDK token to the main application. You can obtain this token from the VideoSDK Dashboard.

public class MainApplication extends Application {

    public static TelecomManager telecomManager;
    public static PhoneAccountHandle phoneAccountHandle;
    static String meetingId;
    static String token="VideoSDK token";
    public static void setMeetingId(String meetingId) {
        MainApplication.meetingId = meetingId;
    }
    public  static String getMeetingId(){
        return meetingId;
    }
    public static String getToken() {
        return token;
    }
    @Override
    public void onCreate() {
        super.onCreate();
        VideoSDK.initialize(getApplicationContext());

        telecomManager = (TelecomManager) getSystemService(TELECOM_SERVICE);
        ComponentName componentName = new ComponentName(this, CallConnectionService.class);
        phoneAccountHandle = new PhoneAccountHandle(componentName, "myAccountId");
    }
}

Now, In MainActivity we'll register the PhoneAccount and check for permission to manage Call Activity.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //telecom Api
        registerPhoneAccount();
    }

    private void registerPhoneAccount() {

    PhoneAccount phoneAccount = PhoneAccount.builder(MainApplication.phoneAccountHandle, "VideoSDK")
            .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
            .build();

    MainApplication.telecomManager.registerPhoneAccount(phoneAccount);


        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        int checkAccount=0;
        List<PhoneAccountHandle> list = MainApplication.telecomManager.getCallCapablePhoneAccounts();
        for (PhoneAccountHandle handle:
             list) {
            if(handle.getComponentName().getClassName().equals("live.videosdk.ConnectionService.quickstart.Services.CallConnectionService"))
            {
                checkAccount++;
                break;
            }
        }

        if(checkAccount == 0) {
            Intent intent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
            startActivity(intent);
        }
    }
}

Next, to manage VoIP calls, we’ll create a new service CallConnectionService that extends ConnectionService. This service will handle the technical aspects of call connections, such as managing call states and routing audio/video.

public class CallConnectionService extends ConnectionService {

String callerID;

    @Override
    public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
        // Create a connection for the incoming call
        Connection connection = new Connection() {
            @Override
            public void onAnswer() {
                super.onAnswer();
               //getting videosdk info
                Bundle extras = request.getExtras();
                String meetingId = extras.getString("meetingId");
                String token = extras.getString("token");
                callerID = extras.getString("callerID");

                // Start the meeting activity with the extracted data
                Intent intent = new Intent(getApplicationContext(), MeetingActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.putExtra("meetingId", meetingId);
                intent.putExtra("token", token);
                startActivity(intent);
                NetworkCallHandler.updateCall("ACCEPTED");

                //update
                setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
                destroy();
            }

            @Override
            public void onReject() {
                super.onReject();
                Intent intent = new Intent(getApplicationContext(), MainActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
                //update
                NetworkCallHandler.updateCall("REJECTED");

                setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
                destroy();
            }
        };

        // Set call address
        connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
        connection.setCallerDisplayName(callerID,TelecomManager.PRESENTATION_ALLOWED);
        connection.setInitializing();  // Indicates that the call is being set up
        connection.setActive();  // Activate the call

        return connection;
    }

    @Override
    public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
        // Create a connection for the outgoing call
        Connection connection = new Connection(){};
        connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
        connection.setActive();
        return connection;
    }
}

Add the service to the AndroidManifest.xml

<service
    android:name=".Services.CallConnectionService"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
    <intent-filter>
        <action android:name="android.telecom.ConnectionService" />
    </intent-filter>
</service>

Now, To display the default Android call UI, we'll create another service called MyInCallService.

public class MyInCallService extends InCallService {

    @Override
    public void onCallAdded(Call call) {
        super.onCallAdded(call);
        call.registerCallback(new Call.Callback() {
            @Override
            public void onStateChanged(Call call, int state) {
                super.onStateChanged(call, state);
                if (state == Call.STATE_ACTIVE) {
                    // Handle the active call state
                }
            }
        });

        // Bring up the default UI for managing the call
        setUpDefaultCallUI(call);
    }

    @Override
    public void onCallRemoved(Call call) {


        super.onCallRemoved(call);
        // Clean up call-related resources
    }

    private void setUpDefaultCallUI(Call call) {
        // Start the default in-call UI
        TelecomManager telecomManager = (TelecomManager) getSystemService(TELECOM_SERVICE);
        if (telecomManager != null) {
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            telecomManager.showInCallScreen(true);
        }
    }
}

Add the service to AndroidMenifest.xml

<service
  android:name=".Services.MyInCallService"
  android:exported="true"
  android:permission="android.permission.BIND_INCALL_SERVICE">
  <intent-filter>
      <action android:name="android.telecom.InCallService" />
  </intent-filter>
</service>

Wow!! You just implemented the calling feature, which works like a charm.
Video SDK Image

VideoSDK Integration for VideoCall

  1. The first step in integrating the VideoSDK is to initialize VideoSDK.

MainApplication.java

public class MainApplication extends Application {
    }
    @Override
    public void onCreate() {
        super.onCreate();
        VideoSDK.initialize(getApplicationContext());
        //...
    }
}

When the call is received, the intent is used to transition from the call screen to the meeting screen, passing the meetingId and videoSDK token along with it.

public class CallConnectionService extends ConnectionService {

String callerID;

    @Override
    public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
        // Create a connection for the incoming call
        Connection connection = new Connection() {
            @Override
            public void onAnswer() {
                super.onAnswer();
                //...
                Intent intent = new Intent(getApplicationContext(), MeetingActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.putExtra("meetingId", meetingId);
                intent.putExtra("token", token);
                startActivity(intent);
   
            }
        };
}

Next, we will add our MeetingView which will show the buttons and Participants View in the MeetingActivity.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MeetingActivity">

    <TextView
        android:id="@+id/tvMeetingId"
        style="@style/TextAppearance.AppCompat.Display1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Meeting Id" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvParticipants"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/btnMic"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginVertical="8dp"
            android:text="Mic"/>

        <Button
            android:id="@+id/btnLeave"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginVertical="8dp"
            android:layout_marginHorizontal="8dp"
            android:text="Leave"/>

        <Button
            android:id="@+id/btnWebcam"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginVertical="8dp"
            android:text="Webcam" />

    </LinearLayout>


</LinearLayout>

Here is the logic for the MeetingActivity

public class MeetingActivity extends AppCompatActivity {

    private static final int PERMISSION_REQ_ID = 22;

    private static final String[] REQUESTED_PERMISSIONS = {
            android.Manifest.permission.RECORD_AUDIO,
            Manifest.permission.CAMERA
    };

    // declare the variables we will be using to handle the meeting
    private Meeting meeting;
    private boolean micEnabled = true;
    private boolean webcamEnabled = true;

    private RecyclerView rvParticipants;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_meeting);

        checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID);
        checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID);

        final String token = getIntent().getStringExtra("token");
        final String meetingId = getIntent().getStringExtra("meetingId");
        final String participantName = "John Doe";

        // 1. Configuration VideoSDK with Token
        VideoSDK.config(token);
        // 2. Initialize VideoSDK Meeting
        meeting = VideoSDK.initMeeting(
                MeetingActivity.this, meetingId, participantName,
                micEnabled, webcamEnabled,null, null, true,null, null);

        // 3. Add event listener for listening upcoming events
        meeting.addEventListener(meetingEventListener);

        //4. Join VideoSDK Meeting
        meeting.join();

        ((TextView)findViewById(R.id.tvMeetingId)).setText(meetingId);

        // actions
        setActionListeners();

        rvParticipants = findViewById(R.id.rvParticipants);
        rvParticipants.setLayoutManager(new GridLayoutManager(this, 2));
        rvParticipants.setAdapter(new ParticipantAdapter(meeting));
    }

    // creating the MeetingEventListener
    private final MeetingEventListener meetingEventListener = new MeetingEventListener() {
        @Override
        public void onMeetingJoined() {
            Log.d("#meeting", "onMeetingJoined()");
        }

        @Override
        public void onMeetingLeft() {
            Log.d("#meeting", "onMeetingLeft()");
            meeting = null;
            if (!isDestroyed()) finish();
        }

        @Override
        public void onParticipantJoined(Participant participant) {
            Toast.makeText(MeetingActivity.this, participant.getDisplayName() + " joined", Toast.LENGTH_SHORT).show();
        }

        @Override
        public void onParticipantLeft(Participant participant) {
            Toast.makeText(MeetingActivity.this, participant.getDisplayName() + " left", Toast.LENGTH_SHORT).show();
        }
    };

    private void setActionListeners() {
        // toggle mic
        findViewById(R.id.btnMic).setOnClickListener(view -> {
            if (micEnabled) {
                // this will mute the local participant's mic
                meeting.muteMic();
                Toast.makeText(MeetingActivity.this, "Mic Disabled", Toast.LENGTH_SHORT).show();
            } else {
                // this will unmute the local participant's mic
                meeting.unmuteMic();
                Toast.makeText(MeetingActivity.this, "Mic Enabled", Toast.LENGTH_SHORT).show();
            }
            micEnabled=!micEnabled;
        });

        // toggle webcam
        findViewById(R.id.btnWebcam).setOnClickListener(view -> {
            if (webcamEnabled) {
                // this will disable the local participant webcam
                meeting.disableWebcam();
                Toast.makeText(MeetingActivity.this, "Webcam Disabled", Toast.LENGTH_SHORT).show();
            } else {
                // this will enable the local participant webcam
                meeting.enableWebcam();
                Toast.makeText(MeetingActivity.this, "Webcam Enabled", Toast.LENGTH_SHORT).show();
            }
            webcamEnabled=!webcamEnabled;
        });

        // leave meeting
        findViewById(R.id.btnLeave).setOnClickListener(view -> {
            // this will make the local participant leave the meeting
            meeting.leave();
        });
    }

    @Override
    protected void onDestroy() {
        rvParticipants.setAdapter(null);
        super.onDestroy();
    }

    private boolean checkSelfPermission(String permission, int requestCode) {
        if (ContextCompat.checkSelfPermission(this, permission) !=
                PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode);
            return false;
        }
        return true;
    }
}

Here, we display the participants in a RecyclerView. To implement this, you'll need to use the following RecyclerView Adapter components:

a. item_remote_peer : UI for each participant

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:background="@color/cardview_dark_background"
    tools:layout_height="200dp">

    <live.videosdk.rtc.android.VideoView
        android:id="@+id/participantView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="#99000000"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:padding="4dp"
            android:textColor="@color/white" />

    </LinearLayout>

</FrameLayout>

b. ParticipantAdapter: Responsible for displaying video call participants in a RecyclerView. It manages participant join/leave events, shows video streams, and updates the view when a participant's video starts or stops.

public class ParticipantAdapter extends RecyclerView.Adapter<ParticipantAdapter.PeerViewHolder> {

    private final List<Participant> participants = new ArrayList<>();

    public ParticipantAdapter(Meeting meeting) {
        // adding the local participant(You) to the list
        participants.add(meeting.getLocalParticipant());

        // adding Meeting Event listener to get the participant join/leave event in the meeting.
        meeting.addEventListener(new MeetingEventListener() {
            @Override
            public void onParticipantJoined(Participant participant) {
                // add participant to the list
                participants.add(participant);
                notifyItemInserted(participants.size() - 1);
            }

            @Override
            public void onParticipantLeft(Participant participant) {
                int pos = -1;
                for (int i = 0; i < participants.size(); i++) {
                    if (participants.get(i).getId().equals(participant.getId())) {
                        pos = i;
                        break;
                    }
                }
                // remove participant from the list
                participants.remove(participant);

                if (pos >= 0) {
                    notifyItemRemoved(pos);
                }
            }
        });
    }

    @NonNull
    @Override
    public PeerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new PeerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_remote_peer, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull PeerViewHolder holder, int position) {
        Participant participant = participants.get(position);

        holder.tvName.setText(participant.getDisplayName());

        // adding the initial video stream for the participant into the 'VideoView'
        for (Map.Entry<String, Stream> entry : participant.getStreams().entrySet()) {
            Stream stream = entry.getValue();
            if (stream.getKind().equalsIgnoreCase("video")) {
                holder.participantView.setVisibility(View.VISIBLE);
                VideoTrack videoTrack = (VideoTrack) stream.getTrack();
                holder.participantView.addTrack(videoTrack);
                break;
            }
        }
        // add Listener to the participant which will update start or stop the video stream of that participant
        participant.addEventListener(new ParticipantEventListener() {
            @Override
            public void onStreamEnabled(Stream stream) {
                if (stream.getKind().equalsIgnoreCase("video")) {
                    holder.participantView.setVisibility(View.VISIBLE);
                    VideoTrack videoTrack = (VideoTrack) stream.getTrack();
                    holder.participantView.addTrack(videoTrack);
                }
            }

            @Override
            public void onStreamDisabled(Stream stream) {
                if (stream.getKind().equalsIgnoreCase("video")) {
                    holder.participantView.removeTrack();
                    holder.participantView.setVisibility(View.GONE);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return participants.size();
    }

    static class PeerViewHolder extends RecyclerView.ViewHolder {
        // 'VideoView' to show Video Stream
        public VideoView participantView;
        public TextView tvName;
        public View itemView;

        PeerViewHolder(@NonNull View view) {
            super(view);
            itemView = view;
            tvName = view.findViewById(R.id.tvName);
            participantView = view.findViewById(R.id.participantView);
        }
    }

    @Override
    public void onViewRecycled(@NonNull PeerViewHolder holder) {
        holder.participantView.releaseSurfaceViewRenderer();
        super.onViewRecycled(holder);
    }
}

The meeting is set up on the callee's side, and now you need to set up the meeting on the caller's side as well.

When the callee accepts the call, the update-call function on the server is called, which triggers a silent notification to the caller.

public  class NetworkCallHandler {

    public static void updateCall(String call_update) {

        String fcmToken = MyFirebaseMessagingService.FCMtoken;

        Map<String, String> callerInfo = new HashMap<>();
        Map<String, Object> callUpdateBody = new HashMap<>();

        //callerInfo
        callerInfo.put("callerId",myCallId);
        callerInfo.put("token",fcmToken);

        //CallUpdateBody
        callUpdateBody.put("callerInfo",callerInfo);
        callUpdateBody.put("type",call_update);

        Call<String> call = apiService.updateCall(callUpdateBody);
        call.enqueue(new Callback<String>() {
            @Override
            public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
                if (response.isSuccessful()) {
                    Log.d("API", "Call updated successfully: " + response.body());
                }
            }

            @Override
            public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
                Log.e("API", "Call update failed", t);
            }
        });
    }
}

When the caller's phone receives this notification, it either starts the meeting upon acceptance or displays a Toast message indicating rejection.

 public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {

        // Handle incoming message (call request)
        Map<String, String> data = remoteMessage.getData();

        if (!data.isEmpty()) {
            try {
                JSONObject object = new JSONObject(data.get("info"));
                JSONObject callerInfo = object.getJSONObject("callerInfo");
                callerID = callerInfo.getString("callerId");
                FCMtoken  =  callerInfo.getString("token");
                if (object.has("videoSDKInfo")) {
                    JSONObject videoSdkInfo = object.getJSONObject("videoSDKInfo");
                    meetingId = videoSdkInfo.getString("meetingId");
                    token = videoSdkInfo.getString("token");
                    handleIncomingCall(callerID);
                }
                String type = (String) object.get("type");

                if(type.equals("ACCEPTED")){
                    startMeeting();
                } else if (type.equals("REJECTED")) {
                    showIncomingCallNotification(callerID);
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            Toast toast = Toast.makeText(getApplicationContext(), "CALL REJECTED FROM CALLER ID: " + callerID, Toast.LENGTH_SHORT);
                            toast.show();
                        }
                    });
                }
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        } else {
            Log.d(TAG, "onMessageReceived: No data found in the notification payload.");
        }
    }
}

Here is how the video call will look with two participants:

Video SDK Image

Hurray!!! With these steps, our video calling feature is complete. Here’s a video demonstrating how it looks.

Conclusion

With this, we've successfully built the Android video calling app using the Call Trigger, VideoSDK, and Firebase. For additional features like chat messaging and screen sharing, feel free to refer to our documentation. If you encounter any issues with the implementation, don’t hesitate to reach out to us through our Discord community.

Here is the Github repo you can clone to access all the source code here