From bbfd9ba74ddee346b707e0d05ad4b17fec442a60 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 1 Feb 2017 17:49:32 -0500 Subject: [PATCH] Place Signal/Redphone calls from system contacts // FREEBIE --- Signal.xcodeproj/project.pbxproj | 10 ++ Signal/src/AppDelegate.m | 47 +++++++++- Signal/src/Signal-Bridging-Header.h | 4 +- Signal/src/call/OutboundCallInitiator.swift | 94 +++++++++++++++++++ Signal/src/environment/Environment.h | 4 + Signal/src/environment/Environment.m | 4 + .../view controllers/MessagesViewController.m | 6 +- 7 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 Signal/src/call/OutboundCallInitiator.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index bdb2b9f79..982e44b8c 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 451DE9FD1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */; }; 451DE9FE1DC1A28200810E42 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */; }; 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; }; + 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; }; + 452C46901E427E200087B011 /* OutboundCallInitiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */; }; 452D1EE81DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */; }; 452E3C8E1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 452E3C8D1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m */; }; 452E3C8F1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 452E3C8D1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m */; }; @@ -62,6 +64,7 @@ 45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; }; 45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; }; 45843D221D223BA10013E85A /* OWSContactsSearcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D211D223BA10013E85A /* OWSContactsSearcherTest.m */; }; + 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 45847E861E4283C30080EAB3 /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 45855F371D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */; }; 45855F381D9498A40084F340 /* OWSContactAvatarBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */; }; 458967111DC117CC00E9DD21 /* AccountManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458967101DC117CC00E9DD21 /* AccountManagerTest.swift */; }; @@ -620,6 +623,7 @@ 451DE9FC1DC1A28200810E42 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SyncPushTokensJob.swift; path = Models/SyncPushTokensJob.swift; sourceTree = ""; }; 4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; 4526BD481CA61C8D00166BC8 /* OWSMessageEditing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageEditing.h; sourceTree = ""; }; + 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutboundCallInitiator.swift; sourceTree = ""; }; 452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = ""; }; 452E3C8C1D935C77002A45B0 /* OWSConversationSettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsTableViewController.h; sourceTree = ""; }; 452E3C8D1D935C77002A45B0 /* OWSConversationSettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OWSConversationSettingsTableViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; @@ -656,6 +660,7 @@ 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSearcher.h; sourceTree = ""; }; 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcher.m; sourceTree = ""; }; 45843D211D223BA10013E85A /* OWSContactsSearcherTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSearcherTest.m; sourceTree = ""; }; + 45847E861E4283C30080EAB3 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; 45855F351D9498A40084F340 /* OWSContactAvatarBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactAvatarBuilder.h; sourceTree = ""; }; 45855F361D9498A40084F340 /* OWSContactAvatarBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactAvatarBuilder.m; sourceTree = ""; }; 4589670F1DC117CC00E9DD21 /* SignalTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SignalTests-Bridging-Header.h"; sourceTree = ""; }; @@ -1242,6 +1247,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 45847E871E4283C30080EAB3 /* Intents.framework in Frameworks */, 4509E79A1DD653700025A59F /* WebRTC.framework in Frameworks */, 456C38961DC7B882007536A7 /* PromiseKit.framework in Frameworks */, 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */, @@ -1555,6 +1561,7 @@ 4574A5D51DD6704700C6B692 /* CallService.swift */, 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */, 45F170BA1E2FC5D3003FC1F2 /* CallAudioService.swift */, + 452C468E1E427E200087B011 /* OutboundCallInitiator.swift */, ); path = call; sourceTree = ""; @@ -2430,6 +2437,7 @@ D221A08C169C9E5E00537ABF /* Frameworks */ = { isa = PBXGroup; children = ( + 45847E861E4283C30080EAB3 /* Intents.framework */, 45BD60811DE9547E00A8F436 /* Contacts.framework */, 4509E7991DD653700025A59F /* WebRTC.framework */, 451DE9F11DC1585F00810E42 /* PromiseKit.framework */, @@ -3199,6 +3207,7 @@ B66B9F721AEA6D1100E2E609 /* NotificationSettingsViewController.m in Sources */, 76EB059018170B33006006FC /* IgnoredPacketFailure.m in Sources */, 76EB05D418170B33006006FC /* ZrtpManager.m in Sources */, + 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */, 76EB058E18170B33006006FC /* HostNameEndPoint.m in Sources */, E19167A418A9687800B7A468 /* DH3KKeyAgreementParticipant.m in Sources */, E16E5BF018AAC40200B7C403 /* EvpKeyAgreement.m in Sources */, @@ -3309,6 +3318,7 @@ B660F7131C29988E00687D6E /* SoundPlayer.m in Sources */, B660F7141C29988E00687D6E /* RecentCall.m in Sources */, B660F7151C29988E00687D6E /* RecentCallManager.m in Sources */, + 452C46901E427E200087B011 /* OutboundCallInitiator.swift in Sources */, 451DE9F81DC18C9500810E42 /* AccountManager.swift in Sources */, B660F7161C29988E00687D6E /* GroupContactsResult.m in Sources */, B660F7171C29988E00687D6E /* OWSContactsManager.m in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index fa7c71317..358020517 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -31,6 +31,7 @@ #import @import WebRTC; +@import Intents; NSString *const AppDelegateStoryboardMain = @"Main"; NSString *const AppDelegateStoryboardRegistration = @"Registration"; @@ -333,10 +334,52 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray * _Nullable))restorationHandler { if ([userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) { + DDLogInfo(@"%@ got start video call intent", self.tag); [[Environment getCurrent].callService handleCallKitStartVideo]; + } else if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]) { + + // TODO guard if less than iOS10. + + DDLogInfo(@"%@ got start audio call intent", self.tag); + + // hoooooooooooly moly. + INInteraction *interaction = [userActivity interaction]; + INIntent *intent = interaction.intent; + + if (![intent isKindOfClass:[INStartAudioCallIntent class]]) { + DDLogError(@"%@ unexpected class for start call audio: %@", self.tag, intent); + return NO; + } + INStartAudioCallIntent *startCallIntent = (INStartAudioCallIntent *)intent; + NSString *_Nullable handle = startCallIntent.contacts.firstObject.personHandle.value; + if (!handle) { + DDLogWarn(@"%@ unable to find handle in startCallIntent: %@", self.tag, startCallIntent); + return NO; + } + + CallUIAdapter *callUIAdapter = [Environment getCurrent].callService.callUIAdapter; + OWSAssert(callUIAdapter); + + ContactsUpdater *contactsUpdater = [Environment getCurrent].contactsUpdater; + OWSAssert(contactsUpdater); + + OWSContactsManager *contactsManager = [Environment getCurrent].contactsManager; + OWSAssert(contactsManager); + + PhoneManager *phoneManager = [Environment getCurrent].phoneManager; + OWSAssert(phoneManager); + + OutboundCallInitiator *outboundCallInitiator = + [[OutboundCallInitiator alloc] initWithRedphoneManager:phoneManager + callUIAdapter:callUIAdapter + contactsManager:contactsManager + contactsUpdater:contactsUpdater]; + return [outboundCallInitiator initiateCallWithHandle:handle]; } else { - DDLogWarn( - @"%@ called %s with userActivity: %@, but not yet supported.", self.tag, __PRETTY_FUNCTION__, userActivity); + DDLogWarn(@"%@ called %s with userActivity: %@, but not yet supported.", + self.tag, + __PRETTY_FUNCTION__, + userActivity.activityType); } // TODO Something like... diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 0e314e27b..9dcf0dadb 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -16,7 +16,7 @@ #import "OWSError.h" #import "OWSLogger.h" #import "OWSWebRTCDataProtos.pb.h" -#import "PhoneNumber.h" +#import "PhoneManager.h" #import "PropertyListPreferences.h" #import "PureLayout.h" #import "PushManager.h" @@ -28,6 +28,7 @@ #import "UIView+OWS.h" #import #import +#import #import #import #import @@ -45,6 +46,7 @@ #import #import #import +#import #import #import #import diff --git a/Signal/src/call/OutboundCallInitiator.swift b/Signal/src/call/OutboundCallInitiator.swift new file mode 100644 index 000000000..f7d27d68f --- /dev/null +++ b/Signal/src/call/OutboundCallInitiator.swift @@ -0,0 +1,94 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +import Foundation + +/** + * Creates an outbound call via either Redphone or WebRTC depending on participant preferences. + */ +@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) { + self.redphoneManager = redphoneManager + self.callUIAdapter = callUIAdapter + + self.contactsManager = contactsManager + self.contactsUpdater = contactsUpdater + } + + /** + * |handle| is a user formatted phone number, e.g. from a system contacts entry + */ + public func initiateCall(handle: String) -> Bool { + Logger.info("\(TAG) in \(#function) with handle: \(handle)") + guard let recipientId = PhoneNumber(fromUserSpecifiedText: handle).toE164() else { + Logger.warn("\(TAG) unable to parse signalId from phone number: \(handle)") + return false + } + + return initiateCall(recipientId: recipientId) + } + + /** + * |recipientId| is a e164 formatted phone number. + */ + public func initiateCall(recipientId: String) -> Bool { + + let localWantsWebRTC = Environment.preferences().isWebRTCEnabled() + if !localWantsWebRTC { + return self.initiateRedphoneCall(recipientId: recipientId) + } + + // Since users can toggle this setting, which is only communicated during contact sync, it's easy to imagine the + // preference getting stale. Especially as users are toggling the feature to test calls. So here, we opt for a + // blocking network request *every* time we place a call to make sure we've got up to date preferences. + // + // e.g. The following would suffice if we weren't worried about stale preferences. + // SignalRecipient *recipient = [SignalRecipient recipientWithTextSecureIdentifier:self.thread.contactIdentifier]; + self.contactsUpdater.lookupIdentifier(recipientId, + success: { recipient in + let remoteWantsWebRTC = recipient.supportsWebRTC + Logger.debug("\(self.TAG) localWantsWebRTC: \(localWantsWebRTC), remoteWantsWebRTC: \(remoteWantsWebRTC)") + + if localWantsWebRTC, remoteWantsWebRTC { + _ = self.initiateWebRTCAudioCall(recipientId: recipientId) + } else { + _ = self.initiateRedphoneCall(recipientId: recipientId) + } + }, + failure: { error in + Logger.warn("\(self.TAG) looking up recipientId: \(recipientId) failed with error \(error)") + // TODO fail with alert. e.g. when someone tries to call a non signal user from their contacts we should inform them. + }) + + // Since we've already dispatched async to make sure we have fresh webrtc preference data + // we don't have a meaningful value to return here - but we're not using it anway. =/ + return true + } + + private func initiateRedphoneCall(recipientId: String) -> Bool { + Logger.info("\(TAG) Placing redphone call to: \(recipientId)") + + let number = PhoneNumber.tryParsePhoneNumber(fromUserSpecifiedText: recipientId) + let contact = self.contactsManager.latestContact(for: number) + assert(number != nil) + assert(contact != nil) + + redphoneManager.initiateOutgoingCall(to: contact, atRemoteNumber: number) + + return true + } + + private func initiateWebRTCAudioCall(recipientId: String) -> Bool { + callUIAdapter.callBack(recipientId: recipientId) + return true + } + +} diff --git a/Signal/src/environment/Environment.h b/Signal/src/environment/Environment.h index c1bfb9ba0..e5af9498a 100644 --- a/Signal/src/environment/Environment.h +++ b/Signal/src/environment/Environment.h @@ -1,3 +1,7 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + #import #import "Logging.h" #import "PacketHandler.h" diff --git a/Signal/src/environment/Environment.m b/Signal/src/environment/Environment.m index a669be33a..d659e0378 100644 --- a/Signal/src/environment/Environment.m +++ b/Signal/src/environment/Environment.m @@ -1,3 +1,7 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + #import "Environment.h" #import "Constraints.h" #import "DH3KKeyAgreementProtocol.h" diff --git a/Signal/src/view controllers/MessagesViewController.m b/Signal/src/view controllers/MessagesViewController.m index bb24e36cf..a9fbb38a1 100644 --- a/Signal/src/view controllers/MessagesViewController.m +++ b/Signal/src/view controllers/MessagesViewController.m @@ -1,9 +1,5 @@ // -// MessagesViewController.m -// Signal -// -// Created by Dylan Bourgeois on 28/10/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "AppDelegate.h"