How To Build A Drop-in Video Chat Application Using Android

Many cities and states have been under lockdown since the outbreak of the coronavirus epidemic. During this difficult time, we’re all looking for new ways to stay connected and support each other. This is when social networking applications such as Houseparty become especially relevant and helpful.

These applications let users meet up and have fun with their friends without having to leave their homes. Users can enter their friend’s virtual room by just clicking a button. Houseparty, in particular, also provides some built-in games that users can play together.

If you’ve ever wondered how these cool applications are made, read on! This blog post will help get you started on the basics of building a similar social networking application on Android.

Prerequisites

  1. A basic-to-intermediate understanding of Java and Android SDK
  2. Agora.io developer account
  3. Android Studio and 2 Android devices

Please Note: While no Java/Android knowledge is needed to follow along, certain basic concepts in Java/Android won’t be explained along the way.

Overview

This guide will go over the steps for building a social networking application similar to Houseparty. This is a list of the core features that will be included in our app:

  • Users can create and login into their account. User account information will be saved in Google Firebase Realtime Database.
  • Users can set up virtual rooms to host video calls.
  • Users can configure the accessibility of their virtual rooms. “Public” rooms are open for all friends to join and “private” rooms are invitation-only.
  • During a video call, users can send private messages to another user in the same room by double-clicking on that user’s remote video.
  • Users can chat with friends who are not in the room by clicking a button next to their names in the friend list.

You can find my Github demo app as a reference for this article.

Set Up New Project

To start, let’s open Android Studio and create a new, blank project.

  1. Open Android Studio and click Start a new Android Studio project.
  2. On the Choose your project panel, choose Phone and Tablet > Empty Activity, and click Next.
  3. Click Finish. Follow the on-screen instructions if you need to install any plug-ins.

Add Project Permissions

Add project permissions in the /app/src/main/AndroidManifest.xml file for device access according to your needs:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.housepartyagora">
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>

Integrate the SDK

Add the following line in the /app/build.gradle file of your project:

dependencies {
    ...
    implementation 'com.google.firebase:firebase-database:16.0.4'
    //Agora RTC SDK for video call
    implementation 'io.agora.rtc:full-sdk:3.0.0.2'
    //Agora RTM SDK for chat messaging
    implementation 'io.agora.rtm:rtm-sdk:1.2.2'
}

Setup Google Firebase Database

Since our application allows users to search and add their friends to their friend lists in the app, we need to use Firebase Realtime Database to save users’ account information. Here are the steps you need to connect your application to Firebase Realtime Database

  1. In your Android Studio, Click “Tools”, then select “Firebase”.
  1. On the right hand side, you should see a Firebase assistant tab showing up. Find “Realtime Database” and click “Save and retrieve Data”.
  1. Then you should see the detailed page for Realtime Database. Click the “Connect to Firebase” button and “Add the Realtime Database to your app” button. Follow the on-screen instruction, if any.
  1. Now your application is connected to Firebase Realtime Database. The last thing you need to do is to go to your Firebase console and change the database rules. Remember to select the Realtime Database instead of the Cloud Firestore. Change the rules for read and write to “true” so that you can have access to the database.

User Login

First, let’s create a landing page for our application.

Pease Note: You can find the .xml file here.

If the user clicks “SIGN UP” button, we will jump to sign up page. If the user clicks “I Already Have An Account” text, we will jump to the login page. Let’s implement the activities transaction logic using Intent.

public void onLoginButtonClick(View view) {
    Intent intent = new Intent(SplashActivity.this, LoginActivity.class);
    startActivity(intent);
}

public void onSignUpButtonClick(View view) {
    Intent intent = new Intent(SplashActivity.this, SigninActivity.class);
    startActivity(intent);
}

Now, let’s create the UIs for the login page and sign up page.

Please Note: You can find the .xml file for Login page here and Sign Up page here.

When the user clicks the “Next” button, we need to get the user’s name and pass that to the next activity, VideoCallActivity, to start the video call. Here is the code for that in the Login activity, and the Signup activity will be similar.

