allow to cancel background operations manually or when another operation starts; block database updates in certain activities, e.g. EntryEdit;

This commit is contained in:
Philipp Crocoll
2025-05-20 17:02:35 +02:00
parent 52121c6a85
commit 324fc1f2ee
25 changed files with 507 additions and 244 deletions

View File

@@ -122,4 +122,20 @@ public class BackgroundOperationRunner
}
public void CancelAll()
{
lock (_taskQueueLock)
{
if (_thread != null)
{
_thread.Interrupt();
_thread = null;
_statusLogger?.EndLogging();
}
_taskQueue.Clear();
_currentlyRunningTask = null;
}
}
}

View File

@@ -78,6 +78,9 @@ namespace keepass2android
SetupProgressDialog(app);
app.CancelBackgroundOperations();
// Set code to run when this is finished
_task.operationFinishedHandler = new AfterTask(app, task.operationFinishedHandler, _handler, this);

View File

@@ -52,7 +52,9 @@ namespace keepass2android
/// <summary>
/// Loads the specified data as the currently open database, as unlocked.
/// </summary>
Database LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey, IKp2aStatusLogger statusLogger, IDatabaseFormat databaseFormat, bool makeCurrent);
Database LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compKey,
IKp2aStatusLogger statusLogger, IDatabaseFormat databaseFormat, bool makeCurrent,
IDatabaseModificationWatcher modificationWatcher);
HashSet<PwGroup> DirtyGroups { get; }
@@ -134,12 +136,12 @@ namespace keepass2android
bool CheckForDuplicateUuids { get; }
#if !NoNet && !EXCLUDE_JAVAFILESTORAGE
ICertificateErrorHandler CertificateErrorHandler { get; }
#endif
bool SyncInBackgroundPreference { get; set; }
void StartBackgroundSyncService();
ReaderWriterLockSlim DatabasesBackgroundModificationLock { get; }
bool CancelBackgroundOperations();
}
}

View File

