Build a Messaging App on Android with Kotlin - Part 2

Jayson DeLancey

Ever wanted to build something like WhatsApp? In this tutorial, we’ll build a functional clone of the very popular WhatsApp.  Building a messaging app used to be a difficult challenge but this tutorial will try to make it easier. You’ll build upon the chat functionality added in Part 1 with Stream’s Chat API and start using Voxeet’s SDK to add calling features.  Let’s jump back into it.

If you haven’t already completed it, review part 1 and the source code:

Clone the WhatsApp Starter Repo

Start by cloning the master branch of the WhatsApp Clone Github repo:

git clone git@github.com:GetStream/WhatsApp-Clone-Android.git

This branch starts where you left off in part 1.
If you build and load into the Pixel 3 / API Level 29 Emulator you should find the CHATS tab to look like this with implementations of a ChannelListFragment and ChannelFragment using Stream’s powerful text Chat APIs.

We’re going to add additional functionality for sending and receiving voice and video conversations using Voxeet, a division of Dolby and the end result should look like this:

We’ve added additional boilerplate code for part 2 that compiles and we’ll begin adding new functionality.  

If you get lost along the way, the **part2** branch has the final project.

git clone -b part2 git@github.com:GetStream/WhatsApp-Clone-Android.git

The new project files added in this part are highlighted below:

Note: If you updated and are running Android Studio 3.6 or later and see an error like this:

AAPT: error: resource attr/flow_horizontalSeparator (aka com.example.whatsappclone:attr/flow_horizontalSeparator) not found.
You may need to update your app/build.gradle to pick up a recent fix.  Previous versions of Android Studio should work as expected.
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3'

Updating Navigation

Open `res/navigation/nav_graph.xml` to make a few changes.

  1. Update ChannelFragment to add a new action that will load the CallView fragment with a gesture.
  2. Add a new fragment for nav_call.

 …
    <fragment
        android:id="@+id/nav_channel"
        android:name="com.example.whatsappclone.ui.channel.ChannelFragment"
        android:label="fragment_channel"
        tools:layout="@layout/fragment_channel" >
        <argument
            android:name="channel_type"
            app:argType="string" />
        <argument
            android:name="channel_id"
            app:argType="string" />
        <action
            android:id="@+id/nav_call2"
            app:destination="@id/nav_call"
            app:enterAnim="@anim/slide_up"
            app:exitAnim="@anim/slide_down" />
    </fragment>
    <fragment
        android:id="@+id/nav_call"
        android:name="com.example.whatsappclone.ui.call.CallView"
        android:label="fragment_call"
        tools:layout="@layout/fragment_call_view" >
        <argument
            android:name="channel_id"
            app:argType="string" />
        <argument
            android:name="is_video_call"
            app:argType="boolean" />
        <argument
            android:name="channel_type"
            app:argType="string"
            app:nullable="true"
            android:defaultValue="@null"/>
    </fragment>

To start a call we want to add an action that pops up the call view. Replace the function `onOptionsItemSelected` in `com.example.whatsappclone.ui.channel.ChannelFragment` with:


override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
    if (menuItem.itemId == android.R.id.home) {
        findNavController().navigateUp()
        return true
    } else if(menuItem.itemId == R.id.miPhone) {
        println("About to start audio call" );

        findNavController().navigate(
            ChannelFragmentDirections.navCall2(args.channelId, false, args.channelType)
        )
    } else if(menuItem.itemId == R.id.miVideo) {
        println("About to start video call" );

        findNavController().navigate(
            ChannelFragmentDirections.navCall2(args.channelId, true, args.channelType)
        )
    }
    return false
}

This will allow a user to open a voice or video conference room associated with a particular chat channel.

Next, we’ll get Voxeet integration in place.

Adding Calls

To support voice and video calls, we need a Communications Platform as a Service (CPaaS).  Voxeet, a division of Dolby, provides high quality audio and video conferencing and is a good solution for this project since there is an Android client SDK.  

Add the following dependency to your app/build.gradle configuration to pull in the SDK:

// Voxeet Android SDK -- https://www.voxeet.com/
implementation("com.voxeet.sdk:public-sdk:2.0.76") {
transitive = true
}

The `transitive` property will fetch dependencies of dependencies.  Typically this is true by default but it may depend on the version of Gradle you are using so is done here for completeness.

You can find more detailed documentation on initializing the Voxeet SDK from the documentation:
https://voxeet.com/documentation/sdk/reference/android/voxeetsdk

Voxeet Developer Account

You will also need to register as a developer with Voxeet to get credentials for using the service.  When you sign-up, you’ll receive free minutes to use for building a proof-of-concept or prototype. That’s plenty for this project.
https://www.voxeet.com/organizations/register

Once you have an account you will create an app from the dashboard.  You can call this app anything you like. Once you do, select the app to get to the Keys tab where you will find your consumerKey and consumerSecret.  These credentials are necessary to initialize the Voxeet SDK.

Create a VoxeetService to handle connections to Voxeet in the file `com/example/whatsappclone/VoxeetService.kt`.  We’ll define an `onCreate` function that will handle initialization details:

  • Initialize Voxeet SDK credentials we obtained in previous step
  • Prepare notification channel
  • Register to NotificationService
  • Register broadcast receiver
  • Start user session

The functions `NotificationCancelReceiver()` and `IncomingNotification()` are defined in `com/example/whatsappclone/ui/call/CallNotification.kt`.  We’ll come back to those later in the section on Sending and Receiving invitations.

Here’s the final source code listing for this function with all the pieces assembled:


package com.example.whatsappclone

import com.example.whatsappclone.incoming.IncomingNotification
import com.example.whatsappclone.incoming.NotificationCancelReceiver

import com.voxeet.promise.solve.ErrorPromise
import com.voxeet.promise.solve.PromiseExec
import com.voxeet.promise.solve.Solver
import com.voxeet.sdk.VoxeetSdk
import com.voxeet.sdk.json.ParticipantInfo
import com.voxeet.sdk.models.Conference
import com.voxeet.sdk.models.v1.CreateConferenceResult
import com.voxeet.sdk.push.center.NotificationCenterFactory
import com.voxeet.sdk.push.center.management.EnforcedNotificationMode
import com.voxeet.sdk.push.center.management.NotificationMode

class VoxeetService : Service() {

    
private val TAG = "VoxeetService"
private val mReceiver: NotificationCancelReceiver = NotificationCancelReceiver()

@Subscribe(threadMode = ThreadMode.MAIN)
override fun onCreate() {

    super.onCreate()
    Log.i(TAG, "Service onCreate")

    // Voxeet SDK initialization
    VoxeetSdk.initialize(
        APP_ID,
        APP_PASSWORD
    )
    // Register the current activity in the Voxeet SDK
    VoxeetSdk.instance()!!.register(this)

    // Register the channelId "VideoConference"
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val CHANNEL_ID = "VideoConference"
        val name: CharSequence = "VideoConference"
        val importance = NotificationManager.IMPORTANCE_HIGH
        val mChannel = NotificationChannel(CHANNEL_ID, name, importance)

        val notificationManager =
            this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(mChannel)
    }

    // Registering into the NotificationService
    NotificationCenterFactory.instance.register(
        NotificationMode.OVERHEAD_INCOMING_CALL,
        IncomingNotification()
    )
    // Setting the application notification mode
    VoxeetSdk.notification()!!.setEnforcedNotificationMode(EnforcedNotificationMode.OVERHEAD_INCOMING_CALL);

    // Register broadcast receiver
    val filter = IntentFilter();
    filter.addAction(BROADCAST_ACTION);
    this.registerReceiver(mReceiver,filter);

    // Start user session
    VoxeetSdk.session()!!.open(ParticipantInfo(NAME, USER_ID, IMAGE))
        .then { result: Boolean?, solver: Solver<Any?>? ->
            println("Voxeet session started")
            Toast.makeText(this, "Voxeet session started...", Toast.LENGTH_SHORT).show()
        }
        .error(error())
}

At the end of a voice or video call there is some cleanup to do.  You should close the session in the `onDestroy` method. Source code to do that:


/**
 * Close session
 */
override fun onDestroy() {
    Log.i(TAG, "Service onDestroy")

    // Unregister the current activity in the Voxeet SDK
    VoxeetSdk.instance()!!.unregister(this)
    // Close user session
    VoxeetSdk.session()!!.close()
        .then { result: Boolean?, solver: Solver<Any?>? ->
            Log.i(TAG,"Voxeet session stopped")
        }.error(error())
    this.unregisterReceiver(mReceiver);

    super.onDestroy()
}

Finally, add the consumer key and secret as constants in the companion object, including using the same username and avatar used in Part 1 for the Stream Chat.


