From ddfe6e79b75e246ef10c1e5d219989ae03d9930e Mon Sep 17 00:00:00 2001
From: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
Date: Wed, 20 Dec 2017 11:29:59 -0500
Subject: [PATCH] call: add call service
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch introduce call service and adapter to manage
call state.

Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>

Change-Id: I8f69aad4f9c91c2001ef27cb78df631fd31bf291
Reviewed-by: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
---
 Ring/Ring.xcodeproj/project.pbxproj           |  18 ++
 Ring/Ring/Bridging/CallsAdapter.h             |  40 ++++
 Ring/Ring/Bridging/CallsAdapter.mm            | 186 +++++++++++++++++
 Ring/Ring/Bridging/Ring-Bridging-Header.h     |   1 +
 Ring/Ring/Models/CallModel.swift              | 144 +++++++++++++
 Ring/Ring/Services/CallsAdapterDelegate.swift |  30 +++
 Ring/Ring/Services/CallsService.swift         | 197 ++++++++++++++++++
 7 files changed, 616 insertions(+)
 create mode 100644 Ring/Ring/Bridging/CallsAdapter.h
 create mode 100644 Ring/Ring/Bridging/CallsAdapter.mm
 create mode 100644 Ring/Ring/Models/CallModel.swift
 create mode 100644 Ring/Ring/Services/CallsAdapterDelegate.swift
 create mode 100644 Ring/Ring/Services/CallsService.swift

diff --git a/Ring/Ring.xcodeproj/project.pbxproj b/Ring/Ring.xcodeproj/project.pbxproj
index ac2a49126..45b26bb1c 100644
--- a/Ring/Ring.xcodeproj/project.pbxproj
+++ b/Ring/Ring.xcodeproj/project.pbxproj
@@ -106,6 +106,10 @@
 		0E403F831F7D79B000C80BC2 /* MessageCellGenerated.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */; };
 		0E48F9D31FDF150700D6CC08 /* ContactRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E48F9D21FDF150700D6CC08 /* ContactRequestManager.swift */; };
 		0E4909611FE97A94005CAA50 /* ActiveLabel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E4909601FE97A94005CAA50 /* ActiveLabel.framework */; };
+		0E49096A1FEAB156005CAA50 /* CallsAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0E4909691FEAB156005CAA50 /* CallsAdapter.mm */; };
+		0E49096C1FEAB225005CAA50 /* CallsAdapterDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49096B1FEAB225005CAA50 /* CallsAdapterDelegate.swift */; };
+		0E49096E1FEAC0DE005CAA50 /* CallsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49096D1FEAC0DE005CAA50 /* CallsService.swift */; };
+		0E4909701FEAC1C6005CAA50 /* CallModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E49096F1FEAC1C6005CAA50 /* CallModel.swift */; };
 		0E6949791FA7E71C0029B60A /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6949781FA7E71C0029B60A /* BaseViewController.swift */; };
 		0E983E6E1FC77C3E0082103E /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E983E6D1FC77C3E0082103E /* ConversationModel.swift */; };
 		0E9D84491FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9D84481FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift */; };
