#1249 fixed copying content URI items
This commit is contained in:
parent
ccbca7c506
commit
c6ec5afba1
3 changed files with 80 additions and 66 deletions
|
@ -17,6 +17,7 @@ All notable changes to this project will be documented in this file.
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- crash when loading large collection
|
- crash when loading large collection
|
||||||
|
- Viewer: copying content URI item
|
||||||
|
|
||||||
## <a id="v1.11.16"></a>[v1.11.16] - 2024-10-10
|
## <a id="v1.11.16"></a>[v1.11.16] - 2024-10-10
|
||||||
|
|
||||||
|
|
|
@ -137,8 +137,7 @@ abstract class ImageProvider {
|
||||||
"success" to false,
|
"success" to false,
|
||||||
)
|
)
|
||||||
|
|
||||||
// prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store
|
if (sourcePath != null) {
|
||||||
if (sourcePath != null && !desiredName.startsWith('.')) {
|
|
||||||
try {
|
try {
|
||||||
var newFields: FieldMap = skippedFieldMap
|
var newFields: FieldMap = skippedFieldMap
|
||||||
if (!isCancelledOp()) {
|
if (!isCancelledOp()) {
|
||||||
|
@ -570,6 +569,20 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createTimeStampFileName() = Date().time.toString()
|
||||||
|
|
||||||
|
private fun sanitizeDesiredFileName(desiredName: String): String {
|
||||||
|
var name = desiredName
|
||||||
|
// prevent creating hidden files
|
||||||
|
while (name.isNotEmpty() && name.startsWith(".")) {
|
||||||
|
name = name.substring(1)
|
||||||
|
}
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
name = createTimeStampFileName()
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
// returns available name to use, or `null` to skip it
|
// returns available name to use, or `null` to skip it
|
||||||
suspend fun resolveTargetFileNameWithoutExtension(
|
suspend fun resolveTargetFileNameWithoutExtension(
|
||||||
contextWrapper: ContextWrapper,
|
contextWrapper: ContextWrapper,
|
||||||
|
@ -578,18 +591,19 @@ abstract class ImageProvider {
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
conflictStrategy: NameConflictStrategy,
|
conflictStrategy: NameConflictStrategy,
|
||||||
): NameConflictResolution {
|
): NameConflictResolution {
|
||||||
var resolvedName: String? = desiredNameWithoutExtension
|
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
|
||||||
|
var resolvedName: String? = sanitizedNameWithoutExtension
|
||||||
var replacementFile: File? = null
|
var replacementFile: File? = null
|
||||||
|
|
||||||
val extension = extensionFor(mimeType)
|
val extension = extensionFor(mimeType)
|
||||||
val targetFile = File(dir, "$desiredNameWithoutExtension$extension")
|
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
|
||||||
when (conflictStrategy) {
|
when (conflictStrategy) {
|
||||||
NameConflictStrategy.RENAME -> {
|
NameConflictStrategy.RENAME -> {
|
||||||
var nameWithoutExtension = desiredNameWithoutExtension
|
var nameWithoutExtension = sanitizedNameWithoutExtension
|
||||||
var i = 0
|
var i = 0
|
||||||
while (File(dir, "$nameWithoutExtension$extension").exists()) {
|
while (File(dir, "$nameWithoutExtension$extension").exists()) {
|
||||||
i++
|
i++
|
||||||
nameWithoutExtension = "$desiredNameWithoutExtension ($i)"
|
nameWithoutExtension = "$sanitizedNameWithoutExtension ($i)"
|
||||||
}
|
}
|
||||||
resolvedName = nameWithoutExtension
|
resolvedName = nameWithoutExtension
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.io.SyncFailedException
|
import java.io.SyncFailedException
|
||||||
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
|
@ -478,64 +479,62 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
"success" to false,
|
"success" to false,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (sourcePath != null) {
|
// on API 30 we cannot get access granted directly to a volume root from its document tree,
|
||||||
// on API 30 we cannot get access granted directly to a volume root from its document tree,
|
// but it is still less constraining to use tree document files than to rely on the Media Store
|
||||||
// but it is still less constraining to use tree document files than to rely on the Media Store
|
//
|
||||||
//
|
// Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but:
|
||||||
// Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but:
|
// - we need to scan the file to get the Media Store content URI
|
||||||
// - we need to scan the file to get the Media Store content URI
|
// - the underlying document provider controls the new file name
|
||||||
// - the underlying document provider controls the new file name
|
//
|
||||||
//
|
// Relying on the Media Store, we can create an item via `ContentResolver.insert()`
|
||||||
// Relying on the Media Store, we can create an item via `ContentResolver.insert()`
|
// with a path, and retrieve its content URI, but:
|
||||||
// with a path, and retrieve its content URI, but:
|
// - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`)
|
||||||
// - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`)
|
// - the volume name should be lower case, not exactly as the `StorageVolume` UUID
|
||||||
// - the volume name should be lower case, not exactly as the `StorageVolume` UUID
|
// cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()`
|
||||||
// cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()`
|
// - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?)
|
||||||
// - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?)
|
// - there is no documentation regarding support for usage with removable storage
|
||||||
// - there is no documentation regarding support for usage with removable storage
|
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
|
||||||
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
|
try {
|
||||||
try {
|
val appDir = when {
|
||||||
val appDir = when {
|
toBin -> StorageUtils.trashDirFor(activity, sourcePath ?: StorageUtils.getPrimaryVolumePath(activity))
|
||||||
toBin -> StorageUtils.trashDirFor(activity, sourcePath)
|
toVault -> File(targetDir)
|
||||||
toVault -> File(targetDir)
|
else -> null
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
if (appDir != null) {
|
|
||||||
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
|
|
||||||
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
|
|
||||||
|
|
||||||
if (toVault) {
|
|
||||||
appDir.mkdirs()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (effectiveTargetDir != null) {
|
|
||||||
val newFields = if (isCancelledOp()) skippedFieldMap else {
|
|
||||||
val sourceFile = File(sourcePath)
|
|
||||||
if (!sourceFile.exists() && toBin) {
|
|
||||||
delete(activity, sourceUri, sourcePath, mimeType = mimeType)
|
|
||||||
deletedFieldMap
|
|
||||||
} else {
|
|
||||||
moveSingle(
|
|
||||||
activity = activity,
|
|
||||||
sourceFile = sourceFile,
|
|
||||||
sourceUri = sourceUri,
|
|
||||||
targetDir = effectiveTargetDir,
|
|
||||||
targetDirDocFile = targetDirDocFile,
|
|
||||||
desiredName = desiredName ?: sourceFile.name,
|
|
||||||
nameConflictStrategy = nameConflictStrategy,
|
|
||||||
mimeType = mimeType,
|
|
||||||
copy = copy,
|
|
||||||
toBin = toBin,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result["newFields"] = newFields
|
|
||||||
result["success"] = true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
|
|
||||||
}
|
}
|
||||||
|
if (appDir != null) {
|
||||||
|
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
|
||||||
|
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
|
||||||
|
|
||||||
|
if (toVault) {
|
||||||
|
appDir.mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectiveTargetDir != null) {
|
||||||
|
val newFields = if (isCancelledOp()) skippedFieldMap else {
|
||||||
|
val sourceFile = if (sourcePath != null) File(sourcePath) else null
|
||||||
|
if (sourceFile != null && !sourceFile.exists() && toBin) {
|
||||||
|
delete(activity, sourceUri, sourcePath, mimeType = mimeType)
|
||||||
|
deletedFieldMap
|
||||||
|
} else {
|
||||||
|
moveSingle(
|
||||||
|
activity = activity,
|
||||||
|
sourceFile = sourceFile,
|
||||||
|
sourceUri = sourceUri,
|
||||||
|
targetDir = effectiveTargetDir,
|
||||||
|
targetDirDocFile = targetDirDocFile,
|
||||||
|
desiredName = desiredName ?: sourceFile?.name ?: sourceUri.lastPathSegment ?: createTimeStampFileName(),
|
||||||
|
nameConflictStrategy = nameConflictStrategy,
|
||||||
|
mimeType = mimeType,
|
||||||
|
copy = copy,
|
||||||
|
toBin = toBin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result["newFields"] = newFields
|
||||||
|
result["success"] = true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
|
||||||
}
|
}
|
||||||
callback.onSuccess(result)
|
callback.onSuccess(result)
|
||||||
}
|
}
|
||||||
|
@ -544,7 +543,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
|
|
||||||
private suspend fun moveSingle(
|
private suspend fun moveSingle(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
sourceFile: File,
|
sourceFile: File?,
|
||||||
sourceUri: Uri,
|
sourceUri: Uri,
|
||||||
targetDir: String,
|
targetDir: String,
|
||||||
targetDirDocFile: DocumentFileCompat?,
|
targetDirDocFile: DocumentFileCompat?,
|
||||||
|
@ -554,8 +553,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
copy: Boolean,
|
copy: Boolean,
|
||||||
toBin: Boolean,
|
toBin: Boolean,
|
||||||
): FieldMap {
|
): FieldMap {
|
||||||
val sourcePath = sourceFile.path
|
val sourcePath = sourceFile?.path
|
||||||
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
|
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
|
||||||
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
|
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
|
||||||
// nothing to do unless it's a renamed copy
|
// nothing to do unless it's a renamed copy
|
||||||
return skippedFieldMap
|
return skippedFieldMap
|
||||||
|
|
Loading…
Reference in a new issue