session-ios/Signal/src/call/CallAudioService.swift
Michael Kirk a328759f0d Don't crash when incoming call on NonCallKit iOS10
Previous logic assumed "VoiceChat" mode, but when the ringer goes off,
we set "SoloAmbient" which is incompatible with that mode, causing
assertion failure.

// FREEBIE
2017-02-03 10:24:16 -05:00

205 lines
6.5 KiB
Swift

//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc class CallAudioService: NSObject, CallObserver {
private let TAG = "[CallAudioService]"
private var vibrateTimer: Timer?
private let soundPlayer = JSQSystemSoundPlayer.shared()!
private let handleRinging: Bool
enum SoundFilenames: String {
case incomingRing = "r"
}
// MARK: Vibration config
private let vibrateRepeatDuration = 1.6
// Our ring buzz is a pair of vibrations.
// `pulseDuration` is the small pause between the two vibrations in the pair.
private let pulseDuration = 0.2
// MARK: - Initializers
init(handleRinging: Bool) {
self.handleRinging = handleRinging
}
// MARK: - CallObserver
internal func stateDidChange(call: SignalCall, state: CallState) {
AssertIsOnMainThread()
self.handleState(call:call)
}
internal func muteDidChange(call: SignalCall, isMuted: Bool) {
AssertIsOnMainThread()
Logger.verbose("\(TAG) in \(#function) is no-op")
}
internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) {
AssertIsOnMainThread()
ensureIsEnabled(call: call)
}
internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) {
AssertIsOnMainThread()
ensureIsEnabled(call: call)
}
private func ensureIsEnabled(call: SignalCall) {
// Auto-enable speakerphone when local video is enabled.
if call.hasLocalVideo {
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
mode: AVAudioSessionModeVideoChat,
options: .defaultToSpeaker)
} else if call.isSpeakerphoneEnabled {
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
mode: AVAudioSessionModeVoiceChat,
options: .defaultToSpeaker)
} else {
setAudioSession(category: AVAudioSessionCategoryPlayAndRecord,
mode: AVAudioSessionModeVoiceChat)
}
}
// MARK: - Service action handlers
public func handleState(call: SignalCall) {
assert(Thread.isMainThread)
Logger.verbose("\(TAG) in \(#function) new state: \(call.state)")
switch call.state {
case .idle: handleIdle()
case .dialing: handleDialing()
case .answering: handleAnswering()
case .remoteRinging: handleRemoteRinging()
case .localRinging: handleLocalRinging()
case .connected: handleConnected(call:call)
case .localFailure: handleLocalFailure()
case .localHangup: handleLocalHangup()
case .remoteHangup: handleRemoteHangup()
case .remoteBusy: handleBusy()
}
}
private func handleIdle() {
Logger.debug("\(TAG) \(#function)")
}
private func handleDialing() {
Logger.debug("\(TAG) \(#function)")
}
private func handleAnswering() {
Logger.debug("\(TAG) \(#function)")
stopRinging()
}
private func handleRemoteRinging() {
Logger.debug("\(TAG) \(#function)")
}
private func handleLocalRinging() {
Logger.debug("\(TAG) in \(#function)")
startRinging()
}
private func handleConnected(call: SignalCall) {
Logger.debug("\(TAG) \(#function)")
stopRinging()
// disable start recording to transmit call audio.
ensureIsEnabled(call: call)
}
private func handleLocalFailure() {
Logger.debug("\(TAG) \(#function)")
stopRinging()
}
private func handleLocalHangup() {
Logger.debug("\(TAG) \(#function)")
stopRinging()
}
private func handleRemoteHangup() {
Logger.debug("\(TAG) \(#function)")
stopRinging()
}
private func handleBusy() {
Logger.debug("\(TAG) \(#function)")
stopRinging()
}
// MARK: - Ringing
private func startRinging() {
guard handleRinging else {
Logger.debug("\(TAG) ignoring \(#function) since CallKit handles it's own ringing state")
return
}
vibrateTimer = WeakTimer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, userInfo: nil, repeats: true) {[weak self] _ in
self?.ringVibration()
}
vibrateTimer?.fire()
// Stop other sounds and play ringer through external speaker
setAudioSession(category: AVAudioSessionCategorySoloAmbient)
soundPlayer.playSound(withFilename: SoundFilenames.incomingRing.rawValue, fileExtension: kJSQSystemSoundTypeCAF)
}
private func stopRinging() {
guard handleRinging else {
Logger.debug("\(TAG) ignoring \(#function) since CallKit handles it's own ringing state")
return
}
Logger.debug("\(TAG) in \(#function)")
// Stop vibrating
vibrateTimer?.invalidate()
vibrateTimer = nil
soundPlayer.stopSound(withFilename: SoundFilenames.incomingRing.rawValue)
// Stop solo audio, revert to default.
setAudioSession(category: AVAudioSessionCategoryAmbient)
}
// public so it can be called by timer via selector
public func ringVibration() {
// Since a call notification is more urgent than a message notifaction, we
// vibrate twice, like a pulse, to differentiate from a normal notification vibration.
soundPlayer.playVibrateSound()
DispatchQueue.default.asyncAfter(deadline: DispatchTime.now() + pulseDuration) {
self.soundPlayer.playVibrateSound()
}
}
private func setAudioSession(category: String,
mode: String? = nil,
options: AVAudioSessionCategoryOptions = AVAudioSessionCategoryOptions(rawValue: 0)) {
do {
if #available(iOS 10.0, *), let mode = mode {
try AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options)
Logger.debug("\(self.TAG) set category: \(category) mode: \(mode) options: \(options)")
} else {
try AVAudioSession.sharedInstance().setCategory(category, with: options)
Logger.debug("\(self.TAG) set category: \(category) options: \(options)")
}
} catch {
let message = "\(self.TAG) in \(#function) failed to set category: \(category) with error: \(error)"
assertionFailure(message)
Logger.error(message)
}
}
}