#167 android: guard getExternalFilesDir, flutter: guard empty volume list
This commit is contained in:
parent
6c53a11ea6
commit
a62ad03851
3 changed files with 70 additions and 58 deletions
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue