From 6fde4ee45f2b0bf47f307a5b44d27248dda5788b Mon Sep 17 00:00:00 2001 From: a5xwin Date: Wed, 13 Aug 2025 23:25:48 +0530 Subject: [PATCH] implemented an auto backup feature --- .../Flutter/ephemeral/flutter_lldb_helper.py | 32 +++ .../ios/Flutter/ephemeral/flutter_lldbinit | 5 + mobile/apps/auth/lib/main.dart | 4 +- .../lib/services/local_backup_service.dart | 45 ++++ mobile/apps/auth/lib/store/code_store.dart | 5 +- mobile/apps/auth/lib/ui/home_page.dart | 3 +- .../ui/settings/data/data_section_widget.dart | 18 +- .../data/local_backup_settings_page.dart | 197 ++++++++++++++++++ mobile/apps/auth/pubspec.lock | 8 +- 9 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldb_helper.py create mode 100644 mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldbinit create mode 100644 mobile/apps/auth/lib/services/local_backup_service.dart create mode 100644 mobile/apps/auth/lib/ui/settings/data/local_backup_settings_page.dart diff --git a/mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldb_helper.py b/mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000000..a88caf99df --- /dev/null +++ b/mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldbinit b/mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000000..e3ba6fbedc --- /dev/null +++ b/mobile/apps/auth/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/mobile/apps/auth/lib/main.dart b/mobile/apps/auth/lib/main.dart index 2bca2feb0d..7187d5f3f0 100644 --- a/mobile/apps/auth/lib/main.dart +++ b/mobile/apps/auth/lib/main.dart @@ -11,6 +11,7 @@ import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/locale.dart'; import 'package:ente_auth/services/authenticator_service.dart'; import 'package:ente_auth/services/billing_service.dart'; +import 'package:ente_auth/services/local_backup_service.dart'; import 'package:ente_auth/services/notification_service.dart'; import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/services/update_service.dart'; @@ -153,6 +154,7 @@ Future _init(bool bool, {String? via}) async { await PreferenceService.instance.init(); await CodeStore.instance.init(); + await LocalBackupService.instance.init(); await CodeDisplayStore.instance.init(); await Configuration.instance.init(); await Network.instance.init(); @@ -163,4 +165,4 @@ Future _init(bool bool, {String? via}) async { await UpdateService.instance.init(); await IconUtils.instance.init(); await LockScreenSettings.instance.init(); -} +} \ No newline at end of file diff --git a/mobile/apps/auth/lib/services/local_backup_service.dart b/mobile/apps/auth/lib/services/local_backup_service.dart new file mode 100644 index 0000000000..50052e69c0 --- /dev/null +++ b/mobile/apps/auth/lib/services/local_backup_service.dart @@ -0,0 +1,45 @@ +import 'dart:io'; + +import 'package:ente_auth/store/code_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LocalBackupService { + static final LocalBackupService instance = + LocalBackupService._privateConstructor(); + LocalBackupService._privateConstructor(); + + Future init() async {} + + Future triggerAutomaticBackup() async { + try { + final prefs = await SharedPreferences.getInstance(); + + final isEnabled = prefs.getBool('isAutoBackupEnabled') ?? false; //checks if toggle is on + if (!isEnabled) return; + + final backupPath = prefs.getString('autoBackupPath'); + if (backupPath == null) return; + + debugPrint("--- Change detected, triggering automatic backup... ---"); + + final allCodes = await CodeStore.instance.getAllCodes(sortCodes: false); + final validCodes = allCodes.where((code) => !code.hasError); + String backupContent = ""; + for (final code in validCodes) { + backupContent += "${code.toOTPAuthUrlFormat()}\n"; + } + + if (backupContent.trim().isEmpty) return; + + final fileName = 'ente-auth-auto-backup.txt'; + final filePath = '$backupPath/$fileName'; + final backupFile = File(filePath); + await backupFile.writeAsString(backupContent); + + debugPrint('Automatic backup successful! Saved to: $filePath'); + } catch (e, s) { + debugPrint('Silent error during automatic backup: $e\n$s'); + } + } +} \ No newline at end of file diff --git a/mobile/apps/auth/lib/store/code_store.dart b/mobile/apps/auth/lib/store/code_store.dart index e6f984f3eb..7939f91f4b 100644 --- a/mobile/apps/auth/lib/store/code_store.dart +++ b/mobile/apps/auth/lib/store/code_store.dart @@ -8,6 +8,7 @@ import 'package:ente_auth/events/codes_updated_event.dart'; import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/code.dart'; import 'package:ente_auth/services/authenticator_service.dart'; +import 'package:ente_auth/services/local_backup_service.dart'; import 'package:ente_auth/store/offline_authenticator_db.dart'; import 'package:logging/logging.dart'; @@ -157,6 +158,7 @@ class CodeStore { ); } Bus.instance.fire(CodesUpdatedEvent()); + LocalBackupService.instance.triggerAutomaticBackup().ignore(); return result; } @@ -164,6 +166,7 @@ class CodeStore { final mode = accountMode ?? _authenticatorService.getAccountMode(); await _authenticatorService.deleteEntry(code.generatedID!, mode); Bus.instance.fire(CodesUpdatedEvent()); + LocalBackupService.instance.triggerAutomaticBackup().ignore(); } bool _isOfflineImportRunning = false; @@ -242,4 +245,4 @@ enum AddResult { newCode, duplicate, updateCode, -} +} \ No newline at end of file diff --git a/mobile/apps/auth/lib/ui/home_page.dart b/mobile/apps/auth/lib/ui/home_page.dart index 3a3d650259..e9fc15eb80 100644 --- a/mobile/apps/auth/lib/ui/home_page.dart +++ b/mobile/apps/auth/lib/ui/home_page.dart @@ -90,6 +90,7 @@ class _HomePageState extends State { @override void initState() { super.initState(); + _codeSortKey = PreferenceService.instance.codeSortKey(); _textController.addListener(_applyFilteringAndRefresh); _loadCodes(); @@ -747,4 +748,4 @@ class _HomePageState extends State { ], ); } -} +} \ No newline at end of file diff --git a/mobile/apps/auth/lib/ui/settings/data/data_section_widget.dart b/mobile/apps/auth/lib/ui/settings/data/data_section_widget.dart index 84fb736380..13bead32f0 100644 --- a/mobile/apps/auth/lib/ui/settings/data/data_section_widget.dart +++ b/mobile/apps/auth/lib/ui/settings/data/data_section_widget.dart @@ -10,6 +10,7 @@ import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/ui/settings/data/duplicate_code_page.dart'; import 'package:ente_auth/ui/settings/data/export_widget.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart'; +import 'package:ente_auth/ui/settings/data/local_backup_settings_page.dart'; //for local backup import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/material.dart'; @@ -29,6 +30,10 @@ class DataSectionWidget extends StatelessWidget { ); } + Future _handleLocalBackupClick(BuildContext context) async { + await routeToPage(context, const LocalBackupSettingsPage()); + } + Column _getSectionOptions(BuildContext context) { final l10n = context.l10n; List children = []; @@ -86,10 +91,21 @@ class DataSectionWidget extends StatelessWidget { ); }, ), + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Local Backup", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + await _handleLocalBackupClick(context); + }, + ), sectionOptionSpacing, ]); return Column( children: children, ); } -} +} \ No newline at end of file diff --git a/mobile/apps/auth/lib/ui/settings/data/local_backup_settings_page.dart b/mobile/apps/auth/lib/ui/settings/data/local_backup_settings_page.dart new file mode 100644 index 0000000000..c10143601d --- /dev/null +++ b/mobile/apps/auth/lib/ui/settings/data/local_backup_settings_page.dart @@ -0,0 +1,197 @@ +import 'package:ente_auth/services/local_backup_service.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class LocalBackupSettingsPage extends StatefulWidget { + const LocalBackupSettingsPage({super.key}); + + @override + State createState() => + _LocalBackupSettingsPageState(); +} + +class _LocalBackupSettingsPageState extends State { + bool _isBackupEnabled = false; + String? _backupPath; + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + // to load the saved settings from SharedPreferences when the page opens. + Future _loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _isBackupEnabled = prefs.getBool('isAutoBackupEnabled') ?? false; + _backupPath = prefs.getString('autoBackupPath'); + }); + } + + // opens directory picker + Future _pickAndSaveBackupLocation() async { + String? directoryPath = await FilePicker.platform.getDirectoryPath(); + + if (directoryPath != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('autoBackupPath', directoryPath); + setState(() { + _backupPath = directoryPath; + }); + + LocalBackupService.instance.triggerAutomaticBackup(); //whenever backup path is set, we trigger + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Location updated and initial backup created!'),), + ); + } +} + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Local Backup Settings"), + ), + body: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enable Automatic Backups", + style: getEnteTextTheme(context).largeBold, + ), + Switch( + value: _isBackupEnabled, + activeColor: Colors.white, + onChanged: (value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool('isAutoBackupEnabled', value); + setState(() { + _isBackupEnabled = value; + }); + + if (value == true) { //if toggle was on: trigger backup + if (_backupPath != null) { //ensuring path was set + LocalBackupService.instance.triggerAutomaticBackup(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Initial backup created!'),), + ); + } + else { + // we ask user to set a backup location + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Please choose a backup location.'),), + ); + } + } + }, + ), + ], + ), + const SizedBox(height: 20), + Opacity( + opacity: _isBackupEnabled ? 1.0 : 0.4, + child: IgnorePointer( + ignoring: !_isBackupEnabled, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Backup Location", + style: getEnteTextTheme(context).largeBold, + ), + const SizedBox(height: 8), + Text( + "Select a folder to save backups. Backups run automatically when entries are added, deleted, or edited.", + style: getEnteTextTheme(context).small, + ), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Location:', + style: getEnteTextTheme(context).body, + ), + const SizedBox(height: 4), + Text( + _backupPath ?? 'Not set. Please choose a location.', + style: getEnteTextTheme(context).small.copyWith( + color: _backupPath != null + ? null + : Colors.grey, + ), + ), + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _pickAndSaveBackupLocation, + child: const Text('Choose Location'), + ), + ), + ], + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.orange.withOpacity(0.3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.security_outlined, + color: Colors.orange, + size: 20, + ), + const SizedBox(width: 8), + Text( + "Security Notice", + style: getEnteTextTheme(context) + .smallBold + .copyWith( + color: Colors.orange, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + "we can give any security warnings here- like say: make sure you choose a safe location etc??", + style: getEnteTextTheme(context).mini.copyWith( + color: Colors.orange.shade700, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/mobile/apps/auth/pubspec.lock b/mobile/apps/auth/pubspec.lock index 4b9e12285f..edb5ff02a8 100644 --- a/mobile/apps/auth/pubspec.lock +++ b/mobile/apps/auth/pubspec.lock @@ -991,18 +991,18 @@ packages: dependency: "direct main" description: name: local_auth_android - sha256: "5351c7eea8823de28e37d8b7b3e386d944b80f2a77edb91a5707fb97a41fc1b1" + sha256: "8bba79f4f0f7bc812fce2ca20915d15618c37721246ba6c3ef2aa7a763a90cf2" url: "https://pub.dev" source: hosted - version: "1.0.45" + version: "1.0.47" local_auth_darwin: dependency: "direct main" description: name: local_auth_darwin - sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.3" local_auth_platform_interface: dependency: transitive description: