Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to share downloaded attachments. #205

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -659,6 +661,15 @@ class AllItemsProcessor @Inject constructor(
}
}

internal fun shareDownloads(ids: Set<String>) {
this.fileShareController.share(
AttachmentFileShareController.ShareType.allForItems(
keys = ids,
libraryId = this.library.identifier
)
)
}

internal fun removeDownloads(ids: Set<String>) {
this.fileCleanupController.delete(
AttachmentFileCleanupController.DeletionType.allForItems(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,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
Expand Down Expand Up @@ -137,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,
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -1474,6 +1479,7 @@ class ItemDetailsViewModel @Inject constructor(
val actions = mutableListOf<LongPressOptionItem>()
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))
}

Expand Down Expand Up @@ -1560,6 +1566,10 @@ class ItemDetailsViewModel @Inject constructor(
}
}

private fun shareFile(attachment: Attachment) {
this.attachmentFileShareController.share(attachment)
}

private fun deleteFile(attachment: Attachment) {
this.fileCleanupController.delete(
AttachmentFileCleanupController.DeletionType.individual(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>, val libraryId: LibraryIdentifier) :
ShareType()

data class library(val libraryId: LibraryIdentifier) : ShareType()
object all : ShareType()
}

fun share(type: ShareType): List<ShareType> {
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<String>,
libraryId: LibraryIdentifier
): ShareType? {
if (keys.isEmpty()) {
return null
}
try {
val toShare = mutableSetOf<String>()

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<String>, libraryId: LibraryIdentifier) {
val uris = mutableListOf<Uri>()
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.baseline_share_24
)

data class DeleteAttachmentFile(val attachment: Attachment): LongPressOptionItem(
titleId = Strings.item_detail_delete_attachment_file,
resIcon = Drawables.delete_24px
Expand All @@ -57,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
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/baseline_share_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">

<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>

</vector>
4 changes: 3 additions & 1 deletion app/src/main/res/values/imported_strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@
<string name="items.action.duplicate">Duplicate</string>
<string name="items.action.create_parent">Create Parent Item</string>
<string name="items.action.download">Download</string>
<string name="items.action.remove_download">Remove Download</string>
<string name="items.action.share_download">Share Downloaded Attachments</string>
<string name="items.action.remove_download">Remove Downloaded Attachments</string>
<string name="items.filters.title">Filters</string>
<string name="items.filters.downloads">Downloaded Files</string>
<string name="items.filters.tags">Tags</string>
Expand Down Expand Up @@ -174,6 +175,7 @@
<string name="item_detail.show_less">Show less</string>
<string name="item_detail.view_pdf">View PDF</string>
<string name="item_detail.data_reloaded">This item has been changed remotely. It will now reload.</string>
<string name="item_detail.share_attachment_file">Share Download</string>
<string name="item_detail.delete_attachment_file">Remove Download</string>
<string name="item_detail.deleted_title">Deleted</string>
<string name="item_detail.deleted_message">This item has been deleted. Do you want to restore it?</string>
Expand Down