vault: fixed export to vault
This commit is contained in:
parent
cbf555a31f
commit
d96a067f18
6 changed files with 157 additions and 86 deletions
|
@ -92,19 +92,6 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
}
|
||||
|
||||
private suspend fun delete() {
|
||||
if (entryMapList.isEmpty()) {
|
||||
endOfStream()
|
||||
return
|
||||
}
|
||||
|
||||
// assume same provider for all entries
|
||||
val firstEntry = entryMapList.first()
|
||||
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
|
||||
if (provider == null) {
|
||||
error("delete-provider", "failed to find provider for entry=$firstEntry", null)
|
||||
return
|
||||
}
|
||||
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
for (entry in entries) {
|
||||
val mimeType = entry.mimeType
|
||||
|
@ -119,12 +106,14 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
if (isCancelledOp()) {
|
||||
result["skipped"] = true
|
||||
} else {
|
||||
try {
|
||||
provider.delete(activity, uri, path, mimeType)
|
||||
result["success"] = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
|
||||
result["success"] = false
|
||||
result["success"] = false
|
||||
getProvider(uri)?.let { provider ->
|
||||
try {
|
||||
provider.delete(activity, uri, path, mimeType)
|
||||
result["success"] = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
success(result)
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
|||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.SourceEntry
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -12,12 +13,19 @@ import java.io.File
|
|||
|
||||
internal class FileImageProvider : ImageProvider() {
|
||||
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) {
|
||||
if (sourceMimeType == null) {
|
||||
callback.onFailure(Exception("MIME type is null for uri=$uri"))
|
||||
return
|
||||
val mimeType = if (sourceMimeType != null) {
|
||||
sourceMimeType
|
||||
} else {
|
||||
val fromExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
||||
if (fromExtension != null) {
|
||||
fromExtension
|
||||
} else {
|
||||
callback.onFailure(Exception("MIME type was not provided and cannot be guessed from extension of uri=$uri"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType)
|
||||
val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, mimeType)
|
||||
|
||||
val path = uri.path
|
||||
if (path != null) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.net.Uri
|
|||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
|
@ -31,10 +32,7 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.updateRotation
|
|||
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateXmp
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.NameConflictStrategy
|
||||
import deckers.thibault.aves.model.*
|
||||
import deckers.thibault.aves.utils.*
|
||||
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
||||
import deckers.thibault.aves.utils.FileUtils.transferTo
|
||||
|
@ -53,6 +51,19 @@ abstract class ImageProvider {
|
|||
callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider"))
|
||||
}
|
||||
|
||||
suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap {
|
||||
return if (StorageUtils.isInVault(context, path)) {
|
||||
hashMapOf(
|
||||
"origin" to SourceEntry.ORIGIN_VAULT,
|
||||
"uri" to File(path).toUri().toString(),
|
||||
"contentId" to null,
|
||||
"path" to path,
|
||||
)
|
||||
} else {
|
||||
MediaStoreImageProvider().scanNewPathByMediaStore(context, path, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
|
||||
throw UnsupportedOperationException("`delete` is not supported by this image provider")
|
||||
}
|
||||
|
@ -294,8 +305,7 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
|
||||
val mediaStoreImageProvider = MediaStoreImageProvider()
|
||||
val targetPath = mediaStoreImageProvider.createSingle(
|
||||
val targetPath = MediaStoreImageProvider().createSingle(
|
||||
activity = activity,
|
||||
mimeType = targetMimeType,
|
||||
targetDir = targetDir,
|
||||
|
@ -303,7 +313,7 @@ abstract class ImageProvider {
|
|||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
write = write,
|
||||
)
|
||||
return mediaStoreImageProvider.scanNewPath(activity, targetPath, exportMimeType)
|
||||
return scanNewPath(activity, targetPath, exportMimeType)
|
||||
} finally {
|
||||
// clearing Glide target should happen after effectively writing the bitmap
|
||||
Glide.with(activity).clear(target)
|
||||
|
@ -422,7 +432,7 @@ abstract class ImageProvider {
|
|||
|
||||
val fileName = targetDocFile.name
|
||||
val targetFullPath = targetDir + fileName
|
||||
val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType)
|
||||
val newFields = scanNewPath(contextWrapper, targetFullPath, captureMimeType)
|
||||
callback.onSuccess(newFields)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
|
||||
|
@ -30,6 +30,7 @@ import deckers.thibault.aves.utils.UriUtils.tryParseId
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.SyncFailedException
|
||||
import java.util.*
|
||||
|
@ -474,7 +475,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType = mimeType,
|
||||
copy = copy,
|
||||
toBin = toBin,
|
||||
toVault = toVault,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -501,7 +501,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType: String,
|
||||
copy: Boolean,
|
||||
toBin: Boolean,
|
||||
toVault: Boolean,
|
||||
): FieldMap {
|
||||
val sourcePath = sourceFile.path
|
||||
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
|
||||
|
@ -550,21 +549,11 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
"trashed" to true,
|
||||
"trashPath" to targetPath,
|
||||
)
|
||||
} else if (toVault) {
|
||||
hashMapOf(
|
||||
"origin" to SourceEntry.ORIGIN_VAULT,
|
||||
"uri" to File(targetPath).toUri().toString(),
|
||||
"contentId" to null,
|
||||
"path" to targetPath,
|
||||
)
|
||||
} else {
|
||||
scanNewPath(activity, targetPath, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
||||
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
|
||||
// when used with entry URI as `sourceDocumentUri`, and targetDirDocFile URI as `targetParentDocumentUri`
|
||||
fun createSingle(
|
||||
activity: Activity,
|
||||
mimeType: String,
|
||||
|
@ -573,33 +562,86 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetNameWithoutExtension: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
if (StorageUtils.isInVault(activity, targetDir)) {
|
||||
return insertByFile(
|
||||
targetDir = targetDir,
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir)
|
||||
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
|
||||
if (isDownloadSubdir) {
|
||||
val volumePath = StorageUtils.getVolumePath(activity, targetDir)
|
||||
val relativePath = targetDir.substring(volumePath?.length ?: 0)
|
||||
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val resolver = activity.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
uri?.let {
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
} ?: throw Exception("MediaStore failed for some reason")
|
||||
|
||||
return File(targetDir, targetFileName).path
|
||||
return insertByMediaStore(
|
||||
activity = activity,
|
||||
targetDir = targetDir,
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return insertByTreeDoc(
|
||||
activity = activity,
|
||||
mimeType = mimeType,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertByFile(
|
||||
targetDir: String,
|
||||
targetFileName: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
val file = File(targetDir, targetFileName)
|
||||
FileOutputStream(file).use(write)
|
||||
return file.path
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun insertByMediaStore(
|
||||
activity: Activity,
|
||||
targetDir: String,
|
||||
targetFileName: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
val volumePath = StorageUtils.getVolumePath(activity, targetDir)
|
||||
val relativePath = targetDir.substring(volumePath?.length ?: 0)
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val resolver = activity.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
uri?.let {
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
} ?: throw Exception("MediaStore failed for some reason")
|
||||
|
||||
return File(targetDir, targetFileName).path
|
||||
}
|
||||
|
||||
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
||||
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
|
||||
// when used with entry URI as `sourceDocumentUri`, and targetDirDocFile URI as `targetParentDocumentUri`
|
||||
private fun insertByTreeDoc(
|
||||
activity: Activity,
|
||||
mimeType: String,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
targetNameWithoutExtension: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
||||
|
||||
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
|
||||
|
@ -670,7 +712,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
|
||||
// URI should not change
|
||||
return scanNewPath(activity, newFile.path, mimeType)
|
||||
return scanNewPathByMediaStore(activity, newFile.path, mimeType)
|
||||
}
|
||||
|
||||
private suspend fun renameSingleByTreeDoc(
|
||||
|
@ -690,7 +732,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
throw Exception("failed to rename document at path=$oldPath")
|
||||
}
|
||||
scanObsoletePath(activity, oldMediaUri, oldPath, mimeType)
|
||||
return scanNewPath(activity, newFile.path, mimeType)
|
||||
return scanNewPathByMediaStore(activity, newFile.path, mimeType)
|
||||
}
|
||||
|
||||
private suspend fun renameSingleByFile(
|
||||
|
@ -706,7 +748,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
throw Exception("failed to rename file at path=$oldPath")
|
||||
}
|
||||
scanObsoletePath(activity, oldMediaUri, oldPath, mimeType)
|
||||
return scanNewPath(activity, newFile.path, mimeType)
|
||||
return scanNewPathByMediaStore(activity, newFile.path, mimeType)
|
||||
}
|
||||
|
||||
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
|
||||
|
@ -757,10 +799,23 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
|
||||
suspendCoroutine { cont -> tryScanNewPath(context, path = path, mimeType = mimeType, cont) }
|
||||
suspend fun scanNewPathByMediaStore(context: Context, path: String, mimeType: String): FieldMap =
|
||||
suspendCoroutine { cont ->
|
||||
tryScanNewPathByMediaStore(
|
||||
context = context,
|
||||
path = path,
|
||||
mimeType = mimeType,
|
||||
cont = cont,
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryScanNewPath(context: Context, path: String, mimeType: String, cont: Continuation<FieldMap>, iteration: Int = 0) {
|
||||
private fun tryScanNewPathByMediaStore(
|
||||
context: Context,
|
||||
path: String,
|
||||
mimeType: String,
|
||||
cont: Continuation<FieldMap>,
|
||||
iteration: Int = 0,
|
||||
) {
|
||||
// `scanFile` may (e.g. when copying to SD card on Android 10 (API 29)):
|
||||
// 1) yield no URI,
|
||||
// 2) yield a temporary URI that fails when queried,
|
||||
|
@ -832,7 +887,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
tryScanNewPath(context, path = path, mimeType = mimeType, cont, iteration + 1)
|
||||
tryScanNewPathByMediaStore(context, path = path, mimeType = mimeType, cont, iteration + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ object MimeTypes {
|
|||
|
||||
fun isRaw(mimeType: String): Boolean {
|
||||
return when (mimeType) {
|
||||
ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW -> true
|
||||
ARW, CR2, CRW, DCR, DNG, ERF, K25, KDC, MRW, NEF, NRW, ORF, PEF, RAF, RAW, RW2, SR2, SRF, SRW, X3F -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
// with items that may be hidden right away because of their metadata
|
||||
addEntries(knownEntries, notify: false);
|
||||
|
||||
await _addVaultEntries(directory);
|
||||
await _loadVaultEntries(directory);
|
||||
|
||||
debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata');
|
||||
if (directory != null) {
|
||||
|
@ -266,6 +266,13 @@ class MediaStoreSource extends CollectionSource {
|
|||
}
|
||||
}
|
||||
|
||||
await _refreshVaultEntries(
|
||||
changedUris: changedUris.where(vaults.isVaultEntryUri).toSet(),
|
||||
newEntries: newEntries,
|
||||
entriesToRefresh: entriesToRefresh,
|
||||
existingDirectories: existingDirectories,
|
||||
);
|
||||
|
||||
invalidateAlbumFilterSummary(directories: existingDirectories);
|
||||
|
||||
if (newEntries.isNotEmpty) {
|
||||
|
@ -278,21 +285,21 @@ class MediaStoreSource extends CollectionSource {
|
|||
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
|
||||
}
|
||||
|
||||
await _refreshVaultEntries(changedUris.where(vaults.isVaultEntryUri).toSet());
|
||||
|
||||
return tempUris;
|
||||
}
|
||||
|
||||
// vault
|
||||
|
||||
Future<void> _addVaultEntries(String? directory) async {
|
||||
Future<void> _loadVaultEntries(String? directory) async {
|
||||
addEntries(await metadataDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
|
||||
}
|
||||
|
||||
Future<void> _refreshVaultEntries(Set<String> changedUris) async {
|
||||
final entriesToRefresh = <AvesEntry>{};
|
||||
final existingDirectories = <String>{};
|
||||
|
||||
Future<void> _refreshVaultEntries({
|
||||
required Set<String> changedUris,
|
||||
required Set<AvesEntry> newEntries,
|
||||
required Set<AvesEntry> entriesToRefresh,
|
||||
required Set<String> existingDirectories,
|
||||
}) async {
|
||||
for (final uri in changedUris) {
|
||||
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
|
||||
if (existingEntry != null) {
|
||||
|
@ -301,13 +308,15 @@ class MediaStoreSource extends CollectionSource {
|
|||
if (existingDirectory != null) {
|
||||
existingDirectories.add(existingDirectory);
|
||||
}
|
||||
} else {
|
||||
final sourceEntry = await mediaFetchService.getEntry(uri, null);
|
||||
if (sourceEntry != null) {
|
||||
newEntries.add(sourceEntry.copyWith(
|
||||
id: metadataDb.nextId,
|
||||
origin: EntryOrigins.vault,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateAlbumFilterSummary(directories: existingDirectories);
|
||||
|
||||
if (entriesToRefresh.isNotEmpty) {
|
||||
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue