#167 android: guard getExternalFilesDir, flutter: guard empty volume list

This commit is contained in:
Thibault Deckers 2022-01-28 10:25:40 +09:00
parent 6c53a11ea6
commit a62ad03851
3 changed files with 70 additions and 58 deletions

View file

@ -89,69 +89,78 @@ object StorageUtils {
} }
private fun findPrimaryVolumePath(context: Context): String? { private fun findPrimaryVolumePath(context: Context): String? {
// we want: try {
// /storage/emulated/0/ // we want:
// `Environment.getExternalStorageDirectory()` (deprecated) yields: // /storage/emulated/0/
// /storage/emulated/0 // `Environment.getExternalStorageDirectory()` (deprecated) yields:
// `context.getExternalFilesDir(null)` yields: // /storage/emulated/0
// /storage/emulated/0/Android/data/{package_name}/files // `context.getExternalFilesDir(null)` yields:
return appSpecificVolumePath(context.getExternalFilesDir(null)) // /storage/emulated/0/Android/data/{package_name}/files
return appSpecificVolumePath(context.getExternalFilesDir(null))
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to find primary volume path", e)
}
return null
} }
private fun findVolumePaths(context: Context): Array<String> { private fun findVolumePaths(context: Context): Array<String> {
// Final set of paths // Final set of paths
val paths = HashSet<String>() val paths = HashSet<String>()
// Primary emulated SD-CARD try {
val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: "" // Primary emulated SD-CARD
if (TextUtils.isEmpty(rawEmulatedStorageTarget)) { val rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET") ?: ""
// fix of empty raw emulated storage on marshmallow if (TextUtils.isEmpty(rawEmulatedStorageTarget)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // fix of empty raw emulated storage on marshmallow
lateinit var files: List<File> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
var validFiles: Boolean lateinit var files: List<File>
do { var validFiles: Boolean
// `getExternalFilesDirs` sometimes include `null` when called right after getting read access do {
// (e.g. on API 30 emulator) so we retry until the file system is ready // `getExternalFilesDirs` sometimes include `null` when called right after getting read access
val externalFilesDirs = context.getExternalFilesDirs(null) // (e.g. on API 30 emulator) so we retry until the file system is ready
validFiles = !externalFilesDirs.contains(null) val externalFilesDirs = context.getExternalFilesDirs(null)
if (validFiles) { validFiles = !externalFilesDirs.contains(null)
files = externalFilesDirs.filterNotNull() if (validFiles) {
} else { files = externalFilesDirs.filterNotNull()
try { } else {
Thread.sleep(100) try {
} catch (e: InterruptedException) { Thread.sleep(100)
Log.e(LOG_TAG, "insomnia", e) } catch (e: InterruptedException) {
Log.e(LOG_TAG, "insomnia", e)
}
} }
} } while (!validFiles)
} while (!validFiles) paths.addAll(files.mapNotNull(::appSpecificVolumePath))
paths.addAll(files.mapNotNull(::appSpecificVolumePath))
} else {
// Primary physical SD-CARD (not emulated)
val rawExternalStorage = System.getenv("EXTERNAL_STORAGE") ?: ""
// Device has physical external storage; use plain paths.
if (TextUtils.isEmpty(rawExternalStorage)) {
// EXTERNAL_STORAGE undefined; falling back to default.
paths.addAll(physicalPaths)
} else { } else {
paths.add(rawExternalStorage) // Primary physical SD-CARD (not emulated)
val rawExternalStorage = System.getenv("EXTERNAL_STORAGE") ?: ""
// Device has physical external storage; use plain paths.
if (TextUtils.isEmpty(rawExternalStorage)) {
// EXTERNAL_STORAGE undefined; falling back to default.
paths.addAll(physicalPaths)
} else {
paths.add(rawExternalStorage)
}
}
} else {
// Device has emulated storage; external storage paths should have userId burned into them.
// /storage/emulated/[0,1,2,...]/
val path = getPrimaryVolumePath(context)
val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: ""
if (rawUserId.isEmpty()) {
paths.add(rawEmulatedStorageTarget)
} else {
paths.add(rawEmulatedStorageTarget + File.separator + rawUserId)
} }
} }
} else {
// Device has emulated storage; external storage paths should have userId burned into them.
// /storage/emulated/[0,1,2,...]/
val path = getPrimaryVolumePath(context)
val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: ""
if (rawUserId.isEmpty()) {
paths.add(rawEmulatedStorageTarget)
} else {
paths.add(rawEmulatedStorageTarget + File.separator + rawUserId)
}
}
// All Secondary SD-CARDs (all exclude primary) separated by ":" // All Secondary SD-CARDs (all exclude primary) separated by ":"
System.getenv("SECONDARY_STORAGE")?.let { secondaryStorages -> System.getenv("SECONDARY_STORAGE")?.let { secondaryStorages ->
paths.addAll(secondaryStorages.split(File.pathSeparator).filter { it.isNotEmpty() }) paths.addAll(secondaryStorages.split(File.pathSeparator).filter { it.isNotEmpty() })
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to find volume paths", e)
} }
return paths.map { ensureTrailingSeparator(it) }.toTypedArray() return paths.map { ensureTrailingSeparator(it) }.toTypedArray()

View file

@ -23,13 +23,13 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
final ValueNotifier<bool> _existsNotifier = ValueNotifier(false); final ValueNotifier<bool> _existsNotifier = ValueNotifier(false);
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false); final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
late Set<StorageVolume> _allVolumes; late Set<StorageVolume> _allVolumes;
late StorageVolume _primaryVolume, _selectedVolume; late StorageVolume? _primaryVolume, _selectedVolume;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_allVolumes = androidFileUtils.storageVolumes; _allVolumes = androidFileUtils.storageVolumes;
_primaryVolume = _allVolumes.firstWhere((volume) => volume.isPrimary, orElse: () => _allVolumes.first); _primaryVolume = _allVolumes.firstWhereOrNull((volume) => volume.isPrimary) ?? _allVolumes.firstOrNull;
_selectedVolume = _primaryVolume; _selectedVolume = _primaryVolume;
_nameFieldFocusNode.addListener(_onFocus); _nameFieldFocusNode.addListener(_onFocus);
} }
@ -144,8 +144,9 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
} }
String _buildAlbumPath(String name) { String _buildAlbumPath(String name) {
if (name.isEmpty) return ''; final selectedVolume = _selectedVolume;
return pContext.join(_selectedVolume.path, 'Pictures', name); if (selectedVolume == null || name.isEmpty) return '';
return pContext.join(selectedVolume.path, 'Pictures', name);
} }
Future<void> _validate() async { Future<void> _validate() async {

View file

@ -37,8 +37,10 @@ class _FilePickerState extends State<FilePicker> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final primaryVolume = volumes.firstWhere((v) => v.isPrimary); final primaryVolume = volumes.firstWhereOrNull((v) => v.isPrimary);
_goTo(primaryVolume.path); if (primaryVolume != null) {
_goTo(primaryVolume.path);
}
} }
@override @override