org.matrix.androidsdk.call.MXCallsManager.java Source code

Java tutorial

Introduction

Here is the source code for org.matrix.androidsdk.call.MXCallsManager.java

Source

/*
 * Copyright 2015 OpenMarket Ltd
 * Copyright 2017 Vector Creations Ltd
 * 
 * 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 org.matrix.androidsdk.call;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Base64;
import android.view.View;

import org.matrix.androidsdk.crypto.MXCryptoError;
import org.matrix.androidsdk.crypto.data.MXDeviceInfo;
import org.matrix.androidsdk.crypto.data.MXUsersDevicesMap;
import org.matrix.androidsdk.rest.callback.SimpleApiCallback;
import org.matrix.androidsdk.util.Log;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.data.Room;
import org.matrix.androidsdk.data.RoomState;
import org.matrix.androidsdk.listeners.MXEventListener;
import org.matrix.androidsdk.rest.callback.ApiCallback;
import org.matrix.androidsdk.rest.client.CallRestClient;
import org.matrix.androidsdk.rest.model.Event;
import org.matrix.androidsdk.rest.model.EventContent;
import org.matrix.androidsdk.rest.model.MatrixError;
import org.matrix.androidsdk.rest.model.RoomMember;
import org.matrix.androidsdk.util.JsonUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;

public class MXCallsManager {
    private static final String LOG_TAG = "MXCallsManager";

    public interface MXCallsManagerListener {
        /**
         * Called when there is an incoming call within the room.
         * @param call the incoming call
         * @param unknownDevices the unknown e2e devices list
         */
        void onIncomingCall(IMXCall call, MXUsersDevicesMap<MXDeviceInfo> unknownDevices);

        /**
         * Called when a called has been hung up
         * @param call the incoming call
         */
        void onCallHangUp(IMXCall call);

        /**
         * A voip conference started in a room.
         * @param roomId the room id
         */
        void onVoipConferenceStarted(String roomId);

        /**
         * A voip conference finished in a room.
         * @param roomId the room id
         */
        void onVoipConferenceFinished(String roomId);
    }

    /**
     * Defines the call classes.
     */
    public enum CallClass {
        CHROME_CLASS, JINGLE_CLASS, DEFAULT_CLASS
    }

    private MXSession mSession = null;
    private Context mContext = null;

    private CallRestClient mCallResClient = null;
    private JsonElement mTurnServer = null;
    private Timer mTurnServerTimer = null;
    private boolean mSuspendTurnServerRefresh = false;

    private CallClass mPreferredCallClass = CallClass.JINGLE_CLASS;

    // active calls
    private final HashMap<String, IMXCall> mCallsByCallId = new HashMap<>();

    // listeners
    private final ArrayList<MXCallsManagerListener> mListeners = new ArrayList<>();

    // incoming calls
    private final ArrayList<String> mxPendingIncomingCallId = new ArrayList<>();

    // UI handler
    private final Handler mUIThreadHandler;

    /**
     * Constructor
     * @param session the session
     * @param context the context
     */
    public MXCallsManager(MXSession session, Context context) {
        mSession = session;
        mContext = context;

        mUIThreadHandler = new Handler(Looper.getMainLooper());

        mCallResClient = mSession.getCallRestClient();

        mSession.getDataHandler().addListener(new MXEventListener() {
            @Override
            public void onLiveEvent(Event event, RoomState roomState) {
                if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_STATE_ROOM_MEMBER)) {
                    // Listen to the membership join/leave events to detect the conference user activity.
                    // This mechanism detects the presence of an established conf call
                    if (TextUtils.equals(event.sender, MXCallsManager.getConferenceUserId(event.roomId))) {
                        EventContent eventContent = JsonUtils.toEventContent(event.getContentAsJsonObject());

                        if (TextUtils.equals(eventContent.membership, RoomMember.MEMBERSHIP_LEAVE)) {
                            dispatchOnVoipConferenceFinished(event.roomId);
                        }
                        if (TextUtils.equals(eventContent.membership, RoomMember.MEMBERSHIP_JOIN)) {
                            dispatchOnVoipConferenceStarted(event.roomId);
                        }
                    }
                }
            }
        });

        refreshTurnServer();
    }

    /**
     * @return true if the call feature is supported
     */
    public boolean isSupported() {
        return MXChromeCall.isSupported() || MXJingleCall.isSupported(mContext);
    }

    /**
     * @return the list of supported classes
     */
    public Collection<CallClass> supportedClass() {
        ArrayList<CallClass> list = new ArrayList<>();

        if (MXChromeCall.isSupported()) {
            list.add(CallClass.CHROME_CLASS);
        }

        if (MXJingleCall.isSupported(mContext)) {
            list.add(CallClass.JINGLE_CLASS);
        }

        Log.d(LOG_TAG, "supportedClass " + list);

        return list;
    }

    /**
     * @param callClass set the default callClass
     */
    public void setDefaultCallClass(CallClass callClass) {
        Log.d(LOG_TAG, "setDefaultCallClass " + callClass);

        boolean isUpdatable = false;

        if (callClass == CallClass.CHROME_CLASS) {
            isUpdatable = MXChromeCall.isSupported();
        }

        if (callClass == CallClass.JINGLE_CLASS) {
            isUpdatable = MXJingleCall.isSupported(mContext);
        }

        if (isUpdatable) {
            mPreferredCallClass = callClass;
        }
    }

    /**
     * create a new call
     * @param callId the call Id (null to use a default value)
     * @return the IMXCall
     */
    private IMXCall createCall(String callId) {
        Log.d(LOG_TAG, "createCall " + callId);

        IMXCall call = null;

        // default
        if (((CallClass.CHROME_CLASS == mPreferredCallClass) || (CallClass.DEFAULT_CLASS == mPreferredCallClass))
                && MXChromeCall.isSupported()) {
            call = new MXChromeCall(mSession, mContext, getTurnServer());
        }

        // Jingle
        if (null == call) {
            try {
                call = new MXJingleCall(mSession, mContext, getTurnServer());
            } catch (Exception e) {
                Log.e(LOG_TAG, "createCall " + e.getLocalizedMessage());
            }
        }

        // a valid callid is provided
        if (null != callId) {
            call.setCallId(callId);
        }

        return call;
    }

    /**
     * Search a call from its dedicated room id.
     * @param roomId the room id
     * @return the IMXCall if it exists
     */
    public IMXCall getCallWithRoomId(String roomId) {
        ArrayList<IMXCall> calls;

        synchronized (this) {
            calls = new ArrayList<>(mCallsByCallId.values());
        }

        for (IMXCall call : calls) {
            if (TextUtils.equals(roomId, call.getRoom().getRoomId())) {
                if (TextUtils.equals(call.getCallState(), IMXCall.CALL_STATE_ENDED)) {
                    Log.d(LOG_TAG, "## getCallWithRoomId() : the call " + call.getCallId() + " has been stopped");
                    synchronized (this) {
                        mCallsByCallId.remove(call.getCallId());
                    }
                } else {
                    return call;
                }
            }
        }

        return null;
    }

    /**
     * Returns the IMXCall from its callId.
     * @param callId the call Id
     * @return the IMXCall if it exists
     */
    public IMXCall getCallWithCallId(String callId) {
        return getCallWithCallId(callId, false);
    }

    /**
     * Returns the IMXCall from its callId.
     * @param callId the call Id
     * @param create create the IMXCall if it does not exist
     * @return the IMXCall if it exists
     */
    private IMXCall getCallWithCallId(String callId, boolean create) {
        IMXCall call = null;

        // check if the call exists
        if (null != callId) {
            synchronized (this) {
                call = mCallsByCallId.get(callId);
            }
        }

        // test if the call has been stopped
        if ((null != call) && TextUtils.equals(call.getCallState(), IMXCall.CALL_STATE_ENDED)) {
            Log.d(LOG_TAG, "## getCallWithCallId() : the call " + callId + " has been stopped");
            synchronized (this) {
                mCallsByCallId.remove(call.getCallId());
            }

            call = null;
        }

        // the call does not exist but request to create it
        if ((null == call) && create) {
            call = createCall(callId);
            synchronized (this) {
                mCallsByCallId.put(call.getCallId(), call);
            }
        }

        Log.d(LOG_TAG, "getCallWithCallId " + callId + " " + call);

        return call;
    }

    /**
     * Tell if a call is in progress
     * @return true if the call is in progress
     */
    public static boolean isCallInProgress(IMXCall call) {
        boolean res = false;

        if (null != call) {
            String callState = call.getCallState();
            res = TextUtils.equals(callState, IMXCall.CALL_STATE_CREATED)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_CREATING_CALL_VIEW)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_FLEDGLING)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_WAIT_CREATE_OFFER)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_INVITE_SENT)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_RINGING)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_CREATE_ANSWER)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_RINGING)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_CONNECTING)
                    || TextUtils.equals(callState, IMXCall.CALL_STATE_CONNECTED);
        }

        return res;
    }

    /**
     * @return true if there are some active calls.
     */
    public boolean hasActiveCalls() {
        synchronized (this) {
            ArrayList<String> callIdsToRemove = new ArrayList<>();

            Set<String> callIds = mCallsByCallId.keySet();

            for (String callId : callIds) {
                IMXCall call = mCallsByCallId.get(callId);

                if (TextUtils.equals(call.getCallState(), IMXCall.CALL_STATE_ENDED)) {
                    Log.d(LOG_TAG, "# hasActiveCalls() : the call " + callId + " is not anymore valid");
                    callIdsToRemove.add(callId);
                } else {
                    Log.d(LOG_TAG, "# hasActiveCalls() : the call " + callId + " is active");
                    return true;
                }
            }

            for (String callIdToRemove : callIdsToRemove) {
                mCallsByCallId.remove(callIdToRemove);
            }
        }

        Log.d(LOG_TAG, "# hasActiveCalls() : no active call");
        return false;
    }

    /**
     * Manage the call events.
     * @param event the call event.
     */
    public void handleCallEvent(final Event event) {
        if (event.isCallEvent() && isSupported()) {
            Log.d(LOG_TAG, "handleCallEvent " + event.getType());

            // always run the call event in the UI thread
            // MXChromeCall does not work properly in other thread (because of the webview)
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    boolean isMyEvent = TextUtils.equals(event.getSender(), mSession.getMyUserId());
                    Room room = mSession.getDataHandler().getRoom(event.roomId);

                    String callId = null;
                    JsonObject eventContent = null;

                    try {
                        eventContent = event.getContentAsJsonObject();
                        callId = eventContent.getAsJsonPrimitive("call_id").getAsString();
                    } catch (Exception e) {
                        Log.e(LOG_TAG, "handleCallEvent : fail to retrieve call_id " + e.getMessage());
                    }
                    // sanity check
                    if ((null != callId) && (null != room)) {
                        // receive an invitation
                        if (Event.EVENT_TYPE_CALL_INVITE.equals(event.getType())) {
                            long lifeTime = event.getAge();

                            if (Long.MAX_VALUE == lifeTime) {
                                lifeTime = System.currentTimeMillis() - event.getOriginServerTs();
                            }

                            // ignore older call messages
                            if (lifeTime < 30000) {
                                // create the call only it is triggered from someone else
                                IMXCall call = getCallWithCallId(callId, !isMyEvent);

                                // sanity check
                                if (null != call) {
                                    // init the information
                                    if (null == call.getRoom()) {
                                        call.setRooms(room, room);
                                    }

                                    if (!isMyEvent) {
                                        call.prepareIncomingCall(eventContent, callId, null);
                                        call.setIsIncoming(true);
                                        mxPendingIncomingCallId.add(callId);
                                    } else {
                                        call.handleCallEvent(event);
                                    }
                                }
                            } else {
                                Log.d(LOG_TAG, "## handleCallEvent() : " + Event.EVENT_TYPE_CALL_INVITE
                                        + " is ignored because it is too old");
                            }
                        } else if (Event.EVENT_TYPE_CALL_CANDIDATES.equals(event.getType())) {
                            if (!isMyEvent) {
                                IMXCall call = getCallWithCallId(callId);

                                if (null != call) {
                                    if (null == call.getRoom()) {
                                        call.setRooms(room, room);
                                    }
                                    call.handleCallEvent(event);
                                }
                            }
                        } else if (Event.EVENT_TYPE_CALL_ANSWER.equals(event.getType())) {
                            IMXCall call = getCallWithCallId(callId);

                            if (null != call) {
                                // assume it is a catch up call.
                                // the creation / candidates /
                                // the call has been answered on another device
                                if (IMXCall.CALL_STATE_CREATED.equals(call.getCallState())) {
                                    call.onAnsweredElsewhere();
                                    synchronized (this) {
                                        mCallsByCallId.remove(callId);
                                    }
                                } else {
                                    if (null == call.getRoom()) {
                                        call.setRooms(room, room);
                                    }
                                    call.handleCallEvent(event);
                                }
                            }
                        } else if (Event.EVENT_TYPE_CALL_HANGUP.equals(event.getType())) {
                            final IMXCall call = getCallWithCallId(callId);
                            if (null != call) {
                                // trigger call events only if the call is active
                                final boolean isActiveCall = !IMXCall.CALL_STATE_CREATED
                                        .equals(call.getCallState());

                                if (null == call.getRoom()) {
                                    call.setRooms(room, room);
                                }

                                if (isActiveCall) {
                                    call.handleCallEvent(event);
                                }

                                synchronized (this) {
                                    mCallsByCallId.remove(callId);
                                }

                                // warn that a call has been hung up
                                mUIThreadHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        // must warn anyway any listener that the call has been killed
                                        // for example, when the device is in locked screen
                                        // the callview is not created but the device is ringing
                                        // if the other participant ends the call, the ring should stop
                                        dispatchOnCallHangUp(call);
                                    }
                                });
                            }
                        }
                    }
                }
            });
        }
    }

    /**
     * check if there is a pending incoming call
     */
    public void checkPendingIncomingCalls() {
        Log.d(LOG_TAG, "checkPendingIncomingCalls");

        mUIThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mxPendingIncomingCallId.size() > 0) {
                    for (String callId : mxPendingIncomingCallId) {
                        final IMXCall call = getCallWithCallId(callId);

                        if (null != call) {
                            final Room room = call.getRoom();

                            // for encrypted rooms with 2 members
                            // check if there are some unknown devices before warning
                            // of the incoming call.
                            // If there are some unknown devices, the answer event would not be encrypted.
                            if ((null != room) && room.isEncrypted() && mSession.getCrypto().warnOnUnknownDevices()
                                    && (room.getJoinedMembers().size() == 2)) {

                                // test if the encrypted events are sent only to the verified devices (any room)
                                mSession.getCrypto()
                                        .getGlobalBlacklistUnverifiedDevices(new SimpleApiCallback<Boolean>() {
                                            @Override
                                            public void onSuccess(Boolean sendToVerifiedDevicesOnly) {
                                                if (sendToVerifiedDevicesOnly) {
                                                    dispatchOnIncomingCall(call, null);
                                                } else {
                                                    //  test if the encrypted events are sent only to the verified devices (only this room)
                                                    mSession.getCrypto().isRoomBlacklistUnverifiedDevices(
                                                            room.getRoomId(), new SimpleApiCallback<Boolean>() {
                                                                @Override
                                                                public void onSuccess(
                                                                        Boolean sendToVerifiedDevicesOnly) {
                                                                    if (sendToVerifiedDevicesOnly) {
                                                                        dispatchOnIncomingCall(call, null);
                                                                    } else {
                                                                        List<RoomMember> members = new ArrayList<>(
                                                                                room.getJoinedMembers());
                                                                        String userId1 = members.get(0).getUserId();
                                                                        String userId2 = members.get(1).getUserId();

                                                                        Log.d(LOG_TAG,
                                                                                "## checkPendingIncomingCalls() : check the unknown devices");

                                                                        //
                                                                        mSession.getCrypto().checkUnknownDevices(
                                                                                Arrays.asList(userId1, userId2),
                                                                                new ApiCallback<Void>() {
                                                                                    @Override
                                                                                    public void onSuccess(
                                                                                            Void anything) {
                                                                                        Log.d(LOG_TAG,
                                                                                                "## checkPendingIncomingCalls() : no unknown device");
                                                                                        dispatchOnIncomingCall(call,
                                                                                                null);
                                                                                    }

                                                                                    @Override
                                                                                    public void onNetworkError(
                                                                                            Exception e) {
                                                                                        Log.e(LOG_TAG,
                                                                                                "## checkPendingIncomingCalls() : checkUnknownDevices failed "
                                                                                                        + e.getMessage());
                                                                                        dispatchOnIncomingCall(call,
                                                                                                null);
                                                                                    }

                                                                                    @Override
                                                                                    public void onMatrixError(
                                                                                            MatrixError e) {
                                                                                        MXUsersDevicesMap<MXDeviceInfo> unknownDevices = null;

                                                                                        if (e instanceof MXCryptoError) {
                                                                                            MXCryptoError cryptoError = (MXCryptoError) e;

                                                                                            if (MXCryptoError.UNKNOWN_DEVICES_CODE
                                                                                                    .equals(cryptoError.errcode)) {
                                                                                                unknownDevices = (MXUsersDevicesMap<MXDeviceInfo>) cryptoError.mExceptionData;
                                                                                            }
                                                                                        }

                                                                                        if (null != unknownDevices) {
                                                                                            Log.d(LOG_TAG,
                                                                                                    "## checkPendingIncomingCalls() : checkUnknownDevices found some unknown devices");
                                                                                        } else {
                                                                                            Log.e(LOG_TAG,
                                                                                                    "## checkPendingIncomingCalls() : checkUnknownDevices failed "
                                                                                                            + e.getMessage());
                                                                                        }

                                                                                        dispatchOnIncomingCall(call,
                                                                                                unknownDevices);
                                                                                    }

                                                                                    @Override
                                                                                    public void onUnexpectedError(
                                                                                            Exception e) {
                                                                                        Log.e(LOG_TAG,
                                                                                                "## checkPendingIncomingCalls() : checkUnknownDevices failed "
                                                                                                        + e.getMessage());
                                                                                        dispatchOnIncomingCall(call,
                                                                                                null);
                                                                                    }
                                                                                });
                                                                    }
                                                                }
                                                            });
                                                }
                                            }
                                        });
                            } else {
                                dispatchOnIncomingCall(call, null);
                            }
                        }
                    }
                }
                mxPendingIncomingCallId.clear();
            }
        });
    }

    /**
     * Create an IMXCall in the room defines by its room Id.
     * -> for a 1:1 call, it is a standard call.
     * -> for a conference call,
     * ----> the conference user is invited to the room (if it was not yet invited)
     * ----> the call signaling room is created (or retrieved) with the conference
     * ----> and the call is started
     *
     * @param roomId the room roomId
     * @param callback the async callback
     */
    public void createCallInRoom(final String roomId, final ApiCallback<IMXCall> callback) {
        Log.d(LOG_TAG, "createCallInRoom in " + roomId);

        final Room room = mSession.getDataHandler().getRoom(roomId);

        // sanity check
        if (null != room) {
            if (isSupported()) {
                int joinedMembers = room.getJoinedMembers().size();

                Log.d(LOG_TAG, "createCallInRoom : the room has " + joinedMembers + " joined members");

                if (joinedMembers > 1) {
                    if (joinedMembers == 2) {
                        // when a room is encrypted, test first there is no unknown device
                        // else the call will fail.
                        // So it seems safer to reject the call creation it it will fail.
                        if (room.isEncrypted() && mSession.getCrypto().warnOnUnknownDevices()) {
                            List<RoomMember> members = new ArrayList<>(room.getJoinedMembers());
                            String userId1 = members.get(0).getUserId();
                            String userId2 = members.get(1).getUserId();

                            // force the refresh to ensure that the devices list is up-to-date
                            mSession.getCrypto().checkUnknownDevices(Arrays.asList(userId1, userId2),
                                    new ApiCallback<Void>() {
                                        @Override
                                        public void onSuccess(Void anything) {
                                            final IMXCall call = getCallWithCallId(null, true);
                                            call.setRooms(room, room);

                                            if (null != callback) {
                                                mUIThreadHandler.post(new Runnable() {
                                                    @Override
                                                    public void run() {
                                                        callback.onSuccess(call);
                                                    }
                                                });
                                            }
                                        }

                                        @Override
                                        public void onNetworkError(Exception e) {
                                            if (null != callback) {
                                                callback.onNetworkError(e);
                                            }
                                        }

                                        @Override
                                        public void onMatrixError(MatrixError e) {
                                            if (null != callback) {
                                                callback.onMatrixError(e);
                                            }
                                        }

                                        @Override
                                        public void onUnexpectedError(Exception e) {
                                            if (null != callback) {
                                                callback.onUnexpectedError(e);
                                            }
                                        }
                                    });
                        } else {
                            final IMXCall call = getCallWithCallId(null, true);
                            call.setRooms(room, room);

                            if (null != callback) {
                                mUIThreadHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        callback.onSuccess(call);
                                    }
                                });
                            }
                        }
                    } else {
                        Log.d(LOG_TAG, "createCallInRoom : inviteConferenceUser");

                        inviteConferenceUser(room, new ApiCallback<Void>() {
                            @Override
                            public void onSuccess(Void info) {
                                Log.d(LOG_TAG, "createCallInRoom : inviteConferenceUser succeeds");

                                getConferenceUserRoom(room.getRoomId(), new ApiCallback<Room>() {
                                    @Override
                                    public void onSuccess(Room conferenceRoom) {

                                        Log.d(LOG_TAG, "createCallInRoom : getConferenceUserRoom succeeds");

                                        final IMXCall call = getCallWithCallId(null, true);
                                        call.setRooms(room, conferenceRoom);
                                        call.setIsConference(true);

                                        if (null != callback) {
                                            mUIThreadHandler.post(new Runnable() {
                                                @Override
                                                public void run() {
                                                    callback.onSuccess(call);
                                                }
                                            });
                                        }
                                    }

                                    @Override
                                    public void onNetworkError(Exception e) {
                                        Log.d(LOG_TAG, "createCallInRoom : getConferenceUserRoom failed "
                                                + e.getLocalizedMessage());

                                        if (null != callback) {
                                            callback.onNetworkError(e);
                                        }
                                    }

                                    @Override
                                    public void onMatrixError(MatrixError e) {
                                        Log.d(LOG_TAG, "createCallInRoom : getConferenceUserRoom failed "
                                                + e.getLocalizedMessage());

                                        if (null != callback) {
                                            callback.onMatrixError(e);
                                        }
                                    }

                                    @Override
                                    public void onUnexpectedError(Exception e) {
                                        Log.d(LOG_TAG, "createCallInRoom : getConferenceUserRoom failed "
                                                + e.getLocalizedMessage());

                                        if (null != callback) {
                                            callback.onUnexpectedError(e);
                                        }
                                    }
                                });
                            }

                            @Override
                            public void onNetworkError(Exception e) {
                                Log.d(LOG_TAG,
                                        "createCallInRoom : inviteConferenceUser fails " + e.getLocalizedMessage());

                                if (null != callback) {
                                    callback.onNetworkError(e);
                                }
                            }

                            @Override
                            public void onMatrixError(MatrixError e) {
                                Log.d(LOG_TAG,
                                        "createCallInRoom : inviteConferenceUser fails " + e.getLocalizedMessage());

                                if (null != callback) {
                                    callback.onMatrixError(e);
                                }
                            }

                            @Override
                            public void onUnexpectedError(Exception e) {
                                Log.d(LOG_TAG,
                                        "createCallInRoom : inviteConferenceUser fails " + e.getLocalizedMessage());

                                if (null != callback) {
                                    callback.onUnexpectedError(e);
                                }
                            }
                        });
                    }
                } else {
                    if (null != callback) {
                        callback.onMatrixError(new MatrixError(MatrixError.NOT_SUPPORTED, "too few users"));
                    }
                }
            } else {
                if (null != callback) {
                    callback.onMatrixError(new MatrixError(MatrixError.NOT_SUPPORTED, "VOIP is not supported"));
                }
            }
        } else {
            if (null != callback) {
                callback.onMatrixError(new MatrixError(MatrixError.NOT_FOUND, "room not found"));
            }
        }
    }

    //==============================================================================================================
    // Turn servers management
    //==============================================================================================================

    /**
     * Suspend the turn server  refresh
     */
    public void pauseTurnServerRefresh() {
        mSuspendTurnServerRefresh = true;
    }

    /**
     * Refresh the turn servers until it succeeds.
     */
    public void unpauseTurnServerRefresh() {
        Log.d(LOG_TAG, "unpauseTurnServerRefresh");

        mSuspendTurnServerRefresh = false;
        if (null != mTurnServerTimer) {
            mTurnServerTimer.cancel();
            mTurnServerTimer = null;
        }
        refreshTurnServer();
    }

    /**
     * Stop the turn servers refresh.
     */
    public void stopTurnServerRefresh() {
        Log.d(LOG_TAG, "stopTurnServerRefresh");

        mSuspendTurnServerRefresh = true;
        if (null != mTurnServerTimer) {
            mTurnServerTimer.cancel();
            mTurnServerTimer = null;
        }
    }

    /**
     * @return the turn server
     */
    private JsonElement getTurnServer() {
        JsonElement res;

        synchronized (LOG_TAG) {
            res = mTurnServer;
        }

        // privacy logs
        //Log.d(LOG_TAG, "getTurnServer " + res);
        Log.d(LOG_TAG, "getTurnServer ");

        return res;
    }

    /**
     * Refresh the turn servers.
     */
    private void refreshTurnServer() {
        if (mSuspendTurnServerRefresh) {
            return;
        }

        Log.d(LOG_TAG, "refreshTurnServer");

        mUIThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                mCallResClient.getTurnServer(new ApiCallback<JsonObject>() {
                    private void restartAfter(int msDelay) {
                        if (null != mTurnServerTimer) {
                            mTurnServerTimer.cancel();
                        }

                        mTurnServerTimer = new Timer();
                        mTurnServerTimer.schedule(new TimerTask() {
                            @Override
                            public void run() {
                                Log.d(LOG_TAG, "refreshTurnServer cancelled");
                                mTurnServerTimer.cancel();
                                mTurnServerTimer = null;

                                refreshTurnServer();
                            }
                        }, msDelay);
                    }

                    @Override
                    public void onSuccess(JsonObject info) {
                        // privacy
                        Log.d(LOG_TAG, "onSuccess ");
                        //Log.d(LOG_TAG, "onSuccess " + info);

                        if (null != info) {
                            if (info.has("uris")) {
                                synchronized (LOG_TAG) {
                                    mTurnServer = info;
                                }
                            }

                            if (info.has("ttl")) {
                                int ttl = 60000;

                                try {
                                    ttl = info.get("ttl").getAsInt();
                                    // restart a 90 % before ttl expires
                                    ttl = ttl * 9 / 10;
                                } catch (Exception e) {
                                    Log.e(LOG_TAG, "Fail to retrieve ttl " + e.getMessage());
                                }

                                restartAfter(ttl);
                            }
                        }
                    }

                    @Override
                    public void onNetworkError(Exception e) {
                        restartAfter(60000);
                    }

                    @Override
                    public void onMatrixError(MatrixError e) {
                        if (TextUtils.equals(e.errcode, MatrixError.LIMIT_EXCEEDED)) {
                            restartAfter(60000);
                        }
                    }

                    @Override
                    public void onUnexpectedError(Exception e) {
                        // should never happen
                    }
                });
            }
        });
    }

    //==============================================================================================================
    // Conference call
    //==============================================================================================================

    // Copied from vector-web:
    // FIXME: This currently forces Vector to try to hit the matrix.org AS for conferencing.
    // This is bad because it prevents people running their own ASes from being used.
    // This isn't permanent and will be customisable in the future: see the proposal
    // at docs/conferencing.md for more info.
    private static final String USER_PREFIX = "fs_";
    private static final String DOMAIN = "matrix.org";
    private static final HashMap<String, String> mConferenceUserIdByRoomId = new HashMap<>();

    /**
     * Return the id of the conference user dedicated for a room Id
     * @param roomId the room id
     * @return the conference user id
     */
    public static String getConferenceUserId(String roomId) {
        // sanity check
        if (null == roomId) {
            return null;
        }

        String conferenceUserId = mConferenceUserIdByRoomId.get(roomId);

        // it does not exist, compute it.
        if (null == conferenceUserId) {
            byte[] data = null;

            try {
                data = roomId.getBytes("UTF-8");
            } catch (Exception e) {
                Log.e(LOG_TAG, "conferenceUserIdForRoom failed " + e.getMessage());
            }

            if (null == data) {
                return null;
            }

            String base64 = Base64.encodeToString(data, Base64.NO_WRAP | Base64.URL_SAFE).replace("=", "");
            conferenceUserId = "@" + USER_PREFIX + base64 + ":" + DOMAIN;

            mConferenceUserIdByRoomId.put(roomId, conferenceUserId);
        }

        return conferenceUserId;
    }

    /**
     * Test if the provided user is a valid conference user Id
     * @param userId the user id to test
     * @return true if it is a valid conference user id
     */
    public static boolean isConferenceUserId(String userId) {
        // test first if it a known conference user id
        if (mConferenceUserIdByRoomId.values().contains(userId)) {
            return true;
        }

        boolean res = false;

        String prefix = "@" + USER_PREFIX;
        String suffix = ":" + DOMAIN;

        if (!TextUtils.isEmpty(userId) && userId.startsWith(prefix) && userId.endsWith(suffix)) {
            String roomIdBase64 = userId.substring(prefix.length(), userId.length() - suffix.length());
            try {
                res = MXSession.isRoomId(
                        (new String(Base64.decode(roomIdBase64, Base64.NO_WRAP | Base64.URL_SAFE), "UTF-8")));
            } catch (Exception e) {
                Log.e(LOG_TAG, "isConferenceUserId : failed " + e.getMessage());
            }
        }

        return res;
    }

    /**
     * Invite the conference user to a room.
     * It is mandatory before starting a conference call.
     * @param room the room
     * @param callback the async callback
     */
    private void inviteConferenceUser(final Room room, final ApiCallback<Void> callback) {
        Log.d(LOG_TAG, "inviteConferenceUser " + room.getRoomId());

        String conferenceUserId = getConferenceUserId(room.getRoomId());
        RoomMember conferenceMember = room.getMember(conferenceUserId);

        if ((null != conferenceMember)
                && TextUtils.equals(conferenceMember.membership, RoomMember.MEMBERSHIP_JOIN)) {
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    callback.onSuccess(null);
                }
            });
        } else {
            room.invite(conferenceUserId, callback);
        }
    }

    /**
     * Get the room with the conference user dedicated for the passed room.
     * @param roomId the room id.
     * @param callback the async callback.
     */
    private void getConferenceUserRoom(final String roomId, final ApiCallback<Room> callback) {
        Log.d(LOG_TAG, "getConferenceUserRoom with room id " + roomId);

        String conferenceUserId = getConferenceUserId(roomId);

        Room conferenceRoom = null;
        Collection<Room> rooms = mSession.getDataHandler().getStore().getRooms();

        // Use an existing 1:1 with the conference user; else make one
        for (Room room : rooms) {
            if (room.isConferenceUserRoom() && (2 == room.getMembers().size())
                    && (null != room.getMember(conferenceUserId))) {
                conferenceRoom = room;
                break;
            }
        }

        if (null != conferenceRoom) {
            Log.d(LOG_TAG, "getConferenceUserRoom : the room already exists");

            final Room fConferenceRoom = conferenceRoom;
            mSession.getDataHandler().getStore().commit();

            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    callback.onSuccess(fConferenceRoom);
                }
            });
        } else {
            Log.d(LOG_TAG, "getConferenceUserRoom : create the room");

            HashMap<String, Object> params = new HashMap<>();
            params.put("preset", "private_chat");
            params.put("invite", Arrays.asList(conferenceUserId));

            mSession.createRoom(params, new ApiCallback<String>() {
                @Override
                public void onSuccess(String roomId) {
                    Log.d(LOG_TAG, "getConferenceUserRoom : the room creation succeeds");

                    Room room = mSession.getDataHandler().getRoom(roomId);

                    if (null != room) {
                        room.setIsConferenceUserRoom(true);
                        mSession.getDataHandler().getStore().commit();
                        callback.onSuccess(room);
                    }
                }

                @Override
                public void onNetworkError(Exception e) {
                    Log.d(LOG_TAG, "getConferenceUserRoom : failed " + e.getMessage());
                    callback.onNetworkError(e);
                }

                @Override
                public void onMatrixError(MatrixError e) {
                    Log.d(LOG_TAG, "getConferenceUserRoom : failed " + e.getLocalizedMessage());
                    callback.onMatrixError(e);
                }

                @Override
                public void onUnexpectedError(Exception e) {
                    Log.d(LOG_TAG, "getConferenceUserRoom : failed " + e.getLocalizedMessage());
                    callback.onUnexpectedError(e);
                }
            });
        }
    }

    //==============================================================================================================
    // listeners management
    //==============================================================================================================

    /**
     * Add a listener
     * @param listener the listener to add
     */
    public void addListener(MXCallsManagerListener listener) {
        if (null != listener) {
            synchronized (this) {
                if (mListeners.indexOf(listener) < 0) {
                    mListeners.add(listener);
                }
            }
        }
    }

    /**
     * Remove a listener
     * @param listener the listener to remove
     */
    public void removeListener(MXCallsManagerListener listener) {
        if (null != listener) {
            synchronized (this) {
                mListeners.remove(listener);
            }
        }
    }

    /**
     * @return a copy of the listeners
     */
    private List<MXCallsManagerListener> getListeners() {
        ArrayList<MXCallsManagerListener> listeners;

        synchronized (this) {
            listeners = new ArrayList<>(mListeners);
        }

        return listeners;
    }

    /**
     * dispatch the onIncomingCall event to the listeners
     * @param call the call
     * @param unknownDevices the unknown e2e devices list.
     */
    private void dispatchOnIncomingCall(IMXCall call, final MXUsersDevicesMap<MXDeviceInfo> unknownDevices) {
        Log.d(LOG_TAG, "dispatchOnIncomingCall " + call.getCallId());

        List<MXCallsManagerListener> listeners = getListeners();

        for (MXCallsManagerListener l : listeners) {
            try {
                l.onIncomingCall(call, unknownDevices);
            } catch (Exception e) {
                Log.e(LOG_TAG, "dispatchOnIncomingCall " + e.getMessage());
            }
        }
    }

    /**
     * dispatch the onCallHangUp event to the listeners
     * @param call the call
     */
    private void dispatchOnCallHangUp(IMXCall call) {
        Log.d(LOG_TAG, "dispatchOnCallHangUp");

        List<MXCallsManagerListener> listeners = getListeners();

        for (MXCallsManagerListener l : listeners) {
            try {
                l.onCallHangUp(call);
            } catch (Exception e) {
                Log.e(LOG_TAG, "dispatchOnCallHangUp " + e.getMessage());
            }
        }
    }

    /**
     * dispatch the onVoipConferenceStarted event to the listeners
     * @param roomId the room Id
     */
    private void dispatchOnVoipConferenceStarted(String roomId) {
        Log.d(LOG_TAG, "dispatchOnVoipConferenceStarted : " + roomId);

        List<MXCallsManagerListener> listeners = getListeners();

        for (MXCallsManagerListener l : listeners) {
            try {
                l.onVoipConferenceStarted(roomId);
            } catch (Exception e) {
                Log.e(LOG_TAG, "dispatchOnVoipConferenceStarted " + e.getMessage());
            }
        }
    }

    /**
     * dispatch the onVoipConferenceFinished event to the listeners
     * @param roomId the room Id
     */
    private void dispatchOnVoipConferenceFinished(String roomId) {
        Log.d(LOG_TAG, "onVoipConferenceFinished : " + roomId);

        List<MXCallsManagerListener> listeners = getListeners();

        for (MXCallsManagerListener l : listeners) {
            try {
                l.onVoipConferenceFinished(roomId);
            } catch (Exception e) {
                Log.e(LOG_TAG, "dispatchOnVoipConferenceFinished " + e.getMessage());
            }
        }

    }
}