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

import UIKit
import Chameleon
import RxSwift
import Reusable
26
import SwiftyBeaver
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
27 28 29

class CallViewController: UIViewController, StoryboardBased, ViewModelBased {

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
30 31 32 33 34 35 36 37 38 39 40 41 42 43
    //preview screen
    @IBOutlet private weak var profileImageView: UIImageView!
    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var durationLabel: UILabel!
    @IBOutlet private weak var infoBottomLabel: UILabel!

    @IBOutlet private weak var mainView: UIView!

    //video screen
    @IBOutlet private weak var callView: UIView!
    @IBOutlet private weak var incomingVideo: UIImageView!
    @IBOutlet private weak var capturedVideo: UIImageView!
    @IBOutlet private weak var infoContainer: UIView!
    @IBOutlet private weak var callProfileImage: UIImageView!
44
    @IBOutlet private weak var audioOnlyImage: UIImageView!
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
45 46
    @IBOutlet private weak var callNameLabel: UILabel!
    @IBOutlet private weak var callInfoTimerLabel: UILabel!
47 48 49 50
    @IBOutlet private weak var infoLabelTopConstraint: NSLayoutConstraint!
    @IBOutlet private weak var callButtonsLeftConstraint: NSLayoutConstraint!
    @IBOutlet private weak var callButtonsRightConstraint: NSLayoutConstraint!
    @IBOutlet private weak var infoLabelHeightConstraint: NSLayoutConstraint!
51
    @IBOutlet private weak var callPulse: UIView!
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
52

53
    @IBOutlet private weak var buttonsContainer: ButtonsContainerView!
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
54 55 56 57 58

    var viewModel: CallViewModel!

    fileprivate let disposeBag = DisposeBag()

59 60
    private let log = SwiftyBeaver.self

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
61 62
    private var task: DispatchWorkItem?

63 64
    private var shouldRotateScreen = false

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
65 66
    override func viewDidLoad() {
        super.viewDidLoad()
67
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(screenTapped))
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
68 69
        self.mainView.addGestureRecognizer(tapGestureRecognizer)
        self.infoContainer.backgroundColor = UIColor.black.withAlphaComponent(0.3)
70 71 72 73 74 75
        self.setUpCallButtons()
        self.setupBindings()
        if self.viewModel.isAudioOnly {
            self.showAllInfo()
        }
        UIDevice.current.isProximityMonitoringEnabled = self.viewModel.isAudioOnly
76 77 78 79

        initCallAnimation()
    }

80 81 82 83 84
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        UIApplication.shared.statusBarStyle = .lightContent
    }

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    func initCallAnimation() {
        self.callPulse.alpha = 0.5
        self.callPulse.layer.cornerRadius = self.callPulse.frame.size.width / 2
        animateCallCircle()
    }

    func animateCallCircle() {
        self.callPulse.alpha = 0.5
        self.callPulse.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        UIView.animate(withDuration: 1.5, animations: {
            self.callPulse.alpha = 0.0
            self.callPulse.transform = CGAffineTransform(scaleX: 2.0, y: 2.0)
            self.view.layoutIfNeeded()
        }, completion: { [unowned self] _ in
            if self.viewModel.call?.state == .ringing || self.viewModel.call?.state == .connecting {
                self.animateCallCircle()
            }
        })
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
103 104
    }

