VoipParticipantActionRequest.kt

/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.telecom.extensions.voip

import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.core.telecom.CallControlScope
import androidx.core.telecom.CallsManager
import androidx.core.telecom.extensions.IActionsResultCallback
import androidx.core.telecom.extensions.Participant
import androidx.core.telecom.internal.utils.CapabilityExchangeUtils
import androidx.core.telecom.util.ExperimentalAppActions

/**
 * This class helps handle callback action requests which will be queued up to a channel
 * so that order of the requests will be preserved.
 */
@ExperimentalAppActions
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)
@RequiresApi(Build.VERSION_CODES.O)
internal class VoipParticipantActionRequest(
    private val session: CallControlScope,
    private val action: @CallsManager.Companion.ExtensionSupportedActions Int,
    private val cb: IActionsResultCallback?,
    private val participantId: Int = CapabilityExchangeUtils.NULL_PARTICIPANT_ID
) {

    companion object {
        private val TAG = VoipParticipantActionRequest::class.simpleName
    }

    /**
     * Process the passed in action from the "queue".
     */
    internal fun processAction() {
        when (action) {
            CallsManager.RAISE_HAND_ACTION -> {
                processToggleHandRaised()
            }
            CallsManager.KICK_PARTICIPANT_ACTION -> {
                processKickParticipant()
            }
            else -> Log.i(TAG, "$action action is not supported (ignoring request).")
        }
    }

    /***********************************************************************************************
     *                           Private Helpers
     *********************************************************************************************/

    /**
     * Process toggle hand raised action. If the state is not defined on the VOIP side or the
     * participant cannot be found, a failure result will be propagated to the ICS.
     */
    private fun processToggleHandRaised() {
        // VOIP side hasn't defined (or doesn't support) a raised hands state for the participants.
        if (session.raisedHandParticipants == null) {
            cb?.onFailure(CapabilityExchangeUtils.VOIP_SERVER_ERROR,
                "Unexpected error on VOIP side. The associated state is not defined.")
            return
        }

        val participantToToggleRaiseHand = findParticipant()
        // Unable to find participant.
        if (participantToToggleRaiseHand == null) {
            cb?.onFailure(CapabilityExchangeUtils.PARTICIPANT_NOT_FOUND_ERROR,
                "Unable to raise hand for non-existent participant with id: $participantId")
            return
        }

        // Toggle raise hand state for the given participant.
        session.raisedHandParticipants!!.value = toggleRaiseHand(
            session.raisedHandParticipants!!.value.toMutableSet(),
            participantToToggleRaiseHand)
        cb?.onSuccess()
    }

    private fun toggleRaiseHand(
        raisedHandsState: MutableSet<Participant>,
        participant: Participant
    ): MutableSet<Participant> {
        if (raisedHandsState.contains(participant)) {
            raisedHandsState.remove(participant)
        } else {
            raisedHandsState.add(participant)
        }
        return raisedHandsState
    }

    /**
     * Process kick participant action. If the state is not defined on the VOIP side or the
     * participant cannot be found, a failure result will be propagated to the ICS.
     */
    private fun processKickParticipant() {
        // VOIP side hasn't defined (or doesn't support) a raised hands state for the participants.
        if (session.participants == null) {
            cb?.onFailure(CapabilityExchangeUtils.VOIP_SERVER_ERROR,
                "Unexpected error on VOIP side. Participants state is not defined.")
            return
        }

        val participantToKick = findParticipant()
        // Unable to find participant.
        if (participantToKick == null) {
            cb?.onFailure(CapabilityExchangeUtils.PARTICIPANT_NOT_FOUND_ERROR,
                "Unable to raise hand for non-existent participant with id: $participantId")
            return
        }

        // Kick the given participant.
        val currentParticipantsState = session.participants!!.value.toMutableSet()
        // This will always be true.
        currentParticipantsState.remove(participantToKick)
        session.participants!!.value = currentParticipantsState
        cb?.onSuccess()
    }

    /**
     * Find the participant in the participants flow pointing to participantId, or null, if not
     * found.
     */
    private fun findParticipant(): Participant? {
        session.participants?.let {
            for (participant in it.value) {
                if (participant.id == participantId) {
                    return participant
                }
            }
        }
        return null
    }
}