Skip to content

Handle call state for app kill or crash

ciscoRankush edited this page Jul 8, 2024 · 2 revisions

In this article we will discuss about how to terminate the call in case of certain situations as when an android application crashes or when the android application is removed from recent tasks list.

If not handled properly, and there is an ongoing call in progress, then certain events like the app crashing or the app being removed from the recent tasks list, doesn't terminate the call and the developer has to handle such scenarios. We have explained below how this can be achieved:

1. When an android application crashes.

Using a global exception handler for your application, you can detect when there is a crash and perform any required cleanup. In our Kitchen Sink sample application, we have created a custom class named GlobalExceptionHandler which does the same. The GlobalExceptionHandler class extends the Thread.UncaughtExceptionHandler interface. This interface has a method uncaughtException(Thread t, Throwable e), which is triggered when the application throws any uncaught exception. You can set this exception handler by calling the Thread.setDefaultUncaughtExceptionHandler method of the Thread class.

The GlobalExceptionHandler will catch any uncaught exception and check if there is any ongoing call. If found, then the call will be terminated. This means that for 1-1 calls, the call will end for both parties and for meetings, the call will end from the user's side and the user will leave the meeting, but the meeting won't end.

In Kitchen Sink, we are setting the global listener when the call is connected and we unregister the listener when the call disconnects.

NOTE: Global exception handling is not in place when there is no active call.

Also currently the exception handling cannot capture crashes that happens in the native code, i.e. within the .so files.

Example

override fun onConnected(call: Call?) {
    // Sets an exception handler on the current thread object for any type of uncaught exception.
    Thread.setDefaultUncaughtExceptionHandler(GlobalExceptionHandler())
}

override fun onDisconnected(call: Call?, event: CallObserver.CallDisconnectedEvent?) {
    Thread.setDefaultUncaughtExceptionHandler(null)
}

Check the GlobalExceptionHandler class for the implementation. You can also set the exception handler before onConnected event, by putting it inside the CallObserver.onStartRinging event and inside the Call.onIncomingCall callbacks, as discussed in the next section.

2. When an android application is killed.

In order to detect when an android application is removed from the recent tasks list, we can use a foreground service. The most generic way to kill an application, is by pressing the home button, opening the recent tasks list and then swiping up the application which you intend to kill.

In cases when an ongoing call is present and the application is killed, then the call doesn't terminate and it needs to be manually ended from the developer side. Since in this case, the app will be removed from memory immediately, so trying to end the call inside the onDestroy method of the activity/fragment won't neccessarily mean that the call will end, since the end call is an asynchronous process. So we can make use of a foreground service in this case. You can capture app kill action inside the onTaskRemoved(rootIntent: Intent?) method of the service. Here you should check for any ongoing call and end it gracefully.

Start the foreground service when an incoming or outgoing call takes place. The best place to start the service in both the cases would be: a. For incoming call, start the service inside the Phone.onIncomingCall(call: Call?) listener, and b. For outgoing call, start the service inside the CallObserver.onStartRinging(call:Call? , ringerType: Call.RingerType) event.

NOTE: It is better to start the service when the call is in ringing state instead of the connected state, as it will make sure that the app is killed as soon as the call was dialled and was in the ringing state itself.

You can stop the service when the call is disconnected i.e. inside the CallObserver.onDisconnected(event: CallDisconnectedEvent?) event.

NOTE: You might need to maintain a list of call objects for the above purpose. You can checkout the CallObjectStorage class in the Kitchen Sink example repository to see how that can be done.

Once you have the active call object inside the onTaskRemoved method, you can perform the end call option by calling either one of the below mentioned APIs.

a. Call.reject(callback: CompletionHandler<Void>): Use this API when your call is in ringing state and the call direction is of type Call.Direction.Incoming

b. Call.hangup(callback: CompletionHandler<Void>): Use this API when the direction of the call is of type Call.Direction.Outgoing or when it is of type Call.Direction.Incoming but not in the ringing state. You can determine the state of the call by calling Call.getStatus(): CallStatus method.

In order to make sure that the call has ended, listen to the onDisconnected(event: CallDisconnectedEvent?) event. Inside this event you can stop the service by calling stopSelf() method of the service instance.

Example code snippets:

Start & Stop the foreground service.

    var serviceIntent: CallManagementService? = null
    webex.phone.setIncomingCallListener(object : Phone.IncomingCallListener {
        override fun onIncomingCall(call: Call?) {
            ...
            startCallMonitoringForegroundService()
        }
    })

    webex.phone.dial("person@example.com", MediaOption.audioVideo(local, remote), CompletionHandler {
        val call = it.data
        call?.setObserver(object : CallObserver {
            override fun onStartRinging(call: Call?, ringerType: Call.RingerType) {
                ...
                startCallMonitoringForegroundService()
            }

            override fun onDisconnected(event: CallDisconnectedEvent?) {
                // Perform cleanup  and stop the service when call is diconnected.
                serviceIntent?.let {
                    stopService(it)
                    serviceIntent = null
                }
            }
        })
    })

    private fun startCallMonitoringForegroundService() {
        serviceIntent = Intent(context, CallManagementService::class.java)
        startForegroundService(serviceIntent)
    }

The foreground service class

    class CallManagementService : Service() {
        override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
            // Start the foreground service
            startForeground(1, buildNotification())
            return START_STICKY
        }

        // This is called when the app is killed from recent tasks list.
        override fun onTaskRemoved(rootIntent: Intent?) {
            super.onTaskRemoved(rootIntent)
            val call = getOngoingCall()
            if (call != null) {
                endCall(call)
            } else {
                // Stop the service when no ongoing call is found.
                stopSelf()
            }
        }

        private fun getOngoingCall(): Call? {
            var call: Call? = null
            for (i in 0 until CallObjectStorage.size()) {
                val callObj = CallObjectStorage.getCallObjectFromIndex(i)
                val status = callObj?.getStatus()
                if (status == Call.CallStatus.CONNECTED || status == Call.CallStatus.RINGING
                    || status == Call.CallStatus.WAITING || status == Call.CallStatus.INITIATED) {
                    call = callObj
                    break
                }
            }
            return call
        }

        private fun endCall(call: Call) {
            call.setObserver(object : CallObserver {
                override fun onDisconnected(event: CallObserver.CallDisconnectedEvent?) {
                    // Call has been disconnected successfully. Stop the service now.
                    stopSelf()
                }
            })
            if (call.getDirection() == Call.Direction.INCOMING && call.getStatus() == Call.CallStatus.RINGING) {
                call.reject {
                    if (it.isSuccessful) {
                        // Call rejected successfully. Waiting for call disconnected event
                    } else {
                        // Call reject failed, reason = it.error?.errorMessage
                        stopSelf()
                    }
                }
            } else {
                call.hangup { result ->
                    if (result.isSuccessful) {
                        // Call hung up success. Waiting for call disconnected event
                    } else {
                        // Call hangup failed, reason = it.error?.errorMessage
                        stopSelf()
                    }
                }
            }
        }
    }
Clone this wiki locally