Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:label="Taskwarrior"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
<application
android:label="Taskwarrior"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
80 changes: 60 additions & 20 deletions lib/app/modules/settings/controllers/settings_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,22 +54,26 @@ class SettingsController extends GetxController {
// InheritedProfiles profilesWidget = ProfilesWidget.of(context);
var profilesWidget = Get.find<SplashController>();
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(
Expand All @@ -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,
Expand All @@ -92,7 +98,7 @@ class SettingsController extends GetxController {
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
Get.back();
},
child: Text(
'OK',
Expand All @@ -102,10 +108,9 @@ class SettingsController extends GetxController {
),
)
],
);
},
);
}
),
);
}
});
}
});
Expand All @@ -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<void> moveDirectoryRecurse(
String fromDirectory, String toDirectory) async {
Directory fromDir = Directory(fromDirectory);
Expand All @@ -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);
Expand Down