From cfb185b53d98af5225854e5f9d1187ee62ab4499 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 13 May 2025 11:10:26 +0200 Subject: [PATCH] LoadDb and SaveDb respect the SyncInBackground preference --- src/Kp2aBusinessLogic/IKp2aApp.cs | 7 ++- .../Io/CachingFileStorage.cs | 16 ++++-- .../Io/OfflineSwitchableFileStorage.cs | 6 ++- src/Kp2aBusinessLogic/database/Database.cs | 4 +- src/Kp2aBusinessLogic/database/edit/LoadDB.cs | 52 ++++++++++++++----- src/Kp2aBusinessLogic/database/edit/SaveDB.cs | 46 ++++++++++++---- src/keepass2android-app/PasswordActivity.cs | 51 ++++++++++++------ .../Resources/layout/password.xml | 27 +++++++++- src/keepass2android-app/SyncUtil.cs | 14 +++-- .../Utils/MessagePresenter.cs | 5 +- src/keepass2android-app/app/App.cs | 35 ++++++++++--- 11 files changed, 204 insertions(+), 59 deletions(-) diff --git a/src/Kp2aBusinessLogic/IKp2aApp.cs b/src/Kp2aBusinessLogic/IKp2aApp.cs index b5533928..af385c85 100644 --- a/src/Kp2aBusinessLogic/IKp2aApp.cs +++ b/src/Kp2aBusinessLogic/IKp2aApp.cs @@ -140,6 +140,9 @@ namespace keepass2android #endif - - } + + bool SyncInBackgroundPreference { get; set; } + + + } } \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs index 0d8e5e27..2e1f6230 100644 --- a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs @@ -186,8 +186,11 @@ namespace keepass2android.Io Kp2aLog.Log("couldn't open from remote " + ioc.Path); #endif Kp2aLog.Log(ex.ToString()); - - _cacheSupervisor.CouldntOpenFromRemote(ioc, ex); + if (TriggerWarningWhenFallingBackToCache) + { + _cacheSupervisor.CouldntOpenFromRemote(ioc, ex); + } + return File.OpenRead(cachedFilePath); } } @@ -327,7 +330,10 @@ namespace keepass2android.Io Kp2aLog.Log("couldn't save to remote " + ioc.Path); Kp2aLog.Log(e.ToString()); //notify the supervisor so it might display a warning or schedule a retry - _cacheSupervisor.CouldntSaveToRemote(ioc, e); + if (TriggerWarningWhenFallingBackToCache) + { + _cacheSupervisor.CouldntSaveToRemote(ioc, e); } + return false; } } @@ -632,7 +638,9 @@ namespace keepass2android.Io set { _cachedStorage.IsOffline = value; } } - public void OnRequestPermissionsResult(IFileStorageSetupActivity fileStorageSetupActivity, int requestCode, + public bool TriggerWarningWhenFallingBackToCache { get; set; } + + public void OnRequestPermissionsResult(IFileStorageSetupActivity fileStorageSetupActivity, int requestCode, string[] permissions, Permission[] grantResults) { _cachedStorage.OnRequestPermissionsResult(fileStorageSetupActivity, requestCode, permissions, grantResults); diff --git a/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs b/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs index 4d82ad26..3971b630 100644 --- a/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/OfflineSwitchableFileStorage.cs @@ -11,7 +11,8 @@ namespace keepass2android.Io public interface IOfflineSwitchable { bool IsOffline { get; set; } - } + bool TriggerWarningWhenFallingBackToCache { get; set; } + } /// /// Encapsulates another IFileStorage. Allows to switch to offline mode by throwing @@ -21,8 +22,9 @@ namespace keepass2android.Io { private readonly IFileStorage _baseStorage; public bool IsOffline { get; set; } + public bool TriggerWarningWhenFallingBackToCache { get; set; } - public OfflineSwitchableFileStorage(IFileStorage baseStorage) + public OfflineSwitchableFileStorage(IFileStorage baseStorage) { _baseStorage = baseStorage; } diff --git a/src/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs index 501bff5b..4f73c2d5 100644 --- a/src/Kp2aBusinessLogic/database/Database.cs +++ b/src/Kp2aBusinessLogic/database/Database.cs @@ -194,9 +194,9 @@ namespace keepass2android } - public void SaveData() { + public void SaveData(IFileStorage fileStorage) { - using (IWriteTransaction trans = _app.GetFileStorage(Ioc).OpenWriteTransaction(Ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions))) + using (IWriteTransaction trans = fileStorage.OpenWriteTransaction(Ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions))) { DatabaseFormat.Save(KpDatabase, trans.OpenFile()); diff --git a/src/Kp2aBusinessLogic/database/edit/LoadDB.cs b/src/Kp2aBusinessLogic/database/edit/LoadDB.cs index db333f1b..d9829b2d 100644 --- a/src/Kp2aBusinessLogic/database/edit/LoadDB.cs +++ b/src/Kp2aBusinessLogic/database/edit/LoadDB.cs @@ -21,8 +21,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Android.App; +using Android.OS; using KeePass.Util; using keepass2android.database.edit; +using keepass2android.Io; using KeePassLib; using KeePassLib.Keys; using KeePassLib.Serialization; @@ -47,8 +49,6 @@ namespace keepass2android _keyfileOrProvider = keyfileOrProvider; _updateLastUsageTimestamp = updateLastUsageTimestamp; _makeCurrent = makeCurrent; - - _rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile); } @@ -65,16 +65,31 @@ namespace keepass2android //make sure the file data is stored in the recent files list even if loading fails SaveFileData(_ioc, _keyfileOrProvider); + var fileStorage = _app.GetFileStorage(_ioc); + + bool requiresSubsequentSync = false; + StatusLogger.UpdateMessage(UiStringKey.loading_database); //get the stream data into a single stream variable (databaseStream) regardless whether its preloaded or not: MemoryStream preloadedMemoryStream = _databaseData == null ? null : _databaseData.Result; MemoryStream databaseStream; - if (preloadedMemoryStream != null) - databaseStream = preloadedMemoryStream; - else + if (preloadedMemoryStream != null) + { + //note: if the stream has been loaded already, we don't need to trigger another sync later on + databaseStream = preloadedMemoryStream; + } + else { - using (Stream s = _app.GetFileStorage(_ioc).OpenFileForRead(_ioc)) + if (_app.SyncInBackgroundPreference && fileStorage is IOfflineSwitchable offlineSwitchable) + { + offlineSwitchable.IsOffline = true; + //no warning. We'll trigger a sync later. + offlineSwitchable.TriggerWarningWhenFallingBackToCache = false; + requiresSubsequentSync = true; + + } + using (Stream s = fileStorage.OpenFileForRead(_ioc)) { databaseStream = new MemoryStream(); s.CopyTo(databaseStream); @@ -83,8 +98,8 @@ namespace keepass2android } //ok, try to load the database. Let's start with Kdbx format and retry later if that is the wrong guess: - _format = new KdbxDatabaseFormat(KdbxDatabaseFormat.GetFormatToUse(_app.GetFileStorage(_ioc).GetFileExtension(_ioc))); - TryLoad(databaseStream); + _format = new KdbxDatabaseFormat(KdbxDatabaseFormat.GetFormatToUse(fileStorage.GetFileExtension(_ioc))); + TryLoad(databaseStream, requiresSubsequentSync); @@ -136,7 +151,7 @@ namespace keepass2android /// public Exception Exception { get; set; } - Database TryLoad(MemoryStream databaseStream) + Database TryLoad(MemoryStream databaseStream, bool requiresSubsequentSync) { //create a copy of the stream so we can try again if we get an exception which indicates we should change parameters //This is not optimal in terms of (short-time) memory usage but is hard to avoid because the Keepass library closes streams also in case of errors. @@ -150,7 +165,20 @@ namespace keepass2android try { Database newDb = _app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent); - Kp2aLog.Log("LoadDB OK"); + Kp2aLog.Log("LoadDB OK"); + + if (requiresSubsequentSync) + { + var syncTask = new SynchronizeCachedDatabase(ActiveActivity, _app, new ActionOnOperationFinished(ActiveActivity, + (success, message, activeActivity) => + { + if (!String.IsNullOrEmpty(message)) + _app.ShowMessage(activeActivity, message, success ? MessageSeverity.Info : MessageSeverity.Error); + + }) + ); + BackgroundOperationRunner.Instance.Run(ActiveActivity, _app, syncTask); + } Finish(true, _format.SuccessMessage); return newDb; @@ -158,7 +186,7 @@ namespace keepass2android catch (OldFormatException) { _format = new KdbDatabaseFormat(_app); - return TryLoad(databaseStream); + return TryLoad(databaseStream, requiresSubsequentSync); } catch (InvalidCompositeKeyException) { @@ -170,7 +198,7 @@ namespace keepass2android //retry without password: _compositeKey.RemoveUserKey(passwordKey); //retry: - return TryLoad(databaseStream); + return TryLoad(databaseStream, requiresSubsequentSync); } else throw; } diff --git a/src/Kp2aBusinessLogic/database/edit/SaveDB.cs b/src/Kp2aBusinessLogic/database/edit/SaveDB.cs index de641535..f6781854 100644 --- a/src/Kp2aBusinessLogic/database/edit/SaveDB.cs +++ b/src/Kp2aBusinessLogic/database/edit/SaveDB.cs @@ -38,6 +38,7 @@ namespace keepass2android private readonly IKp2aApp _app; private readonly Database _db; private readonly bool _dontSave; + private bool requiresSubsequentSync = false; //if true, we need to sync the file after saving. /// /// stream for reading the data from the original file. If this is set to a non-null value, we know we need to sync @@ -108,13 +109,23 @@ namespace keepass2android IOConnectionInfo ioc = _db.Ioc; IFileStorage fileStorage = _app.GetFileStorage(ioc); - if (_streamForOrigFile == null) + if (_app.SyncInBackgroundPreference && fileStorage is IOfflineSwitchable offlineSwitchable) + { + offlineSwitchable.IsOffline = true; + //no warning. We'll trigger a sync later. + offlineSwitchable.TriggerWarningWhenFallingBackToCache = false; + requiresSubsequentSync = true; + + } + + + if (_streamForOrigFile == null) { if ((!_app.GetBooleanPreference(PreferenceKey.CheckForFileChangesOnSave)) || (_db.KpDatabase.HashOfFileOnDisk == null)) //first time saving { PerformSaveWithoutCheck(fileStorage, ioc); - Finish(true); + FinishWithSuccess(); return; } } @@ -123,9 +134,9 @@ namespace keepass2android bool hasStreamForOrigFile = (_streamForOrigFile != null); bool hasChangeFast = hasStreamForOrigFile || fileStorage.CheckForFileChangeFast(ioc, _db.LastFileVersion); //first try to use the fast change detection; - bool hasHashChanged = hasChangeFast || + bool hasHashChanged = !requiresSubsequentSync && (hasChangeFast || (FileHashChanged(ioc, _db.KpDatabase.HashOfFileOnDisk) == - FileHashChange.Changed); //if that fails, hash the file and compare: + FileHashChange.Changed)); //if that fails, hash the file and compare: if (hasHashChanged) { @@ -158,7 +169,7 @@ namespace keepass2android RunInWorkerThread(() => { PerformSaveWithoutCheck(fileStorage, ioc); - Finish(true); + FinishWithSuccess(); }); }, //cancel @@ -174,7 +185,7 @@ namespace keepass2android else { PerformSaveWithoutCheck(fileStorage, ioc); - Finish(true); + FinishWithSuccess(); } } @@ -194,11 +205,28 @@ namespace keepass2android } else { - Finish(true); + FinishWithSuccess(); } } + private void FinishWithSuccess() + { + if (requiresSubsequentSync) + { + var syncTask = new SynchronizeCachedDatabase(ActiveActivity, _app, new ActionOnOperationFinished(ActiveActivity, + (success, message, activeActivity) => + { + if (!System.String.IsNullOrEmpty(message)) + _app.ShowMessage(activeActivity, message, success ? MessageSeverity.Info : MessageSeverity.Error); + + }) + ); + BackgroundOperationRunner.Instance.Run(ActiveActivity, _app, syncTask); + } + Finish(true); + } + private void MergeAndFinish(IFileStorage fileStorage, IOConnectionInfo ioc) { //note: when synced, the file might be downloaded once again from the server. Caching the data @@ -207,7 +235,7 @@ namespace keepass2android MergeIn(fileStorage, ioc); PerformSaveWithoutCheck(fileStorage, ioc); _db.UpdateGlobals(); - Finish(true); + FinishWithSuccess(); } private void RunInWorkerThread(Action runHandler) @@ -282,7 +310,7 @@ namespace keepass2android private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc) { StatusLogger.UpdateSubMessage(""); - _db.SaveData(); + _db.SaveData(fileStorage); _db.LastFileVersion = fileStorage.GetCurrentFileVersionFast(ioc); } diff --git a/src/keepass2android-app/PasswordActivity.cs b/src/keepass2android-app/PasswordActivity.cs index bfe8b03c..533a9dae 100644 --- a/src/keepass2android-app/PasswordActivity.cs +++ b/src/keepass2android-app/PasswordActivity.cs @@ -1753,17 +1753,10 @@ namespace keepass2android cbOfflineMode.Checked = App.Kp2a .OfflineModePreference; //this won't overwrite new user settings because every change is directly saved in settings - LinearLayout offlineModeContainer = FindViewById(Resource.Id.work_offline_container); - var cachingFileStorage = App.Kp2a.GetFileStorage(_ioConnection) as CachingFileStorage; - if ((cachingFileStorage != null) && cachingFileStorage.IsCached(_ioConnection)) - { - offlineModeContainer.Visibility = ViewStates.Visible; - } - else - { - offlineModeContainer.Visibility = ViewStates.Gone; - App.Kp2a.OfflineMode = false; - } + + CheckBox cbSyncInBackground = (CheckBox)FindViewById(Resource.Id.sync_in_background)!; + cbSyncInBackground.Checked = App.Kp2a.SyncInBackgroundPreference; + UpdateInternalCacheCheckboxesVisibility(); @@ -2039,10 +2032,38 @@ namespace keepass2android { App.Kp2a.OfflineModePreference = App.Kp2a.OfflineMode = args.IsChecked; }; - - } - - private String LoadKeyProviderStringForIoc(String filename) { + + CheckBox cbSyncInBackground = (CheckBox)FindViewById(Resource.Id.sync_in_background); + cbSyncInBackground.CheckedChange += (sender, args) => + { + App.Kp2a.SyncInBackgroundPreference = args.IsChecked; + UpdateInternalCacheCheckboxesVisibility(); + + }; + + } + + private void UpdateInternalCacheCheckboxesVisibility() + { + + LinearLayout syncInBackgroundContainer = FindViewById(Resource.Id.sync_in_background_container)!; + + LinearLayout offlineModeContainer = FindViewById(Resource.Id.work_offline_container)!; + var cachingFileStorage = App.Kp2a.GetFileStorage(_ioConnection) as CachingFileStorage; + if ((cachingFileStorage != null) && cachingFileStorage.IsCached(_ioConnection)) + { + syncInBackgroundContainer.Visibility = ViewStates.Visible; + offlineModeContainer.Visibility = + App.Kp2a.SyncInBackgroundPreference ? ViewStates.Gone : ViewStates.Visible; + } + else + { + syncInBackgroundContainer.Visibility = offlineModeContainer.Visibility = ViewStates.Gone; + App.Kp2a.OfflineMode = false; + } + } + + private String LoadKeyProviderStringForIoc(String filename) { if ( _rememberKeyfile ) { string keyfile = App.Kp2a.FileDbHelper.GetKeyFileForFile(filename); if (String.IsNullOrEmpty(keyfile)) diff --git a/src/keepass2android-app/Resources/layout/password.xml b/src/keepass2android-app/Resources/layout/password.xml index b21a343f..c216d9c5 100644 --- a/src/keepass2android-app/Resources/layout/password.xml +++ b/src/keepass2android-app/Resources/layout/password.xml @@ -318,6 +318,30 @@ android:text="@string/help_quickunlock" /> + + + + + + + +