From 20e33f70a8d0bf5ef82f8a7790cdb6fe49976d9b Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 8 Apr 2024 12:07:34 -0700 Subject: [PATCH] attachments: proper metadata plumbing --- .../ui/messagelist/MessageListViewModel.kt | 53 +++++++------------ .../kordophone/backend/model/Message.kt | 8 ++- .../backend/server/ChatRepository.kt | 36 +++++-------- 3 files changed, 39 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt index de2a094..7c62b8d 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListViewModel.kt @@ -2,55 +2,23 @@ package net.buzzert.kordophonedroid.ui.messagelist import android.content.Context import android.net.Uri -import android.util.Log -import androidx.compose.runtime.mutableStateListOf -import androidx.core.net.toFile +import android.webkit.MimeTypeMap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import coil.Coil -import coil.ComponentRegistry -import coil.ImageLoader -import coil.ImageLoaderFactory -import coil.decode.DataSource -import coil.decode.ImageSource -import coil.disk.DiskCache -import coil.fetch.FetchResult -import coil.fetch.Fetcher -import coil.fetch.SourceResult -import coil.memory.MemoryCache -import coil.request.CachePolicy -import coil.request.Options import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import net.buzzert.kordophone.backend.model.Conversation import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophone.backend.model.Message import net.buzzert.kordophone.backend.model.OutgoingMessage +import net.buzzert.kordophone.backend.model.UploadingAttachmentMetadata import net.buzzert.kordophone.backend.server.ChatRepository -import net.buzzert.kordophonedroid.ui.attachments.AttachmentFetchData -import net.buzzert.kordophonedroid.ui.attachments.AttachmentViewModel -import okio.ByteString.Companion.toByteString -import java.io.BufferedInputStream -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.nio.file.FileSystem -import java.util.Date -import java.util.UUID -import java.util.concurrent.TimeUnit import javax.inject.Inject -import javax.xml.transform.Source -import kotlin.time.Duration.Companion.seconds const val MVM_LOG: String = "MessageListViewModel" @@ -106,7 +74,22 @@ class MessageListViewModel @Inject constructor( conversation = conversation!!, attachmentUris = attachmentUris, attachmentDataSource = { uri -> - context.contentResolver.openInputStream(uri) + val resolver = context.contentResolver + val inputStream = resolver.openInputStream(uri) + val mimeType = resolver.getType(uri) + + val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: "jpg" + val filename = uri.lastPathSegment + ".$extension" + + if (inputStream != null && mimeType != null) { + UploadingAttachmentMetadata( + inputStream = inputStream, + mimeType = mimeType, + filename = filename + ) + } else { + null + } } ) diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt b/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt index 66344dc..cfea6a1 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/model/Message.kt @@ -71,11 +71,17 @@ data class Message( } } +data class UploadingAttachmentMetadata( + val inputStream: InputStream, + val mimeType: String, + val filename: String, +) + data class OutgoingMessage( val body: String, val conversation: Conversation, val attachmentUris: Set, - val attachmentDataSource: (Uri) -> InputStream? + val attachmentDataSource: (Uri) -> UploadingAttachmentMetadata? ) { val guid: String = UUID.randomUUID().toString() diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt index 9ff18c2..f07043e 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/server/ChatRepository.kt @@ -1,45 +1,37 @@ package net.buzzert.kordophone.backend.server import android.net.Uri -import android.os.Build import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.net.toFile import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import net.buzzert.kordophone.backend.db.CachedChatDatabase import net.buzzert.kordophone.backend.events.MessageDeliveredEvent import net.buzzert.kordophone.backend.model.Conversation import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophone.backend.model.Message import net.buzzert.kordophone.backend.model.OutgoingMessage -import net.buzzert.kordophone.backend.model.UpdateItem import okhttp3.MediaType -import okhttp3.OkHttpClient import okhttp3.RequestBody import okio.BufferedSource -import java.io.File import java.io.InputStream -import java.lang.Error -import java.net.URL import java.util.Date -import java.util.Queue import java.util.UUID import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.CancellationException +import kotlin.Boolean +import kotlin.Int +import kotlin.String +import kotlin.let const val REPO_LOG: String = "ChatRepository" const val CONVERSATION_MESSAGE_SYNC_COUNT = 10 @@ -210,7 +202,10 @@ class ChatRepository( private suspend fun uploadAttachment(filename: String, mediaType: String, source: InputStream): String { val attachmentData = source.readBytes() val requestBody = RequestBody.create(MediaType.get(mediaType), attachmentData) - source.close() + + withContext(Dispatchers.IO) { + source.close() + } val response = apiInterface.uploadAttachment(filename, requestBody) return response.bodyOnSuccessOrThrow().transferGUID @@ -237,7 +232,7 @@ class ChatRepository( .onEach { it.conversation = conversation } } - private suspend fun handleConversationChangedUpdate(conversation: Conversation) { + private fun handleConversationChangedUpdate(conversation: Conversation) { Log.d(REPO_LOG, "Handling conversation changed update") database.writeConversations(listOf(conversation)) } @@ -276,13 +271,10 @@ class ChatRepository( val attachmentGUIDs = mutableListOf() try { for (uri: Uri in it.attachmentUris) { - val inputStream = it.attachmentDataSource(uri) - ?: throw java.lang.Exception("No input stream") - - val filename = uri.lastPathSegment ?: "attachment.png" - val mediaType = "image/png" // TODO: Actually get this: it needs to be plumbed through ContentResolver - val guid = uploadAttachment(filename, mediaType, inputStream) + val uploadData = it.attachmentDataSource(uri) + ?: throw java.lang.Exception("No upload data.") + val guid = uploadAttachment(uploadData.filename, uploadData.mimeType, uploadData.inputStream) attachmentGUIDs.add(guid) } } catch (e: java.lang.Exception) {