diff --git a/Riot.xcodeproj/project.pbxproj b/Riot.xcodeproj/project.pbxproj index 858f190b9..8491502b2 100644 --- a/Riot.xcodeproj/project.pbxproj +++ b/Riot.xcodeproj/project.pbxproj @@ -889,6 +889,14 @@ EC9A3EC624E1632C00A8CFAE /* PushNotificationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9A3EC424E1616900A8CFAE /* PushNotificationStore.swift */; }; EC9A3EC724E1634100A8CFAE /* KeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1CA87124C823E700DE9EBF /* KeyValueStore.swift */; }; EC9A3EC824E1634800A8CFAE /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1CA87424C8259700DE9EBF /* KeychainStore.swift */; }; + EC9E9B062575AF5C0007C0A0 /* CallVCPresentOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9E9B052575AF5C0007C0A0 /* CallVCPresentOperation.swift */; }; + EC9E9B082575AF730007C0A0 /* CallVCDismissOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9E9B072575AF730007C0A0 /* CallVCDismissOperation.swift */; }; + EC9E9B0A2575AF8A0007C0A0 /* CallBarPresentOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9E9B092575AF8A0007C0A0 /* CallBarPresentOperation.swift */; }; + EC9E9B0C2575AF9B0007C0A0 /* CallBarDismissOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9E9B0B2575AF9B0007C0A0 /* CallBarDismissOperation.swift */; }; + EC9E9B0E2575AFB80007C0A0 /* AsyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9E9B0D2575AFB80007C0A0 /* AsyncOperation.swift */; }; + EC9E9B102575B33E0007C0A0 /* CallServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC9E9B0F2575B33E0007C0A0 /* CallServiceDelegate.swift */; }; + EC9E9B112575B3960007C0A0 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DB4F0A223131600065DBFA /* String.swift */; }; + EC9E9B122575B3A70007C0A0 /* Character.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1DB4F05223015080065DBFA /* Character.swift */; }; ECAE7AE524EC0E01002FA813 /* TableViewSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECAE7AE424EC0E01002FA813 /* TableViewSections.swift */; }; ECAE7AE724EC15F7002FA813 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECAE7AE624EC15F7002FA813 /* Section.swift */; }; ECAE7AE924EC1888002FA813 /* Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECAE7AE824EC1888002FA813 /* Row.swift */; }; @@ -900,6 +908,7 @@ ECB101362477D00700CF8C11 /* UniversalLink.m in Sources */ = {isa = PBXBuildFile; fileRef = ECB101352477D00700CF8C11 /* UniversalLink.m */; }; ECB5D98F255420F8000AD89C /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB5D98E255420F8000AD89C /* Keychain.swift */; }; ECB5D9902554221F000AD89C /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB5D98E255420F8000AD89C /* Keychain.swift */; }; + ECC4C82F256FA7520010BA44 /* CallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC4C82E256FA7520010BA44 /* CallService.swift */; }; ECDC15F224AF41D2003437CF /* FormattedBodyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */; }; ECF57A4425090C23004BBF9D /* CreateRoomCoordinatorBridgePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF57A3825090C23004BBF9D /* CreateRoomCoordinatorBridgePresenter.swift */; }; ECF57A4525090C23004BBF9D /* CreateRoomCoordinatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF57A3925090C23004BBF9D /* CreateRoomCoordinatorType.swift */; }; @@ -2117,6 +2126,12 @@ EC85D753247C0F5B002C44C9 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; EC85D756247E700F002C44C9 /* NSEMemoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEMemoryStore.swift; sourceTree = ""; }; EC9A3EC424E1616900A8CFAE /* PushNotificationStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationStore.swift; sourceTree = ""; }; + EC9E9B052575AF5C0007C0A0 /* CallVCPresentOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVCPresentOperation.swift; sourceTree = ""; }; + EC9E9B072575AF730007C0A0 /* CallVCDismissOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVCDismissOperation.swift; sourceTree = ""; }; + EC9E9B092575AF8A0007C0A0 /* CallBarPresentOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallBarPresentOperation.swift; sourceTree = ""; }; + EC9E9B0B2575AF9B0007C0A0 /* CallBarDismissOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallBarDismissOperation.swift; sourceTree = ""; }; + EC9E9B0D2575AFB80007C0A0 /* AsyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncOperation.swift; sourceTree = ""; }; + EC9E9B0F2575B33E0007C0A0 /* CallServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallServiceDelegate.swift; sourceTree = ""; }; ECAE7AE424EC0E01002FA813 /* TableViewSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSections.swift; sourceTree = ""; }; ECAE7AE624EC15F7002FA813 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; ECAE7AE824EC1888002FA813 /* Row.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Row.swift; sourceTree = ""; }; @@ -2128,6 +2143,7 @@ ECB101342477D00700CF8C11 /* UniversalLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UniversalLink.h; sourceTree = ""; }; ECB101352477D00700CF8C11 /* UniversalLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UniversalLink.m; sourceTree = ""; }; ECB5D98E255420F8000AD89C /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; + ECC4C82E256FA7520010BA44 /* CallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallService.swift; sourceTree = ""; }; ECDC15F124AF41D2003437CF /* FormattedBodyParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyParser.swift; sourceTree = ""; }; ECF57A3825090C23004BBF9D /* CreateRoomCoordinatorBridgePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinatorBridgePresenter.swift; sourceTree = ""; }; ECF57A3925090C23004BBF9D /* CreateRoomCoordinatorType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinatorType.swift; sourceTree = ""; }; @@ -4479,6 +4495,7 @@ B1B5597C20EFC3DF00210D55 /* Managers */ = { isa = PBXGroup; children = ( + ECC4C82D256FA73E0010BA44 /* Call */, EC1CA87824C8840B00DE9EBF /* LocalAuthentication */, EC1CA87324C8257F00DE9EBF /* KeyValueStorage */, B185145324B7CF9500EE19EA /* AppVersion */, @@ -5157,6 +5174,17 @@ path = RiotNSE; sourceTree = ""; }; + EC9E9B042575AF460007C0A0 /* Operations */ = { + isa = PBXGroup; + children = ( + EC9E9B052575AF5C0007C0A0 /* CallVCPresentOperation.swift */, + EC9E9B072575AF730007C0A0 /* CallVCDismissOperation.swift */, + EC9E9B092575AF8A0007C0A0 /* CallBarPresentOperation.swift */, + EC9E9B0B2575AF9B0007C0A0 /* CallBarDismissOperation.swift */, + ); + path = Operations; + sourceTree = ""; + }; ECAE7AEA24EC223D002FA813 /* Models */ = { isa = PBXGroup; children = ( @@ -5175,6 +5203,17 @@ path = Extensions; sourceTree = ""; }; + ECC4C82D256FA73E0010BA44 /* Call */ = { + isa = PBXGroup; + children = ( + ECC4C82E256FA7520010BA44 /* CallService.swift */, + EC9E9B0F2575B33E0007C0A0 /* CallServiceDelegate.swift */, + EC9E9B0D2575AFB80007C0A0 /* AsyncOperation.swift */, + EC9E9B042575AF460007C0A0 /* Operations */, + ); + path = Call; + sourceTree = ""; + }; ECF57A3725090C23004BBF9D /* CreateRoom */ = { isa = PBXGroup; children = ( @@ -6182,11 +6221,13 @@ EC31F09C2524AE1400D407DA /* BiometricsAuthenticationPresenter.swift in Sources */, EC85D752247C0F52002C44C9 /* UNUserNotificationCenter.swift in Sources */, EC9A3EC724E1634100A8CFAE /* KeyValueStore.swift in Sources */, + EC9E9B122575B3A70007C0A0 /* Character.swift in Sources */, 32FD757A24D2C9BA00BA7B37 /* Bundle.swift in Sources */, EC31F0962521FC5300D407DA /* Strings.swift in Sources */, EC31F0972521FC6300D407DA /* Images.swift in Sources */, EC9A3EC824E1634800A8CFAE /* KeychainStore.swift in Sources */, EC31F0942521FC3700D407DA /* LocalAuthenticationService.swift in Sources */, + EC9E9B112575B3960007C0A0 /* String.swift in Sources */, 32FD755B24D15C7A00BA7B37 /* Configurable.swift in Sources */, EC85D755247C0F84002C44C9 /* Constants.swift in Sources */, ); @@ -6489,6 +6530,7 @@ B157FAA623264AE900EBFBD4 /* SettingsDiscoveryThreePidDetailsViewController.swift in Sources */, B126768A2523E4D100BE6B98 /* SecretsResetViewModel.swift in Sources */, B1DCC62422E60CA900625807 /* EmojiPickerCategoryViewData.swift in Sources */, + ECC4C82F256FA7520010BA44 /* CallService.swift in Sources */, B1550FCB2420E8F500CE097B /* QRCodeReaderViewController.swift in Sources */, 324A2056225FC571004FE8B0 /* DeviceVerificationIncomingCoordinator.swift in Sources */, B16932F720F3C50E00746532 /* RecentsDataSource.m in Sources */, @@ -6537,6 +6579,7 @@ B1B5572520EE6C4D00210D55 /* RoomMessagesSearchViewController.m in Sources */, B197B7C6243DE947005ABBF3 /* EncryptionTrustLevelBadgeImageHelper.swift in Sources */, EC51E7A82514D2E100AAE7DB /* RoomInfoListViewController.swift in Sources */, + EC9E9B0A2575AF8A0007C0A0 /* CallBarPresentOperation.swift in Sources */, B12D79FD23E2462200FACEDC /* UserVerificationStartViewModelType.swift in Sources */, B1C543AE23A286A000DCA1FA /* KeyVerificationRequestStatusBubbleCell.swift in Sources */, B139C22121FE5D9D00BB68EC /* KeyBackupRecoverFromPassphraseViewState.swift in Sources */, @@ -6568,6 +6611,7 @@ B125FE1F231D5DF700B72806 /* SettingsDiscoveryViewModelType.swift in Sources */, EC85D7162477DCD7002C44C9 /* KeyVerificationScanConfirmationViewAction.swift in Sources */, EC711BAF24A63B58008F830C /* SecureBackupSetupCoordinatorType.swift in Sources */, + EC9E9B082575AF730007C0A0 /* CallVCDismissOperation.swift in Sources */, ECFBD5D4250A7AAF00DD5F5A /* ShowDirectoryCoordinatorType.swift in Sources */, EC1CA89C24C9C9A200DE9EBF /* SetupBiometricsViewModel.swift in Sources */, B157FAA323264AE900EBFBD4 /* SettingsDiscoveryThreePidDetailsViewState.swift in Sources */, @@ -6598,6 +6642,7 @@ B1CE83D62422817200D07506 /* KeyVerificationVerifyByScanningCoordinator.swift in Sources */, B1B5578F20EF568D00210D55 /* GroupTableViewCell.m in Sources */, 32D5D16023E1EE2700E3E37C /* ManageSessionViewController.m in Sources */, + EC9E9B062575AF5C0007C0A0 /* CallVCPresentOperation.swift in Sources */, B1B5573220EE6C4D00210D55 /* GroupHomeViewController.m in Sources */, B1B5595220EF9A8700210D55 /* RecentTableViewCell.m in Sources */, B158253B2475350A00604D79 /* EventFormatter+DTCoreTextFix.m in Sources */, @@ -6638,6 +6683,7 @@ EC1CA89624C9C9A200DE9EBF /* SetupBiometricsCoordinator.swift in Sources */, EC711B9024A63B37008F830C /* SecretsRecoveryWithKeyCoordinator.swift in Sources */, B1A6C10723881EF2002882FD /* SlidingModalPresenter.swift in Sources */, + EC9E9B102575B33E0007C0A0 /* CallServiceDelegate.swift in Sources */, B1B5573520EE6C4D00210D55 /* GroupDetailsViewController.m in Sources */, B12D7A0223E2462200FACEDC /* UserVerificationStartViewAction.swift in Sources */, EC85D7372477DD97002C44C9 /* LocalContactsSectionHeaderContainerView.m in Sources */, @@ -6678,6 +6724,7 @@ B14F143322144F6500FA0595 /* KeyBackupRecoverFromRecoveryKeyViewModel.swift in Sources */, B1B336C0242B933700F95EC4 /* KeyVerificationSelfVerifyStartViewModelType.swift in Sources */, 32A6001822C661100042C1D9 /* EditHistoryViewModel.swift in Sources */, + EC9E9B0C2575AF9B0007C0A0 /* CallBarDismissOperation.swift in Sources */, 32607D6E243E0A55006674CC /* KeyBackupRecoverFromPrivateKeyViewModelType.swift in Sources */, B1B558D020EF768F00210D55 /* RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.m in Sources */, B1B558CF20EF768F00210D55 /* RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.m in Sources */, @@ -6850,6 +6897,7 @@ 32242F0921E8B05F00725742 /* UIColor.swift in Sources */, B16932E720F3C37100746532 /* HomeMessagesSearchDataSource.m in Sources */, B1B4E9C224D471FD004D5C33 /* BubbleReactionsViewSizer.m in Sources */, + EC9E9B0E2575AFB80007C0A0 /* AsyncOperation.swift in Sources */, B12D79FF23E2462200FACEDC /* UserVerificationStartViewState.swift in Sources */, B1B558CE20EF768F00210D55 /* RoomOutgoingEncryptedAttachmentBubbleCell.m in Sources */, B1B5577D20EE84BF00210D55 /* CircleButton.m in Sources */, diff --git a/Riot/Categories/String.swift b/Riot/Categories/String.swift index f6bbd04bc..1de7cd8a5 100644 --- a/Riot/Categories/String.swift +++ b/Riot/Categories/String.swift @@ -38,4 +38,9 @@ extension String { func vc_caseInsensitiveContains(_ other: String) -> Bool { return self.range(of: other, options: .caseInsensitive) != nil } + + /// Returns a globally unique string + static var vc_unique: String { + return ProcessInfo.processInfo.globallyUniqueString + } } diff --git a/Riot/Managers/Call/AsyncOperation.swift b/Riot/Managers/Call/AsyncOperation.swift new file mode 100644 index 000000000..1e8e87c07 --- /dev/null +++ b/Riot/Managers/Call/AsyncOperation.swift @@ -0,0 +1,90 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class AsyncOperation: Operation { + + @objc private enum State: Int { + case ready + case executing + case finished + } + + private var _state: State = .ready + private let stateQueue = DispatchQueue(label: "AsyncOpStateQueue_\(String.vc_unique)", + attributes: .concurrent) + + @objc private dynamic var state: State { + get { + return stateQueue.sync { + _state + } + } set { + stateQueue.sync(flags: .barrier) { + _state = newValue + } + } + } + + override var isAsynchronous: Bool { + return true + } + + override var isReady: Bool { + return super.isReady && state == .ready + } + + override var isExecuting: Bool { + return state == .executing + } + + override var isFinished: Bool { + return state == .finished + } + + override func start() { + if isCancelled { + finish() + return + } + self.state = .executing + main() + } + + override func main() { + fatalError("Subclasses must implement `main` without calling super.") + } + + @objc class var keyPathsForValuesAffectingIsReady: Set { + return [#keyPath(state)] + } + + @objc class var keyPathsForValuesAffectingIsExecuting: Set { + return [#keyPath(state)] + } + + @objc class var keyPathsForValuesAffectingIsFinished: Set { + return [#keyPath(state)] + } + + func finish() { + if isExecuting { + self.state = .finished + } + } + +} diff --git a/Riot/Managers/Call/CallService.swift b/Riot/Managers/Call/CallService.swift new file mode 100644 index 000000000..d98f085dd --- /dev/null +++ b/Riot/Managers/Call/CallService.swift @@ -0,0 +1,249 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MatrixKit + +@objcMembers +class CallService: NSObject { + + private var callVCs: [String: CallViewController] = [:] + private var callBackgroundTasks: [String: MXBackgroundTask] = [:] + private weak var presentedCallVC: CallViewController? + private weak var inBarCallVC: CallViewController? + private var uiOperationQueue: OperationQueue = .main + private var isStarted: Bool = false + + private var isCallKitEnabled: Bool { + MXCallKitAdapter.callKitAvailable() && MXKAppSettings.standard()?.isCallKitEnabled == true + } + + /// Maximum number of concurrent calls allowed. + let maximumNumberOfConcurrentCalls: UInt = 2 + + /// Delegate object + weak var delegate: CallServiceDelegate? + + /// Start the service + func start() { + addCallObservers() + } + + /// Stop the service + func stop() { + removeCallObservers() + } + + /// Method to be called when the call status bar is tapped. + /// - Returns: If the user interaction handled or not + func callStatusBarButtonTapped() -> Bool { + guard let inBarCallVC = inBarCallVC else { + return false + } + dismissCallBar(for: inBarCallVC) + presentCallVC(inBarCallVC) + return true + } + + // MARK: - Observers + + private func addCallObservers() { + guard !isStarted else { + return + } + + NotificationCenter.default.addObserver(self, + selector: #selector(newCall(_:)), + name: NSNotification.Name(rawValue: kMXCallManagerNewCall), + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(callStateChanged(_:)), + name: NSNotification.Name(rawValue: kMXCallStateDidChange), + object: nil) + + isStarted = true + } + + private func removeCallObservers() { + guard isStarted else { + return + } + + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name(rawValue: kMXCallManagerNewCall), + object: nil) + NotificationCenter.default.removeObserver(self, + name: NSNotification.Name(rawValue: kMXCallStateDidChange), + object: nil) + + isStarted = false + } + + @objc + private func newCall(_ notification: Notification) { + guard let call = notification.object as? MXCall else { + return + } + + if !shouldHandleCall(call) { + return + } + + guard let newCallVC = CallViewController(call) else { + return + } + newCallVC.playRingtone = !isCallKitEnabled + newCallVC.delegate = self + callVCs[call.callId] = newCallVC + + if UIApplication.shared.applicationState == .background && call.isIncoming { + // Create backgound task. + // Without CallKit this will allow us to play vibro until the call was ended + // With CallKit we'll inform the system when the call is ended to let the system terminate our app to save resources + let handler = MXSDKOptions.sharedInstance().backgroundModeHandler + let callBackgroundTask = handler.startBackgroundTask(withName: "[CallService] addMatrixCallObserver", expirationHandler: nil) + + callBackgroundTasks[call.callId] = callBackgroundTask + } + + if call.isIncoming && isCallKitEnabled { + return + } else { + presentCallVC(newCallVC) + } + } + + @objc + private func callStateChanged(_ notification: Notification) { + guard let call = notification.object as? MXCall else { + return + } + + switch call.state { + case .createAnswer: + if call.isIncoming, isCallKitEnabled, let callVC = callVCs[call.callId] { + presentCallVC(callVC) + } + NSLog("[CallService] callStateChanged: call created answer: \(call.callId)") + case .ended: + NSLog("[CallService] callStateChanged: call ended: \(call.callId)") + endCall(withCallId: call.callId) + default: + break + } + } + + private func shouldHandleCall(_ call: MXCall) -> Bool { + if let delegate = delegate, !delegate.callService(self, shouldHandleNewCall: call) { + return false + } + return callVCs.count < maximumNumberOfConcurrentCalls + } + + private func endCall(withCallId callId: String) { + guard let callVC = callVCs[callId] else { + return + } + + let completion = { [weak self] in + self?.callVCs.removeValue(forKey: callId) + callVC.destroy() + self?.callBackgroundTasks[callId]?.stop() + self?.callBackgroundTasks.removeValue(forKey: callId) + } + + if inBarCallVC == callVC { + // this call currently in the status bar, + // first present it and then dismiss it + dismissCallBar(for: callVC) + presentCallVC(callVC) + } + dismissCallVC(callVC, completion: completion) + } + + // MARK: - Call Screens + + private func presentCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { + NSLog("[CallService] presentCallVC: call: \(String(describing: callVC.mxCall?.callId))") + + if let presentedCallVC = presentedCallVC { + dismissCallVC(presentedCallVC) + } + + let operation = CallVCPresentOperation(service: self, callVC: callVC) { [weak self] in + self?.presentedCallVC = callVC + completion?() + } + uiOperationQueue.addOperation(operation) + } + + private func dismissCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) { + NSLog("[CallService] dismissCallVC: call: \(String(describing: callVC.mxCall?.callId))") + + let operation = CallVCDismissOperation(service: self, callVC: callVC) { [weak self] in + if callVC == self?.presentedCallVC { + self?.presentedCallVC = nil + } + completion?() + } + uiOperationQueue.addOperation(operation) + } + + // MARK: - Call Bar + + private func presentCallBar(for callVC: CallViewController, completion: (() -> Void)? = nil) { + NSLog("[CallService] presentCallBar: call: \(String(describing: callVC.mxCall?.callId))") + + let operation = CallBarPresentOperation(service: self, callVC: callVC) { [weak self] in + self?.inBarCallVC = callVC + completion?() + } + uiOperationQueue.addOperation(operation) + } + + private func dismissCallBar(for callVC: CallViewController, completion: (() -> Void)? = nil) { + NSLog("[CallService] dismissCallBar: call: \(String(describing: callVC.mxCall?.callId))") + + let operation = CallBarDismissOperation(service: self, callVC: callVC) { [weak self] in + if callVC == self?.inBarCallVC { + self?.inBarCallVC = nil + } + completion?() + } + uiOperationQueue.addOperation(operation) + } + +} + +extension CallService: MXKCallViewControllerDelegate { + + func dismiss(_ callViewController: MXKCallViewController!, completion: (() -> Void)!) { + guard let callVC = callViewController as? CallViewController else { + // this call screen is not handled by this service + completion?() + return + } + + if callVC.mxCall == nil || callVC.mxCall.state == .ended { + // wait for the call state changes, will be handled there + return + } else { + dismissCallVC(callVC) + self.presentCallBar(for: callVC, completion: completion) + } + } + +} diff --git a/Riot/Managers/Call/CallServiceDelegate.swift b/Riot/Managers/Call/CallServiceDelegate.swift new file mode 100644 index 000000000..9b93080bf --- /dev/null +++ b/Riot/Managers/Call/CallServiceDelegate.swift @@ -0,0 +1,40 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@objc +protocol CallServiceDelegate: class { + // New call + func callService(_ service: CallService, + shouldHandleNewCall call: MXCall) -> Bool + + // Call screens + func callService(_ service: CallService, + presentCallViewController viewController: CallViewController, + completion:(() -> Void)?) + func callService(_ service: CallService, + dismissCallViewController viewController: CallViewController, + completion:(() -> Void)?) + + // Call Bar + func callService(_ service: CallService, + presentCallBarFor viewController: CallViewController, + completion:(() -> Void)?) + func callService(_ service: CallService, + dismissCallBarFor viewController: CallViewController, + completion:(() -> Void)?) +} diff --git a/Riot/Managers/Call/Operations/CallBarDismissOperation.swift b/Riot/Managers/Call/Operations/CallBarDismissOperation.swift new file mode 100644 index 000000000..102beb244 --- /dev/null +++ b/Riot/Managers/Call/Operations/CallBarDismissOperation.swift @@ -0,0 +1,40 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class CallBarDismissOperation: AsyncOperation { + + private var service: CallService + private var callVC: CallViewController + private var completion: (() -> Void)? + + init(service: CallService, + callVC: CallViewController, + completion: (() -> Void)? = nil) { + self.service = service + self.callVC = callVC + self.completion = completion + } + + override func main() { + service.delegate?.callService(service, dismissCallBarFor: callVC, completion: { + self.finish() + self.completion?() + }) + } + +} diff --git a/Riot/Managers/Call/Operations/CallBarPresentOperation.swift b/Riot/Managers/Call/Operations/CallBarPresentOperation.swift new file mode 100644 index 000000000..b7d685c44 --- /dev/null +++ b/Riot/Managers/Call/Operations/CallBarPresentOperation.swift @@ -0,0 +1,40 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class CallBarPresentOperation: AsyncOperation { + + private var service: CallService + private var callVC: CallViewController + private var completion: (() -> Void)? + + init(service: CallService, + callVC: CallViewController, + completion: (() -> Void)? = nil) { + self.service = service + self.callVC = callVC + self.completion = completion + } + + override func main() { + service.delegate?.callService(service, presentCallBarFor: callVC, completion: { + self.finish() + self.completion?() + }) + } + +} diff --git a/Riot/Managers/Call/Operations/CallVCDismissOperation.swift b/Riot/Managers/Call/Operations/CallVCDismissOperation.swift new file mode 100644 index 000000000..a197b9a1c --- /dev/null +++ b/Riot/Managers/Call/Operations/CallVCDismissOperation.swift @@ -0,0 +1,40 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class CallVCDismissOperation: AsyncOperation { + + private var service: CallService + private var callVC: CallViewController + private var completion: (() -> Void)? + + init(service: CallService, + callVC: CallViewController, + completion: (() -> Void)? = nil) { + self.service = service + self.callVC = callVC + self.completion = completion + } + + override func main() { + service.delegate?.callService(service, dismissCallViewController: callVC, completion: { + self.finish() + self.completion?() + }) + } + +} diff --git a/Riot/Managers/Call/Operations/CallVCPresentOperation.swift b/Riot/Managers/Call/Operations/CallVCPresentOperation.swift new file mode 100644 index 000000000..e06f0e4a7 --- /dev/null +++ b/Riot/Managers/Call/Operations/CallVCPresentOperation.swift @@ -0,0 +1,40 @@ +// +// Copyright 2020 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class CallVCPresentOperation: AsyncOperation { + + private var service: CallService + private var callVC: CallViewController + private var completion: (() -> Void)? + + init(service: CallService, + callVC: CallViewController, + completion: (() -> Void)? = nil) { + self.service = service + self.callVC = callVC + self.completion = completion + } + + override func main() { + service.delegate?.callService(service, presentCallViewController: callVC, completion: { + self.finish() + self.completion?() + }) + } + +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.h b/Riot/Modules/Application/LegacyAppDelegate.h index f844adcfc..41afbabc2 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.h +++ b/Riot/Modules/Application/LegacyAppDelegate.h @@ -50,7 +50,11 @@ extern NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey; */ extern NSString *const AppDelegateUniversalLinkDidChangeNotification; -@interface LegacyAppDelegate : UIResponder +@interface LegacyAppDelegate : UIResponder < +UIApplicationDelegate, +UISplitViewControllerDelegate, +UINavigationControllerDelegate, +JitsiViewControllerDelegate> { // background sync management void (^_completionHandler)(UIBackgroundFetchResult); diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 5822ef3d1..625931ed6 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -90,7 +90,7 @@ NSString *const AppDelegateDidValidateEmailNotificationClientSecretKey = @"AppDe NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUniversalLinkDidChangeNotification"; -@interface LegacyAppDelegate () +@interface LegacyAppDelegate () { /** Reachability observer @@ -113,16 +113,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni id addedAccountObserver; id removedAccountObserver; - /** - matrix call observer used to handle incoming/outgoing call. - */ - id matrixCallObserver; - - /** - The current call view controller (if any). - */ - CallViewController *currentCallViewController; - /** Incoming room key requests observers */ @@ -177,7 +167,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni */ BOOL isErrorNotificationSuspended; - /** The listeners to call events. There is one listener per MXSession. @@ -236,6 +225,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @property (nonatomic, strong) PushNotificationService *pushNotificationService; @property (nonatomic, strong) PushNotificationStore *pushNotificationStore; @property (nonatomic, strong) LocalAuthenticationService *localAuthenticationService; +@property (nonatomic, strong) CallService *callService; @property (nonatomic, strong) MajorUpdateManager *majorUpdateManager; @@ -437,7 +427,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [NSBundle mxk_setLanguage:language]; [NSBundle mxk_setFallbackLanguage:@"en"]; - mxSessionArray = [NSMutableArray array]; callEventsListeners = [NSMutableDictionary dictionary]; @@ -459,6 +448,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [[Analytics sharedInstance] start]; self.localAuthenticationService = [[LocalAuthenticationService alloc] initWithPinCodePreferences:[PinCodePreferences shared]]; + + self.callService = [[CallService alloc] init]; + self.callService.delegate = self; self.pushNotificationStore = [PushNotificationStore new]; self.pushNotificationService = [[PushNotificationService alloc] initWithPushNotificationStore:self.pushNotificationStore]; @@ -1756,8 +1748,8 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } else if (mxSession.state == MXSessionStateStoreDataReady) { - // A new call observer may be added here - [self addMatrixCallObserver]; + // start the call service + [self.callService start]; // Look for the account related to this session. NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; @@ -1989,10 +1981,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni [mxSessionArray removeObject:mxSession]; - if (!mxSessionArray.count && matrixCallObserver) + if (!mxSessionArray.count) { - [[NSNotificationCenter defaultCenter] removeObserver:matrixCallObserver]; - matrixCallObserver = nil; + // if no session left, stop the call service + [self.callService stop]; } } @@ -2191,113 +2183,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)addMatrixCallObserver -{ - if (matrixCallObserver) - { - return; - } - - MXWeakify(self); - - // Register call observer in order to handle incoming calls - matrixCallObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerNewCall - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification *notif) - { - MXStrongifyAndReturnIfNil(self); - - // Ignore the call if a call is already in progress - if (!self->currentCallViewController && !self->_jitsiViewController) - { - MXCall *mxCall = (MXCall*)notif.object; - - BOOL isCallKitEnabled = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled; - - // Prepare the call view controller - self->currentCallViewController = [CallViewController callViewController:nil]; - self->currentCallViewController.playRingtone = !isCallKitEnabled; - self->currentCallViewController.mxCall = mxCall; - self->currentCallViewController.delegate = self; - - UIApplicationState applicationState = UIApplication.sharedApplication.applicationState; - - // App has been woken by PushKit notification in the background - if (applicationState == UIApplicationStateBackground && mxCall.isIncoming) - { - // Create backgound task. - // Without CallKit this will allow us to play vibro until the call was ended - // With CallKit we'll inform the system when the call is ended to let the system terminate our app to save resources - id handler = [MXSDKOptions sharedInstance].backgroundModeHandler; - id callBackgroundTask = [handler startBackgroundTaskWithName:@"[AppDelegate] addMatrixCallObserver" expirationHandler:nil]; - - MXWeakify(self); - - // Start listening for call state change notifications - __weak NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - __block id token = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallStateDidChange - object:mxCall - queue:nil - usingBlock:^(NSNotification * _Nonnull note) { - - MXStrongifyAndReturnIfNil(self); - - MXCall *call = (MXCall *)note.object; - - if (call.state == MXCallStateEnded) - { - // Set call vc to nil to let our app handle new incoming calls even it wasn't killed by the system - [self->currentCallViewController destroy]; - self->currentCallViewController = nil; - [notificationCenter removeObserver:token]; - [callBackgroundTask stop]; - } - }]; - } - - if (mxCall.isIncoming && isCallKitEnabled) - { - MXWeakify(self); - - // Let's CallKit display the system incoming call screen - // Show the callVC only after the user answered the call - __weak NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - __block id token = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallStateDidChange - object:mxCall - queue:nil - usingBlock:^(NSNotification * _Nonnull note) { - - MXStrongifyAndReturnIfNil(self); - - MXCall *call = (MXCall *)note.object; - - NSLog(@"[AppDelegate] call.state: %@", call); - - if (call.state == MXCallStateCreateAnswer) - { - [notificationCenter removeObserver:token]; - - NSLog(@"[AppDelegate] presentCallViewController"); - [self presentCallViewController:NO completion:nil]; - } - else if (call.state == MXCallStateEnded) - { - [notificationCenter removeObserver:token]; - - // Set call vc to nil to let our app handle new incoming calls even it wasn't killed by the system - [self dismissCallViewController:self->currentCallViewController completion:nil]; - } - }]; - } - else - { - [self presentCallViewController:YES completion:nil]; - } - } - }]; -} - - (void)handleAppState { MXSession *mainSession = self.mxSessions.firstObject; @@ -3039,78 +2924,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni }]; } -#pragma mark - MXKCallViewControllerDelegate - -- (void)dismissCallViewController:(MXKCallViewController *)callViewController completion:(void (^)(void))completion -{ - if (currentCallViewController && callViewController == currentCallViewController) - { - if (callViewController.isBeingPresented) - { - // Here the presentation of the call view controller is in progress - // Postpone the dismiss - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self dismissCallViewController:callViewController completion:completion]; - }); - } - // Check whether the call view controller is actually presented - else if (callViewController.presentingViewController) - { - BOOL callIsEnded = (callViewController.mxCall.state == MXCallStateEnded); - NSLog(@"Call view controller is dismissed (%d)", callIsEnded); - - [callViewController dismissViewControllerAnimated:YES completion:^{ - - if (!callIsEnded) - { - 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) - { - completion(); - } - - }]; - - if (callIsEnded) - { - [self removeCallStatusBar]; - - // Release properly - [currentCallViewController destroy]; - currentCallViewController = nil; - } - } - else if (_callStatusBarWindow) - { - // Here the call view controller was not presented. - NSLog(@"Call view controller was not presented"); - - // Workaround to manage the "back to call" banner: present temporarily the call screen. - // This will correctly manage the navigation bar layout. - [self presentCallViewController:YES completion:^{ - - [self dismissCallViewController:currentCallViewController completion:completion]; - - }]; - } - else - { - // Release properly - [currentCallViewController destroy]; - currentCallViewController = nil; - } - } -} - - (void)promptForStunServerFallback { [_errorNotification dismissViewControllerAnimated:NO completion:nil]; @@ -3162,7 +2975,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video { #ifdef CALL_STACK_JINGLE - if (!_jitsiViewController && !currentCallViewController) + if (!_jitsiViewController) { MXWeakify(self); [self checkPermissionForNativeWidget:jitsiWidget fromUrl:JitsiService.shared.serverURL completion:^(BOOL granted) { @@ -3448,9 +3261,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni - (void)onCallStatusBarButtonPressed { - if (currentCallViewController) + if ([_callService callStatusBarButtonTapped]) { - [self presentCallViewController:YES completion:nil]; + return; } else if (_jitsiViewController) { @@ -3458,21 +3271,6 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } -- (void)presentCallViewController:(BOOL)animated completion:(void (^)(void))completion -{ - [self removeCallStatusBar]; - - if (currentCallViewController) - { - if (@available(iOS 13.0, *)) - { - currentCallViewController.modalPresentationStyle = UIModalPresentationFullScreen; - } - - [self presentViewController:currentCallViewController animated:animated completion:completion]; - } -} - - (void)statusBarDidChangeFrame { UIApplication *app = [UIApplication sharedApplication]; @@ -4646,4 +4444,71 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } } +#pragma mark - CallServiceDelegate + +- (BOOL)callService:(CallService *)service shouldHandleNewCall:(MXCall *)call +{ + // Ignore the call if a call is already in progress + return _jitsiViewController == nil; +} + +- (void)callService:(CallService *)service presentCallViewController:(CallViewController *)viewController completion:(void (^)(void))completion +{ + if (@available(iOS 13.0, *)) + { + viewController.modalPresentationStyle = UIModalPresentationFullScreen; + } + + [self presentViewController:viewController animated:YES completion:completion]; +} + +- (void)callService:(CallService *)service dismissCallViewController:(CallViewController *)viewController completion:(void (^)(void))completion +{ + // Check whether the call view controller is actually presented + if (viewController.presentingViewController) + { + [viewController dismissViewControllerAnimated:YES completion:^{ + + if (viewController.shouldPromptForStunServerFallback) + { + [self promptForStunServerFallback]; + } + + if (completion) + { + completion(); + } + + }]; + } + else + { + if (completion) + { + completion(); + } + } +} + +- (void)callService:(CallService *)service presentCallBarFor:(CallViewController *)viewController completion:(void (^)(void))completion +{ + NSString *btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"active_call_details", @"Vector", nil), viewController.callerNameLabel.text]; + [self addCallStatusBar:btnTitle]; + + if (completion) + { + completion(); + } +} + +- (void)callService:(CallService *)service dismissCallBarFor:(CallViewController *)viewController completion:(void (^)(void))completion +{ + [self removeCallStatusBar]; + + if (completion) + { + completion(); + } +} + @end diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 4271e28f1..7b2a11e00 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -24,3 +24,4 @@ #import "MXSession+Riot.h" #import "RoomFilesViewController.h" #import "RoomSettingsViewController.h" +#import "CallViewController.h" diff --git a/Riot/Utils/DataProtectionHelper.swift b/Riot/Utils/DataProtectionHelper.swift index fd8f23626..6868c23dd 100644 --- a/Riot/Utils/DataProtectionHelper.swift +++ b/Riot/Utils/DataProtectionHelper.swift @@ -23,7 +23,7 @@ final class DataProtectionHelper { /// - Returns: true if the state detected static func isDeviceInRebootedAndLockedState(appGroupIdentifier: String? = nil) -> Bool { - let dummyString = String.unique + let dummyString = String.vc_unique guard let dummyData = dummyString.data(using: .utf8) else { return true } @@ -38,7 +38,7 @@ final class DataProtectionHelper { } // add a unique filename - url = url.appendingPathComponent(String.unique) + url = url.appendingPathComponent(String.vc_unique) try dummyData.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication) let readData = try Data(contentsOf: url) @@ -54,12 +54,3 @@ final class DataProtectionHelper { } } - -extension String { - - /// Returns a globally unique string - static var unique: String { - return ProcessInfo.processInfo.globallyUniqueString - } - -}