CallViewModel.swift 17.1 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
// swiftlint:disable type_body_length
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
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
            let type = account.type == AccountType.sip

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

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

71
    lazy var contactImageData: Observable<Data?>? = {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
72 73
        guard let call = self.call,
            let account = self.accountService.getAccount(fromAccountId: call.accountId) else {
74 75
            return nil
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
76 77 78 79 80
        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,
81
                                              createIfNotexists: true, accountId: account.id)
82 83 84 85 86 87 88 89 90 91
            .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?
            })
    }()
92

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

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
105
    lazy var dismisVC: Observable<Bool> = {
106 107
        return callService.currentCall.filter({ [weak self] call in
            return call.callId == self?.call?.callId
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
108
        })
109
            .map({ call in
110
                return call.state == .over || call.state == .failure
111
            }).map({ [weak self] hide in
112
                if hide {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
113
                    self?.videoService.setCameraOrientation(orientation: UIDevice.current.orientation, callID: nil)
114
                    self?.videoService.stopAudioDevice()
115
                    if #available(iOS 10.0, *), let call = self?.call {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
116 117
                        self?.callsProvider.stopCall(callUUID: call.callUUID)
                    }
118
                }
119 120
                return hide
            })
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
121 122
    }()

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
123
    lazy var contactName: Driver<String> = {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
124 125 126 127 128 129 130 131 132 133
        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
134
        }).asDriver(onErrorJustReturn: "")
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
135 136
    }()

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
137
    lazy var callDuration: Driver<String> = {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
138 139 140 141 142 143 144 145
        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
146 147 148 149
            }).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
150 151
        }).flatMap({ _ in
            return timer
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
152
        }).asDriver(onErrorJustReturn: "")
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
153 154 155
    }()

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

177 178 179 180
    lazy var isActiveVideoCall: Observable<Bool> = { [unowned self] in
        return (self.callService.currentCall
            .filter({call in
                return call.callId == self.call?.callId
181
            }).map({ call in
182
                return call.state == .current && !self.isAudioOnly
183
            }))
184 185
    }()

186
    lazy var showCallOptions: Observable<Bool> = { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
187
        return self.screenTapped.asObservable()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
188 189
    }()

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

200
    lazy var showCapturedFrame: Observable<Bool> = { [unowned self] in
201 202 203 204 205 206 207 208 209
        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
210 211
    var screenTapped = BehaviorSubject(value: false)

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

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

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

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

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

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

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

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

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

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

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

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

289
    lazy var callPaused: Observable<Bool> = { [unowned self] in
290 291
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
292 293 294 295 296 297 298 299 300 301 302 303
                (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
        })
    }()

304
    var containerViewModel: ButtonsContainerViewModel?
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
305
    let injectionBag: InjectionBag
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
306
    let callsProvider: CallsProviderDelegate
307

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

        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
322 323 324 325
        }).subscribe(onNext: { [weak self] call in
            self?.videoService
                .setCameraOrientation(orientation: UIDevice.current.orientation,
                                      callID: self?.call?.callId)
326
        }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
327 328 329 330 331 332 333 334 335 336 337 338 339 340
        callsProvider.sharedResponseStream
            .filter({ [unowned self] serviceEvent in
                guard let callUUID: String = serviceEvent
                    .getEventInput(ServiceEventInput.callUUID) else {return false}
                return callUUID == self.call?.callUUID.uuidString
            }).subscribe(onNext: { [unowned self] serviceEvent in
                if serviceEvent.eventType == ServiceEventType.callProviderAnswerCall {
                    self.answerCall()
                        .subscribe()
                        .disposed(by: self.disposeBag)
                } else if serviceEvent.eventType == ServiceEventType.callProviderCancellCall {
                    self.cancelCall(stopProvider: false)
                }
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
341
    }
342

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
343 344 345 346
    static func formattedDurationFrom(interval: Int) -> String {
        let seconds = interval % 60
        let minutes = (interval / 60) % 60
        let hours = (interval / 3600)
347 348 349 350 351 352
        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
353 354
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
355
    func cancelCall(stopProvider: Bool) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
356 357 358
        guard let call = self.call else {
            return
        }
359
        if #available(iOS 10.0, *), stopProvider {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
360 361
            self.callsProvider.stopCall(callUUID: call.callUUID)
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
362 363
        self.callService.hangUp(callId: call.callId)
            .subscribe(onCompleted: { [weak self] in
364 365 366
                // switch to either spk or headset (if connected) for loud ringtone
                // incase we were using rcv during the call
                self?.audioService.setToRing()
367
                self?.videoService.stopAudioDevice()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
368 369 370 371 372 373
                self?.log.info("Call canceled")
                }, onError: { [weak self] error in
                    self?.log.error("Failed to cancel the call")
            }).disposed(by: self.disposeBag)
    }

374
    func answerCall() -> Completable {
375 376 377
        if !self.audioService.isHeadsetConnected.value {
            isAudioOnly ?
                self.audioService.overrideToReceiver() : self.audioService.overrideToSpeaker()
378
        }
379
        return self.callService.accept(call: call)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
380 381
    }

382
    func placeCall(with uri: String, userName: String, isAudioOnly: Bool = false) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
383 384 385
        guard let account = self.accountService.currentAccount else {
            return
        }
386 387 388
        if !self.audioService.isHeadsetConnected.value {
            isAudioOnly ?
                self.audioService.overrideToReceiver() : self.audioService.overrideToSpeaker()
389
        }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
390 391
        self.callService.placeCall(withAccount: account,
                                   toRingId: uri,
392 393
                                   userName: userName,
                                   isAudioOnly: isAudioOnly)
394
            .subscribe(onSuccess: { [weak self] callModel in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
395
                callModel.callUUID = UUID()
396
                self?.call = callModel
397
                if #available(iOS 10.0, *) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
398 399 400
                    self?.callsProvider
                        .startCall(account: account, call: callModel)
                }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
401 402
            }).disposed(by: self.disposeBag)
    }
403

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
404 405 406
    func respondOnTap() {
        self.screenTapped.onNext(true)
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
407 408 409

    // MARK: call options

410
    func togglePauseCall() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
        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)
        }
    }

431
    func toggleMuteAudio() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
432 433 434 435 436 437 438
        guard let call = self.call else {
            return
        }
        let mute = !call.audioMuted
        self.callService.muteAudio(call: call.callId, mute: mute)
    }

439
    func toggleMuteVideo() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
440 441 442 443 444 445 446 447 448 449
        guard let call = self.call else {
            return
        }
        let mute = !call.videoMuted
        self.callService.muteVideo(call: call.callId, mute: mute)
    }

    func switchCamera() {
        self.videoService.switchCamera()
    }
450 451 452 453

    func switchSpeaker() {
        self.audioService.switchSpeaker()
    }
454 455

    func setCameraOrientation(orientation: UIDeviceOrientation) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
456 457
        videoService.setCameraOrientation(orientation: orientation,
                                          callID: self.call?.callId)
458
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
459 460 461 462

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