105 106
    func setUpCallButtons() {
        self.buttonsContainer.viewModel = self.viewModel.containerViewModel
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
107
        //bind actions
108
        self.buttonsContainer.cancelButton.rx.tap
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
109
            .subscribe(onNext: { [weak self] in
110 111 112
                self?.removeFromScreen()
                self?.viewModel.cancelCall()
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
113

114
        self.buttonsContainer.muteAudioButton.rx.tap
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
115
            .subscribe(onNext: { [weak self] in
116
                self?.viewModel.toggleMuteAudio()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
117 118
            }).disposed(by: self.disposeBag)

119 120 121 122 123 124 125 126
        if !(self.viewModel.call?.isAudioOnly ?? false) {
            self.buttonsContainer.muteVideoButton.rx.tap
                .subscribe(onNext: { [weak self] in
                    self?.viewModel.toggleMuteVideo()
                }).disposed(by: self.disposeBag)
        }

        self.buttonsContainer.pauseCallButton.rx.tap
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
127
            .subscribe(onNext: { [weak self] in
128
                self?.viewModel.togglePauseCall()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
129 130
            }).disposed(by: self.disposeBag)

131
        self.buttonsContainer.switchCameraButton.rx.tap
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
132
            .subscribe(onNext: { [weak self] in
133
                self?.viewModel.switchCamera()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
134 135
            }).disposed(by: self.disposeBag)

136
        self.buttonsContainer.switchSpeakerButton.rx.tap
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
137
            .subscribe(onNext: { [weak self] in
138
                self?.viewModel.switchSpeaker()
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
139 140
            }).disposed(by: self.disposeBag)

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
141
        //Data bindings
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
        self.viewModel.videoButtonState
            .observeOn(MainScheduler.instance)
            .bind(to: self.buttonsContainer.muteVideoButton.rx.image())
            .disposed(by: self.disposeBag)

        self.buttonsContainer.muteVideoButton.isEnabled = !(self.viewModel.call?.isAudioOnly ?? false)

        self.viewModel.audioButtonState
            .observeOn(MainScheduler.instance)
            .bind(to: self.buttonsContainer.muteAudioButton.rx.image())
            .disposed(by: self.disposeBag)

        self.viewModel.speakerButtonState
            .observeOn(MainScheduler.instance)
            .bind(to: self.buttonsContainer.switchSpeakerButton.rx.image())
            .disposed(by: self.disposeBag)

        self.viewModel.pauseCallButtonState
            .observeOn(MainScheduler.instance)
            .bind(to: self.buttonsContainer.pauseCallButton.rx.image())
            .disposed(by: self.disposeBag)

164 165 166 167 168 169
        self.viewModel.isActiveVideoCall
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] rotate in
                self?.shouldRotateScreen = rotate
            }).disposed(by: self.disposeBag)

170 171 172 173 174 175
        // disable switch camera button for audio only calls
        self.buttonsContainer.switchCameraButton.isEnabled = !(self.viewModel.isAudioOnly)
    }

    func setupBindings() {

176
        self.viewModel.contactImageData?.asObservable()
177 178
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] dataOrNil in
179 180 181 182 183
                if let imageData = dataOrNil {
                    if let image = UIImage(data: imageData) {
                        self?.profileImageView.image = image
                        self?.callProfileImage.image = image
                    }
184
                }
185
            }).disposed(by: self.disposeBag)
186

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
187 188 189
        self.viewModel.dismisVC
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] dismiss in
190 191 192 193
                if dismiss {
                    self?.removeFromScreen()
                }
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
194

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
195
        self.viewModel.contactName.drive(self.nameLabel.rx.text)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
196 197
            .disposed(by: self.disposeBag)

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
198 199 200 201 202 203 204
        self.viewModel.contactName.drive(self.callNameLabel.rx.text)
            .disposed(by: self.disposeBag)

        self.viewModel.callDuration.drive(self.durationLabel.rx.text)
            .disposed(by: self.disposeBag)

        self.viewModel.callDuration.drive(self.callInfoTimerLabel.rx.text)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
205 206 207 208 209 210
            .disposed(by: self.disposeBag)

        self.viewModel.bottomInfo
            .observeOn(MainScheduler.instance)
            .bind(to: self.infoBottomLabel.rx.text)
            .disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
