CallViewModel.swift 15.6 KB
Newer Older
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
1
/*
2
 *  Copyright (C) 2017-2019 Savoir-faire Linux Inc.
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
3 4 5
 *
 *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
 *  Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
6
 *  Author: Quentin Muret <quentin.muret@savoirfairelinux.com>
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
 */

import RxSwift
import SwiftyBeaver
import Contacts
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
26
import RxCocoa
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
27 28 29 30 31 32 33 34 35 36 37 38

class CallViewModel: Stateable, ViewModel {

    //stateable
    private let stateSubject = PublishSubject<State>()
    lazy var state: Observable<State> = {
        return self.stateSubject.asObservable()
    }()

    fileprivate let callService: CallsService
    fileprivate let contactsService: ContactsService
    fileprivate let accountService: AccountsService
39
    fileprivate let videoService: VideoService
40
    fileprivate let audioService: AudioService
41
    fileprivate let profileService: ProfilesService
42

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
43 44 45
    private let disposeBag = DisposeBag()
    fileprivate let log = SwiftyBeaver.self

46 47 48
    var isHeadsetConnected = false
    var isAudioOnly = false

49 50 51 52 53
    var call: CallModel? {
        didSet {
            guard let call = self.call else {
                return
            }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
54
            guard let account = self.accountService.currentAccount else {return}
55 56
            isHeadsetConnected = self.audioService.isHeadsetConnected.value
            isAudioOnly = call.isAudioOnly
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
57 58 59 60 61 62 63 64
            let type = account.type == AccountType.sip

            containerViewModel =
                ButtonsContainerViewModel(isAudioOnly: self.isAudioOnly,
                                                           with: self.callService,
                                                           audioService: self.audioService,
                                                           callID: call.callId,
                                                           isSipCall: type)
65 66
        }
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
67

68
    // data for ViewController binding
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
69

70
    lazy var contactImageData: Observable<Data?>? = {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
71 72
        guard let call = self.call,
            let account = self.accountService.getAccount(fromAccountId: call.accountId) else {
73 74
            return nil
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
75 76 77 78 79
        let type = account.type == AccountType.sip ? URIType.sip : URIType.ring
        guard let uriString = JamiURI.init(schema: type,
                  infoHach: call.participantUri,
                  account: account).uriString else {return nil}
        return self.profileService.getProfile(uri: uriString,
80
                                              createIfNotexists: true, accountId: account.id)
81 82 83 84 85 86 87 88 89 90
            .filter({ profile in
                guard let photo = profile.photo else {
                    return false
                }
                return true
            }).map({ profile in
                return NSData(base64Encoded: profile.photo!,
                              options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data?
            })
    }()
91

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
92 93 94 95 96 97
    lazy var incomingFrame: Observable<UIImage?> = {
        return videoService.incomingVideoFrame.asObservable().map({ frame in
            return frame
        })
    }()
    lazy var capturedFrame: Observable<UIImage?> = {
98
        videoService.startVideoCaptureBeforeCall()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
99 100 101 102 103
        return videoService.capturedVideoFrame.asObservable().map({ frame in
            return frame
        })
    }()

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
104
    lazy var dismisVC: Observable<Bool> = {
105 106
        return callService.currentCall.filter({ [weak self] call in
            return call.callId == self?.call?.callId
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
107
        })
108
            .map({ call in
109
                return call.state == .over || call.state == .failure
110
            }).map({ [weak self] hide in
111
                if hide {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
112
                    self?.videoService.setCameraOrientation(orientation: UIDevice.current.orientation, callID: nil)
113
                }
114 115
                return hide
            })
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
116 117
    }()

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
118
    lazy var contactName: Driver<String> = {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
119 120 121 122 123 124 125 126 127 128
        return callService.currentCall.filter({ [weak self] call in
            return call.state != .over && call.state != .inactive && call.callId == self?.call?.callId
        }).map({ call in
            if !call.displayName.isEmpty {
                return call.displayName
            } else if !call.registeredName.isEmpty {
                return call.registeredName
            } else {
                return L10n.Calls.unknown
            }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
129
        }).asDriver(onErrorJustReturn: "")
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
130 131
    }()

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
132
    lazy var callDuration: Driver<String> = {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
133 134 135 136 137 138 139 140
        let timer = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
            .takeUntil(self.callService.currentCall
                .filter { [weak self] call in
                    call.state == .over &&
                        call.callId == self?.call?.callId
            })
            .map({ elapsed in
                return CallViewModel.formattedDurationFrom(interval: elapsed)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
141 142 143 144
            }).share()
        return self.callService.currentCall.filter({ [weak self] call in
            return call.state == .current &&
                call.callId == self?.call?.callId
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
145 146
        }).flatMap({ _ in
            return timer
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
147
        }).asDriver(onErrorJustReturn: "")
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
148 149 150
    }()

    lazy var bottomInfo: Observable<String> = {
151 152
        return callService.currentCall
            .filter({ [weak self] call in
153 154
                return call.callId == self?.call?.callId &&
                    call.callType == .outgoing
155
            }).map({ [weak self] call in
156 157 158 159 160 161 162 163 164 165
                switch call.state {
                case .connecting :
                        return L10n.Calls.connecting
                case .ringing :
                        return L10n.Calls.ringing
                case .over :
                        return L10n.Calls.callFinished
                default :
                        return ""
                }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
166 167
        })
    }()
168

169 170 171 172
    lazy var isActiveVideoCall: Observable<Bool> = { [unowned self] in
        return (self.callService.currentCall
            .filter({call in
                return call.callId == self.call?.callId
173
            }).map({ call in
174
                return call.state == .current && !self.isAudioOnly
175
            }))
176 177
    }()

178
    lazy var showCallOptions: Observable<Bool> = { [unowned self] in
179
        return Observable.combineLatest(self.screenTapped.asObservable(),
180
                                        isActiveVideoCall) { (tapped, shouldRespond) in
181
            if tapped && shouldRespond {
182 183 184
                return true
            }
            return false
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
185 186 187
        }
    }()

188
    lazy var showCancelOption: Observable<Bool> = { [unowned self] in
189 190
        return self.callService.currentCall
            .filter({ [weak self] call in
191 192
                return call.callId == self?.call?.callId &&
                    (call.state == .connecting || call.state == .ringing || call.state == .current)
193
            }).map({ call in
194
            return call.state == .connecting || call.state == .ringing
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
195 196 197
        })
    }()

198
    lazy var showCapturedFrame: Observable<Bool> = { [unowned self] in
199 200 201 202 203 204 205 206 207
        return self.callService.currentCall
            .filter({ [weak self] call in
                return call.callId == self?.call?.callId &&
                    (call.state == .connecting || call.state == .ringing || call.state == .current)
            }).map({ call in
                call.state == .current
            })
    }()

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
208 209
    var screenTapped = BehaviorSubject(value: false)

210
    lazy var videoButtonState: Observable<UIImage?> = { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
211 212 213
        let onImage = UIImage(asset: Asset.videoRunning)
        let offImage = UIImage(asset: Asset.videoMuted)

214 215
        return self.videoMuted.map({ [weak self] muted in
            let audioOnly = self?.call?.isAudioOnly ?? false
216
            if audioOnly || muted {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
217 218 219 220 221 222
                return offImage
            }
            return onImage
        })
    }()

223
    lazy var videoMuted: Observable<Bool> = { [unowned self] in
224 225
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
226 227 228 229 230 231
                call.state == .current
        }).map({call in
            return call.videoMuted
        })
    }()

232
    lazy var audioButtonState: Observable<UIImage?> = { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
233 234 235 236 237 238 239 240 241 242 243
        let onImage = UIImage(asset: Asset.audioRunning)
        let offImage = UIImage(asset: Asset.audioMuted)

        return self.audioMuted.map({ muted in
            if muted {
                return offImage
            }
            return onImage
        })
    }()

244
    lazy var speakerButtonState: Observable<UIImage?> = { [unowned self] in
245 246 247 248 249 250 251 252 253 254 255 256
        let offImage = UIImage(asset: Asset.disableSpeakerphone)
        let onImage = UIImage(asset: Asset.enableSpeakerphone)

        return self.isOutputToSpeaker
            .map({ speaker in
                if speaker {
                    return onImage
                }
                return offImage
            })
    }()

257
    lazy var isOutputToSpeaker: Observable<Bool> = { [unowned self] in
258 259 260
        return self.audioService.isOutputToSpeaker.asObservable()
    }()

261
    lazy var speakerSwitchable: Observable<Bool> = { [unowned self] in
262 263 264 265
        return self.audioService.isHeadsetConnected.asObservable()
            .map { value in return !value }
    }()

266
    lazy var audioMuted: Observable<Bool> = { [unowned self] in
267 268
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
269 270 271 272 273 274
                call.state == .current
        }).map({call in
            return call.audioMuted
        })
    }()

