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

Java tutorial

Introduction

Here is the source code for org.matrix.androidsdk.call.MXJingleCall.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.hardware.Camera;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.text.TextUtils;

import org.matrix.androidsdk.util.Log;

import android.view.Surface;
import android.view.View;
import android.view.WindowManager;

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

import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.rest.model.Event;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoCapturerAndroid;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoRendererGui;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

public class MXJingleCall extends MXCall {

    private static final String LOG_TAG = "MXJingleCall";

    private static final String VIDEO_TRACK_ID = "ARDAMSv0";
    private static final String AUDIO_TRACK_ID = "ARDAMSa0";

    private static final String MIN_VIDEO_WIDTH_CONSTRAINT = "minWidth";

    private static final int MIN_VIDEO_WIDTH = 640;
    private static final int CAMERA_TYPE_FRONT = 1;
    private static final int CAMERA_TYPE_REAR = 2;
    private static final int CAMERA_TYPE_UNDEFINED = -1;

    static private PeerConnectionFactory mPeerConnectionFactory = null;
    static private String mFrontCameraName = null;
    static private String mBackCameraName = null;
    static private VideoCapturer mVideoCapturer = null;

    private GLSurfaceView mCallView = null;

    private boolean mIsCameraSwitched;
    private boolean mIsVideoSourceStopped = false;
    private VideoSource mVideoSource = null;
    private VideoTrack mLocalVideoTrack = null;
    private AudioSource mAudioSource = null;
    private AudioTrack mLocalAudioTrack = null;
    private MediaStream mLocalMediaStream = null;

    private VideoTrack mRemoteVideoTrack = null;
    private PeerConnection mPeerConnection = null;

    // default value
    private String mCallState = CALL_STATE_CREATED;

    private boolean mUsingLargeLocalRenderer = true;
    private VideoRenderer mLargeRemoteRenderer = null;
    private VideoRenderer mSmallLocalRenderer = null;
    private static int mLocalRenderWidth = -1;
    private static int mLocalRenderHeight = -1;

    private VideoRenderer.Callbacks mLargeLocalRendererCallbacks = null;
    private VideoRenderer.Callbacks mSmallLocalRendererCallbacks;
    private VideoRenderer mLargeLocalRenderer = null;

    private static boolean mIsInitialized = false;
    // null -> not initialized
    // true / false for the supported status
    private static Boolean mIsSupported;

    // candidate management
    private boolean mIsIncomingPrepared = false;
    private JsonArray mPendingCandidates = new JsonArray();

    private JsonObject mCallInviteParams = null;
    private int mCameraInUse = CAMERA_TYPE_UNDEFINED;

    private boolean mIsAnswered = false;

    /**
     * @return true if this stack can perform calls.
     */
    public static boolean isSupported(Context context) {
        if (null == mIsSupported) {
            mIsSupported = Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH;

            // the call initialisation is not yet done
            if (mIsSupported) {
                initializeAndroidGlobals(context.getApplicationContext());
            }

            Log.d(LOG_TAG, "isSupported " + mIsSupported);
        }

        return mIsSupported;
    }

    /**
     * Class creator
     *
     * @param session    the session
     * @param context    the context
     * @param turnServer the turn server
     */
    public MXJingleCall(MXSession session, Context context, JsonElement turnServer) {
        if (!isSupported(context)) {
            throw new AssertionError("MXJingleCall : not supported with the current android version");
        }

        if (null == session) {
            throw new AssertionError("MXJingleCall : session cannot be null");
        }

        if (null == context) {
            throw new AssertionError("MXJingleCall : context cannot be null");
        }

        Log.d(LOG_TAG, "MXJingleCall constructor " + turnServer);

        mCallId = "c" + System.currentTimeMillis();
        mSession = session;
        mContext = context;
        mTurnServer = turnServer;
    }

    /**
     * Initialize the jingle globals
     */
    private static void initializeAndroidGlobals(Context context) {
        if (!mIsInitialized) {
            try {
                mIsInitialized = PeerConnectionFactory.initializeAndroidGlobals(context, true, // enable audio initializing
                        true, // enable video initializing
                        true, // enable hardware acceleration
                        VideoRendererGui.getEGLContext());

                PeerConnectionFactory.initializeFieldTrials(null);
                mIsSupported = true;
                Log.d(LOG_TAG, "## initializeAndroidGlobals(): mIsInitialized=" + mIsInitialized);
            } catch (UnsatisfiedLinkError e) {
                Log.e(LOG_TAG, "## initializeAndroidGlobals(): Exception Msg=" + e.getMessage());
                mIsInitialized = true;
                mIsSupported = false;
            } catch (Exception e) {
                Log.e(LOG_TAG, "## initializeAndroidGlobals(): Exception Msg=" + e.getMessage());
                mIsInitialized = true;
                mIsSupported = false;
            }
        }
    }

