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

@@ -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;
}
});
}
}
}