fix race condition where multiple uploaded media can get same internal id (#2479)
* fix race condition where multiple uploaded media can get same internal id * atomically update media stateflow * atomically update media stateflow
This commit is contained in:
parent
ce5ec15ff1
commit
444e7365c9
2 changed files with 72 additions and 51 deletions
|
@ -51,6 +51,7 @@ import androidx.core.view.ContentInfoCompat
|
||||||
import androidx.core.view.OnReceiveContentListener
|
import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -344,7 +345,8 @@ class ComposeActivity :
|
||||||
viewModel.statusVisibility.observe { visibility ->
|
viewModel.statusVisibility.observe { visibility ->
|
||||||
setStatusVisibility(visibility)
|
setStatusVisibility(visibility)
|
||||||
}
|
}
|
||||||
viewModel.media.observe { media ->
|
lifecycleScope.launch {
|
||||||
|
viewModel.media.collect { media ->
|
||||||
mediaAdapter.submitList(media)
|
mediaAdapter.submitList(media)
|
||||||
if (media.size != mediaCount) {
|
if (media.size != mediaCount) {
|
||||||
mediaCount = media.size
|
mediaCount = media.size
|
||||||
|
@ -352,6 +354,8 @@ class ComposeActivity :
|
||||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.poll.observe { poll ->
|
viewModel.poll.observe { poll ->
|
||||||
binding.pollPreview.visible(poll != null)
|
binding.pollPreview.visible(poll != null)
|
||||||
poll?.let(binding.pollPreview::setPoll)
|
poll?.let(binding.pollPreview::setPoll)
|
||||||
|
@ -364,7 +368,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
updateScheduleButton()
|
updateScheduleButton()
|
||||||
}
|
}
|
||||||
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
|
combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll ->
|
||||||
val active = poll == null &&
|
val active = poll == null &&
|
||||||
media!!.size != 4 &&
|
media!!.size != 4 &&
|
||||||
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||||
|
@ -781,11 +785,11 @@ class ComposeActivity :
|
||||||
spoilerText = binding.composeContentWarningField.text.toString()
|
spoilerText = binding.composeContentWarningField.text.toString()
|
||||||
}
|
}
|
||||||
val characterCount = calculateTextLength()
|
val characterCount = calculateTextLength()
|
||||||
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) {
|
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
||||||
binding.composeEditField.error = getString(R.string.error_empty)
|
binding.composeEditField.error = getString(R.string.error_empty)
|
||||||
enableButtons(true)
|
enableButtons(true)
|
||||||
} else if (characterCount <= maximumTootCharacters) {
|
} else if (characterCount <= maximumTootCharacters) {
|
||||||
if (viewModel.media.value!!.isNotEmpty()) {
|
if (viewModel.media.value.isNotEmpty()) {
|
||||||
finishingUploadDialog = ProgressDialog.show(
|
finishingUploadDialog = ProgressDialog.show(
|
||||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||||
getString(R.string.dialog_message_uploading_media), true, true
|
getString(R.string.dialog_message_uploading_media), true, true
|
||||||
|
@ -983,7 +987,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
data class QueuedMedia(
|
data class QueuedMedia(
|
||||||
val localId: Long,
|
val localId: Int,
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val mediaSize: Long,
|
val mediaSize: Long,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
|
@ -36,15 +37,17 @@ import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.StatusToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
import com.keylesspalace.tusky.util.filter
|
|
||||||
import com.keylesspalace.tusky.util.map
|
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import com.keylesspalace.tusky.util.toLiveData
|
import com.keylesspalace.tusky.util.toLiveData
|
||||||
import com.keylesspalace.tusky.util.withoutFirstWhich
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.flow.updateAndGet
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.rxSingle
|
import kotlinx.coroutines.rx3.rxSingle
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -84,10 +87,10 @@ class ComposeViewModel @Inject constructor(
|
||||||
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
|
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
|
||||||
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
|
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
|
||||||
|
|
||||||
val media = mutableLiveData<List<QueuedMedia>>(listOf())
|
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||||
val uploadError = MutableLiveData<Throwable>()
|
val uploadError = MutableLiveData<Throwable>()
|
||||||
|
|
||||||
private val mediaToJob = mutableMapOf<Long, Job>()
|
private val mediaToJob = mutableMapOf<Int, Job>()
|
||||||
|
|
||||||
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
||||||
|
|
||||||
|
@ -103,7 +106,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri)
|
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri)
|
||||||
val mediaItems = media.value!!
|
val mediaItems = media.value
|
||||||
if (type != QueuedMedia.Type.IMAGE &&
|
if (type != QueuedMedia.Type.IMAGE &&
|
||||||
mediaItems.isNotEmpty() &&
|
mediaItems.isNotEmpty() &&
|
||||||
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
||||||
|
@ -118,29 +121,31 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addMediaToQueue(
|
private suspend fun addMediaToQueue(
|
||||||
type: QueuedMedia.Type,
|
type: QueuedMedia.Type,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mediaSize: Long,
|
mediaSize: Long,
|
||||||
description: String? = null
|
description: String? = null
|
||||||
): QueuedMedia {
|
): QueuedMedia {
|
||||||
|
val mediaItem = media.updateAndGet { mediaValue ->
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = QueuedMedia(
|
||||||
localId = System.currentTimeMillis(),
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
type = type,
|
type = type,
|
||||||
mediaSize = mediaSize,
|
mediaSize = mediaSize,
|
||||||
description = description
|
description = description
|
||||||
)
|
)
|
||||||
media.postValue(media.value!! + mediaItem)
|
mediaValue + mediaItem
|
||||||
|
}.last()
|
||||||
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
||||||
mediaUploader
|
mediaUploader
|
||||||
.uploadMedia(mediaItem)
|
.uploadMedia(mediaItem)
|
||||||
.catch { error ->
|
.catch { error ->
|
||||||
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } }
|
||||||
uploadError.postValue(error)
|
uploadError.postValue(error)
|
||||||
}
|
}
|
||||||
.collect { event ->
|
.collect { event ->
|
||||||
val item = media.value?.find { it.localId == mediaItem.localId }
|
val item = media.value.find { it.localId == mediaItem.localId }
|
||||||
?: return@collect
|
?: return@collect
|
||||||
val newMediaItem = when (event) {
|
val newMediaItem = when (event) {
|
||||||
is UploadEvent.ProgressEvent ->
|
is UploadEvent.ProgressEvent ->
|
||||||
|
@ -148,16 +153,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
is UploadEvent.FinishedEvent ->
|
is UploadEvent.FinishedEvent ->
|
||||||
item.copy(id = event.mediaId, uploadPercent = -1)
|
item.copy(id = event.mediaId, uploadPercent = -1)
|
||||||
}
|
}
|
||||||
synchronized(media) {
|
media.update { mediaValue ->
|
||||||
val mediaValue = media.value!!
|
mediaValue.map { mediaItem ->
|
||||||
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
if (mediaItem.localId == newMediaItem.localId) {
|
||||||
media.postValue(
|
newMediaItem
|
||||||
if (index == -1) {
|
|
||||||
mediaValue + newMediaItem
|
|
||||||
} else {
|
} else {
|
||||||
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
mediaItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,13 +168,23 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
||||||
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, -1, id, description)
|
media.update { mediaValue ->
|
||||||
media.value = media.value!! + mediaItem
|
val mediaItem = QueuedMedia(
|
||||||
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
||||||
|
uri = uri,
|
||||||
|
type = type,
|
||||||
|
mediaSize = 0,
|
||||||
|
uploadPercent = -1,
|
||||||
|
id = id,
|
||||||
|
description = description
|
||||||
|
)
|
||||||
|
mediaValue + mediaItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
mediaToJob[item.localId]?.cancel()
|
mediaToJob[item.localId]?.cancel()
|
||||||
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
|
media.update { mediaValue -> mediaValue.filter { it.localId == item.localId } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleMarkSensitive() {
|
fun toggleMarkSensitive() {
|
||||||
|
@ -211,7 +224,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val mediaUris: MutableList<String> = mutableListOf()
|
val mediaUris: MutableList<String> = mutableListOf()
|
||||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||||
media.value?.forEach { item ->
|
media.value.forEach { item ->
|
||||||
mediaUris.add(item.uri.toString())
|
mediaUris.add(item.uri.toString())
|
||||||
mediaDescriptions.add(item.description)
|
mediaDescriptions.add(item.description)
|
||||||
}
|
}
|
||||||
|
@ -248,14 +261,14 @@ class ComposeViewModel @Inject constructor(
|
||||||
Observable.just(Unit)
|
Observable.just(Unit)
|
||||||
}.toLiveData()
|
}.toLiveData()
|
||||||
|
|
||||||
val sendObservable = media
|
val sendFlow = media
|
||||||
.filter { items -> items.all { it.uploadPercent == -1 } }
|
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||||
.map {
|
.map {
|
||||||
val mediaIds: MutableList<String> = mutableListOf()
|
val mediaIds: MutableList<String> = mutableListOf()
|
||||||
val mediaUris: MutableList<Uri> = mutableListOf()
|
val mediaUris: MutableList<Uri> = mutableListOf()
|
||||||
val mediaDescriptions: MutableList<String> = mutableListOf()
|
val mediaDescriptions: MutableList<String> = mutableListOf()
|
||||||
val mediaProcessed: MutableList<Boolean> = mutableListOf()
|
val mediaProcessed: MutableList<Boolean> = mutableListOf()
|
||||||
for (item in media.value!!) {
|
for (item in media.value) {
|
||||||
mediaIds.add(item.id!!)
|
mediaIds.add(item.id!!)
|
||||||
mediaUris.add(item.uri)
|
mediaUris.add(item.uri)
|
||||||
mediaDescriptions.add(item.description ?: "")
|
mediaDescriptions.add(item.description ?: "")
|
||||||
|
@ -285,17 +298,21 @@ class ComposeViewModel @Inject constructor(
|
||||||
serviceClient.sendToot(tootToSend)
|
serviceClient.sendToot(tootToSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
|
return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateDescription(localId: Long, description: String): Boolean {
|
suspend fun updateDescription(localId: Int, description: String): Boolean {
|
||||||
val newList = media.value!!.toMutableList()
|
val newMediaList = media.updateAndGet { mediaValue ->
|
||||||
val index = newList.indexOfFirst { it.localId == localId }
|
mediaValue.map { mediaItem ->
|
||||||
if (index != -1) {
|
if (mediaItem.localId == localId) {
|
||||||
newList[index] = newList[index].copy(description = description)
|
mediaItem.copy(description = description)
|
||||||
|
} else {
|
||||||
|
mediaItem
|
||||||
}
|
}
|
||||||
media.value = newList
|
}
|
||||||
val updatedItem = newList.find { it.localId == localId }
|
}
|
||||||
|
|
||||||
|
val updatedItem = newMediaList.find { it.localId == localId }
|
||||||
if (updatedItem?.id != null) {
|
if (updatedItem?.id != null) {
|
||||||
return api.updateMedia(updatedItem.id, description)
|
return api.updateMedia(updatedItem.id, description)
|
||||||
.fold({
|
.fold({
|
||||||
|
@ -387,8 +404,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
val draftAttachments = composeOptions?.draftAttachments
|
val draftAttachments = composeOptions?.draftAttachments
|
||||||
if (draftAttachments != null) {
|
if (draftAttachments != null) {
|
||||||
// when coming from DraftActivity
|
// when coming from DraftActivity
|
||||||
draftAttachments.forEach { attachment ->
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
draftAttachments.forEach { attachment ->
|
||||||
pickMedia(attachment.uri, attachment.description)
|
pickMedia(attachment.uri, attachment.description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue