call: save and send vcard

This patch:
- Send vcard during call and save received vcard in database.
- save call details when place call

Change-Id: I2f0a3db9c7db8ea71e7df8f0bb562866f96ea9ae
Reviewed-by: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
diff --git a/Ring/Ring/Account/VCardUtils.swift b/Ring/Ring/Account/VCardUtils.swift
index 9a360b1..4bf21f3 100644
--- a/Ring/Ring/Account/VCardUtils.swift
+++ b/Ring/Ring/Account/VCardUtils.swift
@@ -31,7 +31,6 @@
     case myProfile
 }
 class VCardUtils {
-
     class func saveVCard(vCard: CNContact, withName name: String, inFolder folder: String) -> Observable<Void> {
         return Observable.create { observable in
             if let directoryURL = VCardUtils.getFilePath(forFile: name, inFolder: folder, createIfNotExists: true) {
@@ -124,4 +123,40 @@
         }
         return name
     }
+
+    class func sendVCard(card: CNContact, callID: String, accountID: String, sender: CallsService) {
+        do {
+            let vCard = card
+            let vCardData = try CNContactVCardSerialization.dataWithImageAndUUID(from: vCard, andImageCompression: 40000)
+            guard var vCardString = String(data: vCardData, encoding: String.Encoding.utf8) else {
+                return
+            }
+            var vcardLength = vCardString.count
+            let chunkSize = 1024
+            let idKey = Int64(arc4random_uniform(10000000))
+            let total = vcardLength / chunkSize + (((vcardLength % chunkSize) == 0) ? 0 : 1)
+            var i = 1
+            while vcardLength > 0 {
+                var chunk = [String: String]()
+                let id = "id=" + "\(idKey)" + ","
+                let part = "part=" + "\(i)" + ","
+                let of = "of=" + "\(total)"
+                let key = "x-ring/ring.profile.vcard;" + id + part + of
+                if vcardLength >= chunkSize {
+                    let body = String(vCardString.prefix(chunkSize))
+                    let index = vCardString.index(vCardString.startIndex, offsetBy: (chunkSize))
+                    vCardString = String(vCardString.suffix(from: index))
+                    vcardLength = vCardString.count
+                    chunk[key] = body
+                } else {
+                    vcardLength = 0
+                    chunk[key] = vCardString
+                }
+                i += 1
+                sender.sendChunk(callID: callID, message: chunk, accountId: accountID)
+            }
+        } catch {
+            print(error)
+        }
+    }
 }
diff --git a/Ring/Ring/Bridging/CallsAdapter.h b/Ring/Ring/Bridging/CallsAdapter.h
index eb82cec..3d0e2a3 100644
--- a/Ring/Ring/Bridging/CallsAdapter.h
+++ b/Ring/Ring/Bridging/CallsAdapter.h
@@ -36,5 +36,6 @@
 - (NSString*)placeCallWithAccountId:(NSString*)accountId toRingId:(NSString*)ringId;
 - (NSDictionary<NSString*,NSString*>*)callDetailsWithCallId:(NSString*)callId;
 - (NSArray<NSString*>*)calls;
+- (void) sendTextMessageWithCallID:(NSString*)callId message:(NSDictionary*)message accountId:(NSString*)accountId sMixed:(bool)isMixed;
 
 @end
diff --git a/Ring/Ring/Bridging/CallsAdapter.mm b/Ring/Ring/Bridging/CallsAdapter.mm
index fda3582..d970ee1 100644
--- a/Ring/Ring/Bridging/CallsAdapter.mm
+++ b/Ring/Ring/Bridging/CallsAdapter.mm
@@ -161,6 +161,10 @@
     return [NSString stringWithUTF8String:callId.c_str()];
 }
 
