Skip to content

Commit 9cd58b3

Browse files
harikrishna-auNallana Hari Krishna
andauthored
fix(android,settings): prevent hang when selecting directory; guard SAF destinations; skip flutter runtime assets; use context-free dialogs; add success feedback (fixes #484) (#498)
Co-authored-by: Nallana Hari Krishna <[email protected]>
1 parent 630f359 commit 9cd58b3

File tree

2 files changed

+65
-24
lines changed

2 files changed

+65
-24
lines changed

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
99
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
1010
<uses-permission android:name="android.permission.VIBRATE" />
11-
<application
12-
android:label="Taskwarrior"
13-
android:name="${applicationName}"
14-
android:icon="@mipmap/launcher_icon">
11+
<application
12+
android:label="Taskwarrior"
13+
android:name="${applicationName}"
14+
android:icon="@mipmap/launcher_icon"
15+
android:enableOnBackInvokedCallback="true">
1516
<activity
1617
android:name=".MainActivity"
1718
android:exported="true"

lib/app/modules/settings/controllers/settings_controller.dart

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,26 @@ class SettingsController extends GetxController {
5454
// InheritedProfiles profilesWidget = ProfilesWidget.of(context);
5555
var profilesWidget = Get.find<SplashController>();
5656
Directory source = profilesWidget.baseDirectory();
57-
Directory destination = Directory(value);
57+
Directory destination = Directory(value);
5858
moveDirectory(source.path, destination.path).then((value) async {
5959
isMovingDirectory.value = false;
6060
update();
6161
if (value == "same") {
6262
return;
63-
} else if (value == "success") {
63+
} else if (value == "success") {
6464
profilesWidget.setBaseDirectory(destination);
6565
SharedPreferences prefs = await SharedPreferences.getInstance();
6666
prefs.setString('baseDirectory', destination.path);
6767
baseDirectory.value = destination.path;
68-
} else {
69-
showDialog(
70-
context: context,
71-
builder: (BuildContext context) {
72-
return Utils.showAlertDialog(
68+
Get.snackbar(
69+
'Success',
70+
'Base directory moved successfully',
71+
snackPosition: SnackPosition.BOTTOM,
72+
duration: const Duration(seconds: 2),
73+
);
74+
} else {
75+
Get.dialog(
76+
Utils.showAlertDialog(
7377
title: Text(
7478
'Error',
7579
style: GoogleFonts.poppins(
@@ -83,7 +87,9 @@ class SettingsController extends GetxController {
8387
? "Cannot move to a nested directory"
8488
: value == "not-empty"
8589
? "Destination directory is not empty"
86-
: "An error occurred",
90+
: value == "not-permitted"
91+
? "Selected folder can't be written to (Android SAF). Please choose a different folder."
92+
: "An error occurred",
8793
style: GoogleFonts.poppins(
8894
color: TaskWarriorColors.grey,
8995
fontSize: TaskWarriorFonts.fontSizeSmall,
@@ -92,7 +98,7 @@ class SettingsController extends GetxController {
9298
actions: [
9399
TextButton(
94100
onPressed: () {
95-
Navigator.pop(context);
101+
Get.back();
96102
},
97103
child: Text(
98104
'OK',
@@ -102,10 +108,9 @@ class SettingsController extends GetxController {
102108
),
103109
)
104110
],
105-
);
106-
},
107-
);
108-
}
111+
),
112+
);
113+
}
109114
});
110115
}
111116
});
@@ -120,16 +125,48 @@ class SettingsController extends GetxController {
120125
return "nested";
121126
}
122127

123-
Directory toDir = Directory(toDirectory);
128+
Directory toDir = Directory(toDirectory);
129+
// Ensure destination exists before checking contents
130+
await toDir.create(recursive: true);
124131
final length = await toDir.list().length;
125132
if (length > 0) {
126133
return "not-empty";
127134
}
128135

129-
await moveDirectoryRecurse(fromDirectory, toDirectory);
130-
return "success";
136+
// Preflight: on Android, check that we can actually write to the chosen directory
137+
// to avoid crashing with Operation not permitted when a SAF tree URI was selected.
138+
try {
139+
final testFile = File(path.join(toDirectory, ".tw_write_test"));
140+
await toDir.create(recursive: true);
141+
await testFile.writeAsString("ok");
142+
await testFile.delete();
143+
} on FileSystemException catch (e) {
144+
// Map common permission error to a friendly status
145+
if (e.osError?.errorCode == 1 ||
146+
(e.osError?.message.toLowerCase().contains("operation not permitted") ?? false)) {
147+
return "not-permitted";
148+
}
149+
return "error";
150+
} catch (_) {
151+
return "error";
152+
}
153+
154+
try {
155+
await moveDirectoryRecurse(fromDirectory, toDirectory);
156+
return "success";
157+
} on FileSystemException catch (e) {
158+
if (e.osError?.errorCode == 1 ||
159+
(e.osError?.message.toLowerCase().contains("operation not permitted") ?? false)) {
160+
return "not-permitted";
161+
}
162+
return "error";
163+
} catch (_) {
164+
return "error";
165+
}
131166
}
132167

168+
// ... no hardcoded SAF path mapping; rely on guard and proper APIs if enabled in future
169+
133170
Future<void> moveDirectoryRecurse(
134171
String fromDirectory, String toDirectory) async {
135172
Directory fromDir = Directory(fromDirectory);
@@ -140,18 +177,21 @@ class SettingsController extends GetxController {
140177

141178
// Loop through each file and directory and move it to the toDirectory
142179
await for (final entity in fromDir.list()) {
180+
// Skip flutter runtime assets – they should not be moved
181+
final relativePath = path.relative(entity.path, from: fromDirectory);
182+
if (relativePath.split(path.separator).contains('flutter_assets')) {
183+
continue;
184+
}
143185
if (entity is File) {
144186
// If it's a file, move it to the toDirectory
145187
File file = entity;
146-
String newPath = path.join(
147-
toDirectory, path.relative(file.path, from: fromDirectory));
188+
String newPath = path.join(toDirectory, relativePath);
148189
await File(newPath).writeAsBytes(await file.readAsBytes());
149190
await file.delete();
150191
} else if (entity is Directory) {
151192
// If it's a directory, create it in the toDirectory and recursively move its contents
152193
Directory dir = entity;
153-
String newPath = path.join(
154-
toDirectory, path.relative(dir.path, from: fromDirectory));
194+
String newPath = path.join(toDirectory, relativePath);
155195
Directory newDir = Directory(newPath);
156196
await newDir.create(recursive: true);
157197
await moveDirectoryRecurse(dir.path, newPath);

0 commit comments

Comments
 (0)