Commit 918d99fe authored by Kateryna Kostiuk's avatar Kateryna Kostiuk

call: use CallKit

- use Call Kit for incoming call screen
- add Jami calls to recent call list
- start Jami call from recent call list

Change-Id: I6ee9bb75ddf7c23c1ff15fca904055bceba1b7ba
parent d2ec58d4
......@@ -90,6 +90,7 @@
0E0FF1B51FC3947B003898C2 /* DBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B41FC3947B003898C2 /* DBManager.swift */; };
0E0FF1B71FC398B3003898C2 /* ConversationDataHepler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B61FC398B3003898C2 /* ConversationDataHepler.swift */; };
0E0FF1B91FC398C5003898C2 /* InteractionDataHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E0FF1B81FC398C5003898C2 /* InteractionDataHelper.swift */; };
0E13A91C22B844B100A12A54 /* NSUserActivity+Call.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E13A91B22B844B100A12A54 /* NSUserActivity+Call.swift */; };
0E20E4C72031FF560087C868 /* BlockContactsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E20E4C52031FF560087C868 /* BlockContactsCell.swift */; };
0E20E4C82031FF560087C868 /* BlockContactsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0E20E4C62031FF560087C868 /* BlockContactsCell.xib */; };
0E2D5F531F9145C800D574BF /* LinkNewDeviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E2D5F521F9145C800D574BF /* LinkNewDeviceCell.swift */; };
......@@ -152,6 +153,8 @@
0EB1A5D11F8EBE23009923E2 /* DeviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EB1A5D01F8EBE23009923E2 /* DeviceCell.swift */; };
0EBB72A92034F44200D88F46 /* ProfilesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBB72A82034F44200D88F46 /* ProfilesService.swift */; };
0EBCAA4E202E60F000E2A545 /* default.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0EBCAA4D202E60F000E2A545 /* default.wav */; };
0ECB4E2822B2D4840097CD7B /* CallsProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECB4E2722B2D4840097CD7B /* CallsProviderDelegate.swift */; };
0ECB4E2A22B2D4BB0097CD7B /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ECB4E2922B2D4BB0097CD7B /* CallKit.framework */; };
0ECEE9A3220D1935000E1CF4 /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ECEE9A2220D1935000E1CF4 /* VideoToolbox.framework */; };
0ED2B6FA1F96A075001572F0 /* LinkNewDeviceViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */; };
0ED2B6FC1F96A158001572F0 /* LinkNewDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */; };
......@@ -411,6 +414,7 @@
0E0FF1B41FC3947B003898C2 /* DBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBManager.swift; sourceTree = "<group>"; };
0E0FF1B61FC398B3003898C2 /* ConversationDataHepler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationDataHepler.swift; sourceTree = "<group>"; };
0E0FF1B81FC398C5003898C2 /* InteractionDataHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionDataHelper.swift; sourceTree = "<group>"; };
0E13A91B22B844B100A12A54 /* NSUserActivity+Call.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Call.swift"; sourceTree = "<group>"; };
0E20E4C52031FF560087C868 /* BlockContactsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockContactsCell.swift; sourceTree = "<group>"; };
0E20E4C62031FF560087C868 /* BlockContactsCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BlockContactsCell.xib; sourceTree = "<group>"; };
0E2D5F521F9145C800D574BF /* LinkNewDeviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceCell.swift; sourceTree = "<group>"; };
......@@ -477,6 +481,8 @@
0EB1A5D01F8EBE23009923E2 /* DeviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceCell.swift; sourceTree = "<group>"; };
0EBB72A82034F44200D88F46 /* ProfilesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesService.swift; sourceTree = "<group>"; };
0EBCAA4D202E60F000E2A545 /* default.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = default.wav; sourceTree = "<group>"; };
0ECB4E2722B2D4840097CD7B /* CallsProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallsProviderDelegate.swift; sourceTree = "<group>"; };
0ECB4E2922B2D4BB0097CD7B /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; };
0ECEE9A2220D1935000E1CF4 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; };
0ED2B6F91F96A075001572F0 /* LinkNewDeviceViewController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LinkNewDeviceViewController.storyboard; sourceTree = "<group>"; };
0ED2B6FB1F96A158001572F0 /* LinkNewDeviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkNewDeviceViewController.swift; sourceTree = "<group>"; };
......@@ -691,6 +697,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0ECB4E2A22B2D4BB0097CD7B /* CallKit.framework in Frameworks */,
0ECEE9A3220D1935000E1CF4 /* VideoToolbox.framework in Frameworks */,
628F4DCF206C0AEE0009C44C /* libcrypto.a in Frameworks */,
628F4DCD206BF4740009C44C /* libtls.a in Frameworks */,
......@@ -797,6 +804,7 @@
02AED8171DD4C4B000F740BA /* Frameworks */ = {
isa = PBXGroup;
children = (
0ECB4E2922B2D4BB0097CD7B /* CallKit.framework */,
0E639459224AB32200C0890A /* Contacts.framework */,
0EB12451224AB1030025F8CA /* ContactsUI.framework */,
0ECEE9A2220D1935000E1CF4 /* VideoToolbox.framework */,
......@@ -854,6 +862,7 @@
0EBB72A82034F44200D88F46 /* ProfilesService.swift */,
62B60AF320489E7C001BEACF /* DataTransferService.swift */,
62B60AFA2048A437001BEACF /* DataTransferAdapterDelegate.swift */,
0ECB4E2722B2D4840097CD7B /* CallsProviderDelegate.swift */,
);
path = Services;
sourceTree = "<group>";
......@@ -930,6 +939,7 @@
621231F81F880EDF009B86F0 /* UILabel+Ring.swift */,
0EDCC85F1F98150500B121D7 /* UIView+Rx.swift */,
62006E03203F4DD6003C3197 /* UITextField+Helpers.swift */,
0E13A91B22B844B100A12A54 /* NSUserActivity+Call.swift */,
);
path = Extensions;
sourceTree = "<group>";
......@@ -1925,6 +1935,7 @@
0E6F544F223C0ED600ECC3CE /* AccountPickerAdapter.swift in Sources */,
1AABA7461F0FE9C000739605 /* UIColor+Ring.swift in Sources */,
1A5DC0201F355DCF0075E8EF /* ContactsService.swift in Sources */,
0ECB4E2822B2D4840097CD7B /* CallsProviderDelegate.swift in Sources */,
1A2D18C71F29180700B2C785 /* DeviceModel.swift in Sources */,
1A20418F1F1EAC0E00C08435 /* Coordinator.swift in Sources */,
0E49097C1FEACA4B005CAA50 /* CallViewModel.swift in Sources */,
......@@ -1958,6 +1969,7 @@
0E99F1A022417A0400CF8BD6 /* JamiURI.swift in Sources */,
1A5DC02E1F3565640075E8EF /* ConversationViewModel.swift in Sources */,
0EBB72A92034F44200D88F46 /* ProfilesService.swift in Sources */,
0E13A91C22B844B100A12A54 /* NSUserActivity+Call.swift in Sources */,
1A2D189C1F264AD900B2C785 /* UIViewController+Ring.swift in Sources */,
02C9B63F1E1D4E8C00F82F0C /* ServiceEvent.swift in Sources */,
62B60AF420489E7C001BEACF /* DataTransferService.swift in Sources */,
......
......@@ -28,7 +28,7 @@ import PushKit
import ContactsUI
// swiftlint:disable identifier_name
// swiftlint:disable type_body_length
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
......@@ -42,6 +42,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private let videoService = VideoService(withVideoAdapter: VideoAdapter())
private let audioService = AudioService(withAudioAdapter: AudioAdapter())
private let networkService = NetworkService()
private let callsProvider: CallsProviderDelegate = CallsProviderDelegate()
private var conversationManager: ConversationsManager?
private var interactionsManager: GeneratedInteractionsManager?
private lazy var callService: CallsService = {
......@@ -78,7 +79,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
withVideoService: self.videoService,
withAudioService: self.audioService,
withDataTransferService: self.dataTransferService,
withProfileService: self.profileService)
withProfileService: self.profileService,
withCallsProvider: self.callsProvider)
}()
private lazy var appCoordinator: AppCoordinator = {
return AppCoordinator(with: self.injectionBag)
......@@ -171,6 +173,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} else {
self.unregisterVoipNotifications()
}
if #available(iOS 10.0, *) {
return
}
// reimit new call signal to show incoming call alert
self.callService.checkForIncomingCall()
}, onError: { _ in
......@@ -219,8 +224,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func applicationDidBecomeActive(_ application: UIApplication) {
self.callService.checkForIncomingCall()
self.clearBadgeNumber()
if #available(iOS 10.0, *) {
return
}
self.callService.checkForIncomingCall()
}
func prepareVideoAcceleration() {
......@@ -234,7 +242,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// MARK: - Ring Daemon
fileprivate func startDaemon() {
do {
try self.daemonService.startDaemon()
} catch StartDaemonError.initializationFailure {
......@@ -412,6 +419,76 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
return rootViewController
}
func findContactAndStartCall(hash: String, isVideo: Bool) {
//if saved jami hash
if hash.isSHA1() {
let contactUri = JamiURI(schema: URIType.ring, infoHach: hash)
self.findAccountAndStartCall(uri: contactUri, isVideo: isVideo, type: AccountType.ring)
return
}
//if saved jami registered name
self.nameService.usernameLookupStatus
.observeOn(MainScheduler.instance)
.filter({ usernameLookupStatus in
usernameLookupStatus.name == hash
})
.take(1)
.subscribe(onNext: { usernameLookupStatus in
if usernameLookupStatus.state == .found {
guard let address = usernameLookupStatus.address else {return}
let contactUri = JamiURI(schema: URIType.ring, infoHach: address)
self.findAccountAndStartCall(uri: contactUri, isVideo: isVideo, type: AccountType.ring)
} else {
//if saved sip contact
let contactUri = JamiURI(schema: URIType.sip, infoHach: hash)
self.findAccountAndStartCall(uri: contactUri, isVideo: isVideo, type: AccountType.sip)
}
}).disposed(by: self.disposeBag)
self.nameService.lookupName(withAccount: "", nameserver: "", name: hash)
}
func findAccountAndStartCall(uri: JamiURI, isVideo: Bool, type: AccountType) {
guard let currentAccount = self.accountService
.currentAccount else { return }
var hash = uri.hash ?? ""
var uriString = uri.uriString ?? ""
for account in self.accountService.accounts where account.type == type {
if type == AccountType.sip {
let conatactUri = JamiURI(schema: URIType.sip,
infoHach: hash,
account: account)
hash = conatactUri.hash ?? ""
uriString = conatactUri.uriString ?? ""
}
if hash.isEmpty || uriString.isEmpty {return}
self.contactsService
.getProfileForUri(uri: uriString,
accountId: account.id)
.subscribe(onNext: { (profile) in
if currentAccount != account {
self.accountService.currentAccount = account
}
self.appCoordinator
.startCall(participant: hash,
name: profile.alias ?? "",
isVideo: isVideo)
}).disposed(by: self.disposeBag)
}
}
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if #available(iOS 10.0, *) {
guard let handle = userActivity.startCallHandle else {
return false
}
self.findContactAndStartCall(hash: handle.hash, isVideo: handle.isVideo)
return true
}
return false
}
}
extension AppDelegate: PKPushRegistryDelegate {
......@@ -422,10 +499,13 @@ extension AppDelegate: PKPushRegistryDelegate {
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
self.accountService.pushNotificationReceived(data: payload.dictionaryPayload)
if #available(iOS 10.0, *) {
return
}
if UIApplication.shared.applicationState != .active {
self.audioService.startAVAudioSession()
}
self.accountService.pushNotificationReceived(data: payload.dictionaryPayload)
}
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
......
......@@ -39,6 +39,7 @@
#import <UserNotifications/UserNotifications.h>
#import "CallsAdapter.h"
#import <PushKit/PushKit.h>
#import <CallKit/CallKit.h>
#import <UserNotifications/UserNotifications.h>
#import <GSKStretchyHeaderView/GSKStretchyHeaderView.h>
#import "DataTransferAdapter.h"
......@@ -101,7 +101,7 @@ class ButtonsContainerView: UIView, NibLoadable {
if self.viewModel?.isIncoming ?? false {
acceptCallButton.isHidden = false
cancelButtonBottomConstraint.constant = 60
cancelButtonCenterConstraint.constant = 55
cancelButtonCenterConstraint.constant = -80
return
}
cancelButtonCenterConstraint.constant = 0
......
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
......@@ -215,7 +215,7 @@
</userDefinedRuntimeAttributes>
</button>
<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rfz-9h-HoH" userLabel="Accept">
<rect key="frame" x="42.5" y="10" width="70" height="70"/>
<rect key="frame" x="312.5" y="10" width="70" height="70"/>
<color key="backgroundColor" red="0.45098039215686275" green="0.98039215686274506" blue="0.47450980392156861" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="70" id="cRi-F4-pPw"/>
......@@ -236,9 +236,9 @@
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="rfz-9h-HoH" firstAttribute="leading" secondItem="ZxT-mA-1xU" secondAttribute="trailing" constant="90" id="48d-Rh-4pI"/>
<constraint firstAttribute="bottom" secondItem="w5l-pw-1ET" secondAttribute="bottom" id="6qu-Nn-b1Y"/>
<constraint firstAttribute="height" constant="200" id="Gjk-7U-rEe"/>
<constraint firstItem="ZxT-mA-1xU" firstAttribute="leading" secondItem="rfz-9h-HoH" secondAttribute="trailing" constant="40" id="Hwg-PM-KNI"/>
<constraint firstAttribute="bottom" secondItem="ZxT-mA-1xU" secondAttribute="bottom" constant="120" id="Ilu-Zu-JqW"/>
<constraint firstItem="rfz-9h-HoH" firstAttribute="centerY" secondItem="ZxT-mA-1xU" secondAttribute="centerY" id="NZg-SL-A31"/>
<constraint firstAttribute="trailing" secondItem="w5l-pw-1ET" secondAttribute="trailing" id="TnQ-lp-9B9"/>
......@@ -264,13 +264,13 @@
</view>
</objects>
<resources>
<image name="audio_running" width="48" height="48"/>
<image name="call_button" width="43.5" height="43.5"/>
<image name="dialpad" width="75" height="75"/>
<image name="disable_speakerphone" width="48" height="48"/>
<image name="pause_call" width="48" height="48"/>
<image name="stop_call" width="48" height="48"/>
<image name="switch_camera" width="50" height="50"/>
<image name="video_running" width="43.5" height="43.5"/>
<image name="audio_running" width="24" height="24"/>
<image name="call_button" width="29" height="29"/>
<image name="dialpad" width="37.5" height="37.5"/>
<image name="disable_speakerphone" width="24" height="24"/>
<image name="pause_call" width="24" height="24"/>
<image name="stop_call" width="24" height="24"/>
<image name="switch_camera" width="25" height="25"/>
<image name="video_running" width="29" height="29"/>
</resources>
</document>
......@@ -177,7 +177,7 @@ class CallViewController: UIViewController, StoryboardBased, ViewModelBased {
self.buttonsContainer.viewModel = self.viewModel.containerViewModel
self.buttonsContainer.cancelButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.viewModel.cancelCall()
self?.viewModel.cancelCall(stopProvider: true)
self?.removeFromScreen()
}).disposed(by: self.disposeBag)
......
......@@ -24,7 +24,7 @@ import RxSwift
import SwiftyBeaver
import Contacts
import RxCocoa
// swiftlint:disable type_body_length
class CallViewModel: Stateable, ViewModel {
//stateable
......@@ -112,6 +112,9 @@ class CallViewModel: Stateable, ViewModel {
if hide {
self?.videoService.setCameraOrientation(orientation: UIDevice.current.orientation, callID: nil)
self?.videoService.stopAudioDevice()
if #available(iOS 10.0, *), let call = self?.call {
self?.callsProvider.stopCall(callUUID: call.callUUID)
}
}
return hide
})
......@@ -300,6 +303,7 @@ class CallViewModel: Stateable, ViewModel {
var containerViewModel: ButtonsContainerViewModel?
let injectionBag: InjectionBag
let callsProvider: CallsProviderDelegate
required init(with injectionBag: InjectionBag) {
self.callService = injectionBag.callService
......@@ -308,6 +312,7 @@ class CallViewModel: Stateable, ViewModel {
self.videoService = injectionBag.videoService
self.audioService = injectionBag.audioService
self.profileService = injectionBag.profileService
self.callsProvider = injectionBag.callsProvider
self.injectionBag = injectionBag
callService.currentCall.filter({ [weak self] call in
......@@ -319,6 +324,20 @@ class CallViewModel: Stateable, ViewModel {
.setCameraOrientation(orientation: UIDevice.current.orientation,
callID: self?.call?.callId)
}).disposed(by: self.disposeBag)
callsProvider.sharedResponseStream
.filter({ [unowned self] serviceEvent in
guard let callUUID: String = serviceEvent
.getEventInput(ServiceEventInput.callUUID) else {return false}
return callUUID == self.call?.callUUID.uuidString
}).subscribe(onNext: { [unowned self] serviceEvent in
if serviceEvent.eventType == ServiceEventType.callProviderAnswerCall {
self.answerCall()
.subscribe()
.disposed(by: self.disposeBag)
} else if serviceEvent.eventType == ServiceEventType.callProviderCancellCall {
self.cancelCall(stopProvider: false)
}
}).disposed(by: self.disposeBag)
}
static func formattedDurationFrom(interval: Int) -> String {
......@@ -333,10 +352,13 @@ class CallViewModel: Stateable, ViewModel {
}
}
func cancelCall() {
func cancelCall(stopProvider: Bool) {
guard let call = self.call else {
return
}
if #available(iOS 10.0, *), stopProvider {
self.callsProvider.stopCall(callUUID: call.callUUID)
}
self.callService.hangUp(callId: call.callId)
.subscribe(onCompleted: { [weak self] in
// switch to either spk or headset (if connected) for loud ringtone
......@@ -358,7 +380,6 @@ class CallViewModel: Stateable, ViewModel {
}
func placeCall(with uri: String, userName: String, isAudioOnly: Bool = false) {
guard let account = self.accountService.currentAccount else {
return
}
......@@ -371,7 +392,12 @@ class CallViewModel: Stateable, ViewModel {
userName: userName,
isAudioOnly: isAudioOnly)
.subscribe(onSuccess: { [weak self] callModel in
callModel.callUUID = UUID()
self?.call = callModel
if #available(iOS 10.0, *) {
self?.callsProvider
.startCall(account: account, call: callModel)
}
}).disposed(by: self.disposeBag)
}
......
......@@ -192,4 +192,21 @@ final class AppCoordinator: Coordinator, StateableResponsive {
conversationCoordinator.puchConversation(participantId: participantID)
}
}
func startCall(participant: String, name: String, isVideo: Bool) {
for child in self.childCoordinators {
if let childCoordinattor = child as? ConversationsCoordinator {
if isVideo {
childCoordinattor.stateSubject
.onNext(ConversationState
.startCall(contactRingId: participant, userName: name))
return
}
childCoordinattor.stateSubject
.onNext(ConversationState
.startAudioCall(contactRingId: participant, userName: name))
}
return
}
}
}
......@@ -35,6 +35,7 @@ class InjectionBag {
let audioService: AudioService
let dataTransferService: DataTransferService
let profileService: ProfilesService
let callsProvider: CallsProviderDelegate
init (withDaemonService daemonService: DaemonService,
withAccountService accountService: AccountsService,
......@@ -47,7 +48,8 @@ class InjectionBag {
withVideoService videoService: VideoService,
withAudioService audioService: AudioService,
withDataTransferService dataTransferService: DataTransferService,
withProfileService profileService: ProfilesService) {
withProfileService profileService: ProfilesService,
withCallsProvider callsProvider: CallsProviderDelegate) {
self.daemonService = daemonService
self.accountService = accountService
self.nameService = nameService
......@@ -60,5 +62,6 @@ class InjectionBag {
self.audioService = audioService
self.dataTransferService = dataTransferService
self.profileService = profileService
self.callsProvider = callsProvider
}
}
/*
* Copyright (C) 2019 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import Intents
@available(iOS 10.0, *)
extension NSUserActivity {
var startCallHandle: (hash: String, isVideo: Bool)? {
guard let interaction = interaction else { return nil }
let startVideoCallIntent = interaction.intent as? INStartVideoCallIntent
let startAudioCallIntent = interaction.intent as? INStartAudioCallIntent
if startVideoCallIntent == nil && startAudioCallIntent == nil {
return nil
}
let isVideo = startVideoCallIntent != nil ? true : false
if isVideo {
guard
let intent = startVideoCallIntent,
let contact = intent.contacts?.first,
let handle = contact.personHandle,
let value = handle.value else {
return nil
}
return(value, true)
}
guard
let intent = startAudioCallIntent,
let contact = intent.contacts?.first,
let handle = contact.personHandle,
let value = handle.value else {
return nil
}
return(value, false)
}
}
......@@ -44,6 +44,19 @@ extension String {
return false
}
var isPhoneNumber: Bool {
do {
let detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.phoneNumber.rawValue)
let matches = detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.count))
guard let res = matches.first else { return false }
return res.resultType == .phoneNumber &&
res.range.location == 0 &&
res.range.length == self.count
} catch {
return false
}
}
func toMD5HexString() -> String {
guard let messageData = self.data(using: .utf8) else {return ""}
var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH))
......
......@@ -41,6 +41,7 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa
let callService: CallsService
let accountService: AccountsService
let conversationService: ConversationsService
let callsProvider: CallsProviderDelegate
required init (with injectionBag: InjectionBag) {
self.injectionBag = injectionBag
......@@ -48,6 +49,7 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa
self.callService = injectionBag.callService
self.accountService = injectionBag.accountService
self.conversationService = injectionBag.conversationsService
self.callsProvider = injectionBag.callsProvider
self.addLockFlags()
self.stateSubject.subscribe(onNext: { [unowned self] (state) in
......@@ -60,7 +62,7 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa
case .showGeneralSettings:
self.showGeneralSettings()
case .navigateToCall(let call):
self.openCall(call: call)
self.presentCallController(call: call)
default:
break
}
......@@ -70,11 +72,12 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (call) in
self.showCallController(call: call)
self.showIncomingCall(call: call)
}).disposed(by: self.disposeBag)
self.navigationViewController.viewModel = ChatTabBarItemViewModel(with: self.injectionBag)
self.callbackPlaceCall()
NotificationCenter.default.addObserver(self, selector: #selector(self.incomingCall(_:)), name: NSNotification.Name(NotificationName.answerCallFromNotifications.rawValue), object: nil)
//for iOS version less than 10 support open call from notification
NotificationCenter.default.addObserver(self, selector: #selector(self.answerIncomingCall(_:)), name: NSNotification.Name(NotificationName.answerCallFromNotifications.rawValue), object: nil)
self.accountService.currentAccountChanged
.subscribe(onNext: {[unowned self] _ in
......@@ -83,12 +86,75 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa
}).disposed(by: self.disposeBag)
}
@objc func incomingCall(_ notification: NSNotification) {
guard let callid = notification.userInfo?[NotificationUserInfoKeys.callID.rawValue] as? String,
let call = self.callService.call(callID: callid) else {
/*
* when receive a new call trigger CallKit for iOS 10 and than navigate to
* call controller when call accepted. For iOS less than 10 present
* call controller or trigger notifications, depending of current app state
*/
func showIncomingCall(call: CallModel) {
guard let account = self.accountService
.getAccount(fromAccountId: call.accountId),
!call.callId.isEmpty else {return}
guard let topController = getTopController(),
!topController.isKind(of: (CallViewController).self) else {
return
}
let callViewController = CallViewController
.instantiate(with: self.injectionBag)
callViewController.viewModel.call = call
var tempBag = DisposeBag()
if #available(iOS 10.0, *) {
call.callUUID = UUID()
callsProvider
.reportIncomingCall(account: account, call: call) { _ in
// if starting CallKit failed fallback to jami call screen
if UIApplication.shared.applicationState != .active {
if AccountModelHelper
.init(withAccount: account).isAccountSip() ||
!self.accountService.getCurrentProxyState(accountID: account.id) {
return
}
self.triggerCallNotifications(call: call)
return
}
topController
.present(callViewController,
animated: true,
completion: nil)
}
callsProvider.sharedResponseStream
.filter({ serviceEvent in
if serviceEvent.eventType != ServiceEventType.callProviderAnswerCall {
return false
}
guard let callUUID: String = serviceEvent
.getEventInput(ServiceEventInput.callUUID) else {return false}
return callUUID == call.callUUID.uuidString
}).subscribe(onNext: { _ in
topController.present(callViewController, animated: true, completion: nil)
tempBag = DisposeBag()
}).disposed(by: tempBag)
callViewController.viewModel.dismisVC
.share()
.subscribe(onNext: { hide in
if hide {
tempBag = DisposeBag()
}
}).disposed(by: tempBag)
} else {
if UIApplication.shared.applicationState != .active {
if AccountModelHelper
.init(withAccount: account).isAccountSip() ||
!self.accountService.getCurrentProxyState(accountID: account.id) {
return
}
triggerCallNotifications(call: call)
return
}
topController.present(callViewController, animated: true, completion: nil)
}
self.answerIncomingCall(call: call)
}
func createNewAccount() {
......@@ -139,65 +205,63 @@ class ConversationsCoordinator: Coordinator, StateableResponsive, ConversationNa
presentingVC[VCType.conversation.rawValue] = false
}
func answerIncomingCall(call: CallModel) {
let callViewController = CallViewController.instantiate(with: self.injectionBag)
//open call controller when button navigate to call pressed
func presentCallController (call: CallModel) {
let controlles = self.navigationViewController.viewControllers
for controller in controlles
where controller.isKind(of: (CallViewController).self) {
if let callcontroller = controller as? CallViewController, callcontroller.viewModel.call?.callId == call.callId {
self.navigationViewController
.present(callcontroller,
animated: true,
completion: nil)
return
}
}
guard let topController = getTopController(),
!topController.isKind(of: (CallViewController).self) else {
return
}
let callViewController = CallViewController
.instantiate(with: self.injectionBag)
callViewController.viewModel.call = call
callViewController.viewModel.answerCall()
.subscribe(onCompleted: { [weak self] in
self?.present(viewController: callViewController,
withStyle: .present,
withAnimation: false,
withStateable: callViewController.viewModel)
}).disposed(by: self.disposeBag)
topController.present(callViewController, animated: true, completion: nil)
}
func showCallController (call: CallModel) {
func getTopController() -> UIViewController? {
guard var topController = UIApplication.shared
.keyWindow?.rootViewController else {
return
return nil
}
while let presentedViewController = topController.presentedViewController {
topController = present