+- (void)sendTextMessageWithCallID:(NSString*)callId message:(NSDictionary*)message accountId:(NSString*)accountId sMixed:(bool)isMixed {
+    sendTextMessage(std::string([callId UTF8String]), [Utils dictionnaryToMap:message], std::string([accountId UTF8String]), isMixed);
+}
+
 - (NSDictionary<NSString*,NSString*>*)callDetailsWithCallId:(NSString*)callId {
     std::map<std::string, std::string> callDetails = getCallDetails(std::string([callId UTF8String]));
     return [Utils mapToDictionnary:callDetails];
diff --git a/Ring/Ring/Calls/CallViewController.swift b/Ring/Ring/Calls/CallViewController.swift
index 51f977c..c378561 100644
--- a/Ring/Ring/Calls/CallViewController.swift
+++ b/Ring/Ring/Calls/CallViewController.swift
@@ -56,6 +56,17 @@
         }).disposed(by: self.disposeBag)
 
         //Data bindings
+
+        self.viewModel.contactImageData.asObservable()
+            .observeOn(MainScheduler.instance)
+            .subscribe(onNext: { [weak self] dataOrNil in
+            if let imageData = dataOrNil {
+                if let image = UIImage(data: imageData) {
+                    self?.profileImageView.image = image
+                }
+            }
+        }).disposed(by: self.disposeBag)
+
         self.viewModel.dismisVC
             .observeOn(MainScheduler.instance)
             .subscribe(onNext: { [weak self] dismiss in
@@ -83,5 +94,4 @@
     func removeFromScreen() {
         self.dismiss(animated: false)
     }
-
 }
diff --git a/Ring/Ring/Calls/CallViewModel.swift b/Ring/Ring/Calls/CallViewModel.swift
index 6791413..0759cf2 100644
--- a/Ring/Ring/Calls/CallViewModel.swift
+++ b/Ring/Ring/Calls/CallViewModel.swift
@@ -37,10 +37,41 @@
     private let disposeBag = DisposeBag()
     fileprivate let log = SwiftyBeaver.self
 
-     var call: CallModel?
+    var call: CallModel? {
+        didSet {
+            guard let call = self.call else {
+                return
+            }
+            self.contactsService.getProfileForUri(uri: call.participantRingId)
+                .subscribe(onNext: { [unowned self] profile in
+                    self.profileUpdated(profile: profile)
+                })
+                .disposed(by: self.disposeBag)
+
+            self.callService
+                .sharedResponseStream
+                .filter({ (event) in
+                    if let uri: String = event.getEventInput(ServiceEventInput.uri) {
+                        return event.eventType == ServiceEventType.profileUpdated
+                            && uri == call.participantRingId
+                    }
+                    return false
+                })
+                .subscribe(onNext: { [unowned self] _ in
+                    self.contactsService.getProfileForUri(uri: call.participantRingId)
+                        .subscribe(onNext: { profile in
+                            self.profileUpdated(profile: profile)
+                        })
+                        .disposed(by: self.disposeBag)
+                })
+                .disposed(by: disposeBag)
+        }
+    }
 
     // data for ViewCintroller binding
 
+    var contactImageData = Variable<Data?>(nil)
+
     lazy var dismisVC: Observable<Bool> = {
         return callService.currentCall.map({[weak self] call in
             return call.state == .over || call.state == .failure && call.callId == self?.call?.callId
@@ -91,6 +122,7 @@
             }
         })
     }()
+
     required init(with injectionBag: InjectionBag) {
         self.callService = injectionBag.callService
         self.contactsService = injectionBag.contactsService
@@ -130,6 +162,7 @@
     }
 
     func placeCall(with uri: String, userName: String) {
+
         guard let account = self.accountService.currentAccount else {
             return
         }
@@ -138,9 +171,16 @@
                                    userName: userName)
             .subscribe(onSuccess: { [unowned self] callModel in
                 self.call = callModel
-                self.log.info("Call placed: \(callModel.callId)")
-                }, onError: { [unowned self] error in
-                    self.log.error("Failed to place the call")
             }).disposed(by: self.disposeBag)
     }
+
+    func profileUpdated(profile: Profile) {
+        guard let photo = profile.photo else {
+            return
+        }
+        guard let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? else {
+            return
+        }
+        self.contactImageData.value = data
+    }
 }
diff --git a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift
index 01a1819..0b1a43a 100644
--- a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift
+++ b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift
@@ -158,7 +158,6 @@
             guard rowId > 0 else {
                 throw DataAccessError.databaseError
             }
-            return
         }
     }
 }
