Skip to content
Snippets Groups Projects
AccountService.kt 69.3 KiB
Newer Older
 *  Copyright (C) 2004-2023 Savoir-faire Linux Inc.
Adrien Béraud's avatar
Adrien Béraud committed
 *  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
Adrien Béraud's avatar
Adrien Béraud committed
 *  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
Adrien Béraud's avatar
Adrien Béraud committed
 *  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
Adrien Béraud's avatar
Adrien Béraud committed
 *  along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package net.jami.services

import com.google.gson.JsonParser
import ezvcard.VCard
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.SingleSubject
import io.reactivex.rxjava3.subjects.Subject
import net.jami.daemon.*
import net.jami.model.*
import net.jami.model.Interaction.InteractionStatus
import net.jami.utils.Log
import net.jami.utils.SwigNativeConverter
import net.jami.utils.VCardUtils
import java.io.File
import java.io.UnsupportedEncodingException
import java.net.SocketException
import java.net.URLEncoder
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.absoluteValue
import kotlin.math.min

/**
 * This service handles the accounts
 * - Load and manage the accounts stored in the daemon
 * - Keep a local cache of the accounts
 * - handle the callbacks that are send by the daemon
 */
class AccountService(
    private val mExecutor: ScheduledExecutorService,
    private val mHistoryService: HistoryService,
    private val mDeviceRuntimeService: DeviceRuntimeService,
    private val mVCardService: VCardService
) {
    private val scheduler = Schedulers.from(mExecutor)
    /**
     * @return the current Account from the local cache
     */
    var currentAccount: Account?
        get() = mAccountList.getOrNull(0)
        set(account) {
            // the account order is changed
            // the current Account is now on the top of the list
            val accounts: List<Account> = mAccountList
            if (account == null || accounts.isEmpty() || accounts[0] === account)
                return
            val orderedAccountIdList: MutableList<String> = ArrayList(accounts.size)
Adrien Béraud's avatar
Adrien Béraud committed
            val selectedID = account.accountId
            orderedAccountIdList.add(selectedID)
            for (a in accounts) {
                if (a.accountId == selectedID)
Adrien Béraud's avatar
Adrien Béraud committed
                orderedAccountIdList.add(a.accountId)
            }
            setAccountOrder(orderedAccountIdList)
        }

    private var mAccountList: List<Account> = ArrayList()
    private var mHasSipAccount = false
    private var mHasRingAccount = false
    private var mStartingTransfer: DataTransfer? = null
    private val accountsSubject = BehaviorSubject.create<List<Account>>()
    private val observableAccounts: Subject<Account> = PublishSubject.create()
    val currentAccountSubject: Observable<Account> = accountsSubject
        .filter { l -> l.isNotEmpty() }
        .map { l -> l[0] }
        .distinctUntilChanged()

Adrien Béraud's avatar
Adrien Béraud committed
    data class Message(
        val accountId: String,
        val messageId: String?,
        val callId: String?,
        val author: String,
        val messages: Map<String, String>
    )

    class Location(
        val account: String,
        val callId: String?,
        val peer: Uri,
        var date: Long) {
        enum class Type {
            Position, Stop
        }

        lateinit var type: Type
        var latitude = 0.0
        var longitude = 0.0

        override fun toString(): String = "Location{$type $latitude $longitude $date account:$account callId:$callId peer:$peer}"
    }

    private val incomingMessageSubject: Subject<Message> = PublishSubject.create()
    private val incomingSwarmMessageSubject: Subject<Interaction> = PublishSubject.create()
    private val incomingGroupCallSubject: Subject<Conversation> = PublishSubject.create()
    val incomingMessages: Observable<TextMessage> = incomingMessageSubject
        .flatMapMaybe { msg: Message ->
            val message = msg.messages[CallService.MIME_TEXT_PLAIN]
            if (message != null) {
                return@flatMapMaybe mHistoryService
                    .incomingMessage(msg.accountId, msg.messageId, msg.author, message)
                    .toMaybe()
            }
            Maybe.empty()
        }
        .share()
    val locationUpdates: Observable<Location> = incomingMessageSubject
        .flatMapMaybe { msg: Message ->
            try {
                val loc = msg.messages[CallService.MIME_GEOLOCATION] ?: return@flatMapMaybe Maybe.empty<Location>()
                val obj = JsonParser.parseString(loc).asJsonObject
                if (obj.size() < 2) return@flatMapMaybe Maybe.empty<Location>()
Adrien Béraud's avatar
Adrien Béraud committed
                Maybe.just(Location(msg.accountId, msg.callId, Uri.fromId(msg.author), obj["time"].asLong).apply {
                    val t = obj["type"]
                    if (t == null || t.asString.lowercase() == Location.Type.Position.toString().lowercase()) {
                        type = Location.Type.Position
                        latitude = obj["lat"].asDouble
                        longitude = obj["long"].asDouble
                    } else if (t.asString.lowercase() == Location.Type.Stop.toString().lowercase()) {
                        type = Location.Type.Stop
                    }
                })
            } catch (e: Exception) {
                Log.w(TAG, "Failed to receive geolocation", e)
Adrien Béraud's avatar
Adrien Béraud committed
                Maybe.empty<Location>()
            }
        }
        .share()
    private val messageSubject: Subject<Interaction> = PublishSubject.create()
    val dataTransfers: Subject<DataTransfer> = PublishSubject.create()
    private val incomingRequestsSubject: Subject<TrustRequest> = PublishSubject.create()

    data class RegisteredName(
        val accountId: String,
        val name: String,
        val address: String? = null,
        val state: Int = 0
    )

    data class ConversationSearchResult(val results: List<Interaction>)
    private val conversationSearches: MutableMap<Long, Subject<ConversationSearchResult>> = ConcurrentHashMap()
    private val loadingTasks: MutableMap<Long, SingleSubject<List<Interaction>>> = ConcurrentHashMap()
    class UserSearchResult(val accountId: String, val query: String, var state: Int = 0) {
        var results: List<Contact>? = null
    }

    private val registeredNameSubject: Subject<RegisteredName> = PublishSubject.create()
    private val searchResultSubject: Subject<UserSearchResult> = PublishSubject.create()

Adrien Béraud's avatar
Adrien Béraud committed
    private data class ExportOnRingResult (
        val accountId: String,
        val code: Int,
        val pin: String?
Adrien Béraud's avatar
Adrien Béraud committed
    private data class DeviceRevocationResult (
        val accountId: String,
        val deviceId: String,
        val code: Int
Adrien Béraud's avatar
Adrien Béraud committed
    private data class MigrationResult (
        val accountId: String,
        val state: String
    )

    private val mExportSubject: Subject<ExportOnRingResult> = PublishSubject.create()
    private val mDeviceRevocationSubject: Subject<DeviceRevocationResult> = PublishSubject.create()
    private val mMigrationSubject: Subject<MigrationResult> = PublishSubject.create()
Adrien Béraud's avatar
Adrien Béraud committed
    private val registeredNames: Observable<RegisteredName>
        get() = registeredNameSubject
Adrien Béraud's avatar
Adrien Béraud committed
    private val searchResults: Observable<UserSearchResult>
        get() = searchResultSubject
    val incomingSwarmMessages: Observable<TextMessage>
        get() = incomingSwarmMessageSubject
            .filter { i: Interaction -> i is TextMessage }
            .map { i: Interaction -> i as TextMessage }
    val messageStateChanges: Observable<Interaction>
        get() = messageSubject
    val incomingRequests: Observable<TrustRequest>
        get() = incomingRequestsSubject
    val incomingGroupCall: Observable<Conversation>
        get() = incomingGroupCallSubject

    /**
     * @return true if at least one of the loaded accounts is a SIP one
     */
Adrien Béraud's avatar
Adrien Béraud committed
    fun hasSipAccount(): Boolean = mHasSipAccount
Adrien Béraud's avatar
Adrien Béraud committed
     * @return true if at least one of the loaded accounts is a Jami one
Adrien Béraud's avatar
Adrien Béraud committed
    fun hasJamiAccount(): Boolean = mHasRingAccount

    /**
     * Loads the accounts from the daemon and then builds the local cache (also sends ACCOUNTS_CHANGED event)
     *
     * @param isConnected sets the initial connection state of the accounts
     */
    fun loadAccountsFromDaemon(isConnected: Boolean) {
        refreshAccountsCacheFromDaemon()
        setAccountsActive(isConnected)
    }

    private fun refreshAccountsCacheFromDaemon() {
        val curList: List<Account> = mAccountList
        val toLoad: MutableList<Account> = ArrayList()
        val newAccounts: List<Account> = JamiService.getAccountList().map { id ->
            curList.find { it.accountId == id } ?: Account(id, JamiService.getAccountDetails(id), JamiService.getCredentials(id), JamiService.getVolatileAccountDetails(id)).apply {
                toLoad.add(this)
            }
        mAccountList = newAccounts
        val scheduler = Schedulers.computation()
        toLoad.forEach {
            scheduler.createWorker().schedule {
                loadAccount(it)
                it.loadedSubject.onComplete()
            }
        }
        // Cleanup removed accounts
        for (acc in curList) if (!newAccounts.contains(acc)) acc.cleanup()
        accountsSubject.onNext(newAccounts)
    }
    private fun loadAccount(account: Account) {
        if (!account.isJami) {
            return
        }
        Log.w(TAG, "${account.accountId} loading devices")
        account.devices = JamiService.getKnownRingDevices(account.accountId).toNative()
        Log.w(TAG, "${account.accountId} loading contacts")
        account.setContacts(JamiService.getContacts(account.accountId).toNative())
        val conversations: List<String> = JamiService.getConversations(account.accountId)
        Log.w(TAG, "${account.accountId} loading ${conversations.size} conversations: ")
        for (conversationId in conversations) {
            try {
                val info: Map<String, String> = JamiService.conversationInfos(account.accountId, conversationId).toNativeFromUtf8()
                //info.forEach { (key, value) -> Log.w(TAG, "conversation info: $key $value") }
                val mode = if ("true" == info["syncing"]) Conversation.Mode.Syncing else Conversation.Mode.values()[info["mode"]?.toInt() ?: Conversation.Mode.Syncing.ordinal]
                val conversation = account.newSwarm(conversationId, mode)
                conversation.setProfile(mVCardService.loadConversationProfile(info))
                JamiService.getActiveCalls(account.accountId, conversationId)
                    .map { Conversation.ActiveCall(it) }
                    .let { conversation.setActiveCalls(it) }

                val preferences = // Load conversation preferences (color, symbol, etc.)
                    JamiService.getConversationPreferences(account.accountId, conversationId)
                conversation.updatePreferences(preferences)

                conversation.setLastMessageNotified(mHistoryService.getLastMessageNotified(account.accountId, conversation.uri))
                for (member in JamiService.getConversationMembers(account.accountId, conversationId)) {
                    /*for (Map.Entry<String, String> i : member.entrySet()) {
                        Log.w(TAG, "conversation member: " + i.getKey() + " " + i.getValue());
                    }*/
                    val uri = Uri.fromId(member["uri"]!!)
                    //String role = member.get("role");
                    val lastDisplayed = member["lastDisplayed"]
                    var contact = conversation.findContact(uri)
                    if (contact == null) {
                        contact = account.getContactFromCache(uri)
                        conversation.addContact(contact)
                    if (!lastDisplayed.isNullOrEmpty()) {
                        if (contact.isUser) {
                            conversation.setLastMessageRead(lastDisplayed)
                        } else {
                            conversation.setLastMessageDisplayed(uri.host, lastDisplayed)
                if (!conversation.lastElementLoadedSubject.hasValue())
                    conversation.lastElementLoadedSubject.onSuccess(loadMore(conversation, 8).ignoreElement().cache())
                account.conversationStarted(conversation)
            } catch (e: Exception) {
                Log.w(TAG, "Error loading conversation", e)
        Log.w(TAG, "${account.accountId} loading conversation requests")
        for (requestData in JamiService.getConversationRequests(account.accountId).map { it.toNativeFromUtf8() }) {
            try {
                /* for ((key, value) in requestData.entries)
                Log.e(TAG, "Request: $key $value") */
                val from = Uri.fromString(requestData["from"]!!)
                val conversationId = requestData["id"] ?: continue
                account.addRequest(TrustRequest(
                    account.accountId,
                    from,
                    requestData["received"]!!.toLong() * 1000L,
                    Uri(Uri.SWARM_SCHEME, conversationId),
                    mVCardService.loadConversationProfile(requestData),
                    requestData["mode"]?.let { m -> Conversation.Mode.values()[m.toInt()] } ?: Conversation.Mode.OneToOne))
            } catch (e: Exception) {
                Log.w(TAG, "Error loading request", e)
            }
        }
        account.setHistoryLoaded()
Adrien Béraud's avatar
Adrien Béraud committed
    fun getNewAccountName(prefix: String): String {
        val accountList = mAccountList
Adrien Béraud's avatar
Adrien Béraud committed
        var name = String.format(prefix, "").trim { it <= ' ' }
        if (accountList.firstOrNull { it.alias == name } == null) {
            return name
        }
        var num = 1
        do {
            num++
            name = String.format(prefix, num).trim { it <= ' ' }
        } while (accountList.firstOrNull { it.alias == name } != null)
        return name
    }

    /**
     * Adds a new Account in the Daemon (also sends an ACCOUNT_ADDED event)
     * Sets the new account as the current one
     *
     * @param map the account details
     * @return the created Account
     */
    fun addAccount(map: Map<String, String>): Observable<Account> =
        Single.fromCallable {
            JamiService.addAccount(StringMap.toSwig(map)).apply {
            if (isEmpty()) throw RuntimeException("Can't create account.") }
        .flatMapObservable { accountId ->
            Observable.merge(observableAccountList.mapOptional { Optional.ofNullable(it.firstOrNull { a -> a.accountId == accountId }) },
                observableAccounts.filter { account: Account -> account.accountId == accountId })
        .subscribeOn(scheduler)

    /**
     * @return the Account from the local cache that matches the accountId
     */
    fun getAccount(accountId: String?): Account? =
        if (!accountId.isNullOrEmpty()) mAccountList.find { accountId == it.accountId } else null
    fun getAccountSingle(accountId: String): Single<Account> = accountsSubject
        .firstOrError()
        .map { accounts -> accounts.first { it.accountId == accountId } }

    val observableAccountList: Observable<List<Account>>
        get() = accountsSubject

Adrien Béraud's avatar
Adrien Béraud committed
    fun getObservableAccountUpdates(accountId: String): Observable<Account> =
        observableAccounts.filter { acc -> acc.accountId == accountId }
Adrien Béraud's avatar
Adrien Béraud committed
    fun getObservableAccountProfile(accountId: String): Observable<Pair<Account, Profile>> =
        getObservableAccount(accountId).flatMap { a: Account ->
            mVCardService.loadProfile(a).map { profile -> Pair(a, profile) }
        }

Adrien Béraud's avatar
Adrien Béraud committed
    fun getObservableAccount(accountId: String): Observable<Account> =
Adrien Béraud's avatar
Adrien Béraud committed
        Observable.fromCallable<Account> { getAccount(accountId)!! }
            .concatWith(getObservableAccountUpdates(accountId))

Adrien Béraud's avatar
Adrien Béraud committed
    fun getObservableAccount(account: Account): Observable<Account> =
        Observable.just(account)
            .concatWith(observableAccounts.filter { acc -> acc === account })

    val currentProfileAccountSubject: Observable<Pair<Account, Profile>>
        get() = currentAccountSubject.flatMap { a: Account ->
            mVCardService.loadProfile(a).map { profile -> Pair(a, profile) }
Adrien Béraud's avatar
Adrien Béraud committed
    fun subscribeBuddy(accountID: String, uri: String, flag: Boolean) {
        mExecutor.execute { JamiService.subscribeBuddy(accountID, uri, flag) }
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun setMessageDisplayed(accountId: String?, conversationUri: Uri, messageId: String) {
        mExecutor.execute { JamiService.setMessageDisplayed(accountId, conversationUri.uri, messageId, 3) }
    }

    fun startConversation(accountId: String, initialMembers: Collection<String>): Single<Conversation> =
        getAccountSingle(accountId).map { account ->
            Log.w(TAG, "startConversation")
            val id = JamiService.startConversation(accountId)
            val conversation = account.getSwarm(id)!!
            for (member in initialMembers) {
                Log.w(TAG, "addConversationMember $member")
                JamiService.addConversationMember(accountId, id, member)
                conversation.addContact(account.getContactFromCache(member))
            }
            account.conversationStarted(conversation)
            Log.w(TAG, "loadConversationMessages")
            conversation
        }.subscribeOn(scheduler)
    fun removeConversation(accountId: String, conversationUri: Uri): Completable =
        Completable.fromAction { JamiService.removeConversation(accountId, conversationUri.rawRingId) }
            .subscribeOn(scheduler)
    private fun loadConversationHistory(accountId: String, conversationUri: Uri, root: String, n: Long) =
        Schedulers.io().scheduleDirect { JamiService.loadConversation(accountId, conversationUri.rawRingId, root, n) }
    fun loadMore(conversation: Conversation, n: Int = 32): Single<Conversation> {
        synchronized(conversation) {
            val mode = conversation.mode.blockingFirst()
            if (mode == Conversation.Mode.Syncing || mode == Conversation.Mode.Request) {
                Log.w(TAG, "loadMore: conversation is syncing")
                return Single.just(conversation)
            }
            conversation.loading?.let { return it }
            val ret = SingleSubject.create<Conversation>()
            conversation.loading = ret
            // load n messages before the oldest one in the history
            loadConversationHistory(conversation.accountId, conversation.uri, "", n.toLong())
    fun loadUntil(conversation: Conversation, from: String = "", until: String = ""): Single<List<Interaction>> {
        val mode = conversation.mode.blockingFirst()
        if (mode == Conversation.Mode.Syncing || mode == Conversation.Mode.Request) {
            Log.w(TAG, "loadUntil: conversation is syncing")
            return Single.just(emptyList())
        }
        return SingleSubject.create<List<Interaction>>().apply {
            loadingTasks[JamiService.loadSwarmUntil(conversation.accountId, conversation.uri.rawRingId, from, until)] = this
    fun searchConversation(
        accountId: String,
        conversationUri: Uri,
        query: String = "",
        author: String = "",
        type: String = "",
        lastId: String = "",
        after: Long = 0,
        before: Long = 0,
        maxResult: Long = 0
    ): Observable<ConversationSearchResult> = PublishSubject.create<ConversationSearchResult>().apply {
        conversationSearches[JamiService.searchConversation(
            accountId, conversationUri.rawRingId, author, lastId, query, type, after, before, maxResult, 0)] = this
    }

    fun messagesFound(id: Long, accountId: String, conversationId: String, messages: List<Map<String, String>>) {
        if (conversationId.isEmpty()) {
            conversationSearches.remove(id)?.onComplete()
        } else if (messages.isNotEmpty()) {
            val account = getAccount(accountId) ?: return
            val conversation = account.getSwarm(conversationId) ?: return
            conversationSearches[id]?.onNext(ConversationSearchResult(messages.map { getInteraction(account, conversation, it) }))
    fun sendConversationMessage(accountId: String, conversationUri: Uri, txt: String, replyTo: String?, flag: Int = 0) {
        mExecutor.execute {
            Log.w(TAG, "sendConversationMessage ${conversationUri.rawRingId} $txt $replyTo $flag")
            JamiService.sendMessage(accountId, conversationUri.rawRingId, txt, replyTo ?: "", flag)
    fun deleteConversationMessage(accountId: String, conversationUri: Uri, messageId: String) {
        sendConversationMessage(accountId, conversationUri, "", messageId, 1)
    }
    fun editConversationMessage(accountId: String, conversationUri: Uri, txt: String, messageId: String) {
        sendConversationMessage(accountId, conversationUri, txt, messageId, 1)
    }
    fun sendConversationReaction(accountId: String, conversationUri: Uri, txt: String, replyTo: String) {
        sendConversationMessage(accountId, conversationUri, txt, replyTo, 2)
    }

    /**
     * Sets the order of the accounts in the Daemon
     *
     * @param accountOrder The ordered list of account ids
     */
    private fun setAccountOrder(accountOrder: List<String>) {
        mExecutor.execute {
            val order = StringBuilder()
            for (accountId in accountOrder) {
                order.append(accountId)
                order.append(File.separator)
            }
            JamiService.setAccountsOrder(order.toString())
        }
    }

    /**
     * Sets the account details in the Daemon
     */
    fun setAccountDetails(accountId: String, map: Map<String, String>) {
        Log.i(TAG, "setAccountDetails() $accountId")
        mExecutor.execute { JamiService.setAccountDetails(accountId, StringMap.toSwig(map)) }
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun migrateAccount(accountId: String, password: String): Single<String> {
        return mMigrationSubject
            .filter { r: MigrationResult -> r.accountId == accountId }
            .map { r: MigrationResult -> r.state }
            .firstOrError()
            .doOnSubscribe {
                val details = getAccount(accountId)!!.details
Adrien Béraud's avatar
Adrien Béraud committed
                details[ConfigKey.ARCHIVE_PASSWORD.key] = password
                mExecutor.execute { JamiService.setAccountDetails(accountId, StringMap.toSwig(details)) }
            }
            .subscribeOn(scheduler)
Adrien Béraud's avatar
Adrien Béraud committed
    fun setAccountEnabled(accountId: String, active: Boolean) {
        mExecutor.execute { JamiService.sendRegister(accountId, active) }
    }

    /**
     * Sets the activation state of the account in the Daemon
     */
Adrien Béraud's avatar
Adrien Béraud committed
    fun setAccountActive(accountId: String, active: Boolean) {
        mExecutor.execute { JamiService.setAccountActive(accountId, active) }
    }

    /**
     * Sets the activation state of all the accounts in the Daemon
     */
    fun setAccountsActive(active: Boolean) {
        mExecutor.execute {
            Log.i(TAG, "setAccountsActive() running... $active")
            for (a in mAccountList) {
                // If the proxy is enabled we can considered the account
                // as always active
                if (a.isDhtProxyEnabled) {
                    JamiService.setAccountActive(a.accountId, true)
                } else {
                    JamiService.setAccountActive(a.accountId, active)
                }
            }
        }
    }

    /**
     * Sets the video activation state of all the accounts in the local cache
     */
    fun setAccountsVideoEnabled(isEnabled: Boolean) {
        for (account in mAccountList) {
            account.setDetail(ConfigKey.VIDEO_ENABLED, isEnabled)
        }
    }

    /**
     * @return the default template (account details) for a type of account
     */
    fun getAccountTemplate(accountType: String): Single<HashMap<String, String>> {
        Log.i(TAG, "getAccountTemplate() $accountType")
        return Single.fromCallable { JamiService.getAccountTemplate(accountType).toNative() }
            .subscribeOn(scheduler)
    }

    /**
     * Removes the account in the Daemon as well as local history
     */
    fun removeAccount(accountId: String) {
        Log.i(TAG, "removeAccount() $accountId")
        mExecutor.execute { JamiService.removeAccount(accountId) }
        mHistoryService.clearHistory(accountId).subscribe()
    }

    /**
     * Exports the account on the DHT (used for multi-devices feature)
     */
    fun exportOnRing(accountId: String, password: String): Single<String> =
        mExportSubject
            .filter { r: ExportOnRingResult -> r.accountId == accountId }
            .firstOrError()
            .map { result: ExportOnRingResult ->
                when (result.code) {
                    PIN_GENERATION_SUCCESS -> return@map result.pin!!
                    PIN_GENERATION_WRONG_PASSWORD -> throw IllegalArgumentException()
                    PIN_GENERATION_NETWORK_ERROR -> throw SocketException()
                    else -> throw UnsupportedOperationException()
                }
            }
            .doOnSubscribe {
                Log.i(TAG, "exportOnRing() $accountId")
                mExecutor.execute { JamiService.exportOnRing(accountId, password) }
            }
            .subscribeOn(Schedulers.io())

    /**
     * @return the list of the account's devices from the Daemon
     */
    fun getKnownRingDevices(accountId: String): Map<String, String> {
        Log.i(TAG, "getKnownRingDevices() $accountId")
        return try {
             mExecutor.submit<HashMap<String, String>> {
                JamiService.getKnownRingDevices(accountId).toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running getKnownRingDevices()", e)
            return HashMap()
        }
    }

    /**
     * @param accountId id of the account used with the device
     * @param deviceId  id of the device to revoke
     * @param password  password of the account
     */
    fun revokeDevice(accountId: String, password: String, deviceId: String): Single<Int> =
        mDeviceRevocationSubject
            .filter { r: DeviceRevocationResult -> r.accountId == accountId && r.deviceId == deviceId }
            .firstOrError()
            .map { r: DeviceRevocationResult -> r.code }
            .doOnSubscribe { mExecutor.execute {
                JamiService.revokeDevice(accountId, deviceId, "password", password)
            }}
            .subscribeOn(Schedulers.io())

    /**
     * @param accountId id of the account used with the device
     * @param newName   new device name
     */
    fun renameDevice(accountId: String, newName: String) {
        val account = getAccount(accountId)
        mExecutor.execute {
            Log.i(TAG, "renameDevice() thread running... $newName")
            val details = JamiService.getAccountDetails(accountId)
Adrien Béraud's avatar
Adrien Béraud committed
            details[ConfigKey.ACCOUNT_DEVICE_NAME.key] = newName
            JamiService.setAccountDetails(accountId, details)
            account?.setDetail(ConfigKey.ACCOUNT_DEVICE_NAME, newName)
            account?.devices = JamiService.getKnownRingDevices(accountId).toNative()
    fun exportToFile(accountId: String, absolutePath: String, password: String): Completable =
        Completable.fromAction {
            require(JamiService.exportToFile(accountId, absolutePath, "password", password)) { "Can't export archive" }
        }.subscribeOn(scheduler)

    /**
     * @param accountId   id of the account
     * @param oldPassword old account password
     */
    fun setAccountPassword(accountId: String, oldPassword: String, newPassword: String): Completable =
        Completable.fromAction {
            require(JamiService.changeAccountPassword(accountId, oldPassword, newPassword)) { "Can't change password" }
        }.subscribeOn(scheduler)
    fun getAccountPasswordKey(accountId: String, password: String): Single<ByteArray> =
        Single.fromCallable { JamiService.getPasswordKey(accountId, password).bytes }
            .subscribeOn(Schedulers.computation())

    /**
     * Sets the active codecs list of the account in the Daemon
     */
    fun setActiveCodecList(accountId: String, codecs: List<Long>) {
        mExecutor.execute {
            val list = UintVect()
            list.reserve(codecs.size.toLong())
            list.addAll(codecs)
            JamiService.setActiveCodecList(accountId, list)
Adrien Béraud's avatar
Adrien Béraud committed
            observableAccounts.onNext(getAccount(accountId) ?: return@execute)
        }
    }

    /**
     * @return The account's codecs list from the Daemon
     */
    fun getCodecList(accountId: String): Single<List<Codec>> = Single.fromCallable {
        val activePayloads = JamiService.getActiveCodecList(accountId)
        JamiService.getCodecList()
            .map { Codec(it, JamiService.getCodecDetails(accountId, it), activePayloads.contains(it)) }

    fun validateCertificatePath(
Adrien Béraud's avatar
Adrien Béraud committed
        accountID: String,
        certificatePath: String,
        privateKeyPath: String,
        privateKeyPass: String
    ): Map<String, String>? {
        try {
            return mExecutor.submit<HashMap<String, String>> {
                Log.i(TAG, "validateCertificatePath() running...")
Adrien Béraud's avatar
Adrien Béraud committed
                JamiService.validateCertificatePath(accountID, certificatePath, privateKeyPath, privateKeyPass, "").toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running validateCertificatePath()", e)
        }
        return null
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun validateCertificate(accountId: String, certificate: String): Map<String, String>? {
        try {
            return mExecutor.submit<HashMap<String, String>> {
                Log.i(TAG, "validateCertificate() running...")
                JamiService.validateCertificate(accountId, certificate).toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running validateCertificate()", e)
        }
        return null
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun getCertificateDetailsPath(accountId: String, certificatePath: String): Map<String, String>? {
        try {
            return mExecutor.submit<HashMap<String, String>> {
                Log.i(TAG, "getCertificateDetailsPath() running...")
Adrien Béraud's avatar
Adrien Béraud committed
                JamiService.getCertificateDetails(accountId, certificatePath).toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running getCertificateDetailsPath()", e)
        }
        return null
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun getCertificateDetails(accountId: String, certificateRaw: String): Map<String, String>? {
        try {
            return mExecutor.submit<HashMap<String, String>> {
                Log.i(TAG, "getCertificateDetails() running...")
Adrien Béraud's avatar
Adrien Béraud committed
                JamiService.getCertificateDetails(accountId, certificateRaw).toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running getCertificateDetails()", e)
        }
        return null
    }

    /**
     * @return the supported TLS methods from the Daemon
     */
    val tlsSupportedMethods: List<String>
        get() {
            Log.i(TAG, "getTlsSupportedMethods()")
            return SwigNativeConverter.toJava(JamiService.getSupportedTlsMethod())
        }

    /**
     * @return the account's credentials from the Daemon
     */
Adrien Béraud's avatar
Adrien Béraud committed
    fun getCredentials(accountId: String): List<Map<String, String>>? {
        try {
            return mExecutor.submit<ArrayList<Map<String, String>>> {
                Log.i(TAG, "getCredentials() running...")
                JamiService.getCredentials(accountId).toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running getCredentials()", e)
        }
        return null
    }

    /**
     * Sets the account's credentials in the Daemon
     */
    fun setCredentials(accountId: String, credentials: List<Map<String, String>>) {
        Log.i(TAG, "setCredentials() $accountId")
        mExecutor.execute { JamiService.setCredentials(accountId, SwigNativeConverter.toSwig(credentials)) }
    }

    /**
     * Sets the registration state to true for all the accounts in the Daemon
     */
    fun registerAllAccounts() {
        Log.i(TAG, "registerAllAccounts()")
        mExecutor.execute { registerAllAccounts() }
    }

    /**
     * Registers a new name on the blockchain for the account
     */
    fun registerName(account: Account, password: String?, name: String) {
        if (account.registeringUsername) {
            Log.w(TAG, "Already trying to register username")
            return
        }
        account.registeringUsername = true
Adrien Béraud's avatar
Adrien Béraud committed
        registerName(account.accountId, password ?: "", name)
    }

    /**
     * Register a new name on the blockchain for the account Id
     */
    fun registerName(account: String, password: String, name: String) {
        Log.i(TAG, "registerName()")
        mExecutor.execute { JamiService.registerName(account, name, "password", password) }
    }
    /* contact requests */
    /**
     * @return all trust requests from the daemon for the account Id
     */
    fun getTrustRequests(accountId: String): List<Map<String, String>>? {
        try {
            return mExecutor.submit<ArrayList<Map<String, String>>> {
                JamiService.getTrustRequests(accountId).toNative()
            }.get()
        } catch (e: Exception) {
            Log.e(TAG, "Error running getTrustRequests()", e)
        }
        return null
    }

    /**
     * Accepts a pending trust request
     */
    fun acceptTrustRequest(accountId: String, from: Uri) {
        Log.i(TAG, "acceptRequest() $accountId $from")
        mExecutor.execute {
            if (from.isSwarm)
                JamiService.acceptConversationRequest(accountId, from.rawRingId)
                JamiService.acceptTrustRequest(accountId, from.rawRingId)
                /*getAccount(accountId)?.let { account -> account.getRequest(from)?.vCard?.let{ vcard ->
                    VCardUtils.savePeerProfileToDisk(vcard, accountId, from.rawRingId + ".vcf", mDeviceRuntimeService.provideFilesDir())
                }}*/
            }
    }

    /**
     * Refuses and blocks a pending trust request
     */
    fun discardTrustRequest(accountId: String, contactUri: Uri): Boolean =
        if (contactUri.isSwarm)  {
            JamiService.declineConversationRequest(accountId, contactUri.rawRingId)
            true
        } else {
            val account = getAccount(accountId)
            var removed = false
            if (account != null) {
                removed = account.removeRequest(contactUri) != null
                mHistoryService.clearHistory(contactUri.rawRingId, accountId, true).subscribe()
            }
            mExecutor.execute { JamiService.discardTrustRequest(accountId, contactUri.rawRingId) }
            removed
        }

    /**
     * Sends a new trust request
     */
    fun sendTrustRequest(conversation: Conversation, to: Uri, message: Blob = Blob()) {
        Log.i(TAG, "sendTrustRequest() " + conversation.accountId + " " + to)
        mExecutor.execute { JamiService.sendTrustRequest(conversation.accountId, to.rawRingId, message) }
    }

    /**
     * Add a new contact for the account Id on the Daemon
     */
    fun addContact(accountId: String, uri: String) {
        Log.i(TAG, "addContact() $accountId $uri")
        mExecutor.execute { JamiService.addContact(accountId, uri) }
    }

    /**
     * Remove an existing contact for the account Id on the Daemon
     */
    fun removeContact(accountId: String, uri: String, ban: Boolean) {
        Log.i(TAG, "removeContact() $accountId $uri ban:$ban")
        mExecutor.execute { JamiService.removeContact(accountId, uri, ban) }
    }

    fun findRegistrationByName(account: String, nameserver: String, name: String): Single<RegisteredName> =
        if (name.isEmpty())
            Single.just(RegisteredName(account, name))
        else registeredNames
            .filter { r: RegisteredName -> account == r.accountId && name == r.name }
            .firstOrError()
            .doOnSubscribe {
                mExecutor.execute { JamiService.lookupName(account, nameserver, name) }
            }
            .subscribeOn(scheduler)
    fun findRegistrationByAddress(account: String, nameserver: String, address: String): Single<RegisteredName> =
        if (address.isEmpty())
            Single.error(IllegalArgumentException())
        else registeredNames
            .filter { r: RegisteredName -> account == r.accountId && address == r.address }
            .firstOrError()
            .doOnSubscribe {
                mExecutor.execute { JamiService.lookupAddress(account, nameserver, address) }
            }
            .subscribeOn(scheduler)
    fun searchUser(account: String, query: String): Single<UserSearchResult> {
        if (query.isEmpty()) {
            return Single.just(UserSearchResult(account, query))
        }
        val encodedUrl: String = try {
            URLEncoder.encode(query, "UTF-8")
        } catch (e: UnsupportedEncodingException) {
            return Single.error(e)
        }
        return searchResults
            .filter { r: UserSearchResult -> account == r.accountId && encodedUrl == r.query }
            .firstOrError()
            .doOnSubscribe {
                mExecutor.execute { JamiService.searchUser(account, encodedUrl) }
            }
            .subscribeOn(scheduler)
    }

    /**
     * Reverse looks up the address in the blockchain to find the name
     */
Adrien Béraud's avatar
Adrien Béraud committed
    fun lookupAddress(account: String, nameserver: String, address: String) {
Adrien Béraud's avatar
Adrien Béraud committed
        //Log.w(TAG, "lookupAddress $address")
        mExecutor.execute { JamiService.lookupAddress(account, nameserver, address) }
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun pushNotificationReceived(from: String, data: Map<String, String>) {
        // Log.i(TAG, "pushNotificationReceived() $data");
        mExecutor.execute { JamiService.pushNotificationReceived(from, StringMap.toSwig(data)) }
    }

Adrien Béraud's avatar
Adrien Béraud committed
    fun setPushNotificationToken(pushNotificationToken: String) {
        Log.i(TAG, "setPushNotificationToken()");
        mExecutor.execute { JamiService.setPushNotificationToken(pushNotificationToken) }
    }
    fun setPushNotificationConfig(token: String = "", topic: String = "", platform: String = "") {
        Log.i(TAG, "setPushNotificationConfig() $token $topic $platform");
        mExecutor.execute { JamiService.setPushNotificationConfig(StringMap().apply {
            put("token", token)
            put("topic", topic)
            put("platform", platform)

    fun volumeChanged(device: String, value: Int) {
        Log.w(TAG, "volumeChanged $device $value")
    }

    fun accountsChanged() {
        // Accounts have changed in Daemon, we have to update our local cache
        refreshAccountsCacheFromDaemon()
    }

    fun stunStatusFailure(accountId: String) {
        Log.d(TAG, "stun status failure: $accountId")
    }

    fun registrationStateChanged(accountId: String, newState: String, code: Int, detailString: String?) {
        Log.d(TAG, "registrationStateChanged: $accountId, $newState, $code, $detailString")
        val account = getAccount(accountId) ?: return
        val state = AccountConfig.RegistrationState.valueOf(newState)
        val oldState = account.registrationState
        if (oldState == AccountConfig.RegistrationState.INITIALIZING && state != AccountConfig.RegistrationState.INITIALIZING) {
Adrien Béraud's avatar
Adrien Béraud committed
            account.setDetails(JamiService.getAccountDetails(account.accountId).toNative())
            account.setCredentials(JamiService.getCredentials(account.accountId).toNative())
            account.devices = JamiService.getKnownRingDevices(account.accountId).toNative()
            account.setVolatileDetails(JamiService.getVolatileAccountDetails(account.accountId).toNative())
            account.setRegistrationState(state, code)
            /*synchronized(account) {
                if (!account.loadedSubject.hasValue()) {
                    account.loadedSubject.onSuccess(Completable.fromAction { loadAccount(account) }
                        .subscribeOn(Schedulers.computation()))
                }
            }*/
        if (oldState != state) {
            observableAccounts.onNext(account)
        }
    }

    fun accountDetailsChanged(accountId: String, details: Map<String, String>) {
        val account = getAccount(accountId) ?: return
        Log.d(TAG, "accountDetailsChanged: $accountId ${details.size}")
        account.setDetails(details)
        observableAccounts.onNext(account)
    }

    fun volatileAccountDetailsChanged(accountId: String, details: Map<String, String>) {
        val account = getAccount(accountId) ?: return
        //Log.d(TAG, "volatileAccountDetailsChanged: " + accountId + " " + details.size());
        account.setVolatileDetails(details)
        observableAccounts.onNext(account)
    }

    fun activeCallsChanged(
        accountId: String,
        conversationId: String,
        activeCalls: List<Map<String, String>>,