fixed download directory access when not using reference case
This commit is contained in:
parent
83f273f76e
commit
01e2bcc1b4
9 changed files with 53 additions and 23 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue