Skip to main content
Build a fully customized control panel for your call interface by hiding the default controls and implementing your own UI with call actions. This guide walks you through creating a custom control panel with essential call controls.

Overview

Custom control panels allow you to:
  • Match your app’s branding and design language
  • Simplify the interface by showing only relevant controls
  • Add custom functionality and workflows
  • Create unique user experiences
This guide demonstrates building a basic custom control panel with:
  • Mute/Unmute audio button
  • Pause/Resume video button
  • Switch camera button
  • End call button

Prerequisites


Step 1: Hide Default Controls

Configure your session settings to hide the default control panel:
val sessionSettings = CometChatCalls.SessionSettingsBuilder()
    .hideControlPanel(true)
    .build()
You can also hide individual buttons while keeping the control panel visible. See SessionSettingsBuilder for all options.

Step 2: Create Custom Layout

Create an XML layout for your custom controls:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Call View Container -->
    <FrameLayout
        android:id="@+id/callContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Custom Control Panel -->
    <LinearLayout
        android:id="@+id/customControlPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal"
        android:gravity="center"
        android:padding="16dp"
        android:background="#CC000000">

        <!-- Mute/Unmute Button -->
        <ImageButton
            android:id="@+id/btnToggleAudio"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:layout_margin="8dp"
            android:src="@drawable/ic_mic_on"
            android:background="@drawable/control_button_background"
            android:contentDescription="Toggle Audio" />

        <!-- Pause/Resume Video Button -->
        <ImageButton
            android:id="@+id/btnToggleVideo"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:layout_margin="8dp"
            android:src="@drawable/ic_video_on"
            android:background="@drawable/control_button_background"
            android:contentDescription="Toggle Video" />

        <!-- Switch Camera Button -->
        <ImageButton
            android:id="@+id/btnSwitchCamera"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:layout_margin="8dp"
            android:src="@drawable/ic_switch_camera"
            android:background="@drawable/control_button_background"
            android:contentDescription="Switch Camera" />

        <!-- End Call Button -->
        <ImageButton
            android:id="@+id/btnEndCall"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:layout_margin="8dp"
            android:src="@drawable/ic_call_end"
            android:background="@drawable/end_call_button_background"
            android:contentDescription="End Call" />

    </LinearLayout>
</RelativeLayout>
control_button_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#4D4D4D" />
</shape>
end_call_button_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#FF3B30" />
</shape>

Step 3: Implement Control Actions

Set up button click listeners and call the appropriate actions:
class CallActivity : AppCompatActivity() {
    
    private lateinit var callSession: CallSession
    private var isAudioMuted = false
    private var isVideoPaused = false
    
    private lateinit var btnToggleAudio: ImageButton
    private lateinit var btnToggleVideo: ImageButton
    private lateinit var btnSwitchCamera: ImageButton
    private lateinit var btnEndCall: ImageButton
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_call)
        
        callSession = CallSession.getInstance()
        
        btnToggleAudio = findViewById(R.id.btnToggleAudio)
        btnToggleVideo = findViewById(R.id.btnToggleVideo)
        btnSwitchCamera = findViewById(R.id.btnSwitchCamera)
        btnEndCall = findViewById(R.id.btnEndCall)
        
        setupControlListeners()
    }
    
    private fun setupControlListeners() {
        btnToggleAudio.setOnClickListener {
            if (isAudioMuted) {
                callSession.unMuteAudio()
            } else {
                callSession.muteAudio()
            }
        }
        
        btnToggleVideo.setOnClickListener {
            if (isVideoPaused) {
                callSession.resumeVideo()
            } else {
                callSession.pauseVideo()
            }
        }
        
        btnSwitchCamera.setOnClickListener {
            callSession.switchCamera()
        }
        
        btnEndCall.setOnClickListener {
            callSession.leaveSession()
            finish()
        }
    }
}

Step 4: Handle State Updates

