LoadDb and SaveDb respect the SyncInBackground preference

This commit is contained in:
Philipp Crocoll
2025-05-13 11:10:26 +02:00
parent c3b6612591
commit cfb185b53d
11 changed files with 204 additions and 59 deletions

View File

@@ -141,5 +141,8 @@ namespace keepass2android
#endif #endif
} bool SyncInBackgroundPreference { get; set; }
}
} }

View File

@@ -186,8 +186,11 @@ namespace keepass2android.Io
Kp2aLog.Log("couldn't open from remote " + ioc.Path); Kp2aLog.Log("couldn't open from remote " + ioc.Path);
#endif #endif
Kp2aLog.Log(ex.ToString()); Kp2aLog.Log(ex.ToString());
if (TriggerWarningWhenFallingBackToCache)
{
_cacheSupervisor.CouldntOpenFromRemote(ioc, ex);
}
_cacheSupervisor.CouldntOpenFromRemote(ioc, ex);
return File.OpenRead(cachedFilePath); return File.OpenRead(cachedFilePath);
} }
} }
@@ -327,7 +330,10 @@ namespace keepass2android.Io
Kp2aLog.Log("couldn't save to remote " + ioc.Path); Kp2aLog.Log("couldn't save to remote " + ioc.Path);
Kp2aLog.Log(e.ToString()); Kp2aLog.Log(e.ToString());
//notify the supervisor so it might display a warning or schedule a retry //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; return false;
} }
} }
@@ -632,7 +638,9 @@ namespace keepass2android.Io
set { _cachedStorage.IsOffline = value; } 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) string[] permissions, Permission[] grantResults)
{ {
_cachedStorage.OnRequestPermissionsResult(fileStorageSetupActivity, requestCode, permissions, grantResults); _cachedStorage.OnRequestPermissionsResult(fileStorageSetupActivity, requestCode, permissions, grantResults);

View File

@@ -11,7 +11,8 @@ namespace keepass2android.Io
public interface IOfflineSwitchable public interface IOfflineSwitchable
{ {
bool IsOffline { get; set; } bool IsOffline { get; set; }
} bool TriggerWarningWhenFallingBackToCache { get; set; }
}
/// <summary> /// <summary>
/// Encapsulates another IFileStorage. Allows to switch to offline mode by throwing /// Encapsulates another IFileStorage. Allows to switch to offline mode by throwing
@@ -21,8 +22,9 @@ namespace keepass2android.Io
{ {
private readonly IFileStorage _baseStorage; private readonly IFileStorage _baseStorage;
public bool IsOffline { get; set; } public bool IsOffline { get; set; }
public bool TriggerWarningWhenFallingBackToCache { get; set; }
public OfflineSwitchableFileStorage(IFileStorage baseStorage) public OfflineSwitchableFileStorage(IFileStorage baseStorage)
{ {
_baseStorage = baseStorage; _baseStorage = baseStorage;
} }

View File

@@ -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()); DatabaseFormat.Save(KpDatabase, trans.OpenFile());

View File

@@ -21,8 +21,10 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.OS;
using KeePass.Util; using KeePass.Util;
using keepass2android.database.edit; using keepass2android.database.edit;
using keepass2android.Io;
using KeePassLib; using KeePassLib;
using KeePassLib.Keys; using KeePassLib.Keys;
using KeePassLib.Serialization; using KeePassLib.Serialization;
@@ -47,8 +49,6 @@ namespace keepass2android
_keyfileOrProvider = keyfileOrProvider; _keyfileOrProvider = keyfileOrProvider;
_updateLastUsageTimestamp = updateLastUsageTimestamp; _updateLastUsageTimestamp = updateLastUsageTimestamp;
_makeCurrent = makeCurrent; _makeCurrent = makeCurrent;
_rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile); _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 //make sure the file data is stored in the recent files list even if loading fails
SaveFileData(_ioc, _keyfileOrProvider); SaveFileData(_ioc, _keyfileOrProvider);
var fileStorage = _app.GetFileStorage(_ioc);
bool requiresSubsequentSync = false;
StatusLogger.UpdateMessage(UiStringKey.loading_database); StatusLogger.UpdateMessage(UiStringKey.loading_database);
//get the stream data into a single stream variable (databaseStream) regardless whether its preloaded or not: //get the stream data into a single stream variable (databaseStream) regardless whether its preloaded or not:
MemoryStream preloadedMemoryStream = _databaseData == null ? null : _databaseData.Result; MemoryStream preloadedMemoryStream = _databaseData == null ? null : _databaseData.Result;
MemoryStream databaseStream; MemoryStream databaseStream;
if (preloadedMemoryStream != null) if (preloadedMemoryStream != null)
databaseStream = preloadedMemoryStream; {
else //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(); databaseStream = new MemoryStream();
s.CopyTo(databaseStream); 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: //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))); _format = new KdbxDatabaseFormat(KdbxDatabaseFormat.GetFormatToUse(fileStorage.GetFileExtension(_ioc)));
TryLoad(databaseStream); TryLoad(databaseStream, requiresSubsequentSync);
@@ -136,7 +151,7 @@ namespace keepass2android
/// </summary> /// </summary>
public Exception Exception { get; set; } 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 //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. //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 try
{ {
Database newDb = _app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent); 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); Finish(true, _format.SuccessMessage);
return newDb; return newDb;
@@ -158,7 +186,7 @@ namespace keepass2android
catch (OldFormatException) catch (OldFormatException)
{ {
_format = new KdbDatabaseFormat(_app); _format = new KdbDatabaseFormat(_app);
return TryLoad(databaseStream); return TryLoad(databaseStream, requiresSubsequentSync);
} }
catch (InvalidCompositeKeyException) catch (InvalidCompositeKeyException)
{ {
@@ -170,7 +198,7 @@ namespace keepass2android
//retry without password: //retry without password:
_compositeKey.RemoveUserKey(passwordKey); _compositeKey.RemoveUserKey(passwordKey);
//retry: //retry:
return TryLoad(databaseStream); return TryLoad(databaseStream, requiresSubsequentSync);
} }
else throw; else throw;
} }

View File

@@ -38,6 +38,7 @@ namespace keepass2android
private readonly IKp2aApp _app; private readonly IKp2aApp _app;
private readonly Database _db; private readonly Database _db;
private readonly bool _dontSave; private readonly bool _dontSave;
private bool requiresSubsequentSync = false; //if true, we need to sync the file after saving.
/// <summary> /// <summary>
/// stream for reading the data from the original file. If this is set to a non-null value, we know we need to sync /// 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; IOConnectionInfo ioc = _db.Ioc;
IFileStorage fileStorage = _app.GetFileStorage(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)) if ((!_app.GetBooleanPreference(PreferenceKey.CheckForFileChangesOnSave))
|| (_db.KpDatabase.HashOfFileOnDisk == null)) //first time saving || (_db.KpDatabase.HashOfFileOnDisk == null)) //first time saving
{ {
PerformSaveWithoutCheck(fileStorage, ioc); PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true); FinishWithSuccess();
return; return;
} }
} }
@@ -123,9 +134,9 @@ namespace keepass2android
bool hasStreamForOrigFile = (_streamForOrigFile != null); bool hasStreamForOrigFile = (_streamForOrigFile != null);
bool hasChangeFast = hasStreamForOrigFile || bool hasChangeFast = hasStreamForOrigFile ||
fileStorage.CheckForFileChangeFast(ioc, _db.LastFileVersion); //first try to use the fast change detection; 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) == (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) if (hasHashChanged)
{ {
@@ -158,7 +169,7 @@ namespace keepass2android
RunInWorkerThread(() => RunInWorkerThread(() =>
{ {
PerformSaveWithoutCheck(fileStorage, ioc); PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true); FinishWithSuccess();
}); });
}, },
//cancel //cancel
@@ -174,7 +185,7 @@ namespace keepass2android
else else
{ {
PerformSaveWithoutCheck(fileStorage, ioc); PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true); FinishWithSuccess();
} }
} }
@@ -194,11 +205,28 @@ namespace keepass2android
} }
else 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) private void MergeAndFinish(IFileStorage fileStorage, IOConnectionInfo ioc)
{ {
//note: when synced, the file might be downloaded once again from the server. Caching the data //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); MergeIn(fileStorage, ioc);
PerformSaveWithoutCheck(fileStorage, ioc); PerformSaveWithoutCheck(fileStorage, ioc);
_db.UpdateGlobals(); _db.UpdateGlobals();
Finish(true); FinishWithSuccess();
} }
private void RunInWorkerThread(Action runHandler) private void RunInWorkerThread(Action runHandler)
@@ -282,7 +310,7 @@ namespace keepass2android
private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc) private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc)
{ {
StatusLogger.UpdateSubMessage(""); StatusLogger.UpdateSubMessage("");
_db.SaveData(); _db.SaveData(fileStorage);
_db.LastFileVersion = fileStorage.GetCurrentFileVersionFast(ioc); _db.LastFileVersion = fileStorage.GetCurrentFileVersionFast(ioc);
} }

