From 23d172d00c7be234b7c6ea8d497cd6f554df673e Mon Sep 17 00:00:00 2001 From: Adityarup Laha <30696515+adityaruplaha@users.noreply.github.com> Date: Mon, 30 Dec 2024 02:53:58 +0530 Subject: [PATCH 1/3] Implement sharing file attachments on long-press. --- .../itemdetails/ItemDetailsViewModel.kt | 38 +++++++++++++++++++ .../bottomsheet/LongPressOptionItem.kt | 5 +++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 44 insertions(+) diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt index 9d5e1673..5b90cabe 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt @@ -1,8 +1,10 @@ package org.zotero.android.screens.itemdetails import android.content.Context +import android.content.Intent import android.net.Uri import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider.getUriForFile import androidx.core.net.toFile import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle @@ -1427,6 +1429,9 @@ class ItemDetailsViewModel @Inject constructor( is LongPressOptionItem.MoveToTrashAttachment -> { delete(longPressOptionItem.attachment) } + is LongPressOptionItem.ShareAttachmentFile -> { + shareFile(longPressOptionItem.attachment) + } is LongPressOptionItem.DeleteAttachmentFile -> { deleteFile(longPressOptionItem.attachment) } @@ -1474,6 +1479,7 @@ class ItemDetailsViewModel @Inject constructor( val actions = mutableListOf() val attachmentType = attachment.type if (attachmentType is Attachment.Kind.file && attachmentType.location == Attachment.FileLocation.local) { + actions.add(LongPressOptionItem.ShareAttachmentFile(attachment)) actions.add(LongPressOptionItem.DeleteAttachmentFile(attachment)) } @@ -1560,6 +1566,38 @@ class ItemDetailsViewModel @Inject constructor( } } + private fun shareFile(attachment: Attachment) { + when(val attachmentType = attachment.type) { + is Attachment.Kind.url -> { + attachmentType.url + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, attachmentType.url) + type = "text/plain" + } + val chooserIntent = Intent.createChooser(shareIntent, null) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(chooserIntent) + } + is Attachment.Kind.file -> { + val file = fileStore.attachmentFile( + libraryId = attachment.libraryId, + key = attachment.key, + filename = attachmentType.filename, + ) + val uri = getUriForFile(context, context.packageName + ".provider", file) + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = attachmentType.contentType + } + val chooserIntent = Intent.createChooser(shareIntent, null) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(chooserIntent) + } + } + } + private fun deleteFile(attachment: Attachment) { this.fileCleanupController.delete( AttachmentFileCleanupController.DeletionType.individual( diff --git a/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt b/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt index 922e61f3..c64ae28c 100644 --- a/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt +++ b/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt @@ -36,6 +36,11 @@ sealed class LongPressOptionItem( resIcon = Drawables.delete_24px ) + data class ShareAttachmentFile(val attachment: Attachment): LongPressOptionItem( + titleId = Strings.item_detail_share_attachment_file, + resIcon = Drawables.delete_24px + ) + data class DeleteAttachmentFile(val attachment: Attachment): LongPressOptionItem( titleId = Strings.item_detail_delete_attachment_file, resIcon = Drawables.delete_24px diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ab05615..493cc707 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,5 +36,6 @@ Remove Downloads Empty Trash + Share Download \ No newline at end of file From 70182ad7ab93147d2de2bdd92f0d2a14ad93643c Mon Sep 17 00:00:00 2001 From: Adityarup Laha <30696515+adityaruplaha@users.noreply.github.com> Date: Mon, 30 Dec 2024 03:04:34 +0530 Subject: [PATCH 2/3] Add a new sharing icon. --- .../android/uicomponents/bottomsheet/LongPressOptionItem.kt | 2 +- app/src/main/res/drawable/baseline_share_24.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/baseline_share_24.xml diff --git a/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt b/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt index c64ae28c..c7374cdf 100644 --- a/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt +++ b/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt @@ -38,7 +38,7 @@ sealed class LongPressOptionItem( data class ShareAttachmentFile(val attachment: Attachment): LongPressOptionItem( titleId = Strings.item_detail_share_attachment_file, - resIcon = Drawables.delete_24px + resIcon = Drawables.baseline_share_24 ) data class DeleteAttachmentFile(val attachment: Attachment): LongPressOptionItem( diff --git a/app/src/main/res/drawable/baseline_share_24.xml b/app/src/main/res/drawable/baseline_share_24.xml new file mode 100644 index 00000000..74753b7a --- /dev/null +++ b/app/src/main/res/drawable/baseline_share_24.xml @@ -0,0 +1,5 @@ + + + + + From b76feffd075e71cfadcde739ec31187a47a81cad Mon Sep 17 00:00:00 2001 From: Adityarup Laha <30696515+adityaruplaha@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:21:59 +0530 Subject: [PATCH 3/3] Implement sharing all attachments for an item. Refactor sharing into AttachmentFileShareController. Minor fixups. --- .../screens/allitems/AllItemsViewModel.kt | 4 + .../allitems/processor/AllItemsProcessor.kt | 11 ++ .../itemdetails/ItemDetailsViewModel.kt | 34 +--- .../sync/AttachmentFileShareController.kt | 165 ++++++++++++++++++ .../bottomsheet/LongPressOptionItem.kt | 6 + app/src/main/res/values/imported_strings.xml | 4 +- app/src/main/res/values/strings.xml | 1 - 7 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/org/zotero/android/sync/AttachmentFileShareController.kt diff --git a/app/src/main/java/org/zotero/android/screens/allitems/AllItemsViewModel.kt b/app/src/main/java/org/zotero/android/screens/allitems/AllItemsViewModel.kt index 0cf96e5c..8f56996c 100644 --- a/app/src/main/java/org/zotero/android/screens/allitems/AllItemsViewModel.kt +++ b/app/src/main/java/org/zotero/android/screens/allitems/AllItemsViewModel.kt @@ -677,6 +677,9 @@ internal class AllItemsViewModel @Inject constructor( is LongPressOptionItem.Download -> { allItemsProcessor.downloadAttachments(setOf(longPressOptionItem.item.key)) } + is LongPressOptionItem.ShareDownload -> { + allItemsProcessor.shareDownloads(setOf(longPressOptionItem.item.key)) + } is LongPressOptionItem.RemoveDownload -> { allItemsProcessor.removeDownloads(setOf(longPressOptionItem.item.key)) } @@ -767,6 +770,7 @@ internal class AllItemsViewModel @Inject constructor( when(location) { Attachment.FileLocation.local -> { actions.add(LongPressOptionItem.RemoveDownload(item)) + actions.add(LongPressOptionItem.ShareDownload(item)) } Attachment.FileLocation.remote -> { actions.add(LongPressOptionItem.Download(item)) diff --git a/app/src/main/java/org/zotero/android/screens/allitems/processor/AllItemsProcessor.kt b/app/src/main/java/org/zotero/android/screens/allitems/processor/AllItemsProcessor.kt index 0c63ffcc..fbd3da57 100644 --- a/app/src/main/java/org/zotero/android/screens/allitems/processor/AllItemsProcessor.kt +++ b/app/src/main/java/org/zotero/android/screens/allitems/processor/AllItemsProcessor.kt @@ -40,6 +40,7 @@ import org.zotero.android.screens.sortpicker.data.SortDirectionResult import org.zotero.android.sync.AttachmentCreator import org.zotero.android.sync.AttachmentFileCleanupController import org.zotero.android.sync.AttachmentFileDeletedNotification +import org.zotero.android.sync.AttachmentFileShareController import org.zotero.android.sync.Collection import org.zotero.android.sync.CollectionIdentifier import org.zotero.android.sync.Library @@ -56,6 +57,7 @@ class AllItemsProcessor @Inject constructor( private val dbWrapperMain: DbWrapperMain, private val attachmentDownloaderEventStream: AttachmentDownloaderEventStream, private val fileDownloader: AttachmentDownloader, + private val fileShareController: AttachmentFileShareController, private val fileCleanupController: AttachmentFileCleanupController, ) { private lateinit var processorInterface: AllItemsProcessorInterface @@ -659,6 +661,15 @@ class AllItemsProcessor @Inject constructor( } } + internal fun shareDownloads(ids: Set) { + this.fileShareController.share( + AttachmentFileShareController.ShareType.allForItems( + keys = ids, + libraryId = this.library.identifier + ) + ) + } + internal fun removeDownloads(ids: Set) { this.fileCleanupController.delete( AttachmentFileCleanupController.DeletionType.allForItems( diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt index 5b90cabe..629dacd1 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt @@ -1,10 +1,8 @@ package org.zotero.android.screens.itemdetails import android.content.Context -import android.content.Intent import android.net.Uri import android.webkit.MimeTypeMap -import androidx.core.content.FileProvider.getUriForFile import androidx.core.net.toFile import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle @@ -99,6 +97,7 @@ import org.zotero.android.screens.tagpicker.data.TagPickerArgs import org.zotero.android.screens.tagpicker.data.TagPickerResult import org.zotero.android.sync.AttachmentFileCleanupController import org.zotero.android.sync.AttachmentFileDeletedNotification +import org.zotero.android.sync.AttachmentFileShareController import org.zotero.android.sync.DateParser import org.zotero.android.sync.ItemDetailCreateDataResult import org.zotero.android.sync.ItemDetailDataCreator @@ -139,6 +138,7 @@ class ItemDetailsViewModel @Inject constructor( private val fileDownloader: AttachmentDownloader, private val attachmentDownloaderEventStream: AttachmentDownloaderEventStream, private val getUriDetailsUseCase: GetUriDetailsUseCase, + private val attachmentFileShareController: AttachmentFileShareController, private val fileCleanupController: AttachmentFileCleanupController, private val conflictResolutionUseCase: ConflictResolutionUseCase, private val dateParser: DateParser, @@ -1567,35 +1567,7 @@ class ItemDetailsViewModel @Inject constructor( } private fun shareFile(attachment: Attachment) { - when(val attachmentType = attachment.type) { - is Attachment.Kind.url -> { - attachmentType.url - val shareIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, attachmentType.url) - type = "text/plain" - } - val chooserIntent = Intent.createChooser(shareIntent, null) - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(chooserIntent) - } - is Attachment.Kind.file -> { - val file = fileStore.attachmentFile( - libraryId = attachment.libraryId, - key = attachment.key, - filename = attachmentType.filename, - ) - val uri = getUriForFile(context, context.packageName + ".provider", file) - val shareIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_STREAM, uri) - type = attachmentType.contentType - } - val chooserIntent = Intent.createChooser(shareIntent, null) - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(chooserIntent) - } - } + this.attachmentFileShareController.share(attachment) } private fun deleteFile(attachment: Attachment) { diff --git a/app/src/main/java/org/zotero/android/sync/AttachmentFileShareController.kt b/app/src/main/java/org/zotero/android/sync/AttachmentFileShareController.kt new file mode 100644 index 00000000..f33bb123 --- /dev/null +++ b/app/src/main/java/org/zotero/android/sync/AttachmentFileShareController.kt @@ -0,0 +1,165 @@ +package org.zotero.android.sync + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.core.content.FileProvider.getUriForFile +import org.zotero.android.database.DbWrapperMain +import org.zotero.android.database.objects.Attachment +import org.zotero.android.database.objects.ItemTypes +import org.zotero.android.database.requests.ReadItemsWithKeysDbRequest +import org.zotero.android.database.requests.item +import org.zotero.android.files.FileStore +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AttachmentFileShareController @Inject constructor( + private val fileStore: FileStore, + private val dbWrapperMain: DbWrapperMain, + private val context: Context +) { + sealed class ShareType { + data class individual(val attachment: Attachment, val parentKey: String?) : ShareType() + data class allForItems(val keys: Set, val libraryId: LibraryIdentifier) : + ShareType() + + data class library(val libraryId: LibraryIdentifier) : ShareType() + object all : ShareType() + } + + fun share(type: ShareType): List { + return when (type) { + is ShareType.allForItems -> { + shareDownloadedAttachments( + type.keys, + libraryId = type.libraryId + )?.let { listOf(it) } + ?: emptyList() + } + is ShareType.individual -> { + return if (share(attachment = type.attachment)) listOf(type) else emptyList() + } + else -> { + // TODO: Implement the other two sharing types + return emptyList() + } + } + } + + fun share(attachment: Attachment): Boolean { + try { + when(val attachmentType = attachment.type) { + is Attachment.Kind.url -> { + // This is currently unused, but is implemented anyway + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, attachmentType.url) + type = "text/plain" + } + val chooserIntent = Intent.createChooser(shareIntent, null) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(chooserIntent) + } + is Attachment.Kind.file -> { + val file = fileStore.attachmentFile( + libraryId = attachment.libraryId, + key = attachment.key, + filename = attachmentType.filename, + ) + val uri = getUriForFile(context, context.packageName + ".provider", file) + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = attachmentType.contentType + } + val chooserIntent = Intent.createChooser(shareIntent, null) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(chooserIntent) + } + } + } catch (error: Exception) { + Timber.e(error, "AttachmentFileShareController: can't share attachments for item") + return false + } + return true + } + + private fun shareDownloadedAttachments( + keys: Set, + libraryId: LibraryIdentifier + ): ShareType? { + if (keys.isEmpty()) { + return null + } + try { + val toShare = mutableSetOf() + + dbWrapperMain.realmDbStorage.perform { coordinator -> + val items = coordinator.perform( + request = ReadItemsWithKeysDbRequest( + keys = keys, + libraryId = libraryId + ) + ) + + for (item in items) { + if (item.rawType == ItemTypes.attachment) { + if (!item.fileDownloaded) continue // Attachment is not downloaded, skip + toShare.add(item.key) + continue + } + + // Or the item was a parent item and it may have multiple attachments + for (child in item.children!!.where().item(type = ItemTypes.attachment) + .findAll()) { + if (!child.fileDownloaded) continue // Attachment is not downloaded, skip + toShare.add(child.key) + } + } + + coordinator.invalidate() + } + + shareFiles(toShare, libraryId = libraryId) + + return if (toShare.isEmpty()) { + null + } else { + ShareType.allForItems( + keys = toShare, + libraryId = libraryId + ) + } + } catch (error: Exception) { + Timber.e(error, "AttachmentFileShareController: can't share attachments for item") + return null + } + } + + private fun shareFiles(keys: Iterable, libraryId: LibraryIdentifier) { + val uris = mutableListOf() + for (key in keys) { + val dir = fileStore.attachmentDirectory(libraryId, key) + val files = dir.listFiles() + if (files == null) { + Timber.e("AttachmentFileShareController: argument to shareFiles is not a directory: %s", dir) + continue + } + for (file in files) { + val uri = getUriForFile(context, context.packageName + ".provider", file) + uris.add(uri) + } + } + if (uris.size == 0) return + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND_MULTIPLE + putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris)) + type = "application/octet-stream" + } + val chooserIntent = Intent.createChooser(shareIntent, null) + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(chooserIntent) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt b/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt index c7374cdf..800a88ab 100644 --- a/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt +++ b/app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt @@ -62,6 +62,12 @@ sealed class LongPressOptionItem( textAndIconColor = CustomPalette.ErrorRed, resIcon = Drawables.delete_24px ) + + data class ShareDownload(val item: RItem): LongPressOptionItem( + titleId = Strings.items_action_share_download, + resIcon = Drawables.baseline_share_24 + ) + data class RemoveDownload(val item: RItem): LongPressOptionItem( titleId = Strings.items_action_remove_download, resIcon = Drawables.delete_24px diff --git a/app/src/main/res/values/imported_strings.xml b/app/src/main/res/values/imported_strings.xml index a9120120..272a115c 100644 --- a/app/src/main/res/values/imported_strings.xml +++ b/app/src/main/res/values/imported_strings.xml @@ -120,7 +120,8 @@ Duplicate Create Parent Item Download - Remove Download + Share Downloaded Attachments + Remove Downloaded Attachments Filters Downloaded Files Tags @@ -174,6 +175,7 @@ Show less View PDF This item has been changed remotely. It will now reload. + Share Download Remove Download Deleted This item has been deleted. Do you want to restore it? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 493cc707..5ab05615 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,6 +36,5 @@ Remove Downloads Empty Trash - Share Download \ No newline at end of file