From 2a9aa4c852584120c762a3bf32166b77eabc2b27 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 2 Feb 2017 12:21:48 -0500 Subject: [PATCH] users can opt out of CallKit // FREEBIE --- Signal/src/call/CallAudioService.swift | 1 + Signal/src/call/CallService.swift | 24 +++- Signal/src/call/OutboundCallInitiator.swift | 12 +- .../call/UserInterface/CallUIAdapter.swift | 4 +- Signal/src/environment/Environment.m | 2 - .../src/environment/PropertyListPreferences.h | 9 ++ .../src/environment/PropertyListPreferences.m | 41 ++++-- .../AdvancedSettingsTableViewController.m | 127 +++++++++++++----- .../translations/en.lproj/Localizable.strings | 11 +- 9 files changed, 178 insertions(+), 53 deletions(-) diff --git a/Signal/src/call/CallAudioService.swift b/Signal/src/call/CallAudioService.swift index 0751cb277..db214ee83 100644 --- a/Signal/src/call/CallAudioService.swift +++ b/Signal/src/call/CallAudioService.swift @@ -204,6 +204,7 @@ import Foundation } private func setAudioSession(category: String) { + // FIXME Why this default mode? It doesn't work with e.g. "SoloAmbient", causing `AVAudioSession.sharedInstance().setCategory(category, mode: mode, options: options)` to err setAudioSession(category:category, mode:AVAudioSessionModeVoiceChat, options:AVAudioSessionCategoryOptions(rawValue: 0)) diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index d1460913e..f5bfd868f 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -100,9 +100,13 @@ protocol CallServiceObserver: class { // MARK: Dependencies - let accountManager: AccountManager - let messageSender: MessageSender - var callUIAdapter: CallUIAdapter! + private let accountManager: AccountManager + private let messageSender: MessageSender + private let contactsManager: OWSContactsManager + private let notificationsAdapter: CallNotificationsAdapter + + // Exposed by environment.m + internal var callUIAdapter: CallUIAdapter! // MARK: Class @@ -179,11 +183,13 @@ protocol CallServiceObserver: class { required init(accountManager: AccountManager, contactsManager: OWSContactsManager, messageSender: MessageSender, notificationsAdapter: CallNotificationsAdapter) { self.accountManager = accountManager + self.contactsManager = contactsManager self.messageSender = messageSender + self.notificationsAdapter = notificationsAdapter super.init() - self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: contactsManager, notificationsAdapter: notificationsAdapter) + self.createCallUIAdapter() NotificationCenter.default.addObserver(self, selector:#selector(didEnterBackground), @@ -213,6 +219,16 @@ protocol CallServiceObserver: class { self.updateIsVideoEnabled() } + public func createCallUIAdapter() { + AssertIsOnMainThread() + + if self.call != nil { + Logger.warn("\(TAG) ending current call in \(#function). Did user toggle callkit preference while in a call?") + self.terminateCall() + } + self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationsAdapter: self.notificationsAdapter) + } + // MARK: - Class Methods // MARK: Notifications diff --git a/Signal/src/call/OutboundCallInitiator.swift b/Signal/src/call/OutboundCallInitiator.swift index 377e415bc..547357ab5 100644 --- a/Signal/src/call/OutboundCallInitiator.swift +++ b/Signal/src/call/OutboundCallInitiator.swift @@ -10,14 +10,12 @@ import Foundation @objc class OutboundCallInitiator: NSObject { let TAG = "[OutboundCallInitiator]" - let callUIAdapter: CallUIAdapter let redphoneManager: PhoneManager let contactsManager: OWSContactsManager let contactsUpdater: ContactsUpdater - init(redphoneManager: PhoneManager, callUIAdapter: CallUIAdapter, contactsManager: OWSContactsManager, contactsUpdater: ContactsUpdater) { + init(redphoneManager: PhoneManager, contactsManager: OWSContactsManager, contactsUpdater: ContactsUpdater) { self.redphoneManager = redphoneManager - self.callUIAdapter = callUIAdapter self.contactsManager = contactsManager self.contactsUpdater = contactsUpdater @@ -93,6 +91,14 @@ import Foundation } private func initiateWebRTCAudioCall(recipientId: String) -> Bool { + // Rather than an init-assigned dependency property, we access `callUIAdapter` via Environment + // because it can change after app launch due to user settings + guard let callUIAdapter = Environment.getCurrent().callUIAdapter else { + assertionFailure() + Logger.error("\(TAG) can't initiate call because callUIAdapter is nil") + return false + } + callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId) return true } diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index 53a6dda53..e0989c054 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -77,11 +77,11 @@ extension CallUIAdaptee { // So we use the non-CallKit call UI. Logger.info("\(TAG) choosing non-callkit adaptee for simulator.") adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) - } else if #available(iOS 10.0, *) { + } else if #available(iOS 10.0, *), Environment.getCurrent().preferences.isCallKitEnabled() { Logger.info("\(TAG) choosing callkit adaptee for iOS10+") adaptee = CallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) } else { - Logger.info("\(TAG) choosing non-callkit adaptee for older iOS") + Logger.info("\(TAG) choosing non-callkit adaptee") adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) } diff --git a/Signal/src/environment/Environment.m b/Signal/src/environment/Environment.m index 9bda05e69..03543c0a7 100644 --- a/Signal/src/environment/Environment.m +++ b/Signal/src/environment/Environment.m @@ -183,11 +183,9 @@ static Environment *environment = nil; @synchronized (self) { if (!_outboundCallInitiator) { OWSAssert(self.phoneManager); - OWSAssert(self.callUIAdapter); OWSAssert(self.contactsManager); OWSAssert(self.contactsUpdater); _outboundCallInitiator = [[OutboundCallInitiator alloc] initWithRedphoneManager:self.phoneManager - callUIAdapter:self.callUIAdapter contactsManager:self.contactsManager contactsUpdater:self.contactsUpdater]; } diff --git a/Signal/src/environment/PropertyListPreferences.h b/Signal/src/environment/PropertyListPreferences.h index f6aeb7d03..4693a5a88 100644 --- a/Signal/src/environment/PropertyListPreferences.h +++ b/Signal/src/environment/PropertyListPreferences.h @@ -64,9 +64,18 @@ extern NSString *const PropertyListPreferencesKeyEnableDebugLog; - (nullable NSString *)lastRanVersion; - (NSString *)setAndGetCurrentVersion; +#pragma mark - Calling + +#pragma mark WebRTC + - (BOOL)isWebRTCEnabled; - (void)setIsWebRTCEnabled:(BOOL)flag; +#pragma mark Callkit + +- (BOOL)isCallKitEnabled; +- (void)setIsCallKitEnabled:(BOOL)flag; + #pragma mark - Block on Identity Change - (BOOL)shouldBlockOnIdentityChange; diff --git a/Signal/src/environment/PropertyListPreferences.m b/Signal/src/environment/PropertyListPreferences.m index 8d72d7adb..90de1e94d 100644 --- a/Signal/src/environment/PropertyListPreferences.m +++ b/Signal/src/environment/PropertyListPreferences.m @@ -24,6 +24,7 @@ NSString *const PropertyListPreferencesKeyHasRegisteredVoipPush = @"VOIPPushEnab NSString *const PropertyListPreferencesKeyLastRecordedPushToken = @"LastRecordedPushToken"; NSString *const PropertyListPreferencesKeyLastRecordedVoipToken = @"LastRecordedVoipToken"; NSString *const PropertyListPreferencesKeyWebRTCEnabled = @"WebRTCEnabled"; +NSString *const PropertyListPreferencesKeyCallKitEnabled = @"CallKitEnabled"; @implementation PropertyListPreferences @@ -79,13 +80,6 @@ NSString *const PropertyListPreferencesKeyWebRTCEnabled = @"WebRTCEnabled"; return preference ? [preference boolValue] : YES; } -- (BOOL)isWebRTCEnabled -{ - NSNumber *preference = [self tryGetValueForKey:PropertyListPreferencesKeyWebRTCEnabled]; - // Currently default to NO. - return preference ? [preference boolValue] : NO; -} - - (BOOL)getHasSentAMessage { NSNumber *preference = [self tryGetValueForKey:PropertyListPreferencesKeyHasSentAMessage]; @@ -127,10 +121,6 @@ NSString *const PropertyListPreferencesKeyWebRTCEnabled = @"WebRTCEnabled"; [self setValueForKey:PropertyListPreferencesKeyScreenSecurity toValue:@(flag)]; } -- (void)setIsWebRTCEnabled:(BOOL)flag -{ - [self setValueForKey:PropertyListPreferencesKeyWebRTCEnabled toValue:@(flag)]; -} - (void)setHasRegisteredVOIPPush:(BOOL)enabled { @@ -179,6 +169,35 @@ NSString *const PropertyListPreferencesKeyWebRTCEnabled = @"WebRTCEnabled"; return currentVersion; } +#pragma mark - Calling + +#pragma mark WebRTC + +- (BOOL)isWebRTCEnabled +{ + NSNumber *preference = [self tryGetValueForKey:PropertyListPreferencesKeyWebRTCEnabled]; + // Currently default to NO. + return preference ? [preference boolValue] : NO; +} + +- (void)setIsWebRTCEnabled:(BOOL)flag +{ + [self setValueForKey:PropertyListPreferencesKeyWebRTCEnabled toValue:@(flag)]; +} + +#pragma mark CallKit + +- (BOOL)isCallKitEnabled +{ + NSNumber *preference = [self tryGetValueForKey:PropertyListPreferencesKeyCallKitEnabled]; + return preference ? [preference boolValue] : YES; +} + +- (void)setIsCallKitEnabled:(BOOL)flag +{ + [self setValueForKey:PropertyListPreferencesKeyCallKitEnabled toValue:@(flag)]; +} + #pragma mark Notification Preferences - (BOOL)soundInForeground diff --git a/Signal/src/view controllers/AdvancedSettingsTableViewController.m b/Signal/src/view controllers/AdvancedSettingsTableViewController.m index 81e09011c..442507a56 100644 --- a/Signal/src/view controllers/AdvancedSettingsTableViewController.m +++ b/Signal/src/view controllers/AdvancedSettingsTableViewController.m @@ -17,18 +17,26 @@ NS_ASSUME_NONNULL_BEGIN @interface AdvancedSettingsTableViewController () -@property NSArray *sectionsArray; - @property (nonatomic) UITableViewCell *enableWebRTCCell; +@property (nonatomic) UITableViewCell *enableCallKitCell; @property (nonatomic) UITableViewCell *enableLogCell; @property (nonatomic) UITableViewCell *submitLogCell; @property (nonatomic) UITableViewCell *registerPushCell; @property (nonatomic) UISwitch *enableWebRTCSwitch; +@property (nonatomic) UISwitch *enableCallKitSwitch; @property (nonatomic) UISwitch *enableLogSwitch; +@property (nonatomic, readonly) BOOL supportsCallKit; @end +typedef NS_ENUM(NSInteger, AdvancedSettingsTableViewControllerSection) { + AdvancedSettingsTableViewControllerSectionLogging, + AdvancedSettingsTableViewControllerSectionCalling, + AdvancedSettingsTableViewControllerSectionPushNotifications, + AdvancedSettingsTableViewControllerSection_Count // meta section +}; + @implementation AdvancedSettingsTableViewController - (void)viewDidLoad { @@ -37,16 +45,13 @@ NS_ASSUME_NONNULL_BEGIN self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero]; } -- (instancetype)init { - self.sectionsArray = @[ - NSLocalizedString(@"LOGGING_SECTION", nil), - NSLocalizedString(@"PUSH_REGISTER_TITLE", @"Used in table section header and alert view title contexts") - ]; - +- (instancetype)init +{ return [super initWithStyle:UITableViewStyleGrouped]; } -- (void)loadView { +- (void)loadView +{ [super loadView]; self.title = NSLocalizedString(@"SETTINGS_ADVANCED_TITLE", @""); @@ -62,6 +67,16 @@ NS_ASSUME_NONNULL_BEGIN action:@selector(didToggleEnableWebRTCSwitch:) forControlEvents:UIControlEventTouchUpInside]; self.enableWebRTCCell.accessoryView = self.enableWebRTCSwitch; + + // CallKit opt-out + self.enableCallKitCell = [UITableViewCell new]; + self.enableCallKitCell.textLabel.text = NSLocalizedString(@"SETTINGS_ADVANCED_CALLKIT_TITLE", @"Short table cell label"); + self.enableCallKitSwitch = [UISwitch new]; + [self.enableCallKitSwitch setOn:[[Environment getCurrent].preferences isCallKitEnabled]]; + [self.enableCallKitSwitch addTarget:self + action:@selector(didToggleEnableCallKitSwitch:) + forControlEvents:UIControlEventTouchUpInside]; + self.enableCallKitCell.accessoryView = self.enableCallKitSwitch; // Enable Log self.enableLogCell = [[UITableViewCell alloc] init]; @@ -85,14 +100,18 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - return (NSInteger)[self.sectionsArray count]; + return AdvancedSettingsTableViewControllerSection_Count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - switch (section) { - case 0: - return 1 + (self.enableLogSwitch.isOn ? 2 : 1); - case 1: + + AdvancedSettingsTableViewControllerSection settingsSection = (AdvancedSettingsTableViewControllerSection)section; + switch (settingsSection) { + case AdvancedSettingsTableViewControllerSectionLogging: + return self.enableLogSwitch.isOn ? 2 : 1; + case AdvancedSettingsTableViewControllerSectionCalling: + return self.supportsCallKit ? 2 : 1; + case AdvancedSettingsTableViewControllerSectionPushNotifications: return 1; default: return 0; @@ -101,26 +120,61 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - return [self.sectionsArray objectAtIndex:(NSUInteger)section]; + AdvancedSettingsTableViewControllerSection settingsSection = (AdvancedSettingsTableViewControllerSection)section; + switch (settingsSection) { + case AdvancedSettingsTableViewControllerSectionLogging: + return NSLocalizedString(@"LOGGING_SECTION", nil); + case AdvancedSettingsTableViewControllerSectionCalling: + return NSLocalizedString(@"SETTINGS_SECTION_TITLE_CALLING", @"settings topic header for table section"); + case AdvancedSettingsTableViewControllerSectionPushNotifications: + return NSLocalizedString(@"PUSH_REGISTER_TITLE", @"Used in table section header and alert view title contexts"); + default: + return 0; + } } -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - if (indexPath.section == 0) { - switch (indexPath.row) { - case 0: - return self.enableWebRTCCell; - case 1: - return self.enableLogCell; - case 2: - return self.enableLogSwitch.isOn ? self.submitLogCell : self.registerPushCell; - } - } else { - return self.registerPushCell; +- (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section +{ + AdvancedSettingsTableViewControllerSection settingsSection = (AdvancedSettingsTableViewControllerSection)section; + switch (settingsSection) { + case AdvancedSettingsTableViewControllerSectionCalling: + return NSLocalizedString(@"SETTINGS_SECTION_CALL_KIT_DESCRIPTION", @"Settings table section footer."); + default: + return nil; } +} - NSAssert(false, @"No Cell configured"); - - return nil; +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + AdvancedSettingsTableViewControllerSection settingsSection = (AdvancedSettingsTableViewControllerSection)indexPath.section; + switch (settingsSection) { + case AdvancedSettingsTableViewControllerSectionLogging: + switch (indexPath.row) { + case 0: + return self.enableLogCell; + case 1: + OWSAssert(self.enableLogSwitch.isOn); + return self.submitLogCell; + } + case AdvancedSettingsTableViewControllerSectionCalling: + switch (indexPath.row) { + case 0: + return self.enableWebRTCCell; + case 1: + OWSAssert(self.supportsCallKit); + return self.enableCallKitCell; + default: + // Unknown cell + OWSAssert(NO); + return nil; + } + case AdvancedSettingsTableViewControllerSectionPushNotifications: + return self.registerPushCell; + default: + // Unknown section + OWSAssert(NO); + return nil; + } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { @@ -207,6 +261,19 @@ NS_ASSUME_NONNULL_BEGIN [self.tableView reloadData]; } +- (void)didToggleEnableCallKitSwitch:(UISwitch *)sender { + DDLogInfo(@"%@ user toggled call kit preference: %@", self.tag, (sender.isOn ? @"ON" : @"OFF")); + [[Environment getCurrent].preferences setIsCallKitEnabled:sender.isOn]; + [[Environment getCurrent].callService createCallUIAdapter]; +} + +#pragma mark - Util + +- (BOOL)supportsCallKit +{ + return SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(10, 0); +} + #pragma mark - Logging + (NSString *)tag diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index c744c3565..4527c079e 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -717,6 +717,9 @@ /* Navbar title */ "SETTINGS_ABOUT" = "About"; +/* Short table cell label */ +"SETTINGS_ADVANCED_CALLKIT_TITLE" = "Use CallKit"; + /* No comment provided by engineer. */ "SETTINGS_ADVANCED_DEBUGLOG" = "Enable Debug Log"; @@ -727,7 +730,7 @@ "SETTINGS_ADVANCED_TITLE" = "Advanced"; /* This setting is used to switch between new-style WebRTC calling and old-style RedPhone calling. */ -"SETTINGS_ADVANCED_WEBRTC" = "Enable WebRTC Calling"; +"SETTINGS_ADVANCED_WEBRTC" = "Enable Video Calling (Beta)"; /* The message of the alert shown when updates to the WebRTC property fail. */ "SETTINGS_ADVANCED_WEBRTC_FAILED_MESSAGE" = "Could not update your preferences."; @@ -789,6 +792,12 @@ /* No comment provided by engineer. */ "SETTINGS_SCREEN_SECURITY_DETAIL" = "Prevents Signal previews from appearing in the app switcher."; +/* Settings table section footer. */ +"SETTINGS_SECTION_CALL_KIT_DESCRIPTION" = "CallKit allows you to answer calls directly from your lockscreen. Be aware that when using CallKit, Apple syncs some call metadata to your iCloud account."; + +/* settings topic header for table section */ +"SETTINGS_SECTION_TITLE_CALLING" = "Calling"; + /* Section header */ "SETTINGS_SECURITY_TITLE" = "Screen Security";