fix issues with background sync and multiple databases (especially autoopen)

This commit is contained in:
Philipp Crocoll
2025-06-03 15:37:21 +02:00
parent a4a3112dc6
commit 0cd9df7415
8 changed files with 76 additions and 48 deletions

View File

@@ -9,6 +9,7 @@ using KeePassLib.Serialization;
using keepass2android.Io; using keepass2android.Io;
using KeePass.Util; using KeePass.Util;
using Group.Pals.Android.Lib.UI.Filechooser.Utils; using Group.Pals.Android.Lib.UI.Filechooser.Utils;
using KeePassLib;
namespace keepass2android namespace keepass2android
{ {
@@ -16,12 +17,14 @@ namespace keepass2android
{ {
private readonly IKp2aApp _app; private readonly IKp2aApp _app;
private IDatabaseModificationWatcher _modificationWatcher; private IDatabaseModificationWatcher _modificationWatcher;
private readonly Database _database;
public SynchronizeCachedDatabase(IKp2aApp app, OnOperationFinishedHandler operationFinishedHandler, IDatabaseModificationWatcher modificationWatcher) public SynchronizeCachedDatabase(IKp2aApp app, Database database, OnOperationFinishedHandler operationFinishedHandler, IDatabaseModificationWatcher modificationWatcher)
: base(app, operationFinishedHandler) : base(app, operationFinishedHandler)
{ {
_app = app; _app = app;
_database = database;
_modificationWatcher = modificationWatcher; _modificationWatcher = modificationWatcher;
} }
@@ -29,7 +32,7 @@ namespace keepass2android
{ {
try try
{ {
IOConnectionInfo ioc = _app.CurrentDb.Ioc; IOConnectionInfo ioc = _database.Ioc;
IFileStorage fileStorage = _app.GetFileStorage(ioc); IFileStorage fileStorage = _app.GetFileStorage(ioc);
if (!(fileStorage is CachingFileStorage)) if (!(fileStorage is CachingFileStorage))
{ {
@@ -86,13 +89,13 @@ namespace keepass2android
{ {
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
} }
}), _app.CurrentDb, false, remoteData, _modificationWatcher); }), _database, false, remoteData, _modificationWatcher);
_saveDb.SetStatusLogger(StatusLogger); _saveDb.SetStatusLogger(StatusLogger);
_saveDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message _saveDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message
_saveDb.SyncInBackground = false; _saveDb.SyncInBackground = false;
_saveDb.Run(); _saveDb.Run();
_app.CurrentDb.UpdateGlobals(); _database.UpdateGlobals();
_app.MarkAllGroupsAsDirty(); _app.MarkAllGroupsAsDirty();
} }
@@ -109,7 +112,7 @@ namespace keepass2android
{ {
new Handler(Looper.MainLooper).Post(() => new Handler(Looper.MainLooper).Post(() =>
{ {
_app.CurrentDb.UpdateGlobals(); _database.UpdateGlobals();
_app.MarkAllGroupsAsDirty(); _app.MarkAllGroupsAsDirty();
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
@@ -118,10 +121,9 @@ namespace keepass2android
} }
}); });
var _loadDb = new LoadDb(_app, ioc, Task.FromResult(remoteData), var _loadDb = new LoadDb(_app, ioc, Task.FromResult(remoteData),
_app.CurrentDb.KpDatabase.MasterKey, null, onFinished, true, false, _modificationWatcher); _database.KpDatabase.MasterKey, null, onFinished, true, false, _modificationWatcher);
_loadDb.SetStatusLogger(StatusLogger); _loadDb.SetStatusLogger(StatusLogger);
_loadDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message _loadDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message
_loadDb.SyncInBackground = false;
_loadDb.Run(); _loadDb.Run();
} }

View File