    /**
     * Create the callviews
     */
    @Override
    public void createCallView() {
        if ((null != mIsSupported) && mIsSupported) {
            Log.d(LOG_TAG, "MXJingleCall createCallView");

            dispatchOnStateDidChange(CALL_STATE_CREATING_CALL_VIEW);
            mUIThreadHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mCallView = new GLSurfaceView(mContext); // set the GLSurfaceView where it should render to
                    mCallView.setVisibility(View.GONE);

                    dispatchOnViewLoading(mCallView);

                    mUIThreadHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            dispatchOnStateDidChange(CALL_STATE_FLEDGLING);
                            dispatchOnViewReady();
                        }
                    });
                }
            }, 10);
        }
    }

    /**
     * The connection is terminated
     *
     * @param endCallReasonId the reason of the call ending
     */
    private void terminate(final int endCallReasonId) {
        Log.d(LOG_TAG, "## terminate(): reason= " + endCallReasonId);

        if (isCallEnded()) {
            return;
        }

        dispatchOnStateDidChange(CALL_STATE_ENDED);

        boolean isPeerConnectionFactoryAllowed = false;

        if (null != mPeerConnection) {
            mPeerConnection.dispose();
            mPeerConnection = null;
            // the call has been initialized so mPeerConnectionFactory can be released
            isPeerConnectionFactoryAllowed = true;
        }

        if (null != mVideoSource) {
            mVideoSource.dispose();
            mVideoSource = null;
        }

        if (null != mAudioSource) {
            mAudioSource.dispose();
            mAudioSource = null;
        }

        // mPeerConnectionFactory is static so it might be used by another call
        // so we test that the current has been created
        if (isPeerConnectionFactoryAllowed && (null != mPeerConnectionFactory)) {
            mPeerConnectionFactory.dispose();
            mPeerConnectionFactory = null;
        }

        if (null != mCallView) {
            final View fCallView = mCallView;

            fCallView.post(new Runnable() {
                @Override
                public void run() {
                    fCallView.setVisibility(View.GONE);
                }
            });

            mCallView = null;
        }

        mUIThreadHandler.post(new Runnable() {
            @Override
            public void run() {
                dispatchOnCallEnd(endCallReasonId);
            }
        });
    }

    /**
     * Send the invite event
     *
     * @param sessionDescription the session description.
     */
    private void sendInvite(final SessionDescription sessionDescription) {
        // check if the call has not been killed
        if (isCallEnded()) {
            Log.d(LOG_TAG, "MXJingleCall isCallEnded");
            return;
        }

        Log.d(LOG_TAG, "MXJingleCall sendInvite");

        // build the invitation event
        JsonObject inviteContent = new JsonObject();
        inviteContent.addProperty("version", 0);
        inviteContent.addProperty("call_id", mCallId);
        inviteContent.addProperty("lifetime", 60000);

        JsonObject offerContent = new JsonObject();
        offerContent.addProperty("sdp", sessionDescription.description);
        offerContent.addProperty("type", sessionDescription.type.canonicalForm());
        inviteContent.add("offer", offerContent);

        Event event = new Event(Event.EVENT_TYPE_CALL_INVITE, inviteContent, mSession.getCredentials().userId,
                mCallSignalingRoom.getRoomId());

        mPendingEvents.add(event);
        mCallTimeoutTimer = new Timer();
        mCallTimeoutTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    if (getCallState().equals(IMXCall.CALL_STATE_RINGING)
                            || getCallState().equals(IMXCall.CALL_STATE_INVITE_SENT)) {
                        Log.d(LOG_TAG, "sendInvite : CALL_ERROR_USER_NOT_RESPONDING");
                        dispatchOnCallError(CALL_ERROR_USER_NOT_RESPONDING);
                        hangup(null);
                    }

                    // cancel the timer
                    mCallTimeoutTimer.cancel();
                    mCallTimeoutTimer = null;
                } catch (Exception e) {
                    Log.e(LOG_TAG, "## sendInvite(): Exception Msg= " + e.getMessage());
                }
            }
        }, 60 * 1000);

        sendNextEvent();
    }

    /**
     * Send the answer event
     *
     * @param sessionDescription the session description
     */
    private void sendAnswer(final SessionDescription sessionDescription) {
        // check if the call has not been killed
        if (isCallEnded()) {
            Log.d(LOG_TAG, "sendAnswer isCallEnded");
            return;
        }

        Log.d(LOG_TAG, "sendAnswer");

        // build the invitation event
        JsonObject answerContent = new JsonObject();
        answerContent.addProperty("version", 0);
        answerContent.addProperty("call_id", mCallId);
        answerContent.addProperty("lifetime", 60000);

        JsonObject offerContent = new JsonObject();
        offerContent.addProperty("sdp", sessionDescription.description);
        offerContent.addProperty("type", sessionDescription.type.canonicalForm());
        answerContent.add("answer", offerContent);

        Event event = new Event(Event.EVENT_TYPE_CALL_ANSWER, answerContent, mSession.getCredentials().userId,
                mCallSignalingRoom.getRoomId());
        mPendingEvents.add(event);
        sendNextEvent();

        mIsAnswered = true;
    }

    @Override
    public void updateLocalVideoRendererPosition(VideoLayoutConfiguration aConfigurationToApply) {
        try {
            // compute the new layout
            if ((null != mSmallLocalRendererCallbacks) && (null != aConfigurationToApply)) {
                VideoRendererGui.update(mSmallLocalRendererCallbacks, aConfigurationToApply.mX,
                        aConfigurationToApply.mY, aConfigurationToApply.mWidth, aConfigurationToApply.mHeight,
                        VideoRendererGui.ScalingType.SCALE_ASPECT_FIT, true);
                Log.d(LOG_TAG,
                        "## updateLocalVideoRendererPosition(): X=" + aConfigurationToApply.mX + " Y="
                                + aConfigurationToApply.mY + " width=" + aConfigurationToApply.mWidth + " height"
                                + aConfigurationToApply.mHeight);
            } else {
                Log.w(LOG_TAG, "## updateLocalVideoRendererPosition(): Skipped due to invalid parameters");
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "## updateLocalVideoRendererPosition(): Exception Msg=" + e.getMessage());
            return;
        }

        if (null != mCallView) {
            mCallView.postInvalidate();
        } else {
            Log.w(LOG_TAG, "## updateLocalVideoRendererPosition(): Skipped due to mCallView = null");
        }
    }

    @Override
    public boolean isSwitchCameraSupported() {
        return (VideoCapturerAndroid.getDeviceCount() > 1);
    }

    @Override
    public boolean switchRearFrontCamera() {
        if ((null != mVideoCapturer) && (isSwitchCameraSupported())) {
            VideoCapturerAndroid videoCapturerAndroid = (VideoCapturerAndroid) mVideoCapturer;

            if (videoCapturerAndroid.switchCamera(null)) {
                // toggle the video capturer instance
                if (CAMERA_TYPE_FRONT == mCameraInUse) {
                    mCameraInUse = CAMERA_TYPE_REAR;
                } else {
                    mCameraInUse = CAMERA_TYPE_FRONT;
                }

                // compute camera switch new status
                mIsCameraSwitched = !mIsCameraSwitched;

                mUIThreadHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        listenPreviewUpdate();
                    }
                }, 500);

                return true;

            } else {
                Log.w(LOG_TAG, "## switchRearFrontCamera(): failed");
            }
        } else {
            Log.w(LOG_TAG, "## switchRearFrontCamera(): failure - invalid values");
        }
        return false;
    }

    @Override
    public void muteVideoRecording(boolean muteValue) {
        Log.d(LOG_TAG, "## muteVideoRecording(): muteValue=" + muteValue);

        if (!isCallEnded()) {
            if (null != mLocalVideoTrack) {
                mLocalVideoTrack.setEnabled(!muteValue);
            } else {
                Log.d(LOG_TAG, "## muteVideoRecording(): failure - invalid value");
            }
        } else {
            Log.d(LOG_TAG, "## muteVideoRecording(): the call is ended");
        }
    }

    @Override
    public boolean isVideoRecordingMuted() {
        boolean isMuted = false;

        if (!isCallEnded()) {
            if (null != mLocalVideoTrack) {
                isMuted = !mLocalVideoTrack.enabled();
            } else {
                Log.w(LOG_TAG, "## isVideoRecordingMuted(): failure - invalid value");
            }

            Log.d(LOG_TAG, "## isVideoRecordingMuted() = " + isMuted);
        } else {
            Log.d(LOG_TAG, "## isVideoRecordingMuted() : the call is ended");
        }

        return isMuted;
    }

    @Override
    public boolean isCameraSwitched() {
        return mIsCameraSwitched;
    }

    @Override
    public void addListener(MXCallListener callListener) {
        super.addListener(callListener);

        // warn about the preview update
        if ((-1 != mLocalRenderWidth) && (1 != mLocalRenderHeight)) {
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    dispatchOnPreviewSizeChanged(mLocalRenderWidth, mLocalRenderHeight);
                }
            });
        }
    }

    /**
     * create the local stream
     */
    private void createLocalStream() {
        Log.d(LOG_TAG, "## createLocalStream(): IN");

        // check there is at least one stream to start a call
        if ((null == mLocalVideoTrack) && (null == mLocalAudioTrack)) {
            Log.d(LOG_TAG, "## createLocalStream(): CALL_ERROR_CALL_INIT_FAILED");

            dispatchOnCallError(CALL_ERROR_CALL_INIT_FAILED);
            hangup("no_stream");
            terminate(IMXCall.END_CALL_REASON_UNDEFINED);
            return;
        }

        // create our local stream to add our audio and video tracks
        mLocalMediaStream = mPeerConnectionFactory.createLocalMediaStream("ARDAMS");
        // add video track to local stream
        if (null != mLocalVideoTrack) {
            mLocalMediaStream.addTrack(mLocalVideoTrack);
        }
        // add audio track to local stream
        if (null != mLocalAudioTrack) {
            mLocalMediaStream.addTrack(mLocalAudioTrack);
        }

        // build ICE servers list
        ArrayList<PeerConnection.IceServer> iceServers = new ArrayList<>();

        if (null != mTurnServer) {
            try {
                String username = null;
                String password = null;
                JsonObject object = mTurnServer.getAsJsonObject();

                if (object.has("username")) {
                    username = object.get("username").getAsString();
                }

                if (object.has("password")) {
                    password = object.get("password").getAsString();
                }

                JsonArray uris = object.get("uris").getAsJsonArray();

                for (int index = 0; index < uris.size(); index++) {
                    String url = uris.get(index).getAsString();

                    if ((null != username) && (null != password)) {
                        iceServers.add(new PeerConnection.IceServer(url, username, password));
                    } else {
                        iceServers.add(new PeerConnection.IceServer(url));
                    }
                }
            } catch (Exception e) {
                Log.e(LOG_TAG,
                        "## createLocalStream(): Exception in ICE servers list Msg=" + e.getLocalizedMessage());
            }
        }

        // define at least on server
        if (iceServers.size() == 0) {
            iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));
        }

        // define constraints
        MediaConstraints pcConstraints = new MediaConstraints();
        pcConstraints.optional.add(new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));

        // start connecting to the other peer by creating the peer connection
        mPeerConnection = mPeerConnectionFactory.createPeerConnection(iceServers, pcConstraints,
                new PeerConnection.Observer() {
                    @Override
                    public void onSignalingChange(PeerConnection.SignalingState signalingState) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onSignalingChange state=" + signalingState);
                    }

                    @Override
                    public void onIceConnectionChange(final PeerConnection.IceConnectionState iceConnectionState) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onIceConnectionChange " + iceConnectionState);
                        mUIThreadHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {
                                    if ((null != mLocalVideoTrack) && mUsingLargeLocalRenderer && isVideo()) {
                                        mLocalVideoTrack.setEnabled(false);
                                        VideoRendererGui.remove(mLargeLocalRendererCallbacks);
                                        mLocalVideoTrack.removeRenderer(mLargeLocalRenderer);

                                        // in conference call, there is no local preview,
                                        // the local attendee video is sent by the server among the others conference attendees.
                                        if (!isConference()) {
                                            // add local preview, only for 1:1 call
                                            mLocalVideoTrack.addRenderer(mSmallLocalRenderer);
                                        }

                                        listenPreviewUpdate();

                                        mLocalVideoTrack.setEnabled(true);
                                        mUsingLargeLocalRenderer = false;

                                        mCallView.post(new Runnable() {
                                            @Override
                                            public void run() {
                                                if (null != mCallView) {
                                                    mCallView.invalidate();
                                                }
                                            }
                                        });
                                    }

                                    dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTED);
                                }
                                // theses states are ignored
                                // only the matrix hangup event is managed
                                /*else if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) {
                                    // TODO warn the user ?
                                    hangup(null);
                                } else if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
                                    // TODO warn the user ?
                                    terminate();
                                }*/
                                else if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) {
                                    dispatchOnCallError(CALL_ERROR_ICE_FAILED);
                                    hangup("ice_failed");
                                }
                            }
                        });
                    }

                    @Override
                    public void onIceConnectionReceivingChange(boolean var1) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onIceConnectionReceivingChange " + var1);
                    }

                    @Override
                    public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onIceGatheringChange " + iceGatheringState);
                    }

                    @Override
                    public void onIceCandidate(final IceCandidate iceCandidate) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onIceCandidate " + iceCandidate);

                        mUIThreadHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                if (!isCallEnded()) {
                                    JsonObject content = new JsonObject();
                                    content.addProperty("version", 0);
                                    content.addProperty("call_id", mCallId);

                                    JsonArray candidates = new JsonArray();
                                    JsonObject cand = new JsonObject();
                                    cand.addProperty("sdpMLineIndex", iceCandidate.sdpMLineIndex);
                                    cand.addProperty("sdpMid", iceCandidate.sdpMid);
                                    cand.addProperty("candidate", iceCandidate.sdp);
                                    candidates.add(cand);
                                    content.add("candidates", candidates);

                                    boolean addIt = true;

                                    // merge candidates
                                    if (mPendingEvents.size() > 0) {
                                        try {
                                            Event lastEvent = mPendingEvents.get(mPendingEvents.size() - 1);

                                            if (TextUtils.equals(lastEvent.getType(),
                                                    Event.EVENT_TYPE_CALL_CANDIDATES)) {
                                                // return the content cast as a JsonObject
                                                // it is not a copy
                                                JsonObject lastContent = lastEvent.getContentAsJsonObject();

                                                JsonArray lastContentCandidates = lastContent.get("candidates")
                                                        .getAsJsonArray();
                                                JsonArray newContentCandidates = content.get("candidates")
                                                        .getAsJsonArray();

                                                Log.d(LOG_TAG,
                                                        "Merge candidates from " + lastContentCandidates.size()
                                                                + " to " + (lastContentCandidates.size()
                                                                        + newContentCandidates.size() + " items."));

                                                lastContentCandidates.addAll(newContentCandidates);

                                                // replace the candidates list
                                                lastContent.remove("candidates");
                                                lastContent.add("candidates", lastContentCandidates);

                                                // don't need to save anything, lastContent is a reference not a copy

                                                addIt = false;
                                            }
                                        } catch (Exception e) {
                                            Log.e(LOG_TAG,
                                                    "## createLocalStream(): createPeerConnection - onIceCandidate() Exception Msg="
                                                            + e.getMessage());
                                        }
                                    }

                                    if (addIt) {
                                        Event event = new Event(Event.EVENT_TYPE_CALL_CANDIDATES, content,
                                                mSession.getCredentials().userId, mCallSignalingRoom.getRoomId());

                                        mPendingEvents.add(event);
                                        sendNextEvent();
                                    }
                                }
                            }
                        });
                    }

                    @Override
                    public void onAddStream(final MediaStream mediaStream) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onAddStream " + mediaStream);

                        mUIThreadHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                if ((mediaStream.videoTracks.size() == 1) && !isCallEnded()) {
                                    mRemoteVideoTrack = mediaStream.videoTracks.get(0);
                                    mRemoteVideoTrack.setEnabled(true);
                                    mRemoteVideoTrack.addRenderer(mLargeRemoteRenderer);
                                }
                            }
                        });
                    }

                    @Override
                    public void onRemoveStream(final MediaStream mediaStream) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onRemoveStream " + mediaStream);

                        mUIThreadHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                if (null != mRemoteVideoTrack) {
                                    mRemoteVideoTrack.dispose();
                                    mRemoteVideoTrack = null;
                                    mediaStream.videoTracks.get(0).dispose();
                                }
                            }
                        });

                    }

                    @Override
                    public void onDataChannel(DataChannel dataChannel) {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onDataChannel " + dataChannel);
                    }

                    @Override
                    public void onRenegotiationNeeded() {
                        Log.d(LOG_TAG, "## mPeerConnection creation: onRenegotiationNeeded");
                    }
                });

        // send our local video and audio stream to make it seen by the other part
        mPeerConnection.addStream(mLocalMediaStream);

        MediaConstraints constraints = new MediaConstraints();
        constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
        constraints.mandatory
                .add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideo() ? "true" : "false"));

        // call createOffer only for outgoing calls
        if (!isIncoming()) {
            Log.d(LOG_TAG, "## createLocalStream(): !isIncoming() -> createOffer");

            mPeerConnection.createOffer(new SdpObserver() {
                @Override
                public void onCreateSuccess(SessionDescription sessionDescription) {
                    Log.d(LOG_TAG, "createOffer onCreateSuccess");

                    final SessionDescription sdp = new SessionDescription(sessionDescription.type,
                            sessionDescription.description);

                    mUIThreadHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            if (mPeerConnection != null) {
                                // must be done to before sending the invitation message
                                mPeerConnection.setLocalDescription(new SdpObserver() {
                                    @Override
                                    public void onCreateSuccess(SessionDescription sessionDescription) {
                                        Log.d(LOG_TAG, "setLocalDescription onCreateSuccess");
                                    }

                                    @Override
                                    public void onSetSuccess() {
                                        Log.d(LOG_TAG, "setLocalDescription onSetSuccess");
                                        sendInvite(sdp);
                                        dispatchOnStateDidChange(IMXCall.CALL_STATE_INVITE_SENT);
                                    }

                                    @Override
                                    public void onCreateFailure(String s) {
                                        Log.e(LOG_TAG, "setLocalDescription onCreateFailure " + s);
                                        dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                                        hangup(null);
                                    }

                                    @Override
                                    public void onSetFailure(String s) {
                                        Log.e(LOG_TAG, "setLocalDescription onSetFailure " + s);
                                        dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                                        hangup(null);
                                    }
                                }, sdp);
                            }
                        }
                    });
                }

                @Override
                public void onSetSuccess() {
                    Log.d(LOG_TAG, "createOffer onSetSuccess");
                }

                @Override
                public void onCreateFailure(String s) {
                    Log.d(LOG_TAG, "createOffer onCreateFailure " + s);
                    dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                }

                @Override
                public void onSetFailure(String s) {
                    Log.d(LOG_TAG, "createOffer onSetFailure " + s);
                    dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                }
            }, constraints);

            dispatchOnStateDidChange(IMXCall.CALL_STATE_WAIT_CREATE_OFFER);
        }
    }

    /**
     * @return true if the device has a camera device
     */
    private boolean hasCameraDevice() {
        int devicesNumber = 0;
        try {
            devicesNumber = VideoCapturerAndroid.getDeviceCount();
            mFrontCameraName = VideoCapturerAndroid.getNameOfFrontFacingDevice();
            mBackCameraName = VideoCapturerAndroid.getNameOfBackFacingDevice();
        } catch (Exception e) {
            Log.e(LOG_TAG, "hasCameraDevice " + e.getLocalizedMessage());
        }

        Log.d(LOG_TAG, "hasCameraDevice():  camera number= " + devicesNumber);
        Log.d(LOG_TAG,
                "hasCameraDevice():  frontCameraName=" + mFrontCameraName + " backCameraName=" + mBackCameraName);

        return (null != mFrontCameraName) || (null != mBackCameraName);
    }

    /**
     * Create the local video stack
     *
     * @return the video track
     */
    private VideoTrack createVideoTrack() { // permission crash
        Log.d(LOG_TAG, "createVideoTrack");

        // create the local renderer only if there is a camera on the device
        if (hasCameraDevice()) {

            try {
                if (null != mFrontCameraName) {
                    mVideoCapturer = VideoCapturerAndroid.create(mFrontCameraName);

                    if (null == mVideoCapturer) {
                        Log.e(LOG_TAG, "Cannot create Video Capturer from front camera");
                    } else {
                        mCameraInUse = CAMERA_TYPE_FRONT;
                    }
                }

                if ((null == mVideoCapturer) && (null != mBackCameraName)) {
                    mVideoCapturer = VideoCapturerAndroid.create(mBackCameraName);

                    if (null == mVideoCapturer) {
                        Log.e(LOG_TAG, "Cannot create Video Capturer from back camera");
                    } else {
                        mCameraInUse = CAMERA_TYPE_REAR;
                    }
                }
            } catch (Exception ex2) {
                // catch exception due to Android M permissions, when
                // a call is received and the permissions (camera and audio) were not yet granted
                Log.e(LOG_TAG, "createVideoTrack(): Exception Msg=" + ex2.getMessage());
            }

            if (null != mVideoCapturer) {
                Log.d(LOG_TAG, "createVideoTrack find a video capturer");

                try {
                    MediaConstraints videoConstraints = new MediaConstraints();

                    videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair(MIN_VIDEO_WIDTH_CONSTRAINT,
                            Integer.toString(MIN_VIDEO_WIDTH)));

                    mVideoSource = mPeerConnectionFactory.createVideoSource(mVideoCapturer, videoConstraints);
                    mLocalVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, mVideoSource);
                    mLocalVideoTrack.setEnabled(true);
                    mLocalVideoTrack.addRenderer(mLargeLocalRenderer);
                } catch (Exception e) {
                    Log.e(LOG_TAG, "createVideoSource fails with exception " + e.getLocalizedMessage());

                    mLocalVideoTrack = null;

                    if (null != mVideoSource) {
                        mVideoSource.dispose();
                        mVideoSource = null;
                    }
                }
            } else {
                Log.e(LOG_TAG, "## createVideoTrack(): Cannot create Video Capturer - no camera available");
            }
        }

        return mLocalVideoTrack;
    }

    /**
     * Create the local video stack
     *
     * @return the video track
     */
    private AudioTrack createAudioTrack() {
        Log.d(LOG_TAG, "createAudioTrack");

        MediaConstraints audioConstraints = new MediaConstraints();

        // add all existing audio filters to avoid having echos
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation2", "true"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"));

        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"));

        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl2", "true"));

        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"));

        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAudioMirroring", "false"));
        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));

        mAudioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
        mLocalAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, mAudioSource);

        return mLocalAudioTrack;
    }

    /**
     * Initialize the call UI
     *
     * @param callInviteParams    the invite params
     * @param aLocalVideoPosition position of the local video attendee
     */
    private void initCallUI(final JsonObject callInviteParams, VideoLayoutConfiguration aLocalVideoPosition) {
        Log.d(LOG_TAG, "## initCallUI(): IN");

        if (isCallEnded()) {
            Log.w(LOG_TAG, "## initCallUI(): skipped due to call is ended");
            return;
        }

        if (isVideo()) {
            Log.d(LOG_TAG, "## initCallUI(): building UI video call");

            try {
                // pass a runnable to be run once the surface view is ready
                VideoRendererGui.setView(mCallView, new Runnable() {
                    @Override
                    public void run() {
                        mUIThreadHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                if (null == mPeerConnectionFactory) {
                                    Log.d(LOG_TAG, "## initCallUI(): video call and no mPeerConnectionFactory");

                                    mPeerConnectionFactory = new PeerConnectionFactory();
                                    createVideoTrack();
                                    createAudioTrack();
                                    createLocalStream();

                                    if (null != callInviteParams) {
                                        dispatchOnStateDidChange(CALL_STATE_RINGING);
                                        setRemoteDescription(callInviteParams);
                                    }
                                }
                            }
                        });
                    }
                });
            } catch (Exception e) {
                // GA issue
                // it seems that setView triggers some exception like "setRenderer has already been called"
                Log.e(LOG_TAG, "## initCallUI(): VideoRendererGui.setView : Exception Msg =" + e.getMessage());
            }

            // create the renderers after the VideoRendererGui.setView
            try {
                Log.d(LOG_TAG, "## initCallUI() building UI");
                //  create the video displaying the remote view sent by the server
                if (isConference()) {
                    mLargeRemoteRenderer = VideoRendererGui.createGui(0, 0, 100, 100,
                            VideoRendererGui.ScalingType.SCALE_ASPECT_FIT, false);
                } else {
                    mLargeRemoteRenderer = VideoRendererGui.createGui(0, 0, 100, 100,
                            VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, false);
                }

                mLargeLocalRendererCallbacks = VideoRendererGui.create(0, 0, 100, 100,
                        VideoRendererGui.ScalingType.SCALE_ASPECT_FILL, true);
                mLargeLocalRenderer = new VideoRenderer(mLargeLocalRendererCallbacks);

                // create the video displaying the local user: horizontal center, just above the video buttons menu
                if (null != aLocalVideoPosition) {
                    mSmallLocalRendererCallbacks = VideoRendererGui.create(aLocalVideoPosition.mX,
                            aLocalVideoPosition.mY, aLocalVideoPosition.mWidth, aLocalVideoPosition.mHeight,
                            VideoRendererGui.ScalingType.SCALE_ASPECT_BALANCED, true);
                    Log.d(LOG_TAG, "## initCallUI(): " + aLocalVideoPosition);
                } else {
                    // default layout
                    mSmallLocalRendererCallbacks = VideoRendererGui.create(5, 5, 25, 25,
                            VideoRendererGui.ScalingType.SCALE_ASPECT_BALANCED, true);
                }
                mSmallLocalRenderer = new VideoRenderer(mSmallLocalRendererCallbacks);

            } catch (Exception e) {
                Log.e(LOG_TAG, "## initCallUI(): Exception Msg =" + e.getMessage());
            }

            mCallView.setVisibility(View.VISIBLE);

        } else {
            Log.d(LOG_TAG, "## initCallUI(): build audio call");

            // audio call
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (null == mPeerConnectionFactory) {
                        mPeerConnectionFactory = new PeerConnectionFactory();
                        createAudioTrack();
                        createLocalStream();

                        if (null != callInviteParams) {
                            dispatchOnStateDidChange(CALL_STATE_RINGING);
                            setRemoteDescription(callInviteParams);
                        }
                    }
                }
            });
        }
    }

    // actions (must be done after dispatchOnViewReady()

    /**
     * The activity is paused.
     */
    @Override
    public void onPause() {
        super.onPause();

        Log.d(LOG_TAG, "onPause");

        try {
            if (!isCallEnded()) {

                Log.d(LOG_TAG, "onPause with active call");

                if (null != mCallView) {
                    mCallView.onPause();
                }

                if (mVideoSource != null && !mIsVideoSourceStopped) {
                    mVideoSource.stop();
                    mIsVideoSourceStopped = true;
                }
            }
        } catch (Exception e) {
            // race condition
            Log.e(LOG_TAG, "onPause failed " + e.getLocalizedMessage());
        }
    }

    /**
     * The activity is resumed.
     */
    @Override
    public void onResume() {
        super.onResume();

        Log.d(LOG_TAG, "onResume");

        try {
            if (!isCallEnded()) {

                Log.d(LOG_TAG, "onResume with active call");

                if (null != mCallView) {
                    mCallView.onResume();
                }

                if (mVideoSource != null && mIsVideoSourceStopped) {
                    mVideoSource.restart();
                    mIsVideoSourceStopped = false;
                }

                mUIThreadHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        listenPreviewUpdate();
                    }
                }, 500);

            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "onResume failed " + e.getLocalizedMessage());
        }
    }

    /**
     * Start a call.
     */
    @Override
    public void placeCall(VideoLayoutConfiguration aLocalVideoPosition) {
        Log.d(LOG_TAG, "placeCall");

        dispatchOnStateDidChange(IMXCall.CALL_STATE_WAIT_LOCAL_MEDIA);
        initCallUI(null, aLocalVideoPosition);
    }

    /**
     * Set the remote description
     *
     * @param callInviteParams the invitation params
     */
    private void setRemoteDescription(final JsonObject callInviteParams) {
        Log.d(LOG_TAG, "setRemoteDescription " + callInviteParams);

        SessionDescription aDescription = null;
        // extract the description
        try {
            if (callInviteParams.has("offer")) {
                JsonObject answer = callInviteParams.getAsJsonObject("offer");
                String type = answer.get("type").getAsString();
                String sdp = answer.get("sdp").getAsString();

                if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(sdp)) {
                    aDescription = new SessionDescription(SessionDescription.Type.OFFER, sdp);
                }
            }

        } catch (Exception e) {
            Log.e(LOG_TAG, "## setRemoteDescription(): Exception Msg=" + e.getMessage());
        }

        mPeerConnection.setRemoteDescription(new SdpObserver() {
            @Override
            public void onCreateSuccess(SessionDescription sessionDescription) {
                Log.d(LOG_TAG, "setRemoteDescription onCreateSuccess");
            }

            @Override
            public void onSetSuccess() {
                Log.d(LOG_TAG, "setRemoteDescription onSetSuccess");
                mIsIncomingPrepared = true;
                mUIThreadHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        checkPendingCandidates();
                    }
                });
            }

            @Override
            public void onCreateFailure(String s) {
                Log.e(LOG_TAG, "setRemoteDescription onCreateFailure " + s);
                dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
            }

            @Override
            public void onSetFailure(String s) {
                Log.e(LOG_TAG, "setRemoteDescription onSetFailure " + s);
                dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
            }
        }, aDescription);
    }

    /**
     * Prepare a call reception.
     *
     * @param aCallInviteParams the invitation Event content
     * @param aCallId           the call ID
     */
    @Override
    public void prepareIncomingCall(final JsonObject aCallInviteParams, final String aCallId,
            final VideoLayoutConfiguration aLocalVideoPosition) {

        Log.d(LOG_TAG, "## prepareIncomingCall : call state " + getCallState());

        mCallId = aCallId;

        if (CALL_STATE_FLEDGLING.equals(getCallState())) {
            mIsIncoming = true;

            dispatchOnStateDidChange(CALL_STATE_WAIT_LOCAL_MEDIA);

            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    initCallUI(aCallInviteParams, aLocalVideoPosition);
                }
            });
        } else if (CALL_STATE_CREATED.equals(getCallState())) {
            mCallInviteParams = aCallInviteParams;

            // detect call type from the sdp
            try {
                JsonObject offer = mCallInviteParams.get("offer").getAsJsonObject();
                JsonElement sdp = offer.get("sdp");
                String sdpValue = sdp.getAsString();
                setIsVideo(sdpValue.contains("m=video"));
            } catch (Exception e) {
                Log.e(LOG_TAG, "## prepareIncomingCall(): Exception Msg=" + e.getMessage());
            }
        }
    }

    /**
     * The call has been detected as an incoming one.
     * The application launches the dedicated activity and expects to launch the incoming call.
     * The local video attendee is displayed in the screen according to the values given in aLocalVideoPosition.
     *
     * @param aLocalVideoPosition local video position
     */
    @Override
    public void launchIncomingCall(VideoLayoutConfiguration aLocalVideoPosition) {
        Log.d(LOG_TAG, "launchIncomingCall : call state " + getCallState());

        if (CALL_STATE_FLEDGLING.equals(getCallState())) {
            prepareIncomingCall(mCallInviteParams, mCallId, aLocalVideoPosition);
        }
    }

    /**
     * The callee accepts the call.
     *
     * @param event the event
     */
    private void onCallAnswer(final Event event) {
        Log.d(LOG_TAG, "onCallAnswer : call state " + getCallState());

        if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) {
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTING);
                    SessionDescription aDescription = null;

                    // extract the description
                    try {
                        JsonObject eventContent = event.getContentAsJsonObject();

                        if (eventContent.has("answer")) {
                            JsonObject answer = eventContent.getAsJsonObject("answer");
                            String type = answer.get("type").getAsString();
                            String sdp = answer.get("sdp").getAsString();

                            if (!TextUtils.isEmpty(type) && !TextUtils.isEmpty(sdp) && type.equals("answer")) {
                                aDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
                            }
                        }

                    } catch (Exception e) {
                        Log.d(LOG_TAG, "onCallAnswer : " + e.getLocalizedMessage());
                    }

                    mPeerConnection.setRemoteDescription(new SdpObserver() {
                        @Override
                        public void onCreateSuccess(SessionDescription sessionDescription) {
                            Log.d(LOG_TAG, "setRemoteDescription onCreateSuccess");
                        }

                        @Override
                        public void onSetSuccess() {
                            Log.d(LOG_TAG, "setRemoteDescription onSetSuccess");
                        }

                        @Override
                        public void onCreateFailure(String s) {
                            Log.e(LOG_TAG, "setRemoteDescription onCreateFailure " + s);
                            dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                        }

                        @Override
                        public void onSetFailure(String s) {
                            Log.e(LOG_TAG, "setRemoteDescription onSetFailure " + s);
                            dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                        }
                    }, aDescription);
                }
            });
        }
    }

    /**
     * The other call member hangs up the call.
     *
     * @param event          the event
     * @param hangUpReasonId hang up reason
     */
    private void onCallHangup(final Event event, final int hangUpReasonId) {
        Log.d(LOG_TAG, "## onCallHangup(): call state=" + getCallState());
        String state = getCallState();

        if (!CALL_STATE_CREATED.equals(state) && (null != mPeerConnection)) {
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    terminate(hangUpReasonId);
                }
            });
        } else if (CALL_STATE_WAIT_LOCAL_MEDIA.equals(state) && isVideo()) {
            // specific case fixing: a video call hung up by the calling side
            // when the callee is still displaying the InComingCallActivity dialog.
            // If terminate() was not called, the dialog was never dismissed.
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    terminate(hangUpReasonId);
                }
            });
        }
    }

    /**
     * A new Ice candidate is received
     *
     * @param candidates the channel candidates
     */
    private void onNewCandidates(final JsonArray candidates) {
        Log.d(LOG_TAG, "## onNewCandidates(): call state " + getCallState() + " with candidates " + candidates);

        if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) {
            ArrayList<IceCandidate> candidatesList = new ArrayList<>();

            // convert the JSON to IceCandidate
            for (int index = 0; index < candidates.size(); index++) {
                JsonObject item = candidates.get(index).getAsJsonObject();
                try {
                    String candidate = item.get("candidate").getAsString();
                    String sdpMid = item.get("sdpMid").getAsString();
                    int sdpLineIndex = item.get("sdpMLineIndex").getAsInt();

                    candidatesList.add(new IceCandidate(sdpMid, sdpLineIndex, candidate));
                } catch (Exception e) {
                    Log.e(LOG_TAG, "## onNewCandidates(): Exception Msg=" + e.getMessage());
                }
            }

            for (IceCandidate cand : candidatesList) {
                Log.d(LOG_TAG, "## onNewCandidates(): addIceCandidate " + cand);
                mPeerConnection.addIceCandidate(cand);
            }
        }
    }

    /**
     * Add ice candidates
     *
     * @param candidates ic candidates
     */
    private void addCandidates(JsonArray candidates) {
        if (mIsIncomingPrepared || !isIncoming()) {
            Log.d(LOG_TAG, "addCandidates : ready");
            onNewCandidates(candidates);
        } else {
            synchronized (LOG_TAG) {
                Log.d(LOG_TAG, "addCandidates : pending");
                mPendingCandidates.addAll(candidates);
            }
        }
    }

    /**
     * Some Ice candidates could have been received while creating the call view.
     * Check if some of them have been defined.
     */
    private void checkPendingCandidates() {
        Log.d(LOG_TAG, "checkPendingCandidates");

        synchronized (LOG_TAG) {
            onNewCandidates(mPendingCandidates);
            mPendingCandidates = new JsonArray();
        }
    }

    // events thread

    /**
     * Manage the call events.
     *
     * @param event the call event.
     */
    @Override
    public void handleCallEvent(Event event) {
        if (event.isCallEvent()) {
            String eventType = event.getType();

            Log.d(LOG_TAG, "handleCallEvent " + eventType);

            // event from other member
            if (!TextUtils.equals(event.getSender(), mSession.getMyUserId())) {
                if (Event.EVENT_TYPE_CALL_ANSWER.equals(eventType) && !mIsIncoming) {
                    onCallAnswer(event);
                } else if (Event.EVENT_TYPE_CALL_CANDIDATES.equals(eventType)) {
                    JsonObject eventContent = event.getContentAsJsonObject();

                    JsonArray candidates = eventContent.getAsJsonArray("candidates");
                    addCandidates(candidates);
                } else if (Event.EVENT_TYPE_CALL_HANGUP.equals(eventType)) {
                    onCallHangup(event, IMXCall.END_CALL_REASON_PEER_HANG_UP);
                }

            } else { // event from the current member, but sent from another device
                switch (eventType) {
                case Event.EVENT_TYPE_CALL_INVITE:
                    // warn in the UI thread
                    mUIThreadHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            dispatchOnStateDidChange(CALL_STATE_RINGING);
                        }
                    });
                    break;

                case Event.EVENT_TYPE_CALL_ANSWER:
                    // call answered from another device
                    mUIThreadHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            onAnsweredElsewhere();
                        }
                    });
                    break;

                case Event.EVENT_TYPE_CALL_HANGUP:
                    // current member answered elsewhere
                    onCallHangup(event, IMXCall.END_CALL_REASON_PEER_HANG_UP_ELSEWHERE);
                    break;

                default:
                    break;
                } // switch end
            }
        }
    }

    // user actions

    /**
     * The call is accepted.
     */
    @Override
    public void answer() {
        Log.d(LOG_TAG, "answer " + getCallState());

        if (!CALL_STATE_CREATED.equals(getCallState()) && (null != mPeerConnection)) {
            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (null == mPeerConnection) {
                        Log.d(LOG_TAG, "answer the connection has been closed");
                        return;
                    }

                    dispatchOnStateDidChange(CALL_STATE_CREATE_ANSWER);

                    MediaConstraints constraints = new MediaConstraints();
                    constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
                    constraints.mandatory.add(
                            new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideo() ? "true" : "false"));

                    mPeerConnection.createAnswer(new SdpObserver() {
                        @Override
                        public void onCreateSuccess(SessionDescription sessionDescription) {
                            Log.d(LOG_TAG, "createAnswer onCreateSuccess");

                            final SessionDescription sdp = new SessionDescription(sessionDescription.type,
                                    sessionDescription.description);

                            mUIThreadHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    if (mPeerConnection != null) {
                                        // must be done to before sending the invitation message
                                        mPeerConnection.setLocalDescription(new SdpObserver() {
                                            @Override
                                            public void onCreateSuccess(SessionDescription sessionDescription) {
                                                Log.d(LOG_TAG, "setLocalDescription onCreateSuccess");
                                            }

                                            @Override
                                            public void onSetSuccess() {
                                                Log.d(LOG_TAG, "setLocalDescription onSetSuccess");
                                                sendAnswer(sdp);
                                                dispatchOnStateDidChange(IMXCall.CALL_STATE_CONNECTING);
                                            }

                                            @Override
                                            public void onCreateFailure(String s) {
                                                Log.e(LOG_TAG, "setLocalDescription onCreateFailure " + s);
                                                dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                                                hangup(null);
                                            }

                                            @Override
                                            public void onSetFailure(String s) {
                                                Log.e(LOG_TAG, "setLocalDescription onSetFailure " + s);
                                                dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                                                hangup(null);
                                            }
                                        }, sdp);
                                    }
                                }
                            });
                        }

                        @Override
                        public void onSetSuccess() {
                            Log.d(LOG_TAG, "createAnswer onSetSuccess");
                        }

                        @Override
                        public void onCreateFailure(String s) {
                            Log.e(LOG_TAG, "createAnswer onCreateFailure " + s);
                            dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                            hangup(null);
                        }

                        @Override
                        public void onSetFailure(String s) {
                            Log.e(LOG_TAG, "createAnswer onSetFailure " + s);
                            dispatchOnCallError(CALL_ERROR_CAMERA_INIT_FAILED);
                            hangup(null);
                        }
                    }, constraints);

                }
            });
        }

    }

    /**
     * The call is hung up.
     */
    @Override
    public void hangup(String reason) {
        Log.d(LOG_TAG, "## hangup(): reason=" + reason);

        if (!isCallEnded()) {
            sendHangup(reason);
            terminate(IMXCall.END_CALL_REASON_UNDEFINED);
        }
    }

    /**
     * @return the callstate (must be a CALL_STATE_XX value)
     */
    @Override
    public String getCallState() {
        return mCallState;
    }

    /**
     * @return the callView
     */
    @Override
    public View getCallView() {
        return mCallView;
    }

    /**
     * @return the callView visibility
     */
    @Override
    public int getVisibility() {
        if (null != mCallView) {
            return mCallView.getVisibility();
        } else {
            return View.GONE;
        }
    }

    /**
     * Set the callview visibility
     *
     * @return true if the operation succeeds
     */
    @Override
    public boolean setVisibility(int visibility) {
        if (null != mCallView) {
            mCallView.setVisibility(visibility);
            return true;
        }

        return false;
    }

    /**
     * The call has been answered on another device.
     * We distinguish the case where an account is active on
     * multiple devices and a video call is launched on the account. In this case
     * the callee who did not answer must display a "answered elsewhere" message.
     */
    @Override
    public void onAnsweredElsewhere() {
        String state = getCallState();

        Log.d(LOG_TAG, "onAnsweredElsewhere in state " + state);

        if (!isCallEnded() && !mIsAnswered) {
            dispatchAnsweredElsewhere();
            terminate(IMXCall.END_CALL_REASON_UNDEFINED);

        }
    }

    @Override
    protected void dispatchOnStateDidChange(String newState) {
        Log.d(LOG_TAG, "dispatchOnStateDidChange " + newState);

        mCallState = newState;

        // call timeout management
        if (CALL_STATE_CONNECTING.equals(mCallState) || CALL_STATE_CONNECTING.equals(mCallState)) {
            if (null != mCallTimeoutTimer) {
                mCallTimeoutTimer.cancel();
                mCallTimeoutTimer = null;
            }
        }

        super.dispatchOnStateDidChange(newState);
    }

    //==============================================================================================================
    // Preview size management
    //==============================================================================================================

    /**
     * @return the device rotation angle
     */
    private int getDeviceOrientation() {
        try {
            WindowManager wm = (WindowManager) this.mContext.getApplicationContext()
                    .getSystemService(Context.WINDOW_SERVICE);
            short orientation1;
            switch (wm.getDefaultDisplay().getRotation()) {
            case Surface.ROTATION_0:
            default:
                orientation1 = 0;
                break;
            case Surface.ROTATION_90:
                orientation1 = 90;
                break;
            case Surface.ROTATION_180:
                orientation1 = 180;
                break;
            case Surface.ROTATION_270:
                orientation1 = 270;
            }

            return orientation1;
        } catch (Exception e) {
            Log.e(LOG_TAG, "## getDeviceOrientation() failed " + e.getMessage());
        }

        return 0;
    }

    /**
     * The camera preview frame has been updated
     *
     * @param camera            the camera
     * @param cameraOrientation the camera orientation
     */
    private void onPreviewFrameUpdate(Camera camera, int cameraOrientation) {
        Camera.Size s;

        try {
            s = camera.getParameters().getPreviewSize();
        } catch (Exception e) {
            Log.e(LOG_TAG, "## onPreviewFrameUpdate() failed " + e.getMessage());
            return;
        }

        final int width;
        final int height;
        int rotation = (360 + cameraOrientation + getDeviceOrientation()) % 360;

        if ((rotation == 90) || (rotation == 270)) {
            width = s.height;
            height = s.width;
        } else {
            width = s.width;
            height = s.height;
        }

        if ((width != mLocalRenderWidth) || (height != mLocalRenderHeight)) {
            mLocalRenderWidth = width;
            mLocalRenderHeight = height;

            mUIThreadHandler.post(new Runnable() {
                @Override
                public void run() {
                    dispatchOnPreviewSizeChanged(width, height);
                }
            });
        }
    }

    /**
     * Define a listener to track the local frame update.
     */
    private void listenPreviewUpdate() {
        try {
            if (null != mVideoCapturer) {
                Field field = mVideoCapturer.getClass().getDeclaredField("camera");
                field.setAccessible(true);
                Camera camera = (Camera) field.get(mVideoCapturer);

                if (null != camera) {
                    try {
                        Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
                        Camera.getCameraInfo(mCameraInUse == CAMERA_TYPE_FRONT
                                ? android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT
                                : android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK, info);

                        final int cameraOrientation = info.orientation;

                        camera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
                            @Override
                            public void onPreviewFrame(byte[] data, Camera camera) {
                                onPreviewFrameUpdate(camera, cameraOrientation);
                                try {
                                    ((VideoCapturerAndroid) mVideoCapturer).onPreviewFrame(data, camera);
                                } catch (Exception e) {
                                    Log.e(LOG_TAG,
                                            "## listenPreviewUpdate() : onPreviewFrame failed " + e.getMessage());
                                }
                            }
                        });

                        onPreviewFrameUpdate(camera, cameraOrientation);
                    } catch (Exception e) {
                        Log.e(LOG_TAG,
                                "## listenPreviewUpdate() : fail to update the camera preview " + e.getMessage());
                    }
                } else {
                    Log.e(LOG_TAG, "## listenPreviewUpdate() : did not find the camera");
                }
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "## listenPreviewUpdate() failed " + e.getMessage());
        }
    }

}