diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs index 2cc47c84..668d6892 100644 --- a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs @@ -20,7 +20,7 @@ namespace keepass2android.Io /// /// The file which we tried to write /// The exception why the remote file couldn't be updated - void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e); + void CouldntSaveToRemote(IOConnectionInfo ioc, Exception ex); /// /// Called when only the local file could be opened during an open operation. diff --git a/src/Kp2aBusinessLogic/database/edit/SaveDB.cs b/src/Kp2aBusinessLogic/database/edit/SaveDB.cs index dc3139a7..6dad0cee 100644 --- a/src/Kp2aBusinessLogic/database/edit/SaveDB.cs +++ b/src/Kp2aBusinessLogic/database/edit/SaveDB.cs @@ -99,12 +99,10 @@ namespace keepass2android } - - if ( (_streamForOrigFile != null) || fileStorage.CheckForFileChangeFast(ioc, _app.GetDb().LastFileVersion) //first try to use the fast change detection - || (FileHashChanged(ioc, _app.GetDb().KpDatabase.HashOfFileOnDisk)) //if that fails, hash the file and compare: + || (FileHashChanged(ioc, _app.GetDb().KpDatabase.HashOfFileOnDisk) == FileHashChange.Changed) //if that fails, hash the file and compare: ) { @@ -243,14 +241,26 @@ namespace keepass2android _app.GetDb().LastFileVersion = fileStorage.GetCurrentFileVersionFast(ioc); } - public byte[] HashFile(IOConnectionInfo iocFile) + public byte[] HashOriginalFile(IOConnectionInfo iocFile) { if (iocFile == null) { Debug.Assert(false); return null; } // Assert only Stream sIn; try { - sIn = _app.GetFileStorage(iocFile).OpenFileForRead(iocFile); + IFileStorage fileStorage = _app.GetFileStorage(iocFile); + CachingFileStorage cachingFileStorage = fileStorage as CachingFileStorage; + if (cachingFileStorage != null) + { + string hash; + cachingFileStorage.GetRemoteDataAndHash(iocFile, out hash); + return MemUtil.HexStringToByteArray(hash); + } + else + { + sIn = fileStorage.OpenFileForRead(iocFile); + } + if (sIn == null) throw new FileNotFoundException(); } catch (Exception) { return null; } @@ -267,10 +277,20 @@ namespace keepass2android return pbHash; } - private bool FileHashChanged(IOConnectionInfo ioc, byte[] hashOfFileOnDisk) + enum FileHashChange + { + Equal, + Changed, + FileNotAvailable + } + + private FileHashChange FileHashChanged(IOConnectionInfo ioc, byte[] hashOfFileOnDisk) { StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.CheckingTargetFileForChanges)); - return !MemUtil.ArraysEqual(HashFile(ioc), hashOfFileOnDisk); + byte[] fileHash = HashOriginalFile(ioc); + if (fileHash == null) + return FileHashChange.FileNotAvailable; + return MemUtil.ArraysEqual(fileHash, hashOfFileOnDisk) ? FileHashChange.Equal : FileHashChange.Changed; } diff --git a/src/Kp2aUnitTests/Kp2aUnitTests.csproj b/src/Kp2aUnitTests/Kp2aUnitTests.csproj index e91656dc..ed4c444c 100644 --- a/src/Kp2aUnitTests/Kp2aUnitTests.csproj +++ b/src/Kp2aUnitTests/Kp2aUnitTests.csproj @@ -71,6 +71,7 @@ + diff --git a/src/Kp2aUnitTests/MainActivity.cs b/src/Kp2aUnitTests/MainActivity.cs index bf437fba..84e43dd6 100644 --- a/src/Kp2aUnitTests/MainActivity.cs +++ b/src/Kp2aUnitTests/MainActivity.cs @@ -19,8 +19,10 @@ namespace Kp2aUnitTests TestRunner runner = new TestRunner(); // Run all tests from this assembly runner.AddTests(Assembly.GetExecutingAssembly()); - //runner.AddTests(new List { typeof(TestSynchronizeCachedDatabase) }); - //runner.AddTests(new List { typeof(TestLoadDb) });}} + //runner.AddTests(new List { typeof(TestSaveDbCached) }); + //runner.AddTests(typeof(TestSaveDbCached).GetMethod("TestLoadEditSaveWhenModified")); + + //runner.AddTests(new List { typeof(TestSaveDb) }); //runner.AddTests(new List { typeof(TestCachingFileStorage) }); //runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote")); //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly")); diff --git a/src/Kp2aUnitTests/TestCachingFileStorage.cs b/src/Kp2aUnitTests/TestCachingFileStorage.cs index 6dfe7c18..2f25ea94 100644 --- a/src/Kp2aUnitTests/TestCachingFileStorage.cs +++ b/src/Kp2aUnitTests/TestCachingFileStorage.cs @@ -191,8 +191,6 @@ namespace Kp2aUnitTests Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled); Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); - Assert.IsFalse(_testCacheSupervisor.RestoredRemoteCalled); - } diff --git a/src/Kp2aUnitTests/TestSaveDb.cs b/src/Kp2aUnitTests/TestSaveDb.cs index 3a8d3b84..c5968ddc 100644 --- a/src/Kp2aUnitTests/TestSaveDb.cs +++ b/src/Kp2aUnitTests/TestSaveDb.cs @@ -126,6 +126,35 @@ namespace Kp2aUnitTests } + [TestMethod] + public void TestLoadEditSaveWithWriteBecauseTargetNotExists() + { + //create the default database: + IKp2aApp app = SetupAppWithDefaultDatabase(); + //save it and reload it so we have a base version + SaveDatabase(app); + app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //modify the database by adding a group: + app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true); + + //delete the file: + File.Delete(DefaultFilename); + + //save the database: + SaveDatabase(app); + + //make sure no question was asked + Assert.AreEqual(null, ((TestKp2aApp)app).LastYesNoCancelQuestionTitle); + + //load database to a new app instance: + IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //ensure the file was saved: + AssertDatabasesAreEqual(app.GetDb().KpDatabase, resultApp.GetDb().KpDatabase); + + } + [TestMethod] public void TestLoadEditSaveWithSyncOverwriteBecauseOfNoCheck() { diff --git a/src/Kp2aUnitTests/TestSaveDbCached.cs b/src/Kp2aUnitTests/TestSaveDbCached.cs new file mode 100644 index 00000000..11c738ae --- /dev/null +++ b/src/Kp2aUnitTests/TestSaveDbCached.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using KeePassLib; +using KeePassLib.Serialization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using keepass2android; +using keepass2android.Io; + +namespace Kp2aUnitTests +{ + [TestClass] + class TestSaveDbCached: TestBase + { + private TestCacheSupervisor _testCacheSupervisor = new TestCacheSupervisor(); + private TestFileStorage _testFileStorage = new TestFileStorage(); + + protected override TestKp2aApp CreateTestKp2aApp() + { + TestKp2aApp app = base.CreateTestKp2aApp(); + app.FileStorage = new CachingFileStorage(_testFileStorage, "/mnt/sdcard/kp2atest/cache/", _testCacheSupervisor); + return app; + } + + [TestMethod] + public void TestLoadEditSave() + { + //create the default database: + IKp2aApp app = SetupAppWithDefaultDatabase(); + IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename }); + //save it and reload it so we have a base version + SaveDatabase(app); + app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + //modify the database by adding a group: + app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true); + //save the database again: + SaveDatabase(app); + Assert.IsNull(((TestKp2aApp)app).LastYesNoCancelQuestionTitle); + Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled); + Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); + + //load database to a new app instance: + IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //ensure the change was saved: + AssertDatabasesAreEqual(app.GetDb().KpDatabase, resultApp.GetDb().KpDatabase); + } + + [TestMethod] + public void TestLoadEditSaveWhenDeleted() + { + //create the default database: + IKp2aApp app = SetupAppWithDefaultDatabase(); + IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename }); + //save it and reload it so we have a base version + SaveDatabase(app); + app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //delete the file: + File.Delete(DefaultFilename); + + //modify the database by adding a group: + app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true); + //save the database again: + SaveDatabase(app); + Assert.IsNull(((TestKp2aApp) app).LastYesNoCancelQuestionTitle); + Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled); + Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); + + //load database to a new app instance: + IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //ensure the change was saved: + AssertDatabasesAreEqual(app.GetDb().KpDatabase, resultApp.GetDb().KpDatabase); + } + + + [TestMethod] + public void TestLoadEditSaveWhenModified() + { + //create the default database: + IKp2aApp app = SetupAppWithDefaultDatabase(); + IOConnection.DeleteFile(new IOConnectionInfo { Path = DefaultFilename }); + //save it and reload it so we have a base version + SaveDatabase(app); + app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups) + Kp2aLog.Log("app c: " + group.Name); + + //load once more: + var app2 = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + //modifiy once: + PwGroup group2 = new PwGroup(true, true, "TestGroup2", PwIcon.Apple); + app2.GetDb().KpDatabase.RootGroup.AddGroup(group2, true); + + foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups) + Kp2aLog.Log("app b: " + group.Name); + + SaveDatabase(app2); + + foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups) + Kp2aLog.Log("app d: " + group.Name); + Assert.IsNull(((TestKp2aApp)app).LastYesNoCancelQuestionTitle); + Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled); + Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); + + //modify the database by adding a group: + PwGroup group1 = new PwGroup(true, true, "TestGroup", PwIcon.Apple); + app.GetDb().KpDatabase.RootGroup.AddGroup(group1, true); + + foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups) + Kp2aLog.Log("app a: " + group.Name); + + + //save the database again: + SaveDatabase(app); + Assert.AreEqual(((TestKp2aApp)app).LastYesNoCancelQuestionTitle, UiStringKey.TitleSyncQuestion); + Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled); + Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled); + + + //load database to a new app instance: + IKp2aApp resultApp = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile); + + app2.GetDb().KpDatabase.RootGroup.AddGroup(group1, true); + foreach (var group in app.GetDb().KpDatabase.RootGroup.Groups) + Kp2aLog.Log("app: "+group.Name); + + foreach (var group in resultApp.GetDb().KpDatabase.RootGroup.Groups) + Kp2aLog.Log("resultApp: " + group.Name); + + //ensure the change was saved: + AssertDatabasesAreEqual(app2.GetDb().KpDatabase, resultApp.GetDb().KpDatabase); + + } + } +} \ No newline at end of file diff --git a/src/keepass2android/app/App.cs b/src/keepass2android/app/App.cs index 9af5e2ad..ff4931ac 100644 --- a/src/keepass2android/app/App.cs +++ b/src/keepass2android/app/App.cs @@ -20,6 +20,7 @@ using Android.App; using Android.Content; using Android.OS; using Android.Runtime; +using Android.Widget; using KeePassLib.Serialization; using Android.Preferences; using keepass2android.Io; @@ -57,8 +58,8 @@ namespace keepass2android /// /// Main implementation of the IKp2aApp interface for usage in the real app. /// - public class Kp2aApp: IKp2aApp - { + public class Kp2aApp: IKp2aApp, ICacheSupervisor + { public bool IsShutdown() { return _shutdown; @@ -253,7 +254,13 @@ namespace keepass2android public IFileStorage GetFileStorage(IOConnectionInfo iocInfo) { - return new BuiltInFileStorage(); + if (iocInfo.IsLocalFile()) + return new BuiltInFileStorage(); + else + { + //todo: check if desired + return new CachingFileStorage(new BuiltInFileStorage(), Application.Context.CacheDir.Path, this); + } } public void TriggerReload(Context ctx) @@ -294,7 +301,32 @@ namespace keepass2android _db = new Database(new DrawableFactory(), this); return _db; } - } + + void ShowToast(string message) + { + var handler = new Handler(Looper.MainLooper); + handler.Post(() => { Toast.MakeText(Application.Context, message, ToastLength.Long).Show(); }); + } + + public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e) + { + //TODO use resource strings + ShowToast("Couldn't save to remote: "+e.Message+". Save again or use Sync menu when remote connection is available again."); + } + + //todo: test changes in SaveDb with Cache: Save without conflict, save with conflict + //add test? + + public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex) + { + ShowToast("Couldn't open from remote: " + ex.Message+". Loaded file from local cache. You can still make changes in the database and sync them later."); + } + + public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc) + { + ShowToast("Opened local file due to conflict with changes in remote file. Use Synchronize menu to merge."); + } + } ///Application class for Keepass2Android: Contains static Database variable to be used by all components.