@@ -41,7 +41,7 @@ namespace keepass2android
IDatabaseFormat _format; IDatabaseFormat _format;
public bool DoNotSetStatusLoggerMessage = false; public bool DoNotSetStatusLoggerMessage = false;
public bool SyncInBackground { get; set; }
public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey,
string keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler, string keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler,
@@ -56,7 +56,6 @@ namespace keepass2android
_updateLastUsageTimestamp = updateLastUsageTimestamp; _updateLastUsageTimestamp = updateLastUsageTimestamp;
_makeCurrent = makeCurrent; _makeCurrent = makeCurrent;
_rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile); _rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile);
SyncInBackground = _app.SyncInBackgroundPreference;
} }
protected bool success = false; protected bool success = false;
@@ -76,7 +75,7 @@ namespace keepass2android
var fileStorage = _app.GetFileStorage(_ioc); var fileStorage = _app.GetFileStorage(_ioc);
bool requiresSubsequentSync = false; RequiresSubsequentSync = false;
if (!DoNotSetStatusLoggerMessage) if (!DoNotSetStatusLoggerMessage)
@@ -100,7 +99,7 @@ namespace keepass2android
cachingFileStorage.IsOffline = true; cachingFileStorage.IsOffline = true;
//no warning. We'll trigger a sync later. //no warning. We'll trigger a sync later.
cachingFileStorage.TriggerWarningWhenFallingBackToCache = false; cachingFileStorage.TriggerWarningWhenFallingBackToCache = false;
requiresSubsequentSync = true; RequiresSubsequentSync = true;
} }
using (Stream s = fileStorage.OpenFileForRead(_ioc)) using (Stream s = fileStorage.OpenFileForRead(_ioc))
@@ -118,7 +117,7 @@ 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(fileStorage.GetFileExtension(_ioc))); _format = new KdbxDatabaseFormat(KdbxDatabaseFormat.GetFormatToUse(fileStorage.GetFileExtension(_ioc)));
TryLoad(databaseStream, requiresSubsequentSync); TryLoad(databaseStream);
@@ -171,12 +170,14 @@ namespace keepass2android
} }
public bool RequiresSubsequentSync { get; set; } = false;
/// <summary> /// <summary>
/// Holds the exception which was thrown during execution (if any) /// Holds the exception which was thrown during execution (if any)
/// </summary> /// </summary>
public Exception Exception { get; set; } public Exception Exception { get; set; }
Database TryLoad(MemoryStream databaseStream, bool requiresSubsequentSync) Database TryLoad(MemoryStream databaseStream)
{ {
//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.
@@ -197,19 +198,6 @@ namespace keepass2android
Database newDb = Database newDb =
_app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent, _modificationWatcher); _app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent, _modificationWatcher);
Kp2aLog.Log("LoadDB OK"); Kp2aLog.Log("LoadDB OK");
if (requiresSubsequentSync)
{
var syncTask = new SynchronizeCachedDatabase(_app, new ActionOnOperationFinished(_app,
(success, message, context) =>
{
if (!String.IsNullOrEmpty(message))
_app.ShowMessage(context, message,
success ? MessageSeverity.Info : MessageSeverity.Error);
}), new BackgroundDatabaseModificationLocker(_app)
);
OperationRunner.Instance.Run(_app, syncTask);
}
Finish(true, _format.SuccessMessage); Finish(true, _format.SuccessMessage);
return newDb; return newDb;
@@ -217,7 +205,7 @@ namespace keepass2android
catch (OldFormatException) catch (OldFormatException)
{ {
_format = new KdbDatabaseFormat(_app); _format = new KdbDatabaseFormat(_app);
return TryLoad(databaseStream, requiresSubsequentSync); return TryLoad(databaseStream);
} }
catch (InvalidCompositeKeyException) catch (InvalidCompositeKeyException)
{ {
@@ -229,7 +217,7 @@ namespace keepass2android
//retry without password: //retry without password:
_compositeKey.RemoveUserKey(passwordKey); _compositeKey.RemoveUserKey(passwordKey);
//retry: //retry:
return TryLoad(databaseStream, requiresSubsequentSync); return TryLoad(databaseStream);
} }
else throw; else throw;
} }

View File