diff --git a/Ring/Ring/Services/CallsService.swift b/Ring/Ring/Services/CallsService.swift
index 99d85a0..4a09f7a 100644
--- a/Ring/Ring/Services/CallsService.swift
+++ b/Ring/Ring/Services/CallsService.swift
@@ -32,6 +32,11 @@
     case placeCallFailed
 }
 
+struct Base64VCard {
+    var data: [Int: String] //The key is the number of vCard part
+    var partsReceived: Int
+}
+
 class CallsService: CallsAdapterDelegate {
 
     fileprivate let disposeBag = DisposeBag()
@@ -39,14 +44,21 @@
     fileprivate let log = SwiftyBeaver.self
 
     fileprivate var calls = [String: CallModel]()
-    fileprivate let ringVCardMIMEType = "x-ring/ring.profile.vcard"
+
+    fileprivate var base64VCards = [Int: Base64VCard]() //The key is the vCard id
+    fileprivate let ringVCardMIMEType = "x-ring/ring.profile.vcard;"
 
     let currentCall = ReplaySubject<CallModel>.create(bufferSize: 1)
     let newCall = Variable<CallModel>(CallModel(withCallId: "", callDetails: [:]))
-
+    //let receivedVCard = PublishSubject<Profile>()
+    let dbManager = DBManager(profileHepler: ProfileDataHelper(), conversationHelper: ConversationDataHelper(), interactionHepler: InteractionDataHelper())
+    fileprivate let responseStream = PublishSubject<ServiceEvent>()
+    var sharedResponseStream: Observable<ServiceEvent>
 
     init(withCallsAdapter callsAdapter: CallsAdapter) {
         self.callsAdapter = callsAdapter
+        self.responseStream.disposed(by: disposeBag)
+        self.sharedResponseStream = responseStream.share()
         CallsAdapter.delegate = self
     }
 
@@ -113,9 +125,12 @@
     func placeCall(withAccount account: AccountModel, toRingId ringId: String, userName: String) -> Single<CallModel> {
 
         //Create and emit the call
-        let call = CallModel(withCallId: ringId, callDetails: [String: String]())
+        var callDetails = [String: String]()
+        callDetails[CallDetailKey.callTypeKey.rawValue] = String(describing: CallType.outgoing)
+        callDetails[CallDetailKey.displayNameKey.rawValue] = userName
+        callDetails[CallDetailKey.accountIdKey.rawValue] = account.id
+        let call = CallModel(withCallId: ringId, callDetails: callDetails)
         call.state = .connecting
-        call.registeredName = userName
         return Single<CallModel>.create(subscribe: { single in
             if let callId = self.callsAdapter.placeCall(withAccountId: account.id,
                                                         toRingId: "ring:\(ringId)"),
@@ -150,6 +165,13 @@
             //Update the call
             call?.state = CallState(rawValue: state)!
 
+            //send vCard
+            if (call?.state == .ringing && call?.callType == .outgoing) ||
+                (call?.state == .current && call?.callType == .incoming) {
+                let accountID = call?.accountId
+                self.sendVCard(callID: callId, accountID: accountID!)
+            }
+
             //Emit the call to the observers
             self.currentCall.onNext(call!)
 
@@ -161,8 +183,109 @@
         }
     }
 
+    func sendVCard(callID: String, accountID: String) {
+        if accountID.isEmpty || callID.isEmpty {
+            return
+        }
+        VCardUtils.loadVCard(named: VCardFiles.myProfile.rawValue,
+                             inFolder: VCardFolders.profile.rawValue)
+            .subscribe(onSuccess: { [unowned self] card in
+                VCardUtils.sendVCard(card: card,
+                                     callID: callID,
+                                     accountID: accountID,
+                                     sender: self)
+            }).disposed(by: disposeBag)
+    }
+
+    func sendChunk(callID: String, message: [String: String], accountId: String) {
+        self.callsAdapter.sendTextMessage(withCallID: callID,
+                                          message: message,
+                                          accountId: accountId,
+                                          sMixed: true)
+    }
+
     func didReceiveMessage(withCallId callId: String, fromURI uri: String, message: [String: String]) {
 
+        if let vCardKey = message.keys.filter({ $0.hasPrefix(self.ringVCardMIMEType) }).first {
+
+            //Parse the key to get the number of parts and the current part number
+            let components = vCardKey.components(separatedBy: ",")
+
+            guard let partComponent = components.filter({$0.hasPrefix("part=")}).first else {
+                return
+            }
+
+            guard let ofComponent = components.filter({$0.hasPrefix("of=")}).first else {
+                return
+            }
+
+            guard let idComponent = components.filter({$0.hasPrefix("x-ring/ring.profile.vcard;id=")}).first else {
+                return
+            }
+
+            guard let part = Int(partComponent.components(separatedBy: "=")[1]) else {
+                return
+            }
+
+            guard let of = Int(ofComponent.components(separatedBy: "=")[1]) else {
+                return
+            }
+
+            guard let id = Int(idComponent.components(separatedBy: "=")[1]) else {
+                return
+            }
+            var numberOfReceivedChunk = 1
+            if var chunk = self.base64VCards[id] {
+                chunk.data[part] = message[vCardKey]
+                chunk.partsReceived += 1
+                numberOfReceivedChunk = chunk.partsReceived
+                self.base64VCards[id] = chunk
+            } else {
+                let partMessage = message[vCardKey]
+                let data: [Int: String] = [part: partMessage!]
+                let chunk = Base64VCard(data: data, partsReceived: numberOfReceivedChunk)
+                self.base64VCards[id] = chunk
+            }
+
+            //Emit the vCard when all data are appended
+            if of == numberOfReceivedChunk {
+                guard let vcard = self.base64VCards[id] else {
+                    return
+                }
+
+                let vCardChunks = vcard.data
+
+                //Append data from sorted part numbers
+                var vCardData = Data()
+                for currentPartNumber in vCardChunks.keys.sorted() {
+                    if let currentData = vCardChunks[currentPartNumber]?.data(using: String.Encoding.utf8) {
+                        vCardData.append(currentData)
+                    }
+                }
+
+                //Create the vCard, save and db and emite an event
+                do {
+                    if let vCard = try CNContactVCardSerialization.contacts(with: vCardData).first {
+                        let name = VCardUtils.getName(from: vCard)
+                        var stringImage: String?
+                        if let image = vCard.imageData {
+                            stringImage = image.base64EncodedString()
+                        }
+                        let uri = uri.replacingOccurrences(of: "@ring.dht", with: "")
+                        _ = self.dbManager
+                            .createOrUpdateRingProfile(profileUri: uri,
+                                                       alias: name,
+                                                       image: stringImage,
+                                                       status: ProfileStatus.untrasted)
+                        var event = ServiceEvent(withEventType: .profileUpdated)
+                        event.addEventInput(.uri, value: uri)
+                        self.responseStream.onNext(event)
+                    }
+                } catch {
+                   self.log.error(error)
+                }
+            }
+        }
     }
 
     func receivingCall(withAccountId accountId: String, callId: String, fromURI uri: String) {
diff --git a/Ring/Ring/Services/ContactsService.swift b/Ring/Ring/Services/ContactsService.swift
index 34b51cb..15f0973 100644
--- a/Ring/Ring/Services/ContactsService.swift
+++ b/Ring/Ring/Services/ContactsService.swift
@@ -295,4 +295,9 @@
         let vCard = VCardUtils.loadVCard(named: ringID, inFolder: VCardFolders.contacts.rawValue, contactService: self)
         return vCard
     }
+
+    func getProfileForUri(uri: String) ->Observable<Profile> {
+        return self.dbManager.profileObservable(for: uri, createIfNotExists: false)
+            .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
+    }
 }
diff --git a/Ring/Ring/Services/ServiceEvent.swift b/Ring/Ring/Services/ServiceEvent.swift
index 76decfa..0ee0a73 100644
--- a/Ring/Ring/Services/ServiceEvent.swift
+++ b/Ring/Ring/Services/ServiceEvent.swift
@@ -36,6 +36,7 @@
     case contactRequestSended
     case contactRequestReceived
     case contactRequestDiscarded
+    case profileUpdated
 }
 
 /**