From 5632bd2d83cff73ceb61f2ef388ce8eb53c455ee Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 24 Oct 2018 14:38:05 -0600 Subject: [PATCH] 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 | 4 +- SignalMessaging/environment/Environment.h | 8 +- SignalMessaging/environment/Environment.m | 12 ++- .../environment/OWSAudioSession.swift | 74 +++++++++---------- .../utils/ProximityMonitoringManager.swift | 55 ++++++++++++++ 7 files changed, 120 insertions(+), 46 deletions(-) create mode 100644 SignalMessaging/utils/ProximityMonitoringManager.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 83be1b97b..44752a1f7 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 567417bcd..ff80cc755 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 5ad9cafa4..57e7e1940 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 + lockInteractionController:lockInteractionController preferences:preferences + proximityMonitoringManager:proximityMonitoringManager sounds:sounds - lockInteractionController:lockInteractionController windowManager:windowManager]]; [SSKEnvironment setShared:[[SSKEnvironment alloc] initWithContactsManager:contactsManager diff --git a/SignalMessaging/environment/Environment.h b/SignalMessaging/environment/Environment.h index ee3fadd1f..2ad9aa44a 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 + lockInteractionController:(LockInteractionController *)lockInteractionController preferences:(OWSPreferences *)preferences + proximityMonitoringManager:(id)proximityMonitoringManager sounds:(OWSSounds *)sounds - lockInteractionController:(LockInteractionController *)lockInteractionController 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 0a534c43f..d6bef0832 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) LockInteractionController *lockInteractionController; @property (nonatomic) OWSPreferences *preferences; +@property (nonatomic) id proximityMonitoringManager; @property (nonatomic) OWSSounds *sounds; -@property (nonatomic) LockInteractionController *lockInteractionController; @property (nonatomic) OWSWindowManager *windowManager; @end @@ -49,9 +50,10 @@ static Environment *sharedEnvironment = nil; } - (instancetype)initWithAudioSession:(OWSAudioSession *)audioSession + lockInteractionController:(LockInteractionController *)lockInteractionController preferences:(OWSPreferences *)preferences + proximityMonitoringManager:(id)proximityMonitoringManager sounds:(OWSSounds *)sounds - lockInteractionController:(LockInteractionController *)lockInteractionController windowManager:(OWSWindowManager *)windowManager { self = [super init]; @@ -60,15 +62,17 @@ static Environment *sharedEnvironment = nil; } OWSAssertDebug(audioSession); + OWSAssertDebug(lockInteractionController); OWSAssertDebug(preferences); + OWSAssertDebug(proximityMonitoringManager); OWSAssertDebug(sounds); - OWSAssertDebug(lockInteractionController); OWSAssertDebug(windowManager); _audioSession = audioSession; + _lockInteractionController = lockInteractionController; _preferences = preferences; + _proximityMonitoringManager = proximityMonitoringManager; _sounds = sounds; - _lockInteractionController = lockInteractionController; _windowManager = windowManager; OWSSingletonAssert(); diff --git a/SignalMessaging/environment/OWSAudioSession.swift b/SignalMessaging/environment/OWSAudioSession.swift index ef4868558..91996ebdd 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)") @@ -97,16 +78,36 @@ public class OWSAudioSession: NSObject { } @objc - func proximitySensorStateDidChange(notification: Notification) { + public func endAudioActivity(_ audioActivity: AudioActivity) { + Logger.debug("with audioActivity: \(audioActivity)") + + objc_sync_enter(self) + defer { objc_sync_exit(self) } + + currentActivities = currentActivities.filter { return $0.value != audioActivity } do { - try ensureCategoryForProximityState() + try ensureAudioCategory() } catch { - owsFailDebug("error in response to proximity change: \(error)") + owsFailDebug("error in ensureAudioCategory: \(error)") } } - func ensureCategoryForProximityState() throws { + 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") @@ -116,23 +117,20 @@ public class OWSAudioSession: NSObject { Logger.debug("proximityState: false") try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) } + } else if aggregateBehaviors.contains(.playback) { + try avAudioSession.setCategory(AVAudioSessionCategoryPlayback) + } else { + ensureAudioSessionActivationStateAfterDelay() } } @objc - public func endAudioActivity(_ audioActivity: AudioActivity) { - Logger.debug("with audioActivity: \(audioActivity)") - - objc_sync_enter(self) - defer { objc_sync_exit(self) } - - currentActivities = currentActivities.filter { return $0.value != audioActivity } + func proximitySensorStateDidChange(notification: Notification) { do { - try ensureCategoryForProximityState() + try ensureAudioCategory() } catch { - owsFailDebug("error in ensureProximityState: \(error)") + 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 000000000..7d997a74c --- /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 + } + } +}