From a89bde933dd9d587635a7c8cc454cc38706f3d93 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 18 Jan 2017 09:50:22 -0500 Subject: [PATCH 1/7] Respect silent-switch pre-CallKit 8 Cases considered: (Silent Switch toggled vs. Silent Switch not-toggled) x (App in Foreground vs. App in Background) x (CallKit vs. NonCallKit) CallKit already does the "right thing" // FREEBIE --- Signal/src/Signal-Bridging-Header.h | 1 + Signal/src/environment/NotificationsManager.m | 3 ++- .../view controllers/CallViewController.swift | 25 +++++++++++-------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index aadc349fd..95b58ce4e 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -22,6 +22,7 @@ #import "UIFont+OWS.h" #import "UIUtil.h" #import "UIView+OWS.h" +#import #import #import #import diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m index 627e803ff..348036bee 100644 --- a/Signal/src/environment/NotificationsManager.m +++ b/Signal/src/environment/NotificationsManager.m @@ -94,7 +94,8 @@ UILocalNotification *notification = [UILocalNotification new]; notification.category = PushManagerCategoriesIncomingCall; - notification.soundName = @"r.caf"; + // Rather than using notification sounds, we control the ringtone and repeat vibrations with the CallAudioManager. + // notification.soundName = @"r.caf"; NSString *localCallId = call.localId.UUIDString; notification.userInfo = @{ PushManagerUserInfoKeysLocalCallId : localCallId }; diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index c555531c6..d4f3137f6 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -11,6 +11,11 @@ import PromiseKit private let TAG = "[CallAudioService]" private var vibrateTimer: Timer? private let audioManager = AppAudioManager.sharedInstance() + private let soundPlayer = JSQSystemSoundPlayer.shared()! + + enum SoundFilenames: String { + case incomingRing = "r" + } // Mark: Vibration config private let vibrateRepeatDuration = 1.6 @@ -59,9 +64,9 @@ import PromiseKit private func handleLocalRinging() { Logger.debug("\(TAG) \(#function)") - audioManager.setAudioEnabled(true) - audioManager.handleInboundRing() - vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(vibrate), userInfo: nil, repeats: true) + + vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(ringVibration), userInfo: nil, repeats: true) + soundPlayer.playSound(withFilename: SoundFilenames.incomingRing.rawValue, fileExtension: kJSQSystemSoundTypeCAF) } private func handleConnected() { @@ -96,18 +101,18 @@ import PromiseKit // MARK: Helpers private func stopRinging() { - // Disables external speaker used for ringing, unless user enables speakerphone. - audioManager.setDefaultAudioProfile() - audioManager.cancelAllAudio() - vibrateTimer?.invalidate() vibrateTimer = nil + + soundPlayer.stopSound(withFilename: SoundFilenames.incomingRing.rawValue) } - public func vibrate() { - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) + 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) { - AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) + self.soundPlayer.playVibrateSound() } } } From 4374e431a2eefd7f1e444aa82ea9ecc627999ee1 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 18 Jan 2017 11:46:29 -0500 Subject: [PATCH 2/7] Respect silent switch in and out of app. // FREEBIE --- Signal.xcodeproj/project.pbxproj | 6 + Signal/src/call/CallAudioService.swift | 154 ++++++++++++++++++ .../view controllers/CallViewController.swift | 111 ------------- 3 files changed, 160 insertions(+), 111 deletions(-) create mode 100644 Signal/src/call/CallAudioService.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 323df8580..ba9e295dc 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -102,6 +102,8 @@ 45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; }; 45F170AD1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; }; 45F170AF1E2F0393003FC1F2 /* CallAudioSessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */; }; + 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; + 45F170BC1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; 45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */; }; 45F2B1971D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */; }; 45F2B1981D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */; }; @@ -696,6 +698,7 @@ 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = ""; }; 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallAudioSessionTest.swift; path = test/call/CallAudioSessionTest.swift; sourceTree = ""; }; 45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = ""; }; + 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = ""; }; 45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCollectionViewCell.h; sourceTree = ""; }; 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCollectionViewCell.m; sourceTree = ""; }; 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSIncomingMessageCollectionViewCell.xib; sourceTree = ""; }; @@ -1535,6 +1538,7 @@ 458DE9D51DEE3FD00071BB03 /* PeerConnectionClient.swift */, 4574A5D51DD6704700C6B692 /* CallService.swift */, 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */, + 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */, ); path = call; sourceTree = ""; @@ -3182,6 +3186,7 @@ 453D28B71D32BA5F00D523F0 /* OWSDisplayedMessage.m in Sources */, 76EB05DC18170B33006006FC /* StreamPair.m in Sources */, 76EB064618170B33006006FC /* TimeUtil.m in Sources */, + 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */, 70BAFD5D190584BE00FA5E0B /* NotificationTracker.m in Sources */, 76EB05A418170B33006006FC /* PacketHandler.m in Sources */, E197B62118BBF12700F073E5 /* AppAudioManager.m in Sources */, @@ -3272,6 +3277,7 @@ B660F70C1C29988E00687D6E /* StretchFactorController.m in Sources */, B660F70D1C29988E00687D6E /* AnonymousAudioCallbackHandler.m in Sources */, 45F170AD1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */, + 45F170BC1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */, B660F70E1C29988E00687D6E /* RemoteIOAudio.m in Sources */, B660F70F1C29988E00687D6E /* RemoteIOBufferListWrapper.m in Sources */, 456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */, diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift new file mode 100644 index 000000000..f29181374 --- /dev/null +++ b/Signal/src/call/CallAudioService.swift @@ -0,0 +1,154 @@ +// +// Copyright © 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation + +@objc class CallAudioService: NSObject { + private let TAG = "[CallAudioService]" + private var vibrateTimer: Timer? + private let audioManager = AppAudioManager.sharedInstance() + private let soundPlayer = JSQSystemSoundPlayer.shared()! + + 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 + + public var isSpeakerphoneEnabled = false { + didSet { + handleUpdatedSpeakerphone() + } + } + + public func handleState(_ state: CallState) { + switch state { + case .idle: handleIdle() + case .dialing: handleDialing() + case .answering: handleAnswering() + case .remoteRinging: handleRemoteRinging() + case .localRinging: handleLocalRinging() + case .connected: handleConnected() + 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) \(#function)") + + vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(ringVibration), userInfo: nil, repeats: true) + + // Stop other sounds and play ringer through external speaker + setAudioSession(category: AVAudioSessionCategorySoloAmbient) + soundPlayer.playSound(withFilename: SoundFilenames.incomingRing.rawValue, fileExtension: kJSQSystemSoundTypeCAF) + } + + private func handleConnected() { + Logger.debug("\(TAG) \(#function)") + stopRinging() + + // disable start recording to transmit call audio. + setAudioSession(category: AVAudioSessionCategoryPlayAndRecord) + } + + 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() + } + + private func handleUpdatedSpeakerphone() { + // TODO +// let category = AVAudioSession.sharedInstance().getCategory() +// if isSpeakerphoneEnabled { +// AVAudioSession.sharedInstance().setCategory(category, option: AVAudioSessionCategoryOptionDefaultToSpeaker) +// } else { + // Will this disable speaker? +// AVAudioSession.sharedInstance().setCategory(category) +// } + + audioManager.toggleSpeakerPhone(isEnabled: isSpeakerphoneEnabled) + } + + // MARK: Helpers + + private func stopRinging() { + vibrateTimer?.invalidate() + vibrateTimer = nil + soundPlayer.stopSound(withFilename: SoundFilenames.incomingRing.rawValue) + // Stop playing out of speaker + 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, options: AVAudioSessionCategoryOptions) { + do { + 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) + } + } + + private func setAudioSession(category: String) { + do { + try AVAudioSession.sharedInstance().setCategory(category) + Logger.debug("\(self.TAG) set category: \(category)") + } catch { + let message = "\(self.TAG) in \(#function) failed to set category: \(category) with error: \(error)" + assertionFailure(message) + Logger.error(message) + } + } +} diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index d4f3137f6..18653d144 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -6,117 +6,6 @@ import Foundation import WebRTC import PromiseKit -// TODO move this somewhere else. -@objc class CallAudioService: NSObject { - private let TAG = "[CallAudioService]" - private var vibrateTimer: Timer? - private let audioManager = AppAudioManager.sharedInstance() - private let soundPlayer = JSQSystemSoundPlayer.shared()! - - 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 - - public var isSpeakerphoneEnabled = false { - didSet { - handleUpdatedSpeakerphone() - } - } - - public func handleState(_ state: CallState) { - switch state { - case .idle: handleIdle() - case .dialing: handleDialing() - case .answering: handleAnswering() - case .remoteRinging: handleRemoteRinging() - case .localRinging: handleLocalRinging() - case .connected: handleConnected() - 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) \(#function)") - - vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(ringVibration), userInfo: nil, repeats: true) - soundPlayer.playSound(withFilename: SoundFilenames.incomingRing.rawValue, fileExtension: kJSQSystemSoundTypeCAF) - } - - private func handleConnected() { - Logger.debug("\(TAG) \(#function)") - stopRinging() - } - - 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() - } - - private func handleUpdatedSpeakerphone() { - audioManager.toggleSpeakerPhone(isEnabled: isSpeakerphoneEnabled) - } - - // MARK: Helpers - - private func stopRinging() { - vibrateTimer?.invalidate() - vibrateTimer = nil - - soundPlayer.stopSound(withFilename: SoundFilenames.incomingRing.rawValue) - } - - 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() - } - } -} - // TODO: Add category so that button handlers can be defined where button is created. // TODO: Add logic to button handlers. // TODO: Ensure buttons enabled & disabled as necessary. From 4c23b5e23f04d3e8525c5ffcc7ccd631c643d65b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 18 Jan 2017 11:54:13 -0500 Subject: [PATCH 3/7] Remove dependency on AppAudioManager Implement speakerphone toggle directly. Previously we were using AppAudioManager for several things, but this is that last lingering bit. Much of the AppAudioManager code is based on RedPhone calling, so by removing the dependency we pave the way to throw that code away. // FREEBIE --- Signal/src/call/CallAudioService.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index f29181374..7ee45fe64 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -7,7 +7,6 @@ import Foundation @objc class CallAudioService: NSObject { private let TAG = "[CallAudioService]" private var vibrateTimer: Timer? - private let audioManager = AppAudioManager.sharedInstance() private let soundPlayer = JSQSystemSoundPlayer.shared()! enum SoundFilenames: String { @@ -98,16 +97,11 @@ import Foundation } private func handleUpdatedSpeakerphone() { - // TODO -// let category = AVAudioSession.sharedInstance().getCategory() -// if isSpeakerphoneEnabled { -// AVAudioSession.sharedInstance().setCategory(category, option: AVAudioSessionCategoryOptionDefaultToSpeaker) -// } else { - // Will this disable speaker? -// AVAudioSession.sharedInstance().setCategory(category) -// } - - audioManager.toggleSpeakerPhone(isEnabled: isSpeakerphoneEnabled) + if isSpeakerphoneEnabled { + setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, options: .defaultToSpeaker) + } else { + setAudioSession(category: AVAudioSessionCategoryPlayAndRecord) + } } // MARK: Helpers From 3ee94d57d92eba3a9b92facd3ec35ebd014a7026 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 18 Jan 2017 12:31:18 -0500 Subject: [PATCH 4/7] Only NonCallKit adaptee uses manual ringing - stop executing ringer code on CallKit which manages ringing for us. - Organize ringing code more clearly. // FREEBIE --- Signal/src/call/CallAudioService.swift | 49 +++++++++++++++---- Signal/src/call/NonCallKitCallUIAdaptee.swift | 3 ++ .../Speakerbox/CallKitCallUIAdaptee.swift | 5 +- .../call/UserInterface/CallUIAdapter.swift | 6 +++ .../view controllers/CallViewController.swift | 4 +- 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 7ee45fe64..4e80bd1df 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -8,12 +8,13 @@ import Foundation 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 + // MARK: Vibration config private let vibrateRepeatDuration = 1.6 // Our ring buzz is a pair of vibrations. @@ -26,6 +27,14 @@ import Foundation } } + // MARK: - Initializers + + init(handleRinging: Bool) { + self.handleRinging = handleRinging + } + + // MARK: - Service action handlers + public func handleState(_ state: CallState) { switch state { case .idle: handleIdle() @@ -59,13 +68,8 @@ import Foundation } private func handleLocalRinging() { - Logger.debug("\(TAG) \(#function)") - - vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(ringVibration), userInfo: nil, repeats: true) - - // Stop other sounds and play ringer through external speaker - setAudioSession(category: AVAudioSessionCategorySoloAmbient) - soundPlayer.playSound(withFilename: SoundFilenames.incomingRing.rawValue, fileExtension: kJSQSystemSoundTypeCAF) + Logger.debug("\(TAG) in \(#function)") + startRinging() } private func handleConnected() { @@ -104,13 +108,36 @@ import Foundation } } - // MARK: Helpers + // MARK: - Ringing + + private func startRinging() { + guard handleRinging else { + Logger.debug("\(TAG) ignoring \(#function) since CallKit handles it's own ringing state") + return + } + + vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(ringVibration), userInfo: nil, repeats: true) + + // 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 playing out of speaker + + // Stop solo audio, revert to default. setAudioSession(category: AVAudioSessionCategoryAmbient) } @@ -124,6 +151,8 @@ import Foundation } } + // MARK: - AVAudioSession Mgmt + private func setAudioSession(category: String, options: AVAudioSessionCategoryOptions) { do { try AVAudioSession.sharedInstance().setCategory(category, with: options) diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index 3f5a39742..ce0dbe315 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -14,6 +14,9 @@ class NonCallKitCallUIAdaptee: CallUIAdaptee { let notificationsAdapter: CallNotificationsAdapter let callService: CallService + // Starting/Stopping incoming call ringing is our apps responsibility for the non CallKit interface. + let hasManualRinger = true + required init(callService: CallService, notificationsAdapter: CallNotificationsAdapter) { self.callService = callService self.notificationsAdapter = notificationsAdapter diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index 94ec9bb59..a3019ec98 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -23,7 +23,10 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { internal let notificationsAdapter: CallNotificationsAdapter private let provider: CXProvider - /// The app's provider configuration, representing its CallKit capabilities + // CallKit handles incoming ringer stop/start for us. Yay! + let hasManualRinger = false + + // The app's provider configuration, representing its CallKit capabilities static var providerConfiguration: CXProviderConfiguration { let localizedName = NSLocalizedString("APPLICATION_NAME", comment: "Name of application") let providerConfiguration = CXProviderConfiguration(localizedName: localizedName) diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index 05cc90651..dded773a2 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -8,6 +8,7 @@ import CallKit protocol CallUIAdaptee { var notificationsAdapter: CallNotificationsAdapter { get } + var hasManualRinger: Bool { get } func startOutgoingCall(_ call: SignalCall) func reportIncomingCall(_ call: SignalCall, callerName: String) @@ -102,4 +103,9 @@ class CallUIAdapter { internal func setHasVideo(call: SignalCall, hasVideo: Bool) { adaptee.setHasVideo(call: call, hasVideo: hasVideo) } + + // CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually. + internal var hasManualRinger: Bool { + return adaptee.hasManualRinger + } } diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index 18653d144..e66336d3c 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -81,7 +81,7 @@ class CallViewController: UIViewController, CallDelegate { contactsManager = Environment.getCurrent().contactsManager let callService = Environment.getCurrent().callService! callUIAdapter = callService.callUIAdapter - audioService = CallAudioService() + audioService = CallAudioService(handleRinging: callUIAdapter.hasManualRinger) super.init(coder: aDecoder) } @@ -89,7 +89,7 @@ class CallViewController: UIViewController, CallDelegate { contactsManager = Environment.getCurrent().contactsManager let callService = Environment.getCurrent().callService! callUIAdapter = callService.callUIAdapter - audioService = CallAudioService() + audioService = CallAudioService(handleRinging: callUIAdapter.hasManualRinger) super.init(nibName: nil, bundle: nil) } From 87ed662116ebc5078de67f7c165afc547672db02 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 19 Jan 2017 09:38:50 -0500 Subject: [PATCH 5/7] Persist AudioService if CallViewController is dismissed ...in response to CR, move the AudioService off of the CallViewController Adopt multiple observer pattern vs. a singular delegate. Doing so required implementing some machinery to address the ARC (see: Weak.swift) // FREEBIE --- Signal.xcodeproj/project.pbxproj | 4 ++ Signal/src/call/CallAudioService.swift | 45 ++++++++++------ Signal/src/call/CallService.swift | 1 + Signal/src/call/SignalCall.swift | 52 ++++++++++++++++--- .../call/UserInterface/CallUIAdapter.swift | 15 ++++++ Signal/src/util/Weak.swift | 29 +++++++++++ .../view controllers/CallViewController.swift | 24 +++++---- 7 files changed, 139 insertions(+), 31 deletions(-) create mode 100644 Signal/src/util/Weak.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index ba9e295dc..c44ef0761 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -104,6 +104,7 @@ 45F170AF1E2F0393003FC1F2 /* CallAudioSessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */; }; 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; 45F170BC1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; + 45F170D61E315310003FC1F2 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; }; 45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */; }; 45F2B1971D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */; }; 45F2B1981D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */; }; @@ -699,6 +700,7 @@ 45F170AE1E2F0393003FC1F2 /* CallAudioSessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallAudioSessionTest.swift; path = test/call/CallAudioSessionTest.swift; sourceTree = ""; }; 45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = ""; }; 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = ""; }; + 45F170D51E315310003FC1F2 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; 45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCollectionViewCell.h; sourceTree = ""; }; 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCollectionViewCell.m; sourceTree = ""; }; 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSIncomingMessageCollectionViewCell.xib; sourceTree = ""; }; @@ -1948,6 +1950,7 @@ 45CD81F01DC03A22004C9430 /* OWSLogger.h */, 45CD81F11DC03A22004C9430 /* OWSLogger.m */, 450DF2041E0D74AC003D14BE /* Platform.swift */, + 45F170D51E315310003FC1F2 /* Weak.swift */, ); path = util; sourceTree = ""; @@ -3220,6 +3223,7 @@ BFB074C719A5611000F2947C /* FutureUtil.m in Sources */, 45E1F3A51DEF20A100852CF1 /* NoSignalContactsView.swift in Sources */, FCD274E21A5AFD8000202277 /* PrivacySettingsTableViewController.m in Sources */, + 45F170D61E315310003FC1F2 /* Weak.swift in Sources */, 76EB057218170B33006006FC /* RecentCall.m in Sources */, B97CBFA818860EA3008E0DE9 /* CountryCodeViewController.m in Sources */, B6B1013C196D213F007E3930 /* SignalKeyingStorage.m in Sources */, diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 4e80bd1df..70cd7ab95 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -4,7 +4,8 @@ import Foundation -@objc class CallAudioService: NSObject { +@objc class CallAudioService: NSObject, CallObserver { + private let TAG = "[CallAudioService]" private var vibrateTimer: Timer? private let soundPlayer = JSQSystemSoundPlayer.shared()! @@ -21,21 +22,41 @@ import Foundation // `pulseDuration` is the small pause between the two vibrations in the pair. private let pulseDuration = 0.2 - public var isSpeakerphoneEnabled = false { - didSet { - handleUpdatedSpeakerphone() - } - } - // MARK: - Initializers init(handleRinging: Bool) { self.handleRinging = handleRinging } + // MARK: - CallObserver + + internal func stateDidChange(call: SignalCall, state: CallState) { + DispatchQueue.main.async { + self.handleState(state) + } + } + + internal func muteDidChange(call: SignalCall, isMuted: Bool) { + Logger.verbose("\(TAG) in \(#function) is no-op") + } + + internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) { + if isEnabled { + setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, options: .defaultToSpeaker) + } else { + setAudioSession(category: AVAudioSessionCategoryPlayAndRecord) + } + } + + internal func hasVideoDidChange(call: SignalCall, hasVideo: Bool) { + // no-op + } + // MARK: - Service action handlers public func handleState(_ state: CallState) { + Logger.verbose("\(TAG) in \(#function) new state: \(state)") + switch state { case .idle: handleIdle() case .dialing: handleDialing() @@ -100,14 +121,6 @@ import Foundation stopRinging() } - private func handleUpdatedSpeakerphone() { - if isSpeakerphoneEnabled { - setAudioSession(category: AVAudioSessionCategoryPlayAndRecord, options: .defaultToSpeaker) - } else { - setAudioSession(category: AVAudioSessionCategoryPlayAndRecord) - } - } - // MARK: - Ringing private func startRinging() { @@ -151,7 +164,7 @@ import Foundation } } - // MARK: - AVAudioSession Mgmt + // MARK: - AVAudioSession Helpers private func setAudioSession(category: String, options: AVAudioSessionCategoryOptions) { do { diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 3d553d39a..857424b01 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -924,6 +924,7 @@ fileprivate let timeoutSeconds = 60 peerConnectionClient?.terminate() peerConnectionClient = nil + call?.removeAllObservers() call = nil thread = nil incomingCallPromise = nil diff --git a/Signal/src/call/SignalCall.swift b/Signal/src/call/SignalCall.swift index 98bb18b56..22fcabb26 100644 --- a/Signal/src/call/SignalCall.swift +++ b/Signal/src/call/SignalCall.swift @@ -17,10 +17,11 @@ enum CallState: String { case remoteBusy // terminal } -protocol CallDelegate: class { +protocol CallObserver: class { func stateDidChange(call: SignalCall, state: CallState) func hasVideoDidChange(call: SignalCall, hasVideo: Bool) func muteDidChange(call: SignalCall, isMuted: Bool) + func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) } /** @@ -30,7 +31,7 @@ protocol CallDelegate: class { let TAG = "[SignalCall]" - weak var delegate: CallDelegate? + var observers = [Weak]() let remotePhoneNumber: String // Signal Service identifier for this Call. Used to coordinate the call across remote clients. @@ -38,12 +39,16 @@ protocol CallDelegate: class { // Distinguishes between calls locally, e.g. in CallKit let localId: UUID + var hasVideo = false { didSet { Logger.debug("\(TAG) hasVideo changed: \(oldValue) -> \(hasVideo)") - delegate?.hasVideoDidChange(call: self, hasVideo: hasVideo) + for observer in observers { + observer.value?.hasVideoDidChange(call: self, hasVideo: hasVideo) + } } } + var state: CallState { didSet { Logger.debug("\(TAG) state changed: \(oldValue) -> \(state)") @@ -56,20 +61,35 @@ protocol CallDelegate: class { } else { connectedDate = nil } - - delegate?.stateDidChange(call: self, state: state) + for observer in observers { + observer.value?.stateDidChange(call: self, state: state) + } } } + var isMuted = false { didSet { Logger.debug("\(TAG) muted changed: \(oldValue) -> \(isMuted)") - delegate?.muteDidChange(call: self, isMuted: isMuted) + for observer in observers { + observer.value?.muteDidChange(call: self, isMuted: isMuted) + } + } + } + + var isSpeakerphoneEnabled = false { + didSet { + Logger.debug("\(TAG) isSpeakerphoneEnabled changed: \(oldValue) -> \(isSpeakerphoneEnabled)") + for observer in observers { + observer.value?.speakerphoneDidChange(call: self, isEnabled: isSpeakerphoneEnabled) + } } } var connectedDate: NSDate? var error: CallError? + // MARK: Initializers and Factory Methods + init(localId: UUID, signalingId: UInt64, state: CallState, remotePhoneNumber: String) { self.localId = localId self.signalingId = signalingId @@ -85,7 +105,27 @@ protocol CallDelegate: class { return SignalCall(localId: localId, signalingId: signalingId, state: .answering, remotePhoneNumber: remotePhoneNumber) } + // - + + func addObserverAndSyncState(observer: CallObserver) { + observers.append(Weak(value: observer)) + + // Synchronize observer with current call state + observer.stateDidChange(call: self, state: self.state) + } + + func removeObserver(_ observer: CallObserver) { + while let index = observers.index(where: { $0.value === observer }) { + observers.remove(at: index) + } + } + + func removeAllObservers() { + observers = [] + } + // MARK: Equatable + static func == (lhs: SignalCall, rhs: SignalCall) -> Bool { return lhs.localId == rhs.localId } diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index dded773a2..030b82405 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -42,6 +42,7 @@ class CallUIAdapter { let TAG = "[CallUIAdapter]" private let adaptee: CallUIAdaptee private let contactsManager: OWSContactsManager + private let audioService: CallAudioService required init(callService: CallService, contactsManager: OWSContactsManager, notificationsAdapter: CallNotificationsAdapter) { self.contactsManager = contactsManager @@ -58,9 +59,13 @@ class CallUIAdapter { Logger.info("\(TAG) choosing non-callkit adaptee for older iOS") adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) } + + audioService = CallAudioService(handleRinging: adaptee.hasManualRinger) } internal func reportIncomingCall(_ call: SignalCall, thread: TSContactThread) { + call.addObserverAndSyncState(observer: audioService) + let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber) adaptee.reportIncomingCall(call, callerName: callerName) } @@ -72,6 +77,8 @@ class CallUIAdapter { internal func startOutgoingCall(handle: String) -> SignalCall { let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle) + call.addObserverAndSyncState(observer: audioService) + adaptee.startOutgoingCall(call) return call } @@ -97,6 +104,7 @@ class CallUIAdapter { } internal func setIsMuted(call: SignalCall, isMuted: Bool) { + // With CallKit, muting is handled by a CXAction, so it must go through the adaptee adaptee.setIsMuted(call: call, isMuted: isMuted) } @@ -104,6 +112,13 @@ class CallUIAdapter { adaptee.setHasVideo(call: call, hasVideo: hasVideo) } + internal func toggleSpeakerphone(call: SignalCall, isEnabled: Bool) { + // Speakerphone is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the + // adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's + // assigned property. + call.isSpeakerphoneEnabled = isEnabled + } + // CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually. internal var hasManualRinger: Bool { return adaptee.hasManualRinger diff --git a/Signal/src/util/Weak.swift b/Signal/src/util/Weak.swift new file mode 100644 index 000000000..cefa890f6 --- /dev/null +++ b/Signal/src/util/Weak.swift @@ -0,0 +1,29 @@ +// +// Copyright © 2017 Open Whisper Systems. All rights reserved. +// + +/** + * Container for a weakly referenced object. + * + * Only use this for |T| with reference-semantic entities + * e.g. inheriting from AnyObject or Class-only protocols, but not structs or enums. + * + * + * Based on https://devforums.apple.com/message/981472#981472, but also supports class-only protocols + */ +struct Weak { + private weak var _value: AnyObject? + + var value: T? { + get { + return _value as? T + } + set { + _value = newValue as AnyObject + } + } + + init(value: T) { + self.value = value + } +} diff --git a/Signal/src/view controllers/CallViewController.swift b/Signal/src/view controllers/CallViewController.swift index e66336d3c..ce8da785b 100644 --- a/Signal/src/view controllers/CallViewController.swift +++ b/Signal/src/view controllers/CallViewController.swift @@ -10,7 +10,7 @@ import PromiseKit // TODO: Add logic to button handlers. // TODO: Ensure buttons enabled & disabled as necessary. @objc(OWSCallViewController) -class CallViewController: UIViewController, CallDelegate { +class CallViewController: UIViewController, CallObserver { enum CallDirection { case unspecified, outgoing, incoming @@ -22,7 +22,6 @@ class CallViewController: UIViewController, CallDelegate { let callUIAdapter: CallUIAdapter let contactsManager: OWSContactsManager - let audioService: CallAudioService // MARK: Properties @@ -81,7 +80,6 @@ class CallViewController: UIViewController, CallDelegate { contactsManager = Environment.getCurrent().contactsManager let callService = Environment.getCurrent().callService! callUIAdapter = callService.callUIAdapter - audioService = CallAudioService(handleRinging: callUIAdapter.hasManualRinger) super.init(coder: aDecoder) } @@ -89,7 +87,6 @@ class CallViewController: UIViewController, CallDelegate { contactsManager = Environment.getCurrent().contactsManager let callService = Environment.getCurrent().callService! callUIAdapter = callService.callUIAdapter - audioService = CallAudioService(handleRinging: callUIAdapter.hasManualRinger) super.init(nibName: nil, bundle: nil) } @@ -133,8 +130,8 @@ class CallViewController: UIViewController, CallDelegate { // No-op, since call service is already set up at this point, the result of which was presenting this viewController. } - call.delegate = self - stateDidChange(call: call, state: call.state) + // Subscribe for future call updates + call.addObserverAndSyncState(observer: self) } func createViews() { @@ -488,7 +485,11 @@ class CallViewController: UIViewController, CallDelegate { func didPressSpeakerphone(sender speakerphoneButton: UIButton) { Logger.info("\(TAG) called \(#function)") speakerphoneButton.isSelected = !speakerphoneButton.isSelected - audioService.isSpeakerphoneEnabled = speakerphoneButton.isSelected + if let call = self.call { + callUIAdapter.toggleSpeakerphone(call: call, isEnabled: speakerphoneButton.isSelected) + } else { + Logger.warn("\(TAG) pressed mute, but call was unexpectedly nil") + } } func didPressTextMessage(sender speakerphoneButton: UIButton) { @@ -537,14 +538,13 @@ class CallViewController: UIViewController, CallDelegate { self.dismiss(animated: true) } - // MARK: - CallDelegate + // MARK: - CallObserver internal func stateDidChange(call: SignalCall, state: CallState) { DispatchQueue.main.async { Logger.info("\(self.TAG) new call status: \(state)") self.updateCallUI(callState: state) } - self.audioService.handleState(state) } internal func hasVideoDidChange(call: SignalCall, hasVideo: Bool) { @@ -558,4 +558,10 @@ class CallViewController: UIViewController, CallDelegate { self.updateCallUI(callState: call.state) } } + + internal func speakerphoneDidChange(call: SignalCall, isEnabled: Bool) { + DispatchQueue.main.async { + self.updateCallUI(callState: call.state) + } + } } From b2091431dcb967b747964d82333dbbc2161a357e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 19 Jan 2017 10:57:07 -0500 Subject: [PATCH 6/7] Fix retain cycle // FREEBIE --- Signal.xcodeproj/project.pbxproj | 4 +++ Signal/src/call/CallAudioService.swift | 5 ++- Signal/src/util/WeakTimer.swift | 43 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 Signal/src/util/WeakTimer.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c44ef0761..4640ad18f 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -105,6 +105,7 @@ 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; 45F170BC1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; 45F170D61E315310003FC1F2 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; }; + 45F170CC1E310E22003FC1F2 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */; }; 45F2B1941D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */; }; 45F2B1971D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */; }; 45F2B1981D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 45F2B1961D9CA207000D2C69 /* OWSOutgoingMessageCollectionViewCell.xib */; }; @@ -701,6 +702,7 @@ 45F170B31E2F0A6A003FC1F2 /* RTCAudioSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RTCAudioSession.h; sourceTree = ""; }; 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioService.swift; sourceTree = ""; }; 45F170D51E315310003FC1F2 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; + 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakTimer.swift; sourceTree = ""; }; 45F2B1921D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingMessageCollectionViewCell.h; sourceTree = ""; }; 45F2B1931D9C9F48000D2C69 /* OWSOutgoingMessageCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingMessageCollectionViewCell.m; sourceTree = ""; }; 45F2B1951D9CA207000D2C69 /* OWSIncomingMessageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OWSIncomingMessageCollectionViewCell.xib; sourceTree = ""; }; @@ -1951,6 +1953,7 @@ 45CD81F11DC03A22004C9430 /* OWSLogger.m */, 450DF2041E0D74AC003D14BE /* Platform.swift */, 45F170D51E315310003FC1F2 /* Weak.swift */, + 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */, ); path = util; sourceTree = ""; @@ -3138,6 +3141,7 @@ 453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */, 45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */, B68EF9BB1C0B1EBD009C3DCD /* FLAnimatedImageView.m in Sources */, + 45F170CC1E310E22003FC1F2 /* WeakTimer.swift in Sources */, A5E9D4BB1A65FAD800E4481C /* TSVideoAttachmentAdapter.m in Sources */, E197B61118BBEC1A00F073E5 /* AudioProcessor.m in Sources */, FCAC964019FEF99A0046DFC5 /* InboxTableViewCell.m in Sources */, diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 70cd7ab95..9c1d05ee3 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -129,7 +129,10 @@ import Foundation return } - vibrateTimer = Timer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, selector: #selector(ringVibration), userInfo: nil, repeats: true) + vibrateTimer = WeakTimer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, userInfo: nil, repeats: true) {[weak self] timer in + self?.ringVibration() + } + vibrateTimer?.fire() // Stop other sounds and play ringer through external speaker setAudioSession(category: AVAudioSessionCategorySoloAmbient) diff --git a/Signal/src/util/WeakTimer.swift b/Signal/src/util/WeakTimer.swift new file mode 100644 index 000000000..1eea5e153 --- /dev/null +++ b/Signal/src/util/WeakTimer.swift @@ -0,0 +1,43 @@ +// +// Copyright © 2017 Open Whisper Systems. All rights reserved. +// + +/** + * As of iOS10, the timer API's take a block, which makes it easy to reference weak self in Swift. This class offers a + * similar API that works pre iOS10. + * + * Solution modified from + * http://stackoverflow.com/questions/16821736/weak-reference-to-nstimer-target-to-prevent-retain-cycle/41003985#41003985 + */ +final class WeakTimer { + + fileprivate weak var timer: Timer? + fileprivate weak var target: AnyObject? + fileprivate let action: (Timer) -> Void + + fileprivate init(timeInterval: TimeInterval, target: AnyObject, userInfo: Any?, repeats: Bool, action: @escaping (Timer) -> Void) { + self.target = target + self.action = action + self.timer = Timer.scheduledTimer(timeInterval: timeInterval, + target: self, + selector: #selector(fire), + userInfo: userInfo, + repeats: repeats) + } + + class func scheduledTimer(timeInterval: TimeInterval, target: AnyObject, userInfo: Any?, repeats: Bool, action: @escaping (Timer) -> Void) -> Timer { + return WeakTimer(timeInterval: timeInterval, + target: target, + userInfo: userInfo, + repeats: repeats, + action: action).timer! + } + + @objc fileprivate func fire(timer: Timer) { + if target != nil { + action(timer) + } else { + timer.invalidate() + } + } +} From 333fb6c60f6f26ca640f23f90aeb3cab66ec3abf Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Mon, 23 Jan 2017 16:17:55 -0500 Subject: [PATCH 7/7] assert on main thread // FREEBIE --- Signal/src/call/CallAudioService.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 9c1d05ee3..5d500c42f 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -55,6 +55,8 @@ import Foundation // MARK: - Service action handlers public func handleState(_ state: CallState) { + assert(Thread.isMainThread) + Logger.verbose("\(TAG) in \(#function) new state: \(state)") switch state {