implement UI updates after background sync for Group activity and Entry activity

This commit is contained in:
Philipp Crocoll
2025-05-13 21:34:06 +02:00
parent 41e6e67e87
commit 400e171bc5
9 changed files with 273 additions and 91 deletions

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using KeePassLib.Serialization;
using keepass2android.Io;
using KeePass.Util;
@@ -12,9 +13,8 @@ namespace keepass2android
{
public class SynchronizeCachedDatabase: OperationWithFinishHandler
{
private readonly Context _context;
private readonly IKp2aApp _app;
private SaveDb _saveDb;
public SynchronizeCachedDatabase(IKp2aApp app, OnOperationFinishedHandler operationFinishedHandler)
: base(app, operationFinishedHandler)
@@ -70,7 +70,7 @@ namespace keepass2android
)
{
//conflict! need to merge
_saveDb = new SaveDb(_app, new ActionOnOperationFinished(_app, (success, result, activity) =>
var _saveDb = new SaveDb(_app, new ActionOnOperationFinished(_app, (success, result, activity) =>
{
if (!success)
{
@@ -80,7 +80,6 @@ namespace keepass2android
{
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
_saveDb = null;
}), _app.CurrentDb, false, remoteData);
_saveDb.SetStatusLogger(StatusLogger);
_saveDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message
@@ -93,10 +92,32 @@ namespace keepass2android
}
else
{
//only the remote file was modified -> reload database.
//note: it's best to lock the database and do a complete reload here (also better for UI consistency in case something goes wrong etc.)
_app.TriggerReload(_context, (bool result) => Finish(result));
}
//only the remote file was modified -> reload database.
var onFinished = new ActionOnOperationFinished(_app, (success, result, activity) =>
{
if (!success)
{
Finish(false, result);
}
else
{
new Handler(Looper.MainLooper).Post(() =>
{
_app.CurrentDb.UpdateGlobals();
_app.MarkAllGroupsAsDirty();
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
});
}
});
var _loadDb = new LoadDb(_app, ioc, Task.FromResult(remoteData), _app.CurrentDb.KpDatabase.MasterKey, null, onFinished, true, false);
_loadDb.SetStatusLogger(StatusLogger);
_loadDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message
_loadDb.SyncInBackground = false;
_loadDb.Run();
}
}
else
{
@@ -124,10 +145,5 @@ namespace keepass2android
}
public void JoinWorkerThread()
{
if (_saveDb != null)
_saveDb.JoinWorkerThread();
}
}
}

View File

@@ -35,12 +35,15 @@ namespace keepass2android
private readonly IOConnectionInfo _ioc;
private readonly Task<MemoryStream> _databaseData;
private readonly CompositeKey _compositeKey;
private readonly string _keyfileOrProvider;
private readonly string? _keyfileOrProvider;
private readonly IKp2aApp _app;
private readonly bool _rememberKeyfile;
IDatabaseFormat _format;
public LoadDb(Activity activity, IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, String keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler, bool updateLastUsageTimestamp, bool makeCurrent): base(app, operationFinishedHandler)
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)
{
_app = app;
_ioc = ioc;
@@ -49,8 +52,9 @@ namespace keepass2android
_keyfileOrProvider = keyfileOrProvider;
_updateLastUsageTimestamp = updateLastUsageTimestamp;
_makeCurrent = makeCurrent;
_rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile);
}
_rememberKeyfile = app.GetBooleanPreference(PreferenceKey.remember_keyfile);
SyncInBackground = _app.SyncInBackgroundPreference;
}
protected bool success = false;
private bool _updateLastUsageTimestamp;
@@ -62,15 +66,22 @@ namespace keepass2android
{
try
{
//make sure the file data is stored in the recent files list even if loading fails
SaveFileData(_ioc, _keyfileOrProvider);
if (_keyfileOrProvider != null)
{
//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);
if (!DoNotSetStatusLoggerMessage)
{
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;
@@ -167,7 +178,6 @@ namespace keepass2android
{
Database newDb = _app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent);
Kp2aLog.Log("LoadDB OK");
if (requiresSubsequentSync)
{
var syncTask = new SynchronizeCachedDatabase(_app, new ActionOnOperationFinished(_app,

View File

@@ -111,6 +111,45 @@ namespace keepass2android
protected override View? SnackbarAnchorView => FindViewById(Resource.Id.main_content);
public class UpdateEntryActivityBroadcastReceiver : BroadcastReceiver
{
private readonly EntryActivity _activity;
public UpdateEntryActivityBroadcastReceiver(EntryActivity activity)
{
_activity = activity;
}
public override void OnReceive(Context? context, Intent? intent)
{
if (intent?.Action == Intents.DataUpdated)
{
_activity.OnDataUpdated();
}
}
}
private void OnDataUpdated()
{
if (Entry == null)
{
return;
}
var entryUId = Entry.Uuid;
if (!App.Kp2a.CurrentDb.EntriesById.ContainsKey(entryUId))
{
Finish();
return;
}
var newEntry = App.Kp2a.CurrentDb.EntriesById[entryUId];
if (!newEntry.EqualsEntry(Entry, PwCompareOptions.None, MemProtCmpMode.Full))
{
Recreate();
}
}
public static void Launch(Activity act, PwEntry pw, int pos, AppTask appTask, ActivityFlags? flags = null, int historyIndex=-1)
{
Intent i = new Intent(act, typeof(EntryActivity));
@@ -502,7 +541,13 @@ namespace keepass2android
//the rest of the things to do depends on the current app task:
AppTask.CompleteOnCreateEntryActivity(this, notifyPluginsOnOpenThread);
}
_dataUpdatedIntentReceiver = new UpdateEntryActivityBroadcastReceiver(this);
IntentFilter filter = new IntentFilter();
filter.AddAction(Intents.DataUpdated);
ContextCompat.RegisterReceiver(this, _dataUpdatedIntentReceiver, filter, (int)ReceiverFlags.Exported);
}
private void RemoveFromHistory()
{
@@ -1083,7 +1128,9 @@ namespace keepass2android
UnregisterReceiver(_pluginActionReceiver);
if (_pluginFieldReceiver != null)
UnregisterReceiver(_pluginFieldReceiver);
base.OnDestroy();
if (_dataUpdatedIntentReceiver != null)
UnregisterReceiver(_dataUpdatedIntentReceiver);
base.OnDestroy();
}
private void NotifyPluginsOnClose()
@@ -1359,6 +1406,7 @@ namespace keepass2android
}
bool isPaused = false;
private UpdateEntryActivityBroadcastReceiver _dataUpdatedIntentReceiver;
protected override void OnPause()
{

View File

@@ -533,10 +533,9 @@ namespace keepass2android
}
});
//make sure we can close the EntryEditActivity activity even if the app went to background till we get to the OnOperationFinishedHandler Action
closeOrShowError.AllowInactiveActivity = true;
ActionOnOperationFinished afterAddEntry = new ActionOnOperationFinished(App.Kp2a, (success, message, activity) =>
ActionOnOperationFinished afterAddEntry = new ActionOnOperationFinished(App.Kp2a, (success, message, activity) =>
{
if (success && activity is EntryEditActivity entryEditActivity)
AppTask.AfterAddNewEntry(entryEditActivity, newEntry);

View File

@@ -45,6 +45,7 @@ using AndroidX.AppCompat.Widget;
using Google.Android.Material.Dialog;
using keepass2android.views;
using SearchView = AndroidX.AppCompat.Widget.SearchView;
using AndroidX.Core.Content;
namespace keepass2android
{
@@ -275,6 +276,7 @@ namespace keepass2android
private IMenuItem searchItem;
private IMenuItem searchItemDummy;
private bool isPaused;
private UpdateGroupBaseActivityBroadcastReceiver _dataUpdatedIntentReceiver;
protected override void OnResume()
{
@@ -746,9 +748,10 @@ namespace keepass2android
_dataUpdatedIntentReceiver = new UpdateGroupBaseActivityBroadcastReceiver(this);
IntentFilter filter = new IntentFilter();
filter.AddAction(Intents.DataUpdated);
ContextCompat.RegisterReceiver(this, _dataUpdatedIntentReceiver, filter, (int)ReceiverFlags.Exported);
SetResult(KeePass.ExitNormal);
@@ -1034,6 +1037,13 @@ namespace keepass2android
}
}
protected override void OnDestroy()
{
UnregisterReceiver(_dataUpdatedIntentReceiver);
base.OnDestroy();
}
public override bool OnCreateOptionsMenu(IMenu menu)
{
@@ -1417,6 +1427,50 @@ namespace keepass2android
return FindViewById<BackgroundOperationContainer>(Resource.Id.background_ops_container);
}
}
public void OnDataUpdated()
{
if (Group == null || FragmentManager.IsDestroyed)
{
return;
}
var groupId = Group.Uuid;
if (!App.Kp2a.CurrentDb.GroupsById.ContainsKey(groupId))
{
Finish();
return;
}
Group = App.Kp2a.CurrentDb.GroupsById[groupId];
var fragment = FragmentManager.FindFragmentById<GroupListFragment>(Resource.Id.list_fragment);
if (fragment == null)
{
throw new Exception("did not find fragment");
}
fragment.ListAdapter = new PwGroupListAdapter(this, Group);
SetGroupIcon();
SetGroupTitle();
ListAdapter?.NotifyDataSetChanged();
}
}
public class UpdateGroupBaseActivityBroadcastReceiver : BroadcastReceiver
{
private readonly GroupBaseActivity _groupBaseActivity;
public UpdateGroupBaseActivityBroadcastReceiver(GroupBaseActivity groupBaseActivity)
{
_groupBaseActivity = groupBaseActivity;
}
public override void OnReceive(Context? context, Intent? intent)
{
if (intent?.Action == Intents.DataUpdated)
{
_groupBaseActivity.OnDataUpdated();
}
}
}
public class GroupListFragment : ListFragment, AbsListView.IMultiChoiceModeListener

View File

@@ -38,7 +38,7 @@ namespace keepass2android
protected const string NoLockCheck = "NO_LOCK_CHECK";
protected IOConnectionInfo _ioc;
private BroadcastReceiver _intentReceiver;
private BroadcastReceiver _lockCloseIntentReceiver;
private ActivityDesign _design;
public LockCloseActivity()
@@ -66,11 +66,11 @@ namespace keepass2android
if (Intent.GetBooleanExtra(NoLockCheck, false))
return;
_intentReceiver = new LockCloseActivityBroadcastReceiver(this);
_lockCloseIntentReceiver = new LockCloseActivityBroadcastReceiver(this);
IntentFilter filter = new IntentFilter();
filter.AddAction(Intents.DatabaseLocked);
filter.AddAction(Intent.ActionScreenOff);
ContextCompat.RegisterReceiver(this, _intentReceiver, filter, (int)ReceiverFlags.Exported);
ContextCompat.RegisterReceiver(this, _lockCloseIntentReceiver, filter, (int)ReceiverFlags.Exported);
}
protected override void OnDestroy()
@@ -79,7 +79,7 @@ namespace keepass2android
{
try
{
UnregisterReceiver(_intentReceiver);
UnregisterReceiver(_lockCloseIntentReceiver);
}
catch (Exception ex)
{

View File

@@ -1444,7 +1444,7 @@ namespace keepass2android
LoadDb task = (KeyProviderTypes.Contains(KeyProviders.Otp))
? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, GetKeyProviderString(),
onOperationFinishedHandler, this, true, _makeCurrent)
: new LoadDb(this, 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
new BlockingOperationRunner(App.Kp2a, task).Run();
@@ -1886,7 +1886,7 @@ namespace keepass2android
Handler handler = new Handler();
OnOperationFinishedHandler onOperationFinishedHandler = new AfterLoad(handler, this, _ioConnection);
_performingLoad = true;
LoadDb task = new LoadDb(this, App.Kp2a, _ioConnection, _loadDbFileTask, compositeKeyForImmediateLoad, GetKeyProviderString(),
LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKeyForImmediateLoad, GetKeyProviderString(),
onOperationFinishedHandler, false, _makeCurrent);
_loadDbFileTask = null; // prevent accidental re-use
new BlockingOperationRunner(App.Kp2a, task).Run();
@@ -2276,7 +2276,7 @@ namespace keepass2android
private readonly PasswordActivity _act;
public SaveOtpAuxFileAndLoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, string keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler, PasswordActivity act, bool updateLastUsageTimestamp, bool makeCurrent) : base(act, app, ioc, databaseData, compositeKey, keyfileOrProvider, operationFinishedHandler,updateLastUsageTimestamp,makeCurrent)
public SaveOtpAuxFileAndLoadDb(IKp2aApp app, IOConnectionInfo ioc, Task<MemoryStream> databaseData, CompositeKey compositeKey, string keyfileOrProvider, OnOperationFinishedHandler operationFinishedHandler, PasswordActivity act, bool updateLastUsageTimestamp, bool makeCurrent) : base(app, ioc, databaseData, compositeKey, keyfileOrProvider, operationFinishedHandler,updateLastUsageTimestamp,makeCurrent)
{
_act = act;
}

View File

@@ -151,11 +151,17 @@ namespace keepass2android
BroadcastDatabaseAction(LocaleManager.LocalizedAppContext, Strings.ActionCloseDatabase);
// Couldn't quick-lock, so unload database(s) instead
_openAttempts.Clear();
_openDatabases.Clear();
_currentDatabase = null;
LastOpenedEntry = null;
QuickLocked = false;
lock (_openDatabasesLock)
{
_openAttempts.Clear();
_openDatabases.Clear();
_currentDatabase = null;
LastOpenedEntry = null;
QuickLocked = false;
}
}
}
else
@@ -193,39 +199,59 @@ namespace keepass2android
public Database LoadDatabase(IOConnectionInfo ioConnectionInfo, MemoryStream memoryStream, CompositeKey compositeKey, IKp2aStatusLogger statusLogger, IDatabaseFormat databaseFormat, bool makeCurrent)
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext);
var createBackup = prefs.GetBoolean(LocaleManager.LocalizedAppContext.GetString(Resource.String.CreateBackups_key), true)
var prefs = PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext);
var createBackup =
prefs.GetBoolean(LocaleManager.LocalizedAppContext.GetString(Resource.String.CreateBackups_key),
true)
&& !(new LocalFileStorage(this).IsLocalBackup(ioConnectionInfo));
MemoryStream backupCopy = new MemoryStream();
if (createBackup)
{
MemoryStream backupCopy = new MemoryStream();
if (createBackup)
{
memoryStream.CopyTo(backupCopy);
backupCopy.Seek(0, SeekOrigin.Begin);
//reset stream if we need to reuse it later:
memoryStream.Seek(0, SeekOrigin.Begin);
}
memoryStream.CopyTo(backupCopy);
backupCopy.Seek(0, SeekOrigin.Begin);
//reset stream if we need to reuse it later:
memoryStream.Seek(0, SeekOrigin.Begin);
}
foreach (Database openDb in _openDatabases)
{
if (openDb.Ioc.IsSameFileAs(ioConnectionInfo))
{
//TODO check this earlier and simply open the database's root group
throw new Exception("Database already loaded!");
}
}
_openAttempts.Add(ioConnectionInfo);
var newDb = new Database(new DrawableFactory(), this);
_openAttempts.Add(ioConnectionInfo);
var newDb = new Database(new DrawableFactory(), this);
newDb.LoadData(this, ioConnectionInfo, memoryStream, compositeKey, statusLogger, databaseFormat);
if ((_currentDatabase == null) || makeCurrent)
_currentDatabase = newDb;
_openDatabases.Add(newDb);
lock (_openDatabasesLock)
{
if ((_currentDatabase == null) || makeCurrent) _currentDatabase = newDb;
bool replacedOpenDatabase = false;
for (int i = 0; i < _openDatabases.Count; i++)
{
if (_openDatabases[i].Ioc.IsSameFileAs(ioConnectionInfo))
{
if (_currentDatabase == _openDatabases[i])
{
_currentDatabase = newDb;
}
replacedOpenDatabase = true;
_openDatabases[i] = newDb;
break;
}
}
if (!replacedOpenDatabase)
{
_openDatabases.Add(newDb);
}
}
@@ -285,21 +311,27 @@ namespace keepass2android
public void CloseDatabase(Database db)
{
if (!_openDatabases.Contains(db))
throw new Exception("Cannot close database which is not open!");
if (_openDatabases.Count == 1)
{
Lock(false);
return;
}
if (LastOpenedEntry != null && db.EntriesById.ContainsKey(LastOpenedEntry.Uuid))
{
LastOpenedEntry = null;
}
_openDatabases.Remove(db);
if (_currentDatabase == db)
_currentDatabase = _openDatabases.First();
lock (_openDatabasesLock)
{
//TODO check that Lock() below works without a deadlock
if (!_openDatabases.Contains(db))
throw new Exception("Cannot close database which is not open!");
if (_openDatabases.Count == 1)
{
Lock(false);
return;
}
if (LastOpenedEntry != null && db.EntriesById.ContainsKey(LastOpenedEntry.Uuid))
{
LastOpenedEntry = null;
}
_openDatabases.Remove(db);
if (_currentDatabase == db)
_currentDatabase = _openDatabases.First();
}
UpdateOngoingNotification();
//TODO broadcast event so affected activities can close/update?
}
@@ -376,12 +408,20 @@ namespace keepass2android
private readonly List<IOConnectionInfo> _openAttempts = new List<IOConnectionInfo>(); //stores which files have been attempted to open. Used to avoid that we repeatedly try to load files which failed to load.
private readonly List<Database> _openDatabases = new List<Database>();
private readonly object _openDatabasesLock = new object();
private readonly List<IOConnectionInfo> _childDatabases = new List<IOConnectionInfo>(); //list of databases which were opened as child databases
private Database _currentDatabase;
public IEnumerable<Database> OpenDatabases
{
get { return _openDatabases; }
get
{
lock (_openDatabasesLock)
{
//avoid concurrent access to _openDatabases
return new List<Database>(_openDatabases);
}
}
}
internal ChallengeXCKey _currentlyWaitingXcKey;
@@ -415,8 +455,9 @@ namespace keepass2android
DirtyGroups.Add(group);
}
}
var intent = new Intent(Intents.DataUpdated);
App.Context.SendBroadcast(intent);
}
/// <summary>
@@ -1345,13 +1386,22 @@ namespace keepass2android
public Database TryFindDatabaseForElement(IStructureItem element)
{
foreach (var db in OpenDatabases)
try
{
//we compare UUIDs and not by reference. this is more robust and works with history items as well
if (db.Elements.Any(e => e.Uuid?.Equals(element.Uuid) == true))
return db;
foreach (var db in OpenDatabases)
{
//we compare UUIDs and not by reference. this is more robust and works with history items as well
if (db.Elements.Any(e => e.Uuid?.Equals(element.Uuid) == true))
{
return db;
}
}
}
return null;
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
}
return null;
}
public void RegisterChildDatabase(IOConnectionInfo ioc)

View File

@@ -37,8 +37,13 @@ namespace keepass2android
/// <summary>This intent will be broadcast once the database has been locked. Sensitive information displayed should be hidden and unloaded.</summary>
public const String DatabaseLocked = "keepass2android." + AppNames.PackagePart + ".database_locked";
/// <summary>This intent will be broadcast once the keyboard data has been cleared</summary>
public const String KeyboardCleared = "keepass2android." + AppNames.PackagePart + ".keyboard_cleared";
/// <summary>
/// Signals that the loaded data was updated, e.g. by reloading during sync. All UI elements should be refreshed.
/// </summary>
public const String DataUpdated = "keepass2android." + AppNames.PackagePart + ".data_updated";
/// <summary>This intent will be broadcast once the keyboard data has been cleared</summary>
public const String KeyboardCleared = "keepass2android." + AppNames.PackagePart + ".keyboard_cleared";
public const String CopyUsername = "keepass2android.copy_username";
public const String CopyPassword = "keepass2android.copy_password";