diff --git a/src/Kp2aBusinessLogic/BlockingOperationRunner.cs b/src/Kp2aBusinessLogic/BlockingOperationRunner.cs index 50896824..21024fe1 100644 --- a/src/Kp2aBusinessLogic/BlockingOperationRunner.cs +++ b/src/Kp2aBusinessLogic/BlockingOperationRunner.cs @@ -19,13 +19,199 @@ using Android.App; using Android.Content; using Android.OS; using Java.Lang; +using Java.Security; +using KeePassLib.Interfaces; +using System.Threading.Tasks; +using Enum = System.Enum; namespace keepass2android { - /// - /// Class to run a task while a progress dialog is shown - /// - public class BlockingOperationRunner + public class BackgroundOperationRunner + { + //singleton instance + private static BackgroundOperationRunner _instance = null; + + public static BackgroundOperationRunner Instance + { + get + { + if (_instance == null) + { + _instance = new BackgroundOperationRunner(); + } + + return _instance; + } + } + + private BackgroundOperationRunner() + { + //private constructor + } + + private readonly Queue _taskQueue = new Queue(); + private readonly object _taskQueueLock = new object(); + private Java.Lang.Thread? _thread = null; + private ProgressUiAsStatusLoggerAdapter _statusLogger = null; + + public void Run(Activity activity, IKp2aApp app, OperationWithFinishHandler operation) + { + lock (Instance._taskQueueLock) + { + _taskQueue.Enqueue(operation); + SetNewActiveActivity(activity, app); + + // Start thread to run the task (unless it's already running) + if (_thread == null) + { + _statusLogger.StartLogging("", false); + _thread = new Java.Lang.Thread(() => + { + while (true) + { + OperationWithFinishHandler task; + lock (_taskQueueLock) + { + if (!_taskQueue.Any()) + { + _thread = null; + _statusLogger.EndLogging(); + break; + } + else + { + task = _taskQueue.Dequeue(); + } + } + task.Run(); + } + + }); + _thread.Start(); + } + + } + + } + + public void SetNewActiveActivity(Activity? activity, IKp2aApp app) + { + lock (_taskQueueLock) + { + var progressUi = (activity as IProgressUiProvider)?.ProgressUi; + if (_statusLogger == null) + { + _statusLogger = new ProgressUiAsStatusLoggerAdapter(progressUi, app); + } + else + { + _statusLogger.SetNewProgressUi(progressUi); + } + + foreach (var task in _taskQueue) + { + task.ActiveActivity = activity; + task.SetStatusLogger(_statusLogger); + } + + } + + + } + } + + public class ProgressUiAsStatusLoggerAdapter : IKp2aStatusLogger + { + private IProgressUi? _progressUi; + private readonly IKp2aApp _app; + + private string _lastMessage = ""; + private string _lastSubMessage = ""; + private bool _isVisible = false; + + public ProgressUiAsStatusLoggerAdapter(IProgressUi progressUi, IKp2aApp app) + { + _progressUi = progressUi; + _app = app; + } + + public void SetNewProgressUi(IProgressUi progressUi) + { + _progressUi = progressUi; + if (_isVisible) + { + progressUi?.Show(); + progressUi?.UpdateMessage(_lastMessage); + progressUi?.UpdateSubMessage(_lastSubMessage); + } + else + { + progressUi?.Hide(); + } + } + + public void StartLogging(string strOperation, bool bWriteOperationToLog) + { + _progressUi?.Show(); + _isVisible = true; + } + + public void EndLogging() + { + _progressUi?.Hide(); + _isVisible = false; + } + + public bool SetProgress(uint uPercent) + { + return true; + } + + public bool SetText(string strNewText, LogStatusType lsType) + { + if (strNewText.StartsWith("KP2AKEY_")) + { + UiStringKey key; + if (Enum.TryParse(strNewText.Substring("KP2AKEY_".Length), true, out key)) + { + UpdateMessage(_app.GetResourceString(key)); + return true; + } + } + UpdateMessage(strNewText); + + return true; + } + + public void UpdateMessage(string message) + { + _progressUi?.UpdateMessage(message); + _lastMessage = message; + } + + public void UpdateSubMessage(string submessage) + { + _progressUi?.UpdateSubMessage(submessage); + _lastSubMessage = submessage; + } + + public bool ContinueWork() + { + return true; + } + + public void UpdateMessage(UiStringKey stringKey) + { + if (_app != null) + UpdateMessage(_app.GetResourceString(stringKey)); + } + } + + + /// + /// Class to run a task while a progress dialog is shown + /// + public class BlockingOperationRunner { //for handling Activity recreation situations, we need access to the currently active task. It must hold that there is no more than one active task. private static BlockingOperationRunner _currentTask = null; diff --git a/src/Kp2aBusinessLogic/ProgressDialogStatusLogger.cs b/src/Kp2aBusinessLogic/ProgressDialogStatusLogger.cs index d67b9f51..280a1795 100644 --- a/src/Kp2aBusinessLogic/ProgressDialogStatusLogger.cs +++ b/src/Kp2aBusinessLogic/ProgressDialogStatusLogger.cs @@ -27,6 +27,20 @@ namespace keepass2android void UpdateMessage(UiStringKey stringKey); } + public interface IProgressUi + { + void Show(); + void Hide(); + void UpdateMessage(String message); + void UpdateSubMessage(String submessage); + } + + public interface IProgressUiProvider + { + IProgressUi? ProgressUi { get; } + } + + public class Kp2aNullStatusLogger : IKp2aStatusLogger { public void StartLogging(string strOperation, bool bWriteOperationToLog) diff --git a/src/keepass2android-app/EntryActivity.cs b/src/keepass2android-app/EntryActivity.cs index b3015804..3dfe892b 100644 --- a/src/keepass2android-app/EntryActivity.cs +++ b/src/keepass2android-app/EntryActivity.cs @@ -56,6 +56,7 @@ using Android.Util; using AndroidX.Core.Content; using Google.Android.Material.Dialog; using keepass2android; +using keepass2android.views; namespace keepass2android { @@ -97,7 +98,7 @@ namespace keepass2android [Activity (Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden, Theme = "@style/Kp2aTheme_ActionBar")] - public class EntryActivity : LockCloseActivity + public class EntryActivity : LockCloseActivity, IProgressUiProvider { public const String KeyEntry = "entry"; public const String KeyRefreshPos = "refresh_pos"; @@ -1603,5 +1604,7 @@ namespace keepass2android imageViewerIntent.PutExtra("EntryKey", key); StartActivity(imageViewerIntent); } - } + + public IProgressUi? ProgressUi => FindViewById(Resource.Id.background_ops_container); + } } \ No newline at end of file diff --git a/src/keepass2android-app/GroupBaseActivity.cs b/src/keepass2android-app/GroupBaseActivity.cs index 43a8396e..09614b20 100644 --- a/src/keepass2android-app/GroupBaseActivity.cs +++ b/src/keepass2android-app/GroupBaseActivity.cs @@ -43,11 +43,12 @@ using keepass2android; using KeeTrayTOTP.Libraries; using AndroidX.AppCompat.Widget; using Google.Android.Material.Dialog; +using keepass2android.views; using SearchView = AndroidX.AppCompat.Widget.SearchView; namespace keepass2android { - public abstract class GroupBaseActivity : LockCloseActivity + public abstract class GroupBaseActivity : LockCloseActivity, IProgressUiProvider { public const String KeyEntry = "entry"; public const String KeyMode = "mode"; @@ -1408,6 +1409,14 @@ namespace keepass2android { GroupEditActivity.Launch(this, pwGroup.ParentGroup, pwGroup); } + + public IProgressUi ProgressUi + { + get + { + return FindViewById(Resource.Id.background_ops_container); + } + } } public class GroupListFragment : ListFragment, AbsListView.IMultiChoiceModeListener diff --git a/src/keepass2android-app/LifecycleAwareActivity.cs b/src/keepass2android-app/LifecycleAwareActivity.cs index b6578e53..a91b3a49 100644 --- a/src/keepass2android-app/LifecycleAwareActivity.cs +++ b/src/keepass2android-app/LifecycleAwareActivity.cs @@ -105,6 +105,7 @@ namespace keepass2android protected override void OnStart() { BlockingOperationRunner.SetNewActiveActivity(this); + BackgroundOperationRunner.Instance.SetNewActiveActivity(this, App.Kp2a); base.OnStart(); Kp2aLog.Log(ClassName + ".OnStart" + " " + ID); } diff --git a/src/keepass2android-app/Resources/layout/background_operation_container.xml b/src/keepass2android-app/Resources/layout/background_operation_container.xml new file mode 100644 index 00000000..7d8082fc --- /dev/null +++ b/src/keepass2android-app/Resources/layout/background_operation_container.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/keepass2android-app/Resources/layout/entry_view.xml b/src/keepass2android-app/Resources/layout/entry_view.xml index 4b5d4477..2fef6269 100644 --- a/src/keepass2android-app/Resources/layout/entry_view.xml +++ b/src/keepass2android-app/Resources/layout/entry_view.xml @@ -4,18 +4,35 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> + + + + + + + + + - + - - - - - - + /> { if (!String.IsNullOrEmpty(message)) - App.Kp2a.ShowMessage(activity, message, MessageSeverity.Error); + App.Kp2a.ShowMessage(activity, message, success ? MessageSeverity.Info : MessageSeverity.Error); // Tell the adapter to refresh it's list - BaseAdapter adapter = (activity as GroupBaseActivity)?.ListAdapter; - adapter?.NotifyDataSetChanged(); + BaseAdapter adapter = (activity as GroupBaseActivity)?.ListAdapter; + new Handler(Looper.MainLooper).Post(() => adapter?.NotifyDataSetChanged()); if (App.Kp2a.CurrentDb?.OtpAuxFileIoc != null) { var task2 = new SyncOtpAuxFile(_activity, App.Kp2a.CurrentDb.OtpAuxFileIoc); - - //TODO new BackgroundOperationRunner(App.Kp2a, activity, task2).Run(true); + + BackgroundOperationRunner.Instance.Run(_activity, App.Kp2a, task2); } }); @@ -82,8 +83,7 @@ namespace keepass2android task = new CheckDatabaseForChanges(_activity, App.Kp2a, onOperationFinishedHandler); } - //TODO var backgroundTaskRunner = new BackgroundOperationRunner(App.Kp2a, _activity, task); - //TODO backgroundTaskRunner.Run(); + BackgroundOperationRunner.Instance.Run(_activity, App.Kp2a, task); } } diff --git a/src/keepass2android-app/views/Kp2aShortHelpView.cs b/src/keepass2android-app/views/Kp2aShortHelpView.cs index 438e7e95..c2961a5a 100644 --- a/src/keepass2android-app/views/Kp2aShortHelpView.cs +++ b/src/keepass2android-app/views/Kp2aShortHelpView.cs @@ -18,6 +18,7 @@ using Android.Views; using Android.Widget; using Google.Android.Material.Dialog; using keepass2android; +using KeePassLib.Interfaces; namespace keepass2android.views { @@ -103,4 +104,92 @@ 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(Resource.Id.background_ops_message)!.Visibility = ViewStates.Gone; + FindViewById(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(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(Resource.Id.background_ops_submessage)!; + if (string.IsNullOrEmpty(submessage)) + { + subMessageTextView.Visibility = ViewStates.Gone; + } + else + { + subMessageTextView.Visibility = ViewStates.Visible; + subMessageTextView.Text = submessage; + } + }); + } + } } \ No newline at end of file