diff --git a/CHANGELOG.md b/CHANGELOG.md index ec2afa58d..c87b5c714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ All notable changes to this project will be documented in this file. - Slideshow: option for no transition - Widget: tap action setting +### Fixed + +- restoring to missing Download subdir + ## [v1.7.0] - 2022-09-19 ### Added diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 442d35190..0a7876534 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -7,7 +7,6 @@ import android.content.* import android.media.MediaScannerConnection import android.net.Uri import android.os.Build -import android.os.Environment import android.provider.MediaStore import android.util.Log import com.commonsware.cwac.document.DocumentFileCompat @@ -391,8 +390,13 @@ class MediaStoreImageProvider : ImageProvider() { effectiveTargetDir = targetDir targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) if (!File(targetDir).exists()) { - callback.onFailure(Exception("failed to create directory at path=$targetDir")) - return + val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir) + val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath) + // download subdirectories can be created later by Media Store insertion + if (!isDownloadSubdir) { + callback.onFailure(Exception("failed to create directory at path=$targetDir")) + return + } } } @@ -535,54 +539,57 @@ class MediaStoreImageProvider : ImageProvider() { 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) - put(MediaStore.MediaColumns.IS_PENDING, 1) - } - val resolver = activity.contentResolver - val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) + 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) - 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") - - File(targetDir, targetFileName).path - } else { - targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") - - // 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(mimeType, targetNameWithoutExtension) - val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) - - try { - targetDocFile.openOutputStream().use(write) - } catch (e: Exception) { - // remove empty file - if (targetDocFile.exists()) { - targetDocFile.delete() + 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) } - throw e + 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 } - - // 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* - // - the original extension does not match the extension added by the underlying provider - val fileName = targetDocFile.name - targetDir + fileName } - } - private fun isDownloadDir(context: Context, dirPath: String): Boolean { - val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "") - return relativeDir == Environment.DIRECTORY_DOWNLOADS + targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") + + // 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(mimeType, targetNameWithoutExtension) + val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) + + 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* + // - the original extension does not match the extension added by the underlying provider + val fileName = targetDocFile.name + return targetDir + fileName } override suspend fun renameMultiple( diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 2d78f9a52..08355a5de 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -105,7 +105,11 @@ object PermissionManager { val primaryDir = dirSegments.firstOrNull() if (getRestrictedPrimaryDirectories().contains(primaryDir) && dirSegments.size > 1) { // request secondary directory (if any) for restricted primary directory - dirSet.add(dirSegments.take(2).joinToString(File.separator)) + val dir = dirSegments.take(2).joinToString(File.separator) + // only register directories that exist on storage, so they can be selected for access grant + if (File(volumePath, dir).exists()) { + dirSet.add(dir) + } } else { primaryDir?.let { dirSet.add(it) } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index fef6748b5..db1bd8e80 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -9,6 +9,7 @@ import android.content.pm.PackageManager import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build +import android.os.Environment import android.os.storage.StorageManager import android.provider.DocumentsContract import android.provider.MediaStore @@ -93,6 +94,10 @@ object StorageUtils { return getVolumePaths(context).firstOrNull { anyPath.startsWith(it) } } + fun getDownloadDirPath(context: Context, anyPath: String): String? { + return getVolumePath(context, anyPath)?.let { volumePath -> ensureTrailingSeparator(File(volumePath, Environment.DIRECTORY_DOWNLOADS).path) } + } + private fun getPathStepIterator(context: Context, anyPath: String, root: String?): Iterator? { val rootLength = (root ?: getVolumePath(context, anyPath))?.length ?: return null