mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Consolidate attachment saving logic into unified AttachmentSaver class.
Introduces `AttachmentSaver` to centralize all of the steps needed to save message attachments to the device storage. It handles the entire workflow including: - Showing the save to storage warning/confirmation dialog. - Requesting `WRITE_EXTERNAL_STORAGE` permission. - Showing/dismissing media save progress. Goals of this new class: - Make it easy to save media attachments anywhere with just a few lines of code (and easier to replace the deprecated `SaveAttachmentTask`). - Ensure all of the necessary steps are consistently performed at each usage site (which wasn't the case before). - Make it easier to unit test the save attachment logic.
This commit is contained in:
committed by
Cody Henthorne
parent
86afafac31
commit
b9dc5cbe4f
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Shows a dialog and suspends until user interaction, returning the [AlertDialogResult].
|
||||
*
|
||||
* Note: this method will overwrite any existing dialog button click listeners or cancellation listener.
|
||||
*/
|
||||
suspend fun AlertDialog.awaitResult(
|
||||
positiveButtonTextId: Int? = null,
|
||||
negativeButtonTextId: Int? = null,
|
||||
neutralButtonTextId: Int? = null
|
||||
) = awaitResult(
|
||||
positiveButtonText = positiveButtonTextId?.let(context::getString),
|
||||
negativeButtonText = negativeButtonTextId?.let(context::getString),
|
||||
neutralButtonText = neutralButtonTextId?.let(context::getString)
|
||||
)
|
||||
|
||||
/**
|
||||
* Shows a dialog and suspends until user interaction, returning the [AlertDialogResult].
|
||||
*
|
||||
* Note: this method will overwrite any existing dialog button click listeners or cancellation listener.
|
||||
*/
|
||||
suspend fun AlertDialog.awaitResult(
|
||||
positiveButtonText: String? = null,
|
||||
negativeButtonText: String? = null,
|
||||
neutralButtonText: String? = null
|
||||
) = suspendCancellableCoroutine { continuation ->
|
||||
|
||||
positiveButtonText?.let { text -> setButton(AlertDialog.BUTTON_POSITIVE, text) { _, _ -> continuation.resume(AlertDialogResult.POSITIVE) } }
|
||||
negativeButtonText?.let { text -> setButton(AlertDialog.BUTTON_NEGATIVE, text) { _, _ -> continuation.resume(AlertDialogResult.NEGATIVE) } }
|
||||
neutralButtonText?.let { text -> setButton(AlertDialog.BUTTON_NEUTRAL, text) { _, _ -> continuation.resume(AlertDialogResult.NEUTRAL) } }
|
||||
|
||||
setOnCancelListener { continuation.resume(AlertDialogResult.CANCELED) }
|
||||
continuation.invokeOnCancellation { dismiss() }
|
||||
|
||||
show()
|
||||
}
|
||||
|
||||
enum class AlertDialogResult {
|
||||
POSITIVE,
|
||||
NEGATIVE,
|
||||
NEUTRAL,
|
||||
CANCELED
|
||||
}
|
||||
@@ -16,19 +16,13 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.CheckBox
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -50,37 +44,8 @@ private typealias BatchOperationNameCache = HashMap<Uri, HashSet<String>>
|
||||
* a progress dialog and is not backed by an async task.
|
||||
*/
|
||||
object SaveAttachmentUtil {
|
||||
|
||||
private val TAG = Log.tag(SaveAttachmentUtil::class.java)
|
||||
|
||||
fun showWarningDialogIfNecessary(context: Context, count: Int, onSave: () -> Unit) {
|
||||
if (SignalStore.uiHints.hasDismissedSaveStorageWarning()) {
|
||||
onSave()
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setView(R.layout.dialog_save_attachment)
|
||||
.setTitle(R.string.ConversationFragment__save_to_phone)
|
||||
.setCancelable(true)
|
||||
.setMessage(context.resources.getQuantityString(R.plurals.ConversationFragment__this_media_will_be_saved, count, count))
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
val checkbox = (dialog as AlertDialog).findViewById<CheckBox>(R.id.checkbox)!!
|
||||
if (checkbox.isChecked) {
|
||||
SignalStore.uiHints.markDismissedSaveStorageWarning()
|
||||
}
|
||||
onSave()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAttachmentsForRecord(record: MmsMessageRecord): Set<SaveAttachment> {
|
||||
return record.slideDeck.slides
|
||||
.filter { it.uri != null && (it.hasImage() || it.hasVideo() || it.hasAudio() || it.hasDocument()) }
|
||||
.map { SaveAttachment(it.uri!!, it.contentType, record.dateSent, it.fileName.orNull()) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
suspend fun saveAttachments(attachments: Set<SaveAttachment>): SaveAttachmentsResult {
|
||||
check(attachments.isNotEmpty()) { "must pass in at least one attachment" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user