275
    lazy var pauseCallButtonState: Observable<UIImage?> = { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
276 277 278 279 280 281 282 283 284 285 286
        let unpauseCall = UIImage(asset: Asset.unpauseCall)
        let pauseCall = UIImage(asset: Asset.pauseCall)

        return self.callPaused.map({ muted in
            if muted {
                return unpauseCall
            }
            return pauseCall
        })
    }()

287
    lazy var callPaused: Observable<Bool> = { [unowned self] in
288 289
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
290 291 292 293 294 295 296 297 298 299 300 301
                (call.state == .hold ||
                    call.state == .unhold ||
                    call.state == .current)
        }).map({call in
            if  call.state == .hold ||
                (call.state == .current && call.peerHolding) {
                return true
            }
            return false
        })
    }()

302
    var containerViewModel: ButtonsContainerViewModel?
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
303
    let injectionBag: InjectionBag
304

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
305 306 307 308
    required init(with injectionBag: InjectionBag) {
        self.callService = injectionBag.callService
        self.contactsService = injectionBag.contactsService
        self.accountService = injectionBag.accountService
309
        self.videoService = injectionBag.videoService
310
        self.audioService = injectionBag.audioService
311
        self.profileService = injectionBag.profileService
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
312
        self.injectionBag = injectionBag
313 314 315 316 317

        callService.currentCall.filter({ [weak self] call in
            return call.callId == self?.call?.callId
        }).map({ call in
            return call.state == .current
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
318 319 320 321
        }).subscribe(onNext: { [weak self] call in
            self?.videoService
                .setCameraOrientation(orientation: UIDevice.current.orientation,
                                      callID: self?.call?.callId)
322
        }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
323
    }
324

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
325 326 327 328
    static func formattedDurationFrom(interval: Int) -> String {
        let seconds = interval % 60
        let minutes = (interval / 60) % 60
        let hours = (interval / 3600)
329 330 331 332 333 334
        switch hours {
        case 0:
            return String(format: "%02d:%02d", minutes, seconds)
        default:
            return String(format: "%02d:%02d:%02d", hours, minutes, seconds)
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
335 336 337 338 339 340 341 342
    }

    func cancelCall() {
        guard let call = self.call else {
            return
        }
        self.callService.hangUp(callId: call.callId)
            .subscribe(onCompleted: { [weak self] in
343 344 345
                // switch to either spk or headset (if connected) for loud ringtone
                // incase we were using rcv during the call
                self?.audioService.setToRing()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
346 347 348 349 350 351
                self?.log.info("Call canceled")
                }, onError: { [weak self] error in
                    self?.log.error("Failed to cancel the call")
            }).disposed(by: self.disposeBag)
    }

352
    func answerCall() -> Completable {
353 354 355
        if !self.audioService.isHeadsetConnected.value {
            isAudioOnly ?
                self.audioService.overrideToReceiver() : self.audioService.overrideToSpeaker()
356
        }
357
        return self.callService.accept(call: call)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
358 359
    }

360
    func placeCall(with uri: String, userName: String, isAudioOnly: Bool = false) {
361

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
362 363 364
        guard let account = self.accountService.currentAccount else {
            return
        }
365 366 367
        if !self.audioService.isHeadsetConnected.value {
            isAudioOnly ?
                self.audioService.overrideToReceiver() : self.audioService.overrideToSpeaker()
368
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
369 370
        self.callService.placeCall(withAccount: account,
                                   toRingId: uri,
371 372
                                   userName: userName,
                                   isAudioOnly: isAudioOnly)
373 374
            .subscribe(onSuccess: { [weak self] callModel in
                self?.call = callModel
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
375 376
            }).disposed(by: self.disposeBag)
    }
377

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
378 379 380
    func respondOnTap() {
        self.screenTapped.onNext(true)
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
381 382 383

    // MARK: call options

384
    func togglePauseCall() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
        guard let call = self.call else {
            return
        }
        if call.state == .current {
            self.callService.hold(callId: call.callId)
                .subscribe(onCompleted: { [weak self] in
                    self?.log.info("call paused")
                    }, onError: { [weak self](error) in
                        self?.log.info(error)
                }).disposed(by: self.disposeBag)
        } else if call.state == .hold {
            self.callService.unhold(callId: call.callId)
                .subscribe(onCompleted: { [weak self] in
                    self?.log.info("call unpaused")
                    }, onError: { [weak self](error) in
                        self?.log.info(error)
                }).disposed(by: self.disposeBag)
        }
    }

405
    func toggleMuteAudio() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
406 407 408 409 410 411 412
        guard let call = self.call else {
            return
        }
        let mute = !call.audioMuted
        self.callService.muteAudio(call: call.callId, mute: mute)
    }

413
    func toggleMuteVideo() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
414 415 416 417 418 419 420 421 422 423
        guard let call = self.call else {
            return
        }
        let mute = !call.videoMuted
        self.callService.muteVideo(call: call.callId, mute: mute)
    }

    func switchCamera() {
        self.videoService.switchCamera()
    }
424 425 426 427

    func switchSpeaker() {
        self.audioService.switchSpeaker()
    }
428 429

    func setCameraOrientation(orientation: UIDeviceOrientation) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
430 431
        videoService.setCameraOrientation(orientation: orientation,
                                          callID: self.call?.callId)
432
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
433 434 435 436

    func showDialpad() {
        self.stateSubject.onNext(ConversationState.showDialpad(inCall: true))
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
437
}