export: fixed using a restricted directory as destination
This commit is contained in:
parent
e548134d30
commit
20b1d3a15b
2 changed files with 82 additions and 86 deletions
|
@ -13,6 +13,7 @@ import androidx.exifinterface.media.ExifInterface
|
|||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.FutureTarget
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.decoder.MultiTrackImage
|
||||
|
@ -36,6 +37,7 @@ import deckers.thibault.aves.utils.MimeTypes.isVideo
|
|||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
|
@ -98,12 +100,6 @@ abstract class ImageProvider {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO TLAD [storage] allow inserting by Media Store
|
||||
if (targetDirDocFile == null) {
|
||||
callback.onFailure(Exception("failed to get tree doc for directory at path=$targetDir"))
|
||||
return
|
||||
}
|
||||
|
||||
for (entry in entries) {
|
||||
val sourceUri = entry.uri
|
||||
val sourcePath = entry.path
|
||||
|
@ -118,7 +114,7 @@ abstract class ImageProvider {
|
|||
val sourceMimeType = entry.mimeType
|
||||
val exportMimeType = if (isVideo(sourceMimeType)) sourceMimeType else imageExportMimeType
|
||||
try {
|
||||
val newFields = exportSingleByTreeDocAndScan(
|
||||
val newFields = exportSingle(
|
||||
activity = activity,
|
||||
sourceEntry = entry,
|
||||
targetDir = targetDir,
|
||||
|
@ -138,11 +134,11 @@ abstract class ImageProvider {
|
|||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun exportSingleByTreeDocAndScan(
|
||||
private suspend fun exportSingle(
|
||||
activity: Activity,
|
||||
sourceEntry: AvesEntry,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
width: Int,
|
||||
height: Int,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
|
@ -170,16 +166,16 @@ abstract class ImageProvider {
|
|||
conflictStrategy = nameConflictStrategy,
|
||||
) ?: return skippedFieldMap
|
||||
|
||||
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
|
||||
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
||||
// through a document URI, not a tree URI
|
||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||
val targetTreeFile = targetDirDocFile.createFile(exportMimeType, targetNameWithoutExtension)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
|
||||
val targetMimeType: String
|
||||
val write: (OutputStream) -> Unit
|
||||
var target: FutureTarget<Bitmap>? = null
|
||||
try {
|
||||
if (isVideo(sourceMimeType)) {
|
||||
targetMimeType = sourceMimeType
|
||||
write = { output ->
|
||||
val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
|
||||
sourceDocFile.copyTo(targetDocFile)
|
||||
sourceDocFile.copyTo(output)
|
||||
}
|
||||
} else {
|
||||
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
|
||||
MultiTrackImage(activity, sourceUri, pageId)
|
||||
|
@ -197,19 +193,19 @@ abstract class ImageProvider {
|
|||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
|
||||
val target = Glide.with(activity)
|
||||
target = Glide.with(activity)
|
||||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(model)
|
||||
.submit(width, height)
|
||||
try {
|
||||
var bitmap = target.get()
|
||||
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
|
||||
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
|
||||
}
|
||||
bitmap ?: throw Exception("failed to get image for mimeType=$sourceMimeType uri=$sourceUri page=$pageId")
|
||||
|
||||
targetDocFile.openOutputStream().use { output ->
|
||||
targetMimeType = exportMimeType
|
||||
write = { output ->
|
||||
if (exportMimeType == MimeTypes.BMP) {
|
||||
BmpWriter.writeRGB24(bitmap, output)
|
||||
} else {
|
||||
|
@ -232,21 +228,23 @@ abstract class ImageProvider {
|
|||
bitmap.compress(format, quality, output)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// remove empty file
|
||||
if (targetDocFile.exists()) {
|
||||
targetDocFile.delete()
|
||||
}
|
||||
throw e
|
||||
|
||||
val mediaStoreImageProvider = MediaStoreImageProvider()
|
||||
val targetPath = mediaStoreImageProvider.createSingle(
|
||||
activity = activity,
|
||||
mimeType = targetMimeType,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
write = write,
|
||||
)
|
||||
return mediaStoreImageProvider.scanNewPath(activity, targetPath, exportMimeType)
|
||||
} finally {
|
||||
// clearing Glide target should happen after effectively writing the bitmap
|
||||
Glide.with(activity).clear(target)
|
||||
}
|
||||
}
|
||||
|
||||
val fileName = targetDocFile.name
|
||||
val targetFullPath = targetDir + fileName
|
||||
|
||||
return MediaStoreImageProvider().scanNewPath(activity, targetFullPath, exportMimeType)
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
|
|
|
@ -28,6 +28,7 @@ import deckers.thibault.aves.utils.StorageUtils
|
|||
import deckers.thibault.aves.utils.StorageUtils.PathSegments
|
||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||
import java.io.File
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.collections.ArrayList
|
||||
|
@ -414,34 +415,39 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
conflictStrategy = nameConflictStrategy,
|
||||
) ?: return skippedFieldMap
|
||||
|
||||
return moveSingleByTreeDoc(
|
||||
val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
|
||||
val targetPath = createSingle(
|
||||
activity = activity,
|
||||
mimeType = mimeType,
|
||||
sourceUri = sourceUri,
|
||||
sourcePath = sourcePath,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
copy = copy
|
||||
)
|
||||
) { output: OutputStream -> sourceDocFile.copyTo(output) }
|
||||
|
||||
if (!copy) {
|
||||
// delete original entry
|
||||
try {
|
||||
delete(activity, sourceUri, sourcePath, mimeType)
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
|
||||
}
|
||||
}
|
||||
|
||||
return scanNewPath(activity, targetPath, mimeType)
|
||||
}
|
||||
|
||||
private suspend fun moveSingleByTreeDoc(
|
||||
activity: Activity,
|
||||
mimeType: String,
|
||||
sourceUri: Uri,
|
||||
sourcePath: String,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
targetNameWithoutExtension: String,
|
||||
copy: Boolean
|
||||
): FieldMap {
|
||||
// `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`
|
||||
val source = DocumentFileCompat.fromSingleUri(activity, sourceUri)
|
||||
|
||||
val targetPath = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isDownloadDir(activity, targetDir)) {
|
||||
fun createSingle(
|
||||
activity: Activity,
|
||||
mimeType: String,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
targetNameWithoutExtension: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isDownloadDir(activity, targetDir)) {
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
|
@ -451,10 +457,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
uri?.let {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
resolver.openOutputStream(uri)?.use { output ->
|
||||
source.copyTo(output)
|
||||
}
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
|
@ -468,12 +471,18 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
||||
// through a document URI, not a tree URI
|
||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
source.copyTo(targetDocFile)
|
||||
try {
|
||||
targetDocFile.openOutputStream().use(write)
|
||||
} catch (e: Exception) {
|
||||
// remove empty file
|
||||
if (targetDocFile.exists()) {
|
||||
targetDocFile.delete()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
// the source file name and the created document file name can be different when:
|
||||
// - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not*
|
||||
|
@ -481,17 +490,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val fileName = targetDocFile.name
|
||||
targetDir + fileName
|
||||
}
|
||||
|
||||
if (!copy) {
|
||||
// delete original entry
|
||||
try {
|
||||
delete(activity, sourceUri, sourcePath, mimeType)
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e)
|
||||
}
|
||||
}
|
||||
|
||||
return scanNewPath(activity, targetPath, mimeType)
|
||||
}
|
||||
|
||||
private fun isDownloadDir(context: Context, dirPath: String): Boolean {
|
||||
|
|
Loading…
Reference in a new issue