/* * Copyright 2019 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package im.vector.matrix.android.internal.session.room.send import android.content.Context import androidx.work.* import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.CancelableBag import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon import im.vector.matrix.android.internal.util.CancelableWork import im.vector.matrix.android.internal.worker.AlwaysSuccessfulWorker import im.vector.matrix.android.internal.worker.WorkManagerUtil import im.vector.matrix.android.internal.worker.WorkManagerUtil.matrixOneTimeWorkRequestBuilder import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.startChain import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject private const val UPLOAD_WORK = "UPLOAD_WORK" private const val BACKOFF_DELAY = 10_000L internal class DefaultSendService @Inject constructor(private val context: Context, private val credentials: Credentials, private val roomId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val cryptoService: CryptoService, private val monarchy: Monarchy) : SendService { private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor() override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable { val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also { saveLocalEcho(it) } return sendEvent(event) } override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable { val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also { saveLocalEcho(it) } return sendEvent(event) } private fun sendEvent(event: Event): Cancelable { // Encrypted room handling return if (cryptoService.isRoomEncrypted(roomId)) { Timber.v("Send event in encrypted room") val encryptWork = createEncryptEventWork(event, true) val sendWork = createSendEventWork(event, false) TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, sendWork) CancelableWork(context, encryptWork.id) } else { val sendWork = createSendEventWork(event, true) TimelineSendEventWorkCommon.postWork(context, roomId, sendWork) CancelableWork(context, sendWork.id) } } override fun sendMedias(attachments: List): Cancelable { return attachments.mapTo(CancelableBag()) { sendMedia(it) } } override fun redactEvent(event: Event, reason: String?): Cancelable { //TODO manage media/attachements? val redactWork = createRedactEventWork(event, reason) TimelineSendEventWorkCommon.postWork(context, roomId, redactWork) return CancelableWork(context, redactWork.id) } override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { return sendEvent(localEcho.root) } return null } override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { //TODO this need a refactoring of attachement sending // val clearContent = localEcho.root.getClearContent() // val messageContent = clearContent?.toModel() ?: return null // when (messageContent.type) { // MessageType.MSGTYPE_IMAGE -> { // val imageContent = clearContent.toModel() ?: return null // val url = imageContent.url ?: return null // if (url.startsWith("mxc://")) { // //TODO // } else { // //The image has not yet been sent // val attachmentData = ContentAttachmentData( // size = imageContent.info!!.size.toLong(), // mimeType = imageContent.info.mimeType!!, // width = imageContent.info.width.toLong(), // height = imageContent.info.height.toLong(), // name = imageContent.body, // path = imageContent.url, // type = ContentAttachmentData.Type.IMAGE // ) // monarchy.runTransactionSync { // EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { // it.sendState = SendState.UNSENT // } // } // return internalSendMedia(localEcho.root,attachmentData) // } // } // } return null } return null } override fun deleteFailedEcho(localEcho: TimelineEvent) { monarchy.writeAsync { realm -> TimelineEventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.let { it.deleteFromRealm() } } } override fun clearSendingQueue() { TimelineSendEventWorkCommon.cancelAllWorks(context, roomId) WorkManager.getInstance(context).cancelUniqueWork(buildWorkName(UPLOAD_WORK)) // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied matrixOneTimeWorkRequestBuilder() .build().let { TimelineSendEventWorkCommon.postWork(context, roomId, it, ExistingWorkPolicy.REPLACE) //need to clear also image sending queue WorkManager.getInstance(context) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) .enqueue() } monarchy.writeAsync { realm -> RoomEntity.where(realm, roomId).findFirst()?.let { room -> room.sendingTimelineEvents.forEach { it.root?.sendState = SendState.UNDELIVERED } } } } override fun resendAllFailedMessages() { monarchy.writeAsync { realm -> TimelineEventEntity .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) .sortedBy { it.root?.originServerTs ?: 0 } .forEach { timelineEventEntity -> timelineEventEntity.root?.let { val event = it.asDomain() when (event.getClearType()) { EventType.MESSAGE, EventType.REDACTION, EventType.REACTION -> { val content = event.getClearContent().toModel() if (content != null) { when (content.type) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_LOCATION, MessageType.MSGTYPE_TEXT -> { it.sendState = SendState.UNSENT sendEvent(event) } MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO -> { //need to resend the attachement } else -> { Timber.e("Cannot resend message ${event.type} / ${content.type}") } } } else { Timber.e("Unsupported message to resend ${event.type}") } } else -> { Timber.e("Unsupported message to resend ${event.type}") } } } } } } override fun sendMedia(attachment: ContentAttachmentData): Cancelable { // Create an event with the media file path val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { saveLocalEcho(it) } return internalSendMedia(event, attachment) } private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): CancelableWork { val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) val sendWork = createSendEventWork(localEcho, false) if (isRoomEncrypted) { val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) val op: Operation = WorkManager.getInstance(context) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .then(encryptWork) .then(sendWork) .enqueue() op.result.addListener(Runnable { if (op.result.isCancelled) { Timber.e("CHAIN WAS CANCELLED") } else if (op.state.value is Operation.State.FAILURE) { Timber.e("CHAIN DID FAIL") } }, workerFutureListenerExecutor) } else { WorkManager.getInstance(context) .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) .then(sendWork) .enqueue() } return CancelableWork(context, sendWork.id) } private fun saveLocalEcho(event: Event) { localEchoEventFactory.saveLocalEcho(monarchy, event) } private fun buildWorkName(identifier: String): String { return "${roomId}_$identifier" } private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { // Same parameter val params = EncryptEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(params) return matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) .setInputData(sendWorkData) .startChain(startChain) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { val sendContentWorkerParams = SendEventWorker.Params(credentials.userId, roomId, event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(sendWorkData, startChain) } private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also { saveLocalEcho(it) } val sendContentWorkerParams = RedactEventWorker.Params(credentials.userId, redactEvent.eventId!!, roomId, event.eventId, reason) val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return TimelineSendEventWorkCommon.createWork(redactWorkData, true) } private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData, isRoomEncrypted: Boolean, startChain: Boolean): OneTimeWorkRequest { val uploadMediaWorkerParams = UploadContentWorker.Params(credentials.userId, roomId, event, attachment, isRoomEncrypted) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) return matrixOneTimeWorkRequestBuilder() .setConstraints(WorkManagerUtil.workConstraints) .startChain(startChain) .setInputData(uploadWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } }