From 5632bd2d83cff73ceb61f2ef388ce8eb53c455ee Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 24 Oct 2018 14:38:05 -0600 Subject: [PATCH 1/3] Use reference counting to disable proximity monitoring after audio message Multiple overlapping activities require proximity monitoring (namely, CallViewController and listening to audio messages). These activities can overlap arbitrarily, so we use a reference counting strategy to keep proximity monitoring on as long as one of these activities is active. --- Signal.xcodeproj/project.pbxproj | 4 + .../ViewControllers/CallViewController.swift | 9 +- SignalMessaging/environment/AppSetup.m | 6 +- SignalMessaging/environment/Environment.h | 10 +- SignalMessaging/environment/Environment.m | 20 ++-- .../environment/OWSAudioSession.swift | 98 +++++++++---------- .../utils/ProximityMonitoringManager.swift | 55 +++++++++++ 7 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 SignalMessaging/utils/ProximityMonitoringManager.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 83be1b97b1..44752a1f7c 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -441,6 +441,7 @@ 4CA5F793211E1F06008C2708 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5F792211E1F06008C2708 /* Toast.swift */; }; 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */; }; 4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; }; + 4CB93DC22180FF07004B9764 /* ProximityMonitoringManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */; }; 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; }; 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; }; @@ -1126,6 +1127,7 @@ 4C9CA25C217E676900607C63 /* ZXingObjC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ZXingObjC.framework; path = ThirdParty/Carthage/Build/iOS/ZXingObjC.framework; sourceTree = ""; }; 4CA5F792211E1F06008C2708 /* Toast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toast.swift; sourceTree = ""; }; 4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = ""; }; + 4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = ""; }; 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = ""; }; 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = ""; }; @@ -1517,6 +1519,7 @@ 45F170D51E315310003FC1F2 /* Weak.swift */, 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */, 4C948FF62146EB4800349F0D /* BlockListCache.swift */, + 4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */, ); path = utils; sourceTree = ""; @@ -3152,6 +3155,7 @@ files = ( 45F59A0A2029140500E8D2B0 /* OWSVideoPlayer.swift in Sources */, 45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */, + 4CB93DC22180FF07004B9764 /* ProximityMonitoringManager.swift in Sources */, 34AC09E1211B39B100997B47 /* SelectThreadViewController.m in Sources */, 34AC09EF211B39B100997B47 /* ViewControllerUtils.m in Sources */, 346941A2215D2EE400B5BFAD /* OWSConversationColor.m in Sources */, diff --git a/Signal/src/ViewControllers/CallViewController.swift b/Signal/src/ViewControllers/CallViewController.swift index 567417bcd0..ff80cc7559 100644 --- a/Signal/src/ViewControllers/CallViewController.swift +++ b/Signal/src/ViewControllers/CallViewController.swift @@ -13,10 +13,15 @@ import SignalMessaging class CallViewController: OWSViewController, CallObserver, CallServiceObserver, CallAudioServiceDelegate { // Dependencies + var callUIAdapter: CallUIAdapter { return AppEnvironment.shared.callService.callUIAdapter } + var proximityMonitoringManager: OWSProximityMonitoringManager { + return Environment.shared.proximityMonitoringManager + } + // Feature Flag @objc public static let kShowCallViewOnSeparateWindow = true @@ -165,6 +170,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) + self.proximityMonitoringManager.remove(lifetime: self) UIDevice.current.isProximityMonitoringEnabled = false callDurationTimer?.invalidate() @@ -173,7 +179,8 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - UIDevice.current.isProximityMonitoringEnabled = true + self.proximityMonitoringManager.add(lifetime: self) + updateCallUI(callState: call.state) self.becomeFirstResponder() diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m index 5ad9cafa41..57e7e19404 100644 --- a/SignalMessaging/environment/AppSetup.m +++ b/SignalMessaging/environment/AppSetup.m @@ -86,12 +86,14 @@ NS_ASSUME_NONNULL_BEGIN OWSAudioSession *audioSession = [OWSAudioSession new]; OWSSounds *sounds = [[OWSSounds alloc] initWithPrimaryStorage:primaryStorage]; + id proximityMonitoringManager = [OWSProximityMonitoringManagerImpl new]; LockInteractionController *lockInteractionController = [[LockInteractionController alloc] initDefault]; OWSWindowManager *windowManager = [[OWSWindowManager alloc] initDefault]; [Environment setShared:[[Environment alloc] initWithAudioSession:audioSession - preferences:preferences - sounds:sounds lockInteractionController:lockInteractionController + preferences:preferences + proximityMonitoringManager:proximityMonitoringManager + sounds:sounds windowManager:windowManager]]; [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithContactsManager:contactsManager diff --git a/SignalMessaging/environment/Environment.h b/SignalMessaging/environment/Environment.h index ee3fadd1fd..2ad9aa44a7 100644 --- a/SignalMessaging/environment/Environment.h +++ b/SignalMessaging/environment/Environment.h @@ -11,6 +11,8 @@ @class OWSSounds; @class OWSWindowManager; +@protocol OWSProximityMonitoringManager; + /** * * Environment is a data and data accessor class. @@ -24,16 +26,18 @@ - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithAudioSession:(OWSAudioSession *)audioSession - preferences:(OWSPreferences *)preferences - sounds:(OWSSounds *)sounds lockInteractionController:(LockInteractionController *)lockInteractionController + preferences:(OWSPreferences *)preferences + proximityMonitoringManager:(id)proximityMonitoringManager + sounds:(OWSSounds *)sounds windowManager:(OWSWindowManager *)windowManager; @property (nonatomic, readonly) OWSAudioSession *audioSession; @property (nonatomic, readonly) OWSContactsManager *contactsManager; +@property (nonatomic, readonly) LockInteractionController *lockInteractionController; +@property (nonatomic, readonly) id proximityMonitoringManager; @property (nonatomic, readonly) OWSPreferences *preferences; @property (nonatomic, readonly) OWSSounds *sounds; -@property (nonatomic, readonly) LockInteractionController *lockInteractionController; @property (nonatomic, readonly) OWSWindowManager *windowManager; @property (class, nonatomic) Environment *shared; diff --git a/SignalMessaging/environment/Environment.m b/SignalMessaging/environment/Environment.m index 0a534c43f1..d6bef08323 100644 --- a/SignalMessaging/environment/Environment.m +++ b/SignalMessaging/environment/Environment.m @@ -13,9 +13,10 @@ static Environment *sharedEnvironment = nil; @property (nonatomic) OWSAudioSession *audioSession; @property (nonatomic) OWSContactsManager *contactsManager; -@property (nonatomic) OWSPreferences *preferences; -@property (nonatomic) OWSSounds *sounds; @property (nonatomic) LockInteractionController *lockInteractionController; +@property (nonatomic) OWSPreferences *preferences; +@property (nonatomic) id proximityMonitoringManager; +@property (nonatomic) OWSSounds *sounds; @property (nonatomic) OWSWindowManager *windowManager; @end @@ -49,9 +50,10 @@ static Environment *sharedEnvironment = nil; } - (instancetype)initWithAudioSession:(OWSAudioSession *)audioSession - preferences:(OWSPreferences *)preferences - sounds:(OWSSounds *)sounds lockInteractionController:(LockInteractionController *)lockInteractionController + preferences:(OWSPreferences *)preferences + proximityMonitoringManager:(id)proximityMonitoringManager + sounds:(OWSSounds *)sounds windowManager:(OWSWindowManager *)windowManager { self = [super init]; @@ -60,15 +62,17 @@ static Environment *sharedEnvironment = nil; } OWSAssertDebug(audioSession); - OWSAssertDebug(preferences); - OWSAssertDebug(sounds); OWSAssertDebug(lockInteractionController); + OWSAssertDebug(preferences); + OWSAssertDebug(proximityMonitoringManager); + OWSAssertDebug(sounds); OWSAssertDebug(windowManager); _audioSession = audioSession; - _preferences = preferences; - _sounds = sounds; _lockInteractionController = lockInteractionController; + _preferences = preferences; + _proximityMonitoringManager = proximityMonitoringManager; + _sounds = sounds; _windowManager = windowManager; OWSSingletonAssert(); diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index ef48685581..91996ebdd4 100644 --- a/SignalMessaging/environment/OWSAudioSession.swift +++ b/SignalMessaging/environment/OWSAudioSession.swift @@ -44,6 +44,10 @@ public class OWSAudioSession: NSObject { // MARK: Dependencies + var proximityMonitoringManager: OWSProximityMonitoringManager { + return Environment.shared.proximityMonitoringManager + } + private let avAudioSession = AVAudioSession.sharedInstance() private let device = UIDevice.current @@ -65,30 +69,7 @@ public class OWSAudioSession: NSObject { self.currentActivities.append(Weak(value: audioActivity)) do { - if aggregateBehaviors.contains(.call) { - // Do nothing while on a call. - // WebRTC/CallAudioService manages call audio - // Eventually it would be nice to consolidate more of the audio - // session handling. - } else { - if aggregateBehaviors.contains(.playAndRecord) { - assert(avAudioSession.recordPermission() == .granted) - try avAudioSession.setCategory(AVAudioSessionCategoryRecord) - } else if aggregateBehaviors.contains(.audioMessagePlayback) { - try ensureCategoryForProximityState() - } else if aggregateBehaviors.contains(.playback) { - try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) - } else { - owsFailDebug("no category option specified. Leaving category untouched.") - } - - if aggregateBehaviors.contains(.audioMessagePlayback) { - self.device.isProximityMonitoringEnabled = true - } else { - self.device.isProximityMonitoringEnabled = false - } - } - + try ensureAudioCategory() return true } catch { owsFailDebug("failed with error: \(error)") @@ -96,29 +77,6 @@ public class OWSAudioSession: NSObject { } } - @objc - func proximitySensorStateDidChange(notification: Notification) { - do { - try ensureCategoryForProximityState() - } catch { - owsFailDebug("error in response to proximity change: \(error)") - } - } - - func ensureCategoryForProximityState() throws { - if aggregateBehaviors.contains(.audioMessagePlayback) { - if self.device.proximityState { - Logger.debug("proximityState: true") - - try avAudioSession.setCategory(AVAudioSessionCategoryPlayAndRecord) - try avAudioSession.overrideOutputAudioPort(.none) - } else { - Logger.debug("proximityState: false") - try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) - } - } - } - @objc public func endAudioActivity(_ audioActivity: AudioActivity) { Logger.debug("with audioActivity: \(audioActivity)") @@ -128,11 +86,51 @@ public class OWSAudioSession: NSObject { currentActivities = currentActivities.filter { return $0.value != audioActivity } do { - try ensureCategoryForProximityState() + try ensureAudioCategory() } catch { - owsFailDebug("error in ensureProximityState: \(error)") + owsFailDebug("error in ensureAudioCategory: \(error)") + } + } + + func ensureAudioCategory() throws { + if aggregateBehaviors.contains(.audioMessagePlayback) { + self.proximityMonitoringManager.add(lifetime: self) + } else { + self.proximityMonitoringManager.remove(lifetime: self) + } + + if aggregateBehaviors.contains(.call) { + // Do nothing while on a call. + // WebRTC/CallAudioService manages call audio + // Eventually it would be nice to consolidate more of the audio + // session handling. + } else if aggregateBehaviors.contains(.playAndRecord) { + assert(avAudioSession.recordPermission() == .granted) + try avAudioSession.setCategory(AVAudioSessionCategoryRecord) + } else if aggregateBehaviors.contains(.audioMessagePlayback) { + if self.device.proximityState { + Logger.debug("proximityState: true") + + try avAudioSession.setCategory(AVAudioSessionCategoryPlayAndRecord) + try avAudioSession.overrideOutputAudioPort(.none) + } else { + Logger.debug("proximityState: false") + try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) + } + } else if aggregateBehaviors.contains(.playback) { + try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) + } else { + ensureAudioSessionActivationStateAfterDelay() + } + } + + @objc + func proximitySensorStateDidChange(notification: Notification) { + do { + try ensureAudioCategory() + } catch { + owsFailDebug("error in response to proximity change: \(error)") } - ensureAudioSessionActivationStateAfterDelay() } fileprivate func ensureAudioSessionActivationStateAfterDelay() { diff --git a/SignalMessaging/utils/ProximityMonitoringManager.swift b/SignalMessaging/utils/ProximityMonitoringManager.swift new file mode 100644 index 0000000000..7d997a74c5 --- /dev/null +++ b/SignalMessaging/utils/ProximityMonitoringManager.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +@objc +public protocol OWSProximityMonitoringManager: class { + func add(lifetime: AnyObject) + func remove(lifetime: AnyObject) +} + +@objc +public class OWSProximityMonitoringManagerImpl: NSObject, OWSProximityMonitoringManager { + var lifetimes: [Weak] = [] + let serialQueue = DispatchQueue(label: "ProximityMonitoringManagerImpl") + + // MARK: + + var device: UIDevice { + return UIDevice.current + } + + // MARK: + + @objc + public func add(lifetime: AnyObject) { + serialQueue.sync { + if !lifetimes.contains { $0.value === lifetime } { + lifetimes.append(Weak(value: lifetime)) + } + reconcile() + } + } + + @objc + public func remove(lifetime: AnyObject) { + serialQueue.sync { + lifetimes = lifetimes.filter { $0.value !== lifetime } + reconcile() + } + } + + func reconcile() { + if _isDebugAssertConfiguration() { + assertOnQueue(serialQueue) + } + lifetimes = lifetimes.filter { $0.value != nil } + if lifetimes.isEmpty { + Logger.debug("disabling proximity monitoring") + device.isProximityMonitoringEnabled = false + } else { + Logger.debug("enabling proximity monitoring for lifetimes: \(lifetimes)") + device.isProximityMonitoringEnabled = true + } + } +} From b0a6d185762c1b056dd8148de04cd279236a7401 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 25 Oct 2018 09:11:38 -0600 Subject: [PATCH 2/3] leave proximity enabled as long as CallViewController exists --- Signal/src/ViewControllers/CallViewController.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Signal/src/ViewControllers/CallViewController.swift b/Signal/src/ViewControllers/CallViewController.swift index ff80cc7559..abf8f6c020 100644 --- a/Signal/src/ViewControllers/CallViewController.swift +++ b/Signal/src/ViewControllers/CallViewController.swift @@ -157,6 +157,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, deinit { NotificationCenter.default.removeObserver(self) + self.proximityMonitoringManager.remove(lifetime: self) } @objc func didBecomeActive() { @@ -170,9 +171,6 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - self.proximityMonitoringManager.remove(lifetime: self) - UIDevice.current.isProximityMonitoringEnabled = false - callDurationTimer?.invalidate() callDurationTimer = nil } From 6968dbab1706046d9b505d5ac25c7d2d0aeaa541 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 25 Oct 2018 09:12:20 -0600 Subject: [PATCH 3/3] Update UIDevice on main thread --- SignalMessaging/utils/ProximityMonitoringManager.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/SignalMessaging/utils/ProximityMonitoringManager.swift b/SignalMessaging/utils/ProximityMonitoringManager.swift index 7d997a74c5..619ae6cab3 100644 --- a/SignalMessaging/utils/ProximityMonitoringManager.swift +++ b/SignalMessaging/utils/ProximityMonitoringManager.swift @@ -46,10 +46,14 @@ public class OWSProximityMonitoringManagerImpl: NSObject, OWSProximityMonitoring lifetimes = lifetimes.filter { $0.value != nil } if lifetimes.isEmpty { Logger.debug("disabling proximity monitoring") - device.isProximityMonitoringEnabled = false + DispatchQueue.main.async { + self.device.isProximityMonitoringEnabled = false + } } else { Logger.debug("enabling proximity monitoring for lifetimes: \(lifetimes)") - device.isProximityMonitoringEnabled = true + DispatchQueue.main.async { + self.device.isProximityMonitoringEnabled = true + } } } }