diff --git a/CHANGELOG.md b/CHANGELOG.md index aa01a4ad6..6a60dbe6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1145,6 +1145,7 @@ All notable changes to this project will be documented in this file. - app launching on some devices - corrupting motion photo exif editing (e.g. rotation) +- accessing files in `Download` directory when not using reference case ## [v1.4.9] - 2021-08-20 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 c799df270..f05e6991f 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 @@ -12,6 +12,7 @@ import android.graphics.BitmapFactory 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 androidx.annotation.RequiresApi @@ -452,10 +453,8 @@ class MediaStoreImageProvider : ImageProvider() { effectiveTargetDir = targetDir targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) if (!File(targetDir).exists()) { - 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) { + if (!isDownloadSubdir(activity, targetDir)) { callback.onFailure(Exception("failed to create directory at path=$targetDir")) return } @@ -625,9 +624,7 @@ class MediaStoreImageProvider : ImageProvider() { } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir) - val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath) - if (isDownloadSubdir) { + if (isDownloadSubdir(activity, targetDir)) { return insertByMediaStore( activity = activity, targetDir = targetDir, @@ -647,6 +644,13 @@ class MediaStoreImageProvider : ImageProvider() { ) } + private fun isDownloadSubdir(context: Context, dir: String): Boolean { + val volumePath = StorageUtils.getVolumePath(context, dir) ?: return false + val downloadDirPath = ensureTrailingSeparator(File(volumePath, Environment.DIRECTORY_DOWNLOADS).path) + // effective download path may have a different case + return dir.lowercase().startsWith(downloadDirPath.lowercase()) + } + private fun insertByFile( targetDir: String, targetFileName: String, 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 1636543a9..496c898ee 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 @@ -17,6 +17,7 @@ import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.StorageUtils.PathSegments import java.io.File +import java.util.Locale import java.util.concurrent.CompletableFuture object PermissionManager { @@ -86,6 +87,7 @@ object PermissionManager { fun getInaccessibleDirectories(context: Context, dirPaths: List): List> { val concreteDirPaths = dirPaths.filter { it != StorageUtils.TRASH_PATH_PLACEHOLDER } val accessibleDirs = getAccessibleDirs(context) + val restrictedPrimaryDirectoriesLower = getRestrictedPrimaryDirectories().map { it.lowercase(Locale.ROOT) } // find set of inaccessible directories for each volume val dirsPerVolume = HashMap>() @@ -101,7 +103,7 @@ object PermissionManager { if (relativeDir != null) { val dirSegments = relativeDir.split(File.separator).takeWhile { it.isNotEmpty() } val primaryDir = dirSegments.firstOrNull() - if (getRestrictedPrimaryDirectories().contains(primaryDir) && dirSegments.size > 1) { + if (dirSegments.size > 1 && restrictedPrimaryDirectoriesLower.contains(primaryDir?.lowercase(Locale.ROOT))) { // request secondary directory (if any) for restricted primary directory val dir = dirSegments.take(2).joinToString(File.separator) // only register directories that exist on storage, so they can be selected for access grant @@ -140,10 +142,11 @@ object PermissionManager { fun canInsertByMediaStore(directories: List): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val insertionDirsLower = MEDIA_STORE_INSERTION_PRIMARY_DIRS.map { it.lowercase(Locale.ROOT) } directories.all { val relativeDir = it["relativeDir"] as String val segments = relativeDir.split(File.separator) - segments.isNotEmpty() && MEDIA_STORE_INSERTION_PRIMARY_DIRS.contains(segments.first()) + segments.isNotEmpty() && insertionDirsLower.contains(segments.first().lowercase(Locale.ROOT)) } } else { true 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 d7d4a6011..93f64ccef 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 @@ -120,10 +120,6 @@ 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 diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 765f87200..c97206cda 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -24,7 +24,9 @@ abstract class StorageService { Future> getInaccessibleDirectories(Iterable dirPaths); - Future> getRestrictedDirectories(); + // returns directories with restricted access, + // with the relative part in lowercase, for case-insensitive comparison + Future> getRestrictedDirectoriesLowerCase(); Future revokeDirectoryAccess(String path); @@ -155,11 +157,17 @@ class PlatformStorageService implements StorageService { } @override - Future> getRestrictedDirectories() async { + Future> getRestrictedDirectoriesLowerCase() async { try { final result = await _platform.invokeMethod('getRestrictedDirectories'); if (result != null) { - return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); + return (result as List) + .cast() + .map(VolumeRelativeDirectory.fromMap) + .map((dir) => dir.copyWith( + relativeDir: dir.relativeDir.toLowerCase(), + )) + .toSet(); } } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index 327485eeb..b11f2f1d7 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -48,7 +48,8 @@ class AndroidFileUtils { primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator; // standard dcimPath = pContext.join(primaryStorage, 'DCIM'); - downloadPath = pContext.join(primaryStorage, 'Download'); + // effective download path may have a different case + downloadPath = pContext.join(primaryStorage, 'Download').toLowerCase(); moviesPath = pContext.join(primaryStorage, 'Movies'); picturesPath = pContext.join(primaryStorage, 'Pictures'); avesVideoCapturesPath = pContext.join(dcimPath, 'Video Captures'); @@ -78,7 +79,7 @@ class AndroidFileUtils { bool isVideoCapturesPath(String path) => videoCapturesPaths.contains(path); - bool isDownloadPath(String path) => path == downloadPath; + bool isDownloadPath(String path) => path.toLowerCase() == downloadPath; StorageVolume? getStorageVolume(String? path) { if (path == null) return null; diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 9faa72730..aea14b4a6 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -17,18 +17,25 @@ mixin PermissionAwareMixin { } Future checkStoragePermissionForAlbums(BuildContext context, Set storageDirs, {Set? entries}) async { - final restrictedDirs = await storageService.getRestrictedDirectories(); + final restrictedDirsLowerCase = await storageService.getRestrictedDirectoriesLowerCase(); while (true) { final dirs = await storageService.getInaccessibleDirectories(storageDirs); - final restrictedInaccessibleDirs = dirs.where(restrictedDirs.contains).toSet(); + final restrictedInaccessibleDirs = dirs + .map((dir) => dir.copyWith( + relativeDir: dir.relativeDir.toLowerCase(), + )) + .where(restrictedDirsLowerCase.contains) + .toSet(); if (restrictedInaccessibleDirs.isNotEmpty) { if (entries != null && await storageService.canRequestMediaFileBulkAccess()) { // request media file access for items in restricted directories final uris = [], mimeTypes = []; entries.where((entry) { - final dir = entry.directory; - return dir != null && restrictedInaccessibleDirs.contains(androidFileUtils.relativeDirectoryFromPath(dir)); + final dirPath = entry.directory; + if (dirPath == null) return false; + final dir = androidFileUtils.relativeDirectoryFromPath(dirPath); + return restrictedInaccessibleDirs.contains(dir?.copyWith(relativeDir: dir.relativeDir.toLowerCase())); }).forEach((entry) { uris.add(entry.uri); mimeTypes.add(entry.mimeType); diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 967750ae3..12b6a72b0 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -386,8 +386,8 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with // check whether renaming is possible given OS restrictions, // before asking to input a new name - final restrictedDirs = await storageService.getRestrictedDirectories(); - if (restrictedDirs.contains(dir)) { + final restrictedDirsLowerCase = await storageService.getRestrictedDirectoriesLowerCase(); + if (restrictedDirsLowerCase.contains(dir.copyWith(relativeDir: dir.relativeDir.toLowerCase()))) { await showRestrictedDirectoryDialog(context, dir); return; } diff --git a/plugins/aves_model/lib/src/storage/relative_dir.dart b/plugins/aves_model/lib/src/storage/relative_dir.dart index 393178f25..b7c84a0ca 100644 --- a/plugins/aves_model/lib/src/storage/relative_dir.dart +++ b/plugins/aves_model/lib/src/storage/relative_dir.dart @@ -26,4 +26,14 @@ class VolumeRelativeDirectory extends Equatable { 'volumePath': volumePath, 'relativeDir': relativeDir, }; + + VolumeRelativeDirectory copyWith({ + String? volumePath, + String? relativeDir, + }) { + return VolumeRelativeDirectory( + volumePath: volumePath ?? this.volumePath, + relativeDir: relativeDir ?? this.relativeDir, + ); + } }