@@ -235,7 +235,7 @@ namespace keepass2android
{ {
if (requiresSubsequentSync) if (requiresSubsequentSync)
{ {
var syncTask = new SynchronizeCachedDatabase(_app, new ActionOnOperationFinished(_app, var syncTask = new SynchronizeCachedDatabase(_app, _db, new ActionOnOperationFinished(_app,
(success, message, context) => (success, message, context) =>
{ {
if (!System.String.IsNullOrEmpty(message)) if (!System.String.IsNullOrEmpty(message))

View File

@@ -1222,7 +1222,7 @@ namespace keepass2android
return true; return true;
case Resource.Id.menu_sync: case Resource.Id.menu_sync:
new SyncUtil(this).StartSynchronizeDatabase(); new SyncUtil(this).StartSynchronizeDatabase(App.Kp2a.CurrentDb.Ioc);
return true; return true;
case Resource.Id.menu_work_offline: case Resource.Id.menu_work_offline:
@@ -1233,7 +1233,7 @@ namespace keepass2android
case Resource.Id.menu_work_online: case Resource.Id.menu_work_online:
App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = false; App.Kp2a.OfflineMode = App.Kp2a.OfflineModePreference = false;
UpdateOfflineModeMenu(); UpdateOfflineModeMenu();
new SyncUtil(this).StartSynchronizeDatabase(); new SyncUtil(this).StartSynchronizeDatabase(App.Kp2a.CurrentDb.Ioc);
return true; return true;
case Resource.Id.menu_open_other_db: case Resource.Id.menu_open_other_db:
AppTask.SetActivityResult(this, KeePass.ExitLoadAnotherDb); AppTask.SetActivityResult(this, KeePass.ExitLoadAnotherDb);

View File

@@ -222,6 +222,7 @@ namespace keepass2android
//StackBaseActivity will launch the next activity //StackBaseActivity will launch the next activity
Intent data = new Intent(); Intent data = new Intent();
data.PutExtra("ioc", IOConnectionInfo.SerializeToString(_ioConnection)); data.PutExtra("ioc", IOConnectionInfo.SerializeToString(_ioConnection));
data.PutExtra("requiresSubsequentSync", _lastLoadOperation?.RequiresSubsequentSync == true);
SetResult(Result.Ok, data); SetResult(Result.Ok, data);
@@ -1441,13 +1442,20 @@ namespace keepass2android
Handler handler = new Handler(); Handler handler = new Handler();
OnOperationFinishedHandler onOperationFinishedHandler = new AfterLoad(handler, this, _ioConnection); OnOperationFinishedHandler onOperationFinishedHandler = new AfterLoad(handler, this, _ioConnection);
LoadDb task = (KeyProviderTypes.Contains(KeyProviders.Otp)) LoadDb loadOperation = (KeyProviderTypes.Contains(KeyProviders.Otp))
? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(), ? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(),
onOperationFinishedHandler, this, true, _makeCurrent) onOperationFinishedHandler, this, true, _makeCurrent)
: new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(), onOperationFinishedHandler,true, _makeCurrent); : new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(), onOperationFinishedHandler,true, _makeCurrent);
_loadDbFileTask = null; // prevent accidental re-use _loadDbFileTask = null; // prevent accidental re-use
new BlockingOperationStarter(App.Kp2a, task).Run(); _lastLoadOperation = loadOperation;
//Don't use BlockingOperationStarter as that would cancel running operations.
//This is bad when used with AutoOpen: we might get here when one database has loaded and is now synced in the background.
//We don't want to cancel this.
OperationRunner.Instance.Run(App.Kp2a, loadOperation, true);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -1572,6 +1580,8 @@ namespace keepass2android
} }
private bool hasRequestedKeyboardActivation = false; private bool hasRequestedKeyboardActivation = false;
private LoadDb _lastLoadOperation;
protected override void OnStart() protected override void OnStart()
{ {
base.OnStart(); base.OnStart();
@@ -1886,10 +1896,11 @@ namespace keepass2android
Handler handler = new Handler(); Handler handler = new Handler();
OnOperationFinishedHandler onOperationFinishedHandler = new AfterLoad(handler, this, _ioConnection); OnOperationFinishedHandler onOperationFinishedHandler = new AfterLoad(handler, this, _ioConnection);
_performingLoad = true; _performingLoad = true;
LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKeyForImmediateLoad, GetKeyProviderString(), LoadDb loadOperation = new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKeyForImmediateLoad, GetKeyProviderString(),
onOperationFinishedHandler, false, _makeCurrent); onOperationFinishedHandler, false, _makeCurrent);
_loadDbFileTask = null; // prevent accidental re-use _loadDbFileTask = null; // prevent accidental re-use
new BlockingOperationStarter(App.Kp2a, task).Run(); _lastLoadOperation = loadOperation;
OperationRunner.Instance.Run(App.Kp2a, loadOperation, true);
compositeKeyForImmediateLoad = null; //don't reuse or keep in memory compositeKeyForImmediateLoad = null; //don't reuse or keep in memory
} }
@@ -2242,7 +2253,7 @@ namespace keepass2android
if (!Success) if (!Success)
_act.InitFingerprintUnlock(); _act.InitFingerprintUnlock();
_act._lastLoadOperation = null;
_act._performingLoad = false; _act._performingLoad = false;
} }

View File

@@ -339,7 +339,7 @@ namespace keepass2android
if (PreferenceManager.GetDefaultSharedPreferences(this) if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.SyncAfterQuickUnlock_key), false)) .GetBoolean(GetString(Resource.String.SyncAfterQuickUnlock_key), false))
{ {
new SyncUtil(this).StartSynchronizeDatabase(); new SyncUtil(this).StartSynchronizeDatabase(App.Kp2a.CurrentDb.Ioc);
} }
Finish(); Finish();

