Select Git revision
-
Simon Désaulniers authored
During totallyNormalTest, an initial set of values would be put on the dht. Then, random values from this same set would be put over time, but since values would already be there, no put were really made.
Simon Désaulniers authoredDuring totallyNormalTest, an initial set of values would be put on the dht. Then, random values from this same set would be put over time, but since values would already be there, no put were really made.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
CallPresenter.java 26.18 KiB
/*
* Copyright (C) 2004-2019 Savoir-faire Linux Inc.
*
* Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
* Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package cx.ring.call;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import cx.ring.facades.ConversationFacade;
import cx.ring.model.Conference;
import cx.ring.model.Conversation;
import cx.ring.model.SipCall;
import cx.ring.model.Uri;
import cx.ring.mvp.RootPresenter;
import cx.ring.services.AccountService;
import cx.ring.services.CallService;
import cx.ring.services.ContactService;
import cx.ring.services.DeviceRuntimeService;
import cx.ring.services.HardwareService;
import cx.ring.utils.Log;
import cx.ring.utils.StringUtils;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.Scheduler;
import io.reactivex.disposables.Disposable;
import io.reactivex.subjects.BehaviorSubject;
import io.reactivex.subjects.Subject;
import static cx.ring.daemon.Ringservice.listCallMediaHandlers;
import static cx.ring.daemon.Ringservice.toggleCallMediaHandler;
public class CallPresenter extends RootPresenter<CallView> {
public final static String TAG = CallPresenter.class.getSimpleName();
private AccountService mAccountService;
private ContactService mContactService;
private HardwareService mHardwareService;
private CallService mCallService;
private DeviceRuntimeService mDeviceRuntimeService;
private ConversationFacade mConversationFacade;
private Conference mConference;
private final List<SipCall> mPendingCalls = new ArrayList<>();
private final Subject<List<SipCall>> mPendingSubject = BehaviorSubject.createDefault(mPendingCalls);
private boolean mOnGoingCall = false;
private boolean mAudioOnly = true;
private boolean permissionChanged = false;
private boolean pipIsActive = false;
private boolean incomingIsFullIntent = true;
private boolean callInitialized = false;
private int videoWidth = -1;
private int videoHeight = -1;
private int previewWidth = -1;
private int previewHeight = -1;
private String currentSurfaceId = null;
private String currentPluginSurfaceId = null;
private Disposable timeUpdateTask = null;
@Inject
@Named("UiScheduler")
protected Scheduler mUiScheduler;
@Inject
public CallPresenter(AccountService accountService,
ContactService contactService,
HardwareService hardwareService,
CallService callService,
DeviceRuntimeService deviceRuntimeService,
ConversationFacade conversationFacade) {
mAccountService = accountService;
mContactService = contactService;
mHardwareService = hardwareService;
mCallService = callService;
mDeviceRuntimeService = deviceRuntimeService;
mConversationFacade = conversationFacade;
}
public void cameraPermissionChanged(boolean isGranted) {
if (isGranted && mHardwareService.isVideoAvailable()) {
mHardwareService.initVideo().blockingAwait();
permissionChanged = true;
}
}
public void audioPermissionChanged(boolean isGranted) {
if (isGranted && mHardwareService.hasMicrophone()) {
mCallService.restartAudioLayer();
}
}
@Override
public void unbindView() {
if (!mAudioOnly) {
mHardwareService.endCapture();
}
super.unbindView();
}
@Override
public void bindView(CallView view) {
super.bindView(view);
/*mCompositeDisposable.add(mAccountService.getRegisteredNames()
.observeOn(mUiScheduler)
.subscribe(r -> {
if (mSipCall != null && mSipCall.getContact() != null) {
getView().updateContactBubble(mSipCall.getContact());
}
}));*/
mCompositeDisposable.add(mHardwareService.getVideoEvents()
.observeOn(mUiScheduler)
.subscribe(this::onVideoEvent));
mCompositeDisposable.add(mHardwareService.getAudioState()
.observeOn(mUiScheduler)
.subscribe(state -> getView().updateAudioState(state)));
/*mCompositeDisposable.add(mHardwareService
.getBluetoothEvents()
.subscribe(event -> {
if (!event.connected && mSipCall == null) {
hangupCall();
}
}));*/
}
public void initOutGoing(String accountId, String contactRingId, boolean audioOnly) {
if (accountId == null || contactRingId == null) {
Log.e(TAG, "initOutGoing: null account or contact");
hangupCall();
return;
}
if (!mHardwareService.hasCamera()) {
audioOnly = true;
}
//getView().blockScreenRotation();
mCompositeDisposable.add(mCallService
.placeCall(accountId, StringUtils.toNumber(contactRingId), audioOnly)
//.map(mCallService::getConference)
.flatMapObservable(call -> mCallService.getConfUpdates(call))
.observeOn(mUiScheduler)
.subscribe(conference -> {
contactUpdate(conference);
confUpdate(conference);
}, e -> {
hangupCall();
Log.e(TAG, "Error with initOutgoing: " + e.getMessage());
}));
}
/**
* Returns to or starts an incoming call
*
* @param confId the call id
* @param actionViewOnly true if only returning to call or if using full screen intent
*/
public void initIncomingCall(String confId, boolean actionViewOnly) {
//getView().blockScreenRotation();
// if the call is incoming through a full intent, this allows the incoming call to display
incomingIsFullIntent = actionViewOnly;
Observable<Conference> callObservable = mCallService.getConfUpdates(confId)
.observeOn(mUiScheduler)
.share();
// Handles the case where the call has been accepted, emits a single so as to only check for permissions and start the call once
mCompositeDisposable.add(callObservable
.firstOrError()
.subscribe(call -> {
if (!actionViewOnly) {
contactUpdate(call);
confUpdate(call);
callInitialized = true;
getView().prepareCall(true);
}
}, e -> {
hangupCall();
Log.e(TAG, "Error with initIncoming, preparing call flow :" , e);
}));
// Handles retrieving call updates. Items emitted are only used if call is already in process or if user is returning to a call.
mCompositeDisposable.add(callObservable
.subscribe(call -> {
if (callInitialized || actionViewOnly) {
contactUpdate(call);
confUpdate(call);
}
}, e -> {
hangupCall();
Log.e(TAG, "Error with initIncoming, action view flow: ", e);
}));
}
public void prepareOptionMenu() {
boolean isSpeakerOn = mHardwareService.isSpeakerPhoneOn();
//boolean hasContact = mSipCall != null && null != mSipCall.getContact() && mSipCall.getContact().isUnknown();
boolean canDial = mOnGoingCall && mConference != null && !mConference.isIncoming();
// get the preferences
boolean displayPluginsButton = getView().displayPluginsButton();
boolean showPluginBtn = displayPluginsButton && mOnGoingCall && mConference != null;
boolean hasMultipleCamera = mHardwareService.getCameraCount() > 1 && mOnGoingCall && !mAudioOnly;
getView().initMenu(isSpeakerOn, hasMultipleCamera, canDial, showPluginBtn, mOnGoingCall);
}
public void chatClick() {
if (mConference == null || mConference.getParticipants().isEmpty()) {
return;
}
SipCall firstCall = mConference.getParticipants().get(0);
if (firstCall == null
|| firstCall.getContact() == null
|| firstCall.getContact().getIds() == null
|| firstCall.getContact().getIds().isEmpty()) {
return;
}
getView().goToConversation(firstCall.getAccount(), firstCall.getContact().getIds().get(0));
}
public void speakerClick(boolean checked) {
mHardwareService.toggleSpeakerphone(checked);
}
public void muteMicrophoneToggled(boolean checked) {
mCallService.setMuted(checked);
}
public boolean isMicrophoneMuted() {
return mCallService.isCaptureMuted();
}
public void switchVideoInputClick() {
if(mConference == null)
return;
mHardwareService.switchInput(mConference.getId(), false);
getView().switchCameraIcon(mHardwareService.isPreviewFromFrontCamera());
}
public void configurationChanged(int rotation) {
mHardwareService.setDeviceOrientation(rotation);
}
public void dialpadClick() {
getView().displayDialPadKeyboard();
}
public void acceptCall() {
if (mConference == null) {
return;
}
mCallService.accept(mConference.getId());
}
public void hangupCall() {
List<String> callMediaHandlers = listCallMediaHandlers();
for (String callMediaHandler : callMediaHandlers)
{
toggleCallMediaHandler(callMediaHandler, false);
}
if (mConference != null) {
if (mConference.isConference())
mCallService.hangUpConference(mConference.getId());
else
mCallService.hangUp(mConference.getId());
}
for (SipCall call : mPendingCalls) {
mCallService.hangUp(call.getDaemonIdString());
}
finish();
}
public void refuseCall() {
final Conference call = mConference;
if (call != null) {
mCallService.refuse(call.getId());
}
finish();
}
public void videoSurfaceCreated(Object holder) {
if (mConference == null) {
return;
}
String newId = mConference.getId();
if (!newId.equals(currentSurfaceId)) {
mHardwareService.removeVideoSurface(currentSurfaceId);
currentSurfaceId = newId;
}
mHardwareService.addVideoSurface(mConference.getId(), holder);
getView().displayContactBubble(false);
}
public void videoSurfaceUpdateId(String newId) {
if (!Objects.equals(newId, currentSurfaceId)) {
mHardwareService.updateVideoSurfaceId(currentSurfaceId, newId);
currentSurfaceId = newId;
}
}
public void pluginSurfaceCreated(Object holder) {
if (mConference == null) {
return;
}
String newId = mConference.getPluginId();
if (!newId.equals(currentPluginSurfaceId)) {
mHardwareService.removeVideoSurface(currentPluginSurfaceId);
currentPluginSurfaceId = newId;
}
mHardwareService.addVideoSurface(mConference.getPluginId(), holder);
getView().displayContactBubble(false);
}
public void pluginSurfaceUpdateId(String newId) {
if (!Objects.equals(newId, currentPluginSurfaceId)) {
mHardwareService.updateVideoSurfaceId(currentPluginSurfaceId, newId);
currentPluginSurfaceId = newId;
}
}
public void previewVideoSurfaceCreated(Object holder) {
mHardwareService.addPreviewVideoSurface(holder, mConference);
//mHardwareService.startCapture(null);
}
public void videoSurfaceDestroyed() {
if (currentSurfaceId != null) {
mHardwareService.removeVideoSurface(currentSurfaceId);
currentSurfaceId = null;
}
}
public void pluginSurfaceDestroyed() {
if (currentPluginSurfaceId != null) {
mHardwareService.removeVideoSurface(currentPluginSurfaceId);
currentPluginSurfaceId = null;
}
}
public void previewVideoSurfaceDestroyed() {
mHardwareService.removePreviewVideoSurface();
mHardwareService.endCapture();
}
public void displayChanged() {
mHardwareService.switchInput(mConference.getId(), false);
}
public void layoutChanged() {
//getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
}
public void uiVisibilityChanged(boolean displayed) {
CallView view = getView();
if (view != null)
view.displayHangupButton(mOnGoingCall && displayed);
}
private void finish() {
if (timeUpdateTask != null && !timeUpdateTask.isDisposed()) {
timeUpdateTask.dispose();
timeUpdateTask = null;
}
mConference = null;
CallView view = getView();
if (view != null)
view.finish();
}
private Disposable contactDisposable = null;
private void contactUpdate(final Conference conference) {
if (mConference != conference) {
mConference = conference;
if (contactDisposable != null && !contactDisposable.isDisposed()) {
contactDisposable.dispose();
}
if (conference.getParticipants().isEmpty())
return;
// Updates of participant (and pending participant) list
Observable<List<SipCall>> callsObservable = mPendingSubject
.map(pendingList -> {
Log.w(TAG, "mPendingSubject onNext " + pendingList.size() + " " + conference.getParticipants().size());
if (pendingList.isEmpty())
return conference.getParticipants();
List<SipCall> newList = new ArrayList<>(conference.getParticipants().size() + pendingList.size());
newList.addAll(conference.getParticipants());
newList.addAll(pendingList);
return newList;
});
// Updates of individual contacts
Observable<List<Observable<SipCall>>> contactsObservable = callsObservable
.flatMapSingle(calls -> Observable
.fromIterable(calls)
.map(call -> mContactService.observeContact(call.getAccount(), call.getContact())
.map(contact -> call))
.toList(calls.size()));
// Combined updates of contacts as participant list updates
Observable<List<SipCall>> contactUpdates = contactsObservable
.switchMap(list -> Observable
.combineLatest(list, objects -> {
Log.w(TAG, "flatMapObservable " + objects.length);
ArrayList<SipCall> calls = new ArrayList<>(objects.length);
for (Object call : objects)
calls.add((SipCall)call);
return (List<SipCall>)calls;
}))
.filter(list -> !list.isEmpty());
contactDisposable = contactUpdates
.observeOn(mUiScheduler)
.subscribe(cs -> getView().updateContactBubble(cs), e -> Log.e(TAG, "Error updating contact data", e));
mCompositeDisposable.add(contactDisposable);
}
mPendingSubject.onNext(mPendingCalls);
}
private void confUpdate(Conference call) {
Log.w(TAG, "confUpdate " + call.getId());
mConference = call;
SipCall.CallStatus status = mConference.getState();
if (status == SipCall.CallStatus.HOLD && mCallService.getConferenceList().size() == 1) {
mCallService.unhold(mConference.getId());
}
mAudioOnly = !call.hasVideo();
CallView view = getView();
if (view == null)
return;
view.updateMenu();
if (call.isOnGoing()) {
mOnGoingCall = true;
view.initNormalStateDisplay(mAudioOnly, isMicrophoneMuted());
view.updateMenu();
if (!mAudioOnly) {
mHardwareService.setPreviewSettings();
mHardwareService.updatePreviewVideoSurface(mConference);
videoSurfaceUpdateId(call.getId());
pluginSurfaceUpdateId(call.getPluginId());
view.displayVideoSurface(true, mDeviceRuntimeService.hasVideoPermission());
if (permissionChanged) {
mHardwareService.switchInput(mConference.getId(), permissionChanged);
permissionChanged = false;
}
}
if (timeUpdateTask != null)
timeUpdateTask.dispose();
timeUpdateTask = mUiScheduler.schedulePeriodicallyDirect(this::updateTime, 0, 1, TimeUnit.SECONDS);
} else if (call.isRinging()) {
SipCall scall = call.getCall();
view.handleCallWakelock(mAudioOnly);
if (scall.isIncoming()) {
if (mAccountService.getAccount(scall.getAccount()).isAutoanswerEnabled()) {
mCallService.accept(scall.getDaemonIdString());
// only display the incoming call screen if the notification is a full screen intent
} else if (incomingIsFullIntent) {
view.initIncomingCallDisplay();
}
} else {
mOnGoingCall = false;
view.updateCallStatus(scall.getCallStatus());
view.initOutGoingCallDisplay();
}
} else {
finish();
}
}
private void updateTime() {
CallView view = getView();
if (view != null && mConference != null) {
if (mConference.isOnGoing()) {
long start = mConference.getTimestampStart();
if (start != Long.MAX_VALUE) {
view.updateTime((System.currentTimeMillis() - start) / 1000);
} else {
view.updateTime(-1);
}
}
}
}
private void onVideoEvent(HardwareService.VideoEvent event) {
Log.d(TAG, "VIDEO_EVENT: " + event.start + " " + event.callId + " " + event.w + "x" + event.h);
if (event.start) {
getView().displayVideoSurface(true, !isPipMode() && mDeviceRuntimeService.hasVideoPermission());
} else if (mConference != null && mConference.getId().equals(event.callId)) {
getView().displayVideoSurface(event.started, event.started && !isPipMode() && mDeviceRuntimeService.hasVideoPermission());
if (event.started) {
videoWidth = event.w;
videoHeight = event.h;
getView().resetVideoSize(videoWidth, videoHeight);
}
} else if (event.callId == null) {
if (event.started) {
previewWidth = event.w;
previewHeight = event.h;
getView().resetPreviewVideoSize(previewWidth, previewHeight, event.rot);
}
}
if (mConference != null && mConference.getPluginId().equals(event.callId)) {
if (event.started) {
previewWidth = event.w;
previewHeight = event.h;
getView().resetPluginPreviewVideoSize(previewWidth, previewHeight, event.rot);
}
}
/*if (event.started || event.start) {
getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
}*/
}
public void positiveButtonClicked() {
if (mConference.isRinging() && mConference.isIncoming()) {
acceptCall();
} else {
hangupCall();
}
}
public void negativeButtonClicked() {
if (mConference.isRinging() && mConference.isIncoming()) {
refuseCall();
} else {
hangupCall();
}
}
public void toggleButtonClicked() {
if (mConference != null && !(mConference.isRinging() && mConference.isIncoming())) {
hangupCall();
}
}
public boolean isAudioOnly() {
return mAudioOnly;
}
public void requestPipMode() {
if (mConference != null && mConference.isOnGoing() && mConference.hasVideo()) {
getView().enterPipMode(mConference.getId());
}
}
public boolean isPipMode() {
return pipIsActive;
}
public void pipModeChanged(boolean pip) {
pipIsActive = pip;
if (pip) {
getView().displayHangupButton(false);
getView().displayPreviewSurface(false);
getView().displayVideoSurface(true, false);
} else {
getView().displayPreviewSurface(true);
getView().displayVideoSurface(true, mDeviceRuntimeService.hasVideoPermission());
}
}
public boolean isSpeakerphoneOn() {
return mHardwareService.isSpeakerPhoneOn();
}
public void sendDtmf(CharSequence s) {
mCallService.playDtmf(s.toString());
}
public void addConferenceParticipant(String accountId, String contactId) {
mCompositeDisposable.add(mConversationFacade.startConversation(accountId, new Uri(contactId))
.map(Conversation::getCurrentCalls)
.subscribe(confs -> {
if (confs.isEmpty()) {
final Observer<SipCall> pendingObserver = new Observer<SipCall>() {
private SipCall call = null;
@Override
public void onSubscribe(Disposable d) {}
@Override
public void onNext(SipCall sipCall) {
if (call == null) {
call = sipCall;
mPendingCalls.add(sipCall);
}
mPendingSubject.onNext(mPendingCalls);
}
@Override
public void onError(Throwable e) {}
@Override
public void onComplete() {
if (call != null) {
mPendingCalls.remove(call);
mPendingSubject.onNext(mPendingCalls);
call = null;
}
}
};
// Place new call, join to conference when answered
Maybe<SipCall> newCall = mCallService.placeCallObservable(accountId, contactId, mAudioOnly)
.doOnEach(pendingObserver)
.filter(SipCall::isOnGoing)
.firstElement()
.delay(1, TimeUnit.SECONDS)
.doOnEvent((v, e) -> pendingObserver.onComplete());
mCompositeDisposable.add(newCall.subscribe(call -> {
String id = mConference.getId();
if (mConference.isConference()) {
mCallService.addParticipant(call.getDaemonIdString(), id);
} else {
mCallService.joinParticipant(id, call.getDaemonIdString()).subscribe();
}
}));
} else {
// Selected contact already in call or conference, join it to current conference
Conference selectedConf = confs.get(0);
if (selectedConf != mConference) {
if (mConference.isConference()) {
if (selectedConf.isConference())
mCallService.joinConference(mConference.getId(), selectedConf.getId());
else
mCallService.addParticipant(selectedConf.getId(), mConference.getId());
} else {
if (selectedConf.isConference())
mCallService.addParticipant(mConference.getId(), selectedConf.getId());
else
mCallService.joinParticipant(mConference.getId(), selectedConf.getId()).subscribe();
}
}
}
}));
}
public void startAddParticipant() {
getView().startAddParticipant(mConference.getId());
}
public void hangupParticipant(SipCall call) {
mCallService.hangUp(call.getDaemonIdString());
}
public void openParticipantContact(SipCall call) {
getView().goToContact(call.getAccount(), call.getContact());
}
public void stopCapture() {
mHardwareService.stopCapture();
}
public boolean startScreenShare(Object mediaProjection) {
return mHardwareService.startScreenShare(mediaProjection);
}
public void stopScreenShare() {
mHardwareService.stopScreenShare();
}
}