Skip to content
Snippets Groups Projects
ConversationFacade.kt 39.2 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 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.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import net.jami.model.*
import net.jami.model.Account.ContactLocationEntry
import net.jami.model.Call.CallStatus
import net.jami.model.Interaction.InteractionStatus
import net.jami.services.AccountService.RegisteredName
import net.jami.smartlist.ConversationItemViewModel
import net.jami.utils.FileUtils.moveFile
import net.jami.utils.Log
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList

class ConversationFacade(
    private val mHistoryService: HistoryService,
    private val mCallService: CallService,
    private val mAccountService: AccountService,
    private val mContactService: ContactService,
    private val mNotificationService: NotificationService,
    private val mHardwareService: HardwareService,
    private val mDeviceRuntimeService: DeviceRuntimeService,
    private val mPreferencesService: PreferencesService
) {
    private val mDisposableBag = CompositeDisposable()
    val currentAccountSubject: Observable<Account> = mAccountService
        .currentAccountSubject
        .switchMapSingle { account: Account -> loadSmartlist(account) }

    /**
     * Two cases: Swarm conversation or non-swarm conversation.
     * If swarm conversation, we need to send the preferences to daemon (in order
     * to update preferences also on other devices).
     * Else, we just save preferences to preferences service.
     */
    fun setConversationPreferences(
        accountId: String,
        conversationUri: Uri,
        preferences: Map<String, String>,
    ) {
        if (conversationUri.isSwarm) {
            mAccountService.setConversationPreferences(
                accountId, conversationUri.rawRingId, preferences
            )
        } else {
            mPreferencesService.setConversationPreferences(accountId, conversationUri, preferences)
        }
    }

    fun startConversation(accountId: String, contactId: Uri): Single<Conversation> =
        getAccountSubject(accountId).map { account: Account -> account.getByUri(contactId)!! }
    fun getAccountSubject(accountId: String): Single<Account> = mAccountService.getAccountSingle(accountId)
        .flatMap { account: Account -> loadSmartlist(account) }
    fun messageNotified(accountId: String, conversationUri: Uri, messageId: String) {
        val conversation  = mAccountService.getAccount(accountId)?.getByUri(conversationUri) ?: return
        conversation.getMessage(messageId)?.let { message ->
            message.isNotified = true
        }
        mHistoryService.setMessageNotified(accountId, conversationUri, messageId)
    }

    fun readMessages(accountId: String, contact: Uri): String? {
Adrien Béraud's avatar
Adrien Béraud committed
        val account = mAccountService.getAccount(accountId) ?: return null
        val conversation = account.getByUri(contact) ?: return null
Adrien Béraud's avatar
Adrien Béraud committed
        return readMessages(account, conversation, true)
Adrien Béraud's avatar
Adrien Béraud committed
    fun readMessages(account: Account, conversation: Conversation, cancelNotification: Boolean): String? {
        val lastMessage = readMessages(conversation) ?: return null
        account.refreshed(conversation)

        // Mark the message as read (daemon will deal with "read receipt" parameter on his own).
        mAccountService.setMessageDisplayed(account.accountId, conversation.uri, lastMessage)

Adrien Béraud's avatar
Adrien Béraud committed
        if (cancelNotification) {
Adrien Béraud's avatar
Adrien Béraud committed
            mNotificationService.cancelTextNotification(account.accountId, conversation.uri)
Adrien Béraud's avatar
Adrien Béraud committed
        }
        return lastMessage
    }

    private fun readMessages(conversation: Conversation): String? {
        val messages = conversation.readMessages()
        var lastRead: String? = null
        for (message in messages) {
            if (conversation.isSwarm) {
                mHistoryService.setMessageNotified(conversation.accountId, conversation.uri, message.messageId!!)
                lastRead = message.messageId
            } else {
                val did = message.daemonId
                if (lastRead == null && did != null && did != 0L)
                    lastRead = java.lang.Long.toString(did, 16)
                mHistoryService.updateInteraction(message, conversation.accountId).subscribe()
    fun sendTextMessage(c: Conversation, to: Uri, txt: String, replyTo: String? = null): Completable {
        if (c.isSwarm) {
            mAccountService.sendConversationMessage(c.accountId, c.uri, txt, replyTo)
            return Completable.complete()
        }
        return mCallService.sendAccountTextMessage(c.accountId, to.rawUriString, txt)
            .map { id: Long ->
                val message = TextMessage(null, c.accountId, java.lang.Long.toHexString(id), c, txt)
                if (c.isVisible) message.read()
                mHistoryService.insertInteraction(c.accountId, c, message).subscribe()
                c.addTextMessage(message)
                mAccountService.getAccount(c.accountId)!!.conversationUpdated(c)
                message
            }.ignoreElement()
    }

    fun sendTextMessage(c: Conversation, conf: Conference, txt: String) {
        mCallService.sendTextMessage(conf.accountId, conf.id, txt)
        val message = TextMessage(null, conf.accountId, conf.id, c, txt)
        message.read()
        mHistoryService.insertInteraction(c.accountId, c, message).subscribe()
        c.addTextMessage(message)
    }

    fun setIsComposing(accountId: String, conversationUri: Uri, isComposing: Boolean) {
        mCallService.setIsComposing(accountId, conversationUri.uri, isComposing)
    }

    fun sendFile(conversation: Conversation, to: Uri, file: File): Completable {
        if (!file.exists() || !file.canRead()) {
            return Completable.error(IllegalArgumentException("file not found or not readable"))
        }
        if (conversation.isSwarm) {
            val destPath = mDeviceRuntimeService.getNewConversationPath(conversation.accountId, conversation.uri.rawRingId, file.name)
            moveFile(file, destPath)
            mAccountService.sendFile(conversation, destPath)
        }
        return Completable.complete()
Adrien Béraud's avatar
Adrien Béraud committed
    fun deleteConversationItem(conversation: Conversation, element: Interaction) {
        if (element.type === Interaction.InteractionType.DATA_TRANSFER) {
            val transfer = element as DataTransfer
            if (transfer.status === InteractionStatus.TRANSFER_ONGOING) {
                mAccountService.cancelDataTransfer(conversation.accountId, conversation.uri.rawRingId, transfer.messageId, transfer.fileId!!)
                val file = mDeviceRuntimeService.getConversationPath(conversation.accountId, conversation.uri.rawRingId, transfer.storagePath)
                if (conversation.isSwarm) {
                    mDisposableBag.add(Completable.fromAction {
                        file.delete()
                        transfer.bytesProgress = 0
                    }
                        .subscribeOn(Schedulers.io())
                        .subscribe({
                            transfer.status = InteractionStatus.FILE_AVAILABLE
                            conversation.updateInteraction(transfer)
                        })
Adrien Béraud's avatar
Adrien Béraud committed
                        { e: Throwable -> Log.e(TAG, "Can't delete file transfer", e) })
                } else {
                    mDisposableBag.add(Completable.mergeArrayDelayError(
                        mHistoryService.deleteInteraction(element.id, element.account!!),
                        Completable.fromAction { file.delete() }
                            .subscribeOn(Schedulers.io()))
                        .subscribe({ conversation.removeInteraction(transfer) })
                        { e: Throwable -> Log.e(TAG, "Can't delete file transfer", e) })
                }
            }
        } else {
            // handling is the same for calls and texts
            if (conversation.isSwarm) {
                mAccountService.deleteConversationMessage(conversation.accountId, conversation.uri, element.messageId!!)
            } else {
                mDisposableBag.add(mHistoryService.deleteInteraction(element.id, element.account!!)
                    .subscribeOn(Schedulers.io())
                    .subscribe({ conversation.removeInteraction(element) })
Adrien Béraud's avatar
Adrien Béraud committed
                    { e: Throwable -> Log.e(TAG, "Can't delete message", e) })
    fun cancelMessage(conversation: Conversation, message: Interaction) {
Adrien Béraud's avatar
Adrien Béraud committed
        val accountId = message.account ?: return
        if (conversation.isSwarm) return
        mDisposableBag.add(
            mCallService.cancelMessage(accountId, message.id.toLong()).subscribeOn(Schedulers.io())
                .subscribe({ conversation.removeInteraction(message) }
                ) { e: Throwable -> Log.e(TAG, "Can't cancel message sending", e) })
    }

    /**
     * Loads the smartlist from cache or database
     *
     * @param account the user account
     * @return an account single
     */
    private fun loadSmartlist(account: Account): Single<Account> {
        synchronized(account) {
            account.historyLoader.let { loader ->
                return loader ?: account.loaded.toSingleDefault(account).flatMap { getSmartlist(it) }.cache().apply {
                    account.historyLoader = this
                }
            }
        }
    }

    /**
     * Loads history for a specific conversation from cache or database
     *
     * @param account         the user account
     * @param conversationUri the conversation
     * @return a conversation single
     */
    fun loadConversationHistory(conversation: Conversation): Single<Conversation> {
        synchronized(conversation) {
            if ((!conversation.isSwarm && conversation.id == null) || (conversation.isSwarm && conversation.mode.blockingFirst() == Conversation.Mode.Request)) {
                return Single.just(conversation)
            }
            var ret = conversation.loaded
            if (ret == null) {
                ret = if (conversation.isSwarm) mAccountService.loadMore(conversation)
                else getConversationHistory(conversation)
                conversation.loaded = ret
            }
            return ret
        }
    }

    fun observeConversation(accountId: String, conversationUri: Uri, hasPresence: Boolean): Observable<ConversationItemViewModel> =
        startConversation(accountId, conversationUri)
            .flatMapObservable { conversation -> observeConversation(conversation, hasPresence) }

    fun observeConversation(conversation: Conversation, hasPresence: Boolean = false): Observable<ConversationItemViewModel> {
Adrien Béraud's avatar
Adrien Béraud committed
        val account = mAccountService.getAccount(conversation.accountId) ?: return Observable.empty()
        return observeConversation(account, conversation, hasPresence)
    }

    fun observeConversation(account: Account, conversation: Conversation, hasPresence: Boolean): Observable<ConversationItemViewModel> =
         Observable.combineLatest(account.getConversationSubject()
             .filter { c: Conversation -> c === conversation }
             .startWithItem(conversation)
             .switchMap { c -> c.profile },
             conversation.contactUpdates.switchMap { c -> mContactService.observeContact(conversation.accountId, c, hasPresence) }
         ) { profile, contacts -> ConversationItemViewModel(conversation, profile, contacts, hasPresence) }
    fun observeConversations(
        account: Account,
        conversations: List<Conversation>,
        hasPresence: Boolean,
    ): Observable<List<ConversationItemViewModel>> =
        Observable.combineLatest(
            conversations.map { observeConversation(account, it, hasPresence) }
        ) { result -> result.map { it as ConversationItemViewModel } }

    fun getSmartList(currentAccount: Observable<Account>, hasPresence: Boolean): Observable<List<Observable<ConversationItemViewModel>>> =
        currentAccount.switchMap { account: Account ->
            account.getConversationsSubject()
                .switchMapSingle { conversations -> if (conversations.isEmpty()) Single.just(emptyList())
                else Observable.fromIterable(conversations)
                    .map { conversation: Conversation -> observeConversation(account, conversation, hasPresence) }
                    .toList(conversations.size) } }
    fun getConversationSmartlist(currentAccount: Observable<Account>): Observable<List<Conversation>> =
        currentAccount
            .switchMapSingle { a -> loadSmartlist(a) }
            .switchMap { account: Account -> account.getConversationsSubject() }

    fun getConversationSmartlist(): Observable<List<Conversation>> = getConversationSmartlist(mAccountService.currentAccountSubject)

    /*fun getContactList(currentAccount: Observable<Account>): Observable<MutableList<ConversationItemViewModel>> = currentAccount
        .switchMap { account: Account ->
            account.getConversationsSubject()
                .map { conversations -> conversations.filter { c -> !c.isSwarm }
                    .map { conversation -> mContactService.getLoadedConversation(conversation) }}
                .switchMapSingle { conversations -> Single.zip(conversations) { it.toMutableList() as MutableList<ConversationItemViewModel> } }
    fun getConversationViewModelList(): Observable<MutableList<ConversationItemViewModel>> =
        getConversationViewModelList(mAccountService.currentAccountSubject)
    private fun getConversationViewModelList(
        currentAccount: Observable<Account>,
    ): Observable<MutableList<ConversationItemViewModel>> =
        currentAccount.switchMap { account: Account ->
            account.getConversationsSubject()
                .map { conversations ->
                    conversations.map { conversation ->
                        mContactService.getLoadedConversation(conversation)
                    }
                }
                .switchMapSingle { conversations ->
                    Single.zip(conversations) {
                        it.toMutableList() as MutableList<ConversationItemViewModel>
                    }
                }
    /*fun getPendingList(currentAccount: Observable<Account>): Observable<List<Observable<ConversationItemViewModel>>> =
        currentAccount.switchMap { account: Account ->
            account.getPendingSubject()
                .switchMapSingle { conversations -> Observable.fromIterable(conversations)
                    .map { conversation: Conversation -> observeConversation(account, conversation, false) }
                    .toList() } }*/
    fun getPendingConversationList(currentAccount: Observable<Account>): Observable<List<Conversation>> =
        currentAccount.switchMap { account: Account ->
            account.getPendingSubject()}
    fun getPendingConversationList(): Observable<List<Conversation>> =
        currentAccountSubject.switchMap { account: Account ->
            account.getPendingSubject()}
    /*fun getSmartList(hasPresence: Boolean): Observable<List<Observable<ConversationItemViewModel>>> =
        getSmartList(mAccountService.currentAccountSubject, hasPresence)
    val pendingList: Observable<List<Observable<ConversationItemViewModel>>>
        get() = getPendingList(mAccountService.currentAccountSubject)
    val contactList: Observable<MutableList<ConversationItemViewModel>>
        get() = getContactList(mAccountService.currentAccountSubject)

    private fun getSearchResults(account: Account, query: String): Single<List<Observable<ConversationItemViewModel>>> {
        val uri = Uri.fromString(query)
        return if (account.isSip) {
            val contact = account.getContactFromCache(uri)
Adrien Béraud's avatar
Adrien Béraud committed
            mContactService.loadContactData(contact, account.accountId)
                .map { profile -> listOf(Observable.just(ConversationItemViewModel(account.accountId, ContactViewModel(contact, profile, null, false), contact.primaryNumber, null)))}
        } else if (uri.isHexId) {
Adrien Béraud's avatar
Adrien Béraud committed
            mContactService.getLoadedContact(account.accountId, account.getContactFromCache(uri))
                .map { contact -> listOf(Observable.just(ConversationItemViewModel(account.accountId, contact, contact.contact.primaryNumber, null))) }
        } else if (account.canSearch() && !query.contains("@")) {
Adrien Béraud's avatar
Adrien Béraud committed
            mAccountService.searchUser(account.accountId, query)
                .map { results -> results.results!!.map { contact -> observeConversation(account.getByUri(contact.conversationUri.blockingFirst())!!) } }
Adrien Béraud's avatar
Adrien Béraud committed
            mAccountService.findRegistrationByName(account.accountId, "", query)
                .map { result: RegisteredName ->
                    if (result.state == 0)
                        listOf(observeConversation(account, account.getByKey(result.address!!).apply {
                            contact?.let { c -> synchronized(c) {
                                if (c.username == null)
                                    c.username = Single.just(result.name)
                            }}
                        }, false))
    data class SearchResult(val query: String, val result: List<Conversation>) {
        companion object {
            val EMPTY_RESULT = SearchResult("", emptyList())
        }
    }

    private fun getConversationSearchResults(account: Account, query: String): Single<SearchResult> {
        val uri = Uri.fromString(query)
Adrien Béraud's avatar
Adrien Béraud committed
        return if (uri.isEmpty) {
            Single.just(SearchResult.EMPTY_RESULT)
Adrien Béraud's avatar
Adrien Béraud committed
        } else if (account.isSip || uri.isHexId) {
            Single.just(SearchResult(query, listOf(account.getByUri(uri)!!)))
        } else if (account.canSearch() && !query.contains("@")) {
            mAccountService.searchUser(account.accountId, query)
                .map { results -> SearchResult(query, results.results!!.map { contact -> account.getByUri(contact.conversationUri.blockingFirst())!! }) }
        } else {
            mAccountService.findRegistrationByName(account.accountId, "", query)
                .map { result: RegisteredName ->
                    if (result.state == 0)
                        SearchResult(query, listOf(account.getByKey(result.address!!).apply {
                            contact?.let { c -> synchronized(c) {
                                if (c.username == null)
                                    c.username = Single.just(result.name)
                            }}
                        SearchResult.EMPTY_RESULT
    data class ConversationList(val conversations: List<Conversation> = emptyList(), val searchResult: SearchResult = SearchResult.EMPTY_RESULT, val latestQuery: String = "") {
        fun isEmpty(): Boolean = conversations.isEmpty() && searchResult.result.isEmpty()

        fun getCombinedSize(): Int {
            if (searchResult.result.isEmpty()) return conversations.size
            if (conversations.isEmpty()) return searchResult.result.size + 1
            return conversations.size + searchResult.result.size + 2
        }

        operator fun get(index: Int): Conversation? {
            return if (searchResult.result.isEmpty()) conversations.getOrNull(index)
            else if (conversations.isEmpty() || index < searchResult.result.size + 1) searchResult.result.getOrNull(index - 1)
            else conversations.getOrNull(index - searchResult.result.size - 2)
        }

        fun getHeader(index: Int): ConversationItemViewModel.Title {
            return if (searchResult.result.isEmpty()) ConversationItemViewModel.Title.None
            else if (index == 0) ConversationItemViewModel.Title.PublicDirectory
            else if (conversations.isNotEmpty() && index == searchResult.result.size + 1) ConversationItemViewModel.Title.Conversations
            else ConversationItemViewModel.Title.None
        }
    /*fun getFilteredConversationList(currentAccount: Observable<Account>, query: Observable<String>): Observable<List<Conversation>> =
        currentAccount.switchMap { account ->
            Observable.combineLatest(account.getConversationsSubject(), query) { conversations, q -> ConversationList(conversations, emptyList(), q) }
                .flatMapSingle { list ->
                    if (list.query.isNotEmpty() && list.conversations.isNotEmpty()) {
                        val lq = list.query.lowercase()
                        Maybe.concatEager(list.conversations.map { c -> mContactService.getLoadedConversation(c)
                            .filter { it.matches(lq) }.map { c }
                        }).toList()
                    } else Single.just(list.conversations)
                }
    fun getSearchResults(
        query: Observable<String>, currentAccount: Observable<Account> = currentAccountSubject,
    ): Observable<ConversationList> =
        currentAccount.switchMap { account ->
            Observable.combineLatest(
                account.getConversationsSubject(),
                query.switchMapSingle { getConversationSearchResults(account, it) },
                query
            ) { conversations, searchResults, q ->
                ConversationList(conversations, searchResults, q)
            }
        }.switchMapSingle { list ->
            if (list.latestQuery.isNotBlank() && list.conversations.isNotEmpty()) {
                val lq = list.latestQuery.lowercase()
                Maybe.concatEager(
                    list.conversations.map { c ->
                        mContactService.getLoadedConversation(c)
                            .filter { it.matches(lq) }.map { c }
                    }
                )
                    .toList()
                    .map { newList ->
                        ConversationList(newList, list.searchResult, list.latestQuery)
                    }
            } else Single.just(ConversationList(emptyList(), list.searchResult, list.latestQuery))
        }

    fun getFullConversationList(currentAccount: Observable<Account>, query: Observable<String>, withBanned: Boolean = false): Observable<ConversationList> =
        currentAccount.switchMap { account ->
            Observable.combineLatest(
                account.getConversationsSubject(withBanned),
                query.switchMapSingle { getConversationSearchResults(account, it) },
            ) { conversations, searchResults, q -> ConversationList(conversations, searchResults, q) }
        }.switchMapSingle { list ->
            if (list.latestQuery.isNotBlank() && list.conversations.isNotEmpty()) {
                val lq = list.latestQuery.lowercase()
                Maybe.concatEager(list.conversations.map { c -> mContactService.getLoadedConversation(c)
                    .filter { it.matches(lq) }.map { c }
                }).toList().map { newList -> ConversationList(newList, list.searchResult, list.latestQuery) }
            } else Single.just(list)
    fun getConversationList(currentAccount: Observable<Account>): Observable<ConversationList> =
        currentAccount.switchMap { account -> account.getConversationsSubject() }
            .map { conversations -> ConversationList(conversations) }

    /**
     * Loads the smartlist from the database and updates the view
     *
     * @param account the user account
     */
    private fun getSmartlist(account: Account): Single<Account> {
        val actions: MutableList<Completable> = ArrayList(account.getConversations().size + 1)
        for (c in account.getConversations()) {
            if (c.isSwarm) actions.add(c.lastElementLoaded)
        if (!account.isJami)
            actions.add(mHistoryService.getSmartlist(account.accountId)
                .flatMapCompletable { conversationHistoryList: List<Interaction> ->
                    Completable.fromAction {
                        val conversations: MutableList<Conversation> = ArrayList()
                        for (e in conversationHistoryList) {
                            val conversation = account.getByUri(e.conversation!!.participant) ?: continue
                            conversation.id = e.conversation!!.id
                            conversation.addElement(e)
                            conversation.setLastMessageNotified(mHistoryService.getLastMessageNotified(account.accountId, conversation.uri))
                            // Update the conversation preferences.
                            conversation.updatePreferences(
                                mPreferencesService.getConversationPreferences(
                                    account.accountId,
                                    conversation.uri
                                )
                            conversations.add(conversation)
                        }
                        account.setHistoryLoaded(conversations)
        return Completable.merge(actions)
            .andThen(Single.just(account))
    }

    /**
     * Loads a conversation's history from the database
     *
     * @param conversation a conversation object with a valid conversation ID
     * @return a conversation single
     */
    private fun getConversationHistory(conversation: Conversation): Single<Conversation> =
        mHistoryService.getConversationHistory(conversation.accountId, conversation.id!!)
            .map { loadedConversation: List<Interaction> ->
                conversation.clearHistory(true)
                conversation.setHistory(loadedConversation)
                conversation
            }
            .cache()

    fun clearHistory(accountId: String, contact: Uri): Completable = mHistoryService
        .clearHistory(contact.uri, accountId, false)
        .doOnSubscribe {
            mAccountService.getAccount(accountId)?.clearHistory(contact, false)
        }
    fun clearAllHistory(): Completable = mAccountService.observableAccountList
        .firstElement()
        .flatMapCompletable { accounts: List<Account> ->
            mHistoryService.clearHistory(accounts)
                .doOnSubscribe {
                    for (account in accounts)
                        account.clearAllHistory()
                }
        }

    private fun parseNewMessage(txt: TextMessage) {
        val accountId = txt.account!!
        val uri = if (txt.messageId != null) Uri(Uri.SWARM_SCHEME, txt.conversationId!!) else Uri(Uri.DEFAULT_CONTACT_SCHEME, txt.author!!)
        if (txt.isRead) {
            if (txt.messageId == null) {
                mHistoryService.updateInteraction(txt, accountId).subscribe()
                mAccountService.setMessageDisplayed(txt.account, uri, txt.daemonIdString!!)
            else mAccountService.setMessageDisplayed(txt.account, uri, txt.messageId!!)

        startConversation(accountId, uri).subscribe(mNotificationService::showTextNotification)
    }

    fun acceptRequest(accountId: String, contactUri: Uri) {
        mPreferencesService.removeRequestPreferences(accountId, contactUri.rawRingId)
        mAccountService.acceptTrustRequest(accountId, contactUri)
    }

    fun discardRequest(accountId: String, contact: Uri) {
        //mHistoryService.clearHistory(contact.uri, accountId, true).subscribe()
        mPreferencesService.removeRequestPreferences(accountId, contact.rawRingId)
        mAccountService.discardTrustRequest(accountId, contact)
    }

    private fun handleDataTransferEvent(transfer: DataTransfer) {
        val account = transfer.account!!
        val conversation = mAccountService.getAccount(account)!!.onDataTransferEvent(transfer)
        val status = transfer.status
        Log.d(TAG, "handleDataTransferEvent $status " + transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(account)))
        if (status === InteractionStatus.TRANSFER_AWAITING_HOST || status === InteractionStatus.FILE_AVAILABLE) {
            if (transfer.canAutoAccept(mPreferencesService.getMaxFileAutoAccept(account))) {
                mAccountService.acceptFileTransfer(conversation, transfer.fileId!!, transfer)
                return
            }
        }
        mNotificationService.handleDataTransferNotification(transfer, conversation, conversation.isVisible)
    }

    private fun onConfStateChange(conference: Conference) {
        Log.d(TAG, "onConfStateChange Thread id: " + Thread.currentThread().id)
    }

    private fun onCallStateChange(call: Call) {
        val newState = call.callStatus
        val incomingCall = newState === CallStatus.RINGING && call.isIncoming
        val account = mAccountService.getAccount(call.account!!) ?: return
        val contact = call.contact
        val conversationId = call.conversationId
        Log.w(TAG, "CallStateChange ${call.daemonIdString}->$newState conversationId:$conversationId contact:$contact ${contact?.conversationUri?.blockingFirst()}")
        val conversation = if (conversationId == null)
            if (contact == null)
                null
            else
                account.getByUri(contact.conversationUri.blockingFirst()) ?: account.getByUri(contact.uri)
            account.getByUri(Uri(Uri.SWARM_SCHEME, conversationId))
        val conference = if (conversation != null) (conversation.getConference(call.daemonIdString) ?: Conference(call).apply {
            if (newState === CallStatus.OVER) return@onCallStateChange
            conversation.addConference(this)
            account.updated(conversation)
        mHardwareService.updateAudioState(conference, call, incomingCall, call.hasMedia(Media.MediaType.MEDIA_TYPE_VIDEO))
        Log.w(TAG, "CALL_STATE_CHANGED : updating call state to $newState")
        if ((newState.isRinging || newState === CallStatus.CURRENT) && call.timestamp == 0L) {
            call.timestamp = System.currentTimeMillis()
        }
        if (incomingCall) {
            mNotificationService.handleCallNotification(conference!!, false)
            mHardwareService.setPreviewSettings()
        } else if (newState === CallStatus.CURRENT
            || newState === CallStatus.RINGING && !call.isIncoming) {
            mNotificationService.handleCallNotification(conference!!, false)
        } else if (newState.isOver) {
            if (conference != null)
                mNotificationService.handleCallNotification(conference, true)
            else {
                mNotificationService.removeCallNotification(call.id)
            }
            mHardwareService.closeAudioState()
            val now = System.currentTimeMillis()
            if (call.timestamp == 0L) {
                call.timestamp = now
            }
            if (call.timestampEnd == 0L) {
                call.timestampEnd = now
            }
Adrien Béraud's avatar
Adrien Béraud committed
            if (conference != null && conference.removeParticipant(call) && conversation != null && !conversation.isSwarm) {
                Log.w(TAG, "Adding call history for conversation " + conversation.uri)
Adrien Béraud's avatar
Adrien Béraud committed
                mHistoryService.insertInteraction(account.accountId, conversation, call).subscribe()
                conversation.addCall(call)
                if (call.isIncoming && call.isMissed) {
                    mNotificationService.showMissedCallNotification(call)
                }
                account.updated(conversation)
            }
Adrien Béraud's avatar
Adrien Béraud committed
            if (conversation != null && conference != null && conference.participants.isEmpty()) {
                conversation.removeConference(conference)
            }
        }
    }

    fun cancelFileTransfer(accountId: String, conversationId: Uri, messageId: String?, fileId: String?) {
Adrien Béraud's avatar
Adrien Béraud committed
        mAccountService.cancelDataTransfer(accountId, if (conversationId.isSwarm) conversationId.rawRingId else "", messageId, fileId!!)
        mNotificationService.removeTransferNotification(accountId, conversationId, fileId)
        if (!conversationId.isSwarm)
            mAccountService.getAccount(accountId)?.getDataTransfer(fileId)?.let { transfer ->
                deleteConversationItem(transfer.conversation as Conversation, transfer)
            }
    }

    fun removeConversation(accountId: String, conversationUri: Uri): Completable {
Adrien Béraud's avatar
Adrien Béraud committed
        val account = mAccountService.getAccount(accountId) ?: return Completable.error(IllegalArgumentException("Unknown account"))
        return if (conversationUri.isSwarm) {
            // For a one to one conversation, contact is strongly related, so remove the contact.
            // This will remove related conversations
Adrien Béraud's avatar
Adrien Béraud committed
            val conversation = account.getSwarm(conversationUri.rawRingId)
            if (conversation != null && conversation.mode.blockingFirst() === Conversation.Mode.OneToOne) {
                val contact = conversation.contact
                mAccountService.removeContact(accountId, contact!!.uri.rawRingId, false)
                Completable.complete()
            } else {
                mAccountService.removeConversation(accountId, conversationUri)
            }
        } else {
            mHistoryService
                .clearHistory(conversationUri.uri, accountId, true)
                .doOnSubscribe {
Adrien Béraud's avatar
Adrien Béraud committed
                    account.clearHistory(conversationUri, true)
                    mAccountService.removeContact(accountId, conversationUri.rawRingId, false)
                }
        }
    }

    fun banConversation(accountId: String, conversationUri: Uri) {
        if (conversationUri.isSwarm) {
            mDisposableBag.add(
                startConversation(accountId, conversationUri).subscribe({ v: Conversation ->
                        val contact = v.contact
                        mAccountService.removeContact(accountId, contact!!.uri.rawRingId, true)
                    } catch (e: Exception) {
                        mAccountService.removeConversation(accountId, conversationUri)
                    }
                }, { e: Throwable -> Log.e(TAG, "Error banning conversation", e) })
            )
        } else mAccountService.removeContact(accountId, conversationUri.rawRingId, true)
    }

    fun createConversation(accountId: String, currentSelection: Collection<Contact>): Single<Conversation> {
        val contactIds = currentSelection.map { contact -> contact.primaryNumber }
        return mAccountService.startConversation(accountId, contactIds)
    }

    fun getLoadedContact(accountId: String, conversation: Conversation?, contactIds: Collection<String>): Observable<List<ContactViewModel>> =
        getAccountSubject(accountId).flatMapObservable { account ->
            val contacts = contactIds.map { id -> conversation?.findContact(Uri.fromId(id)) ?: account.getContactFromCache(Uri.fromId(id)) }
            return@flatMapObservable mContactService.observeContact(accountId, contacts, false)
        }

    companion object {
        private val TAG = ConversationFacade::class.simpleName!!
    }

    init {
        mDisposableBag.add(mCallService.callsUpdates
            .subscribe { call: Call -> onCallStateChange(call) })

        /*mDisposableBag.add(mCallService.getConnectionUpdates()
                    .subscribe(mNotificationService::onConnectionUpdate));*/
        mDisposableBag.add(mCallService.confsUpdates
            .observeOn(Schedulers.io())
            .subscribe { conference: Conference -> onConfStateChange(conference) })
        mDisposableBag.add(currentAccountSubject
            .switchMap { a: Account -> a.getPendingSubject()
                    .doOnNext { mNotificationService.showIncomingTrustRequestNotification(a) } }
            .subscribe())
        mDisposableBag.add(mAccountService.incomingRequests
            .concatMapSingle { r: TrustRequest -> getAccountSubject(r.accountId) }
Adrien Béraud's avatar
Adrien Béraud committed
            .subscribe({ account: Account -> mNotificationService.showIncomingTrustRequestNotification(account) })
                { Log.e(TAG, "Error showing contact request") })
        mDisposableBag.add(mAccountService
            .incomingMessages
Adrien Béraud's avatar
Adrien Béraud committed
            .concatMapSingle { msg: TextMessage -> getAccountSubject(msg.account!!)
                    .map { a: Account -> a.addTextMessage(msg)
Adrien Béraud's avatar
Adrien Béraud committed
            .subscribe({ txt: TextMessage -> parseNewMessage(txt) })
                { e: Throwable -> Log.e(TAG, "Error adding text message", e) })
        mDisposableBag.add(mAccountService.incomingSwarmMessages
                .subscribe({ txt: TextMessage -> parseNewMessage(txt) },
                    { e: Throwable -> Log.e(TAG, "Error adding text message", e) }))
        mDisposableBag.add(mAccountService.locationUpdates
            .concatMapSingle { location: AccountService.Location ->
                getAccountSubject(location.account)
                    .map { a: Account ->
                        val expiration = a.onLocationUpdate(location)
Adrien Béraud's avatar
Adrien Béraud committed
                        mDisposableBag.add(Completable.timer(expiration, TimeUnit.MILLISECONDS)
                                .subscribe { a.maintainLocation() })
                        location
                    } }
            .subscribe())
        mDisposableBag.add(mAccountService.observableAccountList
            .switchMap { accounts -> Observable.merge(accounts.map { a -> a.locationUpdates.map { Pair(a, it) } }) }
            .distinctUntilChanged()
            .subscribe { t: Pair<Account, ContactLocationEntry> ->
                Log.e(TAG, "Location reception started for ${t.second.contact}")
                mNotificationService.showLocationNotification(t.first, t.second.contact, t.second.conversation)
                mDisposableBag.add(t.second.location.doOnComplete {
                    mNotificationService.cancelLocationNotification(t.first, t.second.contact)
                }.subscribe()) })
        mDisposableBag.add(mAccountService
            .messageStateChanges
            .concatMapMaybe { e: Interaction ->
                getAccountSubject(e.account!!)
                    .flatMapMaybe { a: Account -> Maybe.fromCallable {
                            if (e.conversation == null)
                                a.getByUri(Uri(Uri.SWARM_SCHEME, e.conversationId!!))
                            else
                                a.getByUri(e.conversation!!.participant)
                        }
Adrien Béraud's avatar
Adrien Béraud committed
                    .doOnSuccess { conversation -> conversation.updateInteraction(e) }
            }
            .subscribe({}) { e: Throwable -> Log.e(TAG, "Error updating text message", e) })
        mDisposableBag.add(mAccountService.dataTransfers
                .subscribe({ transfer: DataTransfer -> handleDataTransferEvent(transfer) },
                     { e: Throwable -> Log.e(TAG, "Error adding data transfer", e) }))
        mDisposableBag.add(mAccountService.incomingGroupCall
            .subscribe({ c -> mNotificationService.showGroupCallNotification(c) },
                { e: Throwable -> Log.e(TAG, "Error showing group call notification", e) }))
Adrien Béraud's avatar
Adrien Béraud committed
}