211 212 213 214

        self.viewModel.incomingFrame
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] frame in
215 216 217 218
                if let image = frame {
                    DispatchQueue.main.async {
                        self?.incomingVideo.image = image
                    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
219
                }
220
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
221 222 223 224

        self.viewModel.capturedFrame
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [weak self] frame in
225 226 227 228
                if let image = frame {
                    DispatchQueue.main.async {
                        self?.capturedVideo.image = image
                    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
229
                }
230
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
231 232

        self.viewModel.showCallOptions
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
233
            .observeOn(MainScheduler.instance)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
234 235 236 237 238
            .subscribe(onNext: { show in
                if show {
                    self.showContactInfo()
                }
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
239

240
        self.viewModel.showCancelOption
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
241
            .observeOn(MainScheduler.instance)
242 243 244 245 246
            .subscribe(onNext: { show in
                if show {
                    self.showCancelButton()
                } else if !self.viewModel.isAudioOnly {
                    self.hideCancelButton()
247 248
                } else {
                    self.buttonsContainer.bottomSpaceConstraint.constant = 30
249 250
                }
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
251 252 253 254 255 256

        self.viewModel.videoMuted
            .observeOn(MainScheduler.instance)
            .bind(to: self.capturedVideo.rx.isHidden)
            .disposed(by: self.disposeBag)

257
        self.audioOnlyImage.isHidden = !self.viewModel.isAudioOnly
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
258 259 260 261 262

        self.viewModel.callPaused
            .observeOn(MainScheduler.instance)
            .bind(to: self.callView.rx.isHidden)
            .disposed(by: self.disposeBag)
263 264 265 266 267 268 269 270
        self.viewModel.callPaused
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { [unowned self] show in
                if show {
                    self.task?.cancel()
                    self.showCallOptions()
                }
            }).disposed(by: self.disposeBag)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
271 272 273
    }

    func removeFromScreen() {
274
        UIDevice.current.isProximityMonitoringEnabled = false
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
275 276
        self.dismiss(animated: false)
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
277

278
    @objc func screenTapped() {
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
279 280 281
        self.viewModel.respondOnTap()
    }

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
    func showCancelButton() {
        self.buttonsContainer.isHidden = false
        self.buttonsContainer.bottomSpaceConstraint.constant = 90
        self.view.layoutIfNeeded()
    }

    func hideCancelButton() {
        self.buttonsContainer.isHidden = true
        self.buttonsContainer.bottomSpaceConstraint.constant = 30
        self.view.layoutIfNeeded()
    }

    func showCallOptions() {
        self.buttonsContainer.isHidden = false
        self.view.layoutIfNeeded()
    }

299 300 301 302 303 304 305 306 307 308 309 310 311
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        let orientation = UIDevice.current.orientation
        switch orientation {
        case .landscapeRight, .landscapeLeft:
            let height = size.height - 150
            self.infoLabelHeightConstraint.constant = height
        default:
           self.infoLabelHeightConstraint.constant = 200
        }
        self.viewModel.setCameraOrientation(orientation: UIDevice.current.orientation)
        super.viewWillTransition(to: size, with: coordinator)
    }

Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
312 313 314 315 316 317
    func showContactInfo() {
        if !self.infoContainer.isHidden {
            task?.cancel()
            self.hideContactInfo()
            return
        }
318 319 320
        self.infoLabelTopConstraint.constant = -200.00
        self.callButtonsRightConstraint.constant = self.view.bounds.width
        self.callButtonsLeftConstraint.constant = -self.view.bounds.width
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
321 322 323 324 325 326
        self.buttonsContainer.isHidden = false
        self.infoContainer.isHidden = false
        self.view.layoutIfNeeded()

        UIView.animate(withDuration: 0.2, delay: 0.0,
                       options: .curveEaseOut,
327 328 329 330 331 332
                       animations: { [unowned self] in
                        self.infoLabelTopConstraint.constant = 0.00
                        self.callButtonsRightConstraint.constant = 0.00
                        self.callButtonsLeftConstraint.constant = 0.00
                        self.view.layoutIfNeeded()
            }, completion: nil)
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
333 334 335 336 337 338 339 340

        task = DispatchWorkItem { self.hideContactInfo() }
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task!)
    }

    func hideContactInfo() {
        UIView.animate(withDuration: 0.2, delay: 0.00,
                       options: .curveEaseOut,
341 342 343 344 345 346 347 348
                       animations: { [unowned self] in
                        self.infoLabelTopConstraint.constant = -200.00
                        self.callButtonsRightConstraint.constant = self.view.bounds.width
                        self.callButtonsLeftConstraint.constant = -self.view.bounds.width
                        self.view.layoutIfNeeded()
            }, completion: { [weak self] _ in
                self?.infoContainer.isHidden = true
                self?.buttonsContainer.isHidden = true
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
349 350
        })
    }
351 352 353 354

    func showAllInfo() {
        self.buttonsContainer.isHidden = false
        self.infoContainer.isHidden = false
355 356 357 358 359 360 361 362 363 364 365 366 367 368
        self.infoLabelTopConstraint.constant = 0.00
    }

    @objc func canRotate() {
        // empty function to support call screen rotation
    }

    override func viewWillDisappear(_ animated: Bool) {
        UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
        super.viewWillDisappear(animated)
    }

    override var shouldAutorotate: Bool {
      return self.shouldRotateScreen
369
    }
Kateryna Kostiuk's avatar
Kateryna Kostiuk committed
370
}