diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b8f116a9..055b29ff 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,10 +8,11 @@ - + (); Directory source = profilesWidget.baseDirectory(); - Directory destination = Directory(value); + Directory destination = Directory(value); moveDirectory(source.path, destination.path).then((value) async { isMovingDirectory.value = false; update(); if (value == "same") { return; - } else if (value == "success") { + } else if (value == "success") { profilesWidget.setBaseDirectory(destination); SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString('baseDirectory', destination.path); baseDirectory.value = destination.path; - } else { - showDialog( - context: context, - builder: (BuildContext context) { - return Utils.showAlertDialog( + Get.snackbar( + 'Success', + 'Base directory moved successfully', + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + ); + } else { + Get.dialog( + Utils.showAlertDialog( title: Text( 'Error', style: GoogleFonts.poppins( @@ -83,7 +87,9 @@ class SettingsController extends GetxController { ? "Cannot move to a nested directory" : value == "not-empty" ? "Destination directory is not empty" - : "An error occurred", + : value == "not-permitted" + ? "Selected folder can't be written to (Android SAF). Please choose a different folder." + : "An error occurred", style: GoogleFonts.poppins( color: TaskWarriorColors.grey, fontSize: TaskWarriorFonts.fontSizeSmall, @@ -92,7 +98,7 @@ class SettingsController extends GetxController { actions: [ TextButton( onPressed: () { - Navigator.pop(context); + Get.back(); }, child: Text( 'OK', @@ -102,10 +108,9 @@ class SettingsController extends GetxController { ), ) ], - ); - }, - ); - } + ), + ); + } }); } }); @@ -120,16 +125,48 @@ class SettingsController extends GetxController { return "nested"; } - Directory toDir = Directory(toDirectory); + Directory toDir = Directory(toDirectory); + // Ensure destination exists before checking contents + await toDir.create(recursive: true); final length = await toDir.list().length; if (length > 0) { return "not-empty"; } - await moveDirectoryRecurse(fromDirectory, toDirectory); - return "success"; + // Preflight: on Android, check that we can actually write to the chosen directory + // to avoid crashing with Operation not permitted when a SAF tree URI was selected. + try { + final testFile = File(path.join(toDirectory, ".tw_write_test")); + await toDir.create(recursive: true); + await testFile.writeAsString("ok"); + await testFile.delete(); + } on FileSystemException catch (e) { + // Map common permission error to a friendly status + if (e.osError?.errorCode == 1 || + (e.osError?.message.toLowerCase().contains("operation not permitted") ?? false)) { + return "not-permitted"; + } + return "error"; + } catch (_) { + return "error"; + } + + try { + await moveDirectoryRecurse(fromDirectory, toDirectory); + return "success"; + } on FileSystemException catch (e) { + if (e.osError?.errorCode == 1 || + (e.osError?.message.toLowerCase().contains("operation not permitted") ?? false)) { + return "not-permitted"; + } + return "error"; + } catch (_) { + return "error"; + } } + // ... no hardcoded SAF path mapping; rely on guard and proper APIs if enabled in future + Future moveDirectoryRecurse( String fromDirectory, String toDirectory) async { Directory fromDir = Directory(fromDirectory); @@ -140,18 +177,21 @@ class SettingsController extends GetxController { // Loop through each file and directory and move it to the toDirectory await for (final entity in fromDir.list()) { + // Skip flutter runtime assets – they should not be moved + final relativePath = path.relative(entity.path, from: fromDirectory); + if (relativePath.split(path.separator).contains('flutter_assets')) { + continue; + } if (entity is File) { // If it's a file, move it to the toDirectory File file = entity; - String newPath = path.join( - toDirectory, path.relative(file.path, from: fromDirectory)); + String newPath = path.join(toDirectory, relativePath); await File(newPath).writeAsBytes(await file.readAsBytes()); await file.delete(); } else if (entity is Directory) { // If it's a directory, create it in the toDirectory and recursively move its contents Directory dir = entity; - String newPath = path.join( - toDirectory, path.relative(dir.path, from: fromDirectory)); + String newPath = path.join(toDirectory, relativePath); Directory newDir = Directory(newPath); await newDir.create(recursive: true); await moveDirectoryRecurse(dir.path, newPath);