// Voxeet params
const val NAME = "Xaranoid Android";
const val IMAGE = "https://bit.ly/2TIt8NR";
const val USER_ID = "empty-queen-5"
const val APP_ID = "(YOUR VOXEET CONSUMER KEY)"
const val APP_PASSWORD = "(YOUR VOXEET CONSUMER SECRET)"
const val BROADCAST_ACTION = "com.example.whatsappclone.cancelNotification";

Replace credentials for `APP_ID` and `APP_PASSWORD` with your own `consumerKey` and `consumerSecret` retrieved from the Voxeet dashboard.

Note: This is a demo application and that we’d recommend using consumer credentials from a server app that uses yet another method to authenticate its users (typically OAuth2.0 or an enterprise auth method as appropriate).

Media Permissions

We will need to ask the user for permission to access the microphone and camera.  To do that we’ll need to modify `com/example/whatsappclone/MainActivity.kt`.

The source code should look like this:


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val intent = Intent(getApplicationContext(), VoxeetService::class.java)
        startService(intent)

        // Here will be put the permission check
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.RECORD_AUDIO
            ) != PackageManager.PERMISSION_GRANTED
            ||
            ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.CAMERA
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf(
                    Manifest.permission.RECORD_AUDIO,
                    Manifest.permission.CAMERA
                ),
                0x20
            )
        }
        setContentView(R.layout.activity_main)
    }
}

When your application starts, it will ask the user for permission to record audio.  There are still a few more steps, but once complete it will look like this:

While establishing a connection to the Voxeet service, the app will briefly show a status message as the session starts as seen in the screenshot below:


Making a call

While viewing a chat interaction, we want to allow users to click on either the camera icon to start a video call or the phone icon to begin a voice call.

Open the `com.example.whatsappclone.ui.channel.ChannelFragment` where we can enable or disable the conference buttons depending on the conference session status.  The state will need to be maintained and respond to events appropriately.

Here’s the relevant source code for this:


package com.example.whatsappclone.ui.channel
...
import com.voxeet.sdk.VoxeetSdk
import com.voxeet.sdk.events.sdk.ConferenceStatusUpdatedEvent

class ChannelFragment : Fragment() {

    private val args: ChannelFragmentArgs by navArgs()
    lateinit var binding: FragmentChannelBinding
    private var channelMenu: Menu? = null

    ...

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onEvent(event: ConferenceStatusUpdatedEvent) {
        println("Voxeet ConferenceStatusUpdatedEvent: " + event.toString());
        updateViews();
    }

    fun updateViews()
    {
        val miPhone: MenuItem = channelMenu!!.findItem(R.id.miPhone)
        val miVideo: MenuItem = channelMenu!!.findItem(R.id.miVideo)
        println("Socket open: " + VoxeetSdk.session()!!.isSocketOpen().toString());
        miVideo.setEnabled(VoxeetSdk.session()!!.isSocketOpen())
        miPhone.setEnabled(VoxeetSdk.session()!!.isSocketOpen())
    }
}

Joining a conference

After the user joins the conference, we should display both the remote participant and local camera stream when available.  For this demo, we are assuming a one-on-one conversation with only two users in the call.

In joining a voice or video conference, we want to take the user to an in-call view.  This view may take a few moments to load so should show a loader while establishing the connection.

To get started with this view, open `res/layout/**fragment_call_view**.xml` and add two VideoView components, one for each participant.  We’ll make the local participant a picture-in-picture (pip) video smaller.

Here’s the source code:


<com.voxeet.sdk.views.VideoView
    android:id="@+id/remote_video"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/stream_black" />

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight=".4"
        android:background="#00000000" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight=".6"
        android:background="@color/stream_transparent"
        android:gravity="end|bottom">

        <com.voxeet.sdk.views.VideoView
            android:id="@+id/pip_video"
            android:layout_width="match_parent"
            android:layout_height="200dp" />

    </LinearLayout>
</LinearLayout>

Next, let’s update the code for joining and leaving a conference.  

We need to account for the first participant who will create the conference, and the second participant will join by invitation of the other chat member.

Every conference must have a name for tracking, so we can use the channel id as the conference alias.  This value is passed in as a view argument from the channel view. If the conference already exists, the app should just let the user join.

Here’s the source code for `com/example/whatsappclone/ui/call/CallView.kt`.  


fun join() {
    if(args.conferenceId==null){
        // Must create conference
        VoxeetSdk.conference()!!.create(args.channelId.toString())
            .then(PromiseExec { result: CreateConferenceResult?, solver: Solver<Conference?> ->
                solver.resolve(
                    VoxeetSdk.conference()!!.join(result!!.conferenceId)

                )
                // Invite other only for new conference
                if(result!!.isNew)
                    inviteParticipants(result!!.conferenceId);
            })
            .then { result: Conference?, solver: Solver<Any?>? ->
                Toast.makeText(activity!!.applicationContext, "Joined...", Toast.LENGTH_SHORT)
                    .show()
                updateViews()
                if (args.isVideoCall!!) {
                    startVideo();
                    updateViews()
                }
            }
            .error(error())
    } else {
        // Just join, we have conferenceId
        VoxeetSdk.conference()!!.join(args.conferenceId.toString())
            .then { result: Conference?, solver: Solver<Any?>? ->
                Toast.makeText(activity!!.applicationContext, "Joined...", Toast.LENGTH_SHORT)
                    .show()
                updateViews()
                if (args.isVideoCall!!) {
                    startVideo();
                    updateViews()
                }
            }
            .error(error())
    }
}

When a user leaves the conference, we should do some cleanup and disconnect.


fun leave() {
        VoxeetSdk.conference()!!.leave()
            .then { result: Boolean?, solver: Solver<Any?>? ->
                updateViews()

                getActivity()!!.onBackPressed();
            }
            .error(error())
    }

We added functions for starting/stopping video during the call without ending the conference or disconnecting.


fun startVideo() {
        return VoxeetSdk.conference()!!.startVideo()
            .then { result: Boolean?, solver: Solver<Any?>? -> updateViews() }
            .error(error())
    }

    fun stopVideo() {
        VoxeetSdk.conference()!!.stopVideo()
            .then { result: Boolean?, solver: Solver<Any?>? -> updateViews() }
            .error(error())
    }

When a user joins the conference, there is an exchange of information about added, updated, and removed streams.  The app subscribes to these events so that we can update state.

Add these to the CallView:


