diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a97d1610f8..0ddf065ec3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -432,10 +432,7 @@ - - - + android:exported="false"> , runThreadUpdate: Boolean ): MessageId? - fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false) + fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean = false, updateNotification: Boolean = true) fun markConversationAsUnread(threadId: Long) fun getLastSeen(threadId: Long): Long fun ensureMessageHashesAreSender(hashes: Set, sender: String, closedGroupId: String): Boolean diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index f02e548701..a052562d29 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -14,7 +14,6 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.utilities.Data import org.session.libsession.snode.OnionRequestAPI -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.DownloadUtilities @@ -167,7 +166,7 @@ class AttachmentDownloadJob @AssistedInject constructor( Log.d("AttachmentDownloadJob", "downloading open group attachment") val url = attachment.url.toHttpUrlOrNull()!! val fileID = url.pathSegments.last() - OpenGroupApi.download(fileID, room = threadRecipient.address.room, server = threadRecipient.address.serverUrl).await() + OpenGroupApi.download(fileID, room = threadRecipient.address.room, server = threadRecipient.address.serverUrl) } tempFile = createTempFile().also { file -> diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 20a497eaba..86db6428fa 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -80,8 +80,8 @@ class AttachmentUploadJob @AssistedInject constructor( } handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second) } else { - val keyAndResult = upload(attachment, FileServerApi.FILE_SERVER_URL, true) { - FileServerApi.upload(it).map { it.fileId } + val keyAndResult = upload(attachment, FileServerApi.FILE_SERVER_URL, true) { file -> + FileServerApi.upload(file).map { it.fileId }.await() } handleSuccess(dispatcherName, attachment, keyAndResult.first, keyAndResult.second) } @@ -94,7 +94,7 @@ class AttachmentUploadJob @AssistedInject constructor( } } - private suspend fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { + private suspend fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: suspend (ByteArray) -> String): Pair { // Key val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0) // Length @@ -120,7 +120,7 @@ class AttachmentUploadJob @AssistedInject constructor( drb.writeTo(b) val data = b.readByteArray() // Upload the data - val id = upload(data).await() + val id = upload(data) val digest = drb.transmittedDigest // Return return Pair(key, UploadResult(id, "${server}/file/$id", digest)) diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index fc1bbfac42..484a95ca64 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -253,7 +253,7 @@ class BatchMessageReceiveJob @AssistedInject constructor( } is UnsendRequest -> { - val deletedMessage = receivedMessageHandler.handleUnsendRequest(message) + val deletedMessage = receivedMessageHandler.handleUnsendRequest(message, threadId) // If we removed a message then ensure it isn't in the 'messageIds' if (deletedMessage != null) { diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt index e94134bd9a..1a4c5cc9f9 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/InviteContactsJob.kt @@ -16,7 +16,6 @@ import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.MessageAuthentication.buildGroupInviteSignature import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.getGroup import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateInviteMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateMessage @@ -78,7 +77,6 @@ class InviteContactsJob(val groupSessionId: String, val memberSessionIds: Array< } MessageSender.sendNonDurably(update, Destination.Contact(memberSessionId), false) - .await() } } } diff --git a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 44395deca3..8cdc660584 100644 --- a/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/app/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -20,7 +20,6 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.Data -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsession.utilities.ConfigUpdateNotification import org.session.libsignal.utilities.AccountId @@ -97,7 +96,7 @@ class MessageSendJob @AssistedInject constructor( .waitForGroupEncryptionKeys(AccountId(destination.publicKey)) } - MessageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync).await() + MessageSender.sendNonDurably(this@MessageSendJob.message, destination, isSync) } this.handleSuccess(dispatcherName) diff --git a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 7692f118ce..3d650db223 100644 --- a/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/app/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -5,17 +5,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.PropertyNamingStrategy import com.fasterxml.jackson.databind.annotation.JsonNaming -import com.fasterxml.jackson.databind.type.TypeFactory -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.decodeFromStream import network.loki.messenger.libsession_util.ED25519 import network.loki.messenger.libsession_util.Hash import network.loki.messenger.libsession_util.util.BlindKeyAPI -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.MediaType.Companion.toMediaType @@ -24,7 +21,6 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionResponse import org.session.libsession.snode.SnodeAPI -import org.session.libsession.snode.utilities.asyncPromise import org.session.libsession.snode.utilities.await import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Base64.encodeBytes @@ -40,9 +36,6 @@ import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import java.security.SecureRandom import java.util.concurrent.TimeUnit -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set object OpenGroupApi { val defaultRooms = MutableSharedFlow>(replay = 1) @@ -215,12 +208,16 @@ object OpenGroupApi { val index: Long = 0 ) + @Serializable data class AddReactionResponse( + @SerialName("seqno") val seqNo: Long, val added: Boolean ) + @Serializable data class DeleteReactionResponse( + @SerialName("seqno") val seqNo: Long, val removed: Boolean ) @@ -283,24 +280,23 @@ object OpenGroupApi { return RequestBody.create("application/json".toMediaType(), parametersAsJSON) } - private fun getResponseBody( + private suspend fun getResponseBody( request: Request, signRequest: Boolean = true, serverPubKeyHex: String? = null - ): Promise { - return send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex).map { response -> - response.body ?: throw Error.ParsingFailed - } + ): ByteArraySlice { + val response = send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex) + + return response.body ?: throw Error.ParsingFailed } - private fun getResponseBodyJson( + private suspend fun getResponseBodyJson( request: Request, signRequest: Boolean = true, serverPubKeyHex: String? = null - ): Promise, Exception> { - return send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex).map { - JsonUtil.fromJson(it.body, Map::class.java) - } + ): Map<*, *> { + val response = send(request, signRequest = signRequest, serverPubKeyHex = serverPubKeyHex) + return JsonUtil.fromJson(response.body, Map::class.java) } suspend fun getOrFetchServerCapabilities(server: String): List { @@ -313,14 +309,15 @@ object OpenGroupApi { val fetched = getCapabilities(server, serverPubKeyHex = defaultServerPublicKey.takeIf { server == defaultServer } - ).await() + ) storage.setServerCapabilities(server, fetched.capabilities) return fetched.capabilities } - private fun send(request: Request, signRequest: Boolean, serverPubKeyHex: String? = null): Promise { - request.server.toHttpUrlOrNull() ?: return Promise.ofFail(Error.InvalidURL) + private suspend fun send(request: Request, signRequest: Boolean, serverPubKeyHex: String? = null): OnionResponse { + request.server.toHttpUrlOrNull() ?: throw(Error.InvalidURL) + val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}") if (request.verb == GET && request.queryParameters.isNotEmpty()) { urlBuilder.append("?") @@ -329,113 +326,109 @@ object OpenGroupApi { } } - suspend fun execute(): OnionResponse { - val serverPublicKey = serverPubKeyHex - ?: MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) - ?: throw Error.NoPublicKey - val urlRequest = urlBuilder.toString() - - val headers = if (signRequest) { - val serverCapabilities = getOrFetchServerCapabilities(request.server) - - val ed25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() - ?: throw Error.NoEd25519KeyPair - - val headers = request.headers.toMutableMap() - val nonce = ByteArray(16).also { SecureRandom().nextBytes(it) } - val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset) - val bodyHash = if (request.parameters != null) { - val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray() - Hash.hash64(parameterBytes) - } else if (request.body != null) { - Hash.hash64(request.body) - } else { - byteArrayOf() - } + val serverPublicKey = serverPubKeyHex + ?: MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) + ?: throw Error.NoPublicKey + val urlRequest = urlBuilder.toString() + + val headers = if (signRequest) { + val serverCapabilities = getOrFetchServerCapabilities(request.server) + + val ed25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserED25519KeyPair() + ?: throw Error.NoEd25519KeyPair + + val headers = request.headers.toMutableMap() + val nonce = ByteArray(16).also { SecureRandom().nextBytes(it) } + val timestamp = TimeUnit.MILLISECONDS.toSeconds(SnodeAPI.nowWithOffset) + val bodyHash = if (request.parameters != null) { + val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray() + Hash.hash64(parameterBytes) + } else if (request.body != null) { + Hash.hash64(request.body) + } else { + byteArrayOf() + } - val messageBytes = Hex.fromStringCondensed(serverPublicKey) - .plus(nonce) - .plus("$timestamp".toByteArray(Charsets.US_ASCII)) - .plus(request.verb.rawValue.toByteArray()) - .plus("/${request.endpoint.value}".toByteArray()) - .plus(bodyHash) - - val signature: ByteArray - val pubKey: String - - if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) { - pubKey = AccountId( - IdPrefix.BLINDED, - BlindKeyAPI.blind15KeyPair( - ed25519SecretKey = ed25519KeyPair.secretKey.data, - serverPubKey = Hex.fromStringCondensed(serverPublicKey) - ).pubKey.data - ).hexString - - try { - signature = BlindKeyAPI.blind15Sign( - ed25519SecretKey = ed25519KeyPair.secretKey.data, - serverPubKey = serverPublicKey, - message = messageBytes - ) - } catch (e: Exception) { - throw Error.SigningFailed - } - } else { - pubKey = AccountId( - IdPrefix.UN_BLINDED, - ed25519KeyPair.pubKey.data - ).hexString - - signature = ED25519.sign( - ed25519PrivateKey = ed25519KeyPair.secretKey.data, + val messageBytes = Hex.fromStringCondensed(serverPublicKey) + .plus(nonce) + .plus("$timestamp".toByteArray(Charsets.US_ASCII)) + .plus(request.verb.rawValue.toByteArray()) + .plus("/${request.endpoint.value}".toByteArray()) + .plus(bodyHash) + + val signature: ByteArray + val pubKey: String + + if (serverCapabilities.isEmpty() || serverCapabilities.contains(Capability.BLIND.name.lowercase())) { + pubKey = AccountId( + IdPrefix.BLINDED, + BlindKeyAPI.blind15KeyPair( + ed25519SecretKey = ed25519KeyPair.secretKey.data, + serverPubKey = Hex.fromStringCondensed(serverPublicKey) + ).pubKey.data + ).hexString + + try { + signature = BlindKeyAPI.blind15Sign( + ed25519SecretKey = ed25519KeyPair.secretKey.data, + serverPubKey = serverPublicKey, message = messageBytes ) + } catch (e: Exception) { + throw Error.SigningFailed } - headers["X-SOGS-Nonce"] = encodeBytes(nonce) - headers["X-SOGS-Timestamp"] = "$timestamp" - headers["X-SOGS-Pubkey"] = pubKey - headers["X-SOGS-Signature"] = encodeBytes(signature) - headers - } else { - request.headers - } - - val requestBuilder = okhttp3.Request.Builder() - .url(urlRequest) - .headers(headers.toHeaders()) - when (request.verb) { - GET -> requestBuilder.get() - PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!) - POST -> requestBuilder.post(createBody(request.body, request.parameters)!!) - DELETE -> requestBuilder.delete(createBody(request.body, request.parameters)) - } - if (!request.room.isNullOrEmpty()) { - requestBuilder.header("Room", request.room) - } - return if (request.useOnionRouting) { - OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, serverPublicKey).fail { e -> - when (e) { - // No need for the stack trace for HTTP errors - is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}") - else -> Log.e("SOGS", "Failed onion request", e) - } - }.await() } else { - throw IllegalStateException("It's currently not allowed to send non onion routed requests.") + pubKey = AccountId( + IdPrefix.UN_BLINDED, + ed25519KeyPair.pubKey.data + ).hexString + + signature = ED25519.sign( + ed25519PrivateKey = ed25519KeyPair.secretKey.data, + message = messageBytes + ) } + headers["X-SOGS-Nonce"] = encodeBytes(nonce) + headers["X-SOGS-Timestamp"] = "$timestamp" + headers["X-SOGS-Pubkey"] = pubKey + headers["X-SOGS-Signature"] = encodeBytes(signature) + headers + } else { + request.headers } - return GlobalScope.asyncPromise(block=::execute) + val requestBuilder = okhttp3.Request.Builder() + .url(urlRequest) + .headers(headers.toHeaders()) + when (request.verb) { + GET -> requestBuilder.get() + PUT -> requestBuilder.put(createBody(request.body, request.parameters)!!) + POST -> requestBuilder.post(createBody(request.body, request.parameters)!!) + DELETE -> requestBuilder.delete(createBody(request.body, request.parameters)) + } + if (!request.room.isNullOrEmpty()) { + requestBuilder.header("Room", request.room) + } + return if (request.useOnionRouting) { + OnionRequestAPI.sendOnionRequest(requestBuilder.build(), request.server, serverPublicKey).fail { e -> + when (e) { + // No need for the stack trace for HTTP errors + is HTTP.HTTPRequestFailedException -> Log.e("SOGS", "Failed onion request: ${e.message}") + else -> Log.e("SOGS", "Failed onion request", e) + } + }.await() + } else { + throw IllegalStateException("It's currently not allowed to send non onion routed requests.") + } } - fun downloadOpenGroupProfilePicture( + suspend fun downloadOpenGroupProfilePicture( server: String, roomID: String, imageId: String, signRequest: Boolean = true, serverPubKeyHex: String? = null, - ): Promise { + ): ByteArraySlice { val request = Request( verb = GET, room = roomID, @@ -446,7 +439,7 @@ object OpenGroupApi { } // region Upload/Download - fun upload(file: ByteArray, room: String, server: String): Promise { + suspend fun upload(file: ByteArray, room: String, server: String): String { val request = Request( verb = POST, room = room, @@ -458,12 +451,11 @@ object OpenGroupApi { "Content-Type" to "application/octet-stream" ) ) - return getResponseBodyJson(request, signRequest = true).map { json -> - json["id"]?.toString() ?: throw Error.ParsingFailed - } + val json = getResponseBodyJson(request, signRequest = true) + return json["id"]?.toString() ?: throw Error.ParsingFailed } - fun download(fileId: String, room: String, server: String): Promise { + suspend fun download(fileId: String, room: String, server: String): ByteArraySlice { val request = Request( verb = GET, room = room, @@ -475,15 +467,15 @@ object OpenGroupApi { // endregion // region Sending - fun sendMessage( + suspend fun sendMessage( message: OpenGroupMessage, room: String, server: String, whisperTo: List? = null, whisperMods: Boolean? = null, fileIds: List? = null - ): Promise { - val signedMessage = message.sign(server) ?: return Promise.ofFail(Error.SigningFailed) + ): OpenGroupMessage { + val signedMessage = message.sign(server) ?:throw Error.SigningFailed val parameters = signedMessage.toJSON().toMutableMap() // add file IDs if there are any (from attachments) @@ -498,18 +490,18 @@ object OpenGroupApi { endpoint = Endpoint.RoomMessage(room), parameters = parameters ) - return getResponseBodyJson(request, signRequest = true).map { json -> - @Suppress("UNCHECKED_CAST") val rawMessage = json as? Map - ?: throw Error.ParsingFailed - val result = OpenGroupMessage.fromJSON(rawMessage) ?: throw Error.ParsingFailed - val storage = MessagingModuleConfiguration.shared.storage - storage.addReceivedMessageTimestamp(result.sentTimestamp) - result - } + val json = getResponseBodyJson(request, signRequest = true) + @Suppress("UNCHECKED_CAST") val rawMessage = json as? Map + ?: throw Error.ParsingFailed + val result = OpenGroupMessage.fromJSON(rawMessage) ?: throw Error.ParsingFailed + val storage = MessagingModuleConfiguration.shared.storage + storage.addReceivedMessageTimestamp(result.sentTimestamp) + return result } // endregion - fun addReaction(room: String, server: String, messageId: Long, emoji: String): Promise { + @OptIn(ExperimentalSerializationApi::class) + suspend fun addReaction(room: String, server: String, messageId: Long, emoji: String): AddReactionResponse { val request = Request( verb = PUT, room = room, @@ -517,89 +509,51 @@ object OpenGroupApi { endpoint = Endpoint.Reaction(room, messageId, emoji), parameters = emptyMap() ) - val pendingReaction = PendingReaction(server, room, messageId, emoji, true) - return getResponseBody(request, signRequest = true).map { response -> - JsonUtil.fromJson(response, AddReactionResponse::class.java).also { - val index = pendingReactions.indexOf(pendingReaction) - pendingReactions[index].seqNo = it.seqNo - } - } + + val response = getResponseBody(request, signRequest = true) + val reaction: AddReactionResponse = response.inputStream().use( MessagingModuleConfiguration.shared.json::decodeFromStream) + + return reaction } - fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): Promise { + @OptIn(ExperimentalSerializationApi::class) + suspend fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): DeleteReactionResponse { val request = Request( verb = DELETE, room = room, server = server, endpoint = Endpoint.Reaction(room, messageId, emoji) ) - val pendingReaction = PendingReaction(server, room, messageId, emoji, true) - return getResponseBody(request, signRequest = true).map { response -> - JsonUtil.fromJson(response, DeleteReactionResponse::class.java).also { - val index = pendingReactions.indexOf(pendingReaction) - pendingReactions[index].seqNo = it.seqNo - } - } + + val response = getResponseBody(request, signRequest = true) + val reaction: DeleteReactionResponse = MessagingModuleConfiguration.shared.json.decodeFromStream(response.inputStream()) + + return reaction } - fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): Promise { + suspend fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): DeleteAllReactionsResponse { val request = Request( verb = DELETE, room = room, server = server, endpoint = Endpoint.ReactionDelete(room, messageId, emoji) ) - return getResponseBody(request, signRequest = true).map { response -> - JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java) - } + val response = getResponseBody(request, signRequest = true) + return JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java) } // endregion // region Message Deletion - @JvmStatic - fun deleteMessage(serverID: Long, room: String, server: String): Promise { + suspend fun deleteMessage(serverID: Long, room: String, server: String) { val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID)) - return send(request, signRequest = true).map { - Log.d("Loki", "Message deletion successful.") - } + send(request, signRequest = true) + Log.d("Loki", "Message deletion successful.") } - fun getDeletedMessages( - room: String, - server: String - ): Promise, Exception> { - val storage = MessagingModuleConfiguration.shared.storage - val queryParameters = mutableMapOf() - storage.getLastDeletionServerID(room, server)?.let { last -> - queryParameters["from_server_id"] = last.toString() - } - val request = Request( - verb = GET, - room = room, - server = server, - endpoint = Endpoint.RoomDeleteMessages(room, storage.getUserPublicKey() ?: ""), - queryParameters = queryParameters - ) - return getResponseBody(request, signRequest = true).map { response -> - val json = JsonUtil.fromJson(response, Map::class.java) - val type = TypeFactory.defaultInstance() - .constructCollectionType(List::class.java, MessageDeletion::class.java) - val idsAsString = JsonUtil.toJson(json["ids"]) - val serverIDs = JsonUtil.fromJson>(idsAsString, type) - ?: throw Error.ParsingFailed - val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0 - val serverID = serverIDs.maxByOrNull { it.id } ?: MessageDeletion.empty - if (serverID.id > lastMessageServerId) { - storage.setLastDeletionServerID(room, server, serverID.id) - } - serverIDs - } - } // endregion // region Moderation - @JvmStatic - fun ban(publicKey: String, room: String, server: String): Promise { + suspend fun ban(publicKey: String, room: String, server: String) { val parameters = mapOf("rooms" to listOf(room)) val request = Request( verb = POST, @@ -608,12 +562,12 @@ object OpenGroupApi { endpoint = Endpoint.UserBan(publicKey), parameters = parameters ) - return send(request, signRequest = true).map { - Log.d("Loki", "Banned user: $publicKey from: $server.$room.") - } + + send(request, signRequest = true) + Log.d("Loki", "Banned user: $publicKey from: $server.$room.") } - fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise { + suspend fun banAndDeleteAll(publicKey: String, room: String, server: String) { val requests = mutableListOf>( // Ban request @@ -633,25 +587,23 @@ object OpenGroupApi { responseType = object: TypeReference(){} ) ) - return sequentialBatch(server, requests).map { - Log.d("Loki", "Banned user: $publicKey from: $server.$room.") - } + sequentialBatch(server, requests) + Log.d("Loki", "Banned user: $publicKey from: $server.$room.") } - fun unban(publicKey: String, room: String, server: String): Promise { + suspend fun unban(publicKey: String, room: String, server: String) { val request = Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.UserUnban(publicKey)) - return send(request, signRequest = true).map { - Log.d("Loki", "Unbanned user: $publicKey from: $server.$room") - } + send(request, signRequest = true) + Log.d("Loki", "Unbanned user: $publicKey from: $server.$room") } // endregion // region General - fun parallelBatch( + suspend fun parallelBatch( server: String, requests: MutableList> - ): Promise>, Exception> { + ): List> { val request = Request( verb = POST, room = null, @@ -662,10 +614,10 @@ object OpenGroupApi { return getBatchResponseJson(request, requests) } - private fun sequentialBatch( + private suspend fun sequentialBatch( server: String, requests: MutableList> - ): Promise>, Exception> { + ): List> { val request = Request( verb = POST, room = null, @@ -676,97 +628,93 @@ object OpenGroupApi { return getBatchResponseJson(request, requests) } - private fun getBatchResponseJson( + private suspend fun getBatchResponseJson( request: Request, requests: MutableList>, signRequest: Boolean = true - ): Promise>, Exception> { - return getResponseBody(request, signRequest = signRequest).map { batch -> - val results = JsonUtil.fromJson(batch, List::class.java) ?: throw Error.ParsingFailed - results.mapIndexed { idx, result -> - val response = result as? Map<*, *> ?: throw Error.ParsingFailed - val code = response["code"] as Int - BatchResponse( - endpoint = requests[idx].endpoint, - code = code, - headers = response["headers"] as Map, - body = if (code in 200..299) { - requests[idx].responseType?.let { respType -> - JsonUtil.toJson(response["body"]).takeIf { it != "[]" }?.let { - JsonUtil.fromJson(it, respType) - } ?: response["body"] - } - - } else null - ) - } + ): List> { + val batch = getResponseBody(request, signRequest = signRequest) + val results = JsonUtil.fromJson(batch, List::class.java) ?: throw Error.ParsingFailed + return results.mapIndexed { idx, result -> + val response = result as? Map<*, *> ?: throw Error.ParsingFailed + val code = response["code"] as Int + BatchResponse( + endpoint = requests[idx].endpoint, + code = code, + headers = response["headers"] as Map, + body = if (code in 200..299) { + requests[idx].responseType?.let { respType -> + JsonUtil.toJson(response["body"]).takeIf { it != "[]" }?.let { + JsonUtil.fromJson(it, respType) + } ?: response["body"] + } + + } else null + ) } } - fun getDefaultServerCapabilities(): Promise, Exception> { - return GlobalScope.asyncPromise { getOrFetchServerCapabilities(defaultServer) } + suspend fun getDefaultServerCapabilities(): List { + return getOrFetchServerCapabilities(defaultServer) } - fun getDefaultRoomsIfNeeded(): Promise, Exception> { - return GlobalScope.asyncPromise { - val groups = getAllRooms().await() + suspend fun getDefaultRoomsIfNeeded(): List { + val groups = getAllRooms() - val earlyGroups = groups.map { group -> - DefaultGroup(group.token, group.name, null) - } - // See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results - defaultRooms.replayCache.firstOrNull()?.let { replayed -> - if (replayed.none { it.image?.isNotEmpty() == true }) { - defaultRooms.tryEmit(earlyGroups) - } - } - val images = groups.associate { group -> - group.token to group.imageId?.let { downloadOpenGroupProfilePicture( - server = defaultServer, - roomID = group.token, - imageId = it, - signRequest = false, - serverPubKeyHex = defaultServerPublicKey, - ) } + val earlyGroups = groups.map { group -> + DefaultGroup(group.token, group.name, null) + } + // See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results + defaultRooms.replayCache.firstOrNull()?.let { replayed -> + if (replayed.none { it.image?.isNotEmpty() == true }) { + defaultRooms.tryEmit(earlyGroups) } - groups.map { group -> - val image = try { - images[group.token]!!.await() - } catch (e: Exception) { - // No image or image failed to download - null - } - DefaultGroup(group.token, group.name, image) - }.also(defaultRooms::tryEmit) } + val images = groups.associate { group -> + group.token to group.imageId?.let { downloadOpenGroupProfilePicture( + server = defaultServer, + roomID = group.token, + imageId = it, + signRequest = false, + serverPubKeyHex = defaultServerPublicKey, + ) } + } + + return groups.map { group -> + val image = try { + images[group.token]!! + } catch (e: Exception) { + // No image or image failed to download + null + } + DefaultGroup(group.token, group.name, image) + }.also(defaultRooms::tryEmit) } - private fun getAllRooms(): Promise, Exception> { + private suspend fun getAllRooms(): List { val request = Request( verb = GET, room = null, server = defaultServer, endpoint = Endpoint.Rooms ) - return getResponseBody( + val response = getResponseBody( request = request, signRequest = false, serverPubKeyHex = defaultServerPublicKey - ).map { response -> - MessagingModuleConfiguration.shared.json - .decodeFromStream>(response.inputStream()) - .toList() - } + ) + + return MessagingModuleConfiguration.shared.json + .decodeFromStream>(response.inputStream()).toList() } - fun getCapabilities(server: String, serverPubKeyHex: String? = null): Promise { + suspend fun getCapabilities(server: String, serverPubKeyHex: String? = null): Capabilities { val request = Request(verb = GET, room = null, server = server, endpoint = Endpoint.Capabilities) - return getResponseBody(request, signRequest = false, serverPubKeyHex).map { response -> - JsonUtil.fromJson(response, Capabilities::class.java) - } + val response = getResponseBody(request, signRequest = false, serverPubKeyHex) + return JsonUtil.fromJson(response, Capabilities::class.java) } - fun sendDirectMessage(message: String, blindedAccountId: String, server: String): Promise { + suspend fun sendDirectMessage(message: String, blindedAccountId: String, server: String): DirectMessage { val request = Request( verb = POST, room = null, @@ -774,21 +722,19 @@ object OpenGroupApi { endpoint = Endpoint.InboxFor(blindedAccountId), parameters = mapOf("message" to message) ) - return getResponseBody(request).map { response -> - JsonUtil.fromJson(response, DirectMessage::class.java) - } + val response = getResponseBody(request) + return JsonUtil.fromJson(response, DirectMessage::class.java) } - fun deleteAllInboxMessages(server: String): Promise, java.lang.Exception> { + suspend fun deleteAllInboxMessages(server: String): Map<*, *> { val request = Request( verb = DELETE, room = null, server = server, endpoint = Endpoint.Inbox ) - return getResponseBody(request).map { response -> - JsonUtil.fromJson(response, Map::class.java) - } + val response = getResponseBody(request) + return JsonUtil.fromJson(response, Map::class.java) } // endregion diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 8be647e12c..0b5158ce5f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -11,8 +11,6 @@ import network.loki.messenger.libsession_util.ConfigBase.Companion.PRIORITY_VISI import network.loki.messenger.libsession_util.Namespace import network.loki.messenger.libsession_util.util.BlindKeyAPI import network.loki.messenger.libsession_util.util.ExpiryMode -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.messages.Destination @@ -32,7 +30,6 @@ import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI.nowWithOffset import org.session.libsession.snode.SnodeMessage -import org.session.libsession.snode.utilities.asyncPromise import org.session.libsession.utilities.Address import org.session.libsession.utilities.SSKEnvironment import org.session.libsignal.crypto.PushTransportDetails @@ -46,6 +43,7 @@ import org.session.libsignal.utilities.defaultRequiresAuth import org.session.libsignal.utilities.hasNamespaces import org.session.libsignal.utilities.hexEncodedPublicKey import java.util.concurrent.TimeUnit +import kotlin.coroutines.cancellation.CancellationException import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote @@ -72,13 +70,11 @@ object MessageSender { } // Convenience - fun sendNonDurably(message: Message, destination: Destination, isSyncMessage: Boolean): Promise { + suspend fun sendNonDurably(message: Message, destination: Destination, isSyncMessage: Boolean) { return if (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup || destination is Destination.OpenGroupInbox) { sendToOpenGroupDestination(destination, message) } else { - GlobalScope.asyncPromise { - sendToSnodeDestination(destination, message, isSyncMessage) - } + sendToSnodeDestination(destination, message, isSyncMessage) } } @@ -197,12 +193,7 @@ object MessageSender { // One-on-One Chats & Closed Groups private suspend fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false) = supervisorScope { - val storage = MessagingModuleConfiguration.shared.storage val configFactory = MessagingModuleConfiguration.shared.configFactory - val userPublicKey = storage.getUserPublicKey() - - // recipient will be set later, so initialize it as a function here - val isSelfSend = { message.recipient == userPublicKey } // Set the failure handler (need it here already for precondition failure handling) fun handleFailure(error: Exception) { @@ -293,8 +284,7 @@ object MessageSender { } // Open Groups - private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { - val deferred = deferred() + private suspend fun sendToOpenGroupDestination(destination: Destination, message: Message) { val storage = MessagingModuleConfiguration.shared.storage val configFactory = MessagingModuleConfiguration.shared.configFactory if (message.sentTimestamp == null) { @@ -343,11 +333,7 @@ object MessageSender { AccountId(IdPrefix.UN_BLINDED, userEdKeyPair.pubKey.data).hexString } message.sender = messageSender - // Set the failure handler (need it here already for precondition failure handling) - fun handleFailure(error: Exception) { - handleFailedMessageSend(message, error) - deferred.reject(error) - } + try { // Attach the user's profile if needed if (message is VisibleMessage) { @@ -372,13 +358,19 @@ object MessageSender { sentTimestamp = message.sentTimestamp!!, base64EncodedData = Base64.encodeBytes(plaintext), ) - OpenGroupApi.sendMessage(openGroupMessage, destination.roomToken, destination.server, destination.whisperTo, destination.whisperMods, destination.fileIds).success { - message.openGroupServerMessageID = it.serverID - handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp) - deferred.resolve(Unit) - }.fail { - handleFailure(it) - } + + val response = OpenGroupApi.sendMessage( + openGroupMessage, + destination.roomToken, + destination.server, + destination.whisperTo, + destination.whisperMods, + destination.fileIds + ) + + message.openGroupServerMessageID = response.serverID + handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = response.sentTimestamp) + return } is Destination.OpenGroupInbox -> { message.recipient = destination.blindedPublicKey @@ -394,20 +386,22 @@ object MessageSender { destination.serverPublicKey ) val base64EncodedData = Base64.encodeBytes(ciphertext) - OpenGroupApi.sendDirectMessage(base64EncodedData, destination.blindedPublicKey, destination.server).success { - message.openGroupServerMessageID = it.id - handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = TimeUnit.SECONDS.toMillis(it.postedAt)) - deferred.resolve(Unit) - }.fail { - handleFailure(it) - } + val response = OpenGroupApi.sendDirectMessage( + base64EncodedData, + destination.blindedPublicKey, + destination.server + ) + + message.openGroupServerMessageID = response.id + handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = TimeUnit.SECONDS.toMillis(response.postedAt)) + return } else -> throw IllegalStateException("Invalid destination.") } } catch (exception: Exception) { - handleFailure(exception) + if (exception !is CancellationException) handleFailedMessageSend(message, exception) + throw exception } - return deferred.promise } // Result Handling @@ -552,10 +546,10 @@ object MessageSender { resultChannel.receive().getOrThrow() } - fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean): Promise { + suspend fun sendNonDurably(message: Message, address: Address, isSyncMessage: Boolean) { val threadID = MessagingModuleConfiguration.shared.storage.getThreadId(address) message.threadID = threadID val destination = Destination.from(address) - return sendNonDurably(message, destination, isSyncMessage) + sendNonDurably(message, destination, isSyncMessage) } } \ No newline at end of file diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index 7f7ac86059..cd04dec911 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -128,7 +128,7 @@ class ReceivedMessageHandler @Inject constructor( } } is DataExtractionNotification -> handleDataExtractionNotification(message) - is UnsendRequest -> handleUnsendRequest(message) + is UnsendRequest -> handleUnsendRequest(message, threadId) is MessageRequestResponse -> messageRequestResponseHandler.get().handleExplicitRequestResponseMessage(message) is VisibleMessage -> handleVisibleMessage( message = message, @@ -220,7 +220,7 @@ class ReceivedMessageHandler @Inject constructor( } - fun handleUnsendRequest(message: UnsendRequest): MessageId? { + fun handleUnsendRequest(message: UnsendRequest, threadId: Long): MessageId? { val userPublicKey = storage.getUserPublicKey() val userAuth = storage.userAuth ?: return null val isLegacyGroupAdmin: Boolean = message.groupPublicKey?.let { key -> @@ -243,7 +243,6 @@ class ReceivedMessageHandler @Inject constructor( val timestamp = message.timestamp ?: return null val author = message.author ?: return null - val threadId = message.threadID ?: return null val messageToDelete = storage.getMessageBy(threadId, timestamp, author) ?: return null val messageIdToDelete = messageToDelete.messageId val messageType = messageToDelete.individualRecipient?.getType() @@ -375,7 +374,7 @@ class ReceivedMessageHandler @Inject constructor( emoji = reaction.emoji!!, messageTimestamp = reaction.timestamp!!, threadId = context.threadId, - author = reaction.publicKey!!, + author = senderAddress.address, notifyUnread = threadIsGroup ) } @@ -769,17 +768,13 @@ fun constructReactionRecords( out: MutableMap> ) { if (reactions.isNullOrEmpty()) return - val communityAddress = context.threadAddress as? Address.Community ?: return + if (context.threadAddress !is Address.Community) return val messageId = context.messageDataProvider.getMessageID(openGroupMessageServerID, context.threadId) ?: return val outList = out.getOrPut(messageId) { arrayListOf() } for ((emoji, reaction) in reactions) { - val pendingUserReaction = OpenGroupApi.pendingReactions - .filter { it.server == communityAddress.serverUrl && it.room == communityAddress.room && it.messageId == openGroupMessageServerID && it.add } - .sortedByDescending { it.seqNo } - .any { it.emoji == emoji } - val shouldAddUserReaction = pendingUserReaction || reaction.you || reaction.reactors.contains(context.userPublicKey) + val shouldAddUserReaction = reaction.you || reaction.reactors.contains(context.userPublicKey) val reactorIds = reaction.reactors.filter { it != context.userBlindedKey && it != context.userPublicKey } val count = if (reaction.you) reaction.count - 1 else reaction.count // Add the first reaction (with the count) diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt index 0573561c9f..3b09af6919 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/MessageNotifier.kt @@ -5,11 +5,8 @@ import android.content.Context interface MessageNotifier { fun setHomeScreenVisible(isVisible: Boolean) fun setVisibleThread(threadId: Long) - fun setLastDesktopActivityTimestamp(timestamp: Long) - fun cancelDelayedNotifications() fun updateNotification(context: Context) fun updateNotification(context: Context, threadId: Long) fun updateNotification(context: Context, threadId: Long, signal: Boolean) fun updateNotification(context: Context, signal: Boolean, reminderCount: Int) - fun clearReminder(context: Context) } diff --git a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index 9411049169..043f1f390f 100644 --- a/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/app/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -37,7 +37,6 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi.parallelBatch import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.ReceivedMessageHandler -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.toAddress import org.session.libsession.utilities.ConfigFactoryProtocol @@ -283,7 +282,7 @@ class OpenGroupPoller @AssistedInject constructor( } ) } - return parallelBatch(server, requests).await() + return parallelBatch(server, requests) } @@ -295,7 +294,6 @@ class OpenGroupPoller @AssistedInject constructor( val sortedMessages = messages.sortedBy { it.seqno } sortedMessages.maxOfOrNull { it.seqno }?.let { seqNo -> storage.setLastMessageServerID(roomToken, server, seqNo) - OpenGroupApi.pendingReactions.removeAll { !(it.seqNo == null || it.seqNo!! > seqNo) } } val (deletions, additions) = sortedMessages.partition { it.deleted } handleNewMessages(server, roomToken, additions.map { diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt index c86568de4d..864fe1757f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/RemoteFileDownloadWorker.kt @@ -244,7 +244,7 @@ class RemoteFileDownloadWorker @AssistedInject constructor( fileId = file.fileId, room = file.roomId, server = file.communityServerBaseUrl - ).await() + ) data to FileMetadata() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 87f4a1fbef..68139a8268 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -62,6 +62,7 @@ import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow @@ -169,6 +170,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.groups.GroupMembersActivity import org.thoughtcrime.securesms.groups.OpenGroupManager @@ -261,6 +263,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, @Inject lateinit var openGroupManager: OpenGroupManager @Inject lateinit var attachmentDatabase: AttachmentDatabase @Inject lateinit var clock: SnodeClock + @Inject @ManagerScope + lateinit var scope: CoroutineScope override val applyDefaultWindowInsets: Boolean get() = false @@ -1377,10 +1381,18 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val maybeTargetVisiblePosition = layoutManager?.findLastVisibleItemPosition() val targetVisiblePosition = maybeTargetVisiblePosition ?: RecyclerView.NO_POSITION if (!firstLoad.get() && targetVisiblePosition != RecyclerView.NO_POSITION) { - adapter.getTimestampForItemAt(targetVisiblePosition)?.let { visibleItemTimestamp -> - bufferedLastSeenChannel.trySend(visibleItemTimestamp).apply { - if (isFailure) Log.e(TAG, "trySend failed", exceptionOrNull()) - } + val timestampToSend: Long? = if (binding.conversationRecyclerView.isFullyScrolled) { + // We are at the bottom, so mark "now" as the last seen time + clock.currentTimeMills() + } else { + // We are not at the bottom, so just mark the timestamp of the last visible message + adapter.getTimestampForItemAt(targetVisiblePosition) + } + + timestampToSend?.let { + bufferedLastSeenChannel.trySend(it).apply { + if (isFailure) Log.e(TAG, "trySend failed", exceptionOrNull()) + } } } @@ -1752,11 +1764,16 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val messageServerId = lokiMessageDb.getServerID(originalMessage.messageId) ?: return Log.w(TAG, "Failed to find message server ID when adding emoji reaction") - OpenGroupApi.addReaction( - room = recipient.address.room, - server = recipient.address.serverUrl, - messageId = messageServerId, - emoji = emoji) + scope.launch { + runCatching { + OpenGroupApi.addReaction( + room = recipient.address.room, + server = recipient.address.serverUrl, + messageId = messageServerId, + emoji = emoji + ) + } + } } else { MessageSender.send(reactionMessage, recipient.address) } @@ -1785,16 +1802,31 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, ) val originalAuthor = if (originalMessage.isOutgoing) { - fromSerialized(viewModel.blindedPublicKey ?: textSecurePreferences.getLocalNumber()!!) + fromSerialized(viewModel.blindedPublicKey ?: author) } else originalMessage.individualRecipient.address - message.reaction = Reaction.from(originalMessage.timestamp, originalAuthor.toString(), emoji, false) + message.reaction = Reaction.from( + timestamp = originalMessage.timestamp, + author = originalAuthor.address, + emoji = emoji, + react = false + ) + if (recipient.address is Address.Community) { val messageServerId = lokiMessageDb.getServerID(originalMessage.messageId) ?: return Log.w(TAG, "Failed to find message server ID when removing emoji reaction") - OpenGroupApi.deleteReaction(recipient.address.room, recipient.address.serverUrl, messageServerId, emoji) + scope.launch { + runCatching { + OpenGroupApi.deleteReaction( + recipient.address.room, + recipient.address.serverUrl, + messageServerId, + emoji + ) + } + } } else { MessageSender.send(message, recipient.address) } @@ -2507,15 +2539,35 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } override fun resyncMessage(messages: Set) { - messages.iterator().forEach { messageRecord -> - ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey, isResync = true) + val accountId = textSecurePreferences.getLocalNumber() + scope.launch { + messages.iterator().forEach { messageRecord -> + runCatching { + ResendMessageUtilities.resend( + accountId, + messageRecord, + viewModel.blindedPublicKey, + isResync = true + ) + } + } } + endActionMode() } override fun resendMessage(messages: Set) { - messages.iterator().forEach { messageRecord -> - ResendMessageUtilities.resend(this, messageRecord, viewModel.blindedPublicKey) + val accountId = textSecurePreferences.getLocalNumber() + scope.launch { + messages.iterator().forEach { messageRecord -> + runCatching { + ResendMessageUtilities.resend( + accountId, + messageRecord, + viewModel.blindedPublicKey + ) + } + } } endActionMode() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index 2d72b4cf5a..89c980c0c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -1257,12 +1257,14 @@ class ConversationViewModel @AssistedInject constructor( reactionDb.deleteEmojiReactions(emoji, messageId) (address as? Address.Community)?.let { openGroup -> lokiMessageDb.getServerID(messageId)?.let { serverId -> - OpenGroupApi.deleteAllReactions( - openGroup.room, - openGroup.serverUrl, - serverId, - emoji - ) + runCatching { + OpenGroupApi.deleteAllReactions( + openGroup.room, + openGroup.serverUrl, + serverId, + emoji + ) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 74f1f73ff2..42a6ba52c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -277,10 +277,10 @@ class VisibleMessageView : FrameLayout { if (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) { emojiReactionsBinding.value.root.let { root -> root.setReactions(message.messageId, message.reactions, message.isOutgoing, delegate) - root.isVisible = true - (root.layoutParams as ConstraintLayout.LayoutParams).apply { + root.layoutParams = (root.layoutParams as ConstraintLayout.LayoutParams).apply { horizontalBias = if (message.isOutgoing) 1f else 0f } + root.isVisible = true } } else if (emojiReactionsBinding.isInitialized()) { emojiReactionsBinding.value.root.isVisible = false diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt index cf00cd0f9d..2f47041204 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ResendMessageUtilities.kt @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.conversation.v2.utilities -import android.content.Context import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.messages.Destination import org.session.libsession.messaging.messages.visible.LinkPreview @@ -9,7 +8,6 @@ import org.session.libsession.messaging.messages.visible.Quote import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.utilities.UpdateMessageData -import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.isGroupOrCommunity import org.session.libsession.utilities.toGroupString import org.thoughtcrime.securesms.database.model.MessageRecord @@ -17,7 +15,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord object ResendMessageUtilities { - fun resend(context: Context, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { + suspend fun resend(accountId: String?, messageRecord: MessageRecord, userBlindedKey: String?, isResync: Boolean = false) { val recipient = messageRecord.recipient.address val message = VisibleMessage() message.id = messageRecord.messageId @@ -45,7 +43,7 @@ object ResendMessageUtilities { messageRecord.linkPreviews.firstOrNull()?.let { message.linkPreview = LinkPreview.from(it) } messageRecord.quote?.quoteModel?.let { message.quote = Quote.from(it)?.apply { - if (userBlindedKey != null && publicKey == TextSecurePreferences.getLocalNumber(context)) { + if (userBlindedKey != null && publicKey == accountId) { publicKey = userBlindedKey } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index d3b175264d..d356cfb19b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -191,16 +191,17 @@ open class Storage @Inject constructor( return messages.map { it.second } // return the message hashes } - override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean) { + override fun markConversationAsRead(threadId: Long, lastSeenTime: Long, force: Boolean, updateNotification: Boolean) { val threadDb = threadDatabase getRecipientForThread(threadId)?.let { recipient -> - val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() // don't set the last read in the volatile if we didn't set it in the DB - if (!threadDb.markAllAsRead(threadId, lastSeenTime, force) && !force) return + if (!threadDb.markAllAsRead(threadId, lastSeenTime, force, updateNotification) && !force) return // don't process configs for inbox recipients if (recipient.isCommunityInboxRecipient) return + val currentLastRead = threadDb.getLastSeenAndHasSent(threadId).first() + configFactory.withMutableUserConfigs { configs -> val config = configs.convoInfoVolatile val convo = getConvo(recipient, config) ?: return@withMutableUserConfigs diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 26e01384f9..ec0cdbf912 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -761,13 +761,15 @@ public boolean isRead(long threadId) { /** * @param threadId * @param lastSeenTime + * @param force + * @param updateNotifications - if true, update the notification state. Set to false if you already came from a notification interaction * @return true if we have set the last seen for the thread, false if there were no messages in the thread */ - public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force) { + public boolean markAllAsRead(long threadId, long lastSeenTime, boolean force, boolean updateNotifications) { if (mmsSmsDatabase.get().getConversationCount(threadId) <= 0 && !force) return false; List messages = setRead(threadId, lastSeenTime); MarkReadReceiver.process(context, messages); - messageNotifier.get().updateNotification(context, threadId); + if(updateNotifications) messageNotifier.get().updateNotification(context, threadId); return setLastSeen(threadId, lastSeenTime); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt index b0fa5a0a4b..08ec99a3d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt @@ -29,24 +29,10 @@ interface DatabaseComponent { fun mediaDatabase(): MediaDatabase fun threadDatabase(): ThreadDatabase fun mmsSmsDatabase(): MmsSmsDatabase - fun draftDatabase(): DraftDatabase - fun pushDatabase(): PushDatabase fun groupDatabase(): GroupDatabase fun recipientDatabase(): RecipientDatabase - fun groupReceiptDatabase(): GroupReceiptDatabase - fun searchDatabase(): SearchDatabase fun lokiAPIDatabase(): LokiAPIDatabase fun lokiMessageDatabase(): LokiMessageDatabase - fun lokiUserDatabase(): LokiUserDatabase - fun lokiBackupFilesDatabase(): LokiBackupFilesDatabase - fun sessionJobDatabase(): SessionJobDatabase - fun sessionContactDatabase(): SessionContactDatabase fun reactionDatabase(): ReactionDatabase - fun emojiSearchDatabase(): EmojiSearchDatabase fun storage(): Storage - fun attachmentProvider(): MessageDataProvider - fun blindedIdMappingDatabase(): BlindedIdMappingDatabase - fun groupMemberDatabase(): GroupMemberDatabase - fun expirationConfigurationDatabase(): ExpirationConfigurationDatabase - fun configDatabase(): ConfigDatabase } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt index 30f49b1978..c5e3acf8dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2Impl.kt @@ -4,6 +4,7 @@ import android.content.Context import com.google.protobuf.ByteString import com.squareup.phrase.Phrase import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async @@ -46,8 +47,8 @@ import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.Address import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.getGroup -import org.session.libsession.utilities.recipients.RecipientData import org.session.libsession.utilities.recipients.Recipient +import org.session.libsession.utilities.recipients.RecipientData import org.session.libsession.utilities.waitUntilGroupConfigsPushed import org.session.libsignal.protos.SignalServiceProtos.DataMessage import org.session.libsignal.protos.SignalServiceProtos.DataMessage.GroupUpdateDeleteMemberContentMessage @@ -527,7 +528,7 @@ class GroupManagerV2Impl @Inject constructor( message = promoteMessage, address = Address.fromSerialized(member.hexString), isSyncMessage = false, - ).await() + ) } } @@ -657,11 +658,13 @@ class GroupManagerV2Impl @Inject constructor( .setInviteResponse(inviteResponse) val responseMessage = GroupUpdated(responseData.build(), profile = storage.getUserProfile()) // this will fail the first couple of times :) - MessageSender.sendNonDurably( - responseMessage, - Destination.ClosedGroup(group.groupAccountId), - isSyncMessage = false - ) + runCatching { + MessageSender.sendNonDurably( + responseMessage, + Destination.ClosedGroup(group.groupAccountId), + isSyncMessage = false + ) + } } else { // If we are invited as admin, we can just update the group info ourselves configFactory.withMutableGroupConfigs(AccountId(group.groupAccountId)) { configs -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index 154ef35869..93159ed2ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -6,7 +6,6 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerManager -import org.session.libsession.snode.utilities.await import org.session.libsession.utilities.ConfigFactoryProtocol import org.session.libsignal.utilities.Log import org.thoughtcrime.securesms.database.LokiAPIDatabase @@ -30,7 +29,7 @@ class OpenGroupManager @Inject constructor( // for the user to see if the server they are adding is reachable. // The addition of the community to the config later will always succeed and the poller // will be started regardless of the server's status. - val caps = OpenGroupApi.getCapabilities(server, serverPubKeyHex = publicKey).await() + val caps = OpenGroupApi.getCapabilities(server, serverPubKeyHex = publicKey) lokiAPIDatabase.setServerCapabilities(server, caps.capabilities) // We should be good, now go ahead and add the community to the config diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt index e6951effbf..ab7961b1b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/startconversation/community/JoinCommunityViewModel.kt @@ -45,8 +45,11 @@ class JoinCommunityViewModel @Inject constructor( private val qrDebounceTime = 3000L init { - OpenGroupApi.getDefaultServerCapabilities().map { - OpenGroupApi.getDefaultRoomsIfNeeded() + viewModelScope.launch(Dispatchers.Default) { + runCatching { + OpenGroupApi.getDefaultServerCapabilities() + OpenGroupApi.getDefaultRoomsIfNeeded() + } } viewModelScope.launch(Dispatchers.Default) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt index f5f11730f8..11af694ca2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.kt @@ -16,19 +16,11 @@ */ package org.thoughtcrime.securesms.notifications -import android.Manifest import android.annotation.SuppressLint -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Bitmap -import android.os.AsyncTask import android.text.TextUtils -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import coil3.ImageLoader @@ -42,7 +34,6 @@ import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.getLocalNumber import org.session.libsession.utilities.TextSecurePreferences.Companion.getNotificationPrivacy -import org.session.libsession.utilities.TextSecurePreferences.Companion.getRepeatAlertsCount import org.session.libsession.utilities.TextSecurePreferences.Companion.isNotificationsEnabled import org.session.libsession.utilities.TextSecurePreferences.Companion.removeHasHiddenMessageRequests import org.session.libsession.utilities.recipients.RecipientData @@ -50,8 +41,6 @@ import org.session.libsignal.utilities.AccountId import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.Util -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.crypto.KeyPairUtilities.getUserED25519KeyPair import org.thoughtcrime.securesms.database.MmsSmsColumns.NOTIFIED @@ -68,10 +57,7 @@ import org.thoughtcrime.securesms.util.AvatarUtils import org.thoughtcrime.securesms.util.SessionMetaProtocol.canUserReplyToNotification import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder.Companion.WEBRTC_NOTIFICATION -import java.util.concurrent.Executor -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Provider import kotlin.concurrent.Volatile @@ -100,14 +86,6 @@ class DefaultMessageNotifier @Inject constructor( homeScreenVisible = isVisible } - override fun setLastDesktopActivityTimestamp(timestamp: Long) { - lastDesktopActivityTimestamp = timestamp - } - - override fun cancelDelayedNotifications() { - executor.cancel() - } - private fun cancelActiveNotifications(context: Context): Boolean { val notifications = ServiceUtil.getNotificationManager(context) val hasNotifications = notifications.activeNotifications.size > 0 @@ -178,12 +156,7 @@ class DefaultMessageNotifier @Inject constructor( } override fun updateNotification(context: Context, threadId: Long) { - if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) { - Log.i(TAG, "Scheduling delayed notification...") - executor.execute(DelayedNotification(context, threadId)) - } else { - updateNotification(context, threadId, true) - } + updateNotification(context, threadId, true) } override fun updateNotification(context: Context, threadId: Long, signal: Boolean) { @@ -239,7 +212,6 @@ class DefaultMessageNotifier @Inject constructor( // early exit if (nothingToDo || localNumber == null) { cancelActiveNotifications(context) - clearReminder(context) return } @@ -285,13 +257,8 @@ class DefaultMessageNotifier @Inject constructor( if (normalItems.notificationCount == 0 && requestItems.notificationCount == 0) { // Request-aware cleanup (keeps active request notifs alive) cancelOrphanedNotifications(context, normalItems) - clearReminder(context) return } - - if (playNotificationAudio) { - scheduleReminder(context, reminderCount) - } } catch (e: Exception) { Log.e(TAG, "Error creating notification", e) } @@ -303,6 +270,7 @@ class DefaultMessageNotifier @Inject constructor( } // Note: The `signal` parameter means "play an audio signal for the notification". + @SuppressLint("MissingPermission") private fun sendSingleThreadNotification( context: Context, notificationState: NotificationState, @@ -456,29 +424,16 @@ class DefaultMessageNotifier @Inject constructor( val notification = builder.build() - // TODO - ACL to fix this properly & will do on 2024-08-26, but just skipping for now so review can start - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - // TODO: Consider calling - // ActivityCompat#requestPermissions - // here to request the missing permissions, and then overriding - // public void onRequestPermissionsResult(int requestCode, String[] permissions, - // int[] grantResults) - // to handle the case where the user grants the permission. See the documentation - // for ActivityCompat#requestPermissions for more details. - return - } + if (hasNotificationPermissions(context)) { + if (isRequest) { + NotificationManagerCompat.from(context) + .notify(REQUEST_TAG, notificationId, notification) + } else { + NotificationManagerCompat.from(context).notify(notificationId, notification) + } - if (isRequest) { - NotificationManagerCompat.from(context).notify(REQUEST_TAG, notificationId, notification) - } else { - NotificationManagerCompat.from(context).notify(notificationId, notification) + Log.i(TAG, "Posted notification. $notificationId") } - - Log.i(TAG, "Posted notification. $notification") } private fun getNotificationSignature(notification: NotificationItem): String { @@ -486,6 +441,7 @@ class DefaultMessageNotifier @Inject constructor( } // Note: The `signal` parameter means "play an audio signal for the notification". + @SuppressLint("MissingPermission") private fun sendMultipleThreadNotification( context: Context, notificationState: NotificationState, @@ -571,25 +527,16 @@ class DefaultMessageNotifier @Inject constructor( builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag) - // TODO - ACL to fix this properly & will do on 2024-08-26, but just skipping for now so review can start - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - // TODO: Consider calling - // ActivityCompat#requestPermissions - // here to request the missing permissions, and then overriding - // public void onRequestPermissionsResult(int requestCode, String[] permissions, - // int[] grantResults) - // to handle the case where the user grants the permission. See the documentation - // for ActivityCompat#requestPermissions for more details. - return + + if (hasNotificationPermissions(context)) { + val notification = builder.build() + NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, notification) + Log.i(TAG, "Posted notification. $notification") } + } - val notification = builder.build() - NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, notification) - Log.i(TAG, "Posted notification. $notification") + private fun hasNotificationPermissions(context: Context): Boolean { + return NotificationManagerCompat.from(context).areNotificationsEnabled() } private fun constructNotificationState(context: Context, cursor: Cursor): NotificationState { @@ -854,128 +801,6 @@ class DefaultMessageNotifier @Inject constructor( return null } - private fun scheduleReminder(context: Context, count: Int) { - if (count >= getRepeatAlertsCount(context)) { - return - } - - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - val alarmIntent = Intent(ReminderReceiver.REMINDER_ACTION) - alarmIntent.putExtra("reminder_count", count) - - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - alarmIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val timeout = TimeUnit.MINUTES.toMillis(2) - - alarmManager[AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + timeout] = pendingIntent - } - - override fun clearReminder(context: Context) { - val alarmIntent = Intent(ReminderReceiver.REMINDER_ACTION) - val pendingIntent = PendingIntent.getBroadcast( - context, - 0, - alarmIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - alarmManager.cancel(pendingIntent) - } - - class ReminderReceiver : BroadcastReceiver() { - @SuppressLint("StaticFieldLeak") - override fun onReceive(context: Context, intent: Intent) { - object : AsyncTask() { - - override fun doInBackground(vararg params: Void?): Void? { - val reminderCount = intent.getIntExtra("reminder_count", 0) - ApplicationContext.getInstance(context).messageNotifier.updateNotification( - context, - true, - reminderCount + 1 - ) - return null - } - - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) - } - - companion object { - const val REMINDER_ACTION: String = - "network.loki.securesms.MessageNotifier.REMINDER_ACTION" - } - } - - private class DelayedNotification(private val context: Context, private val threadId: Long) : - Runnable { - private val canceled = AtomicBoolean(false) - - private val delayUntil: Long - - init { - this.delayUntil = System.currentTimeMillis() + DELAY - } - - override fun run() { - val delayMillis = delayUntil - System.currentTimeMillis() - Log.i(TAG, "Waiting to notify: $delayMillis") - - if (delayMillis > 0) { - Util.sleep(delayMillis) - } - - if (!canceled.get()) { - Log.i(TAG, "Not canceled, notifying...") - ApplicationContext.getInstance(context).messageNotifier.updateNotification( - context, - threadId, - true - ) - ApplicationContext.getInstance(context).messageNotifier.cancelDelayedNotifications() - } else { - Log.w(TAG, "Canceled, not notifying...") - } - } - - fun cancel() { - canceled.set(true) - } - - companion object { - private val DELAY = TimeUnit.SECONDS.toMillis(5) - } - } - - private class CancelableExecutor { - private val executor: Executor = Executors.newSingleThreadExecutor() - private val tasks: MutableSet = HashSet() - - fun execute(runnable: DelayedNotification) { - synchronized(tasks) { tasks.add(runnable) } - - val wrapper = Runnable { - runnable.run() - synchronized(tasks) { - tasks.remove(runnable) - } - } - - executor.execute(wrapper) - } - - fun cancel() { - synchronized(tasks) { - for (task in tasks) { - task.cancel() - } - } - } - } - companion object { private val TAG: String = DefaultMessageNotifier::class.java.simpleName @@ -996,7 +821,6 @@ class DefaultMessageNotifier @Inject constructor( private const val REQUEST_TAG = "message_request" private val MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(5) - private val DESKTOP_ACTIVITY_PERIOD = TimeUnit.MINUTES.toMillis(1) @Volatile private var visibleThread: Long = -1 @@ -1004,11 +828,7 @@ class DefaultMessageNotifier @Inject constructor( @Volatile private var homeScreenVisible = false - @Volatile - private var lastDesktopActivityTimestamp: Long = -1 - @Volatile private var lastAudibleNotification: Long = -1 - private val executor = CancelableExecutor() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java deleted file mode 100644 index e95bbbb6f8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DeleteNotificationReceiver.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.thoughtcrime.securesms.notifications; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; - -import org.thoughtcrime.securesms.ApplicationContext; -import org.thoughtcrime.securesms.dependencies.DatabaseComponent; - -public class DeleteNotificationReceiver extends BroadcastReceiver { - - public static String DELETE_NOTIFICATION_ACTION = "network.loki.securesms.DELETE_NOTIFICATION"; - - public static String EXTRA_IDS = "message_ids"; - public static String EXTRA_MMS = "is_mms"; - - @Override - public void onReceive(final Context context, Intent intent) { - if (DELETE_NOTIFICATION_ACTION.equals(intent.getAction())) { - ApplicationContext.getInstance(context).getMessageNotifier().clearReminder(context); - - final long[] ids = intent.getLongArrayExtra(EXTRA_IDS); - final boolean[] mms = intent.getBooleanArrayExtra(EXTRA_MMS); - - if (ids == null || mms == null || ids.length != mms.length) return; - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - for (int i=0;i { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt index ddbbb89eb0..d272784213 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsViewModel.kt @@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.attachments.AvatarUploadManager import org.thoughtcrime.securesms.conversation.v2.utilities.TextUtilities.textSizeInBytes import org.thoughtcrime.securesms.database.RecipientRepository import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.preferences.prosettings.ProSettingsViewModel.Commands.ShowOpenUrlDialog import org.thoughtcrime.securesms.pro.ProStatusManager import org.thoughtcrime.securesms.pro.SubscriptionState import org.thoughtcrime.securesms.pro.getDefaultSubscriptionStateData @@ -440,7 +439,7 @@ class SettingsViewModel @Inject constructor( coroutineScope { allCommunityServers.map { server -> launch { - runCatching { OpenGroupApi.deleteAllInboxMessages(server).await() } + runCatching { OpenGroupApi.deleteAllInboxMessages(server) } .onFailure { Log.e(TAG, "Error deleting messages for $server", it) } } }.joinAll() diff --git a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt index 6d94fc83f7..56b8d0bef7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt @@ -386,7 +386,7 @@ class DefaultConversationRepository @Inject constructor( ) { messages.forEach { message -> lokiMessageDb.getServerID(message.messageId)?.let { messageServerID -> - OpenGroupApi.deleteMessage(messageServerID, community.room, community.serverUrl).await() + OpenGroupApi.deleteMessage(messageServerID, community.room, community.serverUrl) } } } @@ -484,7 +484,7 @@ class DefaultConversationRepository @Inject constructor( publicKey = userId.hexString, room = community.room, server = community.serverUrl, - ).await() + ) } override suspend fun banAndDeleteAll(community: Address.Community, userId: AccountId) = runCatching { @@ -493,7 +493,7 @@ class DefaultConversationRepository @Inject constructor( publicKey = userId.hexString, room = community.room, server = community.serverUrl - ).await() + ) } override suspend fun deleteMessageRequest(thread: ThreadRecord): Result { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 6c1395d1be..aa2ef6f7cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -10,6 +10,7 @@ import androidx.compose.material3.Shapes import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -19,8 +20,8 @@ import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.ApplicationContext // Globally accessible composition local objects -val LocalColors = compositionLocalOf { ClassicDark() } -val LocalType = compositionLocalOf { sessionTypography } +val LocalColors = staticCompositionLocalOf { ClassicDark() } +val LocalType = staticCompositionLocalOf { sessionTypography } var cachedColorsProvider: ThemeColorsProvider? = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt index 0fb4d6755b..5a864b0f80 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallManager.kt @@ -5,11 +5,13 @@ import android.content.pm.PackageManager import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.boolean @@ -29,6 +31,7 @@ import org.session.libsession.utilities.Debouncer import org.session.libsession.utilities.Util import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CANDIDATES import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.dependencies.ManagerScope import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdate import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat @@ -68,6 +71,7 @@ import org.thoughtcrime.securesms.webrtc.data.State as CallState @Singleton class CallManager @Inject constructor( @param:ApplicationContext private val context: Context, + @param:ManagerScope private val scope: CoroutineScope, audioManager: AudioManagerCompat, private val storage: StorageProtocol, ): PeerConnection.Observer, @@ -333,7 +337,17 @@ class CallManager @Inject constructor( currentCallId ) .applyExpiryMode(expectedRecipient) - .also { MessageSender.sendNonDurably(it, currentRecipient, isSyncMessage = currentRecipient.isLocalNumber) } + .also { + scope.launch { + runCatching { + MessageSender.sendNonDurably( + it, + currentRecipient, + isSyncMessage = currentRecipient.isLocalNumber + ) + } + } + } } } @@ -457,13 +471,13 @@ class CallManager @Inject constructor( } } - fun onNewOffer(offer: String, callId: UUID, recipient: Address): Promise { - if (callId != this.callId) return Promise.ofFail(NullPointerException("No callId")) - if (recipient != this.recipient) return Promise.ofFail(NullPointerException("No recipient")) - val connection = peerConnection ?: return Promise.ofFail(NullPointerException("No peer connection wrapper")) + suspend fun onNewOffer(offer: String, callId: UUID, recipient: Address) { + if (callId != this.callId) throw NullPointerException("No callId") + if (recipient != this.recipient) throw NullPointerException("No recipient") + val connection = peerConnection ?: throw NullPointerException("No peer connection wrapper") val reconnected = stateProcessor.processEvent(Event.ReceiveOffer) && stateProcessor.processEvent(Event.SendAnswer) - return if (reconnected) { + if (reconnected) { Log.i("Loki", "Handling new offer, restarting ice session") connection.setNewRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, offer)) // re-established an ice @@ -475,9 +489,16 @@ class CallManager @Inject constructor( pendingIncomingIceUpdates.clear() val answerMessage = CallMessage.answer(answer.description, callId).applyExpiryMode(recipient) Log.i("Loki", "Posting new answer") - MessageSender.sendNonDurably(answerMessage, recipient, isSyncMessage = recipient.isLocalNumber) + + runCatching { + MessageSender.sendNonDurably( + answerMessage, + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } } else { - Promise.ofFail(Exception("Couldn't reconnect from current state")) + throw Exception("Couldn't reconnect from current state") } } @@ -493,15 +514,15 @@ class CallManager @Inject constructor( } } - fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise { + suspend fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false) { lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING) - val callId = callId ?: return Promise.ofFail(NullPointerException("callId is null")) - val recipient = recipient ?: return Promise.ofFail(NullPointerException("recipient is null")) - val offer = pendingOffer ?: return Promise.ofFail(NullPointerException("pendingOffer is null")) - val factory = peerConnectionFactory ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) - val local = floatingRenderer ?: return Promise.ofFail(NullPointerException("localRenderer is null")) - val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) + val callId = callId ?: throw NullPointerException("callId is null") + val recipient = recipient ?: throw NullPointerException("recipient is null") + val offer = pendingOffer ?: throw NullPointerException("pendingOffer is null") + val factory = peerConnectionFactory ?: throw NullPointerException("peerConnectionFactory is null") + val local = floatingRenderer ?: throw NullPointerException("localRenderer is null") + val base = eglBase ?: throw NullPointerException("eglBase is null") val connection = PeerConnectionWrapper( context, @@ -521,12 +542,24 @@ class CallManager @Inject constructor( val answer = connection.createAnswer(MediaConstraints()) connection.setLocalDescription(answer) val answerMessage = CallMessage.answer(answer.description, callId).applyExpiryMode(recipient) - val userAddress = storage.getUserPublicKey() ?: return Promise.ofFail(NullPointerException("No user public key")) - MessageSender.sendNonDurably(answerMessage, Address.fromSerialized(userAddress), isSyncMessage = true) - val sendAnswerMessage = MessageSender.sendNonDurably(CallMessage.answer( - answer.description, - callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + val userAddress = storage.getUserPublicKey() ?: throw NullPointerException("No user public key") + + runCatching { + MessageSender.sendNonDurably( + answerMessage, + Address.fromSerialized(userAddress), + isSyncMessage = true + ) + } + + runCatching { + MessageSender.sendNonDurably( + CallMessage.answer( + answer.description, + callId + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber + ) + } insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING, false) @@ -534,28 +567,27 @@ class CallManager @Inject constructor( val candidate = pendingIncomingIceUpdates.pop() ?: break connection.addIceCandidate(candidate) } - return sendAnswerMessage.success { - pendingOffer = null - pendingOfferTime = -1 - } + + pendingOffer = null + pendingOfferTime = -1 } - fun onOutgoingCall(context: Context, isAlwaysTurn: Boolean = false): Promise { + suspend fun onOutgoingCall(context: Context, isAlwaysTurn: Boolean = false) { lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL) - val callId = callId ?: return Promise.ofFail(NullPointerException("callId is null")) + val callId = callId ?: throw NullPointerException("callId is null") val recipient = recipient - ?: return Promise.ofFail(NullPointerException("recipient is null")) + ?: throw NullPointerException("recipient is null") val factory = peerConnectionFactory - ?: return Promise.ofFail(NullPointerException("peerConnectionFactory is null")) + ?: throw NullPointerException("peerConnectionFactory is null") val local = floatingRenderer - ?: return Promise.ofFail(NullPointerException("localRenderer is null")) - val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null")) + ?: throw NullPointerException("localRenderer is null") + val base = eglBase ?: throw NullPointerException("eglBase is null") val sentOffer = stateProcessor.processEvent(Event.SendOffer) if (!sentOffer) { - return Promise.ofFail(Exception("Couldn't transition to sent offer state")) + throw Exception("Couldn't transition to sent offer state") } else { val connection = PeerConnectionWrapper( context, @@ -576,20 +608,26 @@ class CallManager @Inject constructor( connection.setLocalDescription(offer) Log.d("Loki", "Sending pre-offer") - return MessageSender.sendNonDurably(CallMessage.preOffer( - callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber).bind { + try { + MessageSender.sendNonDurably( + CallMessage.preOffer( + callId + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber + ) + Log.d("Loki", "Sent pre-offer") Log.d("Loki", "Sending offer") postViewModelState(CallViewModel.State.CALL_OFFER_OUTGOING) + MessageSender.sendNonDurably(CallMessage.offer( offer.description, callId - ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber).success { - Log.d("Loki", "Sent offer") - }.fail { - Log.e("Loki", "Failed to send offer", it) - } + ).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + + Log.d("Loki", "Sent offer") + } catch (e: Exception) { + Log.e("Loki", "Failed to send offer", e) + throw e } } } @@ -599,10 +637,26 @@ class CallManager @Inject constructor( val recipient = recipient ?: return val userAddress = storage.getUserPublicKey() ?: return stateProcessor.processEvent(Event.DeclineCall) { - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(recipient), Address.fromSerialized(userAddress), isSyncMessage = true) - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) - insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) + scope.launch { + runCatching { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + Address.fromSerialized(userAddress), + isSyncMessage = true + ) + } + } + scope.launch { + runCatching { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } + } + insertCallMessage(recipient.toString(), CallMessageType.CALL_INCOMING) } } @@ -624,7 +678,15 @@ class CallManager @Inject constructor( channel.send(buffer) } - MessageSender.sendNonDurably(CallMessage.endCall(callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + scope.launch { + runCatching { + MessageSender.sendNonDurably( + CallMessage.endCall(callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } + } } } @@ -855,7 +917,15 @@ class CallManager @Inject constructor( mandatory.add(MediaConstraints.KeyValuePair("IceRestart", "true")) }) connection.setLocalDescription(offer) - MessageSender.sendNonDurably(CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), recipient, isSyncMessage = recipient.isLocalNumber) + scope.launch { + runCatching { + MessageSender.sendNonDurably( + CallMessage.offer(offer.description, callId).applyExpiryMode(recipient), + recipient, + isSyncMessage = recipient.isLocalNumber + ) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt index 9ed60cbdd5..70c0a68b21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/WebRtcCallBridge.kt @@ -50,6 +50,7 @@ import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import kotlin.coroutines.cancellation.CancellationException import org.thoughtcrime.securesms.webrtc.data.State as CallState //todo PHONE We want to eventually remove this bridging class and move the logic here to a better place, probably in the callManager @@ -66,7 +67,7 @@ class WebRtcCallBridge @Inject constructor( private val networkConnectivity: NetworkConnectivity, private val recipientRepository: RecipientRepository, private val storage: StorageProtocol, - @ManagerScope scope: CoroutineScope, + @ManagerScope private val scope: CoroutineScope, ): CallManager.WebRtcListener, OnAppStartupComponent { companion object { @@ -159,10 +160,17 @@ class WebRtcCallBridge @Inject constructor( private fun handleNewOffer(address: Address, sdp: String, callId: UUID) { Log.d(TAG, "Handle new offer") - callManager.onNewOffer(sdp, callId, address).fail { - Log.e("Loki", "Error handling new offer", it) - callManager.postConnectionError() - terminate() + scope.launch { + try { + callManager.onNewOffer(sdp, callId, address) + } catch (e: CancellationException) { + Log.d(TAG, "onNewOffer coroutine cancelled", e) + throw e + } catch (e: Exception) { + Log.e("Loki", "Error handling new offer", e) + callManager.postConnectionError() + terminate() + } } } @@ -268,9 +276,10 @@ class WebRtcCallBridge @Inject constructor( val expectedState = callManager.currentConnectionState val expectedCallId = callManager.callId - try { - val offerFuture = callManager.onOutgoingCall(context) - offerFuture.fail { e -> + scope.launch { + try { + callManager.onOutgoingCall(context) + } catch (e: Exception) { if (isConsistentState( expectedState, expectedCallId, @@ -278,16 +287,13 @@ class WebRtcCallBridge @Inject constructor( callManager.callId ) ) { - Log.e(TAG, e) callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE) - callManager.postConnectionError() - terminate() } + + Log.e(TAG, e) + callManager.postConnectionError() + terminate() } - } catch (e: Exception) { - Log.e(TAG, e) - callManager.postConnectionError() - terminate() } } } @@ -348,9 +354,12 @@ class WebRtcCallBridge @Inject constructor( val expectedState = callManager.currentConnectionState val expectedCallId = callManager.callId - try { - val answerFuture = callManager.onIncomingCall(context) - answerFuture.fail { e -> + scope.launch { + try { + callManager.onIncomingCall(context) + } catch (e: Exception) { + Log.e(TAG, "incoming call error: $e") + if (isConsistentState( expectedState, expectedCallId, @@ -358,19 +367,15 @@ class WebRtcCallBridge @Inject constructor( callManager.callId ) ) { - Log.e(TAG, "incoming call error: $e") insertMissedCall( recipient, true ) //todo PHONE do we want a missed call in this case? Or just [xxx] called you ? - callManager.postConnectionError() - terminate() } + + callManager.postConnectionError() + terminate() } - } catch (e: Exception) { - Log.e(TAG, e) - callManager.postConnectionError() - terminate() } } }