diff --git a/CHANGES.rst b/CHANGES.rst index aa25f8af2..7e7b2e145 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,7 @@ Improvements: * Privacy: Make clear that device names are publicly readable (#2662). * Widgets: Whitelist [MSC1961](https://github.com/matrix-org/matrix-doc/pull/1961) widget urls. * Settings: CALLS section: Always display the CallKit option but grey it out when not available (only on China). + * VoIP: Fallback to matrix.org STUN server with a confirmation dialog (#2646). Changes in 0.9.2 (2019-08-08) =============================================== diff --git a/Riot/AppDelegate.m b/Riot/AppDelegate.m index 9e6cf8f5b..e6dab7807 100644 --- a/Riot/AppDelegate.m +++ b/Riot/AppDelegate.m @@ -2605,6 +2605,11 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN // Let's call invite be valid for 1 minute mxSession.callManager.inviteLifetime = 60000; + if (RiotSettings.shared.allowStunServerFallback) + { + mxSession.callManager.fallbackSTUNServer = RiotSettings.shared.stunServerFallback; + } + // Setup CallKit if ([MXCallKitAdapter callKitAvailable]) { @@ -3850,6 +3855,12 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN NSString *btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"active_call_details", @"Vector", nil), callViewController.callerNameLabel.text]; [self addCallStatusBar:btnTitle]; } + + if ([callViewController isKindOfClass:[CallViewController class]] + && ((CallViewController*)callViewController).shouldPromptForStunServerFallback) + { + [self promptForStunServerFallback]; + } if (completion) { @@ -3883,6 +3894,52 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN } } +- (void)promptForStunServerFallback +{ + [_errorNotification dismissViewControllerAnimated:NO completion:nil]; + + NSString *stunFallbackHost = RiotSettings.shared.stunServerFallback; + // Remove "stun:" + stunFallbackHost = [stunFallbackHost componentsSeparatedByString:@":"].lastObject; + + MXSession *mainSession = self.mxSessions.firstObject; + NSString *homeServerName = mainSession.matrixRestClient.credentials.homeServerName; + + NSString *message = [NSString stringWithFormat:@"%@\n\n%@", + [NSString stringWithFormat:NSLocalizedStringFromTable(@"call_no_stun_server_error_message_1", @"Vector", nil), homeServerName], + [NSString stringWithFormat: NSLocalizedStringFromTable(@"call_no_stun_server_error_message_2", @"Vector", nil), stunFallbackHost]]; + + _errorNotification = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"call_no_stun_server_error_title", @"Vector", nil) + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + [_errorNotification addAction:[UIAlertAction actionWithTitle:[NSString stringWithFormat: NSLocalizedStringFromTable(@"call_no_stun_server_error_use_fallback_button", @"Vector", nil), stunFallbackHost] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + + RiotSettings.shared.allowStunServerFallback = YES; + mainSession.callManager.fallbackSTUNServer = RiotSettings.shared.stunServerFallback; + + [AppDelegate theDelegate].errorNotification = nil; + }]]; + + [_errorNotification addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"cancel"] + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * action) { + + RiotSettings.shared.allowStunServerFallback = NO; + + [AppDelegate theDelegate].errorNotification = nil; + }]]; + + // Display the error notification + if (!isErrorNotificationSuspended) + { + [_errorNotification mxk_setAccessibilityIdentifier:@"AppDelegateErrorAlert"]; + [self showNotificationAlert:_errorNotification]; + } +} + #pragma mark - Jitsi call - (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video diff --git a/Riot/Assets/Riot-Defaults.plist b/Riot/Assets/Riot-Defaults.plist index ebef1344a..5c360dd63 100644 --- a/Riot/Assets/Riot-Defaults.plist +++ b/Riot/Assets/Riot-Defaults.plist @@ -59,6 +59,8 @@ createConferenceCallsWithJitsi + stunServerFallback + stun:turn.matrix.org enableRageShake maxAllowedMediaCacheSize diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 0451b075f..872ed3340 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -405,6 +405,9 @@ "settings_enable_callkit" = "Integrated calling"; "settings_callkit_info" = "Receive incoming calls on your lock screen. See your Riot calls in the system's call history. If iCloud is enabled, this call history will be shared with Apple."; +"settings_calls_stun_server_fallback_button" = "Allow fallback call assist server"; +"settings_calls_stun_server_fallback_description" = "Allow fallback call assist server %@ when your homeserver does not offer one (your IP address would be shared during a call)."; + "settings_ui_language" = "Language"; "settings_ui_theme" = "Theme"; "settings_ui_theme_auto" = "Auto"; @@ -631,6 +634,11 @@ "call_already_displayed" = "There is already a call in progress."; "call_jitsi_error" = "Failed to join the conference call."; +"call_no_stun_server_error_title" ="Call failed due to misconfigured server"; +"call_no_stun_server_error_message_1" ="Please ask the administrator of your homeserver %@ to configure a TURN server in order for calls to work reliably."; +"call_no_stun_server_error_message_2" ="Alternatively, you can try to use the public server at %@, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings"; +"call_no_stun_server_error_use_fallback_button" = "Try using %@"; + // No VoIP support "no_voip_title" = "Incoming call"; "no_voip" = "%@ is calling you but %@ does not support calls yet.\nYou can ignore this notification and answer the call from another device or you can reject it."; diff --git a/Riot/Generated/RiotDefaults.swift b/Riot/Generated/RiotDefaults.swift index b760f5619..c82a21c44 100644 --- a/Riot/Generated/RiotDefaults.swift +++ b/Riot/Generated/RiotDefaults.swift @@ -39,6 +39,7 @@ internal enum RiotDefaults { internal static let showRedactionsInRoomHistory: Bool = _document["showRedactionsInRoomHistory"] internal static let showUnsupportedEventsInRoomHistory: Bool = _document["showUnsupportedEventsInRoomHistory"] internal static let sortRoomMembersUsingLastSeenTime: Bool = _document["sortRoomMembersUsingLastSeenTime"] + internal static let stunServerFallback: String = _document["stunServerFallback"] internal static let syncLocalContacts: Bool = _document["syncLocalContacts"] internal static let webAppUrl: String = _document["webAppUrl"] internal static let webAppUrlBeta: String = _document["webAppUrlBeta"] diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index c1660708c..72fa0f4df 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -370,6 +370,22 @@ internal enum VectorL10n { internal static var callJitsiError: String { return VectorL10n.tr("Vector", "call_jitsi_error") } + /// Please ask the administrator of your homeserver %@ to configure a TURN server in order for calls to work reliably. + internal static func callNoStunServerErrorMessage1(_ p1: String) -> String { + return VectorL10n.tr("Vector", "call_no_stun_server_error_message_1", p1) + } + /// Alternatively, you can try to use the public server at %@, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings + internal static func callNoStunServerErrorMessage2(_ p1: String) -> String { + return VectorL10n.tr("Vector", "call_no_stun_server_error_message_2", p1) + } + /// Call failed due to misconfigured server + internal static var callNoStunServerErrorTitle: String { + return VectorL10n.tr("Vector", "call_no_stun_server_error_title") + } + /// Try using %@ + internal static func callNoStunServerErrorUseFallbackButton(_ p1: String) -> String { + return VectorL10n.tr("Vector", "call_no_stun_server_error_use_fallback_button", p1) + } /// Camera internal static var camera: String { return VectorL10n.tr("Vector", "camera") @@ -2394,6 +2410,14 @@ internal enum VectorL10n { internal static var settingsCallsSettings: String { return VectorL10n.tr("Vector", "settings_calls_settings") } + /// Allow fallback call assist server + internal static var settingsCallsStunServerFallbackButton: String { + return VectorL10n.tr("Vector", "settings_calls_stun_server_fallback_button") + } + /// Allow fallback call assist server %@ when your homeserver does not offer one (your IP address would be shared during a call). + internal static func settingsCallsStunServerFallbackDescription(_ p1: String) -> String { + return VectorL10n.tr("Vector", "settings_calls_stun_server_fallback_description", p1) + } /// Change password internal static var settingsChangePassword: String { return VectorL10n.tr("Vector", "settings_change_password") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 6db8ec7b3..59c45e6ab 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -30,6 +30,8 @@ final class RiotSettings: NSObject { static let notificationsShowDecryptedContent = "showDecryptedContent" static let pinRoomsWithMissedNotifications = "pinRoomsWithMissedNotif" static let pinRoomsWithUnreadMessages = "pinRoomsWithUnread" + static let allowStunServerFallback = "allowStunServerFallback" + static let stunServerFallback = "stunServerFallback" } /// Riot Standard Room Member Power Level @@ -119,4 +121,24 @@ final class RiotSettings: NSObject { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.createConferenceCallsWithJitsi) } } + + + // MARK: Calls + + /// Indicate if `allowStunServerFallback` settings has been set once. + var isAllowStunServerFallbackHasBeenSetOnce: Bool { + return UserDefaults.standard.object(forKey: UserDefaultsKeys.allowStunServerFallback) != nil + } + + var allowStunServerFallback: Bool { + get { + return UserDefaults.standard.bool(forKey: UserDefaultsKeys.allowStunServerFallback) + } set { + UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.allowStunServerFallback) + } + } + + var stunServerFallback: String? { + return UserDefaults.standard.string(forKey: UserDefaultsKeys.stunServerFallback) + } } diff --git a/Riot/Modules/Call/CallViewController.h b/Riot/Modules/Call/CallViewController.h index 3b46760aa..23fe8b82e 100644 --- a/Riot/Modules/Call/CallViewController.h +++ b/Riot/Modules/Call/CallViewController.h @@ -26,4 +26,7 @@ @property (unsafe_unretained, nonatomic) IBOutlet NSLayoutConstraint *callerImageViewWidthConstraint; +// At the end of call, this flag indicates if the prompt to use the fallback should be displayed +@property (nonatomic) BOOL shouldPromptForStunServerFallback; + @end diff --git a/Riot/Modules/Call/CallViewController.m b/Riot/Modules/Call/CallViewController.m index 7753347a8..c2a1c48ff 100644 --- a/Riot/Modules/Call/CallViewController.m +++ b/Riot/Modules/Call/CallViewController.m @@ -39,6 +39,9 @@ // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; + + // Flag to compute self.shouldPromptForStunServerFallback + BOOL promptForStunServerFallback; } @end @@ -52,6 +55,9 @@ // Setup `MXKViewControllerHandling` properties self.enableBarTintColorStatusChange = NO; self.rageShakeManager = [RageShakeManager sharedManager]; + + promptForStunServerFallback = NO; + _shouldPromptForStunServerFallback = NO; } - (void)viewDidLoad @@ -229,6 +235,13 @@ #pragma mark - MXCallDelegate +- (void)call:(MXCall *)call stateDidChange:(MXCallState)state reason:(MXEvent *)event +{ + [super call:call stateDidChange:state reason:event]; + + [self checkStunServerFallbackWithCallState:state]; +} + - (void)call:(MXCall *)call didEncounterError:(NSError *)error { if ([error.domain isEqualToString:MXEncryptingErrorDomain] @@ -333,6 +346,41 @@ } } + +#pragma mark - Fallback STUN server + +- (void)checkStunServerFallbackWithCallState:(MXCallState)callState +{ + // Detect if we should display the prompt to fallback to the STUN server defined + // in the app plist if the homeserver does not provide STUN or TURN servers. + // We should if the call ends while we were in connecting state + if (!self.mainSession.callManager.turnServers + && !self.mainSession.callManager.fallbackSTUNServer + && !RiotSettings.shared.isAllowStunServerFallbackHasBeenSetOnce) + { + switch (callState) + { + case MXCallStateConnecting: + promptForStunServerFallback = YES; + break; + + case MXCallStateConnected: + promptForStunServerFallback = NO; + break; + + case MXCallStateEnded: + if (promptForStunServerFallback) + { + _shouldPromptForStunServerFallback = YES; + } + + default: + break; + } + } +} + + #pragma mark - Properties - (UIImage*)picturePlaceholder diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 421fa8d03..4131df9cb 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -86,7 +86,9 @@ enum enum { CALLS_ENABLE_CALLKIT_INDEX = 0, - CALLS_DESCRIPTION_INDEX, + CALLS_CALLKIT_DESCRIPTION_INDEX, + CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX, + CALLS_STUN_SERVER_FALLBACK_DESCRIPTION_INDEX, CALLS_COUNT }; @@ -1266,6 +1268,11 @@ SingleImagePickerPresenterDelegate> else if (section == SETTINGS_SECTION_CALLS_INDEX) { count = CALLS_COUNT; + + if (!RiotSettings.shared.stunServerFallback) + { + count -= 2; + } } else if (section == SETTINGS_SECTION_USER_INTERFACE_INDEX) { @@ -1834,7 +1841,7 @@ SingleImagePickerPresenterDelegate> cell = labelAndSwitchCell; } - else if (row == CALLS_DESCRIPTION_INDEX) + else if (row == CALLS_CALLKIT_DESCRIPTION_INDEX) { MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView]; globalInfoCell.textLabel.text = NSLocalizedStringFromTable(@"settings_callkit_info", @"Vector", nil); @@ -1846,6 +1853,30 @@ SingleImagePickerPresenterDelegate> globalInfoCell.textLabel.enabled = NO; } + cell = globalInfoCell; + } + else if (row == CALLS_ENABLE_STUN_SERVER_FALLBACK_INDEX) + { + MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_calls_stun_server_fallback_button", @"Vector", nil); + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.allowStunServerFallback; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + labelAndSwitchCell.mxkSwitch.enabled = YES; + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleStunServerFallback:) forControlEvents:UIControlEventTouchUpInside]; + + cell = labelAndSwitchCell; + } + else if (row == CALLS_STUN_SERVER_FALLBACK_DESCRIPTION_INDEX) + { + NSString *stunFallbackHost = RiotSettings.shared.stunServerFallback; + // Remove "stun:" + stunFallbackHost = [stunFallbackHost componentsSeparatedByString:@":"].lastObject; + + MXKTableViewCell *globalInfoCell = [self getDefaultTableViewCell:tableView]; + globalInfoCell.textLabel.text = [NSString stringWithFormat:NSLocalizedStringFromTable(@"settings_calls_stun_server_fallback_description", @"Vector", nil), stunFallbackHost]; + globalInfoCell.textLabel.numberOfLines = 0; + globalInfoCell.selectionStyle = UITableViewCellSelectionStyleNone; + cell = globalInfoCell; } } @@ -2945,6 +2976,14 @@ SingleImagePickerPresenterDelegate> [MXKAppSettings standardAppSettings].enableCallKit = switchButton.isOn; } +- (void)toggleStunServerFallback:(id)sender +{ + UISwitch *switchButton = (UISwitch*)sender; + RiotSettings.shared.allowStunServerFallback = switchButton.isOn; + + self.mainSession.callManager.fallbackSTUNServer = RiotSettings.shared.allowStunServerFallback ? RiotSettings.shared.stunServerFallback : nil; +} + - (void)toggleShowDecodedContent:(id)sender { UISwitch *switchButton = (UISwitch*)sender;