Use MediaEventsListener to keep your UI synchronized with the actual call state. The listener is lifecycle-aware and automatically removed when the Activity is destroyed.
private fun setupMediaEventsListener() {
    callSession.addMediaEventsListener(this, object : MediaEventsListener() {
        override fun onAudioMuted() {
            runOnUiThread {
                isAudioMuted = true
                btnToggleAudio.setImageResource(R.drawable.ic_mic_off)
            }
        }

        override fun onAudioUnMuted() {
            runOnUiThread {
                isAudioMuted = false
                btnToggleAudio.setImageResource(R.drawable.ic_mic_on)
            }
        }

        override fun onVideoPaused() {
            runOnUiThread {
                isVideoPaused = true
                btnToggleVideo.setImageResource(R.drawable.ic_video_off)
            }
        }

        override fun onVideoResumed() {
            runOnUiThread {
                isVideoPaused = false
                btnToggleVideo.setImageResource(R.drawable.ic_video_on)
            }
        }
    })
}
Use SessionStatusListener to handle session end events:
private fun setupSessionStatusListener() {
    callSession.addSessionStatusListener(this, object : SessionStatusListener() {
        override fun onSessionLeft() {
            runOnUiThread { finish() }
        }

        override fun onConnectionClosed() {
            runOnUiThread { finish() }
        }
    })
}

Complete Example

Here’s the full implementation combining all steps:
class CallActivity : AppCompatActivity() {
    
    private lateinit var callSession: CallSession
    private var isAudioMuted = false
    private var isVideoPaused = false
    
    private lateinit var btnToggleAudio: ImageButton
    private lateinit var btnToggleVideo: ImageButton
    private lateinit var btnSwitchCamera: ImageButton
    private lateinit var btnEndCall: ImageButton
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_call)
        
        callSession = CallSession.getInstance()
        
        btnToggleAudio = findViewById(R.id.btnToggleAudio)
        btnToggleVideo = findViewById(R.id.btnToggleVideo)
        btnSwitchCamera = findViewById(R.id.btnSwitchCamera)
        btnEndCall = findViewById(R.id.btnEndCall)
        
        setupControlListeners()
        setupMediaEventsListener()
        setupSessionStatusListener()
        joinCall()
    }
    
    private fun joinCall() {
        val sessionSettings = CometChatCalls.SessionSettingsBuilder()
            .setDisplayName("John Doe")
            .setType(SessionType.VIDEO)
            .hideControlPanel(true)
            .build()

        val callContainer = findViewById<FrameLayout>(R.id.callContainer)

        CometChatCalls.joinSession(
            sessionId = "SESSION_ID",
            sessionSettings = sessionSettings,
            view = callContainer,
            context = this,
            listener = object : CometChatCalls.CallbackListener<Void>() {
                override fun onSuccess(p0: Void?) {
                    Log.d(TAG, "Joined call successfully")
                }

                override fun onError(exception: CometChatException) {
                    Log.e(TAG, "Failed to join: ${exception.message}")
                    finish()
                }
            }
        )
    }
    
    private fun setupControlListeners() {
        btnToggleAudio.setOnClickListener {
            if (isAudioMuted) callSession.unMuteAudio() 
            else callSession.muteAudio()
        }
        
        btnToggleVideo.setOnClickListener {
            if (isVideoPaused) callSession.resumeVideo() 
            else callSession.pauseVideo()
        }
        
        btnSwitchCamera.setOnClickListener {
            callSession.switchCamera()
        }
        
        btnEndCall.setOnClickListener {
            callSession.leaveSession()
            finish()
        }
    }
    
    private fun setupMediaEventsListener() {
        callSession.addMediaEventsListener(this, object : MediaEventsListener() {
            override fun onAudioMuted() {
                runOnUiThread {
                    isAudioMuted = true
                    btnToggleAudio.setImageResource(R.drawable.ic_mic_off)
                }
            }

            override fun onAudioUnMuted() {
                runOnUiThread {
                    isAudioMuted = false
                    btnToggleAudio.setImageResource(R.drawable.ic_mic_on)
                }
            }

            override fun onVideoPaused() {
                runOnUiThread {
                    isVideoPaused = true
                    btnToggleVideo.setImageResource(R.drawable.ic_video_off)
                }
            }

            override fun onVideoResumed() {
                runOnUiThread {
                    isVideoPaused = false
                    btnToggleVideo.setImageResource(R.drawable.ic_video_on)
                }
            }
        })
    }
    
    private fun setupSessionStatusListener() {
        callSession.addSessionStatusListener(this, object : SessionStatusListener() {
            override fun onSessionLeft() {
                runOnUiThread { finish() }
            }

            override fun onConnectionClosed() {
                runOnUiThread { finish() }
            }
        })
    }
    
    companion object {
        private const val TAG = "CallActivity"
    }
}