View File

@@ -1753,17 +1753,10 @@ namespace keepass2android
cbOfflineMode.Checked = cbOfflineMode.Checked =
App.Kp2a App.Kp2a
.OfflineModePreference; //this won't overwrite new user settings because every change is directly saved in settings .OfflineModePreference; //this won't overwrite new user settings because every change is directly saved in settings
LinearLayout offlineModeContainer = FindViewById<LinearLayout>(Resource.Id.work_offline_container);
var cachingFileStorage = App.Kp2a.GetFileStorage(_ioConnection) as CachingFileStorage; CheckBox cbSyncInBackground = (CheckBox)FindViewById(Resource.Id.sync_in_background)!;
if ((cachingFileStorage != null) && cachingFileStorage.IsCached(_ioConnection)) cbSyncInBackground.Checked = App.Kp2a.SyncInBackgroundPreference;
{ UpdateInternalCacheCheckboxesVisibility();
offlineModeContainer.Visibility = ViewStates.Visible;
}
else
{
offlineModeContainer.Visibility = ViewStates.Gone;
App.Kp2a.OfflineMode = false;
}
@@ -2040,9 +2033,37 @@ namespace keepass2android
App.Kp2a.OfflineModePreference = App.Kp2a.OfflineMode = args.IsChecked; App.Kp2a.OfflineModePreference = App.Kp2a.OfflineMode = args.IsChecked;
}; };
} CheckBox cbSyncInBackground = (CheckBox)FindViewById(Resource.Id.sync_in_background);
cbSyncInBackground.CheckedChange += (sender, args) =>
{
App.Kp2a.SyncInBackgroundPreference = args.IsChecked;
UpdateInternalCacheCheckboxesVisibility();
private String LoadKeyProviderStringForIoc(String filename) { };
}
private void UpdateInternalCacheCheckboxesVisibility()
{
LinearLayout syncInBackgroundContainer = FindViewById<LinearLayout>(Resource.Id.sync_in_background_container)!;
LinearLayout offlineModeContainer = FindViewById<LinearLayout>(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 ) { if ( _rememberKeyfile ) {
string keyfile = App.Kp2a.FileDbHelper.GetKeyFileForFile(filename); string keyfile = App.Kp2a.FileDbHelper.GetKeyFileForFile(filename);
if (String.IsNullOrEmpty(keyfile)) if (String.IsNullOrEmpty(keyfile))

View File

@@ -318,6 +318,30 @@
android:text="@string/help_quickunlock" android:text="@string/help_quickunlock"
/> />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/sync_in_background_container"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<CheckBox
android:id="@+id/sync_in_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/SyncOfflineCacheInBackground_title" />
<keepass2android.views.Kp2aShortHelpView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/TextAppearance_Help_Dense"
app:help_text="@string/SyncOfflineCacheInBackground_summary"
app:title_text="@string/SyncOfflineCacheInBackground_title"
android:text="@string/SyncOfflineCacheInBackground_summary"
/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/work_offline_container" android:id="@+id/work_offline_container"
android:orientation="horizontal" android:orientation="horizontal"
@@ -328,7 +352,7 @@
android:id="@+id/work_offline" android:id="@+id/work_offline"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/UseOfflineMode" /> android:text="@string/UseOfflineMode" />
<keepass2android.views.Kp2aShortHelpView <keepass2android.views.Kp2aShortHelpView
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -341,6 +365,7 @@
/> />
</LinearLayout> </LinearLayout>
<Button <Button
android:id="@+id/kill_app" android:id="@+id/kill_app"
android:text="@string/kill_app_label" android:text="@string/kill_app_label"

View File

@@ -56,12 +56,16 @@ namespace keepass2android
OperationWithFinishHandler task; OperationWithFinishHandler task;
OnOperationFinishedHandler onOperationFinishedHandler = new ActionOnOperationFinished(_activity, (success, message, activity) => OnOperationFinishedHandler onOperationFinishedHandler = new ActionOnOperationFinished(_activity, (success, message, activity) =>
{ {
if (!String.IsNullOrEmpty(message)) new Handler(Looper.MainLooper).Post(() =>
App.Kp2a.ShowMessage(activity, message, success ? MessageSeverity.Info : MessageSeverity.Error); {
if (!String.IsNullOrEmpty(message))
App.Kp2a.ShowMessage(activity, message, success ? MessageSeverity.Info : MessageSeverity.Error);
// Tell the adapter to refresh it's list // Tell the adapter to refresh it's list
BaseAdapter adapter = (activity as GroupBaseActivity)?.ListAdapter; BaseAdapter adapter = (activity as GroupBaseActivity)?.ListAdapter;
new Handler(Looper.MainLooper).Post(() => adapter?.NotifyDataSetChanged());
adapter?.NotifyDataSetChanged();
});
if (App.Kp2a.CurrentDb?.OtpAuxFileIoc != null) if (App.Kp2a.CurrentDb?.OtpAuxFileIoc != null)
{ {

View File

@@ -46,7 +46,10 @@ namespace keepass2android.Utils
{ {
public void ShowMessage(Message message) public void ShowMessage(Message message)
{ {
Toast.MakeText(App.Context, message.Text, ToastLength.Long).Show(); new Handler(Looper.MainLooper).Post(() =>
{
Toast.MakeText(App.Context, message.Text, ToastLength.Long).Show();
});
} }
public List<Message> PendingMessages => new(); public List<Message> PendingMessages => new();

View File

@@ -798,9 +798,14 @@ namespace keepass2android
fileStorage = innerFileStorage; fileStorage = innerFileStorage;
} }
} }
if (fileStorage is IOfflineSwitchable) if (fileStorage is IOfflineSwitchable switchable)
{ {
((IOfflineSwitchable)fileStorage).IsOffline = App.Kp2a.OfflineMode; switchable.IsOffline = App.Kp2a.OfflineMode;
if (switchable.IsOffline)
{
//users of the file storage can set this to false, but the default is to show a warning:
switchable.TriggerWarningWhenFallingBackToCache = true;
}
} }
return fileStorage; return fileStorage;
} }
@@ -1151,10 +1156,28 @@ namespace keepass2android
} }
} }
/// <summary>
/// true if the app is used in offline mode public bool SyncInBackgroundPreference
/// </summary> {
public bool OfflineMode get
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext);
return prefs.GetBoolean(LocaleManager.LocalizedAppContext.GetString(Resource.String.SyncOfflineCacheInBackground_key), false);
}
set
{
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext);
ISharedPreferencesEditor edit = prefs.Edit();
edit.PutBoolean(LocaleManager.LocalizedAppContext.GetString(Resource.String.SyncOfflineCacheInBackground_key), value);
edit.Commit();
}
}
/// <summary>
/// true if the app is used in offline mode
/// </summary>
public bool OfflineMode
{ {
get; set; get; set;
} }