@Subscribe(threadMode = ThreadMode.MAIN)
    fun onEvent(event: StreamAddedEvent) {
        println("Voxeet StreamAddedEvent: " + event.toString());
        updateStreams();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onEvent(event: StreamUpdatedEvent) {
        println("Voxeet StreamUpdatedEvent: " + event.toString());
        updateStreams();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    fun onEvent(event: StreamRemovedEvent) {
        println("Voxeet StreamRemovedEvent: " + event.toString());
        updateStreams();
    }

    fun updateStreams() {
        val pip_view: VideoView = view!!.findViewById(R.id.pip_video) as VideoView
        val remote_view: VideoView = view!!.findViewById(R.id.remote_video) as VideoView

        for (user in VoxeetSdk.conference()!!.participants) {
            if(user.status==ConferenceParticipantStatus.RESERVED)
                continue
            val isLocal = user.id == VoxeetSdk.session()!!.participantId
            val stream = user.streamsHandler().getFirst(
                MediaStreamType.Camera
            )
            val video: VideoView = if (isLocal) pip_view else remote_view
            if (null != stream && !stream.videoTracks().isEmpty()) {
                val t_user_name = view!!.findViewById<View>(R.id.user_name) as TextView
                if (!isLocal) {
                    t_user_name.text = user.info!!.name.toString()
                }
                video.attach(user.id!!, stream)
                video.visibility = View.VISIBLE
            } else {
                video.visibility = View.INVISIBLE
            }
        }
    }

The `updateViews` function also handles the progress loader until the user has joined, and then hides it.  Here’s the source code for that:


  fun updateViews() {
        val progressBar = view!!.findViewById(R.id.conecting_progress) as ProgressBar
        if (VoxeetSdk.conference()!!.isLive)
            progressBar.visibility = View.GONE
        else
            progressBar.visibility = View.VISIBLE
    }

Putting that all together, we’re able to send and receive audio / video calls from a chat.

Development Tools

Without having a friend on stand-by it can be difficult to test during development.  The Voxeet developer portal has a Test tab in the dashboard for this purpose. The conference id “dev-portal” establishes a connection from your web browser to the android device you are testing.

You can find more details about this in that Test tab with links to documentation.

Note: In VoxeetService we defined the CHANNEL_ID as “VideoConference”.  You’ll need to change this to “dev-portal” to join the same conference.

Once you join you’ll be able to test your app by using both your computer and android emulator as two participants in a conference.

Invitations and answering a call

We’ve handled how to initiate a voice or video conference but what about the remote party.  They must be able to accept an invitation and answer a call.  

Only users with an open Voxeet session are able to receive invitations.  The user will receive a small notification of the incoming call with options to dismiss or join the conference using audio and video.  Selecting this option will open the CallView allowing the remote user to join the conference.

For more detailed descriptions from the Voxeet Advanced Invite docs.

Sending invitations

In the `com.example.whatsappclone.VoxeetService` we initialized all the services needed to support invitations.  We still need to send invitations to all members of the chat.

Open `com.example.whatsappclone.ui.call.CallView` and edit the `inviteParticipant` function.


fun inviteParticipants(conferenceId: String)
    {
        try {
            if (args.channelType == null)
                return

            val channelId = args.channelId;

            // Add new ParticipantInfo(....) for each channel Participant to invite
            val activity: AppCompatActivity = activity as AppCompatActivity
            val client = StreamChat.getInstance(activity.application)
            val channel = client.channel(args.channelType, args.channelId)
            val otherUsers: List<com.getstream.sdk.chat.rest.User> = channel.channelState.otherUsers
            val participants: List<ParticipantInfo> =
                otherUsers.map { user -> ParticipantInfo(user.name, user.id, user.image) }

            // ...then call and resolve the following promise
            val promise: Promise<MutableList<com.voxeet.sdk.models.Participant>> =
                VoxeetSdk.notification()!!.invite(conferenceId, participants)
            promise.execute()
        }

    }

Receiving invitations

When receiving a call, we need to present that in the interface.  The `IncomingInvitationListener` will receive an `InvitationBundle` to consume and propagate invitations.  It also handles canceled invitations.

Edit `com/example/whatsappclone/ui/call/CallNotification.kt` to make the following source code changes:


package com.example.whatsappclone.incoming


import com.voxeet.sdk.push.center.invitation.IIncomingInvitationListener
import com.voxeet.sdk.push.center.invitation.InvitationBundle
import com.voxeet.sdk.utils.AndroidManifest

class IncomingNotification : IIncomingInvitationListener {
    private val random: SecureRandom
    private var notificationId = -1

    override fun onInvitation(
        context: Context,
        invitationBundle: InvitationBundle
    ) {
        println("IncomingNotification onInvitation")
        notificationId = random.nextInt(Int.MAX_VALUE / 2)
        if (null != invitationBundle.conferenceId) {
            notificationId = invitationBundle.conferenceId.hashCode()
        }
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        val channelId = getChannelId(context)

        // Get intents for all actions
        val acceptAudio = CallActivity.createAudioIntent(notificationId, context, invitationBundle)
        val dismiss = NotificationCancelReceiver.createDismissIntent(notificationId, context)
        val acceptVideo = CallActivity.createVideoIntent(notificationId, context, invitationBundle)

        if (null == acceptVideo || null == acceptAudio) {
            println("onInvitation: accept intent is null !!")
            return
        }
        val inviterName =
            if (!TextUtils.isEmpty(invitationBundle.inviterName)) invitationBundle.inviterName else "(Unknown)"

        val lastNotification: Notification = NotificationCompat.Builder(context, channelId as String)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentTitle(
                "Incoming call from "+inviterName
            )
            .setContentText("Would you like to join to the conference?")
            .setSmallIcon(R.drawable.sym_action_call)
            .addAction(
                R.drawable.ic_menu_close_clear_cancel,
                "Dismiss",
                dismiss
            )
            .addAction(
                R.drawable.ic_menu_call,
                "Join audio",
                acceptAudio
            )
            .addAction(
                R.drawable.ic_menu_camera,
                "Join video",
                acceptVideo
            )
            .setAutoCancel(true)
            .setOngoing(true)
            .build()
        // Notify user
        notificationManager.notify(notificationId, lastNotification)
    }

   override fun onInvitationCanceled( context: Context, conferenceId: String ) {
        println("IncomingNotification onInvitationCanceled")
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (-1 != notificationId) notificationManager.cancel(notificationId)
        notificationId = 0
    }

    companion object {
        private const val SDK_CHANNEL_ID = "voxeet_sdk_channel_id"
        private const val DEFAULT_ID = "VideoConference"
        const val INCOMING_NOTIFICATION_REQUEST_CODE = 928
        const val EXTRA_NOTIFICATION_ID = "EXTRA_NOTIFICATION_ID"
        fun getChannelId(context: Context): String? {
            return AndroidManifest.readMetadata(
                context,
                SDK_CHANNEL_ID,
                DEFAULT_ID
            )
        }
    }

    init {
        random = SecureRandom()
    }
}

class NotificationCancelReceiver : BroadcastReceiver() {
    override fun onReceive(
        context: Context,
        intent: Intent
    ) {
        val notificationId = intent.getIntExtra(IncomingNotification.EXTRA_NOTIFICATION_ID, -1)
        if (notificationId != -1) {
            // Cancel your ongoing Notification
            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
            notificationManager!!.cancel(notificationId)
        }
    }

    companion object {
        fun createDismissIntent(notificationId: Int ,  context: Context): PendingIntent {
            val cancel = Intent("com.example.whatsappclone.cancelNotification")
            cancel.putExtra(IncomingNotification.EXTRA_NOTIFICATION_ID, notificationId);
            val dismissIntent =
                PendingIntent.getBroadcast(context, IncomingNotification.INCOMING_NOTIFICATION_REQUEST_CODE+2, cancel, PendingIntent.FLAG_CANCEL_CURRENT)
            return dismissIntent;
        }
    }
}

To join the conference, we must run `CallActivity` to load the CallView for the conference a user is invited to.


public fun createVideoIntent(
    notificationId: Int,
    context: Context,
    invitationBundle: InvitationBundle
): PendingIntent? {
    val extras = invitationBundle.asBundle()

    val notificationIntent = Intent(context, CallActivity::class.java)

    notificationIntent.putExtras(extras)
    notificationIntent.putExtra(IncomingNotification.EXTRA_NOTIFICATION_ID, notificationId);
    notificationIntent.putExtra("isVideoEnabled", true);

    val intent = PendingIntent.getActivity(
        context, INCOMING_NOTIFICATION_REQUEST_CODE,
        notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT
    )

    //w if invalid, returning null
    if (null == intent)
        return null

    return intent
}

The `onCreate` function in `com/example/whatsappclone/ui/call/CallActivity` will pass the conference id and type of connection to `CallView`.


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // Cancel your ongoing Notification
    val notificationId = intent.getIntExtra(IncomingNotification.EXTRA_NOTIFICATION_ID, -1)
    if (notificationId != -1) {
        val notificationManager =  this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
        notificationManager!!.cancel(notificationId)
    }

    setContentView(com.example.whatsappclone.R.layout.activity_call)

    // Register the current activity in the Voxeet SDK
    VoxeetSdk.instance()!!.register(this)

    val channelId = getChannelId(this@CallActivity)
    val extras = intent.extras
    val conferenceId : String?
    if (extras != null) {
        conferenceId = extras.getString("ConfId")
    }

    val isVideoEnabled = intent.getBooleanExtra("isVideoEnabled", true)

    println("CallActivity found conferenceId:" + conferenceId)
    if(conferenceId==null) {
        return;
    }

    // This will pass the parameter to the fragment
    val fragment: CallView = CallView.newInstance(conferenceId, isVideoEnabled)
    val fm: FragmentManager = supportFragmentManager
    if (savedInstanceState == null) {
        supportFragmentManager.beginTransaction()
            .replace(com.example.whatsappclone.R.id.call_fragment_container, fragment)
            .commitNow()
    }

}

Wrapping Up and Going to Production

In Part 1 you created a WhatsApp clone that supported chats.  To recap what we did here in Part 2, we’ve now added voice and video calls by:

– Adding a service to handle initialization of Voxeet SDK and start all the necessary services.

– Creating a new view with the layout for the call.

– Creating a new activity that handles receiving incoming calls.

With a fully featured client SDK it wasn’t difficult to build a fairly sophisticated and useful app.

Moving beyond this sample application to a production app requires a bit more work.  We tried to make the code understandable and easy to implement. Doing things like distributing credentials with the mobile app were included but is not an ideal security policy.  Hard coding values of IDs doesn’t allow for privacy, so need to generate one appropriate for each user.

The `VoxeetService.kt` illustrates this by including alternate values for NAME, IMAGE, and USER_ID that you would need to use.


companion object {

        const val NAME = "Paranoid Android";
        const val IMAGE = "https://bit.ly/2TIt8NR";
        const val USER_ID = "empty-queen-5"

       //  Switch to be a different user
       //  const val NAME = "Carmen Velasco";
       //  const val IMAGE = "https://randomuser.me/api/portraits/women/31.jpg";
       //  const val USER_ID = "cac036a2-a4ba-4ad9-a2e4-c51c8de29912"

        ...

    }

That’s it for now.  Give it a try and if you have any trouble, reach out and now we can try to setup a call and help you through any issues.

Power your app with Voxeet

The best API for live voice and video calling. Period.