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
|
- app launching on some devices
|
||||||
- corrupting motion photo exif editing (e.g. rotation)
|
- corrupting motion photo exif editing (e.g. rotation)
|
||||||
|
- accessing files in `Download` directory when not using reference case
|
||||||
|
|
||||||
## [v1.4.9] - 2021-08-20
|
## [v1.4.9] - 2021-08-20
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import android.graphics.BitmapFactory
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
@ -452,10 +453,8 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
effectiveTargetDir = targetDir
|
effectiveTargetDir = targetDir
|
||||||
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
|
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
|
||||||
if (!File(targetDir).exists()) {
|
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
|
// 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"))
|
callback.onFailure(Exception("failed to create directory at path=$targetDir"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -625,9 +624,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir)
|
if (isDownloadSubdir(activity, targetDir)) {
|
||||||
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
|
|
||||||
if (isDownloadSubdir) {
|
|
||||||
return insertByMediaStore(
|
return insertByMediaStore(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
targetDir = targetDir,
|
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(
|
private fun insertByFile(
|
||||||
targetDir: String,
|
targetDir: String,
|
||||||
targetFileName: String,
|
targetFileName: String,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import deckers.thibault.aves.PendingStorageAccessResultHandler
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.StorageUtils.PathSegments
|
import deckers.thibault.aves.utils.StorageUtils.PathSegments
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
object PermissionManager {
|
object PermissionManager {
|
||||||
|
@ -86,6 +87,7 @@ object PermissionManager {
|
||||||
fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> {
|
fun getInaccessibleDirectories(context: Context, dirPaths: List<String>): List<Map<String, String>> {
|
||||||
val concreteDirPaths = dirPaths.filter { it != StorageUtils.TRASH_PATH_PLACEHOLDER }
|
val concreteDirPaths = dirPaths.filter { it != StorageUtils.TRASH_PATH_PLACEHOLDER }
|
||||||
val accessibleDirs = getAccessibleDirs(context)
|
val accessibleDirs = getAccessibleDirs(context)
|
||||||
|
val restrictedPrimaryDirectoriesLower = getRestrictedPrimaryDirectories().map { it.lowercase(Locale.ROOT) }
|
||||||
|
|
||||||
// find set of inaccessible directories for each volume
|
// find set of inaccessible directories for each volume
|
||||||
val dirsPerVolume = HashMap<String, MutableSet<String>>()
|
val dirsPerVolume = HashMap<String, MutableSet<String>>()
|
||||||
|
@ -101,7 +103,7 @@ object PermissionManager {
|
||||||
if (relativeDir != null) {
|
if (relativeDir != null) {
|
||||||
val dirSegments = relativeDir.split(File.separator).takeWhile { it.isNotEmpty() }
|
val dirSegments = relativeDir.split(File.separator).takeWhile { it.isNotEmpty() }
|
||||||
val primaryDir = dirSegments.firstOrNull()
|
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
|
// request secondary directory (if any) for restricted primary directory
|
||||||
val dir = 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
|
// 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 {
|
fun canInsertByMediaStore(directories: List<FieldMap>): Boolean {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val insertionDirsLower = MEDIA_STORE_INSERTION_PRIMARY_DIRS.map { it.lowercase(Locale.ROOT) }
|
||||||
directories.all {
|
directories.all {
|
||||||
val relativeDir = it["relativeDir"] as String
|
val relativeDir = it["relativeDir"] as String
|
||||||
val segments = relativeDir.split(File.separator)
|
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 {
|
} else {
|
||||||
true
|
true
|
||||||
|
|
|
@ -120,10 +120,6 @@ object StorageUtils {
|
||||||
return getVolumePaths(context).firstOrNull { anyPath.startsWith(it) }
|
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?>? {
|
private fun getPathStepIterator(context: Context, anyPath: String, root: String?): Iterator<String?>? {
|
||||||
val rootLength = (root ?: getVolumePath(context, anyPath))?.length ?: return null
|
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>> 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);
|
Future<void> revokeDirectoryAccess(String path);
|
||||||
|
|
||||||
|
@ -155,11 +157,17 @@ class PlatformStorageService implements StorageService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectories() async {
|
Future<Set<VolumeRelativeDirectory>> getRestrictedDirectoriesLowerCase() async {
|
||||||
try {
|
try {
|
||||||
final result = await _platform.invokeMethod('getRestrictedDirectories');
|
final result = await _platform.invokeMethod('getRestrictedDirectories');
|
||||||
if (result != null) {
|
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) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
|
|
|
@ -48,7 +48,8 @@ class AndroidFileUtils {
|
||||||
primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator;
|
primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator;
|
||||||
// standard
|
// standard
|
||||||
dcimPath = pContext.join(primaryStorage, 'DCIM');
|
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');
|
moviesPath = pContext.join(primaryStorage, 'Movies');
|
||||||
picturesPath = pContext.join(primaryStorage, 'Pictures');
|
picturesPath = pContext.join(primaryStorage, 'Pictures');
|
||||||
avesVideoCapturesPath = pContext.join(dcimPath, 'Video Captures');
|
avesVideoCapturesPath = pContext.join(dcimPath, 'Video Captures');
|
||||||
|
@ -78,7 +79,7 @@ class AndroidFileUtils {
|
||||||
|
|
||||||
bool isVideoCapturesPath(String path) => videoCapturesPaths.contains(path);
|
bool isVideoCapturesPath(String path) => videoCapturesPaths.contains(path);
|
||||||
|
|
||||||
bool isDownloadPath(String path) => path == downloadPath;
|
bool isDownloadPath(String path) => path.toLowerCase() == downloadPath;
|
||||||
|
|
||||||
StorageVolume? getStorageVolume(String? path) {
|
StorageVolume? getStorageVolume(String? path) {
|
||||||
if (path == null) return null;
|
if (path == null) return null;
|
||||||
|
|
|
@ -17,18 +17,25 @@ mixin PermissionAwareMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> checkStoragePermissionForAlbums(BuildContext context, Set<String> storageDirs, {Set<AvesEntry>? entries}) async {
|
Future<bool> checkStoragePermissionForAlbums(BuildContext context, Set<String> storageDirs, {Set<AvesEntry>? entries}) async {
|
||||||
final restrictedDirs = await storageService.getRestrictedDirectories();
|
final restrictedDirsLowerCase = await storageService.getRestrictedDirectoriesLowerCase();
|
||||||
while (true) {
|
while (true) {
|
||||||
final dirs = await storageService.getInaccessibleDirectories(storageDirs);
|
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 (restrictedInaccessibleDirs.isNotEmpty) {
|
||||||
if (entries != null && await storageService.canRequestMediaFileBulkAccess()) {
|
if (entries != null && await storageService.canRequestMediaFileBulkAccess()) {
|
||||||
// request media file access for items in restricted directories
|
// request media file access for items in restricted directories
|
||||||
final uris = <String>[], mimeTypes = <String>[];
|
final uris = <String>[], mimeTypes = <String>[];
|
||||||
entries.where((entry) {
|
entries.where((entry) {
|
||||||
final dir = entry.directory;
|
final dirPath = entry.directory;
|
||||||
return dir != null && restrictedInaccessibleDirs.contains(androidFileUtils.relativeDirectoryFromPath(dir));
|
if (dirPath == null) return false;
|
||||||
|
final dir = androidFileUtils.relativeDirectoryFromPath(dirPath);
|
||||||
|
return restrictedInaccessibleDirs.contains(dir?.copyWith(relativeDir: dir.relativeDir.toLowerCase()));
|
||||||
}).forEach((entry) {
|
}).forEach((entry) {
|
||||||
uris.add(entry.uri);
|
uris.add(entry.uri);
|
||||||
mimeTypes.add(entry.mimeType);
|
mimeTypes.add(entry.mimeType);
|
||||||
|
|
|
@ -386,8 +386,8 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
||||||
|
|
||||||
// check whether renaming is possible given OS restrictions,
|
// check whether renaming is possible given OS restrictions,
|
||||||
// before asking to input a new name
|
// before asking to input a new name
|
||||||
final restrictedDirs = await storageService.getRestrictedDirectories();
|
final restrictedDirsLowerCase = await storageService.getRestrictedDirectoriesLowerCase();
|
||||||
if (restrictedDirs.contains(dir)) {
|
if (restrictedDirsLowerCase.contains(dir.copyWith(relativeDir: dir.relativeDir.toLowerCase()))) {
|
||||||
await showRestrictedDirectoryDialog(context, dir);
|
await showRestrictedDirectoryDialog(context, dir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,4 +26,14 @@ class VolumeRelativeDirectory extends Equatable {
|
||||||
'volumePath': volumePath,
|
'volumePath': volumePath,
|
||||||
'relativeDir': relativeDir,
|
'relativeDir': relativeDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
VolumeRelativeDirectory copyWith({
|
||||||
|
String? volumePath,
|
||||||
|
String? relativeDir,
|
||||||
|
}) {
|
||||||
|
return VolumeRelativeDirectory(
|
||||||
|
volumePath: volumePath ?? this.volumePath,
|
||||||
|
relativeDir: relativeDir ?? this.relativeDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue