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:
		| @@ -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; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
| 		     | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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 ?? | ||||
|   | ||||
| @@ -79,7 +79,7 @@ public class ProgressUiAsStatusLoggerAdapter : IKp2aStatusLogger | ||||
|  | ||||
|     public bool ContinueWork() | ||||
|     { | ||||
|         return true; | ||||
|         return !Java.Lang.Thread.Interrupted(); | ||||
|     } | ||||
|  | ||||
|     public void UpdateMessage(UiStringKey stringKey) | ||||
|   | ||||
| @@ -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)); | ||||
|   | ||||
| @@ -8,18 +8,21 @@ 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; | ||||
|             _modificationWatcher = modificationWatcher; | ||||
|         } | ||||
|  | ||||
| 		public override void Run() | ||||
| @@ -32,6 +35,7 @@ namespace keepass2android | ||||
|                 { | ||||
|                     throw new Exception("Cannot sync a non-cached database!"); | ||||
|                 } | ||||
|  | ||||
|                 StatusLogger.UpdateMessage(UiStringKey.SynchronizingCachedDatabase); | ||||
|                 CachingFileStorage cachingFileStorage = (CachingFileStorage)fileStorage; | ||||
|  | ||||
| @@ -51,7 +55,8 @@ namespace keepass2android | ||||
|                 catch (FileNotFoundException) | ||||
|                 { | ||||
|                     StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.RestoringRemoteFile)); | ||||
| 					cachingFileStorage.UpdateRemoteFile(ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions)); | ||||
|                     cachingFileStorage.UpdateRemoteFile(ioc, | ||||
|                         _app.GetBooleanPreference(PreferenceKey.UseFileTransactions)); | ||||
|                     Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); | ||||
|                     Kp2aLog.Log("Checking for file change: file not found"); | ||||
|                     return; | ||||
| @@ -66,11 +71,12 @@ namespace keepass2android | ||||
|                 { | ||||
|                     //remote file is modified | ||||
|                     if (cachingFileStorage.HasLocalChanges(ioc) | ||||
| 						|| true //TODO remove | ||||
|                         || false //TODO remove | ||||
|                        ) | ||||
|                     { | ||||
|                         //conflict! need to merge | ||||
| 						var _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 +86,7 @@ namespace keepass2android | ||||
|                                 { | ||||
|                                     Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); | ||||
|                                 } | ||||
| 							}), _app.CurrentDb, false, remoteData); | ||||
|                             }), _app.CurrentDb, false, remoteData, _modificationWatcher); | ||||
|                         _saveDb.SetStatusLogger(StatusLogger); | ||||
|                         _saveDb.DoNotSetStatusLoggerMessage = true; //Keep "sync db" as main message | ||||
|                         _saveDb.SyncInBackground = false; | ||||
| @@ -111,7 +117,8 @@ 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; | ||||
| @@ -126,7 +133,8 @@ namespace keepass2android | ||||
|                     { | ||||
|                         //but we have local changes -> upload: | ||||
|                         StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.UploadingFile)); | ||||
| 						cachingFileStorage.UpdateRemoteFile(ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions)); | ||||
|                         cachingFileStorage.UpdateRemoteFile(ioc, | ||||
|                             _app.GetBooleanPreference(PreferenceKey.UseFileTransactions)); | ||||
|                         StatusLogger.UpdateSubMessage(""); | ||||
|                         Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully)); | ||||
|                     } | ||||
| @@ -136,6 +144,16 @@ namespace keepass2android | ||||
|                         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) | ||||
| 			{ | ||||
|   | ||||
| @@ -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(); | ||||
| 		} | ||||
|   | ||||
| @@ -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>(); | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
| @@ -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); | ||||
|   | ||||
| @@ -43,8 +43,11 @@ 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) | ||||
| 		{ | ||||
|             _modificationWatcher = modificationWatcher ?? new NullDatabaseModificationWatcher(); | ||||
|             _app = app; | ||||
| 			_ioc = ioc; | ||||
| 			_databaseData = databaseData; | ||||
| @@ -59,6 +62,7 @@ namespace keepass2android | ||||
| 	    protected bool success = false; | ||||
| 	    private bool _updateLastUsageTimestamp; | ||||
| 	    private readonly bool _makeCurrent; | ||||
|         private readonly IDatabaseModificationWatcher _modificationWatcher; | ||||
|  | ||||
|         public override void Run() | ||||
| 		{ | ||||
| @@ -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,6 +156,12 @@ namespace keepass2android | ||||
| 				Finish(false, _app.GetResourceString(UiStringKey.DuplicateUuidsError) + " " + ExceptionUtil.GetErrorMessage(e) + _app.GetResourceString(UiStringKey.DuplicateUuidsErrorAdditional), false, Exception); | ||||
| 				return; | ||||
| 			} | ||||
|             catch (Java.Lang.InterruptedException) | ||||
|             { | ||||
| 				Kp2aLog.Log("Load interrupted"); | ||||
| 				//close without Finish() | ||||
|                 return; | ||||
|             } | ||||
|             catch (Exception e) | ||||
| 			{ | ||||
| 				if (!(e is InvalidCompositeKeyException)) | ||||
| @@ -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); | ||||
|                 Database newDb = | ||||
|                     _app.LoadDatabase(_ioc, workingCopy, _compositeKey, StatusLogger, _format, _makeCurrent, _modificationWatcher); | ||||
|                 Kp2aLog.Log("LoadDB OK"); | ||||
|                 if (requiresSubsequentSync) | ||||
|                 { | ||||
| @@ -184,9 +205,10 @@ 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); | ||||
|                 } | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -35,10 +35,15 @@ 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 { | ||||
| 		private readonly IKp2aApp _app; | ||||
| 	    private readonly Database _db; | ||||
| 	    private readonly bool _dontSave; | ||||
|         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,9 +71,10 @@ 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) | ||||
| 		{ | ||||
|             _modificationWatcher = modificationWatcher ?? new NullDatabaseModificationWatcher(); | ||||
|             _db = db; | ||||
| 			_app = app; | ||||
| 			_dontSave = dontSave; | ||||
| @@ -76,9 +83,11 @@ namespace keepass2android | ||||
|  | ||||
|         } | ||||
|  | ||||
| 		public SaveDb(IKp2aApp app, Database db, OnOperationFinishedHandler operationFinishedHandler) | ||||
| 		public SaveDb(IKp2aApp app, Database db, OnOperationFinishedHandler operationFinishedHandler, IDatabaseModificationWatcher modificationWatcher = null) | ||||
| 			: base(app, operationFinishedHandler) | ||||
| 		{ | ||||
|              | ||||
|             _modificationWatcher = modificationWatcher ?? new NullDatabaseModificationWatcher(); | ||||
|             _app = app; | ||||
| 		    _db = db; | ||||
| 		    _dontSave = false; | ||||
| @@ -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. | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 _modificationWatcher.BeforeModifyDatabases(); | ||||
|             } | ||||
|             catch (Java.Lang.InterruptedException) | ||||
|             { | ||||
|                 // leave without Finish() | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 MergeIn(fileStorage, ioc); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 _modificationWatcher.AfterModifyDatabases(); | ||||
|                  | ||||
|             } | ||||
|  | ||||
|             PerformSaveWithoutCheck(fileStorage, ioc); | ||||
|             new Handler(Looper.MainLooper).Post(() => | ||||
|             { | ||||
|                 _db.UpdateGlobals(); | ||||
|             }); | ||||
|  | ||||
|             FinishWithSuccess(); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         private void RunInWorkerThread(Action runHandler) | ||||
| 		{ | ||||
| 			try | ||||
|   | ||||
| @@ -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(); | ||||
| 		} | ||||
|   | ||||
| @@ -123,6 +123,14 @@ namespace keepass2android | ||||
|  | ||||
| 		protected override void OnCreate(Bundle 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) | ||||
|   | ||||
| @@ -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> | ||||
| 		 | ||||
| @@ -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> | ||||
| @@ -78,6 +78,14 @@ namespace keepass2android | ||||
|  | ||||
|         protected override void OnCreate(Bundle 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: | ||||
| @@ -288,5 +296,12 @@ namespace keepass2android | ||||
| 	    { | ||||
| 	        get { return null; } | ||||
| 	    } | ||||
| 	}} | ||||
|  | ||||
|         protected override void OnDestroy() | ||||
|         { | ||||
|             base.OnDestroy(); | ||||
|             App.Kp2a.DatabasesBackgroundModificationLock.ExitReadLock(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|             { | ||||
|   | ||||
| @@ -116,14 +116,11 @@ 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) | ||||
| 	    { | ||||
| 			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,12 +214,19 @@ 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(); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -247,7 +253,11 @@ namespace keepass2android | ||||
|                 { | ||||
|                     _openDatabases.Add(newDb); | ||||
|                 } | ||||
|  | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
| 				modificationWatcher.AfterModifyDatabases(); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             if (createBackup) | ||||
| @@ -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,8 +1487,36 @@ 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. | ||||
| #if NoNet | ||||
|   | ||||
| @@ -48,6 +48,14 @@ namespace keepass2android.search | ||||
| 	{ | ||||
| 	    protected override void OnCreate (Bundle 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 ) { | ||||
| @@ -59,6 +67,12 @@ namespace keepass2android.search | ||||
| 			ProcessIntent(Intent); | ||||
| 		} | ||||
|  | ||||
|         protected override void OnDestroy() | ||||
|         { | ||||
|             base.OnDestroy(); | ||||
| 			App.Kp2a.DatabasesBackgroundModificationLock.ExitReadLock(); | ||||
|         } | ||||
|  | ||||
|         public override bool EntriesBelongToCurrentDatabaseOnly | ||||
| 	    { | ||||
| 	        get { return false; } | ||||
|   | ||||
| @@ -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; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Philipp Crocoll
					Philipp Crocoll