Skip to content
Snippets Groups Projects
Select Git revision
  • 1cbc8dfb82f0e9dc69fc62ca95f418e723a34f85
  • master default protected
  • release/202005
  • release/202001
  • release/201912
  • release/windows-test/201910
  • release/201908
  • release/201906
  • release/201905
  • release/201904
  • release/201903
  • release/201902
  • release/201901
  • release/201812
  • release/201811
  • release/201808
  • wip/patches_poly_2017/cedryk_doucet/abderahmane_bouziane
  • releases/beta1
  • android/release_461
  • android/release_460
  • android/release_459
  • android/release_458
  • android/release_457
  • android/release_456
  • android/release_455
  • android/release_454
  • android/release_453
  • android/release_452
  • android/release_451
  • android/release_450
  • android/release_449
  • android/release_448
  • android/release_447
  • android/release_446
  • android/release_445
  • android/release_444
  • android/release_443
  • android/release_442
38 results

CallPresenter.java

Blame
  • 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();
        }
    }