@@ -356,6 +360,11 @@
 		0E403F821F7D79B000C80BC2 /* MessageCellGenerated.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MessageCellGenerated.xib; sourceTree = "<group>"; };
 		0E48F9D21FDF150700D6CC08 /* ContactRequestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestManager.swift; sourceTree = "<group>"; };
 		0E4909601FE97A94005CAA50 /* ActiveLabel.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ActiveLabel.framework; path = Carthage/Build/iOS/ActiveLabel.framework; sourceTree = "<group>"; };
+		0E4909681FEAB156005CAA50 /* CallsAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CallsAdapter.h; sourceTree = "<group>"; };
+		0E4909691FEAB156005CAA50 /* CallsAdapter.mm */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = CallsAdapter.mm; sourceTree = "<group>"; };
+		0E49096B1FEAB225005CAA50 /* CallsAdapterDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallsAdapterDelegate.swift; sourceTree = "<group>"; };
+		0E49096D1FEAC0DE005CAA50 /* CallsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallsService.swift; sourceTree = "<group>"; };
+		0E49096F1FEAC1C6005CAA50 /* CallModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallModel.swift; sourceTree = "<group>"; };
 		0E6949781FA7E71C0029B60A /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = "<group>"; };
 		0E983E6D1FC77C3E0082103E /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; };
 		0E9D84481FA7DA6A00C561EB /* ChatTabBarItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTabBarItemViewModel.swift; sourceTree = "<group>"; };
@@ -704,6 +713,8 @@
 				62DFAB2D1F9FF0D0002D6F9C /* NetworkService.swift */,
 				0EF78DE21FD0AE3000FC6966 /* ConversationsManager.swift */,
 				0E48F9D21FDF150700D6CC08 /* ContactRequestManager.swift */,
+				0E49096B1FEAB225005CAA50 /* CallsAdapterDelegate.swift */,
+				0E49096D1FEAC0DE005CAA50 /* CallsService.swift */,
 			);
 			path = Services;
 			sourceTree = "<group>";
@@ -737,6 +748,8 @@
 				563AEC731EA6627F003A5641 /* NameRegistration */,
 				62A88D351F6C2E5F00F8AB18 /* PresenceAdapter.h */,
 				62A88D381F6C323500F8AB18 /* PresenceAdapter.mm */,
+				0E4909681FEAB156005CAA50 /* CallsAdapter.h */,
+				0E4909691FEAB156005CAA50 /* CallsAdapter.mm */,
 			);
 			path = Bridging;
 			sourceTree = "<group>";
@@ -1137,6 +1150,7 @@
 				1A2D18BD1F29180700B2C785 /* ContactModel.swift */,
 				1A2D18BF1F29180700B2C785 /* DeviceModel.swift */,
 				0E983E6D1FC77C3E0082103E /* ConversationModel.swift */,
+				0E49096F1FEAC1C6005CAA50 /* CallModel.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1581,6 +1595,7 @@
 				1A2D18C51F29180700B2C785 /* ContactModel.swift in Sources */,
 				1A2D18F71F292D7200B2C785 /* MessageCellSent.swift in Sources */,
 				04399AAC1D1C304300E99CD9 /* AccountAdapter.mm in Sources */,
+				0E49096C1FEAB225005CAA50 /* CallsAdapterDelegate.swift in Sources */,
 				1AABA7461F0FE9C000739605 /* UIColor+Ring.swift in Sources */,
 				1A5DC0201F355DCF0075E8EF /* ContactsService.swift in Sources */,
 				1A2D18C71F29180700B2C785 /* DeviceModel.swift in Sources */,
@@ -1591,8 +1606,10 @@
 				02B22E091DF7585F000358C9 /* DaemonService.swift in Sources */,
 				5CE66F761FBF769B00EE9291 /* InitialLoadingViewController.swift in Sources */,
 				56BBC99F1ED714CB00CDAF8B /* MessagesAdapter.mm in Sources */,
+				0E49096A1FEAB156005CAA50 /* CallsAdapter.mm in Sources */,
 				1A2D18A61F27F7A400B2C785 /* UIViewController+Rx.swift in Sources */,
 				1A5DC0241F3564360075E8EF /* ContactRequestModel.swift in Sources */,
+				0E4909701FEAC1C6005CAA50 /* CallModel.swift in Sources */,
 				1A5DC03F1F35678D0075E8EF /* ContactRequestsViewController.swift in Sources */,
 				1A20418B1F1EA58A00C08435 /* ViewModelBased.swift in Sources */,
 				1A5DC01E1F355DA70075E8EF /* ContactsAdapterDelegate.swift in Sources */,
