From abb51b565abe97c2856bf35403e8ccf6c010ac4a Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 16 Feb 2018 12:32:29 -0800 Subject: [PATCH] Don't de-activate audio sesion when other audio activities are happening // FREEBIE --- Signal.xcodeproj/project.pbxproj | 6 +- .../ConversationViewController.m | 7 +- Signal/src/call/NonCallKitCallUIAdaptee.swift | 6 +- Signal/src/call/SignalCall.swift | 3 +- .../Speakerbox/CallKitCallUIAdaptee.swift | 11 +-- .../attachments/OWSAudioAttachmentPlayer.m | 10 ++- .../attachments/OWSVideoPlayer.swift | 8 +- .../environment/OWSAudioSession.swift | 79 ++++++++++++++++--- .../util => SignalMessaging/utils}/Weak.swift | 11 ++- 9 files changed, 103 insertions(+), 38 deletions(-) rename {Signal/src/util => SignalMessaging/utils}/Weak.swift (64%) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index bb7f74882..ef74f7690 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -248,6 +248,7 @@ 4523149E1F7E916B003A428C /* SlideOffAnimatedTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149D1F7E916B003A428C /* SlideOffAnimatedTransition.swift */; }; 452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */; }; 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; }; + 452C7CA72037628B003D51A5 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; }; 452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; }; 452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; }; 452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; }; @@ -308,7 +309,6 @@ 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; }; 45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */; }; 45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */; }; - 45F170D61E315310003FC1F2 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170D51E315310003FC1F2 /* Weak.swift */; }; 45F59A082028E4FB00E8D2B0 /* OWSAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* OWSAudioSession.swift */; }; 45F59A0A2029140500E8D2B0 /* OWSVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F59A092029140500E8D2B0 /* OWSVideoPlayer.swift */; }; 45F659731E1BD99C00444429 /* CallKitCallUIAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F659721E1BD99C00444429 /* CallKitCallUIAdaptee.swift */; }; @@ -1138,6 +1138,7 @@ 346129BE1FD2068600532771 /* ThreadUtil.m */, B97940251832BD2400BD66CB /* UIUtil.h */, B97940261832BD2400BD66CB /* UIUtil.m */, + 45F170D51E315310003FC1F2 /* Weak.swift */, 346129751FD1E0B500532771 /* WeakTimer.swift */, ); path = utils; @@ -1756,7 +1757,6 @@ FCFA64B11A24F29E0007FB87 /* UI Categories */, FCC81A961A44558300DFEC7D /* UIDevice+TSHardwareVersion.h */, FCC81A971A44558300DFEC7D /* UIDevice+TSHardwareVersion.m */, - 45F170D51E315310003FC1F2 /* Weak.swift */, ); path = util; sourceTree = ""; @@ -2803,6 +2803,7 @@ 3478506B1FD9B78A007B8332 /* NoopCallMessageHandler.swift in Sources */, 451F8A3D1FD713CA005CB9DA /* ThreadViewHelper.m in Sources */, 346129AD1FD1F34E00532771 /* ImageCache.swift in Sources */, + 452C7CA72037628B003D51A5 /* Weak.swift in Sources */, 451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */, 346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */, 346129FE1FD5F31400532771 /* OWS106EnsureProfileComplete.swift in Sources */, @@ -2977,7 +2978,6 @@ 4574A5D61DD6704700C6B692 /* CallService.swift in Sources */, 34B3F8721E8DF1700035BE1A /* AdvancedSettingsTableViewController.m in Sources */, 3461299C1FD1EA9E00532771 /* NotificationsManager.m in Sources */, - 45F170D61E315310003FC1F2 /* Weak.swift in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 34B3F8891E8DF1700035BE1A /* OWSConversationSettingsViewController.m in Sources */, 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */, diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 2bf639e2e..d12987c82 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -151,6 +151,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic) TSThread *thread; @property (nonatomic) YapDatabaseConnection *editingDatabaseConnection; +@property (nonatomic, readonly) AudioActivity *voiceNoteAudioActivity; // These two properties must be updated in lockstep. // @@ -277,6 +278,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { _networkManager = [TSNetworkManager sharedManager]; _blockingManager = [OWSBlockingManager sharedManager]; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; + NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ voice note", self.logTag]; + _voiceNoteAudioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; } - (void)addNotificationListeners @@ -3173,7 +3176,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { NSURL *fileURL = [NSURL fileURLWithPath:filepath]; // Setup audio session - BOOL configuredAudio = [OWSAudioSession.shared setRecordCategory]; + BOOL configuredAudio = [OWSAudioSession.shared setRecordCategoryWithAudioActivity:self.voiceNoteAudioActivity]; if (!configuredAudio) { OWSFail(@"%@ Couldn't configure audio session", self.logTag); [self cancelVoiceMemo]; @@ -3278,7 +3281,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)stopRecording { [self.audioRecorder stop]; - [OWSAudioSession.shared endAudioActivity]; + [OWSAudioSession.shared endAudioActivity:self.voiceNoteAudioActivity]; } - (void)cancelRecordingVoiceMemo diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index b7848d611..2b2700714 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -15,6 +15,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { let notificationsAdapter: CallNotificationsAdapter let callService: CallService + let audioActivity: AudioActivity // Starting/Stopping incoming call ringing is our apps responsibility for the non CallKit interface. let hasManualRinger = true @@ -24,6 +25,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { self.callService = callService self.notificationsAdapter = notificationsAdapter + self.audioActivity = AudioActivity(audioDescription: "[NonCallKitCallUIAdaptee]") super.init() @@ -89,7 +91,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { return } - OWSAudioSession.shared.isRTCAudioEnabled = true + OWSAudioSession.shared.enableRTCAudio(audioActivity: audioActivity) self.callService.handleAnswerCall(call) } @@ -123,7 +125,7 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { func recipientAcceptedCall(_ call: SignalCall) { AssertIsOnMainThread() - OWSAudioSession.shared.isRTCAudioEnabled = true + OWSAudioSession.shared.enableRTCAudio(audioActivity: audioActivity) } func localHangupCall(_ call: SignalCall) { diff --git a/Signal/src/call/SignalCall.swift b/Signal/src/call/SignalCall.swift index 49084000b..8121fa48a 100644 --- a/Signal/src/call/SignalCall.swift +++ b/Signal/src/call/SignalCall.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @@ -148,7 +148,6 @@ protocol CallObserver: class { self.signalingId = signalingId self.state = state self.remotePhoneNumber = remotePhoneNumber - self.thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber) } diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index 9226070ef..4bf768a9e 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -25,6 +25,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { internal let notificationsAdapter: CallNotificationsAdapter internal let contactsManager: OWSContactsManager private let provider: CXProvider + private let audioActivity: AudioActivity // CallKit handles incoming ringer stop/start for us. Yay! let hasManualRinger = false @@ -60,6 +61,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { self.contactsManager = contactsManager self.notificationsAdapter = notificationsAdapter self.provider = CXProvider(configuration: type(of: self).providerConfiguration) + self.audioActivity = AudioActivity(audioDescription: "CallKitCallUIAdaptee audio") super.init() @@ -343,16 +345,15 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { AssertIsOnMainThread() - Logger.debug("\(TAG) Received \(#function)") - - OWSAudioSession.shared.isRTCAudioEnabled = true + Logger.debug("\(TAG) in \(#function)") + OWSAudioSession.shared.enableRTCAudio(audioActivity: audioActivity) } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { AssertIsOnMainThread() - Logger.debug("\(TAG) Received \(#function)") - OWSAudioSession.shared.isRTCAudioEnabled = false + Logger.debug("\(TAG) in \(#function)") + OWSAudioSession.shared.disableRTCAudio(audioActivity: audioActivity) } // MARK: - Util diff --git a/SignalMessaging/attachments/OWSAudioAttachmentPlayer.m b/SignalMessaging/attachments/OWSAudioAttachmentPlayer.m index 1e440fa08..4f5d723c2 100644 --- a/SignalMessaging/attachments/OWSAudioAttachmentPlayer.m +++ b/SignalMessaging/attachments/OWSAudioAttachmentPlayer.m @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSURL *mediaUrl; @property (nonatomic, nullable) AVAudioPlayer *audioPlayer; @property (nonatomic, nullable) NSTimer *audioPlayerPoller; +@property (nonatomic, readonly) AudioActivity *audioActivity; @end @@ -35,6 +36,9 @@ NS_ASSUME_NONNULL_BEGIN _delegate = delegate; _mediaUrl = mediaUrl; + NSString *audioActivityDescription = [NSString stringWithFormat:@"%@ %@", self.logTag, self.mediaUrl]; + _audioActivity = [[AudioActivity alloc] initWithAudioDescription:audioActivityDescription]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:OWSApplicationDidEnterBackgroundNotification @@ -65,7 +69,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.mediaUrl); OWSAssert([self.delegate audioPlaybackState] != AudioPlaybackState_Playing); - [OWSAudioSession.shared setPlaybackCategory]; + [OWSAudioSession.shared setPlaybackCategoryWithAudioActivity:self.audioActivity]; [self.audioPlayerPoller invalidate]; @@ -111,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN [self.audioPlayerPoller invalidate]; [self.delegate setAudioProgress:[self.audioPlayer currentTime] duration:[self.audioPlayer duration]]; - [OWSAudioSession.shared endAudioActivity]; + [OWSAudioSession.shared endAudioActivity:self.audioActivity]; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; } @@ -124,7 +128,7 @@ NS_ASSUME_NONNULL_BEGIN [self.audioPlayerPoller invalidate]; [self.delegate setAudioProgress:0 duration:0]; - [OWSAudioSession.shared endAudioActivity]; + [OWSAudioSession.shared endAudioActivity:self.audioActivity]; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; } diff --git a/SignalMessaging/attachments/OWSVideoPlayer.swift b/SignalMessaging/attachments/OWSVideoPlayer.swift index 4134fb5df..e781a18db 100644 --- a/SignalMessaging/attachments/OWSVideoPlayer.swift +++ b/SignalMessaging/attachments/OWSVideoPlayer.swift @@ -15,12 +15,14 @@ protocol OWSVideoPlayerDelegate: class { public class OWSVideoPlayer: NSObject { let avPlayer: AVPlayer + let audioActivity: AudioActivity weak var delegate: OWSVideoPlayerDelegate? @available(iOS 9.0, *) init(url: URL) { self.avPlayer = AVPlayer(url: url) + self.audioActivity = AudioActivity(audioDescription: "[OWSVideoPlayer] url:\(url)") super.init() @@ -35,12 +37,12 @@ public class OWSVideoPlayer: NSObject { @available(iOS 9.0, *) public func pause() { avPlayer.pause() - OWSAudioSession.shared.endAudioActivity() + OWSAudioSession.shared.endAudioActivity(self.audioActivity) } @available(iOS 9.0, *) public func play() { - OWSAudioSession.shared.setPlaybackCategory() + OWSAudioSession.shared.setPlaybackCategory(audioActivity: self.audioActivity) guard let item = avPlayer.currentItem else { owsFail("\(logTag) video player item was unexpectedly nil") @@ -67,6 +69,6 @@ public class OWSVideoPlayer: NSObject { @available(iOS 9.0, *) private func playerItemDidPlayToCompletion(_ notification: Notification) { self.delegate?.videoPlayerDidPlayToCompletion(self) - OWSAudioSession.shared.endAudioActivity() + OWSAudioSession.shared.endAudioActivity(self.audioActivity) } } diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index 685b9243c..b27dcfe70 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -5,6 +5,24 @@ import Foundation import WebRTC +@objc +public class AudioActivity: NSObject { + let audioDescription: String + + override public var description: String { + return "<\(self.logTag) audioDescription: \"\(audioDescription)\">" + } + + public + init(audioDescription: String) { + self.audioDescription = audioDescription + } + + deinit { + OWSAudioSession.shared.ensureAudioSessionActivationState() + } +} + @objc public class OWSAudioSession: NSObject { @@ -13,14 +31,18 @@ public class OWSAudioSession: NSObject { private override init() {} private let avAudioSession = AVAudioSession.sharedInstance() + private var currentActivities: [Weak] = [] + // Ignores hardware mute switch, plays through external speaker - public func setPlaybackCategory() { + public func setPlaybackCategory(audioActivity: AudioActivity) { Logger.debug("\(logTag) in \(#function)") // In general, we should have put the audio session back to it's default // category when we were done with whatever activity required it to be modified assert(avAudioSession.category == AVAudioSessionCategorySoloAmbient) + startAudioActivity(audioActivity) + do { try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) } catch { @@ -28,7 +50,7 @@ public class OWSAudioSession: NSObject { } } - public func setRecordCategory() -> Bool { + public func setRecordCategory(audioActivity: AudioActivity) -> Bool { Logger.debug("\(logTag) in \(#function)") // In general, we should have put the audio session back to it's default @@ -37,6 +59,8 @@ public class OWSAudioSession: NSObject { assert(avAudioSession.recordPermission() == .granted) + startAudioActivity(audioActivity) + do { try avAudioSession.setCategory(AVAudioSessionCategoryRecord) return true @@ -46,8 +70,38 @@ public class OWSAudioSession: NSObject { } } - public func endAudioActivity() { - Logger.debug("\(logTag) in \(#function)") + private func startAudioActivity(_ audioActivity: AudioActivity) { + Logger.debug("\(logTag) in \(#function) with \(audioActivity)") + + self.currentActivities.append(Weak(value: audioActivity)) + } + + public func endAudioActivity(_ audioActivity: AudioActivity) { + Logger.debug("\(logTag) in \(#function) with audioActivity: \(audioActivity)") + + currentActivities = currentActivities.filter { return $0.value != audioActivity } + ensureAudioSessionActivationState() + } + + fileprivate func ensureAudioSessionActivationState() { + // Cull any stale activities + currentActivities = currentActivities.flatMap { oldActivity in + guard oldActivity.value != nil else { + // Normally we should be explicitly stopping an audio activity, but this allows + // for recovery if the owner of the AudioAcivity was GC'd without ending it's + // audio activity + Logger.warn("\(logTag) an old activity has been gc'd") + return nil + } + + // return any still-active activities + return oldActivity + } + + guard currentActivities.count == 0 else { + Logger.debug("\(logTag) not deactivating due to currentActivities: \(currentActivities)") + return + } do { try avAudioSession.setCategory(AVAudioSessionCategorySoloAmbient) @@ -88,14 +142,15 @@ public class OWSAudioSession: NSObject { /** * Because we useManualAudio with our RTCAudioSession, we have to start/stop the recording audio session ourselves. - * See header for details on manual audio. + * See "WebRTC Audio" comment for details on manual audio. */ - public var isRTCAudioEnabled: Bool { - get { - return rtcAudioSession.isAudioEnabled - } - set { - rtcAudioSession.isAudioEnabled = newValue - } + public func enableRTCAudio(audioActivity: AudioActivity) { + startAudioActivity(audioActivity) + rtcAudioSession.isAudioEnabled = true + } + + public func disableRTCAudio(audioActivity: AudioActivity) { + rtcAudioSession.isAudioEnabled = false + endAudioActivity(audioActivity) } } diff --git a/Signal/src/util/Weak.swift b/SignalMessaging/utils/Weak.swift similarity index 64% rename from Signal/src/util/Weak.swift rename to SignalMessaging/utils/Weak.swift index cefa890f6..fbcca5906 100644 --- a/Signal/src/util/Weak.swift +++ b/SignalMessaging/utils/Weak.swift @@ -1,20 +1,19 @@ // -// Copyright © 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 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. - * + * That is - should inherit 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 { +public struct Weak { private weak var _value: AnyObject? - var value: T? { + public var value: T? { get { return _value as? T } @@ -23,7 +22,7 @@ struct Weak { } } - init(value: T) { + public init(value: T) { self.value = value } }