@@ -111,6 +111,11 @@ namespace keepass2android.Io
}
Java.Lang.Exception exception = e as Java.Lang.Exception;
if ((exception is Java.Lang.InterruptedException) || (exception is Java.IO.InterruptedIOException))
{
throw new Java.Lang.InterruptedException(exception.Message);
}
if (exception != null)
{
var ex = new Exception(exception.LocalizedMessage ??

View File

@@ -79,7 +79,7 @@ public class ProgressUiAsStatusLoggerAdapter : IKp2aStatusLogger
public bool ContinueWork()
{
return true;
return !Java.Lang.Thread.Interrupted();
}
public void UpdateMessage(UiStringKey stringKey)

View File

@@ -396,8 +396,6 @@ namespace keepass2android
{
PwGroupV3 toGroup = new PwGroupV3();
toGroup.Name = fromGroup.Name;
//todo remove
Android.Util.Log.Debug("KP2A", "save kdb: group " + fromGroup.Name);
toGroup.TCreation = new PwDate(ConvertTime(fromGroup.CreationTime));
toGroup.TLastAccess= new PwDate(ConvertTime(fromGroup.LastAccessTime));

View File

@@ -8,92 +8,98 @@ using Android.OS;
using KeePassLib.Serialization;
using keepass2android.Io;
using KeePass.Util;
using Group.Pals.Android.Lib.UI.Filechooser.Utils;
namespace keepass2android
{
public class SynchronizeCachedDatabase: OperationWithFinishHandler
{
private readonly IKp2aApp _app;
private IDatabaseModificationWatcher _modificationWatcher;
public SynchronizeCachedDatabase(IKp2aApp app, OnOperationFinishedHandler operationFinishedHandler)
public SynchronizeCachedDatabase(IKp2aApp app, OnOperationFinishedHandler operationFinishedHandler, IDatabaseModificationWatcher modificationWatcher)
: base(app, operationFinishedHandler)
{
_app = app;
}
{
_app = app;
_modificationWatcher = modificationWatcher;
}
public override void Run()
{
try
{
IOConnectionInfo ioc = _app.CurrentDb.Ioc;
IFileStorage fileStorage = _app.GetFileStorage(ioc);
if (!(fileStorage is CachingFileStorage))
{
throw new Exception("Cannot sync a non-cached database!");
}
StatusLogger.UpdateMessage(UiStringKey.SynchronizingCachedDatabase);
CachingFileStorage cachingFileStorage = (CachingFileStorage) fileStorage;
try
{
IOConnectionInfo ioc = _app.CurrentDb.Ioc;
IFileStorage fileStorage = _app.GetFileStorage(ioc);
if (!(fileStorage is CachingFileStorage))
{
throw new Exception("Cannot sync a non-cached database!");
}
//download file from remote location and calculate hash:
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.DownloadingRemoteFile));
string hash;
StatusLogger.UpdateMessage(UiStringKey.SynchronizingCachedDatabase);
CachingFileStorage cachingFileStorage = (CachingFileStorage)fileStorage;
//TODO remove
//download file from remote location and calculate hash:
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.DownloadingRemoteFile));
string hash;
//TODO remove
Thread.Sleep(5000);
MemoryStream remoteData;
try
{
remoteData = cachingFileStorage.GetRemoteDataAndHash(ioc, out hash);
Kp2aLog.Log("Checking for file change. Current hash = " + hash);
}
catch (FileNotFoundException)
{
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.RestoringRemoteFile));
cachingFileStorage.UpdateRemoteFile(ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions));
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
Kp2aLog.Log("Checking for file change: file not found");
return;
}
//check if remote file was modified:
MemoryStream remoteData;
try
{
remoteData = cachingFileStorage.GetRemoteDataAndHash(ioc, out hash);
Kp2aLog.Log("Checking for file change. Current hash = " + hash);
}
catch (FileNotFoundException)
{
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.RestoringRemoteFile));
cachingFileStorage.UpdateRemoteFile(ioc,
_app.GetBooleanPreference(PreferenceKey.UseFileTransactions));
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
Kp2aLog.Log("Checking for file change: file not found");
return;
}
//check if remote file was modified:
var baseVersionHash = cachingFileStorage.GetBaseVersionHash(ioc);
Kp2aLog.Log("Checking for file change. baseVersionHash = " + baseVersionHash);
if (baseVersionHash != hash ||
true//TODO remove
)
{
//remote file is modified
if (cachingFileStorage.HasLocalChanges(ioc)
|| true //TODO remove
)
{
//conflict! need to merge
var _saveDb = new SaveDb(_app, new ActionOnOperationFinished(_app, (success, result, activity) =>
{
if (!success)
{
Finish(false, result);
}
else
{
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
}), _app.CurrentDb, false, remoteData);
if (baseVersionHash != hash ||
true //TODO remove
)
{
//remote file is modified
if (cachingFileStorage.HasLocalChanges(ioc)
|| false //TODO remove
)
{
//conflict! need to merge
var _saveDb = new SaveDb(_app, new ActionOnOperationFinished(_app,
(success, result, activity) =>
{
if (!success)
{
Finish(false, result);
}
else
{
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
}), _app.CurrentDb, false, remoteData, _modificationWatcher);
_saveDb.SetStatusLogger(StatusLogger);
_saveDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message
_saveDb.SyncInBackground = false;
_saveDb.Run();
_saveDb.Run();
_app.CurrentDb.UpdateGlobals();
_app.MarkAllGroupsAsDirty();
}
else
{
_app.MarkAllGroupsAsDirty();
}
else
{
//only the remote file was modified -> reload database.
var onFinished = new ActionOnOperationFinished(_app, (success, result, activity) =>
var onFinished = new ActionOnOperationFinished(_app, (success, result, activity) =>
{
if (!success)
{
@@ -111,32 +117,44 @@ namespace keepass2android
}
});
var _loadDb = new LoadDb(_app, ioc, Task.FromResult(remoteData), _app.CurrentDb.KpDatabase.MasterKey, null, onFinished, true, false);
var _loadDb = new LoadDb(_app, ioc, Task.FromResult(remoteData),
_app.CurrentDb.KpDatabase.MasterKey, null, onFinished, true, false, _modificationWatcher);
_loadDb.SetStatusLogger(StatusLogger);
_loadDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message
_loadDb.SyncInBackground = false;
_loadDb.Run();
}
}
else
{
//remote file is unmodified
if (cachingFileStorage.HasLocalChanges(ioc))
{
//but we have local changes -> upload:
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.UploadingFile));
cachingFileStorage.UpdateRemoteFile(ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions));
StatusLogger.UpdateSubMessage("");
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
else
{
//files are in sync: just set the result
Finish(true, _app.GetResourceString(UiStringKey.FilesInSync));
}
}
}
}
else
{
//remote file is unmodified
if (cachingFileStorage.HasLocalChanges(ioc))
{
//but we have local changes -> upload:
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.UploadingFile));
cachingFileStorage.UpdateRemoteFile(ioc,
_app.GetBooleanPreference(PreferenceKey.UseFileTransactions));
StatusLogger.UpdateSubMessage("");
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
else
{
//files are in sync: just set the result
Finish(true, _app.GetResourceString(UiStringKey.FilesInSync));
}
}
}
catch (Java.Lang.InterruptedException e)
{
Kp2aLog.LogUnexpectedError(e);
//no Finish()
}
catch (Java.IO.InterruptedIOException e)
{
Kp2aLog.LogUnexpectedError(e);
//no Finish()
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);