@@ -1673,6 +1690,7 @@
 				04399AAD1D1C304300E99CD9 /* DRingAdapter.mm in Sources */,
 				1A2D18D11F29182500B2C785 /* ConversationViewController.swift in Sources */,
 				0EE1B5501F75AD4700BA98EE /* VCardUtils.swift in Sources */,
+				0E49096E1FEAC0DE005CAA50 /* CallsService.swift in Sources */,
 				0273C2FF1E0C438F00CF00BA /* AccountAdapterDelegate.swift in Sources */,
 				1A2D19011F29353A00B2C785 /* MeDetailViewModel.swift in Sources */,
 				1A2D18A41F27EF5200B2C785 /* AppCoordinator.swift in Sources */,
diff --git a/Ring/Ring/Bridging/CallsAdapter.h b/Ring/Ring/Bridging/CallsAdapter.h
new file mode 100644
index 000000000..eb82cec7c
--- /dev/null
+++ b/Ring/Ring/Bridging/CallsAdapter.h
@@ -0,0 +1,40 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
+ *  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/Foundation.h>
+
+@protocol CallsAdapterDelegate;
+
+@interface CallsAdapter : NSObject
+
+@property (class, nonatomic, weak) id <CallsAdapterDelegate> delegate;
+
+- (BOOL)acceptCallWithId:(NSString*)callId;
+- (BOOL)refuseCallWithId:(NSString*)callId;
+- (BOOL)hangUpCallWithId:(NSString*)callId;
+- (BOOL)holdCallWithId:(NSString*)callId;
+- (BOOL)unholdCallWithId:(NSString*)callId;
+
+- (NSString*)placeCallWithAccountId:(NSString*)accountId toRingId:(NSString*)ringId;
+- (NSDictionary<NSString*,NSString*>*)callDetailsWithCallId:(NSString*)callId;
+- (NSArray<NSString*>*)calls;
+
+@end
diff --git a/Ring/Ring/Bridging/CallsAdapter.mm b/Ring/Ring/Bridging/CallsAdapter.mm
new file mode 100644
index 000000000..fda358220
--- /dev/null
+++ b/Ring/Ring/Bridging/CallsAdapter.mm
@@ -0,0 +1,186 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
+ *  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 "CallsAdapter.h"
+#import "Utils.h"
+#import "dring/callmanager_interface.h"
+#import "Ring-Swift.h"
+
+using namespace DRing;
+
+@implementation CallsAdapter
+/// Static delegate that will receive the propagated daemon events
+static id <CallsAdapterDelegate> _delegate;
+
+#pragma mark Init
+
+- (id)init {
+    if (self = [super init]) {
+        [self registerCallHandler];
+    }
+    return self;
+}
+
+#pragma mark -
+
+#pragma mark Callbacks registration
+
+- (void)registerCallHandler {
+
+    std::map<std::string, std::shared_ptr<CallbackWrapperBase>> callHandlers;
+
+    //State changed signal
+    callHandlers.insert(exportable_callback<CallSignal::StateChange>([&](const std::string& callId,
+                                                                         const std::string& state,
+                                                                         int errorCode) {
+        if (CallsAdapter.delegate) {
+            NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
+            NSString* stateString = [NSString stringWithUTF8String:state.c_str()];
+            [CallsAdapter.delegate didChangeCallStateWithCallId:callIdString
+                                                          state:stateString
+                                                      stateCode:errorCode];
+        }
+    }));
+
+    //Incoming message signal
+    callHandlers.insert(exportable_callback<CallSignal::IncomingMessage>([&](const std::string& callId,
+                                                                             const std::string& fromURI,
+                                                                             const std::map<std::string,
+                                                                             std::string>& message) {
+
+        if (CallsAdapter.delegate) {
+            NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
+            NSString* fromURIString = [NSString stringWithUTF8String:fromURI.c_str()];
+            NSDictionary* messageDict = [Utils mapToDictionnary:message];
+            [CallsAdapter.delegate didReceiveMessageWithCallId:callIdString
+                                                       fromURI:fromURIString
+                                                       message:messageDict];
+        }
+    }));
+
+    //Incoming call signal
+    callHandlers.insert(exportable_callback<CallSignal::IncomingCall>([&](const std::string& accountId,
+                                                                          const std::string& callId,
+                                                                          const std::string& fromURI) {
+        if (CallsAdapter.delegate) {
+            NSString* accountIdString = [NSString stringWithUTF8String:accountId.c_str()];
+            NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
+            NSString* fromURIString = [NSString stringWithUTF8String:fromURI.c_str()];
+            [CallsAdapter.delegate receivingCallWithAccountId:accountIdString
+                                                       callId:callIdString
+                                                      fromURI:fromURIString];
+        }
+    }));
+
+    //New outgoing call signal
+    callHandlers.insert(exportable_callback<CallSignal::NewCallCreated>([&](const std::string& accountId,
+                                                                            const std::string& callId,
+                                                                            const std::string& toURI) {
+        if (CallsAdapter.delegate) {
+            NSString* accountIdString = [NSString stringWithUTF8String:accountId.c_str()];
+            NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
+            NSString* toURIString = [NSString stringWithUTF8String:toURI.c_str()];
+            [CallsAdapter.delegate newCallStartedWithAccountId: accountIdString
+                                                        callId: callIdString
+                                                         toURI: toURIString];
+
+        }
+    }));
+
+    //Peer place call on hold signal
+    callHandlers.insert(exportable_callback<CallSignal::PeerHold>([&](const std::string& callId,
+                                                                      bool holding) {
+        if (CallsAdapter.delegate) {
+            NSString* callIdString = [NSString stringWithUTF8String: callId.c_str()];
+            [CallsAdapter.delegate callPlacedOnHoldWithCallId:callIdString holding:holding];
+        }
+    }));
+
+    callHandlers.insert(exportable_callback<CallSignal::VideoMuted>([&](const std::string& callId,
+                                                                        bool muted) {
+        if (CallsAdapter.delegate) {
+            NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
+            [CallsAdapter.delegate muteVideoWithCall: callIdString mute: muted];
+        }
+    }));
+
+    callHandlers.insert(exportable_callback<CallSignal::AudioMuted>([&](const std::string& callId,
+                                                                        bool muted) {
+        if (CallsAdapter.delegate) {
+            NSString* callIdString = [NSString stringWithUTF8String:callId.c_str()];
+            [CallsAdapter.delegate muteAudioWithCall: callIdString mute: muted];
+
+        }
+    }));
+
+    registerCallHandlers(callHandlers);
+}
+
+#pragma mark -
+
+- (BOOL)acceptCallWithId:(NSString*)callId {
+    return accept(std::string([callId UTF8String]));
+}
+
+- (BOOL)refuseCallWithId:(NSString*)callId {
+    return refuse(std::string([callId UTF8String]));
+}
+
+- (BOOL)hangUpCallWithId:(NSString*)callId {
+    return hangUp(std::string([callId UTF8String]));
+}
+
+- (BOOL)holdCallWithId:(NSString*)callId {
+    return hold(std::string([callId UTF8String]));
+}
+
+- (BOOL)unholdCallWithId:(NSString*)callId {
+    return unhold(std::string([callId UTF8String]));
+}
+
+- (NSString*)placeCallWithAccountId:(NSString*)accountId toRingId:(NSString*)ringId {
+    std::string callId = placeCall(std::string([accountId UTF8String]), std::string([ringId UTF8String]));
+    return [NSString stringWithUTF8String:callId.c_str()];
+}
+
+- (NSDictionary<NSString*,NSString*>*)callDetailsWithCallId:(NSString*)callId {
+    std::map<std::string, std::string> callDetails = getCallDetails(std::string([callId UTF8String]));
+    return [Utils mapToDictionnary:callDetails];
+}
+
+- (NSArray<NSString*>*)calls {
+    std::vector<std::string> calls = getCallList();
+    return [Utils vectorToArray:calls];
+}
+
+#pragma mark AccountAdapterDelegate
+
++ (id <CallsAdapterDelegate>)delegate {
+    return _delegate;
+}
+
++ (void) setDelegate:(id<CallsAdapterDelegate>)delegate {
+    _delegate = delegate;
+}
+
+#pragma mark -
+
+@end
diff --git a/Ring/Ring/Bridging/Ring-Bridging-Header.h b/Ring/Ring/Bridging/Ring-Bridging-Header.h
index 174232018..54365de06 100644
--- a/Ring/Ring/Bridging/Ring-Bridging-Header.h
+++ b/Ring/Ring/Bridging/Ring-Bridging-Header.h
@@ -35,3 +35,4 @@
 #import "PresenceAdapter.h"
 #import <CommonCrypto/CommonCrypto.h>
 #import <Contacts/Contacts.h>