public void onLoginNextClick(View view) {
    EditText userNameEditText = findViewById(R.id.et_login_user_name);
    String userName = userNameEditText.getText().toString();

    if(userName == null || userName == "") {
        Toast.makeText(this, "user name cannot be empty", Toast.LENGTH_SHORT).show();
    }else {
        Intent intent = new Intent(this, VideoCallActivity.class);
        intent.putExtra("userName", userName);
        startActivity(intent);
    }
}

Save User Account In Firebase Database

In the onCreate method in the VideoCallActivity, we need to save user’s information in the Firebase Database. First, get the database reference by calling:

mRef = FirebaseDatabase.getInstance().getReference("Users");

Then we call setValue to save user information as a child in the database. User information is kept in the DBUser format. In a DBUser object, it maintains a user name, a uid, a user room state and a list of friends.

mRef.push();
mRef.child(userName).setValue(new DBUser(userName, user.getAgoraUid(), localState, DBFriend));

At this point, if you run this application, you should be able to see new user information saved in your database after you login into the app.

Start Video Call

After the user logs into the app, he is in his virtual room by default. This user should be in a video call channel so that his friend can join his channel for a call. So in the VideoCallActivity, we need to implement the video call logic. But how to start a video call? It seems very complicated to do that. Luckily, I found out that Agora Video SDK provides the easiest way to start a video call.

First, let’s create a UI for this activity:

Please Note: You can find the .xml file here.

Then, in the onCreate() method in the VideoCallActivity, let’s do following several things:

  1. Initialize Agora RtcEngine
  2. Setup local video Canvas
  3. Join channel

1. Initialize Agora Rtc Engine

In order to initialize the Agora video engine, just simply call RtcEngine.create(context, appID, RtcEventHandler) to create a RtcEngine instance.

mRtcEngine = RtcEngine.create(getBaseContext(), appID, mRtcEventHandler);

In order to get the appID in the parameter, follow these steps:

  1. Create an Agora project in the Agora Console.
  2. Click the Project Management tab on the left navigation panel.
  3. Click “Create” and follow the on-screen instructions to set the project name, choose an authentication mechanism, and click “Submit”.
  4. On the Project Management page, find the App ID of your project.

The mRtcEventHandler is a handler to manage different events occurring with the RtcEngine. Let’s implement it with some basic event handlers needed for this application.

private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
    @Override
    // Listen for the onJoinChannelSuccess callback.
    // This callback occurs when the local user successfully joins the channel.
    public void onJoinChannelSuccess(String channel, final int uid, int elapsed) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                user.setAgoraUid(uid);
                mRef.child(getUserName()).setValue(new DBUser(getUserName(), user.getAgoraUid(), localState, DBFriend));
            }
        });
    }

    @Override
    // Listen for the onFirstRemoteVideoDecoded callback.
    // This callback occurs when the first video frame of a remote user is received and decoded after the remote user successfully joins the channel.
    // You can call the setupRemoteVideo method in this callback to set up the remote video view.
    public void onFirstRemoteVideoDecoded(final int uid, int width, int height, int elapsed) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                SurfaceView mRemoteView = RtcEngine.CreateRendererView(getApplicationContext());

                mRemoteView.setZOrderOnTop(true);
                mRemoteView.setZOrderMediaOverlay(true);
                mRtcEngine.setupRemoteVideo(new VideoCanvas(mRemoteView, VideoCanvas.RENDER_MODE_HIDDEN, uid));
            }
        });
    }

    @Override
    // Listen for the onUserOffline callback.
    // This callback occurs when the remote user leaves the channel or drops offline.
    public void onUserOffline(final int uid, int reason) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                onRemoteUserLeft(uid);
            }
        });
    }
};

Please check the comments on the top of each event handler methods to have a better understanding of them. For more RtcEngine event handlers that you can use, check the Agora Rtc API document.

Please Note: Some of the logic to show video views on the screen is hidden. You can check the Github demo app for a better understanding of how to display and remove video views on the screen dynamically.