View File

@@ -72,7 +72,7 @@ namespace keepass2android
_app.CurrentDb.Elements.Add(Group);
// Commit to disk
SaveDb save = new SaveDb(_app, _app.CurrentDb, operationFinishedHandler, DontSave);
SaveDb save = new SaveDb(_app, _app.CurrentDb, operationFinishedHandler, DontSave, null);
save.SetStatusLogger(StatusLogger);
save.Run();
}

View File

@@ -311,7 +311,7 @@ namespace keepass2android
_app.DirtyGroups.Add(templateGroup);
// Commit to disk
SaveDb save = new SaveDb( _app, _app.CurrentDb, operationFinishedHandler);
SaveDb save = new SaveDb(_app, _app.CurrentDb, operationFinishedHandler);
save.SetStatusLogger(StatusLogger);
save.Run();
}
@@ -335,7 +335,6 @@ namespace keepass2android
_app.DirtyGroups.Add(_app.CurrentDb.KpDatabase.RootGroup);
_app.CurrentDb.GroupsById[templateGroup.Uuid] = templateGroup;
_app.CurrentDb.Elements.Add(templateGroup);
}
addedEntries = new List<PwEntry>();

View File

@@ -84,7 +84,7 @@ namespace keepass2android
addTemplates.AddTemplates(out addedEntries);
// Commit changes
SaveDb save = new SaveDb(_app, db, operationFinishedHandler, _dontSave);
SaveDb save = new SaveDb(_app, db, operationFinishedHandler, _dontSave, null);
save.SetStatusLogger(StatusLogger);
_operationFinishedHandler = null;
save.Run();

View File

@@ -0,0 +1,39 @@
using Java.Lang;
namespace keepass2android;
public interface IDatabaseModificationWatcher
{
void BeforeModifyDatabases();
void AfterModifyDatabases();
}
public class NullDatabaseModificationWatcher : IDatabaseModificationWatcher
{
public void BeforeModifyDatabases() { }
public void AfterModifyDatabases() { }
}
public class BackgroundDatabaseModificationLocker(IKp2aApp app) : IDatabaseModificationWatcher
{
public void BeforeModifyDatabases()
{
while (true)
{
if (app.DatabasesBackgroundModificationLock.TryEnterWriteLock(TimeSpan.FromSeconds(0.1)))
{
break;
}
if (Java.Lang.Thread.Interrupted())
{
throw new InterruptedException();
}
}
}
public void AfterModifyDatabases()
{
app.DatabasesBackgroundModificationLock.ExitWriteLock();
}
}

View File

@@ -238,7 +238,7 @@ namespace keepass2android
}, operationFinishedHandler);
// Commit database
SaveDb save = new SaveDb( App, Db, operationFinishedHandler, false);
SaveDb save = new SaveDb( App, Db, operationFinishedHandler, false, null);
save.ShowDatabaseIocInStatus = ShowDatabaseIocInStatus;
save.SetStatusLogger(StatusLogger);

View File

