Skip to content
Snippets Groups Projects
CallPresenter.kt 28.6 KiB
Newer Older
 *  Copyright (C) 2004-2023 Savoir-faire Linux Inc.
Adrien Béraud's avatar
Adrien Béraud committed
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
Adrien Béraud's avatar
Adrien Béraud committed
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
Adrien Béraud's avatar
Adrien Béraud committed
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
Adrien Béraud's avatar
Adrien Béraud committed
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package net.jami.call

import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Observer
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.disposables.Disposable
import net.jami.daemon.JamiService
import net.jami.services.ConversationFacade
import net.jami.model.*
import net.jami.model.Call.CallStatus
import net.jami.model.Conference.ParticipantInfo
import net.jami.model.Uri.Companion.fromString
import net.jami.mvp.RootPresenter
import net.jami.services.*
import net.jami.services.HardwareService.AudioState
import net.jami.services.HardwareService.VideoEvent
import net.jami.utils.Log
import net.jami.utils.StringUtils.toNumber
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Named

class CallPresenter @Inject constructor(
    private val mAccountService: AccountService,
    private val mContactService: ContactService,
    private val mHardwareService: HardwareService,
    private val mCallService: CallService,
    private val mDeviceRuntimeService: DeviceRuntimeService,
Adrien Béraud's avatar
Adrien Béraud committed
    private val mConversationFacade: ConversationFacade,
    private val mNotificationService: NotificationService,
Adrien Béraud's avatar
Adrien Béraud committed
    @param:Named("UiScheduler") private val mUiScheduler: Scheduler
) : RootPresenter<CallView>() {
    private var mConference: Conference? = null
Maxime Callet's avatar
Maxime Callet committed
    var mOnGoingCall = false
        private set
    private var permissionChanged = false
    private var incomingIsFullIntent = true
    private var callInitialized = false
    private var currentSurfaceId: String? = null
    private var currentPluginSurfaceId: String? = null
    private var timeUpdateTask: Disposable? = null
    fun isSpeakerphoneOn(): Boolean = mHardwareService.isSpeakerphoneOn()
Maxime Callet's avatar
Maxime Callet committed
    var isMicrophoneMuted: Boolean = false
    var wantVideo = false

    fun isVideoActive(): Boolean = mConference?.hasActiveVideo() == true

    fun cameraPermissionChanged(isGranted: Boolean) {
        if (isGranted && mHardwareService.isVideoAvailable) {
            mHardwareService.initVideo()
                .onErrorComplete()
                .blockingAwait()
            permissionChanged = true
        }
    }

    fun audioPermissionChanged(isGranted: Boolean) {
        if (isGranted && mHardwareService.hasMicrophone()) {
            mCallService.restartAudioLayer()
        }
    }

    override fun bindView(view: CallView) {
        super.bindView(view)
        mCompositeDisposable.add(mHardwareService.getCameraEvents()
            .observeOn(mUiScheduler)
            .subscribe { event: VideoEvent -> onCameraEvent(event) })
        mCompositeDisposable.add(mHardwareService.audioState
            .observeOn(mUiScheduler)
            .subscribe { state: AudioState -> this.view?.updateAudioState(state) })
    fun initOutGoing(accountId: String, conversationUri: Uri?, contactUri: String?, hasVideo: Boolean) {
        Log.e(TAG, "initOutGoing")
        if (accountId.isEmpty() || contactUri == null) {
            Log.e(TAG, "initOutGoing: null account or contact")
            hangupCall()
            return
        }
        val pHasVideo = hasVideo && mHardwareService.hasCamera()
        val callObservable = mCallService
            .placeCallIfAllowed(accountId, conversationUri, fromString(toNumber(contactUri)!!), pHasVideo)
            .flatMapObservable { call: Call -> mCallService.getConfUpdates(call) }
            .share()
        mCompositeDisposable.add(callObservable
            .observeOn(mUiScheduler)
Maxime Callet's avatar
Maxime Callet committed
            .subscribe({ conference ->
                confUpdate(conference)
            }) { e: Throwable ->
                hangupCall()
                Log.e(TAG, "Error with initOutgoing: " + e.message, e)
            })
        showConference(callObservable)
    }

    /**
     * Returns to or starts an incoming call
     *
     * @param confId         the call id
     * @param actionViewOnly true if only returning to call or if using full screen intent
     */
    fun initIncomingCall(confId: String, actionViewOnly: Boolean) {
        // if the call is incoming through a full intent, this allows the incoming call to display
        incomingIsFullIntent = actionViewOnly
        val callObservable = mCallService.getConfUpdates(confId)
            .observeOn(mUiScheduler)

        // Handles the case where the call has been accepted, emits a single so as to only check for permissions and start the call once
        if (!actionViewOnly) {
            mCompositeDisposable.add(callObservable
                .firstOrError()
                .subscribe({ call: Conference ->
                    confUpdate(call)
                    callInitialized = true
                    view!!.prepareCall(true)
                }) { e: Throwable ->
                    hangupCall()
                    Log.e(TAG, "Error with initIncoming, preparing call flow :", e)
                })
        }

        // Handles retrieving call updates. Items emitted are only used if call is already in process or if user is returning to a call.
        mCompositeDisposable.add(callObservable
            .subscribe({ call: Conference ->
                if (callInitialized || actionViewOnly) {
                    confUpdate(call)
                }
                hangupCall()
                Log.e(TAG, "Error with initIncoming, action view flow: ", e)
            })
        showConference(callObservable)
    }

     * Show conference receive an Observable of conference.
     *
     * [1] create an observer used to pass updated data of type List<Conference.ParticipantInfo> to the view.
     * It's using the observable characteristics to use Rx operators: a switchmap and a combine latest.
     * CombineLatest takes 3 params: Observable<List<Conference.ParticipantInfo>>,  Subject<List<Conference.ParticipantInfo>>,  Observable<ContactViewModel>
     * then return an updated : List<Conference.ParticipantInfo>! which will be passed as data for the onSubscribe()
     * onSubscribe will use the participant info and update the view with the data
     *
     * [2] create an observer used to pass updated data of type List<ContactViewModel> to the view.
     * It's using the observable characteristics to use Rx operators: a switchMap and a switchMapSingle
     * SwitchMapSingle returns Single<List<ContactViewModel>>
     * onSubscribe will use List<ContactViewModel> and update the view with this data
     *
     * @param conference: conference whose value have been updated
     */
Maxime Callet's avatar
Maxime Callet committed
    private fun showConference(conference: Observable<Conference>){
        val conference = conference.distinctUntilChanged()
        mCompositeDisposable.add(conference
            .switchMap { obj: Conference ->
                Observable.combineLatest(obj.participantInfo, obj.pendingCalls,
                if (obj.isConference)
                    ContactViewModel.EMPTY_VM
                else
                    mContactService.observeContact(obj.accountId, obj.call!!.contact!!, false))
            { participants, pending, callContact ->
                val p = if (participants.isEmpty() && !obj.isConference)
                    listOf(ParticipantInfo(obj.call, callContact, mapOf(
                        "sinkId" to (obj.call?.daemonIdString ?: ""),
                        "active" to "true"
                    )))
                    participants
                if (p.isEmpty()) p else p + pending
            }}
            .observeOn(mUiScheduler)
            .subscribe({ info: List<ParticipantInfo> -> view?.updateConfInfo(info) })
            { e: Throwable -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
        mCompositeDisposable.add(conference
            .switchMap { obj: Conference -> obj.participantRecording
                .switchMapSingle { participants -> mContactService.getLoadedContact(obj.accountId, participants) } }
            .observeOn(mUiScheduler)
            .subscribe({ contacts -> view?.updateParticipantRecording(contacts) })
            { e: Throwable -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
Maxime Callet's avatar
Maxime Callet committed
    /**
     * Get all the call details in order to display each elements correctly
     * */
    fun prepareBottomSheetButtonsStatus() {
        val conference = mConference ?: return
Maxime Callet's avatar
Maxime Callet committed
        val canDial = mOnGoingCall
        val displayPluginsButton = view?.displayPluginsButton() == true
Maxime Callet's avatar
Maxime Callet committed
        val showPluginBtn = displayPluginsButton && mOnGoingCall
        val hasActiveCameraVideo = conference.hasActiveNonScreenShareVideo()
        val hasActiveScreenShare = conference.hasActiveScreenSharing()
        val hasMultipleCamera = mHardwareService.cameraCount() > 1 && mOnGoingCall && hasActiveCameraVideo
        val isConference = conference.isConference
        view?.updateBottomSheetButtonStatus(isConference, isSpeakerphoneOn(), conference.isAudioMuted, hasMultipleCamera, canDial, showPluginBtn, mOnGoingCall, hasActiveCameraVideo, hasActiveScreenShare)
        val conference = mConference ?: return
        if (conference.participants.isEmpty()) return
        val firstCall = conference.participants[0]
        val c = firstCall.conversation
        if (c is Conversation) {
            view?.goToConversation(c.accountId, c.uri)
        } else if (firstCall.contact != null) {
            view?.goToConversation(firstCall.account!!, firstCall.contact!!.conversationUri.blockingFirst())
        }
    }

    fun speakerClick(checked: Boolean) {
        val conference = mConference ?: return
        mHardwareService.toggleSpeakerphone(conference, checked)
Maxime Callet's avatar
Maxime Callet committed
    /**
     * Mute the local microphone
     * this function is used by the main panel control
     * */
    fun muteMicrophoneToggled(checked: Boolean) {
        val conference = mConference ?: return
        val callId = conference.call?.daemonIdString
        if(callId != null) {
            mCallService.setLocalMediaMuted(
                conference.accountId,
                callId,
                CallService.MEDIA_TYPE_AUDIO,
                checked
            )
        }
        mCallService.setLocalMediaMuted(
            conference.accountId,
            conference.id,
            CallService.MEDIA_TYPE_AUDIO,
            checked
        )
    }

    fun switchVideoInputClick() {
        val conference = mConference ?: return
        if(conference.hasActiveNonScreenShareVideo()) {
            val camId = mHardwareService.changeCamera() ?: return
            mCallService.replaceVideoMedia(conference, "camera://$camId", false)
        }
    }

    fun switchOnOffCamera() {
        val conference = mConference ?: return
        val camId = mHardwareService.changeCamera(true)
        mCallService.replaceVideoMedia(conference, "camera://$camId", conference.hasActiveNonScreenShareVideo())
    }

    fun configurationChanged(rotation: Int) {
        mHardwareService.setDeviceOrientation(rotation)
    }

    fun dialpadClick() {
        view?.displayDialPadKeyboard()
    }

    fun acceptCall(hasVideo: Boolean) {
        mConference?.let { mCallService.accept(it.accountId, it.id, hasVideo) }
        // Hang up the conference call if it exists.
        mConference?.let { conference ->
            if (!conference.isSimpleCall)
                mCallService.hangUpConference(conference.accountId, conference.id)
                mCallService.hangUp(conference.accountId, conference.id)
            // Hang up pending calls.
            for (participant in conference.pendingCalls.blockingFirst()) {
                val call = participant.call ?: continue
                mCallService.hangUp(call.account!!, call.daemonIdString!!)
            }
        finish()
    }

    fun refuseCall() {
        mConference?.let { mCallService.refuse(it.accountId, it.id) }
    fun videoSurfaceCreated(holder: Any) {
        val conference = mConference ?: return
        val newId = conference.id
        if (newId != currentSurfaceId) {
            currentSurfaceId?.let { id ->
                mHardwareService.removeVideoSurface(id)
            }
            currentSurfaceId = newId
        }
        mHardwareService.addVideoSurface(conference.id, holder)
    private fun videoSurfaceUpdateId(newId: String) {
        if (newId != currentSurfaceId) {
            currentSurfaceId?.let { oldId ->
                mHardwareService.updateVideoSurfaceId(oldId, newId)
            }
            currentSurfaceId = newId
        }
    }

    fun pluginSurfaceCreated(holder: Any) {
        val conference = mConference ?: return
        if (conference.hasActiveVideo()) {
            val mediaList = conference.getMediaList()!!
            for (m in mediaList) if (m.mediaType == Media.MediaType.MEDIA_TYPE_VIDEO) {
                newId = m.source!!
                if (newId != currentPluginSurfaceId) {
                    currentPluginSurfaceId?.let { id ->
                        mHardwareService.removeVideoSurface(id)
                    }
                    currentPluginSurfaceId = newId
                }
                mHardwareService.addVideoSurface(newId, holder)
                //view?.displayContactBubble(false)
    private fun pluginSurfaceUpdateId(newId: String) {
        if (newId != currentPluginSurfaceId) {
            currentPluginSurfaceId?.let { oldId ->
                mHardwareService.updateVideoSurfaceId(oldId, newId)
            }
            currentPluginSurfaceId = newId
        }
    }

    fun previewVideoSurfaceCreated(holder: Any) {
        mHardwareService.addPreviewVideoSurface(holder, mConference)
        //mHardwareService.startCapture(null);
    }

    fun videoSurfaceDestroyed() {
        currentSurfaceId?.let { id ->
            mHardwareService.removeVideoSurface(id)
            currentSurfaceId = null
        }
    }

    fun pluginSurfaceDestroyed() {
        currentPluginSurfaceId?.let { id ->
            mHardwareService.removeVideoSurface(id)
            currentPluginSurfaceId = null
        }
    }

    fun previewVideoSurfaceDestroyed() {
        mHardwareService.removePreviewVideoSurface()
        //mHardwareService.endCapture()
    }

    fun uiVisibilityChanged(displayed: Boolean) {
        Log.w(TAG, "uiVisibilityChanged $mOnGoingCall $displayed")
        view?.displayHangupButton(mOnGoingCall && displayed)
    }

    private fun finish() {
        timeUpdateTask?.let { task ->
            if (!task.isDisposed)
                task.dispose()
            timeUpdateTask = null
        }
        mConference = null
        val view = view
        view?.finish()
    }

    /**
     * This fonctions define some global var and the UI screen/elements to show based on the Call/Conference properties.
     * @example: it will update the bottomSheet elements based on the conference data.
     * */
    private fun confUpdate(call: Conference) {
        mConference = call
        val status = call.state
        if (status === CallStatus.HOLD) {
            if (call.isSimpleCall) mCallService.unhold(call.accountId, call.id) else JamiService.addMainParticipant(call.accountId, call.id)
        val hasVideo = call.hasVideo()
        val hasActiveCameraVideo = call.hasActiveNonScreenShareVideo()
        val view = view ?: return
        if (call.isOnGoing) {
            mOnGoingCall = true
Maxime Callet's avatar
Maxime Callet committed
            view.initNormalStateDisplay()
            if (hasVideo) {
                mHardwareService.setPreviewSettings()
                mHardwareService.updatePreviewVideoSurface(call)
                videoSurfaceUpdateId(call.id)
                pluginSurfaceUpdateId(call.pluginId)
                view.displayLocalVideo(hasActiveCameraVideo && mDeviceRuntimeService.hasVideoPermission())
                if (permissionChanged) {
                    val camId = mHardwareService.changeCamera(true)
                    mCallService.replaceVideoMedia(call, "camera://$camId", true)
                    permissionChanged = false
                }
            }
            /*if (mHardwareService.hasInput(call.id)) {
                view.displayPeerVideo(true)
            timeUpdateTask = mUiScheduler.schedulePeriodicallyDirect({ updateTime() }, 0, 1, TimeUnit.SECONDS)
        } else if (call.isRinging) {
            val scall = call.call!!
            view.handleCallWakelock(!hasVideo)
            if (scall.isIncoming) {
                if (mAccountService.getAccount(scall.account)?.isAutoanswerEnabled == true) {
Amirhossein Naghshzan's avatar
Amirhossein Naghshzan committed
                    Log.w(TAG, "Accept because of autoanswer")
                    mCallService.accept(scall.account!!, scall.daemonIdString!!, wantVideo)
                    // only display the incoming call screen if the notification is a full screen intent
                } else if (incomingIsFullIntent) {
                    view.initIncomingCallDisplay(hasVideo)
                }
            } else {
                mOnGoingCall = false
                view.updateCallStatus(scall.callStatus)
                view.initOutGoingCallDisplay()
            }
        } else {
            finish()
        }
    }

    fun maximizeParticipant(info: ParticipantInfo?) {
        val conference = mConference ?: return
        val contact = info?.contact
        val toMaximize = if (conference.maximizedParticipant == contact?.contact) null else info
        conference.maximizedParticipant = toMaximize?.contact?.contact
        if (toMaximize != null) {
            mCallService.setConfMaximizedParticipant(conference.accountId, conference.id, toMaximize.contact.contact.uri)
            mCallService.setConfGridLayout(conference.accountId, conference.id)
        }
    }

    private fun updateTime() {
        val conference = mConference ?: return
        val view = view ?: return
        if (conference.isOnGoing) {
            val start = conference.timestampStart
            if (start != Long.MAX_VALUE) {
                view.updateTime((System.currentTimeMillis() - start) / 1000)
            } else {
                view.updateTime(-1)
    /*private fun onVideoEvent(event: VideoEvent) {
        Log.w(TAG, "onVideoEvent  $event")
        val conference = mConference
        if (conference != null && conference.id == event.sinkId) {
                if (event.started) {
                    view.resetVideoSize(event.w, event.h)
                } else {
                    view.displayPeerVideo(true)
                }
                if (event.started) {

                } else {
                    view.displayPeerVideo(false)
                }
    }*/

    private fun onCameraEvent(event: VideoEvent) {
        Log.w(TAG, "onVideoEvent  $event")
        val view = view ?: return
        if (event.start) {
            view.displayLocalVideo(true)
        }
        if (event.started) {
            view.resetPreviewVideoSize(event.w, event.h, event.rot)
        }
    }

    fun positiveButtonClicked() {
        val conference = mConference ?: return
        if (conference.isRinging && conference.isIncoming) {
            acceptCall(true)
        } else {
            hangupCall()
        }
    }

    fun negativeButtonClicked() {
        val conference = mConference ?: return
        if (conference.isRinging && conference.isIncoming) {
            refuseCall()
        } else {
            hangupCall()
        }
    }

    fun toggleButtonClicked() {
        val conference = mConference ?: return
        if (!(conference.isRinging && conference.isIncoming)) {
            hangupCall()
        }
    }

    fun requestPipMode() {
        val conference = mConference ?: return
        if (conference.isOnGoing && conference.hasVideo()) {
            view?.enterPipMode(conference.accountId, conference.firstCall?.daemonIdString)
    fun toggleCallMediaHandler(id: String, toggle: Boolean) {
        val conference = mConference ?: return
        if (conference.isOnGoing && conference.hasVideo()) {
            view?.toggleCallMediaHandler(id, conference.id, toggle)
        }
    }

    fun sendDtmf(s: CharSequence) {
        mCallService.playDtmf(s.toString())
    }

    fun addConferenceParticipant(accountId: String, uri: Uri) {
        val conference = mConference ?: return
        mCompositeDisposable.add(mConversationFacade.startConversation(accountId, uri)
            .subscribe { conversation: Conversation ->
                val conf = conversation.currentCall
                if (conf == null) {
                    val pendingObserver: Observer<Call> = object : Observer<Call> {
                        private var call: ParticipantInfo? = null
                        override fun onSubscribe(d: Disposable) {}
                        override fun onNext(sipCall: Call) {
                            if (call == null) {
                                val contact = sipCall.contact ?: conversation.contact!!
                                call = ParticipantInfo(sipCall, ContactViewModel(contact, contact.profile.blockingFirst()), mapOf("sinkId" to (sipCall.daemonIdString ?: "")), pending = true)
                                    .apply { conference.addPending(this) }
                        private fun onFinally()  {
                            call?.let {
                                conference.removePending(it)
                        override fun onError(e: Throwable) {
                            onFinally()
                        }
                        override fun onComplete() {
                            onFinally()
                        }
                    }
                    val contactUri = if (uri.isSwarm) conversation.contact!!.uri else uri

                    // Place new call, join to conference when answered
                    val newCall = mCallService.placeCallObservable(accountId, null, contactUri, wantVideo)
                        .takeWhile{ c -> !c.callStatus.isOver }
                        .doOnEach(pendingObserver)
                        .filter(Call::isOnGoing)
                        .firstElement()
                        .delay(1, TimeUnit.SECONDS)
                        .doOnEvent { call: Call?, e: Throwable? ->
                            pendingObserver.onComplete()
                        }

                    mCompositeDisposable.add(newCall.subscribe { call: Call ->
                        val id = conference.id
                        if (conference.isConference) {
                            mCallService.addParticipant(call.account!!, call.daemonIdString!!, conference.accountId, id)
                            mCallService.joinParticipant(conference.accountId, id, call.account!!, call.daemonIdString!!).subscribe()
                } else if (conf !== conference) {
                    if (conference.isConference) {
                        if (conf.isConference)
                            mCallService.joinConference(conference.accountId, conference.id, conf.accountId, conf.id)
                            mCallService.addParticipant(conf.accountId, conf.id, conference.accountId, conference.id)
                    } else {
                        if (conf.isConference)
                            mCallService.addParticipant(conference.accountId, conference.id, conf.accountId, conf.id)
                            mCallService.joinParticipant(conference.accountId, conference.id, conf.accountId, conf.id).subscribe()
                    }
                }
            })
    }

    fun startAddParticipant() {
        view!!.startAddParticipant(mConference!!.id)
    }

    fun hangupParticipant(info: ParticipantInfo) {
        if (info.call != null)
            mCallService.hangUp(info.call.account!!, info.call.daemonIdString!!)
            mCallService.hangupParticipant(mConference!!.accountId, mConference!!.id, info.contact.contact.primaryNumber)
Maxime Callet's avatar
Maxime Callet committed
    /**
    * Mute a participant when in conference mode
     * this fonction is used by the recycler view for each participant
     * it allow user to mute when they are moderator
    * */
    fun muteParticipant(info: ParticipantInfo, mute: Boolean) {
        mCallService.muteParticipant(mConference!!.accountId, mConference!!.id, info.contact.contact.primaryNumber, mute)
    }

    fun openParticipantContact(info: ParticipantInfo) {
        val call = info.call ?: mConference?.firstCall ?: return
        view?.goToContact(call.account!!, info.contact.contact)
    fun raiseHand(state: Boolean){
        val call = mConference ?: return
        mCallService.raiseHand(call.accountId, call.id, mAccountService.getAccount(call.accountId)?.uri!!, state, getDeviceId())
    fun switchOnOffScreenShare() {
        val conference = mConference ?: return
        val camId = mHardwareService.changeCamera(true)
        if(conference.hasActiveScreenSharing())
            mCallService.replaceVideoMedia(conference, "camera://$camId", true)
    fun startScreenShare(resultCode: Int, data: Any): Boolean {
        val conference = mConference ?: return false
        mNotificationService.preparePendingScreenshare(conference) {
            val mediaProjection = view?.getMediaProjection(resultCode, data) ?: return@preparePendingScreenshare
            mHardwareService.setPendingScreenShareProjection(mediaProjection)
            mCallService.replaceVideoMedia(conference, "camera://desktop", false)
    }

    fun isMaximized(info: ParticipantInfo): Boolean {
        return mConference?.maximizedParticipant == info.contact.contact
    fun startPlugin(mediaHandlerId: String) {
        mHardwareService.startMediaHandler(mediaHandlerId)
        val conference = mConference ?: return
        val media = conference.getMediaList() ?: return
        val source = media.firstOrNull {
            it.mediaType == Media.MediaType.MEDIA_TYPE_VIDEO && it.source != "camera://desktop"
        }?.source ?: return
        mHardwareService.switchInput(
            conference.accountId,
            conference.id,
            source
        )
    }

    fun stopPlugin() {
        mHardwareService.stopMediaHandler()
        val conference = mConference ?: return
        val media = conference.getMediaList() ?: return
        val source = media.firstOrNull {
            it.mediaType == Media.MediaType.MEDIA_TYPE_VIDEO &&
                    it.source != "camera://desktop"
        }?.source ?: return
        mHardwareService.switchInput(
            conference.accountId,
            conference.id,
            source
        )
    fun getDeviceId(): String? {
        val conference = mConference ?: return null
        return mAccountService.getAccount(conference.accountId)?.deviceId
    }

    fun hangupCurrentCall() {
        mCallService.currentConferences().filter { it != mConference }.forEach { conf ->
            if (conf.isSimpleCall)
                mCallService.hangUp(conf.accountId, conf.id)
            else
                mCallService.hangUpConference(conf.accountId, conf.id)
        }
    }

    fun holdCurrentCall() {
        mCallService.currentConferences().filter { it != mConference }.forEach { conf ->
            mCallService.holdCallOrConference(conf)
        }
    }

    fun handleOption(option: String?) {
        if (option == ACCEPT_END) {
            hangupCurrentCall()
        } else if (option == ACCEPT_HOLD) {
            holdCurrentCall()
        }
    }

    companion object {
        val TAG = CallPresenter::class.simpleName!!
        /** Describes what to do if there is another active call when accepting this call  */
        const val KEY_ACCEPT_OPTION = "acceptOpt"
        /** Hold the other call when accepting */
        const val ACCEPT_HOLD = "hold"
        /** End the other call when accepting */
        const val ACCEPT_END = "end"