mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-27 04:04:43 +01:00
Reshape entry point for V3 media screens.
This commit is contained in:
committed by
Greyson Parrelli
parent
6d944c0f8c
commit
5c415139fd
@@ -15,28 +15,11 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.mediasend.preupload.PreUploadManager
|
||||
import org.signal.mediasend.select.MediaSelectScreen
|
||||
|
||||
/**
|
||||
* Abstract base activity for the media sending flow.
|
||||
*
|
||||
* App-layer implementations must extend this class and provide:
|
||||
* - [preUploadCallback] — For pre-upload job management
|
||||
* - [repository] — For media validation, sending, and other app-layer operations
|
||||
* - UI slots: [CameraSlot], [TextStoryEditorSlot], [MediaSelectSlot], [ImageEditorSlot], [VideoEditorSlot], [SendSlot]
|
||||
*
|
||||
* The concrete implementation should be registered in the app's manifest.
|
||||
* Activity for the media sending flow.
|
||||
*/
|
||||
abstract class MediaSendActivity : FragmentActivity() {
|
||||
|
||||
/** Pre-upload callback implementation for job management. */
|
||||
protected abstract val preUploadCallback: PreUploadManager.Callback
|
||||
|
||||
/** Repository implementation for app-layer operations. */
|
||||
protected abstract val repository: MediaSendRepository
|
||||
|
||||
/** Contract args extracted from intent. Available after super.onCreate(). */
|
||||
protected lateinit var contractArgs: MediaSendActivityContract.Args
|
||||
private set
|
||||
|
||||
@@ -49,11 +32,8 @@ abstract class MediaSendActivity : FragmentActivity() {
|
||||
setContent {
|
||||
val viewModel by viewModels<MediaSendViewModel>(factoryProducer = {
|
||||
MediaSendViewModel.Factory(
|
||||
context = applicationContext,
|
||||
args = contractArgs,
|
||||
isMeteredFlow = MeteredConnectivity.isMetered(applicationContext),
|
||||
repository = repository,
|
||||
preUploadCallback = preUploadCallback
|
||||
isMeteredFlow = MeteredConnectivity.isMetered(applicationContext)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -62,68 +42,35 @@ abstract class MediaSendActivity : FragmentActivity() {
|
||||
if (state.isCameraFirst) MediaSendNavKey.Capture.Camera else MediaSendNavKey.Select
|
||||
)
|
||||
|
||||
Theme {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
MediaSendNavDisplay(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
callback = viewModel,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
cameraSlot = { CameraSlot() },
|
||||
textStoryEditorSlot = { TextStoryEditorSlot() },
|
||||
mediaSelectSlot = {
|
||||
MediaSelectScreen(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
callback = viewModel
|
||||
)
|
||||
},
|
||||
videoEditorSlot = { VideoEditorSlot() },
|
||||
sendSlot = { SendSlot() }
|
||||
cameraSlot = { },
|
||||
textStoryEditorSlot = { },
|
||||
videoEditorSlot = { },
|
||||
sendSlot = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Theme wrapper */
|
||||
@Composable
|
||||
protected open fun Theme(content: @Composable () -> Unit) {
|
||||
SignalTheme(incognitoKeyboardEnabled = false) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/** Camera capture UI slot. */
|
||||
@Composable
|
||||
protected abstract fun CameraSlot()
|
||||
|
||||
/** Text story editor UI slot. */
|
||||
@Composable
|
||||
protected abstract fun TextStoryEditorSlot()
|
||||
|
||||
/** Video editor UI slot. */
|
||||
@Composable
|
||||
protected abstract fun VideoEditorSlot()
|
||||
|
||||
/** Send/review UI slot. */
|
||||
@Composable
|
||||
protected abstract fun SendSlot()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates an intent for a concrete [MediaSendActivity] subclass.
|
||||
* Creates an intent for [MediaSendActivity].
|
||||
*
|
||||
* @param context The context.
|
||||
* @param activityClass The concrete activity class to launch.
|
||||
* @param args The activity arguments.
|
||||
*/
|
||||
fun <T : MediaSendActivity> createIntent(
|
||||
fun createIntent(
|
||||
context: Context,
|
||||
activityClass: Class<T>,
|
||||
args: MediaSendActivityContract.Args = MediaSendActivityContract.Args()
|
||||
): Intent {
|
||||
return Intent(context, activityClass).apply {
|
||||
return Intent(context, MediaSendActivity::class.java).apply {
|
||||
putExtra(MediaSendActivityContract.EXTRA_ARGS, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,7 @@ import org.signal.core.models.media.Media
|
||||
* class MyMediaSendContract : MediaSendActivityContract(MyMediaSendActivity::class.java)
|
||||
* ```
|
||||
*/
|
||||
open class MediaSendActivityContract(
|
||||
private val activityClass: Class<out MediaSendActivity>? = null
|
||||
) : ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?>() {
|
||||
class MediaSendActivityContract : ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?>() {
|
||||
|
||||
/**
|
||||
* Creates the intent to launch the media send activity.
|
||||
@@ -40,12 +38,7 @@ open class MediaSendActivityContract(
|
||||
* Subclasses should override this if not using the constructor parameter.
|
||||
*/
|
||||
override fun createIntent(context: Context, input: Args): Intent {
|
||||
val clazz = activityClass
|
||||
?: throw IllegalStateException(
|
||||
"MediaSendActivityContract requires either a concrete activity class in the constructor " +
|
||||
"or an overridden createIntent() method. MediaSendActivity is abstract and cannot be launched directly."
|
||||
)
|
||||
return MediaSendActivity.createIntent(context, clazz, input)
|
||||
return MediaSendActivity.createIntent(context, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Result? {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend
|
||||
|
||||
import android.app.Application
|
||||
import org.signal.mediasend.preupload.PreUploadRepository
|
||||
|
||||
/**
|
||||
* MediaSend Feature Module dependencies
|
||||
*/
|
||||
object MediaSendDependencies {
|
||||
private lateinit var _application: Application
|
||||
private lateinit var _provider: Provider
|
||||
|
||||
@Synchronized
|
||||
fun init(application: Application, provider: Provider) {
|
||||
if (this::_application.isInitialized || this::_provider.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
_application = application
|
||||
_provider = provider
|
||||
}
|
||||
|
||||
val application
|
||||
get() = _application
|
||||
|
||||
val preUploadRepository: PreUploadRepository
|
||||
get() = _provider.providePreUploadRepository()
|
||||
|
||||
val mediaSendRepository: MediaSendRepository
|
||||
get() = _provider.provideMediaSendRepository()
|
||||
|
||||
interface Provider {
|
||||
fun provideMediaSendRepository(): MediaSendRepository
|
||||
fun providePreUploadRepository(): PreUploadRepository
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import androidx.navigationevent.compose.rememberNavigationEventDispatcherOwner
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.mediasend.edit.MediaEditScreen
|
||||
import org.signal.mediasend.select.MediaSelectScreen
|
||||
|
||||
/**
|
||||
* Enforces the following flow of:
|
||||
@@ -32,7 +33,6 @@ fun MediaSendNavDisplay(
|
||||
modifier: Modifier = Modifier,
|
||||
cameraSlot: @Composable () -> Unit = {},
|
||||
textStoryEditorSlot: @Composable () -> Unit = {},
|
||||
mediaSelectSlot: @Composable () -> Unit = {},
|
||||
videoEditorSlot: @Composable () -> Unit = {},
|
||||
sendSlot: @Composable () -> Unit = {}
|
||||
) {
|
||||
@@ -50,7 +50,11 @@ fun MediaSendNavDisplay(
|
||||
}
|
||||
|
||||
MediaSendNavKey.Select -> NavEntry(key) {
|
||||
mediaSelectSlot()
|
||||
MediaSelectScreen(
|
||||
state = state,
|
||||
backStack = backStack,
|
||||
callback = callback
|
||||
)
|
||||
}
|
||||
|
||||
is MediaSendNavKey.Edit -> NavEntry(MediaSendNavKey.Edit) {
|
||||
@@ -82,7 +86,6 @@ private fun MediaSendNavDisplayPreview() {
|
||||
callback = MediaSendCallback.Empty,
|
||||
cameraSlot = { BoxWithText("Camera Slot") },
|
||||
textStoryEditorSlot = { BoxWithText("Text Story Editor Slot") },
|
||||
mediaSelectSlot = { BoxWithText("Media Select Slot") },
|
||||
videoEditorSlot = { BoxWithText("Video Editor Slot") },
|
||||
sendSlot = { BoxWithText("Send Slot") }
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
package org.signal.mediasend
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
@@ -38,7 +37,7 @@ import org.signal.core.util.StringUtil
|
||||
import org.signal.imageeditor.core.model.EditorElement
|
||||
import org.signal.imageeditor.core.model.EditorModel
|
||||
import org.signal.imageeditor.core.renderers.UriGlideRenderer
|
||||
import org.signal.mediasend.preupload.PreUploadManager
|
||||
import org.signal.mediasend.preupload.PreUploadController
|
||||
import java.util.Collections
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -54,7 +53,7 @@ class MediaSendViewModel(
|
||||
isMeteredFlow: Flow<Boolean>,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val repository: MediaSendRepository,
|
||||
private val preUploadManager: PreUploadManager
|
||||
private val preUploadController: PreUploadController
|
||||
) : ViewModel(), MediaSendCallback {
|
||||
|
||||
private val defaultState = MediaSendState(
|
||||
@@ -309,8 +308,8 @@ class MediaSendViewModel(
|
||||
viewModelScope.launch {
|
||||
repository.deleteBlobs(media.toList())
|
||||
}
|
||||
preUploadManager.cancelUpload(media)
|
||||
preUploadManager.updateDisplayOrder(newSelection)
|
||||
preUploadController.cancelUpload(media)
|
||||
preUploadController.updateDisplayOrder(newSelection)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,9 +322,9 @@ class MediaSendViewModel(
|
||||
val updatedSelection = snapshot.selectedMedia.map { oldToNew[it] ?: it }
|
||||
updateState { copy(selectedMedia = updatedSelection) }
|
||||
|
||||
preUploadManager.applyMediaUpdates(oldToNew, snapshot.recipientId)
|
||||
preUploadManager.updateCaptions(updatedSelection)
|
||||
preUploadManager.updateDisplayOrder(updatedSelection)
|
||||
preUploadController.applyMediaUpdates(oldToNew, snapshot.recipientId)
|
||||
preUploadController.updateCaptions(updatedSelection)
|
||||
preUploadController.updateDisplayOrder(updatedSelection)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,7 +332,7 @@ class MediaSendViewModel(
|
||||
*/
|
||||
fun setDisplayOrder(mediaInOrder: List<Media>) {
|
||||
updateState { copy(selectedMedia = mediaInOrder) }
|
||||
preUploadManager.updateDisplayOrder(mediaInOrder)
|
||||
preUploadController.updateDisplayOrder(mediaInOrder)
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -350,9 +349,9 @@ class MediaSendViewModel(
|
||||
media.filter { ContentTypeUtil.isStorySupportedType(it.contentType) }
|
||||
}
|
||||
|
||||
preUploadManager.startUpload(filteredPreUploadMedia, snapshot.recipientId)
|
||||
preUploadManager.updateCaptions(snapshot.selectedMedia)
|
||||
preUploadManager.updateDisplayOrder(snapshot.selectedMedia)
|
||||
preUploadController.startUpload(filteredPreUploadMedia, snapshot.recipientId)
|
||||
preUploadController.updateCaptions(snapshot.selectedMedia)
|
||||
preUploadController.updateDisplayOrder(snapshot.selectedMedia)
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -369,7 +368,7 @@ class MediaSendViewModel(
|
||||
if (snapshot.sentMediaQuality == sentMediaQuality) return
|
||||
|
||||
updateState { copy(sentMediaQuality = sentMediaQuality, isPreUploadEnabled = false) }
|
||||
preUploadManager.cancelAllUploads()
|
||||
preUploadController.cancelAllUploads()
|
||||
|
||||
// Re-clamp video durations based on new quality
|
||||
val maxVideoDurationUs = getMaxVideoDurationUs()
|
||||
@@ -404,7 +403,7 @@ class MediaSendViewModel(
|
||||
savedStateHandle[KEY_EDITED_VIDEO_URIS] = ArrayList(editedVideoUris)
|
||||
|
||||
val media = state.value.selectedMedia.firstOrNull { it.uri == uri } ?: return
|
||||
preUploadManager.cancelUpload(media)
|
||||
preUploadController.cancelUpload(media)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,7 +442,7 @@ class MediaSendViewModel(
|
||||
if (unedited && durationEdited) {
|
||||
val media = snapshot.selectedMedia.firstOrNull { it.uri == uri }
|
||||
if (media != null) {
|
||||
preUploadManager.cancelUpload(media)
|
||||
preUploadController.cancelUpload(media)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,7 +531,7 @@ class MediaSendViewModel(
|
||||
|
||||
fun onMediaDragFinished() {
|
||||
lastMediaDrag = Pair(0, 0)
|
||||
preUploadManager.updateDisplayOrder(internalState.value.selectedMedia)
|
||||
preUploadController.updateDisplayOrder(internalState.value.selectedMedia)
|
||||
}
|
||||
|
||||
//endregion
|
||||
@@ -704,8 +703,8 @@ class MediaSendViewModel(
|
||||
//region Lifecycle
|
||||
|
||||
override fun onCleared() {
|
||||
preUploadManager.cancelAllUploads()
|
||||
preUploadManager.deleteAbandonedAttachments()
|
||||
preUploadController.cancelAllUploads()
|
||||
preUploadController.deleteAbandonedAttachments()
|
||||
}
|
||||
|
||||
private fun shouldPreUpload(metered: Boolean): Boolean = !metered
|
||||
@@ -715,26 +714,22 @@ class MediaSendViewModel(
|
||||
//region Factory
|
||||
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val args: MediaSendActivityContract.Args,
|
||||
private val identityChangesSince: Long = System.currentTimeMillis(),
|
||||
private val isMeteredFlow: Flow<Boolean>,
|
||||
private val repository: MediaSendRepository,
|
||||
private val preUploadCallback: PreUploadManager.Callback
|
||||
private val isMeteredFlow: Flow<Boolean>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||
val savedStateHandle = extras.createSavedStateHandle()
|
||||
val manager = PreUploadManager(context.applicationContext, preUploadCallback)
|
||||
|
||||
return MediaSendViewModel(
|
||||
args = args,
|
||||
identityChangesSince = identityChangesSince,
|
||||
isMeteredFlow = isMeteredFlow,
|
||||
savedStateHandle = savedStateHandle,
|
||||
repository = repository,
|
||||
preUploadManager = manager
|
||||
repository = MediaSendDependencies.mediaSendRepository,
|
||||
preUploadController = PreUploadController()
|
||||
) as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.mediasend.MediaRecipientId
|
||||
import org.signal.mediasend.MediaSendDependencies
|
||||
import java.util.LinkedHashMap
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
@@ -27,12 +28,10 @@ import java.util.concurrent.Executor
|
||||
*
|
||||
* This class is stateful.
|
||||
*/
|
||||
class PreUploadManager(
|
||||
context: Context,
|
||||
private val callback: Callback
|
||||
) {
|
||||
class PreUploadController {
|
||||
|
||||
private val context: Context = context.applicationContext
|
||||
private val callback: PreUploadRepository = MediaSendDependencies.preUploadRepository
|
||||
private val context: Context = MediaSendDependencies.application
|
||||
private val uploadResults: LinkedHashMap<Media, PreUploadResult> = LinkedHashMap()
|
||||
private val executor: Executor =
|
||||
SignalExecutors.newCachedSingleThreadExecutor("signal-PreUpload", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
|
||||
@@ -234,74 +233,7 @@ class PreUploadManager(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callbacks that perform the real side-effects (DB ops, job scheduling/cancelation, etc).
|
||||
*
|
||||
* This keeps `feature/media-send` free of direct dependencies on app-specific systems.
|
||||
*
|
||||
* Threading: all callback methods are invoked on this manager's serialized background executor
|
||||
* thread (i.e., not the main thread).
|
||||
*/
|
||||
interface Callback {
|
||||
/**
|
||||
* Performs pre-upload side-effects (e.g., create attachment state + enqueue jobs).
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param media The media item being pre-uploaded.
|
||||
* @param recipientId Optional recipient identifier, if known.
|
||||
* @return A [PreUploadResult] if enqueued, or `null` if it failed or should not pre-upload.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun preUpload(context: Context, media: Media, recipientId: MediaRecipientId?): PreUploadResult?
|
||||
|
||||
/**
|
||||
* Cancels any scheduled/running work for the provided job ids.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param jobIds Job identifiers to cancel.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun cancelJobs(context: Context, jobIds: List<String>)
|
||||
|
||||
/**
|
||||
* Deletes any persisted attachment state for [attachmentId].
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param attachmentId Attachment identifier to delete.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun deleteAttachment(context: Context, attachmentId: Long)
|
||||
|
||||
/**
|
||||
* Updates the caption for [attachmentId].
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param attachmentId Attachment identifier.
|
||||
* @param caption New caption (or `null` to clear).
|
||||
*/
|
||||
@WorkerThread
|
||||
fun updateAttachmentCaption(context: Context, attachmentId: Long, caption: String?)
|
||||
|
||||
/**
|
||||
* Updates display order for attachments.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param orderMap Map of attachment id -> display order index.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun updateDisplayOrder(context: Context, orderMap: Map<Long, Int>)
|
||||
|
||||
/**
|
||||
* Deletes any pre-uploaded attachments that are no longer referenced.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @return The number of attachments deleted.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun deleteAbandonedPreuploadedAttachments(context: Context): Int
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val TAG = Log.tag(PreUploadManager::class.java)
|
||||
private val TAG = Log.tag(PreUploadController::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.mediasend.preupload
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.core.models.media.Media
|
||||
import org.signal.mediasend.MediaRecipientId
|
||||
|
||||
/**
|
||||
* Callbacks that perform the real side-effects (DB ops, job scheduling/cancelation, etc).
|
||||
*
|
||||
* This keeps `feature/media-send` free of direct dependencies on app-specific systems.
|
||||
*
|
||||
* Threading: all callback methods are invoked on this manager's serialized background executor
|
||||
* thread (i.e., not the main thread).
|
||||
*/
|
||||
interface PreUploadRepository {
|
||||
/**
|
||||
* Performs pre-upload side-effects (e.g., create attachment state + enqueue jobs).
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param media The media item being pre-uploaded.
|
||||
* @param recipientId Optional recipient identifier, if known.
|
||||
* @return A [PreUploadResult] if enqueued, or `null` if it failed or should not pre-upload.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun preUpload(context: Context, media: Media, recipientId: MediaRecipientId?): PreUploadResult?
|
||||
|
||||
/**
|
||||
* Cancels any scheduled/running work for the provided job ids.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param jobIds Job identifiers to cancel.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun cancelJobs(context: Context, jobIds: List<String>)
|
||||
|
||||
/**
|
||||
* Deletes any persisted attachment state for [attachmentId].
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param attachmentId Attachment identifier to delete.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun deleteAttachment(context: Context, attachmentId: Long)
|
||||
|
||||
/**
|
||||
* Updates the caption for [attachmentId].
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param attachmentId Attachment identifier.
|
||||
* @param caption New caption (or `null` to clear).
|
||||
*/
|
||||
@WorkerThread
|
||||
fun updateAttachmentCaption(context: Context, attachmentId: Long, caption: String?)
|
||||
|
||||
/**
|
||||
* Updates display order for attachments.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @param orderMap Map of attachment id -> display order index.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun updateDisplayOrder(context: Context, orderMap: Map<Long, Int>)
|
||||
|
||||
/**
|
||||
* Deletes any pre-uploaded attachments that are no longer referenced.
|
||||
*
|
||||
* @param context Application context.
|
||||
* @return The number of attachments deleted.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun deleteAbandonedPreuploadedAttachments(context: Context): Int
|
||||
}
|
||||
Reference in New Issue
Block a user