@@ -43,9 +43,12 @@ namespace keepass2android
public bool DoNotSetStatusLoggerMessage = false;
public bool SyncInBackground { get; set; }
public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, String keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler, bool updateLastUsageTimestamp, bool makeCurrent): base(app, operationFinishedHandler)
public LoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey,
string keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler,
bool updateLastUsageTimestamp, bool makeCurrent, IDatabaseModificationWatcher modificationWatcher = null): base(app, operationFinishedHandler)
{
_app = app;
_modificationWatcher = modificationWatcher ?? new NullDatabaseModificationWatcher();
_app = app;
_ioc = ioc;
_databaseData = databaseData;
_compositeKey = compositeKey;
@@ -59,8 +62,9 @@ namespace keepass2android
protected bool success = false;
private bool _updateLastUsageTimestamp;
private readonly bool _makeCurrent;
private readonly IDatabaseModificationWatcher _modificationWatcher;
public override void Run()
public override void Run()
{
try
{
@@ -109,6 +113,11 @@ namespace keepass2android
}
}
if (!StatusLogger.ContinueWork())
{
return;
}
//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)));
TryLoad(databaseStream, requiresSubsequentSync);
@@ -147,7 +156,13 @@ namespace keepass2android
Finish(false, _app.GetResourceString(UiStringKey.DuplicateUuidsError) + " " + ExceptionUtil.GetErrorMessage(e) + _app.GetResourceString(UiStringKey.DuplicateUuidsErrorAdditional), false, Exception);
return;
}
catch (Exception e)
catch (Java.Lang.InterruptedException)
{
Kp2aLog.Log("Load interrupted");
//close without Finish()
return;
}
catch (Exception e)
{
if (!(e is InvalidCompositeKeyException))
Kp2aLog.LogUnexpectedError(e);
@@ -173,10 +188,16 @@ namespace keepass2android
workingCopy.Seek(0, SeekOrigin.Begin);
//reset stream if we need to reuse it later:
databaseStream.Seek(0, SeekOrigin.Begin);
if (!StatusLogger.ContinueWork())
{
throw new Java.Lang.InterruptedException();
}
//now let's go:
try
{
Database newDb = _app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent);
try
{
Database newDb =
_app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent, _modificationWatcher);
Kp2aLog.Log("LoadDB OK");
if (requiresSubsequentSync)
{
@@ -184,16 +205,17 @@ namespace keepass2android
(success, message, activeActivity) =>
{
if (!String.IsNullOrEmpty(message))
_app.ShowMessage(activeActivity, message, success ? MessageSeverity.Info : MessageSeverity.Error);
})
_app.ShowMessage(activeActivity, message,
success ? MessageSeverity.Info : MessageSeverity.Error);
}), new BackgroundDatabaseModificationLocker(_app)
);
BackgroundOperationRunner.Instance.Run(_app, syncTask);
}
Finish(true, _format.SuccessMessage);
return newDb;
}
return newDb;
}
catch (OldFormatException)
{
_format = new KdbDatabaseFormat(_app);

View File

@@ -131,14 +131,14 @@ namespace keepass2android.database.edit
operationFinishedHandler.Run();
return;
}
SaveDb saveDb = new SaveDb( _app, allDatabasesToSave[indexToSave], new ActionOnOperationFinished(_app, ContinueSave), false);
SaveDb saveDb = new SaveDb( _app, allDatabasesToSave[indexToSave], new ActionOnOperationFinished(_app, ContinueSave), false, null);
saveDb.SetStatusLogger(StatusLogger);
saveDb.ShowDatabaseIocInStatus = allDatabasesToSave.Count > 1;
saveDb.Run();
}
SaveDb save = new SaveDb(_app, allDatabasesToSave[0], new ActionOnOperationFinished(_app, ContinueSave), false);
SaveDb save = new SaveDb(_app, allDatabasesToSave[0], new ActionOnOperationFinished(_app, ContinueSave), false, null);
save.SetStatusLogger(StatusLogger);
save.ShowDatabaseIocInStatus = allDatabasesToSave.Count > 1;
save.Run();

View File

