diff --git a/Ring/Ring/Database/DBContainer.swift b/Ring/Ring/Database/DBContainer.swift index 1e20a64e8b54b22a642955dcf882579fc736174f..e61d0bc512308d62a7f9674b72527fc4b41a5dd7 100644 --- a/Ring/Ring/Database/DBContainer.swift +++ b/Ring/Ring/Database/DBContainer.swift @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2019 Savoir-faire Linux Inc. + * Copyright (C) 2017-2020 Savoir-faire Linux Inc. * * Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> * @@ -21,6 +21,25 @@ import SQLite import SwiftyBeaver +//================================================================================ +// jami files structure +// +// Jami Documents folder +// └──{ account_id } +// ├── config.yml +// ├── contacts +// ├── archive.gz +// ├── incomingTrustRequests +// ├── knownDevicesNames +// ├── { account_id }.db < --conversations and interactions database +// ├── profile.vcf < --account vcard +// ├── profiles < --account contact vcards +// │  │──{ contact_uri }.vcf +// │ └── ... +// ├── ring_device.crt +// └── ring_device.key +//================================================================================ + enum DataAccessError: Error { case datastoreConnectionError case databaseMigrationError @@ -32,32 +51,18 @@ final class DBContainer { private var connections = [String: Connection?]() var connectionsSemaphore = DispatchSemaphore(value: 1) private let log = SwiftyBeaver.self - private let jamiDBName = "ring.db" - private let path: String? - private let dbVersion = 1 + private let dbVersions = [1, 2] - init() { - path = NSSearchPathForDirectoriesInDomains( + let documentsPath = { + return NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true ).first - } - - func getJamiDB() -> Connection? { - if jamiDB != nil { - return jamiDB - } - guard let dbPath = path else { return nil } - do { - jamiDB = try Connection("\(dbPath)/" + jamiDBName) - } catch { - jamiDB = nil - log.error("Unable to open database") - } - return jamiDB - } + }() - func removeJamiDB() { - self.removeDBNamed(dbName: jamiDBName) + func removeDBForAccount(account: String, removeFolder: Bool) { + connections[account] = nil + if !removeFolder { return } + self.removeAccountFolder(accountId: account) } func removeDBForAccount(account: String) { @@ -69,11 +74,11 @@ final class DBContainer { if connections[account] != nil { return connections[account] ?? nil } - guard let dbPath = path else { return nil } + guard let dbPath = accountDbPath(accountId: account) else { return nil } do { self.connectionsSemaphore.wait() - connections[account] = try Connection("\(dbPath)/" + "\(account).db") - connections[account]??.userVersion = dbVersion + connections[account] = try Connection(dbPath) + connections[account]??.userVersion = dbVersions.last self.connectionsSemaphore.signal() return connections[account] ?? nil } catch { @@ -83,24 +88,84 @@ final class DBContainer { } } - func isDBExistsFor(account: String) -> Bool { - guard let dbPath = path else { return false } - let url = NSURL(fileURLWithPath: dbPath) - if let pathComponent = url.appendingPathComponent("/" + "\(account).db") { - let filePath = pathComponent.path - let fileManager = FileManager.default - if fileManager.fileExists(atPath: filePath) { - return false - } else { - return true - } - } else { + // MARK: paths + + private func accountFolderPath(accountId: String) -> String? { + guard let documents = documentsPath else { return nil } + return documents + "/" + "\(accountId)" + "/" + } + + private func accountDbPath(accountId: String) -> String? { + guard let accountFolder = accountFolderPath(accountId: accountId) else { return nil } + return accountFolder + "\(accountId).db" + } + + func contactsPath(accountId: String, createIfNotExists: Bool) -> String? { + guard let accountFolder = accountFolderPath(accountId: accountId) else { return nil } + let profilesFolder = accountFolder + "profiles/" + let fileManager = FileManager.default + if fileManager.fileExists(atPath: profilesFolder) { return profilesFolder } + if !createIfNotExists { return nil } + do { + try fileManager.createDirectory(atPath: profilesFolder, + withIntermediateDirectories: true, + attributes: nil) + } catch { + return nil + } + return fileManager.fileExists(atPath: profilesFolder) ? profilesFolder : nil + } + + private func isDbExists(accountId: String) -> Bool { + guard let path = accountDbPath(accountId: accountId) else { return false } + return isFileExists(path: path) + } + + private func isFileExists(path: String) -> Bool { + if path.isEmpty { + return false + } + let fileManager = FileManager.default + return fileManager.fileExists(atPath: path) + } + + func contactProfilePath(accountId: String, profileURI: String, createifNotExists: Bool) -> String? { + guard let profilesFolder = contactsPath(accountId: accountId, + createIfNotExists: createifNotExists) else { return nil } + return profilesFolder + "\(profileURI.toBase64()).vcf" + } + + func accountProfilePath(accountId: String) -> String? { + guard let accountFolder = accountFolderPath(accountId: accountId) else { return nil } + return accountFolder + "profile.vcf" + } + + func isAccountProfileExists(accountId: String) -> Bool { + guard let path = accountProfilePath(accountId: accountId) else { return false } + return isFileExists(path: path) + } + + func isContactProfileExists(accountId: String, profileURI: String) -> Bool { + guard let path = contactProfilePath(accountId: accountId, profileURI: profileURI, createifNotExists: false) else { return false } + return isFileExists(path: path) + } + + func isMigrationToDBv2Needed(for accountId: String) -> Bool { + if !isDbExists(accountId: accountId) { return true } + guard let dbase = self.forAccount(account: accountId) else { return true } + let table = Table("profiles") + do { + try _ = dbase.scalar(table.exists) + return true + } catch { + return false + } } private func removeDBNamed(dbName: String) { - guard let dbPath = path else { return } + guard let dbPath = documentsPath else { return } let url = NSURL(fileURLWithPath: dbPath) guard let pathComponent = url .appendingPathComponent("/" + dbName) else { @@ -116,6 +181,50 @@ final class DBContainer { print("Error on delete old database!!!") } } + + func createAccountfolder(for accountId: String) { + guard let accountFolder = accountFolderPath(accountId: accountId) else { return } + let fileManager = FileManager.default + if fileManager.fileExists(atPath: accountFolder) { return } + do { + try fileManager.createDirectory(atPath: accountFolder, + withIntermediateDirectories: true, + attributes: nil) + } catch { + return + } + } + + func copyDbToAccountFolder(for accountId: String) -> Bool { + if isDbExists(accountId: accountId) { return true } + guard let dbPath = documentsPath else { return false } + let url = NSURL(fileURLWithPath: dbPath) + guard let oldPath = url.appendingPathComponent("/" + "\(accountId).db") else { return false } + guard let newPath = accountDbPath(accountId: accountId) else { return false } + let fileManager = FileManager.default + do { + try fileManager.copyItem(atPath: oldPath.path, toPath: newPath) + return fileManager.fileExists(atPath: newPath) + } catch _ as NSError { + return false + } + } + + func removeContacts(accountId: String) { + guard let contacts = self.contactsPath(accountId: accountId, createIfNotExists: false) else { return } + let fileManager = FileManager.default + do { + try fileManager.removeItem(atPath: contacts) + } catch _ as NSError {} + } + + func removeAccountFolder(accountId: String) { + guard let account = self.accountFolderPath(accountId: accountId) else { return } + let fileManager = FileManager.default + do { + try fileManager.removeItem(atPath: account) + } catch _ as NSError {} + } } extension Connection { diff --git a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift index 4387db61c721912b23335fde8644f5f9f3849fdd..0fb3efd91bbe064a3ed8286bc5020106f1022954 100644 --- a/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift +++ b/Ring/Ring/Database/DBHelpers/ProfileDataHelper.swift @@ -37,132 +37,33 @@ final class ProfileDataHelper { let type = Expression<String>("type") private let log = SwiftyBeaver.self - //migrate from legacy db - let id = Expression<Int64>("id") - func getLegacyProfileID(profileURI: String, dataBase: Connection) throws -> Int64? { - let query = contactsProfileTable.filter(uri == profileURI) - let items = try dataBase.prepare(query) - for item in items { - return item[id] - } - return nil - } - func getLegacyProfiles(accountURI: String, - accountId: String, - database: Connection) throws -> [Int64: String] { - let query = contactsProfileTable.filter(accountId != uri && accountURI != uri) - let items = try database.prepare(query) - var profiles = [Int64: String]() - for item in items { - profiles[item[id]] = item[uri] - } - return profiles - } - func migrateToDBForAccount (from oldDB: Connection, - to newDB: Connection, - jamiId: String, - accountId: String) throws { - // migrate account profile - // get account profile, it should be only one - let accountQuery = contactsProfileTable.filter(uri == jamiId) - let items = try oldDB.prepare(accountQuery) - for item in items { - let query = accountProfileTable.insert(alias <- item[alias], - photo <- item[photo]) - try newDB.run(query) - } - - //migrate contacts rofiles - let contactQuery = contactsProfileTable.filter((uri != jamiId) && (uri != accountId)) - let rows = try oldDB.prepare(contactQuery) - for row in rows { - let query = contactsProfileTable.insert(uri <- "ring:" + row[uri], - alias <- row[alias], - photo <- row[photo], - type <- row[type]) - try newDB.run(query) - } - } - - func createAccountTable(accountDb: Connection) { + func dropAccountTable(accountDb: Connection) { do { - try accountDb.run(accountProfileTable.create(ifNotExists: true) { table in - table.column(alias) - table.column(photo) - }) - } catch _ { - print("Table already exists") + try accountDb.run(accountProfileTable.drop(ifExists: true)) + } catch { + debugPrint(error) } } - func updateAccountProfile(accountAlias: String?, accountPhoto: String?, dataBase: Connection) -> Bool { + func dropProfileTable(accountDb: Connection) { do { - if try dataBase.pluck(accountProfileTable) != nil { - try dataBase.run(accountProfileTable.update(alias <- accountAlias, - photo <- accountPhoto)) - } else { - try dataBase.run(accountProfileTable.insert(alias <- accountAlias, - photo <- accountPhoto)) - } - return true + try accountDb.run(contactsProfileTable.drop(ifExists: true)) } catch { - return false + debugPrint(error) } } - func getAccountProfile(dataBase: Connection) -> AccountProfile? { + func getAccountProfile(dataBase: Connection) -> Profile? { do { guard let row = try dataBase.pluck(accountProfileTable) else { return nil} - return (row[alias], row[photo]) + // account profile saved in db does not have uri and type, + // return default values that need to be updated by function caller + return Profile("", row[alias], row[photo], ProfileType.ring.rawValue) } catch { return nil } } - func createContactsTable(accountDb: Connection) { - do { - try accountDb.run(contactsProfileTable.create(ifNotExists: true) { table in - table.column(uri, unique: true) - table.column(alias) - table.column(photo) - table.column(type) - }) - try accountDb.run(contactsProfileTable.createIndex(uri)) - } catch _ { - print("Table already exists") - } - } - - func insert(item: Profile, dataBase: Connection) -> Bool { - let query = contactsProfileTable.insert(uri <- item.uri, - alias <- item.alias, - photo <- item.photo, - type <- item.type) - do { - let rowId = try dataBase.run(query) - guard rowId > 0 else { - return false - } - return true - } catch _ { - return false - } - } - - func delete(item: Profile, dataBase: Connection) -> Bool { - let profileUri = item.uri - let query = contactsProfileTable.filter(uri == profileUri) - do { - let deletedRows = try dataBase.run(query.delete()) - guard deletedRows == 1 else { - return false - } - return true - } catch _ { - return false - } - } - func selectAll(dataBase: Connection) throws -> [Profile]? { var profiles = [Profile]() let items = try dataBase.prepare(contactsProfileTable) @@ -172,46 +73,4 @@ final class ProfileDataHelper { } return profiles } - - func selectProfile(profileURI: String, dataBase: Connection) throws -> Profile? { - let query = contactsProfileTable.filter(uri == profileURI) - let items = try dataBase.prepare(query) - // for one URI we should have only one profile - for item in items { - return Profile(uri: item[uri], alias: item[alias], - photo: item[photo], type: item[type]) - } - return nil - } - - func insertOrUpdateProfile(item: Profile, dataBase: Connection) throws { - try dataBase.transaction { - let selectQuery = contactsProfileTable.filter(uri == item.uri) - let rows = try dataBase.run(selectQuery.update(alias <- item.alias, - photo <- item.photo)) - if rows > 0 { - return - } - let insertQuery = contactsProfileTable.insert(uri <- item.uri, - alias <- item.alias, - photo <- item.photo, - type <- item.type) - let rowId = try dataBase.run(insertQuery) - guard rowId > 0 else { - throw DataAccessError.databaseError - } - } - } - - func deleteAll(dataBase: Connection) -> Bool { - do { - if try dataBase.run(contactsProfileTable.delete()) > 0 { - return true - } else { - return false - } - } catch { - return false - } - } } diff --git a/Ring/Ring/Database/DBManager.swift b/Ring/Ring/Database/DBManager.swift index 200473c77a0196bd9db76595214751132e6dbd55..526a3055c691db614544d27666b2b951f7337655 100644 --- a/Ring/Ring/Database/DBManager.swift +++ b/Ring/Ring/Database/DBManager.swift @@ -178,18 +178,49 @@ class DBManager { self.dbConnections = dbConnections } - func isNeedMigrationToAccountDB(accountId: String) -> Bool { - return self.dbConnections.isDBExistsFor(account: accountId) + func isMigrationToDBv2Needed(accountId: String) -> Bool { + return self.dbConnections.isMigrationToDBv2Needed(for: accountId) } - func createDatabaseForAccount(accountId: String) throws -> Bool { + func migrateToDbVersion2(accountId: String, accountURI: String) -> Bool { + if !accountURI.contains("ring:") { + self.dbConnections.createAccountfolder(for: accountId) + } + if !self.dbConnections.copyDbToAccountFolder(for: accountId) { + return false + } + guard let newDB = self.dbConnections.forAccount(account: accountId) else { + return false + } + // move profiles to vcards + do { + try newDB.transaction { [weak self] in + guard let self = self else { throw DataAccessError.databaseError } + if try !self.migrateAccountToVCard(for: accountId, accountURI: accountURI, dataBase: newDB) { + throw DataAccessError.databaseError + } + if try !self.migrateProfilesToVCards(for: accountId, dataBase: newDB) { + throw DataAccessError.databaseError + } + // remove db from documents folder + self.dbConnections.removeDBForAccount(account: accountId) + newDB.userVersion = 2 + } + } catch _ as NSError { + return false + } + return true + } + + func createDatabaseForAccount(accountId: String, createFolder: Bool = false) throws -> Bool { + if createFolder { + self.dbConnections.createAccountfolder(for: accountId) + } guard let newDB = self.dbConnections.forAccount(account: accountId) else { return false } do { try newDB.transaction { - profileHepler.createAccountTable(accountDb: newDB) - profileHepler.createContactsTable(accountDb: newDB) conversationHelper.createTable(accountDb: newDB) interactionHepler.createTable(accountDb: newDB) } @@ -199,45 +230,42 @@ class DBManager { return true } - func migrateToAccountDB(accountId: String, jamiId: String) throws { - do { - guard let newDB = self.dbConnections.forAccount(account: accountId) else { - throw DataAccessError.datastoreConnectionError + func migrateProfilesToVCards(for accountId: String, dataBase: Connection) throws -> Bool { + guard let profiles = try? self.profileHepler.selectAll(dataBase: dataBase) else { + return false + } + for profile in profiles { + if self.dbConnections.isContactProfileExists(accountId: accountId, profileURI: profile.uri) { + continue } - try newDB.transaction { - profileHepler.createAccountTable(accountDb: newDB) - profileHepler.createContactsTable(accountDb: newDB) - conversationHelper.createTable(accountDb: newDB) - interactionHepler.createTable(accountDb: newDB) - guard let oldDb = self.dbConnections.getJamiDB() else {throw DataAccessError.datastoreConnectionError} - let profileFromJamiId = try self.profileHepler.getLegacyProfileID(profileURI: jamiId, dataBase: oldDb) - let profileFromAccountId = try self.profileHepler.getLegacyProfileID(profileURI: accountId, dataBase: oldDb) - let profile = profileFromJamiId != nil ? profileFromJamiId : profileFromAccountId - guard let accountProfile = profile else {throw DataAccessError.datastoreConnectionError} - let allProfiles = try self.profileHepler.getLegacyProfiles(accountURI: jamiId, accountId: accountId, database: oldDb) - try profileHepler.migrateToDBForAccount(from: oldDb, to: newDB, jamiId: jamiId, accountId: accountId) - VCardUtils.loadVCard(named: VCardFiles.myProfile.rawValue, - inFolder: VCardFolders.profile.rawValue) - .subscribe(onSuccess: { [unowned self] card in - let name = card.familyName - let imageData: String? = card.imageData?.base64EncodedString() - _ = self.saveAccountProfile(alias: name, photo: imageData, - accountId: accountId) - }).disposed(by: self.disposeBag) - try conversationHelper.migrateToDBForAccount(from: oldDb, to: newDB, - accountProfileId: accountProfile, - contactsMap: allProfiles) - try interactionHepler.migrateToDBForAccount(from: oldDb, to: newDB, - accountProfileId: accountProfile, - contactsMap: allProfiles) + guard let profilePath = self.dbConnections.contactProfilePath(accountId: accountId, profileURI: profile.uri, createifNotExists: true) else { return false } + try self.saveProfile(profile: profile, path: profilePath) + if !self.dbConnections.isContactProfileExists(accountId: accountId, profileURI: profile.uri) { + return false } - } catch { - throw DataAccessError.databaseMigrationError } + self.profileHepler.dropProfileTable(accountDb: dataBase) + return true + } + + func migrateAccountToVCard(for accountId: String, accountURI: String, dataBase: Connection) throws -> Bool { + if self.dbConnections.isAccountProfileExists(accountId: accountId) { return true } + guard let accountProfile = self.profileHepler.getAccountProfile(dataBase: dataBase) else { + return self.dbConnections.isAccountProfileExists(accountId: accountId) + } + guard let path = self.dbConnections.accountProfilePath(accountId: accountId) else { return false } + let type = accountURI.contains("ring:") ? URIType.ring : URIType.sip + let profile = Profile(accountURI, accountProfile.alias, accountProfile.photo, type.getString()) + try self.saveProfile(profile: profile, path: path) + if !self.dbConnections.isAccountProfileExists(accountId: accountId) { + return false + } + self.profileHepler.dropAccountTable(accountDb: dataBase) + return true } - func removeDBForAccount(accountId: String) { - self.dbConnections.removeDBForAccount(account: accountId) + func removeDBForAccount(accountId: String, removeFolder: Bool) { + self.dbConnections.removeDBForAccount(account: accountId, removeFolder: removeFolder) } func createConversationsFor(contactUri: String, accountId: String) { @@ -247,7 +275,7 @@ class DBManager { do { try _ = self.getConversationsFor(contactUri: contactUri, createIfNotExists: true, - dataBase: dataBase) + dataBase: dataBase, accountId: accountId) } catch {} } @@ -266,7 +294,7 @@ class DBManager { let author: String? = incoming ? contactUri : nil guard let conversationID = try self?.getConversationsFor(contactUri: contactUri, createIfNotExists: true, - dataBase: dataBase) else { + dataBase: dataBase, accountId: accountId) else { throw DBBridgingError.saveMessageFailed } let result = self?.addMessageTo(conversation: conversationID, author: author, @@ -324,15 +352,22 @@ class DBManager { } } - func getProfilesForAccount(accountID: String) -> [Profile]? { - guard let database = self.dbConnections.forAccount(account: accountID) else { - return nil - } + func getProfilesForAccount(accountId: String) -> [Profile]? { + var profiles = [Profile]() do { - return try self.profileHepler.selectAll(dataBase: database) + guard let path = self.dbConnections.contactsPath(accountId: accountId, + createIfNotExists: true) else { return nil } + guard let documentURL = URL(string: path) else { return nil } + let directoryContents = try FileManager.default.contentsOfDirectory(at: documentURL, includingPropertiesForKeys: nil, options: []) + for url in directoryContents { + if let profile = getProfileFromPath(path: url.path) { + profiles.append(profile) + } + } } catch { - return nil + print(error.localizedDescription) } + return profiles } func updateFileName(interactionID: Int64, name: String, accountId: String) -> Completable { @@ -425,10 +460,7 @@ class DBManager { .deleteAll(dataBase: dataBase) { completable(.error(DBBridgingError.deleteConversationFailed)) } - if !self.profileHepler - .deleteAll(dataBase: dataBase) { - completable(.error(DBBridgingError.deleteConversationFailed)) - } + self.dbConnections.removeContacts(accountId: accountId) completable(.completed) } } catch { @@ -447,12 +479,12 @@ class DBManager { throw DBBridgingError.deleteConversationFailed } try dataBase.transaction { - guard (try self.getProfile(for: participantUri, createIfNotExists: false, accounId: accountId)) != nil else { + guard (try self.getProfile(for: participantUri, createIfNotExists: false, accountId: accountId)) != nil else { throw DBBridgingError.deleteConversationFailed } guard let conversationsId = try self.getConversationsFor(contactUri: participantUri, createIfNotExists: true, - dataBase: dataBase) else { + dataBase: dataBase, accountId: accountId) else { throw DBBridgingError.deleteConversationFailed } guard let interactions = try self.interactionHepler @@ -491,7 +523,7 @@ class DBManager { do { if let profile = try self.getProfile(for: profileUri, createIfNotExists: createIfNotExists, - accounId: accountId) { + accountId: accountId) { observable.onNext(profile) observable.on(.completed) } @@ -502,13 +534,9 @@ class DBManager { } } - func accountProfileObservable(for accountId: String) -> Observable<AccountProfile> { + func accountProfileObservable(for accountId: String) -> Observable<Profile> { return Observable.create { observable in - guard let dataBase = self.dbConnections.forAccount(account: accountId) else { - observable.on(.error(DBBridgingError.getProfileFailed)) - return Disposables.create { } - } - guard let profile = self.profileHepler.getAccountProfile(dataBase: dataBase) else { + guard let profile = self.accountProfile(for: accountId) else { observable.on(.error(DBBridgingError.getProfileFailed)) return Disposables.create { } } @@ -518,33 +546,47 @@ class DBManager { } } - func accountProfile(for accountId: String) -> AccountProfile? { - guard let dataBase = self.dbConnections.forAccount(account: accountId) else { - return nil - } - return self.profileHepler.getAccountProfile(dataBase: dataBase) + func accountProfile(for accountId: String) -> Profile? { + guard let path = self.dbConnections.accountProfilePath(accountId: accountId) else { return nil } + return self.getProfileFromPath(path: path) + } + + func accountVCard(for accountId: String) -> CNContact? { + guard let path = self.dbConnections.accountProfilePath(accountId: accountId), + let data = FileManager.default.contents(atPath: path) else { return nil } + return CNContactVCardSerialization.parseToVCard(data: data) } func createOrUpdateRingProfile(profileUri: String, alias: String?, image: String?, accountId: String) -> Bool { - guard let dataBase = self.dbConnections.forAccount(account: accountId) else { - return false + let type = profileUri.contains("ring") ? ProfileType.ring : ProfileType.sip + if type == ProfileType.sip { + self.dbConnections.createAccountfolder(for: accountId) } + guard let path = self.dbConnections.contactProfilePath(accountId: accountId, profileURI: profileUri, createifNotExists: true) else {return false} + let profile = Profile(profileUri, alias, image, ProfileType.ring.rawValue) + do { - try self.profileHepler.insertOrUpdateProfile(item: profile, dataBase: dataBase) + try self.saveProfile(profile: profile, path: path) } catch { - return false + return false } - return true + return self.dbConnections.isContactProfileExists(accountId: accountId, profileURI: profileUri) } - func saveAccountProfile(alias: String?, photo: String?, accountId: String) -> Bool { - guard let dataBase = self.dbConnections.forAccount(account: accountId) else { + func saveAccountProfile(alias: String?, photo: String?, accountId: String, accountURI: String) -> Bool { + let type = accountURI.contains("ring") ? ProfileType.ring : ProfileType.sip + if type == ProfileType.sip { + self.dbConnections.createAccountfolder(for: accountId) + } + guard let path = self.dbConnections.accountProfilePath(accountId: accountId) else { return false } + let profile = Profile(accountURI, alias, photo, type.rawValue) + do { + try self.saveProfile(profile: profile, path: path) + return self.dbConnections.isAccountProfileExists(accountId: accountId) + } catch { return false } - return self.profileHepler.updateAccountProfile(accountAlias: alias, - accountPhoto: photo, - dataBase: dataBase) } // MARK: Private functions @@ -565,11 +607,10 @@ class DBManager { let partisipant = participants.first else { continue } - guard let participantProfile = try self.profileHepler.selectProfile(profileURI: partisipant, - dataBase: dataBase) else { + guard let participantProfile = try self.getProfile(for: partisipant, createIfNotExists: false, accountId: accountId) else { continue } - let type = participantProfile.uri.contains("sip:") ? URIType.sip : URIType.ring + let type = participantProfile.uri.contains("ring:") ? URIType.ring : URIType.sip let uri = JamiURI.init(schema: type, infoHach: participantProfile.uri) let conversationModel = ConversationModel(withParticipantUri: uri, accountId: accountId) @@ -672,31 +713,57 @@ class DBManager { return self.interactionHepler.insert(item: interaction, dataBase: dataBase) } - func getProfile(for profileUri: String, createIfNotExists: Bool, accounId: String) throws -> Profile? { - guard let dataBase = self.dbConnections.forAccount(account: accounId) else { - throw DataAccessError.datastoreConnectionError - } - if let profile = try self.profileHepler.selectProfile(profileURI: profileUri, dataBase: dataBase) { - return profile + func getProfile(for profileUri: String, createIfNotExists: Bool, accountId: String) throws -> Profile? { + let type = profileUri.contains("ring") ? ProfileType.ring : ProfileType.sip + if createIfNotExists && type == ProfileType.sip { + self.dbConnections.createAccountfolder(for: accountId) } - if !createIfNotExists { - return nil + guard let profilePath = self.dbConnections + .contactProfilePath(accountId: accountId, + profileURI: profileUri, + createifNotExists: createIfNotExists) else {return nil} + if self.dbConnections + .isContactProfileExists(accountId: accountId, + profileURI: profileUri) || !createIfNotExists { + return getProfileFromPath(path: profilePath) } - // for now we use template profile - let profile = self.createTemplateProfile(uri: profileUri) - if self.profileHepler.insert(item: profile, dataBase: dataBase) { - return try self.profileHepler.selectProfile(profileURI: profileUri, dataBase: dataBase) + let profile = Profile(profileUri, nil, nil, type.rawValue) + try self.saveProfile(profile: profile, path: profilePath) + return getProfileFromPath(path: profilePath) + } + + private func getProfileFromPath(path: String) -> Profile? { + guard let data = FileManager.default.contents(atPath: path), + let vCard = CNContactVCardSerialization.parseToVCard(data: data) else { + return nil } - return nil + let profileURI = vCard.phoneNumbers.isEmpty ? "" : vCard.phoneNumbers[0].value.stringValue + let type = profileURI.contains("ring") ? ProfileType.ring : ProfileType.sip + let imageString = {(data: Data?) -> String in + guard let data = data else { return "" } + return data.base64EncodedString() + }(vCard.imageData) + let profile = Profile(profileURI, vCard.familyName, imageString, type.rawValue) + return profile } - private func createTemplateProfile(uri: String) -> Profile { - let type = uri.contains("ring") ? ProfileType.ring : ProfileType.sip - return Profile(uri, nil, nil, type.rawValue) + private func saveProfile(profile: Profile, path: String) throws { + let url = URL(fileURLWithPath: path) + let contactCard = CNMutableContact() + if let name = profile.alias { + contactCard.familyName = name + } + contactCard.phoneNumbers = [CNLabeledValue(label: CNLabelPhoneNumberiPhone, value: CNPhoneNumber(stringValue: profile.uri))] + if let photo = profile.photo { + contactCard.imageData = NSData(base64Encoded: photo, + options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? + } + let data = try CNContactVCardSerialization.dataWithImageAndUUID(from: contactCard, andImageCompression: 40000) + try data.write(to: url) } private func getConversationsFor(contactUri: String, - createIfNotExists: Bool, dataBase: Connection) throws -> Int64? { + createIfNotExists: Bool, dataBase: Connection, accountId: String) throws -> Int64? { if let contactConversations = try self.conversationHelper .selectConversationsForProfile(profileUri: contactUri, dataBase: dataBase), let conv = contactConversations.first { @@ -706,7 +773,7 @@ class DBManager { return nil } let conversationID = Int64(arc4random_uniform(10000000)) - _ = self.profileHepler.insert(item: self.createTemplateProfile(uri: contactUri), dataBase: dataBase) + _ = try self.getProfile(for: contactUri, createIfNotExists: true, accountId: accountId) let conversationForContact = Conversation(conversationID, contactUri) if !self.conversationHelper.insert(item: conversationForContact, dataBase: dataBase) { return nil diff --git a/Ring/Ring/EditProfileViewModel.swift b/Ring/Ring/EditProfileViewModel.swift index d9f3694b51812ce3479e3bd7e168969a835cfe64..e6aad3d1c2ba1d9d7f0273abec72269e9adba30d 100644 --- a/Ring/Ring/EditProfileViewModel.swift +++ b/Ring/Ring/EditProfileViewModel.swift @@ -57,7 +57,7 @@ class EditProfileViewModel { }) }() - var profileForCurrentAccount = PublishSubject<AccountProfile>() + var profileForCurrentAccount = PublishSubject<Profile>() lazy var profileName: Observable<String?> = { [unowned self] in return profileForCurrentAccount.share() @@ -107,9 +107,11 @@ class EditProfileViewModel { details.set(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.displayName), withValue: self.name) account.details = details self.accountService.setAccountDetails(forAccountId: account.id, withDetails: details) + let accountUri = AccountModelHelper.init(withAccount: account).uri ?? "" self.profileService.updateAccountProfile(accountId: account.id, - alias: self.name, - photo: photo) + alias: self.name, + photo: photo, accountURI: accountUri) + } func updateImage(_ image: UIImage) { diff --git a/Ring/Ring/Extensions/CNContactVCardSerialization+Helpers.swift b/Ring/Ring/Extensions/CNContactVCardSerialization+Helpers.swift index 41095a150d5ce1b124685b182d05adf4eb76219c..e1a3d30937ee1d834482ec7a1074ecfdc1bcae36 100644 --- a/Ring/Ring/Extensions/CNContactVCardSerialization+Helpers.swift +++ b/Ring/Ring/Extensions/CNContactVCardSerialization+Helpers.swift @@ -30,8 +30,8 @@ enum VCardFields: String { case photoJPEG = "PHOTO;ENCODING=BASE64;TYPE=JPEG:" case photoPNG = "PHOTO;ENCODING=BASE64;TYPE=PNG:" case end = "END:VCARD" - case name = "N:" case fullName = "FN:" + case telephone = "TEL;other:" } extension CNContactVCardSerialization { @@ -42,11 +42,12 @@ extension CNContactVCardSerialization { let beginString = VCardFields.begin.rawValue + "\n" let entryUIDString = VCardFields.uid.rawValue + contact.identifier + "\n" let name = contact.familyName.trimmingCharacters(in: .whitespacesAndNewlines) - let firstnameString = VCardFields.name.rawValue + name + "\n" + let phone = contact.phoneNumbers.isEmpty ? "" : contact.phoneNumbers[0].value.stringValue + let telephoneString = VCardFields.telephone.rawValue + phone + "\n" let fullNameString = VCardFields.fullName.rawValue + name + "\n" let endString = VCardFields.end.rawValue - var vCardString = beginString + entryUIDString + firstnameString + fullNameString + endString + var vCardString = beginString + entryUIDString + fullNameString + telephoneString + endString // if contact have an image add it to vCard data guard var image = contact.imageData else { @@ -96,6 +97,7 @@ extension CNContactVCardSerialization { let vcard = CNMutableContact() let name = String(nameRow.suffix(nameRow.count - 3)) vcard.familyName = name + vcard.phoneNumbers = vCard.phoneNumbers vcard.imageData = vCard.imageData return vcard } diff --git a/Ring/Ring/Extensions/String+Helpers.swift b/Ring/Ring/Extensions/String+Helpers.swift index 3fbafddbedd4b632839aa144db120b7886cde994..2437ee333149101cbaa52aba6a1854b89558cbb0 100644 --- a/Ring/Ring/Extensions/String+Helpers.swift +++ b/Ring/Ring/Extensions/String+Helpers.swift @@ -131,4 +131,8 @@ extension String { } return fileIsImage } + + func toBase64() -> String { + return Data(self.utf8).base64EncodedString() + } } diff --git a/Ring/Ring/Features/ContactRequests/ContactRequestItem.swift b/Ring/Ring/Features/ContactRequests/ContactRequestItem.swift index 504f11d70f758e8d6778ebd327c8a2808c6a7b74..38994f1836d5251f03ff48445197d7fcc653c0a1 100644 --- a/Ring/Ring/Features/ContactRequests/ContactRequestItem.swift +++ b/Ring/Ring/Features/ContactRequests/ContactRequestItem.swift @@ -55,7 +55,7 @@ class ContactRequestItem { createIfNotexists: false, accountId: contactRequest.accountId) .subscribe(onNext: { [weak self] profile in - if let photo = profile.photo, + if let photo = profile.photo, !photo.isEmpty, let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? { self?.profileImageData.value = data diff --git a/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift b/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift index b3b330f9a9cbfc9a1b39c66b68adf3c34144d6ae..a32c2dec17a59d8317c9845eabf37ceb93e29a46 100644 --- a/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift +++ b/Ring/Ring/Features/Conversations/SmartList/Cells/AccountPickerAdapter.swift @@ -24,7 +24,7 @@ import RxCocoa struct AccountItem { let account: AccountModel - let profileObservable: Observable<AccountProfile> + let profileObservable: Observable<Profile> } final class AccountPickerAdapter: NSObject, UIPickerViewDataSource, UIPickerViewDelegate, RxPickerViewDataSourceType, SectionedViewDataSourceType { diff --git a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift index 8a1cd465ee536588c632a0737f3ec2076854e5a1..286ddf4da67239822bf5fce351d4ac131a4eb4dc 100644 --- a/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift +++ b/Ring/Ring/Features/Conversations/SmartList/SmartlistViewModel.swift @@ -90,7 +90,7 @@ class SmartlistViewModel: Stateable, ViewModel, FilterConversationDataSource { } let injectionBag: InjectionBag //Values need to be updated when selected account changed - var profileImageForCurrentAccount = PublishSubject<AccountProfile>() + var profileImageForCurrentAccount = PublishSubject<Profile>() lazy var profileImage: Observable<UIImage> = { [unowned self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.01, execute: { diff --git a/Ring/Ring/Services/AccountsService.swift b/Ring/Ring/Services/AccountsService.swift index 60c9bc467628570ff2a555eccd6caedbd0fb526c..949e6c67a466edd8e4f9efa8870b3a4287a14744 100644 --- a/Ring/Ring/Services/AccountsService.swift +++ b/Ring/Ring/Services/AccountsService.swift @@ -202,14 +202,12 @@ class AccountsService: AccountAdapterDelegate { fileprivate func loadDatabases() -> Bool { for account in accountList { - if dbManager.isNeedMigrationToAccountDB(accountId: account.id) { - do { - if let jamiId = AccountModelHelper - .init(withAccount: account).ringId { - try dbManager.migrateToAccountDB(accountId: account.id, - jamiId: jamiId) - } - } catch { return false} + if dbManager.isMigrationToDBv2Needed(accountId: account.id) { + if let accountURI = AccountModelHelper + .init(withAccount: account).uri { + if !dbManager.migrateToDbVersion2(accountId: account.id, + accountURI: accountURI) { return false } + } } else { do { // return false if could not open database connection @@ -300,7 +298,7 @@ class AccountsService: AccountAdapterDelegate { return true } - func getAccountProfile(accountId: String) -> AccountProfile? { + func getAccountProfile(accountId: String) -> Profile? { return self.dbManager.accountProfile(for: accountId) } @@ -361,7 +359,9 @@ class AccountsService: AccountAdapterDelegate { if try !self.dbManager.createDatabaseForAccount(accountId: accountModel.id) { throw AddAccountError.unknownError } - _ = self.dbManager.saveAccountProfile(alias: nil, photo: nil, accountId: accountModel.id) + let uri = JamiURI(schema: URIType.ring, infoHach: accountModel.jamiId) + let uriString = uri.uriString ?? "" + _ = self.dbManager.saveAccountProfile(alias: nil, photo: nil, accountId: accountModel.id, accountURI: uriString) self.loadAccountsFromDaemon() return accountModel }.take(1) @@ -385,12 +385,13 @@ class AccountsService: AccountAdapterDelegate { accountDetails.updateValue(password, forKey: ConfigKey.localPort.rawValue) } guard let account = self.accountAdapter.addAccount(accountDetails) else {return false} - _ = try self.dbManager.createDatabaseForAccount(accountId: account) - _ = self.dbManager.saveAccountProfile(alias: nil, photo: nil, accountId: account) + _ = try self.dbManager.createDatabaseForAccount(accountId: account, createFolder: true) self.loadAccountsFromDaemon() - let newAccount = self.getAccount(fromAccountId: account) + guard let newAccount = self.getAccount(fromAccountId: account) else { return false } self.currentAccount = newAccount UserDefaults.standard.set(account, forKey: self.selectedAccountID) + let accountUri = AccountModelHelper.init(withAccount: newAccount).uri ?? "" + _ = self.dbManager.saveAccountProfile(alias: nil, photo: nil, accountId: account, accountURI: accountUri) return true } catch { return false @@ -440,7 +441,9 @@ class AccountsService: AccountAdapterDelegate { if try !self.dbManager.createDatabaseForAccount(accountId: accountModel.id) { throw AddAccountError.unknownError } - _ = self.dbManager.saveAccountProfile(alias: nil, photo: nil, accountId: accountModel.id) + let uri = JamiURI(schema: URIType.ring, infoHach: accountModel.jamiId) + let uriString = uri.uriString ?? "" + _ = self.dbManager.saveAccountProfile(alias: nil, photo: nil, accountId: accountModel.id, accountURI: uriString) self.loadAccountsFromDaemon() return accountModel }.take(1) @@ -781,11 +784,12 @@ class AccountsService: AccountAdapterDelegate { } func removeAccount(id: String) { - if self.getAccount(fromAccountId: id) == nil {return} + guard let account = self.getAccount(fromAccountId: id) else {return} + let shouldRemoveFolder = AccountModelHelper.init(withAccount: account).isAccountSip() self.accountAdapter.removeAccount(id) self.loadAccountsFromDaemon() if self.getAccount(fromAccountId: id) == nil { - self.dbManager.removeDBForAccount(accountId: id) + self.dbManager.removeDBForAccount(accountId: id, removeFolder: shouldRemoveFolder) guard let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } @@ -875,7 +879,11 @@ class AccountsService: AccountAdapterDelegate { } let accountDetails = getAccountDetails(fromAccountId: account) let displayName: String? = accountDetails.get(withConfigKeyModel: ConfigKeyModel(withKey: ConfigKey.displayName)) - _ = self.dbManager.saveAccountProfile(alias: displayName, photo: photo, accountId: account) + + guard let accountToUpdate = self.getAccount(fromAccountId: account), + let accountURI = AccountModelHelper + .init(withAccount: accountToUpdate).uri else {return} + _ = self.dbManager.saveAccountProfile(alias: displayName, photo: photo, accountId: account, accountURI: accountURI) } // MARK: Push Notifications diff --git a/Ring/Ring/Services/CallsService.swift b/Ring/Ring/Services/CallsService.swift index ed56f8e9052ba5c9842290e416b3cb3460506bd8..9028bbe3d918e50788816630cd0655f2fc70f3cf 100644 --- a/Ring/Ring/Services/CallsService.swift +++ b/Ring/Ring/Services/CallsService.swift @@ -328,25 +328,12 @@ class CallsService: CallsAdapterDelegate { if accountID.isEmpty || callID.isEmpty { return } - guard let accountProfile = self.dbManager.accountProfile(for: accountID) else {return} - let vCard = CNMutableContact() - var cardChanged = false - if let name = accountProfile.alias { - vCard.familyName = name - cardChanged = true - } - if let photo = accountProfile.photo { - vCard.imageData = NSData(base64Encoded: photo, - options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? - cardChanged = true - } - if cardChanged { - DispatchQueue.main.async { [unowned self] in - VCardUtils.sendVCard(card: vCard, - callID: callID, - accountID: accountID, - sender: self) - } + guard let vCard = self.dbManager.accountVCard(for: accountID) else {return} + DispatchQueue.main.async { [unowned self] in + VCardUtils.sendVCard(card: vCard, + callID: callID, + accountID: accountID, + sender: self) } } diff --git a/Ring/Ring/Services/ContactsService.swift b/Ring/Ring/Services/ContactsService.swift index 7a9abbc210f0389086f1cf5fec4dc8328929c9e2..f3a4eef911413a15163cb170285f6265abdf0cfe 100644 --- a/Ring/Ring/Services/ContactsService.swift +++ b/Ring/Ring/Services/ContactsService.swift @@ -85,7 +85,7 @@ class ContactsService { func loadSipContacts(withAccount account: AccountModel) { guard let profiles = self.dbManager - .getProfilesForAccount(accountID: account.id) else {return} + .getProfilesForAccount(accountId: account.id) else {return} let contacts = profiles.map({ profile in return ContactModel(withUri: JamiURI.init(schema: URIType.sip, infoHach: profile.uri)) }) @@ -151,8 +151,10 @@ class ContactsService { stringImage = image.base64EncodedString() } let name = VCardUtils.getName(from: contactRequest.vCard) + let uri = JamiURI(schema: URIType.ring, infoHach: contactRequest.ringId) + let uriString = uri.uriString ?? contactRequest.ringId _ = self.dbManager - .createOrUpdateRingProfile(profileUri: contactRequest.ringId, + .createOrUpdateRingProfile(profileUri: uriString, alias: name, image: stringImage, accountId: account.id) @@ -375,7 +377,7 @@ extension ContactsService: ContactsAdapterDelegate { func getProfile(uri: String, accountId: String) -> Profile? { do { - return try self.dbManager.getProfile(for: uri, createIfNotExists: false, accounId: accountId) + return try self.dbManager.getProfile(for: uri, createIfNotExists: false, accountId: accountId) } catch { return nil } diff --git a/Ring/Ring/Services/ProfilesService.swift b/Ring/Ring/Services/ProfilesService.swift index a3c0c4b40550086af623cc8f54ec268b718d3365..c3142a0413241c99665e339bd3279d9f8691c15f 100644 --- a/Ring/Ring/Services/ProfilesService.swift +++ b/Ring/Ring/Services/ProfilesService.swift @@ -46,7 +46,7 @@ class ProfilesService { fileprivate let log = SwiftyBeaver.self var profiles = [String: ReplaySubject<Profile>]() - var accountProfiles = [String: ReplaySubject<AccountProfile>]() + var accountProfiles = [String: ReplaySubject<Profile>]() let dbManager: DBManager @@ -69,7 +69,10 @@ class ProfilesService { guard let accountId = notification.userInfo?[ProfileNotificationsKeys.accountId.rawValue] as? String else { return } - self.triggerProfileSignal(uri: ringId, createIfNotexists: false, accountId: accountId) + + let uri = JamiURI(schema: URIType.ring, infoHach: ringId) + let uriString = uri.uriString ?? ringId + self.triggerProfileSignal(uri: uriString, createIfNotexists: false, accountId: accountId) } // swiftlint:disable cyclomatic_complexity @@ -197,13 +200,12 @@ class ProfilesService { } // MARK: account profile -typealias AccountProfile = (alias: String?, photo: String?) extension ProfilesService { - func getAccountProfile(accountId: String) -> Observable<AccountProfile> { + func getAccountProfile(accountId: String) -> Observable<Profile> { if let profile = self.accountProfiles[accountId] { return profile.asObservable().share() } - let profileObservable = ReplaySubject<AccountProfile>.create(bufferSize: 1) + let profileObservable = ReplaySubject<Profile>.create(bufferSize: 1) self.accountProfiles[accountId] = profileObservable DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.triggerAccountProfileSignal(accountId: accountId) @@ -222,10 +224,10 @@ extension ProfilesService { }).disposed(by: self.disposeBag) } - func updateAccountProfile(accountId: String, alias: String?, photo: String?) { + func updateAccountProfile(accountId: String, alias: String?, photo: String?, accountURI: String) { if self.dbManager .saveAccountProfile(alias: alias, photo: photo, - accountId: accountId) { + accountId: accountId, accountURI: accountURI) { self.triggerAccountProfileSignal(accountId: accountId) } }