fixed download directory access when not using reference case

This commit is contained in:
Thibault Deckers 2024-10-08 23:48:39 +02:00
parent 83f273f76e
commit 01e2bcc1b4
9 changed files with 53 additions and 23 deletions

View file

@ -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

View file

@ -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,

View file

@ -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<String>): List<Map<String, String>> {
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<String, MutableSet<String>>()
@ -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<FieldMap>): 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

View file

@ -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<String?>? {
val rootLength = (root ?: getVolumePath(context, anyPath))?.length ?: return null

View file

@ -24,7 +24,9 @@ abstract class StorageService {
Future<Set<VolumeRelativeDirectory>> getInaccessibleDirectories(Iterable<String> dirPaths);
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectories();
// returns directories with restricted access,
// with the relative part in lowercase, for case-insensitive comparison
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectoriesLowerCase();
Future<void> revokeDirectoryAccess(String path);
@ -155,11 +157,17 @@ class PlatformStorageService implements StorageService {
}
@override
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectories() async {
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectoriesLowerCase() async {
try {
final result = await _platform.invokeMethod('getRestrictedDirectories');
if (result != null) {
return (result as List).cast<Map>().map(VolumeRelativeDirectory.fromMap).toSet();
return (result as List)
.cast<Map>()
.map(VolumeRelativeDirectory.fromMap)
.map((dir) => dir.copyWith(
relativeDir: dir.relativeDir.toLowerCase(),
))
.toSet();
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);

View file

@ -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;

View file

@ -17,18 +17,25 @@ mixin PermissionAwareMixin {
}
Future<bool> checkStoragePermissionForAlbums(BuildContext context, Set<String> storageDirs, {Set<AvesEntry>? 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 = <String>[], mimeTypes = <String>[];
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);

View file

@ -386,8 +386,8 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> 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;
}

View file

@ -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,
);
}
}