@@ -34,12 +34,17 @@ using Thread = System.Threading.Thread;
namespace keepass2android
{
/// <summary>
/// Save the database. If the file has changed, ask the user if he wants to overwrite or sync.
/// </summary>
public class SaveDb : OperationWithFinishHandler {
public class SaveDb : OperationWithFinishHandler {
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.
private readonly IDatabaseModificationWatcher _modificationWatcher;
private bool requiresSubsequentSync = false; //if true, we need to sync the file after saving.
public bool DoNotSetStatusLoggerMessage = false;
@@ -50,12 +55,13 @@ namespace keepass2android
private Java.Lang.Thread _workerThread;
public SaveDb(IKp2aApp app, Database db, OnOperationFinishedHandler operationFinishedHandler, bool dontSave)
public SaveDb(IKp2aApp app, Database db, OnOperationFinishedHandler operationFinishedHandler, bool dontSave, IDatabaseModificationWatcher modificationWatcher)
: base(app, operationFinishedHandler)
{
_db = db;
_app = app;
_dontSave = dontSave;
_modificationWatcher = modificationWatcher;
}
/// <summary>
@@ -65,10 +71,11 @@ namespace keepass2android
/// <param name="operationFinishedHandler"></param>
/// <param name="dontSave"></param>
/// <param name="streamForOrigFile">Stream for reading the data from the (changed) original location</param>
public SaveDb(IKp2aApp app, OnOperationFinishedHandler operationFinishedHandler, Database db, bool dontSave, Stream streamForOrigFile)
public SaveDb(IKp2aApp app, OnOperationFinishedHandler operationFinishedHandler, Database db, bool dontSave, Stream streamForOrigFile, IDatabaseModificationWatcher modificationWatcher = null)
: base(app, operationFinishedHandler)
{
_db = db;
_modificationWatcher = modificationWatcher ?? new NullDatabaseModificationWatcher();
_db = db;
_app = app;
_dontSave = dontSave;
_streamForOrigFile = streamForOrigFile;
@@ -76,10 +83,12 @@ namespace keepass2android
}
public SaveDb(IKp2aApp app, Database db, OnOperationFinishedHandler operationFinishedHandler)
public SaveDb(IKp2aApp app, Database db, OnOperationFinishedHandler operationFinishedHandler, IDatabaseModificationWatcher modificationWatcher = null)
: base(app, operationFinishedHandler)
{
_app = app;
_modificationWatcher = modificationWatcher ?? new NullDatabaseModificationWatcher();
_app = app;
_db = db;
_dontSave = false;
SyncInBackground = _app.SyncInBackgroundPreference;
@@ -135,11 +144,17 @@ namespace keepass2android
}
}
//TODO remove
Thread.Sleep(5000);
bool hasStreamForOrigFile = (_streamForOrigFile != null);
bool hasChangeFast = hasStreamForOrigFile ||
fileStorage.CheckForFileChangeFast(ioc, _db.LastFileVersion); //first try to use the fast change detection;
bool hasHashChanged = !requiresSubsequentSync && (hasChangeFast ||
bool hasHashChanged = !requiresSubsequentSync && (
//TODO remove
true ||
hasChangeFast ||
(FileHashChanged(ioc, _db.KpDatabase.HashOfFileOnDisk) ==
FileHashChange.Changed)); //if that fails, hash the file and compare:
@@ -226,7 +241,7 @@ namespace keepass2android
if (!System.String.IsNullOrEmpty(message))
_app.ShowMessage(activeActivity, message, success ? MessageSeverity.Info : MessageSeverity.Error);
})
}), new BackgroundDatabaseModificationLocker(_app)
);
BackgroundOperationRunner.Instance.Run(_app, syncTask);
}
@@ -238,12 +253,38 @@ namespace keepass2android
//note: when synced, the file might be downloaded once again from the server. Caching the data
//in the hashing function would solve this but increases complexity. I currently assume the files are
//small.
MergeIn(fileStorage, ioc);
try
{
_modificationWatcher.BeforeModifyDatabases();
}
catch (Java.Lang.InterruptedException)
{
// leave without Finish()
return;
}
try
{
MergeIn(fileStorage, ioc);
}
finally
{
_modificationWatcher.AfterModifyDatabases();
}
PerformSaveWithoutCheck(fileStorage, ioc);
_db.UpdateGlobals();
new Handler(Looper.MainLooper).Post(() =>
{
_db.UpdateGlobals();
});
FinishWithSuccess();
}
private void RunInWorkerThread(Action runHandler)
{
try

View File

@@ -72,7 +72,7 @@ namespace keepass2android
// Save Database
_operationFinishedHandler = new AfterSave(_app, previousKey, previousMasterKeyChanged, pm, operationFinishedHandler);
SaveDb save = new SaveDb(_app, _app.CurrentDb, operationFinishedHandler, _dontSave);
SaveDb save = new SaveDb(_app, _app.CurrentDb, operationFinishedHandler, _dontSave, null);
save.SetStatusLogger(StatusLogger);
save.Run();
}

View File

