Skip to content

Commit b893439

Browse files
committed
Implement sharing all attachments for an item.
Refactor sharing into AttachmentFileShareController. Minor fixups.
1 parent 2c4163e commit b893439

File tree

7 files changed

+192
-33
lines changed

7 files changed

+192
-33
lines changed

app/src/main/java/org/zotero/android/screens/allitems/AllItemsViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@ internal class AllItemsViewModel @Inject constructor(
676676
is LongPressOptionItem.Download -> {
677677
allItemsProcessor.downloadAttachments(setOf(longPressOptionItem.item.key))
678678
}
679+
is LongPressOptionItem.ShareDownload -> {
680+
allItemsProcessor.shareDownloads(setOf(longPressOptionItem.item.key))
681+
}
679682
is LongPressOptionItem.RemoveDownload -> {
680683
allItemsProcessor.removeDownloads(setOf(longPressOptionItem.item.key))
681684
}
@@ -790,6 +793,7 @@ internal class AllItemsViewModel @Inject constructor(
790793
when(location) {
791794
Attachment.FileLocation.local -> {
792795
actions.add(LongPressOptionItem.RemoveDownload(item))
796+
actions.add(LongPressOptionItem.ShareDownload(item))
793797
}
794798
Attachment.FileLocation.remote -> {
795799
actions.add(LongPressOptionItem.Download(item))

app/src/main/java/org/zotero/android/screens/allitems/processor/AllItemsProcessor.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import org.zotero.android.screens.sortpicker.data.SortDirectionResult
4040
import org.zotero.android.sync.AttachmentCreator
4141
import org.zotero.android.sync.AttachmentFileCleanupController
4242
import org.zotero.android.sync.AttachmentFileDeletedNotification
43+
import org.zotero.android.sync.AttachmentFileShareController
4344
import org.zotero.android.sync.Collection
4445
import org.zotero.android.sync.CollectionIdentifier
4546
import org.zotero.android.sync.Library
@@ -56,6 +57,7 @@ class AllItemsProcessor @Inject constructor(
5657
private val dbWrapperMain: DbWrapperMain,
5758
private val attachmentDownloaderEventStream: AttachmentDownloaderEventStream,
5859
private val fileDownloader: AttachmentDownloader,
60+
private val fileShareController: AttachmentFileShareController,
5961
private val fileCleanupController: AttachmentFileCleanupController,
6062
) {
6163
private lateinit var processorInterface: AllItemsProcessorInterface
@@ -659,6 +661,15 @@ class AllItemsProcessor @Inject constructor(
659661
}
660662
}
661663

664+
internal fun shareDownloads(ids: Set<String>) {
665+
this.fileShareController.share(
666+
AttachmentFileShareController.ShareType.allForItems(
667+
keys = ids,
668+
libraryId = this.library.identifier
669+
)
670+
)
671+
}
672+
662673
internal fun removeDownloads(ids: Set<String>) {
663674
this.fileCleanupController.delete(
664675
AttachmentFileCleanupController.DeletionType.allForItems(

app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package org.zotero.android.screens.itemdetails
22

33
import android.content.Context
4-
import android.content.Intent
54
import android.net.Uri
65
import android.webkit.MimeTypeMap
7-
import androidx.core.content.FileProvider.getUriForFile
86
import androidx.core.net.toFile
97
import androidx.core.net.toUri
108
import androidx.lifecycle.SavedStateHandle
@@ -99,6 +97,7 @@ import org.zotero.android.screens.tagpicker.data.TagPickerArgs
9997
import org.zotero.android.screens.tagpicker.data.TagPickerResult
10098
import org.zotero.android.sync.AttachmentFileCleanupController
10199
import org.zotero.android.sync.AttachmentFileDeletedNotification
100+
import org.zotero.android.sync.AttachmentFileShareController
102101
import org.zotero.android.sync.DateParser
103102
import org.zotero.android.sync.ItemDetailCreateDataResult
104103
import org.zotero.android.sync.ItemDetailDataCreator
@@ -139,6 +138,7 @@ class ItemDetailsViewModel @Inject constructor(
139138
private val fileDownloader: AttachmentDownloader,
140139
private val attachmentDownloaderEventStream: AttachmentDownloaderEventStream,
141140
private val getUriDetailsUseCase: GetUriDetailsUseCase,
141+
private val attachmentFileShareController: AttachmentFileShareController,
142142
private val fileCleanupController: AttachmentFileCleanupController,
143143
private val conflictResolutionUseCase: ConflictResolutionUseCase,
144144
private val dateParser: DateParser,
@@ -1570,35 +1570,7 @@ class ItemDetailsViewModel @Inject constructor(
15701570
}
15711571

15721572
private fun shareFile(attachment: Attachment) {
1573-
when(val attachmentType = attachment.type) {
1574-
is Attachment.Kind.url -> {
1575-
attachmentType.url
1576-
val shareIntent: Intent = Intent().apply {
1577-
action = Intent.ACTION_SEND
1578-
putExtra(Intent.EXTRA_TEXT, attachmentType.url)
1579-
type = "text/plain"
1580-
}
1581-
val chooserIntent = Intent.createChooser(shareIntent, null)
1582-
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1583-
context.startActivity(chooserIntent)
1584-
}
1585-
is Attachment.Kind.file -> {
1586-
val file = fileStore.attachmentFile(
1587-
libraryId = attachment.libraryId,
1588-
key = attachment.key,
1589-
filename = attachmentType.filename,
1590-
)
1591-
val uri = getUriForFile(context, context.packageName + ".provider", file)
1592-
val shareIntent: Intent = Intent().apply {
1593-
action = Intent.ACTION_SEND
1594-
putExtra(Intent.EXTRA_STREAM, uri)
1595-
type = attachmentType.contentType
1596-
}
1597-
val chooserIntent = Intent.createChooser(shareIntent, null)
1598-
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1599-
context.startActivity(chooserIntent)
1600-
}
1601-
}
1573+
this.attachmentFileShareController.share(attachment)
16021574
}
16031575

16041576
private fun deleteFile(attachment: Attachment) {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package org.zotero.android.sync
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.net.Uri
6+
import androidx.core.content.FileProvider.getUriForFile
7+
import org.zotero.android.database.DbWrapperMain
8+
import org.zotero.android.database.objects.Attachment
9+
import org.zotero.android.database.objects.ItemTypes
10+
import org.zotero.android.database.requests.ReadItemsWithKeysDbRequest
11+
import org.zotero.android.database.requests.item
12+
import org.zotero.android.files.FileStore
13+
import timber.log.Timber
14+
import javax.inject.Inject
15+
import javax.inject.Singleton
16+
17+
@Singleton
18+
class AttachmentFileShareController @Inject constructor(
19+
private val fileStore: FileStore,
20+
private val dbWrapperMain: DbWrapperMain,
21+
private val context: Context
22+
) {
23+
sealed class ShareType {
24+
data class individual(val attachment: Attachment, val parentKey: String?) : ShareType()
25+
data class allForItems(val keys: Set<String>, val libraryId: LibraryIdentifier) :
26+
ShareType()
27+
28+
data class library(val libraryId: LibraryIdentifier) : ShareType()
29+
object all : ShareType()
30+
}
31+
32+
fun share(type: ShareType): List<ShareType> {
33+
return when (type) {
34+
is ShareType.allForItems -> {
35+
shareDownloadedAttachments(
36+
type.keys,
37+
libraryId = type.libraryId
38+
)?.let { listOf(it) }
39+
?: emptyList()
40+
}
41+
is ShareType.individual -> {
42+
return if (share(attachment = type.attachment)) listOf(type) else emptyList()
43+
}
44+
else -> {
45+
// TODO: Implement the other two sharing types
46+
return emptyList()
47+
}
48+
}
49+
}
50+
51+
fun share(attachment: Attachment): Boolean {
52+
try {
53+
when(val attachmentType = attachment.type) {
54+
is Attachment.Kind.url -> {
55+
// This is currently unused, but is implemented anyway
56+
val shareIntent: Intent = Intent().apply {
57+
action = Intent.ACTION_SEND
58+
putExtra(Intent.EXTRA_TEXT, attachmentType.url)
59+
type = "text/plain"
60+
}
61+
val chooserIntent = Intent.createChooser(shareIntent, null)
62+
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
63+
context.startActivity(chooserIntent)
64+
}
65+
is Attachment.Kind.file -> {
66+
val file = fileStore.attachmentFile(
67+
libraryId = attachment.libraryId,
68+
key = attachment.key,
69+
filename = attachmentType.filename,
70+
)
71+
val uri = getUriForFile(context, context.packageName + ".provider", file)
72+
val shareIntent: Intent = Intent().apply {
73+
action = Intent.ACTION_SEND
74+
putExtra(Intent.EXTRA_STREAM, uri)
75+
type = attachmentType.contentType
76+
}
77+
val chooserIntent = Intent.createChooser(shareIntent, null)
78+
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
79+
context.startActivity(chooserIntent)
80+
}
81+
}
82+
} catch (error: Exception) {
83+
Timber.e(error, "AttachmentFileShareController: can't share attachments for item")
84+
return false
85+
}
86+
return true
87+
}
88+
89+
private fun shareDownloadedAttachments(
90+
keys: Set<String>,
91+
libraryId: LibraryIdentifier
92+
): ShareType? {
93+
if (keys.isEmpty()) {
94+
return null
95+
}
96+
try {
97+
val toShare = mutableSetOf<String>()
98+
99+
dbWrapperMain.realmDbStorage.perform { coordinator ->
100+
val items = coordinator.perform(
101+
request = ReadItemsWithKeysDbRequest(
102+
keys = keys,
103+
libraryId = libraryId
104+
)
105+
)
106+
107+
for (item in items) {
108+
if (item.rawType == ItemTypes.attachment) {
109+
if (!item.fileDownloaded) continue // Attachment is not downloaded, skip
110+
toShare.add(item.key)
111+
continue
112+
}
113+
114+
// Or the item was a parent item and it may have multiple attachments
115+
for (child in item.children!!.where().item(type = ItemTypes.attachment)
116+
.findAll()) {
117+
if (!child.fileDownloaded) continue // Attachment is not downloaded, skip
118+
toShare.add(child.key)
119+
}
120+
}
121+
122+
coordinator.invalidate()
123+
}
124+
125+
shareFiles(toShare, libraryId = libraryId)
126+
127+
return if (toShare.isEmpty()) {
128+
null
129+
} else {
130+
ShareType.allForItems(
131+
keys = toShare,
132+
libraryId = libraryId
133+
)
134+
}
135+
} catch (error: Exception) {
136+
Timber.e(error, "AttachmentFileShareController: can't share attachments for item")
137+
return null
138+
}
139+
}
140+
141+
private fun shareFiles(keys: Iterable<String>, libraryId: LibraryIdentifier) {
142+
val uris = mutableListOf<Uri>()
143+
for (key in keys) {
144+
val dir = fileStore.attachmentDirectory(libraryId, key)
145+
val files = dir.listFiles()
146+
if (files == null) {
147+
Timber.e("AttachmentFileShareController: argument to shareFiles is not a directory: %s", dir)
148+
continue
149+
}
150+
for (file in files) {
151+
val uri = getUriForFile(context, context.packageName + ".provider", file)
152+
uris.add(uri)
153+
}
154+
}
155+
if (uris.size == 0) return
156+
val shareIntent = Intent().apply {
157+
action = Intent.ACTION_SEND_MULTIPLE
158+
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
159+
type = "application/octet-stream"
160+
}
161+
val chooserIntent = Intent.createChooser(shareIntent, null)
162+
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
163+
context.startActivity(chooserIntent)
164+
}
165+
}

app/src/main/java/org/zotero/android/uicomponents/bottomsheet/LongPressOptionItem.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ sealed class LongPressOptionItem(
6262
textAndIconColor = CustomPalette.ErrorRed,
6363
resIcon = Drawables.delete_24px
6464
)
65+
66+
data class ShareDownload(val item: RItem): LongPressOptionItem(
67+
titleId = Strings.items_action_share_download,
68+
resIcon = Drawables.baseline_share_24
69+
)
70+
6571
data class RemoveDownload(val item: RItem): LongPressOptionItem(
6672
titleId = Strings.items_action_remove_download,
6773
resIcon = Drawables.file_download_off_24px

app/src/main/res/values/imported_strings.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@
120120
<string name="items.action.duplicate">Duplicate</string>
121121
<string name="items.action.create_parent">Create Parent Item</string>
122122
<string name="items.action.download">Download</string>
123-
<string name="items.action.remove_download">Remove Download</string>
123+
<string name="items.action.share_download">Share Downloaded Attachments</string>
124+
<string name="items.action.remove_download">Remove Downloaded Attachments</string>
124125
<string name="items.filters.title">Filters</string>
125126
<string name="items.filters.downloads">Downloaded Files</string>
126127
<string name="items.filters.tags">Tags</string>
@@ -174,6 +175,7 @@
174175
<string name="item_detail.show_less">Show less</string>
175176
<string name="item_detail.view_pdf">View PDF</string>
176177
<string name="item_detail.data_reloaded">This item has been changed remotely. It will now reload.</string>
178+
<string name="item_detail.share_attachment_file">Share Download</string>
177179
<string name="item_detail.delete_attachment_file">Remove Download</string>
178180
<string name="item_detail.deleted_title">Deleted</string>
179181
<string name="item_detail.deleted_message">This item has been deleted. Do you want to restore it?</string>

app/src/main/res/values/strings.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
<string name="collection_remove_downloads">Remove Downloads</string>
3838
<string name="collection_empty_trash">Empty Trash</string>
39-
<string name="item_detail.share_attachment_file">Share Download</string>
4039

4140
<string name="items.action.retrieve_metadata">Retrieve Metadata</string>
4241

0 commit comments

Comments
 (0)