2. Setup local video Canvas

To start a local video (see yourself on the screen), you need to call two functions: enableVideo() and setupLocalVideo() on a RtcEngine instance. In function setupLocalVideo(), a surfaceView created by calling RtcEngine.CreateRenderView(context) is passed as a parameter.

<spanmRtcEngine.enableVideo();
mRtcEngine.enableInEarMonitoring(true);
mRtcEngine.setInEarMonitoringVolume(80);

SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, 0));
surfaceView.setZOrderOnTop(false);
surfaceView.setZOrderMediaOverlay(false);

3. Join channel

Now we are ready to join the channel by calling joinChannel() on the RtcEngine instance. The channelName is the user name we get from the previous activity. In this case, each user will join into a video call channel with his entered user name when he login.

mRtcEngine.joinChannel(token, channelName, "Extra Optional Data", 0);

Please Note: The token in the parameter can be set to null. You can get more information about token here.

By calling this function and successfully joining the channel, the RtcEngineEventHandler will trigger the onJoinChannelSuccess() method that we implemented in the previous step. It will return a unique id from Agora server. We need to update the user information with the uid in the database.

@Override
public void onJoinChannelSuccess(String channel, final int uid, int elapsed) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            user.setAgoraUid(uid);
            mRef.child(getUserName()).setValue(new DBUser(getUserName(), user.getAgoraUid(), localState, DBFriend));
        }
    });
}

4. Search and add friends

Like a real social application, a user should be able to search his friends by names and add them in his friend list. So when user clicks on the top right button, the app should show a search friend panel like this:

The user can enter his friend’s name in the edit text and click the search button. We need to search his friend’s name in the database and display the result in the recycler view. Here is how to search people by name in the database:

childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
        DBUser result = dataSnapshot.getValue(DBUser.class);

        searchFriendList.add(result);
        mRef.orderByChild("name").startAt(searchFriendName).endAt(searchFriendName + "\uf8ff").removeEventListener(childEventListener);

        mFriendListRecyclerViewAdapter = new FriendListRecyclerViewAdapter(searchFriendList);
        mFriendListRecyclerViewAdapter.setOnItemClickListener(new FriendListRecyclerViewAdapter.ClickListener() {
            @Override
            public void onItemClick(int position, View v) {
                addFriend(searchFriendList.get(position).getName());
                mSearchFriendEditText.setText("");
                searchFriendList.clear();
                mFriendListRecyclerView.setAdapter(mFriendListRecyclerViewAdapter);
            }
        });
        RecyclerView.LayoutManager manager = new GridLayoutManager(getBaseContext(), 1);
        mFriendListRecyclerView.setLayoutManager(manager);

        mFriendListRecyclerView.setAdapter(mFriendListRecyclerViewAdapter);
    }

    ...
};

mRef.orderByChild("name").startAt(searchFriendName).endAt(searchFriendName + "\uf8ff").addChildEventListener(childEventListener);

In the recycler view, we will display the friend name searched in the database with a “Add” button on the side. If the user clicks the “Add” button, that friend is added in the user’s friend list. We also need to update the user’s friend list in the database.

public void addFriend(String userName) {
    DBFriend.add(userName);
    mRef.child(this.userName).setValue(new DBUser(this.userName, user.getAgoraUid(), localState, DBFriend));
}

5. Join a friend

Since we have an updated friend list, we can display the user’s friends panel when the user clicks on the top left button. Now the user can see his friend list and choose which friend to join the call. On the side of his friend’s name, create a “Join” button for the user to join this friend’s virtual room and have a video call with him.

public void joinFriend(String friendName){
    channelName = friendName;
    finishCalling();
    startCalling();
}

Let’s change the channelName to the user’s friend’s name so that when the startCalling method is triggered, the user will join a different channel with the same name as the name of his friend.