@@ -123,7 +123,15 @@ namespace keepass2android
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
//we don't want any background thread to update/reload the database while we're in this activity.
if (!App.Kp2a.DatabasesBackgroundModificationLock.TryEnterReadLock(TimeSpan.FromSeconds(5)))
{
App.Kp2a.ShowMessage(this, GetString(Resource.String.failed_to_access_database), MessageSeverity.Error);
Finish();
return;
}
base.OnCreate(savedInstanceState);
if (LastNonConfigurationInstance != null)
{
@@ -329,11 +337,19 @@ namespace keepass2android
}
protected override void OnDestroy()
{
base.OnDestroy();
//we don't want any background thread to update/reload the database while we're in this activity.
App.Kp2a.DatabasesBackgroundModificationLock.ExitReadLock();
}
private bool hasRequestedKeyboardActivation = false;
protected override void OnStart()
{
{
base.OnStart();
if (PreferenceManager.GetDefaultSharedPreferences(this)
.GetBoolean(GetString(Resource.String.UseKp2aKeyboardInKp2a_key), false)

View File

@@ -14,8 +14,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/background_ops_message"
android:layout_width="wrap_content"
@@ -37,6 +47,19 @@
android:textSize="12sp"
android:text="" />
</LinearLayout>
<Button
android:id="@+id/cancel_background"
style="?attr/materialIconButtonStyle"
app:icon="@drawable/baseline_close_24"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dip"
android:layout_margin="6dip"
android:layout_weight="0"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -1256,4 +1256,5 @@
<string name="switch_keyboard_inside_kp2a_enabled">Note: You have enabled App - Security - Use built-in keyboard inside Keepass2Android. This can cause this window to show when you open the app or edit an entry.</string>
<string name="switch_keyboard_on_search_enabled">Note: You have enabled App - Security - Password access - Keyboard switching - Switch keyboard. This can cause this window to show when you search for an entry from the browser.</string>
<string name="user_interaction_required">User interaction required. Please open the app.</string>
<string name="failed_to_access_database">Database is currently in use and cannot be accessed.</string>
</resources>

View File

@@ -78,7 +78,15 @@ namespace keepass2android
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
//we don't want any background thread to update/reload the database while we're in this activity.
if (!App.Kp2a.DatabasesBackgroundModificationLock.TryEnterReadLock(TimeSpan.FromSeconds(5)))
{
App.Kp2a.ShowMessage(this, GetString(Resource.String.failed_to_access_database), MessageSeverity.Error);
Finish();
return;
}
base.OnCreate(savedInstanceState);
//if user presses back to leave this activity:
SetResult(Result.Canceled);
@@ -288,5 +296,12 @@ namespace keepass2android
{
get { return null; }
}
}}
protected override void OnDestroy()
{
base.OnDestroy();
App.Kp2a.DatabasesBackgroundModificationLock.ExitReadLock();
}
}
}

View File

