What changed, and why it matters
This commit changes how the Stack Wallet app on Android reads and writes files. It replaces a workaround that directly reached into shared phone storage with the proper Android Storage Access Framework (SAF), which asks the user to pick a folder or file before the app can use it. That is generally a security improvement because it reduces the app's direct access to files it does not own. However, the change is large, touches backup/restore code, and some backup paths still appear to use direct file access on non-Android platforms, so it should be reviewed carefully rather than treated as purely safe.
Treat this as a hardening/refactoring change rather than an active vulnerability. Review the new FS helper for edge cases: ensure content:// URIs are validated before use, confirm that backup restore paths cannot be tricked by malicious content:// URIs or path traversal in file names, and verify that the switch from a forked file_picker to the upstream package does not reintroduce permission issues. Test backup creation and restore on Android 10+ scoped-storage devices.
Security signals we found
Removal of hard-coded Android shared-storage path helper (wtfAndroidDocumentsPath)
Adoption of Android Storage Access Framework (SAF) via saf_util / saf_stream
User-mediated directory/file picking on Android replaces direct filesystem access
Backup creation now writes encrypted backups through a shared FS helper
Validation logic special-cases content:// URIs when checking directory existence
Desktop and iOS paths still use direct File I/O and file_picker
Evidence from the diff
The patch migrates several Android file I/O flows from a custom helper (StackFileSystem.wtfAndroidDocumentsPath, which built a hard-coded /storage/emulated/
Changed components
lib/utilities/fs.dart (new SAF wrapper)lib/utilities/stack_file_system.dartlib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dartlib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dartlib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dartlib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dartlib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dartlib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dartlib/pages/monkey/monkey_view.dartlib/pages/ordinals/ordinal_details_view.dartpubspec.lock / pubspec.template.yamlInspect captured patch +502 / −486
diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart
index 49329cc..e16f8d6 100644
--- a/lib/pages/monkey/monkey_view.dart
+++ b/lib/pages/monkey/monkey_view.dart
@@ -6,6 +6,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
+import 'package:saf_stream/saf_stream.dart';
+import 'package:saf_util/saf_util.dart';
import '../../notifications/show_flush_bar.dart';
import '../../providers/global/wallets_provider.dart';
@@ -13,8 +15,8 @@ import '../../services/monkey_service.dart';
import '../../themes/coin_icon_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
+import '../../utilities/fs.dart';
import '../../utilities/show_loading.dart';
-import '../../utilities/stack_file_system.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
@@ -51,13 +53,13 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
.updateMonkeyImageBytes(monKeyBytes.toList());
}
- Future<Directory?> _getDocsDir() async {
+ Future<String?> _getDocsDir() async {
try {
if (Platform.isAndroid) {
- return await StackFileSystem.wtfAndroidDocumentsPath();
+ return await FS.pickDirectory();
}
- return await getApplicationDocumentsDirectory();
+ return (await getApplicationDocumentsDirectory()).path;
} catch (_) {
return null;
}
@@ -70,27 +72,40 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
bool isPNG = false,
bool overwrite = false,
}) async {
- final dir = await _getDocsDir();
- if (dir == null) {
- throw Exception("Failed to get documents directory to save monKey image");
+ final dirPath = await _getDocsDir();
+ if (dirPath == null) {
+ throw Exception("Failed to get directory path to save monKey image");
}
- final address =
- await ref
- .read(pWallets)
- .getWallet(walletId)
- .getCurrentReceivingAddress();
- String filePath = path.join(dir.path, "monkey_${address?.value}");
+ final address = await ref
+ .read(pWallets)
+ .getWallet(walletId)
+ .getCurrentReceivingAddress();
- filePath += isPNG ? ".png" : ".svg";
+ final fileName = "monkey_${address?.value}${isPNG ? ".png" : ".svg"}";
+ final filePath = path.join(dirPath, fileName);
- final File imgFile = File(filePath);
+ if (Platform.isAndroid) {
+ if (!overwrite && await SafUtil().exists(filePath, false)) {
+ throw Exception("File already exists");
+ }
+
+ await SafStream().writeFileBytes(
+ dirPath,
+ fileName,
+ isPNG ? "png" : "svg",
+ bytes,
+ );
+ } else {
+ final File imgFile = File(filePath);
+
+ if (imgFile.existsSync() && !overwrite) {
+ throw Exception("File already exists");
+ }
- if (imgFile.existsSync() && !overwrite) {
- throw Exception("File already exists");
+ await imgFile.writeAsBytes(bytes);
}
- await imgFile.writeAsBytes(bytes);
_monkeyPath = filePath;
}
@@ -113,313 +128,296 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
return Background(
child: ConditionalParent(
condition: isDesktop,
- builder:
- (child) => DesktopScaffold(
- appBar: DesktopAppBar(
- background: Theme.of(context).extension<StackColors>()!.popupBG,
- leading: Expanded(
- child: Row(
- children: [
- const SizedBox(width: 32),
- AppBarIconButton(
- size: 32,
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textFieldDefaultBG,
- shadows: const [],
- icon: SvgPicture.asset(
- Assets.svg.arrowLeft,
- width: 18,
- height: 18,
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.topNavIconPrimary,
- ),
- onPressed: Navigator.of(context).pop,
- ),
- const SizedBox(width: 15),
- SvgPicture.asset(
- Assets.svg.monkey,
- width: 32,
- height: 32,
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textSubtitle1,
- ),
- const SizedBox(width: 12),
- Text("MonKey", style: STextStyles.desktopH3(context)),
- ],
+ builder: (child) => DesktopScaffold(
+ appBar: DesktopAppBar(
+ background: Theme.of(context).extension<StackColors>()!.popupBG,
+ leading: Expanded(
+ child: Row(
+ children: [
+ const SizedBox(width: 32),
+ AppBarIconButton(
+ size: 32,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textFieldDefaultBG,
+ shadows: const [],
+ icon: SvgPicture.asset(
+ Assets.svg.arrowLeft,
+ width: 18,
+ height: 18,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.topNavIconPrimary,
+ ),
+ onPressed: Navigator.of(context).pop,
),
- ),
- trailing: RawMaterialButton(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(1000),
+ const SizedBox(width: 15),
+ SvgPicture.asset(
+ Assets.svg.monkey,
+ width: 32,
+ height: 32,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textSubtitle1,
),
- onPressed: () {
- showDialog<void>(
- context: context,
- useSafeArea: false,
- barrierDismissible: true,
- builder: (context) {
- return DesktopDialog(
- maxHeight: double.infinity,
- child: Column(
+ const SizedBox(width: 12),
+ Text("MonKey", style: STextStyles.desktopH3(context)),
+ ],
+ ),
+ ),
+ trailing: RawMaterialButton(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(1000),
+ ),
+ onPressed: () {
+ showDialog<void>(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return DesktopDialog(
+ maxHeight: double.infinity,
+ child: Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Row(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- children: [
- Padding(
- padding: const EdgeInsets.only(left: 32),
- child: Text(
- "About MonKeys",
- style: STextStyles.desktopH3(context),
- ),
- ),
- const DesktopDialogCloseButton(),
- ],
- ),
- Text(
- "A MonKey is a visual representation of your Banano address.",
- style: STextStyles.desktopTextMedium(
- context,
- ).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textDark3,
+ Padding(
+ padding: const EdgeInsets.only(left: 32),
+ child: Text(
+ "About MonKeys",
+ style: STextStyles.desktopH3(context),
),
),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Padding(
- padding: const EdgeInsets.all(32),
- child: PrimaryButton(
- width: 272.5,
- label: "OK",
- onPressed: () {
- Navigator.of(context).pop();
- },
- ),
- ),
- ],
+ const DesktopDialogCloseButton(),
+ ],
+ ),
+ Text(
+ "A MonKey is a visual representation of your Banano address.",
+ style: STextStyles.desktopTextMedium(context)
+ .copyWith(
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textDark3,
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(32),
+ child: PrimaryButton(
+ width: 272.5,
+ label: "OK",
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
),
],
),
- );
- },
+ ],
+ ),
);
},
- child: Padding(
- padding: const EdgeInsets.symmetric(
- vertical: 19,
- horizontal: 32,
+ );
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 19,
+ horizontal: 32,
+ ),
+ child: Row(
+ children: [
+ SvgPicture.asset(
+ Assets.svg.circleQuestion,
+ width: 20,
+ height: 20,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.customTextButtonEnabledText,
),
- child: Row(
- children: [
- SvgPicture.asset(
- Assets.svg.circleQuestion,
- width: 20,
- height: 20,
- color:
- Theme.of(context)
- .extension<StackColors>()!
- .customTextButtonEnabledText,
- ),
- const SizedBox(width: 8),
- Text(
- "What is MonKey?",
- style: STextStyles.desktopMenuItemSelected(
- context,
- ).copyWith(
- color:
- Theme.of(context)
- .extension<StackColors>()!
- .customTextButtonEnabledText,
+ const SizedBox(width: 8),
+ Text(
+ "What is MonKey?",
+ style: STextStyles.desktopMenuItemSelected(context)
+ .copyWith(
+ color: Theme.of(context)
+ .extension<StackColors>()!
+ .customTextButtonEnabledText,
),
- ),
- ],
),
- ),
+ ],
),
- useSpacers: false,
- isCompactHeight: true,
),
- body: child,
),
+ useSpacers: false,
+ isCompactHeight: true,
+ ),
+ body: child,
+ ),
child: ConditionalParent(
condition: !isDesktop,
- builder:
- (child) => Scaffold(
- appBar: AppBar(
- leading: AppBarBackButton(
+ builder: (child) => Scaffold(
+ appBar: AppBar(
+ leading: AppBarBackButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ title: Text("MonKey", style: STextStyles.navBarTitle(context)),
+ actions: [
+ AspectRatio(
+ aspectRatio: 1,
+ child: AppBarIconButton(
+ icon: SvgPicture.asset(Assets.svg.circleQuestion),
onPressed: () {
- Navigator.of(context).pop();
- },
- ),
- title: Text(
- "MonKey",
- style: STextStyles.navBarTitle(context),
- ),
- actions: [
- AspectRatio(
- aspectRatio: 1,
- child: AppBarIconButton(
- icon: SvgPicture.asset(Assets.svg.circleQuestion),
- onPressed: () {
- showDialog<dynamic>(
- context: context,
- useSafeArea: false,
- barrierDismissible: true,
- builder: (context) {
- return const StackOkDialog(
- title: "About MonKeys",
- message:
- "A MonKey is a visual representation of your Banano address.",
- );
- },
+ showDialog<dynamic>(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return const StackOkDialog(
+ title: "About MonKeys",
+ message:
+ "A MonKey is a visual representation of your Banano address.",
);
},
- ),
- ),
- ],
+ );
+ },
+ ),
),
- body: SafeArea(child: child),
- ),
+ ],
+ ),
+ body: SafeArea(child: child),
+ ),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => SizedBox(width: 318, child: child),
child: ConditionalParent(
condition: imageBytes != null,
- builder:
- (_) => Column(
- children: [
- isDesktop
- ? const SizedBox(height: 50)
- : const Spacer(flex: 1),
- if (imageBytes != null)
- SizedBox(
- width: 300,
- height: 300,
- child: SvgPicture.memory(
- Uint8List.fromList(imageBytes!),
- ),
- ),
- isDesktop
- ? const SizedBox(height: 50)
- : const Spacer(flex: 1),
- Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- children: [
- SecondaryButton(
- label: "Save as SVG",
- onPressed: () async {
- bool didError = false;
- await showLoading(
- whileFuture: Future.wait([
- _saveMonKeyToFile(
- bytes: Uint8List.fromList(
- (wallet as BananoWallet)
- .getMonkeyImageBytes()!,
- ),
- ),
- Future<void>.delayed(
- const Duration(seconds: 2),
- ),
- ]),
+ builder: (_) => Column(
+ children: [
+ isDesktop
+ ? const SizedBox(height: 50)
+ : const Spacer(flex: 1),
+ if (imageBytes != null)
+ SizedBox(
+ width: 300,
+ height: 300,
+ child: SvgPicture.memory(Uint8List.fromList(imageBytes!)),
+ ),
+ isDesktop
+ ? const SizedBox(height: 50)
+ : const Spacer(flex: 1),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ children: [
+ SecondaryButton(
+ label: "Save as SVG",
+ onPressed: () async {
+ bool didError = false;
+ await showLoading(
+ whileFuture: Future.wait([
+ _saveMonKeyToFile(
+ bytes: Uint8List.fromList(
+ (wallet as BananoWallet)
+ .getMonkeyImageBytes()!,
+ ),
+ ),
+ Future<void>.delayed(
+ const Duration(seconds: 2),
+ ),
+ ]),
+ context: context,
+ rootNavigator: Util.isDesktop,
+ message: "Saving MonKey svg",
+ onException: (e) {
+ didError = true;
+ String msg = e.toString();
+ while (msg.isNotEmpty &&
+ msg.startsWith("Exception:")) {
+ msg = msg.substring(10).trim();
+ }
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: msg,
context: context,
- rootNavigator: Util.isDesktop,
- message: "Saving MonKey svg",
- onException: (e) {
- didError = true;
- String msg = e.toString();
- while (msg.isNotEmpty &&
- msg.startsWith("Exception:")) {
- msg = msg.substring(10).trim();
- }
- showFloatingFlushBar(
- type: FlushBarType.warning,
- message: msg,
- context: context,
- );
- },
);
+ },
+ );
- if (!didError && mounted) {
- await showFloatingFlushBar(
- type: FlushBarType.success,
- message:
- "SVG MonKey image saved to $_monkeyPath",
- context: context,
- );
+ if (!didError && mounted) {
+ await showFloatingFlushBar(
+ type: FlushBarType.success,
+ message:
+ "SVG MonKey image saved to $_monkeyPath",
+ context: context,
+ );
+ }
+ },
+ ),
+ const SizedBox(height: 12),
+ SecondaryButton(
+ label: "Download as PNG",
+ onPressed: () async {
+ bool didError = false;
+ await showLoading(
+ whileFuture: Future.wait([
+ wallet.getCurrentReceivingAddress().then(
+ (address) async => await ref
+ .read(pMonKeyService)
+ .fetchMonKey(
+ address: address!.value,
+ png: true,
+ )
+ .then(
+ (monKeyBytes) async =>
+ await _saveMonKeyToFile(
+ bytes: monKeyBytes,
+ isPNG: true,
+ ),
+ ),
+ ),
+ Future<void>.delayed(
+ const Duration(seconds: 2),
+ ),
+ ]),
+ context: context,
+ rootNavigator: Util.isDesktop,
+ message: "Downloading MonKey png",
+ onException: (e) {
+ didError = true;
+ String msg = e.toString();
+ while (msg.isNotEmpty &&
+ msg.startsWith("Exception:")) {
+ msg = msg.substring(10).trim();
}
- },
- ),
- const SizedBox(height: 12),
- SecondaryButton(
- label: "Download as PNG",
- onPressed: () async {
- bool didError = false;
- await showLoading(
- whileFuture: Future.wait([
- wallet.getCurrentReceivingAddress().then(
- (address) async => await ref
- .read(pMonKeyService)
- .fetchMonKey(
- address: address!.value,
- png: true,
- )
- .then(
- (monKeyBytes) async =>
- await _saveMonKeyToFile(
- bytes: monKeyBytes,
- isPNG: true,
- ),
- ),
- ),
- Future<void>.delayed(
- const Duration(seconds: 2),
- ),
- ]),
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: msg,
context: context,
- rootNavigator: Util.isDesktop,
- message: "Downloading MonKey png",
- onException: (e) {
- didError = true;
- String msg = e.toString();
- while (msg.isNotEmpty &&
- msg.startsWith("Exception:")) {
- msg = msg.substring(10).trim();
- }
- showFloatingFlushBar(
- type: FlushBarType.warning,
- message: msg,
- context: context,
- );
- },
);
-
- if (!didError && mounted) {
- await showFloatingFlushBar(
- type: FlushBarType.success,
- message:
- "PNG MonKey image saved to $_monkeyPath",
- context: context,
- );
- }
},
- ),
- ],
+ );
+
+ if (!didError && mounted) {
+ await showFloatingFlushBar(
+ type: FlushBarType.success,
+ message:
+ "PNG MonKey image saved to $_monkeyPath",
+ context: context,
+ );
+ }
+ },
),
- ),
- // child,
- ],
+ ],
+ ),
),
+ // child,
+ ],
+ ),
child: Column(
children: [
isDesktop
@@ -440,10 +438,9 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
Text(
"You do not have a MonKey yet. \nFetch yours now!",
style: STextStyles.smallMed14(context).copyWith(
- color:
- Theme.of(
- context,
- ).extension<StackColors>()!.textDark3,
+ color: Theme.of(
+ context,
+ ).extension<StackColors>()!.textDark3,
),
textAlign: TextAlign.center,
),
@@ -489,8 +486,8 @@ class _MonkeyViewState extends ConsumerState<MonkeyView> {
},
);
- imageBytes =
- (wallet as BananoWallet).getMonkeyImageBytes();
+ imageBytes = (wallet as BananoWallet)
+ .getMonkeyImageBytes();
if (imageBytes != null) {
setState(() {});
diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart
index 996f20d..7ea7c2d 100644
--- a/lib/pages/ordinals/ordinal_details_view.dart
+++ b/lib/pages/ordinals/ordinal_details_view.dart
@@ -7,6 +7,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
+import 'package:saf_stream/saf_stream.dart';
+import 'package:saf_util/saf_util.dart';
import '../../app_config.dart';
import '../../models/isar/models/blockchain_data/utxo.dart';
@@ -21,8 +23,8 @@ import '../../utilities/amount/amount.dart';
import '../../utilities/amount/amount_formatter.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
+import '../../utilities/fs.dart';
import '../../utilities/show_loading.dart';
-import '../../utilities/stack_file_system.dart';
import '../../utilities/text_styles.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../widgets/background.dart';
@@ -210,6 +212,18 @@ class _OrdinalImageGroup extends ConsumerWidget {
static const _spacing = 12.0;
+ Future<String?> _getDocsDir() async {
+ try {
+ if (Platform.isAndroid) {
+ return await FS.pickDirectory();
+ }
+
+ return (await getApplicationDocumentsDirectory()).path;
+ } catch (_) {
+ return null;
+ }
+ }
+
Future<String> _savePngToFile(WidgetRef ref) async {
final HTTP client = HTTP();
@@ -230,21 +244,36 @@ class _OrdinalImageGroup extends ConsumerWidget {
final bytes = response.bodyBytes;
- final dir = Platform.isAndroid
- ? await StackFileSystem.wtfAndroidDocumentsPath()
- : await getApplicationDocumentsDirectory();
- final filePath = path.join(
- dir.path,
- "ordinal_${ordinal.inscriptionNumber}.png",
- );
+ final dirPath = await _getDocsDir();
+ if (dirPath == null) {
+ throw Exception("Failed to get directory path to save ordinal image");
+ }
+
+ final fileName = "ordinal_${ordinal.inscriptionNumber}.png";
+
+ final filePath = path.join(dirPath, fileName);
+
+ if (Platform.isAndroid) {
+ if (await SafUtil().exists(filePath, false)) {
+ throw Exception("File already exists");
+ }
+
+ await SafStream().writeFileBytes(
+ dirPath,
+ fileName,
+ "png",
+ Uint8List.fromList(bytes),
+ );
+ } else {
+ final File imgFile = File(filePath);
- final File imgFile = File(filePath);
+ if (imgFile.existsSync()) {
+ throw Exception("File already exists");
+ }
- if (imgFile.existsSync()) {
- throw Exception("File already exists");
+ await imgFile.writeAsBytes(bytes);
}
- await imgFile.writeAsBytes(bytes);
return filePath;
}
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart
index eae292f..c1bd1b2 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_auto_backup_view.dart
@@ -27,6 +27,7 @@ import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/flutter_secure_storage_interface.dart';
import '../../../../utilities/format.dart';
+import '../../../../utilities/fs.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
@@ -120,16 +121,11 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
adkVersion,
);
- if (Platform.isAndroid) {
- // TODO SAF
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- } else {
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- }
+ await FS.writeStringToFile(
+ encryptedDataString,
+ pathToSave,
+ fileToSavePath.split("/").last,
+ );
return fileToSavePath;
}(),
@@ -263,18 +259,16 @@ class _EnableAutoBackupViewState extends ConsumerState<CreateAutoBackupView> {
: () async {
try {
await stackFileSystem.prepareStorage();
-
- if (mounted) {
- await stackFileSystem.pickDir(
- context,
- );
- }
-
if (mounted) {
- setState(() {
- fileLocationController.text =
- stackFileSystem.dirPath ?? "";
- });
+ final filePath = await stackFileSystem
+ .openFile();
+
+ if (mounted) {
+ setState(() {
+ fileLocationController.text =
+ filePath ?? "";
+ });
+ }
}
} catch (e, s) {
Logging.instance.e(
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart
index f2d2bc2..0df1b21 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/create_backup_view.dart
@@ -22,6 +22,7 @@ import '../../../../providers/global/secure_store_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
+import '../../../../utilities/fs.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
@@ -100,16 +101,11 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
final encryptedDataString = await SWB
.encryptStackWalletWithPassphrase(passphrase, jsonEncode(backup));
- if (Platform.isAndroid) {
- // TODO SAF
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- } else {
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- }
+ await FS.writeStringToFile(
+ encryptedDataString,
+ pathToSave,
+ fileToSavePath.split("/").last,
+ );
return fileToSavePath;
}(),
@@ -312,16 +308,16 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
: () async {
try {
await stackFileSystem.prepareStorage();
-
- if (context.mounted) {
- await stackFileSystem.pickDir(context);
- }
-
if (mounted) {
- setState(() {
- fileLocationController.text =
- stackFileSystem.dirPath ?? "";
- });
+ final filePath = await stackFileSystem
+ .openFile();
+
+ if (mounted) {
+ setState(() {
+ fileLocationController.text =
+ filePath ?? "";
+ });
+ }
}
} catch (e, s) {
Logging.instance.e("", error: e, stackTrace: s);
@@ -366,8 +362,7 @@ class _RestoreFromFileViewState extends ConsumerState<CreateBackupView> {
);
},
),
- if (!Platform.isAndroid && !Platform.isIOS)
- SizedBox(height: !isDesktop ? 8 : 24),
+ if (!Platform.isIOS) SizedBox(height: !isDesktop ? 8 : 24),
if (isDesktop)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart
index e1d5fd3..81d7b9d 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/edit_auto_backup_view.dart
@@ -28,6 +28,7 @@ import '../../../../utilities/constants.dart';
import '../../../../utilities/enums/backup_frequency_type.dart';
import '../../../../utilities/flutter_secure_storage_interface.dart';
import '../../../../utilities/format.dart';
+import '../../../../utilities/fs.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
@@ -131,16 +132,11 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
adkVersion,
);
- if (Platform.isAndroid) {
- // TODO SAF
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- } else {
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- }
+ await FS.writeStringToFile(
+ encryptedDataString,
+ pathToSave,
+ fileToSavePath.split("/").last,
+ );
return fileToSavePath;
}(),
@@ -305,16 +301,14 @@ class _EditAutoBackupViewState extends ConsumerState<EditAutoBackupView> {
: () async {
try {
await stackFileSystem.prepareStorage();
-
- if (context.mounted) {
- await stackFileSystem.pickDir(context);
- }
-
if (mounted) {
- setState(() {
- fileLocationController.text =
- stackFileSystem.dirPath ?? "";
- });
+ final filePath = await stackFileSystem.openFile();
+
+ if (mounted) {
+ setState(() {
+ fileLocationController.text = filePath ?? "";
+ });
+ }
}
} catch (e, s) {
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart
index 58333ed..a681186 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart
@@ -102,7 +102,8 @@ bool validateFail(
) {
for (final e in [
[pathToSave.isEmpty, "Directory not chosen"],
- [!(Directory(pathToSave).existsSync()), "Directory does not exist"],
+ if (!pathToSave.startsWith("content://"))
+ [!(Directory(pathToSave).existsSync()), "Directory does not exist"],
[passphrase.isEmpty, "A passphrase is required"],
[passphrase != repeatPassphrase, "Passphrase does not match"],
]) {
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart
index 9954cb0..c02f91d 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart
@@ -11,94 +11,63 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
-import 'package:flutter/material.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '../../../../../app_config.dart';
-import '../../../../../utilities/stack_file_system.dart';
-import '../../../../../utilities/util.dart';
+import '../../../../../utilities/fs.dart';
class SWBFileSystem {
- Directory? rootPath;
- Directory? startPath;
-
- String? filePath;
- String? dirPath;
-
- final bool isDesktop = Util.isDesktop;
+ Directory? _startPath;
Future<Directory> prepareStorage() async {
- if (Platform.isAndroid) {
- rootPath = await StackFileSystem.wtfAndroidDocumentsPath();
- } else {
- rootPath = await getApplicationDocumentsDirectory();
- }
- //todo: check if print needed
- // debugPrint(rootPath!.absolute.toString());
+ if (_startPath != null) _startPath;
+
+ final _rootPath = await getApplicationDocumentsDirectory();
late Directory sampleFolder;
const dirName = "${AppConfig.prefix}_backup";
if (Platform.isIOS) {
- sampleFolder = Directory(rootPath!.path);
+ sampleFolder = Directory(_rootPath.path);
} else if (Platform.isAndroid ||
Platform.isLinux ||
Platform.isWindows ||
Platform.isMacOS) {
- sampleFolder = Directory(path.join(rootPath!.path, dirName));
+ sampleFolder = Directory(path.join(_rootPath.path, dirName));
}
- try {
- if (!sampleFolder.existsSync()) {
- sampleFolder.createSync(recursive: true);
- }
- } catch (e, s) {
- // todo: come back to this
- debugPrint("$e $s");
+ if (!sampleFolder.existsSync()) {
+ sampleFolder.createSync(recursive: true);
}
File sampleFile = File('${sampleFolder.path}/Backups_Go_Here.info');
if (Platform.isIOS) {
- sampleFile = File('${rootPath!.path}/Backups_Go_Here.info');
+ sampleFile = File('${_rootPath.path}/Backups_Go_Here.info');
}
- try {
- if (!sampleFile.existsSync()) {
- sampleFile.createSync();
- }
- } catch (e, s) {
- // todo: come back to this
- debugPrint("$e $s");
+ if (!sampleFile.existsSync()) {
+ sampleFile.createSync();
}
- startPath = sampleFolder;
+
+ _startPath = sampleFolder;
return sampleFolder;
}
- Future<void> pickDir(BuildContext context) async {
- final String? chosenPath;
- if (Platform.isIOS) {
- chosenPath = startPath?.path;
- } else {
- final String path =
- Platform.isWindows
- ? startPath!.path.replaceAll("/", "\\")
- : startPath!.path;
- chosenPath = await FilePicker.platform.getDirectoryPath(
- dialogTitle: "Choose Backup location",
- initialDirectory: path,
- lockParentWindow: true,
- );
- }
- dirPath = chosenPath;
+ Future<String?> pickDir() {
+ return FS.pickDirectory(
+ initialDirectory: Platform.isWindows
+ ? _startPath?.path.replaceAll("/", "\\")
+ : _startPath?.path,
+ );
}
- Future<void> openFile(BuildContext context) async {
+ Future<String?> openFile() async {
FilePickerResult? result;
if (Platform.isAndroid) {
result = await FilePicker.platform.pickFiles(
dialogTitle: "Load backup file",
- initialDirectory: startPath!.path,
+ initialDirectory: _startPath!.path,
type: FileType.any,
allowCompression: false,
lockParentWindow: true,
@@ -106,7 +75,7 @@ class SWBFileSystem {
} else if (Platform.isIOS) {
result = await FilePicker.platform.pickFiles(
dialogTitle: "Load backup file",
- initialDirectory: startPath!.path,
+ initialDirectory: _startPath!.path,
type: FileType.any,
allowCompression: false,
lockParentWindow: true,
@@ -114,7 +83,7 @@ class SWBFileSystem {
} else {
result = await FilePicker.platform.pickFiles(
dialogTitle: "Load backup file",
- initialDirectory: startPath!.path,
+ initialDirectory: _startPath!.path,
type: FileType.custom,
allowedExtensions: ['bin', 'swb'],
allowCompression: false,
@@ -122,6 +91,6 @@ class SWBFileSystem {
);
}
- filePath = result?.paths.first;
+ return result?.paths.first;
}
}
diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart
index 3a7224a..1b8f428 100644
--- a/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart
+++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/restore_from_file_view.dart
@@ -163,14 +163,13 @@ class _RestoreFromFileViewState extends ConsumerState<RestoreFromFileView> {
try {
await stackFileSystem.prepareStorage();
if (mounted) {
- await stackFileSystem.openFile(context);
- }
+ final filePath = await stackFileSystem.openFile();
- if (mounted) {
- setState(() {
- fileLocationController.text =
- stackFileSystem.filePath ?? "";
- });
+ if (mounted) {
+ setState(() {
+ fileLocationController.text = filePath ?? "";
+ });
+ }
}
} catch (e, s) {
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart
index 6c2a12e..f503d0b 100644
--- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart
+++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart
@@ -22,7 +22,6 @@ import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/prefs.dart';
import '../../utilities/show_loading.dart';
-import '../../utilities/stack_file_system.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_app_bar.dart';
@@ -73,9 +72,7 @@ class _DesktopOrdinalDetailsViewState
final bytes = response.bodyBytes;
- final dir = Platform.isAndroid
- ? await StackFileSystem.wtfAndroidDocumentsPath()
- : await getApplicationDocumentsDirectory();
+ final dir = await getApplicationDocumentsDirectory();
final filePath = path.join(
dir.path,
diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart
index 1d672c5..f4f4de3 100644
--- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart
+++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart
@@ -252,14 +252,13 @@ class _ForgottenPassphraseRestoreFromSWBState
try {
await stackFileSystem.prepareStorage();
if (mounted) {
- await stackFileSystem.openFile(context);
- }
+ final filePath = await stackFileSystem.openFile();
- if (mounted) {
- setState(() {
- fileLocationController.text =
- stackFileSystem.filePath ?? "";
- });
+ if (mounted) {
+ setState(() {
+ fileLocationController.text = filePath ?? "";
+ });
+ }
}
} catch (e, s) {
Logging.instance.e("$e\n$s", error: e, stackTrace: s);
diff --git a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart
index ce19167..929c3c2 100644
--- a/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart
+++ b/lib/pages_desktop_specific/settings/settings_menu/backup_and_restore/create_auto_backup.dart
@@ -31,6 +31,7 @@ import '../../../../utilities/constants.dart';
import '../../../../utilities/enums/backup_frequency_type.dart';
import '../../../../utilities/flutter_secure_storage_interface.dart';
import '../../../../utilities/format.dart';
+import '../../../../utilities/fs.dart';
import '../../../../utilities/logger.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
@@ -129,16 +130,11 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
adkVersion,
);
- if (Platform.isAndroid) {
- // TODO SAF
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- } else {
- File(
- fileToSavePath,
- ).writeAsStringSync(encryptedDataString, flush: true);
- }
+ await FS.writeStringToFile(
+ encryptedDataString,
+ pathToSave,
+ fileToSavePath.split("/").last,
+ );
return fileToSavePath;
}(),
@@ -323,16 +319,16 @@ class _CreateAutoBackup extends ConsumerState<CreateAutoBackup> {
: () async {
try {
await stackFileSystem.prepareStorage();
-
- if (mounted) {
- await stackFileSystem.pickDir(context);
- }
-
if (mounted) {
- setState(() {
- fileLocationController.text =
- stackFileSystem.dirPath ?? "";
- });
+ final filePath = await stackFileSystem
+ .openFile();
+
+ if (mounted) {
+ setState(() {
+ fileLocationController.text =
+ filePath ?? "";
+ });
+ }
}
} catch (e, s) {
Logging.instance.e(
diff --git a/lib/utilities/fs.dart b/lib/utilities/fs.dart
new file mode 100644
index 0000000..b1c1b84
--- /dev/null
+++ b/lib/utilities/fs.dart
@@ -0,0 +1,46 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:file_picker/file_picker.dart';
+import 'package:path/path.dart';
+import 'package:saf_stream/saf_stream.dart';
+import 'package:saf_util/saf_util.dart';
+
+abstract final class FS {
+ static Future<String?> pickDirectory({String? initialDirectory}) async {
+ final String? path;
+ if (Platform.isAndroid) {
+ final dir = await SafUtil().pickDirectory(
+ writePermission: true,
+ persistablePermission: true,
+ initialUri: initialDirectory,
+ );
+
+ path = dir?.uri;
+ } else {
+ path = await FilePicker.platform.getDirectoryPath(
+ lockParentWindow: true,
+ initialDirectory: initialDirectory,
+ );
+ }
+
+ return path;
+ }
+
+ static Future<void> writeStringToFile(
+ String content,
+ String dirPath,
+ String fileName,
+ ) {
+ if (Platform.isAndroid && dirPath.startsWith("content://")) {
+ return SafStream().writeFileBytes(
+ dirPath,
+ fileName,
+ "txt",
+ utf8.encode(content),
+ );
+ } else {
+ return File(join(dirPath, fileName)).writeAsString(content, flush: true);
+ }
+ }
+}
diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart
index e135577..292dda1 100644
--- a/lib/utilities/stack_file_system.dart
+++ b/lib/utilities/stack_file_system.dart
@@ -238,18 +238,4 @@ abstract class StackFileSystem {
return logsDir;
}
-
- static Future<Directory> wtfAndroidDocumentsPath() async {
- const base = "/storage/emulated/";
- final rootDir = await applicationRootDirectory();
- final parts = rootDir.path.replaceFirst("/data/user/", "").split("/");
- if (parts.isNotEmpty) {
- final id = int.tryParse(parts.first);
-
- if (id != null) {
- return Directory(path.join(base, id.toString(), "Documents"));
- }
- }
- throw Exception("Unsupported Android flavor");
- }
}
diff --git a/pubspec.lock b/pubspec.lock
index fd2dde3..bcd7d7b 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -872,12 +872,11 @@ packages:
file_picker:
dependency: "direct main"
description:
- path: "."
- ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23
- resolved-ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23
- url: "https://github.com/cypherstack/flutter_file_picker.git"
- source: git
- version: "8.3.1"
+ name: file_picker
+ sha256: f2d9f173c2c14635cc0e9b14c143c49ef30b4934e8d1d274d6206fcb0086a06f
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.3.3"
fixnum:
dependency: "direct main"
description:
@@ -1901,6 +1900,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.3"
+ saf_stream:
+ dependency: "direct main"
+ description:
+ name: saf_stream
+ sha256: c05449997698c481a03e428162a999f93b1ee1bcc0349d651899a59f7b10230a
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.3"
+ saf_util:
+ dependency: "direct main"
+ description:
+ name: saf_util
+ sha256: "219f983e5f17b28998335158cdc97add9d52af9884e38b5a43f10dcc070510ec"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.0"
sec:
dependency: transitive
description:
diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml
index 5649487..d29dea1 100644
--- a/scripts/app_config/templates/pubspec.template.yaml
+++ b/scripts/app_config/templates/pubspec.template.yaml
@@ -171,10 +171,7 @@ dependencies:
pointycastle: ^3.6.0
package_info_plus: ^8.0.2
lottie: ^2.3.2
- file_picker:
- git:
- url: https://github.com/cypherstack/flutter_file_picker.git
- ref: b2849e63e1d418ad8d943c886cd3f4ed20d0ff23
+ file_picker: ^10.3.3
connectivity_plus: ^4.0.1
isar_community: 3.3.0-dev.2
isar_community_flutter_libs: 3.3.0-dev.2
@@ -246,6 +243,8 @@ dependencies:
path: ^1.9.1
mweb_client: ^0.2.0
fixnum: ^1.1.1
+ saf_util: ^0.11.0
+ saf_stream: ^0.12.3
dev_dependencies:
flutter_test:
Why this scored 33/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.