+#import "CallsAdapter.h"
diff --git a/Ring/Ring/Models/CallModel.swift b/Ring/Ring/Models/CallModel.swift
new file mode 100644
index 000000000..86fedfd87
--- /dev/null
+++ b/Ring/Ring/Models/CallModel.swift
@@ -0,0 +1,144 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
+ *  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.
+ */
+
+enum CallState: String {
+    case incoming = "INCOMING"
+    case connecting = "CONNECTING"
+    case ringing = "RINGING"
+    case current = "CURRENT"
+    case hungup = "HUNGUP"
+    case busy = "BUSY"
+    case failure = "FAILURE"
+    case hold = "HOLD"
+    case unhold = "UNHOLD"
+    case inactive = "INACTIVE"
+    case over = "OVER"
+    case unknown = "UNKNOWN"
+}
+
+enum CallType: Int {
+    case incoming = 0
+    case outgoing
+    case missed
+}
+
+enum CallDetailKey: String {
+    case callTypeKey = "CALL_TYPE"
+    case peerNumberKey = "PEER_NUMBER"
+    case registeredNameKey = "REGISTERED_NAME"
+    case displayNameKey = "DISPLAY_NAME"
+    case timeStampStartKey = "TIMESTAMP_START"
+    case accountIdKey = "ACCOUNTID"
+    case peerHoldingKey = "PEER_HOLDING"
+    case audioMutedKey = "AUDIO_MUTED"
+    case videoMutedKey = "VIDEO_MUTED"
+    case videoSourceKey = "VIDEO_SOURCE"
+}
+
+class CallModel {
+
+    var callId: String = ""
+    var dateReceived: Date = Date()
+    var participantRingId: String = ""
+    var displayName: String = ""
+    var registeredName: String = ""
+    var accountId: String = ""
+    var audioMuted: Bool = false
+    var videoMuted: Bool = false
+
+    var stateValue = CallState.unknown.rawValue
+    var callTypeValue = CallType.missed.rawValue
+
+    var state: CallState {
+        get {
+            if let state = CallState(rawValue: stateValue) {
+                return state
+            }
+            return CallState.unknown
+        }
+        set {
+            stateValue = newValue.rawValue
+        }
+    }
+
+    var callType: CallType {
+        get {
+            if let type = CallType(rawValue: callTypeValue) {
+                return type
+            }
+            return CallType.missed
+        }
+        set {
+            callTypeValue = newValue.rawValue
+        }
+    }
+
+    init(withCallId callId: String, callDetails dictionary: [String: String]) {
+        self.callId = callId
+
+        if let fromRingId = dictionary[CallDetailKey.peerNumberKey.rawValue]?.components(separatedBy: "@").first {
+            self.participantRingId = fromRingId
+        }
+
+        if let accountId = dictionary[CallDetailKey.accountIdKey.rawValue] {
+            self.accountId = accountId
+        }
+
+        if let callType = dictionary[CallDetailKey.callTypeKey.rawValue] {
+            if let callTypeInt = Int(callType) {
+                self.callType = CallType(rawValue: callTypeInt)!
+            } else {
+                self.callType = .missed
+            }
+        }
+
+        self.update(withDictionary: dictionary)
+    }
+
+    func update(withDictionary dictionary: [String: String]) {
+
+        self.dateReceived = Date()
+
+        if let displayName = dictionary[CallDetailKey.displayNameKey.rawValue] {
+            self.displayName = displayName
+        }
+
+        if let registeredName = dictionary[CallDetailKey.registeredNameKey.rawValue] {
+            self.registeredName = registeredName
+        }
+
+        if let videoMuted = dictionary[CallDetailKey.videoMutedKey.rawValue]?.toBool() {
+            self.videoMuted = videoMuted
+        }
+
+        if let audioMuted = dictionary[CallDetailKey.audioMutedKey.rawValue]?.toBool() {
+            self.audioMuted = audioMuted
+        }
+
+        if let participantRingId = dictionary[CallDetailKey.peerNumberKey.rawValue]?.components(separatedBy: "@").first {
+            self.participantRingId = participantRingId
+        }
+
+        if let accountId = dictionary[CallDetailKey.accountIdKey.rawValue] {
+            self.accountId = accountId
+        }
+    }
+}
diff --git a/Ring/Ring/Services/CallsAdapterDelegate.swift b/Ring/Ring/Services/CallsAdapterDelegate.swift
new file mode 100644
index 000000000..1267fba25
--- /dev/null
+++ b/Ring/Ring/Services/CallsAdapterDelegate.swift
@@ -0,0 +1,30 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
+ *  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.
+ */
+
+@objc protocol CallsAdapterDelegate {
+    func didChangeCallState(withCallId callId: String, state: String, stateCode: NSInteger)
+    func didReceiveMessage(withCallId callId: String, fromURI uri: String, message: [String: String])
+    func receivingCall(withAccountId accountId: String, callId: String, fromURI uri: String)
+    func newCallStarted(withAccountId accountId: String, callId: String, toURI uri: String)
+    func callPlacedOnHold(withCallId callId: String, holding: Bool)
+    func muteAudio(call callId: String, mute: Bool)
+    func muteVideo(call callId: String, mute: Bool)
+}
diff --git a/Ring/Ring/Services/CallsService.swift b/Ring/Ring/Services/CallsService.swift
new file mode 100644
index 000000000..18fc8c44c
--- /dev/null
+++ b/Ring/Ring/Services/CallsService.swift
@@ -0,0 +1,197 @@
+/*
+ *  Copyright (C) 2017 Savoir-faire Linux Inc.
+ *
+ *  Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
+ *  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 RxSwift
+import SwiftyBeaver
+import Contacts
+
+enum CallServiceError: Error {
+    case acceptCallFailed
+    case refuseCallFailed
+    case hangUpCallFailed
+    case holdCallFailed
+    case unholdCallFailed
+    case placeCallFailed
+}
+
+class CallsService: CallsAdapterDelegate {
+
+    fileprivate let disposeBag = DisposeBag()
+    fileprivate let callsAdapter: CallsAdapter
+    fileprivate let log = SwiftyBeaver.self
+
+    fileprivate var calls = [String: CallModel]()
+    fileprivate let ringVCardMIMEType = "x-ring/ring.profile.vcard"
+
+    let currentCall = ReplaySubject<CallModel>.create(bufferSize: 1)
+    let newcall = Variable<CallModel>(CallModel(withCallId: "", callDetails: [:]))
+
+    init(withCallsAdapter callsAdapter: CallsAdapter) {
+        self.callsAdapter = callsAdapter
+        CallsAdapter.delegate = self
+    }
+
+    func accept(callId: String) -> Completable {
+        return Completable.create(subscribe: { completable in
+            let success = self.callsAdapter.acceptCall(withId: callId)
+            if success {
+                completable(.completed)
+            } else {
+                completable(.error(CallServiceError.acceptCallFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    func refuse(callId: String) -> Completable {
+        return Completable.create(subscribe: { completable in
+            let success = self.callsAdapter.refuseCall(withId: callId)
+            if success {
+                completable(.completed)
+            } else {
+                completable(.error(CallServiceError.refuseCallFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    func hangUp(callId: String) -> Completable {
+        return Completable.create(subscribe: { completable in
+            let success = self.callsAdapter.hangUpCall(withId: callId)
+            if success {
+                completable(.completed)
+            } else {
+                completable(.error(CallServiceError.hangUpCallFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    func hold(callId: String) -> Completable {
+        return Completable.create(subscribe: { completable in
+            let success = self.callsAdapter.holdCall(withId: callId)
+            if success {
+                completable(.completed)
+            } else {
+                completable(.error(CallServiceError.holdCallFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    func unhold(callId: String) -> Completable {
+        return Completable.create(subscribe: { completable in
+            let success = self.callsAdapter.unholdCall(withId: callId)
+            if success {
+                completable(.completed)
+            } else {
+                completable(.error(CallServiceError.unholdCallFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    func placeCall(withAccount account: AccountModel, toRingId ringId: String) -> Single<CallModel> {
+
+        //Create and emit the call
+        let call = CallModel(withCallId: ringId, callDetails: [String: String]())
+        call.state = .connecting
+
+        return Single<CallModel>.create(subscribe: { single in
+            if let callId = self.callsAdapter.placeCall(withAccountId: account.id,
+                                                        toRingId: "ring:\(ringId)"),
+                let callDictionary = self.callsAdapter.callDetails(withCallId: callId) {
+                call.update(withDictionary: callDictionary)
+                call.callId = callId
+                self.currentCall.onNext(call)
+                self.calls[callId] = call
+                single(.success(call))
+            } else {
+                single(.error(CallServiceError.placeCallFailed))
+            }
+            return Disposables.create { }
+        })
+    }
+
+    // MARK: CallsAdapterDelegate
+
+    func didChangeCallState(withCallId callId: String, state: String, stateCode: NSInteger) {
+
+        if let callDictionary = self.callsAdapter.callDetails(withCallId: callId) {
+
+            //Add or update new call
+            var call = self.calls[callId]
+            if call == nil {
+                call = CallModel(withCallId: callId, callDetails: callDictionary)
+            } else {
+                call?.update(withDictionary: callDictionary)
+            }
+
+            //Update the call
+            call?.state = CallState(rawValue: state)!
+
+            //Emit the call to the observers
+            self.currentCall.onNext(call!)
+
+            //Remove from the cache if the call is over
+            if call?.state == .over {
+                // TODO save history
+                self.calls[callId] = nil
+            }
+        }
+    }
+
+    func didReceiveMessage(withCallId callId: String, fromURI uri: String, message: [String: String]) {
+
+    }
+
+    func receivingCall(withAccountId accountId: String, callId: String, fromURI uri: String) {
+        if let callDictionary = self.callsAdapter.callDetails(withCallId: callId) {
+
+            //Add or update new call
+            var call = self.calls[callId]
+            if call == nil {
+                call = CallModel(withCallId: callId, callDetails: callDictionary)
+            } else {
+                call?.update(withDictionary: callDictionary)
+            }
+            //Emit the call to the observers
+            self.newcall.value = call!
+        }
+
+    }
+
+    func newCallStarted(withAccountId accountId: String, callId: String, toURI uri: String) {
+
+    }
+
+    func callPlacedOnHold(withCallId callId: String, holding: Bool) {
+
+    }
+
+    func muteAudio(call callId: String, mute: Bool) {
+
+    }
+
+    func muteVideo(call callId: String, mute: Bool) {
+
+    }
+}
-- 
GitLab