@@ -79,7 +79,7 @@ namespace keepass2android
if (filestorage is CachingFileStorage)
{
task = new SynchronizeCachedDatabase(App.Kp2a, onOperationFinishedHandler);
task = new SynchronizeCachedDatabase(App.Kp2a, onOperationFinishedHandler, new BackgroundDatabaseModificationLocker(App.Kp2a));
}
else
{

View File

@@ -116,15 +116,12 @@ namespace keepass2android
#endif
/// <summary>
/// Main implementation of the IKp2aApp interface for usage in the real app.
/// </summary>
public class Kp2aApp: IKp2aApp, ICacheSupervisor
{
public void Lock(bool allowQuickUnlock = true, bool lockWasTriggeredByTimeout = false)
{
public void Lock(bool allowQuickUnlock = true, bool lockWasTriggeredByTimeout = false)
{
if (OpenDatabases.Any())
{
@@ -194,7 +191,9 @@ namespace keepass2android
public Database LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, IKp2aStatusLogger statusLogger, IDatabaseFormat databaseFormat, bool makeCurrent)
public Database LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream,
CompositeKey compositeKey, IKp2aStatusLogger statusLogger, IDatabaseFormat databaseFormat, bool makeCurrent,
IDatabaseModificationWatcher modificationWatcher)
{
@@ -215,39 +214,50 @@ namespace keepass2android
memoryStream.Seek(0, SeekOrigin.Begin);
}
if (!statusLogger.ContinueWork())
{
throw new Java.Lang.InterruptedException();
}
_openAttempts.Add(ioConnectionInfo);
var newDb = new Database(new DrawableFactory(), this);
newDb.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseFormat);
modificationWatcher.BeforeModifyDatabases();
if ((_currentDatabase == null) || makeCurrent) _currentDatabase = newDb;
bool replacedOpenDatabase = false;
for (int i = 0; i < _openDatabases.Count; i++)
try
{
if (_openDatabases[i].Ioc.IsSameFileAs(ioConnectionInfo))
if ((_currentDatabase == null) || makeCurrent) _currentDatabase = newDb;
bool replacedOpenDatabase = false;
for (int i = 0; i < _openDatabases.Count; i++)
{
if (_currentDatabase == _openDatabases[i])
if (_openDatabases[i].Ioc.IsSameFileAs(ioConnectionInfo))
{
_currentDatabase = newDb;
if (_currentDatabase == _openDatabases[i])
{
_currentDatabase = newDb;
}
replacedOpenDatabase = true;
_openDatabases[i] = newDb;
break;
}
replacedOpenDatabase = true;
_openDatabases[i] = newDb;
break;
}
}
if (!replacedOpenDatabase)
if (!replacedOpenDatabase)
{
_openDatabases.Add(newDb);
}
}
finally
{
modificationWatcher.AfterModifyDatabases();
}
if (createBackup)
@@ -302,9 +312,9 @@ namespace keepass2android
return newDb;
}
public void CloseDatabase(Database db)
public void CloseDatabase(Database db)
{
if (!_openDatabases.Contains(db))
@@ -745,6 +755,11 @@ namespace keepass2android
EventHandler<DialogClickEventArgs> cancelHandler,
EventHandler dismissHandler,string messageSuffix = "")
{
if (Java.Lang.Thread.Interrupted())
{
throw new Java.Lang.InterruptedException();
}
_currentlyPendingYesNoCancelQuestion = new YesNoCancelQuestion()
{
TitleKey = titleKey,
@@ -1472,10 +1487,38 @@ namespace keepass2android
_currentlyPendingYesNoCancelQuestion?.TryShow(this, OnUserInputDialogClose, OnUserInputDialogClose);
}
}
/// <summary>
/// If the database is updated from a background operation, that operation needs to acquire a writer lock on this.
/// </summary>
/// Activities can acquire a reader lock if they want to make sure that no background operation is modifying the database while they are open.
public ReaderWriterLockSlim DatabasesBackgroundModificationLock { get; } = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
public bool CancelBackgroundOperations()
{
if (!DatabasesBackgroundModificationLock.TryEnterReadLock(TimeSpan.FromSeconds(5)))
{
return false;
}
try
{
BackgroundOperationRunner.Instance.CancelAll();
}
finally
{
DatabasesBackgroundModificationLock.ExitReadLock();
}
return true;
}
}
///Application class for Keepass2Android: Contains static Database variable to be used by all components.
///Application class for Keepass2Android: Contains static Database variable to be used by all components.
#if NoNet
[Application(Debuggable=false, Label=AppNames.AppName)]
#else

View File

@@ -48,7 +48,15 @@ namespace keepass2android.search
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
//we don't want any background thread to update/reload the database while we're in this activity. We're showing a temporary group, so background updating doesn't work well.
if (!App.Kp2a.DatabasesBackgroundModificationLock.TryEnterReadLock(TimeSpan.FromSeconds(5)))
{
App.Kp2a.ShowMessage(this, GetString(Resource.String.failed_to_access_database), MessageSeverity.Error);
Finish();
return;
}
base.OnCreate (bundle);
if ( IsFinishing ) {
return;
@@ -59,7 +67,13 @@ namespace keepass2android.search
ProcessIntent(Intent);
}
public override bool EntriesBelongToCurrentDatabaseOnly
protected override void OnDestroy()
{
base.OnDestroy();
App.Kp2a.DatabasesBackgroundModificationLock.ExitReadLock();
}
public override bool EntriesBelongToCurrentDatabaseOnly
{
get { return false; }
}

View File

