CallViewModel.swift 15.3 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
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
179
        return self.screenTapped.asObservable()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
180 181
    }()

182
    lazy var showCancelOption: Observable<Bool> = { [unowned self] in
183 184
        return self.callService.currentCall
            .filter({ [weak self] call in
185 186
                return call.callId == self?.call?.callId &&
                    (call.state == .connecting || call.state == .ringing || call.state == .current)
187
            }).map({ call in
188
            return call.state == .connecting || call.state == .ringing
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
189 190 191
        })
    }()

192
    lazy var showCapturedFrame: Observable<Bool> = { [unowned self] in
193 194 195 196 197 198 199 200 201
        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
202 203
    var screenTapped = BehaviorSubject(value: false)

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

208 209
        return self.videoMuted.map({ [weak self] muted in
            let audioOnly = self?.call?.isAudioOnly ?? false
210
            if audioOnly || muted {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
211 212 213 214 215 216
                return offImage
            }
            return onImage
        })
    }()

217
    lazy var videoMuted: Observable<Bool> = { [unowned self] in
218 219
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
220 221 222 223 224 225
                call.state == .current
        }).map({call in
            return call.videoMuted
        })
    }()

226
    lazy var audioButtonState: Observable<UIImage?> = { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
227 228 229 230 231 232 233 234 235 236 237
        let onImage = UIImage(asset: Asset.audioRunning)
        let offImage = UIImage(asset: Asset.audioMuted)

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

238
    lazy var speakerButtonState: Observable<UIImage?> = { [unowned self] in
239 240 241 242 243 244 245 246 247 248 249 250
        let offImage = UIImage(asset: Asset.disableSpeakerphone)
        let onImage = UIImage(asset: Asset.enableSpeakerphone)

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

251
    lazy var isOutputToSpeaker: Observable<Bool> = { [unowned self] in
252 253 254
        return self.audioService.isOutputToSpeaker.asObservable()
    }()

255
    lazy var speakerSwitchable: Observable<Bool> = { [unowned self] in
256 257 258 259
        return self.audioService.isHeadsetConnected.asObservable()
            .map { value in return !value }
    }()

260
    lazy var audioMuted: Observable<Bool> = { [unowned self] in
261 262
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
263 264 265 266 267 268
                call.state == .current
        }).map({call in
            return call.audioMuted
        })
    }()

269
    lazy var pauseCallButtonState: Observable<UIImage?> = { [unowned self] in
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
270 271 272 273 274 275 276 277 278 279 280
        let unpauseCall = UIImage(asset: Asset.unpauseCall)
        let pauseCall = UIImage(asset: Asset.pauseCall)

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

281
    lazy var callPaused: Observable<Bool> = { [unowned self] in
282 283
        return self.callService.currentCall.filter({ [weak self] call in
            call.callId == self?.call?.callId &&
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
284 285 286 287 288 289 290 291 292 293 294 295
                (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
        })
    }()

296
    var containerViewModel: ButtonsContainerViewModel?
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
297
    let injectionBag: InjectionBag
298

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
299 300 301 302
    required init(with injectionBag: InjectionBag) {
        self.callService = injectionBag.callService
        self.contactsService = injectionBag.contactsService
        self.accountService = injectionBag.accountService
303
        self.videoService = injectionBag.videoService
304
        self.audioService = injectionBag.audioService
305
        self.profileService = injectionBag.profileService
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
306
        self.injectionBag = injectionBag
307 308 309 310 311

        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
312 313 314 315
        }).subscribe(onNext: { [weak self] call in
            self?.videoService
                .setCameraOrientation(orientation: UIDevice.current.orientation,
                                      callID: self?.call?.callId)
316
        }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
317
    }
318

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
319 320 321 322
    static func formattedDurationFrom(interval: Int) -> String {
        let seconds = interval % 60
        let minutes = (interval / 60) % 60
        let hours = (interval / 3600)
323 324 325 326 327 328
        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
329 330 331 332 333 334 335 336
    }

    func cancelCall() {
        guard let call = self.call else {
            return
        }
        self.callService.hangUp(callId: call.callId)
            .subscribe(onCompleted: { [weak self] in
337 338 339
                // 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
340 341 342 343 344 345
                self?.log.info("Call canceled")
                }, onError: { [weak self] error in
                    self?.log.error("Failed to cancel the call")
            }).disposed(by: self.disposeBag)
    }

346
    func answerCall() -> Completable {
347 348 349
        if !self.audioService.isHeadsetConnected.value {
            isAudioOnly ?
                self.audioService.overrideToReceiver() : self.audioService.overrideToSpeaker()
350
        }
351
        return self.callService.accept(call: call)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
352 353
    }

354
    func placeCall(with uri: String, userName: String, isAudioOnly: Bool = false) {
355

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

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
372 373 374
    func respondOnTap() {
        self.screenTapped.onNext(true)
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
375 376 377

    // MARK: call options

378
    func togglePauseCall() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
        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)
        }
    }

399
    func toggleMuteAudio() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
400 401 402 403 404 405 406
        guard let call = self.call else {
            return
        }
        let mute = !call.audioMuted
        self.callService.muteAudio(call: call.callId, mute: mute)
    }

407
    func toggleMuteVideo() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
408 409 410 411 412 413 414 415 416 417
        guard let call = self.call else {
            return
        }
        let mute = !call.videoMuted
        self.callService.muteVideo(call: call.callId, mute: mute)
    }

    func switchCamera() {
        self.videoService.switchCamera()
    }
418 419 420 421

    func switchSpeaker() {
        self.audioService.switchSpeaker()
    }
422 423

    func setCameraOrientation(orientation: UIDeviceOrientation) {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
424 425
        videoService.setCameraOrientation(orientation: orientation,
                                          callID: self.call?.callId)
426
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
427 428 429 430

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