private void startCalling() {
    //set up local video canvas
    mRtcEngine.enableVideo();
    mRtcEngine.enableInEarMonitoring(true);
    mRtcEngine.setInEarMonitoringVolume(80);

    SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
    mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, 0));
    surfaceView.setZOrderOnTop(false);
    surfaceView.setZOrderMediaOverlay(false);
  
    //join the channel
    mRtcEngine.joinChannel(null, channelName, "Extra Optional Data", 0);}

If the user wants to leave the call from his friend’s room and go back to his own virtual room, he can click on the “X” button at the center bottom to achieve that. Let’s create a boolean called isLocalCall to track if the user is in his own virtual room or not. We need to set the channelName as the user’s userName so that he will join the channel with the same channel name as his username after calling startCalling method. In this way, he is back to his virtual room.

if (isLocalCall) {
    //when the user is in his own room
    ...
    }
}else {
    //when user is joining other people's room
    //leave that room and come back to user's own room
    isLocalCall = true;
    finishCalling();
    channelName = userName;
    startCalling();
}

6. Lock the room

Sometimes, a user wants to set his own virtual room to become a “private” room so that no one else can join his room except those who are already in the room. So how should we achieve that?

Create a String called localState to track the state of the room. There are two states: “Open” or “Lock”. By default, a room state is “Open”. When the user clicks the “X” button at the center bottom while he is in his own virtual room (which mean isLocalCall == true), we need to set the room state to “Lock” to set the room state to “private”. If he clicks that again, we need to set the room state back to “Open” again. Also, we have to update the room state information in the database so that other users can know the current state for this room.

if (isLocalCall) {
    //when the user is in his own room
    if (localState.equals(Constant.USER_STATE_LOCK)) {
        //set the room to public
        localState = Constant.USER_STATE_OPEN;
        mRef.child(this.userName).setValue(new DBUser(this.userName, user.getAgoraUid(), localState, DBFriend));
        showToast("Room set to public");
    }else {
        //set the room to private so that no one can join the room
        localState = Constant.USER_STATE_LOCK;
        mRef.child(this.userName).setValue(new DBUser(this.userName, user.getAgoraUid(), localState, DBFriend));
        showToast("Room set to private");
    }
}else {
    //when user is joining other people's room
    //leave that room and come back to user's own room
    isLocalCall = true;
    finishCalling();
    channelName = userName;
    startCalling();
}

After that, every time before a user joins a friend’s channel, we need to check if that friend’s room is not private. Otherwise, we will display a toast on the screen.

joinFriendChildEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
        DBUser result = dataSnapshot.getValue(DBUser.class);
        if (result.getState().equals(Constant.USER_STATE_OPEN)) {
            joinFriend(DBFriend.get(position));
            mShowFriendLinearLayout.setVisibility(View.GONE);
        }else {
            showToast(DBFriend.get(position) + "'s room is locked. You can message him to say hi!");
        }

        mRef.orderByChild("name").startAt(DBFriend.get(position)).endAt(DBFriend.get(position) + "\uf8ff").removeEventListener(joinFriendChildEventListener);
    }

    ...
};
mRef.orderByChild("name").startAt(DBFriend.get(position)).endAt(DBFriend.get(position) + "\uf8ff").addChildEventListener(joinFriendChildEventListener);

At this point, users in our application are able to search and add friends, join a friend’s virtual room and lock his virtual room. Now let’s implement the chat messaging functionality for our application.

Chat Messaging

Implementing chat messaging functionality is also very simple using Agora Messaging SDK.

Start RTMClient

In order to enable chatting function in our application, we need to create a RtmClient using Agora Messaging SDK.

mRtmclient = RtmClient.createInstance(mContext, appID, new RtmClientListener() {
        @Override
        public void onConnectionStateChanged(int state, int reason){
            for (RtmClientListener listener : mListenerList) {
                listener.onConnectionStateChanged(state, reason);
            }
        }@Override
        public void onMessageReceived(RtmMessage rtmMessage, String peerId) { ... }
});

Here, we pass the same appID as we used when we initialize the Agora Video engine.

In the onConnectionStateChanged() callback, we are calling RtmClientListener. We will implement this later in the Message activity.