@@ -0,0 +1,97 @@
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
namespace keepass2android.views;
public class BackgroundOperationContainer : LinearLayout, IProgressUi
{
protected BackgroundOperationContainer(IntPtr javaReference, JniHandleOwnership transfer) : base(
javaReference, transfer)
{
}
public BackgroundOperationContainer(Context context) : base(context)
{
}
public BackgroundOperationContainer(Context context, IAttributeSet attrs) : base(context, attrs)
{
Initialize(attrs);
}
public BackgroundOperationContainer(Context context, IAttributeSet attrs, int defStyle) : base(context,
attrs, defStyle)
{
Initialize(attrs);
}
private void Initialize(IAttributeSet attrs)
{
LayoutInflater inflater = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
inflater.Inflate(Resource.Layout.background_operation_container, this);
FindViewById(Resource.Id.cancel_background).Click += (obj,args) =>
{
App.Kp2a.CancelBackgroundOperations();
};
}
public void Show()
{
new Handler(Looper.MainLooper).Post(() =>
{
Visibility = ViewStates.Visible;
FindViewById<TextView>(Resource.Id.background_ops_message)!.Visibility = ViewStates.Gone;
FindViewById<TextView>(Resource.Id.background_ops_submessage)!.Visibility = ViewStates.Gone;
});
}
public void Hide()
{
new Handler(Looper.MainLooper).Post(() =>
{
String activityType = Context.GetType().FullName;
Kp2aLog.Log("Hiding background ops container in" + activityType);
Visibility = ViewStates.Gone;
});
}
public void UpdateMessage(string message)
{
new Handler(Looper.MainLooper).Post(() =>
{
TextView messageTextView = FindViewById<TextView>(Resource.Id.background_ops_message)!;
if (string.IsNullOrEmpty(message))
{
messageTextView.Visibility = ViewStates.Gone;
}
else
{
messageTextView.Visibility = ViewStates.Visible;
messageTextView.Text = message;
}
});
}
public void UpdateSubMessage(string submessage)
{
new Handler(Looper.MainLooper).Post(() =>
{
TextView subMessageTextView = FindViewById<TextView>(Resource.Id.background_ops_submessage)!;
if (string.IsNullOrEmpty(submessage))
{
subMessageTextView.Visibility = ViewStates.Gone;
}
else
{
subMessageTextView.Visibility = ViewStates.Visible;
subMessageTextView.Text = submessage;
}
});
}
}

View File

@@ -7,7 +7,6 @@ using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Text.Method;
@@ -104,92 +103,4 @@ namespace keepass2android.views
}
}
public class BackgroundOperationContainer : LinearLayout, IProgressUi
{
protected BackgroundOperationContainer(IntPtr javaReference, JniHandleOwnership transfer) : base(
javaReference, transfer)
{
}
public BackgroundOperationContainer(Context context) : base(context)
{
}
public BackgroundOperationContainer(Context context, IAttributeSet attrs) : base(context, attrs)
{
Initialize(attrs);
}
public BackgroundOperationContainer(Context context, IAttributeSet attrs, int defStyle) : base(context,
attrs, defStyle)
{
Initialize(attrs);
}
private void Initialize(IAttributeSet attrs)
{
LayoutInflater inflater = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
inflater.Inflate(Resource.Layout.background_operation_container, this);
}
public void Show()
{
new Handler(Looper.MainLooper).Post(() =>
{
Visibility = ViewStates.Visible;
FindViewById<TextView>(Resource.Id.background_ops_message)!.Visibility = ViewStates.Gone;
FindViewById<TextView>(Resource.Id.background_ops_submessage)!.Visibility = ViewStates.Gone;
});
}
public void Hide()
{
new Handler(Looper.MainLooper).Post(() =>
{
String activityType = Context.GetType().FullName;
Kp2aLog.Log("Hiding background ops container in" + activityType);
Visibility = ViewStates.Gone;
});
}
public void UpdateMessage(string message)
{
new Handler(Looper.MainLooper).Post(() =>
{
TextView messageTextView = FindViewById<TextView>(Resource.Id.background_ops_message)!;
if (string.IsNullOrEmpty(message))
{
messageTextView.Visibility = ViewStates.Gone;
}
else
{
messageTextView.Visibility = ViewStates.Visible;
messageTextView.Text = message;
}
});
}
public void UpdateSubMessage(string submessage)
{
new Handler(Looper.MainLooper).Post(() =>
{
TextView subMessageTextView = FindViewById<TextView>(Resource.Id.background_ops_submessage)!;
if (string.IsNullOrEmpty(submessage))
{
subMessageTextView.Visibility = ViewStates.Gone;
}
else
{
subMessageTextView.Visibility = ViewStates.Visible;
subMessageTextView.Text = submessage;
}
});
}
}
}