/*
* 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());
}
}
}
}