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