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.