In the onCreate method in the VideoCallActivity, we need to login RtmClient.

mRtmClient.login(null, userName, new io.agora.rtm.ResultCallback<Void>() {
    @Override
    public void onSuccess(Void aVoid) {
        ...
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        ...
    }
});

We use the same userName as the one we used for video call.

Now, let’s create a “Chat” button next to the “Join” button in the user’s friend list so that when user clicks it, it will display a chat messaging panel like this:

The user can edit his message in the edit text and send the message to his friend by clicking the “Send” button. The messages between the user and his friend will be displayed in the recycler view.

First implement the onClick logic for the “Send” button.

public void onClickSend(View v) {
    String content = mMsgEditText.getText().toString();    RtmMessage message = mRtmClient.createMessage();
    message.setText(content);

    // step 2: send message to peer
    mRtmClient.sendMessageToPeer(mPeerId, message, mChatManager.getSendMessageOptions(), new ResultCallback() {
    @Override
    public void onSuccess(Void aVoid) {
        ...
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        ...
    }
    });
}

Then we need to add logic to receive the chat messages. To receive private chat messages, we need to register RtmClientListener. Remember that when we created the RtmClient instance, we were calling RtmClientListener in the callbacks. Now, let’s implement this. In the callback onMessageReceived(), we put the logic to show the message on the RecyclerView.

class MyRtmClientListener implements RtmClientListener {

    @Override
    public void onConnectionStateChanged(final int state, final int reason) {
        ...
    }

    @Override
    public void onMessageReceived(final RtmMessage message, final String peerId) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                String content = message.getText();
                if (peerId.equals(mPeerId)) {
                    MessageBean messageBean = new MessageBean(peerId, content,false);
                    messageBean.setBackground(getMessageColor(peerId));
                    mMessageBeanList.add(messageBean);
                    mMessageAdapter.notifyItemRangeChanged(mMessageBeanList.size(), 1);
                    mRecyclerView.scrollToPosition(mMessageBeanList.size() - 1);
                } else {
                    MessageUtil.addMessageBean(peerId, content);
                }
            }
        });
    }
    ...
}

Now we are able to let users chat within the app. But they only can start chatting by clicking a button in their friend list. Let’s make it a little fancier! We want to allow the user to start chatting with his friend by double clicking on this friend’s remote video when they are in a video call. How can we achieve that?

First, we need to add the double click listener for the VideoViewContainer. Then, when the double click is triggered, we can get the remote user’s Agora uid and find that user’s name in the database.

chatSearchChildEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String s) {
        DBUser result = dataSnapshot.getValue(DBUser.class);
        startMessaging(result.getName());

        mRef.orderByChild("uid").startAt(user.mUid).endAt(user.mUid + "\uf8ff").removeEventListener(chatSearchChildEventListener);

    }

    ...
};

mRef.orderByChild("uid").startAt(user.mUid).endAt(user.mUid + "\uf8ff").addChildEventListener(chatSearchChildEventListener);

After that, we can follow the previous steps to start the messaging functionality by passing the user name.

Switch Camera and Audio Mute

At this point, all the core functionalities in our application are achieved. Let’s implement the switch camera and audio mute buttons onClick logic.

Switch camera

This button is to switch from using your mobile’s front camera to rear camera or vise versa. To do that, simply call switchCamera() on the RtcEngine instance.

mRtcEngine.switchCamera();

Audio mute/unmute

Sometimes users want to mute their input voice during a video call. That’s also easy to implement by just calling muteLocalAudioStream() on the RtcEngine instance and passing whether the user is already muted in the parameter.

mRtcEngine.muteLocalAudioStream(isMuted);

Build and Test on Device

Now let’s run our application!

Go to Android Studio, make sure your Android device is plugged in, and click Run to build the application on your device. Remember to build the application on two devices to start the video call.

Done!

Congratulations! You just built yourself a social application similar to Houseparty! Thank you for following along. Here is the email address for any of the questions you might have: devrel@agora.io.

Add high-quality voice, video and streaming to any app with ease.