View File

@@ -558,6 +558,7 @@ namespace keepass2android
private OpenDatabaseAdapter _adapter; private OpenDatabaseAdapter _adapter;
private MyBroadcastReceiver _intentReceiver; private MyBroadcastReceiver _intentReceiver;
private bool _isForeground; private bool _isForeground;
private readonly List<IOConnectionInfo> _pendingBackgroundSyncs = new List<IOConnectionInfo>();
public override void OnBackPressed() public override void OnBackPressed()
{ {
@@ -598,11 +599,36 @@ namespace keepass2android
string iocString = data?.GetStringExtra("ioc"); string iocString = data?.GetStringExtra("ioc");
IOConnectionInfo ioc = IOConnectionInfo.UnserializeFromString(iocString); IOConnectionInfo ioc = IOConnectionInfo.UnserializeFromString(iocString);
//we first store the required sync operation and delay its execution until we loaded all AutoOpen entries (from local file)
//if required
bool requiresSubsequentSync = data?.GetBooleanExtra("requiresSubsequentSync", false) ?? false;
if (requiresSubsequentSync)
{
_pendingBackgroundSyncs.Add(ioc);
}
if (App.Kp2a.TrySelectCurrentDb(ioc)) if (App.Kp2a.TrySelectCurrentDb(ioc))
{ {
if (OpenAutoExecEntries(App.Kp2a.CurrentDb)) return; if (OpenAutoExecEntries(App.Kp2a.CurrentDb)) return;
LaunchingOther = true; LaunchingOther = true;
AppTask.CanActivateSearchViewOnStart = true; AppTask.CanActivateSearchViewOnStart = true;
foreach (var pendingSyncIoc in _pendingBackgroundSyncs)
{
try
{
new SyncUtil(this).StartSynchronizeDatabase(pendingSyncIoc);
}
catch (Exception e)
{
App.Kp2a.ShowMessage(this, "Failed to synchronize database", MessageSeverity.Error);
Kp2aLog.LogUnexpectedError(e);
}
}
_pendingBackgroundSyncs.Clear();
AppTask.LaunchFirstGroupActivity(this); AppTask.LaunchFirstGroupActivity(this);
} }

View File

@@ -50,9 +50,11 @@ namespace keepass2android
} }
public void StartSynchronizeDatabase() public void StartSynchronizeDatabase(IOConnectionInfo ioc)
{ {
var filestorage = App.Kp2a.GetFileStorage(App.Kp2a.CurrentDb.Ioc); var filestorage = App.Kp2a.GetFileStorage(ioc);
var databaseForIoc = App.Kp2a.GetDatabase(ioc);
OperationWithFinishHandler task; OperationWithFinishHandler task;
OnOperationFinishedHandler onOperationFinishedHandler = new ActionInContextInstanceOnOperationFinished(_activity.ContextInstanceId, App.Kp2a, (success, message, context) => OnOperationFinishedHandler onOperationFinishedHandler = new ActionInContextInstanceOnOperationFinished(_activity.ContextInstanceId, App.Kp2a, (success, message, context) =>
{ {
@@ -67,9 +69,9 @@ namespace keepass2android
adapter?.NotifyDataSetChanged(); adapter?.NotifyDataSetChanged();
}); });
if (App.Kp2a.CurrentDb?.OtpAuxFileIoc != null) if (databaseForIoc?.OtpAuxFileIoc != null)
{ {
var task2 = new SyncOtpAuxFile(_activity, App.Kp2a.CurrentDb.OtpAuxFileIoc); var task2 = new SyncOtpAuxFile(_activity, databaseForIoc.OtpAuxFileIoc);
OperationRunner.Instance.Run(App.Kp2a, task2); OperationRunner.Instance.Run(App.Kp2a, task2);
} }
@@ -79,11 +81,10 @@ namespace keepass2android
if (filestorage is CachingFileStorage) if (filestorage is CachingFileStorage)
{ {
task = new SynchronizeCachedDatabase(App.Kp2a, onOperationFinishedHandler, new BackgroundDatabaseModificationLocker(App.Kp2a)); task = new SynchronizeCachedDatabase(App.Kp2a, databaseForIoc, onOperationFinishedHandler, new BackgroundDatabaseModificationLocker(App.Kp2a));
} }
else else
{ {
//TODO do we want this to run in the background?
task = new CheckDatabaseForChanges( App.Kp2a, onOperationFinishedHandler); task = new CheckDatabaseForChanges( App.Kp2a, onOperationFinishedHandler);
} }