diff --git a/Podfile b/Podfile index 55d7dcd08..c5d586e9b 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ target 'Signal' do pod 'SocketRocket', :git => 'https://github.com/facebook/SocketRocket.git' pod 'AxolotlKit', git: 'https://github.com/WhisperSystems/SignalProtocolKit.git' #pod 'AxolotlKit', path: '../SignalProtocolKit' - pod 'SignalServiceKit', git: 'https://github.com/WhisperSystems/SignalServiceKit.git' + pod 'SignalServiceKit', git: 'https://github.com/WhisperSystems/SignalServiceKit.git', branch: 'mkirk/reject-unseen-id-calls' #pod 'SignalServiceKit', path: '../SignalServiceKit' pod 'OpenSSL' pod 'JSQMessagesViewController', git: 'https://github.com/WhisperSystems/JSQMessagesViewController.git', branch: 'mkirk/position-edit-menu' diff --git a/Podfile.lock b/Podfile.lock index 3f4861a53..9888d0801 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -114,7 +114,7 @@ DEPENDENCIES: - OpenSSL - PureLayout - Reachability - - SignalServiceKit (from `https://github.com/WhisperSystems/SignalServiceKit.git`) + - SignalServiceKit (from `https://github.com/WhisperSystems/SignalServiceKit.git`, branch `mkirk/reject-unseen-id-calls`) - SocketRocket (from `https://github.com/facebook/SocketRocket.git`) EXTERNAL SOURCES: @@ -124,6 +124,7 @@ EXTERNAL SOURCES: :branch: mkirk/position-edit-menu :git: https://github.com/WhisperSystems/JSQMessagesViewController.git SignalServiceKit: + :branch: mkirk/reject-unseen-id-calls :git: https://github.com/WhisperSystems/SignalServiceKit.git SocketRocket: :git: https://github.com/facebook/SocketRocket.git @@ -136,7 +137,7 @@ CHECKOUT OPTIONS: :commit: 7054e4b13ee5bcd6d524adb6dc9a726e8c466308 :git: https://github.com/WhisperSystems/JSQMessagesViewController.git SignalServiceKit: - :commit: 0c46288cf96c6dadc775c2c2d089245d65490e78 + :commit: e10cc0c1803c598a7518b6c8b28195e20f0eb12e :git: https://github.com/WhisperSystems/SignalServiceKit.git SocketRocket: :commit: 877ac7438be3ad0b45ef5ca3969574e4b97112bf @@ -164,6 +165,6 @@ SPEC CHECKSUMS: UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d YapDatabase: cd911121580ff16675f65ad742a9eb0ab4d9e266 -PODFILE CHECKSUM: 48e80d7f1e049bbf544a689fdfdf33e8196c640a +PODFILE CHECKSUM: 0e50a094f857d1833a01f162c8395b89604b2c35 COCOAPODS: 1.2.1 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index d8d9f524c..4c25a1163 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -482,7 +482,7 @@ 34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = ""; }; 34D8C0241ED3673300188D7C /* DebugUIMessages.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMessages.m; sourceTree = ""; }; 34D8C0251ED3673300188D7C /* DebugUITableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUITableViewController.h; sourceTree = ""; }; - 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUITableViewController.m; sourceTree = ""; }; + 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = DebugUITableViewController.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 34D8C0291ED3685800188D7C /* DebugUIContacts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIContacts.h; sourceTree = ""; }; 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIContacts.m; sourceTree = ""; }; 34DFCB831E8E04B400053165 /* AddToBlockListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddToBlockListViewController.h; sourceTree = ""; }; @@ -512,7 +512,7 @@ 4516E3FE1DD2193B00DC4206 /* OWS101ExistingUsersBlockOnIdentityChange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWS101ExistingUsersBlockOnIdentityChange.m; path = Migrations/OWS101ExistingUsersBlockOnIdentityChange.m; sourceTree = ""; }; 451764281DE939FD00EDB8B9 /* ContactCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactCell.xib; sourceTree = ""; }; 451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = ""; }; - 451A13B01E13DED2000A50FD /* CallNotificationsAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallNotificationsAdapter.swift; path = ../UserInterface/Notifications/CallNotificationsAdapter.swift; sourceTree = ""; }; + 451A13B01E13DED2000A50FD /* CallNotificationsAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = CallNotificationsAdapter.swift; path = ../UserInterface/Notifications/CallNotificationsAdapter.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 451DE9F11DC1585F00810E42 /* PromiseKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PromiseKit.framework; path = Carthage/Build/iOS/PromiseKit.framework; sourceTree = ""; }; 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; }; @@ -548,7 +548,7 @@ 45666F7C1D9C0814008FE134 /* OWSDatabaseMigrationRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSDatabaseMigrationRunner.h; path = Migrations/OWSDatabaseMigrationRunner.h; sourceTree = ""; }; 45666F7D1D9C0814008FE134 /* OWSDatabaseMigrationRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDatabaseMigrationRunner.m; path = Migrations/OWSDatabaseMigrationRunner.m; sourceTree = ""; }; 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerConnectionClientTest.swift; sourceTree = ""; }; - 4574A5D51DD6704700C6B692 /* CallService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallService.swift; sourceTree = ""; }; + 4574A5D51DD6704700C6B692 /* CallService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CallService.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 4579431C1E7C8CE9008ED0C0 /* Pastelog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Pastelog.h; sourceTree = ""; }; 4579431D1E7C8CE9008ED0C0 /* Pastelog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Pastelog.m; sourceTree = ""; }; 45794E851E00620000066731 /* CallUIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallUIAdapter.swift; path = UserInterface/CallUIAdapter.swift; sourceTree = ""; }; @@ -601,8 +601,8 @@ 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompareSafetyNumbersActivity.swift; sourceTree = ""; }; 45E282DE1D08E67800ADD4C8 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = translations/gl.lproj/Localizable.strings; sourceTree = ""; }; 45E282DF1D08E6CC00ADD4C8 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = translations/id.lproj/Localizable.strings; sourceTree = ""; }; - 45E2E91E1E13EE3500457AA0 /* OWSCallNotificationsAdaptee.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSCallNotificationsAdaptee.h; path = UserInterface/OWSCallNotificationsAdaptee.h; sourceTree = ""; }; - 45E2E91F1E153B3D00457AA0 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Strings.swift; path = UserInterface/Strings.swift; sourceTree = ""; }; + 45E2E91E1E13EE3500457AA0 /* OWSCallNotificationsAdaptee.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = OWSCallNotificationsAdaptee.h; path = UserInterface/OWSCallNotificationsAdaptee.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 45E2E91F1E153B3D00457AA0 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = Strings.swift; path = UserInterface/Strings.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 45E615151E8C590B0018AD52 /* DisplayableTextFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableTextFilter.swift; sourceTree = ""; }; 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableTextFilterTest.swift; sourceTree = ""; }; 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = ""; }; diff --git a/Signal/src/UserInterface/Notifications/CallNotificationsAdapter.swift b/Signal/src/UserInterface/Notifications/CallNotificationsAdapter.swift index d2363ab3c..8d0d95a40 100644 --- a/Signal/src/UserInterface/Notifications/CallNotificationsAdapter.swift +++ b/Signal/src/UserInterface/Notifications/CallNotificationsAdapter.swift @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 12/28/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// import Foundation @@ -34,4 +35,9 @@ class CallNotificationsAdapter: NSObject { Logger.debug("\(TAG) in \(#function)") adaptee.presentMissedCall(call, callerName: callerName) } + + func presentRejectedCallWithUnseenIdentityChange(_ call: SignalCall, callerName: String) { + Logger.debug("\(TAG) in \(#function)") + adaptee.presentRejectedCallWithUnseenIdentityChange(call, callerName: callerName) + } } diff --git a/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift b/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift index 744e0c69f..676a47179 100644 --- a/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift +++ b/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift @@ -13,14 +13,17 @@ import UserNotifications @available(iOS 10.0, *) struct AppNotifications { enum Category { - case missedCall + case missedCall, + rejectedCallFromUnseenIdentity // Don't forget to update this! We use it to register categories. - static let allValues = [ missedCall ] + static let allValues = [ missedCall, rejectedCallFromUnseenIdentity ] } enum Action { - case callBack + case callBack, + showThread, + confirmIdentityAndCallBack } static var allCategories: Set { @@ -35,6 +38,12 @@ struct AppNotifications { actions: [ action(.callBack) ], intentIdentifiers: [], options: []) + + case .rejectedCallFromUnseenIdentity: + return UNNotificationCategory(identifier: "org.whispersystems.signal.AppNotifications.Category.rejectedCallFromUnseenIdentity", + actions: [ action(.confirmIdentityAndCallBack), action(.showThread) ], + intentIdentifiers: [], + options: []) } } @@ -44,6 +53,14 @@ struct AppNotifications { return UNNotificationAction(identifier: "org.whispersystems.signal.AppNotifications.Action.callBack", title: CallStrings.callBackButtonTitle, options: .authenticationRequired) + case .showThread: + return UNNotificationAction(identifier: "org.whispersystems.signal.AppNotifications.Action.showThread", + title: CallStrings.showThreadButtonTitle, + options: .authenticationRequired) + case .confirmIdentityAndCallBack: + return UNNotificationAction(identifier: "org.whispersystems.signal.AppNotifications.Action.confirmIdentityAndCallBack", + title: CallStrings.confirmIdentityAndCallBackButtonTitle, + options: .authenticationRequired) } } } @@ -88,7 +105,7 @@ class UserNotificationsAdaptee: NSObject, OWSCallNotificationsAdaptee, UNUserNot public func presentIncomingCall(_ call: SignalCall, callerName: String) { Logger.debug("\(TAG) \(#function) is no-op, because it's handled with callkit.") // TODO since CallKit doesn't currently work on the simulator, - // we could implement UNNotifications for simulator testing. + // we could implement UNNotifications for simulator testing, or if people have opted out of callkit. } public func presentMissedCall(_ call: SignalCall, callerName: String) { @@ -116,4 +133,30 @@ class UserNotificationsAdaptee: NSObject, OWSCallNotificationsAdaptee, UNUserNot center.add(request) } + + func presentRejectedCallWithUnseenIdentityChange(_ call: SignalCall, callerName: String) { + Logger.debug("\(TAG) \(#function)") + + let content = UNMutableNotificationContent() + // TODO group by thread identifier + // content.threadIdentifier = threadId + + let notificationBody = { () -> String in + switch previewType { + case .noNameNoPreview: + return CallStrings.rejectedCallWithUnseenIdentityChangeNotificationBody + case .nameNoPreview, .namePreview: + return (Environment.getCurrent().preferences.isCallKitPrivacyEnabled() + ? CallStrings.rejectedCallWithUnseenIdentityChangeNotificationBodyWithoutCallerName + : String(format: CallStrings.rejectedCallWithUnseenIdentityChangeNotificationBodyWithCallerName, callerName)) + }}() + + content.body = notificationBody + content.sound = UNNotificationSound.default() + content.categoryIdentifier = AppNotifications.category(.rejectedCallFromUnseenIdentity).identifier + + let request = UNNotificationRequest.init(identifier: call.localId.uuidString, content: content, trigger: nil) + + center.add(request) + } } diff --git a/Signal/src/UserInterface/Strings.swift b/Signal/src/UserInterface/Strings.swift index 9efc38da5..73da726e3 100644 --- a/Signal/src/UserInterface/Strings.swift +++ b/Signal/src/UserInterface/Strings.swift @@ -8,9 +8,21 @@ import Foundation * Strings re-used in multiple places should be added here. */ @objc class CallStrings: NSObject { + + static let callStatusFormat = NSLocalizedString("CALL_STATUS_FORMAT", comment: "embeds {{Call Status}} in call screen label. For ongoing calls, {{Call Status}} is a seconds timer like 01:23, otherwise {{Call Status}} is a short text like 'Ringing', 'Busy', or 'Failed Call'") + + // MARK: Notification actions static let callBackButtonTitle = NSLocalizedString("CALLBACK_BUTTON_TITLE", comment: "notification action") + static let confirmIdentityAndCallBackButtonTitle = NSLocalizedString("CONFIRM_IDENTITY_AND_CALLBACK_BUTTON_TITLE", comment: "notification action, confirming that it's OK to proceed calling after a caller's Safety Number has changed") + static let showThreadButtonTitle = NSLocalizedString("SHOW_THREAD_BUTTON_TITLE", comment: "notification action") + + // MARK: Missed Call Notification static let missedCallNotificationBody = NSLocalizedString("MISSED_CALL", comment: "notification title") static let missedCallNotificationBodyWithCallerName = NSLocalizedString("MSGVIEW_MISSED_CALL_WITH_NAME", comment: "notification title. Embeds {{Caller's Name}}") static let missedCallNotificationBodyWithoutCallerName = NSLocalizedString("MSGVIEW_MISSED_CALL_WITHOUT_NAME", comment: "notification title.") - static let callStatusFormat = NSLocalizedString("CALL_STATUS_FORMAT", comment: "embeds {{Call Status}} in call screen label. For ongoing calls, {{Call Status}} is a seconds timer like 01:23, otherwise {{Call Status}} is a short text like 'Ringing', 'Busy', or 'Failed Call'") + + // MARK: Missed with Unseen identity Notification + static let rejectedCallWithUnseenIdentityChangeNotificationBody = NSLocalizedString("MISSED_CALL_WITH_UNSEEN_IDENTITY_BODY", comment: "notification action") + static let rejectedCallWithUnseenIdentityChangeNotificationBodyWithoutCallerName = NSLocalizedString("MISSED_CALL_WITH_UNSEEN_IDENTITY_BODY_WITHOUT_CALLER_NAME", comment: "notification action") + static let rejectedCallWithUnseenIdentityChangeNotificationBodyWithCallerName = NSLocalizedString("MISSED_CALL_WITH_UNSEEN_IDENTITY_BODY_WITH_CALLER_NAME", comment: "notification action") } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m b/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m index 7bd1c6361..00446267b 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUITableViewController.m @@ -84,6 +84,51 @@ NS_ASSUME_NONNULL_BEGIN [contents addSection:[DebugUIContacts section]]; + // After enqueing the notification you may want to background the app or lock the screen before it triggers, so we + // give a little delay. + uint64_t notificationDelay = 5; + [contents + addSection:[OWSTableSection + sectionWithTitle:[NSString stringWithFormat:@"Call Notifications (%llu second delay)", + notificationDelay] + items:@[ + [OWSTableItem itemWithTitle:@"Missed Call" + actionBlock:^{ + SignalCall *call = [SignalCall + incomingCallWithLocalId:[NSUUID new] + remotePhoneNumber:thread.contactIdentifier + signalingId:0]; + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(notificationDelay * NSEC_PER_SEC)), + dispatch_get_main_queue(), + ^{ + [[Environment getCurrent] + .callService.notificationsAdapter + presentMissedCall:call + callerName:thread.name]; + }); + }], + [OWSTableItem + itemWithTitle:@"Rejected Call with Unseen Safety Number" + actionBlock:^{ + SignalCall *call = + [SignalCall incomingCallWithLocalId:[NSUUID new] + remotePhoneNumber:thread.contactIdentifier + signalingId:0]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(notificationDelay * NSEC_PER_SEC)), + dispatch_get_main_queue(), + ^{ + [[Environment getCurrent].callService.notificationsAdapter + presentRejectedCallWithUnseenIdentityChange:call + callerName:thread.name]; + }); + }], + ]]]; + DebugUITableViewController *viewController = [DebugUITableViewController new]; viewController.contents = contents; [viewController presentFromViewController:fromViewController]; diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 51d82c536..d73b9a4d4 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -110,9 +110,10 @@ protocol CallServiceObserver: class { private let accountManager: AccountManager private let messageSender: MessageSender private let contactsManager: OWSContactsManager - private let notificationsAdapter: CallNotificationsAdapter + private let storageManager: TSStorageManager // Exposed by environment.m + internal let notificationsAdapter: CallNotificationsAdapter internal var callUIAdapter: CallUIAdapter! // MARK: Class @@ -200,6 +201,7 @@ protocol CallServiceObserver: class { self.contactsManager = contactsManager self.messageSender = messageSender self.notificationsAdapter = notificationsAdapter + self.storageManager = TSStorageManager.shared() super.init() @@ -465,9 +467,16 @@ protocol CallServiceObserver: class { AssertIsOnMainThread() Logger.info("\(TAG) receivedCallOffer for thread:\(thread)") + let newCall = SignalCall.incomingCall(localId: UUID(), remotePhoneNumber: thread.contactIdentifier(), signalingId: callId) - guard call == nil else { + guard !self.storageManager.hasUnseenIdentityChange(forRecipientId: thread.contactIdentifier()) else { + let callerName = self.contactsManager.displayName(forPhoneIdentifier: thread.contactIdentifier()) + self.notificationsAdapter.presentRejectedCallWithUnseenIdentityChange(newCall, callerName: callerName) + return + } + + guard self.call == nil else { // TODO on iOS10+ we can use CallKit to swap calls rather than just returning busy immediately. Logger.verbose("\(TAG) receivedCallOffer for thread: \(thread) but we're already in call: \(call!)") diff --git a/Signal/src/call/OutboundCallInitiator.swift b/Signal/src/call/OutboundCallInitiator.swift index c15c9e8e9..b88b363ee 100644 --- a/Signal/src/call/OutboundCallInitiator.swift +++ b/Signal/src/call/OutboundCallInitiator.swift @@ -46,6 +46,8 @@ import Foundation return false } + // TODO possible to get here when. e.g. dialing from contacts/recent calls. Should verify seen latest SN. + // Check for microphone permissions // Alternative way without prompting for permissions: // if AVAudioSession.sharedInstance().recordPermission() == .denied { diff --git a/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h b/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h index 060a58007..d80a09319 100644 --- a/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h +++ b/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h @@ -1,5 +1,6 @@ -// Created by Michael Kirk on 12/28/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// NS_ASSUME_NONNULL_BEGIN @@ -11,6 +12,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)presentMissedCall:(SignalCall *)call callerName:(NSString *)callerName; +- (void)presentRejectedCallWithUnseenIdentityChange:(SignalCall *)call + callerName:(NSString *)callerName + NS_SWIFT_NAME(presentRejectedCallWithUnseenIdentityChange(_:callerName:)); + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m index 7fb70dd3e..dc7bb85bb 100644 --- a/Signal/src/environment/NotificationsManager.m +++ b/Signal/src/environment/NotificationsManager.m @@ -113,6 +113,43 @@ [self presentNotification:notification identifier:localCallId]; } +- (void)presentRejectedCallWithUnseenIdentityChange:(SignalCall *)call callerName:(NSString *)callerName +{ + TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:call.remotePhoneNumber]; + OWSAssert(thread != nil); + + UILocalNotification *notification = [UILocalNotification new]; + notification.category = PushManagerCategoriesRejectedCallFromUnseenIdentityChange; + NSString *localCallId = call.localId.UUIDString; + notification.userInfo = @{ + PushManagerUserInfoKeysLocalCallId : localCallId, + PushManagerUserInfoKeysCallBackSignalRecipientId : call.remotePhoneNumber, + Signal_Thread_UserInfo_Key : thread.uniqueId + }; + + NSString *alertMessage; + switch (self.notificationPreviewType) { + case NotificationNoNameNoPreview: { + alertMessage = [CallStrings rejectedCallWithUnseenIdentityChangeNotificationBody]; + break; + } + case NotificationNameNoPreview: + case NotificationNamePreview: { + alertMessage = (([UIDevice currentDevice].supportsCallKit && + [[Environment getCurrent].preferences isCallKitPrivacyEnabled]) + ? [CallStrings rejectedCallWithUnseenIdentityChangeNotificationBodyWithoutCallerName] + : [NSString + stringWithFormat:[CallStrings + rejectedCallWithUnseenIdentityChangeNotificationBodyWithCallerName], + callerName]); + break; + } + } + notification.alertBody = [NSString stringWithFormat:@"☎️ %@", alertMessage]; + + [self presentNotification:notification identifier:localCallId]; +} + #pragma mark - Signal Messages - (void)notifyUserForErrorMessage:(TSErrorMessage *)message inThread:(TSThread *)thread { diff --git a/Signal/src/network/PushManager.h b/Signal/src/network/PushManager.h index 3482fc1b8..d3db26da0 100644 --- a/Signal/src/network/PushManager.h +++ b/Signal/src/network/PushManager.h @@ -10,22 +10,25 @@ NS_ASSUME_NONNULL_BEGIN @class UILocalNotification; -#define Signal_Thread_UserInfo_Key @"Signal_Thread_Id" -#define Signal_Message_UserInfo_Key @"Signal_Message_Id" +FOUNDATION_EXPORT NSString *const Signal_Thread_UserInfo_Key; +FOUNDATION_EXPORT NSString *const Signal_Message_UserInfo_Key; -#define Signal_Full_New_Message_Category @"Signal_Full_New_Message" +FOUNDATION_EXPORT NSString *const Signal_Full_New_Message_Category; -#define Signal_Message_Reply_Identifier @"Signal_New_Message_Reply" -#define Signal_Message_MarkAsRead_Identifier @"Signal_Message_MarkAsRead" +FOUNDATION_EXPORT NSString *const Signal_Message_Reply_Identifier; +FOUNDATION_EXPORT NSString *const Signal_Message_MarkAsRead_Identifier; #pragma mark Signal Calls constants FOUNDATION_EXPORT NSString *const PushManagerCategoriesIncomingCall; FOUNDATION_EXPORT NSString *const PushManagerCategoriesMissedCall; +FOUNDATION_EXPORT NSString *const PushManagerCategoriesRejectedCallFromUnseenIdentityChange; FOUNDATION_EXPORT NSString *const PushManagerActionsAcceptCall; FOUNDATION_EXPORT NSString *const PushManagerActionsDeclineCall; FOUNDATION_EXPORT NSString *const PushManagerActionsCallBack; +FOUNDATION_EXPORT NSString *const PushManagerActionsConfirmIdentityAndCallBack; +FOUNDATION_EXPORT NSString *const PushManagerActionsShowThread; FOUNDATION_EXPORT NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId; FOUNDATION_EXPORT NSString *const PushManagerUserInfoKeysLocalCallId; diff --git a/Signal/src/network/PushManager.m b/Signal/src/network/PushManager.m index 9dfe06f95..fe242de84 100644 --- a/Signal/src/network/PushManager.m +++ b/Signal/src/network/PushManager.m @@ -16,7 +16,13 @@ #import #import -#define pushManagerDomain @"org.whispersystems.pushmanager" +NSString *const Signal_Thread_UserInfo_Key = @"Signal_Thread_Id"; +NSString *const Signal_Message_UserInfo_Key = @"Signal_Message_Id"; + +NSString *const Signal_Full_New_Message_Category = @"Signal_Full_New_Message"; + +NSString *const Signal_Message_Reply_Identifier = @"Signal_New_Message_Reply"; +NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRead"; @interface PushManager () @@ -132,10 +138,9 @@ withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)())completionHandler { - DDLogInfo(@"received: %s", __PRETTY_FUNCTION__); + DDLogInfo(@"%@ handling action with identifier: %@", self.tag, identifier); if ([identifier isEqualToString:Signal_Message_Reply_Identifier]) { - DDLogInfo(@"%@ received reply identifier", self.tag); NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; if (threadId) { @@ -146,6 +151,7 @@ messageBody:responseInfo[UIUserNotificationActionResponseTypedTextKey]]; [self.messageSender sendMessage:message success:^{ + // TODO do we really want to mark them all as read? [self markAllInThreadAsRead:notification.userInfo completionHandler:completionHandler]; [[[[Environment getCurrent] signalsViewController] tableView] reloadData]; } @@ -162,10 +168,9 @@ }]; } } else if ([identifier isEqualToString:Signal_Message_MarkAsRead_Identifier]) { + // TODO mark all as read? Or just this one? [self markAllInThreadAsRead:notification.userInfo completionHandler:completionHandler]; } else if ([identifier isEqualToString:PushManagerActionsAcceptCall]) { - DDLogInfo(@"%@ received accept call action", self.tag); - NSString *localIdString = notification.userInfo[PushManagerUserInfoKeysLocalCallId]; if (!localIdString) { DDLogError(@"%@ missing localIdString.", self.tag); @@ -178,11 +183,9 @@ return; } - [self.callUIAdapter answerCallWithLocalId:localId]; + completionHandler(); } else if ([identifier isEqualToString:PushManagerActionsDeclineCall]) { - DDLogInfo(@"%@ received decline call action", self.tag); - NSString *localIdString = notification.userInfo[PushManagerUserInfoKeysLocalCallId]; if (!localIdString) { DDLogError(@"%@ missing localIdString.", self.tag); @@ -196,19 +199,35 @@ } [self.callUIAdapter declineCallWithLocalId:localId]; + completionHandler(); } else if ([identifier isEqualToString:PushManagerActionsCallBack]) { - DDLogInfo(@"%@ received call back action", self.tag); + NSString *recipientId = notification.userInfo[PushManagerUserInfoKeysCallBackSignalRecipientId]; + if (!recipientId) { + DDLogError(@"%@ missing call back id", self.tag); + return; + } + [self.callUIAdapter startAndShowOutgoingCallWithRecipientId:recipientId]; + completionHandler(); + } else if ([identifier isEqualToString:PushManagerActionsConfirmIdentityAndCallBack]) { NSString *recipientId = notification.userInfo[PushManagerUserInfoKeysCallBackSignalRecipientId]; if (!recipientId) { DDLogError(@"%@ missing call back id", self.tag); return; } + TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId]; + [MarkIdentityAsSeenJob runWithThread:thread]; + [self.callUIAdapter startAndShowOutgoingCallWithRecipientId:recipientId]; + completionHandler(); + } else if ([identifier isEqualToString:PushManagerActionsShowThread]) { + NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; + [Environment messageThreadId:threadId]; + completionHandler(); } else { - DDLogDebug(@"%@ Unhandled action with identifier: %@", self.tag, identifier); - + DDLogError(@"%@ Unhandled action with identifier: %@", self.tag, identifier); + OWSFail(@"Unhandled action"); NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; [Environment messageThreadId:threadId]; completionHandler(); @@ -338,10 +357,14 @@ NSString *const PushManagerCategoriesIncomingCall = @"PushManagerCategoriesIncomingCall"; NSString *const PushManagerCategoriesMissedCall = @"PushManagerCategoriesMissedCall"; +NSString *const PushManagerCategoriesRejectedCallFromUnseenIdentityChange = + @"PushManagerCategoriesRejectedCallFromUnseenIdentityChange"; NSString *const PushManagerActionsAcceptCall = @"PushManagerActionsAcceptCall"; NSString *const PushManagerActionsDeclineCall = @"PushManagerActionsDeclineCall"; NSString *const PushManagerActionsCallBack = @"PushManagerActionsCallBack"; +NSString *const PushManagerActionsConfirmIdentityAndCallBack = @"PushManagerActionsConfirmIdentityAndCallBack"; +NSString *const PushManagerActionsShowThread = @"PushManagerActionsShowThread"; NSString *const PushManagerUserInfoKeysLocalCallId = @"PushManagerUserInfoKeysLocalCallId"; NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId = @"PushManagerUserInfoKeysCallBackSignalRecipientId"; @@ -379,12 +402,38 @@ NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId = @"PushManager callBackAction.destructive = NO; callBackAction.authenticationRequired = YES; - UIMutableUserNotificationCategory *callCategory = [UIMutableUserNotificationCategory new]; - callCategory.identifier = PushManagerCategoriesMissedCall; - [callCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextMinimal]; - [callCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextDefault]; + UIMutableUserNotificationCategory *missedCallCategory = [UIMutableUserNotificationCategory new]; + missedCallCategory.identifier = PushManagerCategoriesMissedCall; + [missedCallCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextMinimal]; + [missedCallCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextDefault]; - return callCategory; + return missedCallCategory; +} + +- (UIUserNotificationCategory *)signalRejectedCallWithUnseenIdentityChangeCategory +{ + UIMutableUserNotificationAction *confirmAndCallBackAction = [UIMutableUserNotificationAction new]; + confirmAndCallBackAction.identifier = PushManagerActionsConfirmIdentityAndCallBack; + confirmAndCallBackAction.title = [CallStrings confirmIdentityAndCallBackButtonTitle]; + confirmAndCallBackAction.activationMode = UIUserNotificationActivationModeForeground; + confirmAndCallBackAction.destructive = NO; + confirmAndCallBackAction.authenticationRequired = YES; + + UIMutableUserNotificationAction *showThreadAction = [UIMutableUserNotificationAction new]; + showThreadAction.identifier = PushManagerActionsShowThread; + showThreadAction.title = [CallStrings showThreadButtonTitle]; + showThreadAction.activationMode = UIUserNotificationActivationModeForeground; + showThreadAction.destructive = NO; + showThreadAction.authenticationRequired = YES; + + UIMutableUserNotificationCategory *rejectedCallCategory = [UIMutableUserNotificationCategory new]; + rejectedCallCategory.identifier = PushManagerCategoriesRejectedCallFromUnseenIdentityChange; + [rejectedCallCategory setActions:@[ confirmAndCallBackAction, showThreadAction ] + forContext:UIUserNotificationActionContextMinimal]; + [rejectedCallCategory setActions:@[ confirmAndCallBackAction, showThreadAction ] + forContext:UIUserNotificationActionContextDefault]; + + return rejectedCallCategory; } #pragma mark Util @@ -408,6 +457,7 @@ NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId = @"PushManager categories:[NSSet setWithObjects:[self fullNewMessageNotificationCategory], [self signalIncomingCallCategory], [self signalMissedCallCategory], + [self signalRejectedCallWithUnseenIdentityChangeCategory], nil]]; [UIApplication.sharedApplication registerUserNotificationSettings:settings]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index dfccf5130..45781ec67 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -212,7 +212,7 @@ "CALL_VIEW_SETTINGS_NAG_SHOW_CALL_SETTINGS" = "Show Privacy Settings"; /* notification action */ -"CALLBACK_BUTTON_TITLE" = "Call back"; +"CALLBACK_BUTTON_TITLE" = "Call Back"; /* The generic name used for calls if CallKit privacy is enabled */ "CALLKIT_ANONYMOUS_CONTACT_NAME" = "Signal User"; @@ -232,6 +232,9 @@ /* No comment provided by engineer. */ "CONFIRM_ACCOUNT_DESTRUCTION_TITLE" = "Are you sure you want to delete your account?"; +/* notification action, confirming that it's OK to proceed calling after a caller's Safety Number has changed */ +"CONFIRM_IDENTITY_AND_CALLBACK_BUTTON_TITLE" = "Confirm and Call Back"; + /* Alert body */ "CONFIRM_LEAVE_GROUP_DESCRIPTION" = "You will no longer be able to send or receive messages in this group."; @@ -670,9 +673,6 @@ /* table cell label in conversation settings */ "LIST_GROUP_MEMBERS_ACTION" = "List Group Members"; -/* No comment provided by engineer. */ -"load_earlier_messages" = "load_earlier_messages"; - /* No comment provided by engineer. */ "LOGGING_SECTION" = "Logging"; @@ -736,6 +736,15 @@ /* notification title */ "MISSED_CALL" = "Missed call"; +/* notification action */ +"MISSED_CALL_WITH_UNSEEN_IDENTITY_BODY" = "Missed call because the caller's Safety Number changed."; + +/* notification action */ +"MISSED_CALL_WITH_UNSEEN_IDENTITY_BODY_WITH_CALLER_NAME" = "Missed call from %@ because their Safety Number changed."; + +/* notification action */ +"MISSED_CALL_WITH_UNSEEN_IDENTITY_BODY_WITHOUT_CALLER_NAME" = "Missed call because the caller's Safety Number changed."; + /* Alert body Alert body when camera is not authorized */ "MISSING_CAMERA_PERMISSION_MESSAGE" = "Signal needs access to your camera for video calls. You can grant this permission in the Settings app >> Privacy >> Camera >> Signal"; @@ -950,7 +959,7 @@ "PROCEED_BUTTON" = "Proceed"; /* No comment provided by engineer. */ -"PUSH_MANAGER_MARKREAD" = "Mark as read"; +"PUSH_MANAGER_MARKREAD" = "Mark as Read"; /* No comment provided by engineer. */ "PUSH_MANAGER_REPLY" = "Reply"; @@ -1258,6 +1267,9 @@ /* Action sheet item */ "SHOW_SAFETY_NUMBER_ACTION" = "Show new safety number"; +/* notification action */ +"SHOW_THREAD_BUTTON_TITLE" = "Show Thread"; + /* {{1 day}} embedded in strings, e.g. 'Alice updated disappearing messages expiration to {{1 day}}'. See other *_TIME_AMOUNT strings */ "SINGLE_DAY_TIME_AMOUNT" = "%u day";