diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java deleted file mode 100644 index 7d86b214290..00000000000 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.fsck.k9.ui.messageview; - - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import android.content.ActivityNotFoundException; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.AsyncTask; -import android.widget.Toast; - -import androidx.annotation.WorkerThread; -import net.thunderbird.core.android.account.LegacyAccountDto; -import com.fsck.k9.Preferences; -import com.fsck.k9.controller.MessagingController; -import app.k9mail.legacy.message.controller.SimpleMessagingListener; -import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.Part; -import com.fsck.k9.mailstore.AttachmentViewInfo; -import com.fsck.k9.mailstore.LocalMessage; -import com.fsck.k9.mailstore.LocalPart; -import com.fsck.k9.provider.AttachmentTempFileProvider; -import com.fsck.k9.ui.R; -import org.apache.commons.io.IOUtils; -import net.thunderbird.core.logging.legacy.Log; - - -public class AttachmentController { - private final Context context; - private final MessagingController controller; - private final MessageViewFragment messageViewFragment; - private final AttachmentViewInfo attachment; - private final ViewIntentFinder viewIntentFinder; - - - AttachmentController(Context context, MessagingController controller, MessageViewFragment messageViewFragment, - AttachmentViewInfo attachment) { - this.context = context; - this.controller = controller; - this.messageViewFragment = messageViewFragment; - this.attachment = attachment; - viewIntentFinder = new ViewIntentFinder(context); - } - - public void viewAttachment() { - if (!attachment.isContentAvailable()) { - downloadAndViewAttachment((LocalPart) attachment.part); - } else { - viewLocalAttachment(); - } - } - - public void saveAttachmentTo(Uri documentUri) { - if (!attachment.isContentAvailable()) { - downloadAndSaveAttachmentTo((LocalPart) attachment.part, documentUri); - } else { - saveLocalAttachmentTo(documentUri); - } - } - - private void downloadAndViewAttachment(LocalPart localPart) { - downloadAttachment(localPart, new Runnable() { - @Override - public void run() { - messageViewFragment.refreshAttachmentThumbnail(attachment); - viewLocalAttachment(); - } - }); - } - - private void downloadAndSaveAttachmentTo(LocalPart localPart, final Uri documentUri) { - downloadAttachment(localPart, new Runnable() { - @Override - public void run() { - messageViewFragment.refreshAttachmentThumbnail(attachment); - saveLocalAttachmentTo(documentUri); - } - }); - } - - private void downloadAttachment(LocalPart localPart, final Runnable attachmentDownloadedCallback) { - String accountUuid = localPart.getAccountUuid(); - LegacyAccountDto account = Preferences.getPreferences().getAccount(accountUuid); - LocalMessage message = localPart.getMessage(); - - messageViewFragment.showAttachmentLoadingDialog(); - controller.loadAttachment(account, message, attachment.part, new SimpleMessagingListener() { - @Override - public void loadAttachmentFinished(LegacyAccountDto account, Message message, Part part) { - attachment.setContentAvailable(); - messageViewFragment.hideAttachmentLoadingDialogOnMainThread(); - messageViewFragment.runOnMainThread(attachmentDownloadedCallback); - } - - @Override - public void loadAttachmentFailed(LegacyAccountDto account, Message message, Part part, String reason) { - messageViewFragment.hideAttachmentLoadingDialogOnMainThread(); - } - }); - } - - private void viewLocalAttachment() { - new ViewAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void saveLocalAttachmentTo(Uri documentUri) { - new SaveAttachmentAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, documentUri); - } - - private void writeAttachment(Uri documentUri) throws IOException { - ContentResolver contentResolver = context.getContentResolver(); - InputStream in = contentResolver.openInputStream(attachment.internalUri); - try { - OutputStream out = contentResolver.openOutputStream(documentUri, "wt"); - try { - IOUtils.copy(in, out); - out.flush(); - } finally { - out.close(); - } - } finally { - in.close(); - } - } - - @WorkerThread - private Intent getBestViewIntent() { - try { - Uri intentDataUri = AttachmentTempFileProvider.createTempUriForContentUri(context, attachment.internalUri, attachment.displayName); - - return viewIntentFinder.getBestViewIntent(intentDataUri, attachment.displayName, attachment.mimeType); - } catch (IOException e) { - Log.e(e, "Error creating temp file for attachment!"); - return null; - } - } - - private void displayAttachmentNotSavedMessage() { - String message = context.getString(R.string.message_view_status_attachment_not_saved); - displayMessageToUser(message); - } - - private void displayMessageToUser(String message) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } - - private class ViewAttachmentAsyncTask extends AsyncTask { - - @Override - protected Intent doInBackground(Void... params) { - return getBestViewIntent(); - } - - @Override - protected void onPostExecute(Intent intent) { - viewAttachment(intent); - } - - private void viewAttachment(Intent intent) { - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - Log.e(e, "Could not display attachment of type %s", attachment.mimeType); - - String message = context.getString(R.string.message_view_no_viewer, attachment.mimeType); - displayMessageToUser(message); - } - } - } - - private class SaveAttachmentAsyncTask extends AsyncTask { - - @Override - protected Boolean doInBackground(Uri... params) { - try { - Uri documentUri = params[0]; - writeAttachment(documentUri); - return true; - } catch (IOException e) { - Log.e(e, "Error saving attachment"); - return false; - } - } - - @Override - protected void onPostExecute(Boolean success) { - if (!success) { - displayAttachmentNotSavedMessage(); - } - } - } -} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.kt new file mode 100644 index 00000000000..a9254a6e645 --- /dev/null +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/AttachmentController.kt @@ -0,0 +1,188 @@ +package com.fsck.k9.ui.messageview + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.DocumentsContract +import android.widget.Toast +import androidx.annotation.WorkerThread +import app.k9mail.legacy.message.controller.SimpleMessagingListener +import com.fsck.k9.Preferences.Companion.getPreferences +import com.fsck.k9.controller.MessagingController +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.Part +import com.fsck.k9.mailstore.AttachmentViewInfo +import com.fsck.k9.mailstore.LocalPart +import com.fsck.k9.provider.AttachmentTempFileProvider +import com.fsck.k9.ui.R +import java.io.IOException +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import net.thunderbird.core.android.account.LegacyAccountDto +import net.thunderbird.core.logging.legacy.Log.e +import org.apache.commons.io.IOUtils + +class AttachmentController internal constructor( + private val context: Context, + private val controller: MessagingController, + private val messageViewFragment: MessageViewFragment, + private val attachment: AttachmentViewInfo, +) { + private val viewIntentFinder = ViewIntentFinder(context) + + fun viewAttachment(scope: CoroutineScope) { + scope.launch { + if (!attachment.isContentAvailable) { + val success = downloadAttachment() + if (success) { + messageViewFragment.refreshAttachmentThumbnail(attachment) + viewLocalAttachment() + } + } else { + viewLocalAttachment() + } + } + } + + fun saveAttachmentTo(scope: CoroutineScope, documentUri: Uri?) { + if (documentUri == null) return + scope.launch { + if (!attachment.isContentAvailable) { + val success = downloadAttachment() + if (success) { + messageViewFragment.refreshAttachmentThumbnail(attachment) + saveLocalAttachmentTo(documentUri) + } + } else { + saveLocalAttachmentTo(documentUri) + } + } + } + + fun saveAttachmentToDirectory(scope: CoroutineScope, directoryUri: Uri?) { + if (directoryUri == null) return + scope.launch { + if (!attachment.isContentAvailable) { + val success = downloadAttachment() + if (success) { + messageViewFragment.refreshAttachmentThumbnail(attachment) + saveLocalAttachmentToDirectory(directoryUri) + } + } else { + saveLocalAttachmentToDirectory(directoryUri) + } + } + } + + private suspend fun saveLocalAttachmentTo(documentUri: Uri) { + val success = withContext(Dispatchers.IO) { + try { + writeAttachment(documentUri) + true + } catch (e: IOException) { + e(e, "Error saving attachment") + false + } + } + if (!success) displayAttachmentNotSavedMessage() + } + + private suspend fun saveLocalAttachmentToDirectory(directoryUri: Uri) { + val success = withContext(Dispatchers.IO) { + try { + val contentResolver = context.contentResolver + val treeId = DocumentsContract.getTreeDocumentId(directoryUri) + val rootDocUri = DocumentsContract.buildDocumentUriUsingTree(directoryUri, treeId) + + val documentUri = DocumentsContract.createDocument( + contentResolver, + rootDocUri, + attachment.mimeType, + attachment.displayName + ) ?: return@withContext false + + writeAttachment(documentUri) + true + } catch (e: IOException) { + e(e, "Error saving attachment to directory") + false + } + } + if (!success) displayAttachmentNotSavedMessage() + } + + private suspend fun downloadAttachment(): Boolean = suspendCancellableCoroutine { continuation -> + val localPart = attachment.part as LocalPart + val account = getPreferences().getAccount(localPart.accountUuid) + + messageViewFragment.showAttachmentLoadingDialog() + controller.loadAttachment(account, localPart.message, attachment.part, + object : SimpleMessagingListener() { + override fun loadAttachmentFinished(account: LegacyAccountDto?, message: Message?, part: Part?) { + attachment.setContentAvailable() + messageViewFragment.hideAttachmentLoadingDialogOnMainThread() + if (continuation.isActive) { + continuation.resume(true) + } + } + + override fun loadAttachmentFailed(account: LegacyAccountDto?, message: Message?, part: Part?, reason: String?) { + messageViewFragment.hideAttachmentLoadingDialogOnMainThread() + if (continuation.isActive) { + continuation.resume(false) + } + } + }) + } + + private suspend fun viewLocalAttachment() { + val intent = withContext(Dispatchers.IO) { getBestViewIntent() } + if (intent != null) { + try { + context.startActivity(intent) + } catch (_: ActivityNotFoundException) { + val errorMsg = context.getString(R.string.message_view_no_viewer, attachment.mimeType) + displayMessageToUser(errorMsg) + } + } + } + + @Throws(IOException::class) + private fun writeAttachment(documentUri: Uri) { + val contentResolver = context.contentResolver + contentResolver.openInputStream(attachment.internalUri).use { inputStream -> + contentResolver.openOutputStream(documentUri, "wt").use { outputStream -> + if (inputStream != null && outputStream != null) { + IOUtils.copy(inputStream, outputStream) + outputStream.flush() + } + } + } + } + + @WorkerThread + private fun getBestViewIntent(): Intent? { + return try { + val intentDataUri = AttachmentTempFileProvider.createTempUriForContentUri( + context, attachment.internalUri, attachment.displayName + ) + viewIntentFinder.getBestViewIntent(intentDataUri, attachment.displayName, attachment.mimeType) + } catch (e: IOException) { + e(e, "Error creating temp file for attachment!") + null + } + } + + private fun displayAttachmentNotSavedMessage() { + displayMessageToUser(context.getString(R.string.message_view_status_attachment_not_saved)) + } + + private fun displayMessageToUser(message: String?) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } +} diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt index 45d7db9a624..256a2e47e99 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/MessageViewFragment.kt @@ -20,6 +20,7 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView @@ -110,6 +111,10 @@ class MessageViewFragment : registerForActivityResult(CreateDocumentResultContract()) { documentUri -> onCreateDocumentResult(documentUri) } + private val openDocumentTreeLauncher: ActivityResultLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { directoryUri -> + onOpenDocumentTreeResult(directoryUri) + } private val chooseFolderForCopyLauncher: ActivityResultLauncher = registerForActivityResult(ChooseFolderResultContract(ChooseFolderActivity.Action.COPY)) { result -> onChooseFolderCopyResult(result) @@ -236,11 +241,7 @@ class MessageViewFragment : onSaveClick = { attachment -> onSaveAttachment(attachment) }, - onSaveAllClick = { - attachments.forEach { item -> - onSaveAttachment(item.attachment) - } - }, + onSaveAllClick = { onSaveAllAttachments() }, ) } } @@ -813,7 +814,21 @@ class MessageViewFragment : return } - createAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(uri) + currentAttachmentViewInfo?.let { + createAttachmentController(it).saveAttachmentTo(viewLifecycleOwner.lifecycleScope, uri) + } + } + + private fun onOpenDocumentTreeResult(directoryUri: Uri?) { + if (directoryUri == null) return + + val messageView = mMessageViewInfo ?: return + val attachments = messageView.attachments.filter { !it.inlineAttachment } + attachments.forEach { + currentAttachmentViewInfo = it + createAttachmentController(it) + .saveAttachmentToDirectory(viewLifecycleOwner.lifecycleScope ,directoryUri) + } } private fun onChooseFolderMoveResult(result: ChooseFolderResultContract.Result?) { @@ -1175,7 +1190,15 @@ class MessageViewFragment : override fun onViewAttachment(attachment: AttachmentViewInfo) { currentAttachmentViewInfo = attachment - createAttachmentController(attachment).viewAttachment() + createAttachmentController(attachment).viewAttachment(viewLifecycleOwner.lifecycleScope) + } + + fun onSaveAllAttachments() { + try { + openDocumentTreeLauncher.launch(null) + } catch (_: ActivityNotFoundException) { + Toast.makeText(requireContext(), R.string.error_activity_not_found, Toast.LENGTH_LONG).show() + } } override fun onSaveAttachment(attachment: AttachmentViewInfo) { @@ -1193,7 +1216,7 @@ class MessageViewFragment : } } - private fun createAttachmentController(attachment: AttachmentViewInfo?): AttachmentController { + private fun createAttachmentController(attachment: AttachmentViewInfo): AttachmentController { return AttachmentController(requireContext(), messagingController, this, attachment) }