diff --git a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs
index 13579079..e2fcb571 100644
--- a/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs
+++ b/src/Kp2aBusinessLogic/Io/BuiltInFileStorage.cs
@@ -36,7 +36,7 @@ namespace keepass2android.Io
TimeSpan diff = currentModificationDate - previousDate;
return diff > TimeSpan.FromSeconds(1);
//don't use > operator because milliseconds are truncated
- return File.GetLastWriteTimeUtc(ioc.Path) - previousDate >= TimeSpan.FromSeconds(1);
+ //return File.GetLastWriteTimeUtc(ioc.Path) - previousDate >= TimeSpan.FromSeconds(1);
}
diff --git a/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs
new file mode 100644
index 00000000..2db1155f
--- /dev/null
+++ b/src/Kp2aBusinessLogic/Io/CachingFileStorage.cs
@@ -0,0 +1,325 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using KeePassLib.Cryptography;
+using KeePassLib.Serialization;
+using KeePassLib.Utility;
+
+namespace keepass2android.Io
+{
+ ///
+ /// Interface for classes which can handle certain Cache events on a higher level (e.g. by user interaction)
+ ///
+ public interface ICacheSupervisor
+ {
+ ///
+ /// called when a save operation only updated the cache but not the remote file
+ ///
+ /// The file which we tried to write
+ /// The exception why the remote file couldn't be updated
+ void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e);
+
+ ///
+ /// Called when only the local file could be opened during an open operation.
+ ///
+ void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex);
+
+ ///
+ /// Called to notify the supervisor that the file described by ioc is opened from the cache because there's a conflict
+ /// with local and remote changes
+ ///
+ ///
+ void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc);
+ }
+
+ ///
+ /// Implements the IFileStorage interface as a proxy: A base storage is used as a remote storage. Local files are used to cache the
+ /// files on remote.
+ ///
+ public class CachingFileStorage: IFileStorage
+ {
+ private readonly IFileStorage _cachedStorage;
+ private readonly ICacheSupervisor _cacheSupervisor;
+ private readonly string _streamCacheDir;
+
+ public CachingFileStorage(IFileStorage cachedStorage, string cacheDir, ICacheSupervisor cacheSupervisor)
+ {
+ _cachedStorage = cachedStorage;
+ _cacheSupervisor = cacheSupervisor;
+ _streamCacheDir = cacheDir + Java.IO.File.Separator + "OfflineCache" + Java.IO.File.Separator;
+ if (!Directory.Exists(_streamCacheDir))
+ Directory.CreateDirectory(_streamCacheDir);
+
+ }
+
+ public void ClearCache()
+ {
+ IoUtil.DeleteDir(new Java.IO.File(_streamCacheDir), true);
+ }
+
+ public void DeleteFile(IOConnectionInfo ioc)
+ {
+ if (IsCached(ioc))
+ {
+ File.Delete(CachedFilePath(ioc));
+ File.Delete(VersionFilePath(ioc));
+ File.Delete(BaseVersionFilePath(ioc));
+ }
+
+ _cachedStorage.DeleteFile(ioc);
+ }
+
+ private string CachedFilePath(IOConnectionInfo ioc)
+ {
+ SHA256Managed sha256 = new SHA256Managed();
+ string iocAsHexString = MemUtil.ByteArrayToHexString(sha256.ComputeHash(Encoding.Unicode.GetBytes(ioc.Path.ToCharArray())))+".cache";
+ return _streamCacheDir + iocAsHexString;
+ }
+
+ private bool IsCached(IOConnectionInfo ioc)
+ {
+ return File.Exists(CachedFilePath(ioc))
+ && File.Exists(VersionFilePath(ioc))
+ && File.Exists(BaseVersionFilePath(ioc));
+ }
+
+ public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
+ {
+ //see comment in GetCurrentFileVersionFast
+ return false;
+ }
+
+ public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
+ {
+ //fast file version checking is not supported by CachingFileStorage:
+ //it's hard to return good versions in cases that the base source is offline
+ //or after modifying the cache.
+ //It's probably not relevant because fast file version checking is meant for local storage
+ //which is not cached.
+ return String.Empty;
+ }
+
+ private string VersionFilePath(IOConnectionInfo ioc)
+ {
+ return CachedFilePath(ioc)+".version";
+ }
+
+ private string BaseVersionFilePath(IOConnectionInfo ioc)
+ {
+ return CachedFilePath(ioc) + ".baseversion";
+ }
+
+ public Stream OpenFileForRead(IOConnectionInfo ioc)
+ {
+ string cachedFilePath = CachedFilePath(ioc);
+ try
+ {
+ if (!IsCached(ioc)
+ || File.ReadAllText(VersionFilePath(ioc)) == File.ReadAllText(BaseVersionFilePath(ioc)))
+ {
+ return OpenFileForReadWhenNoLocalChanges(ioc, cachedFilePath);
+ }
+ else
+ {
+ return OpenFileForReadWhenLocalChanges(ioc, cachedFilePath);
+ }
+ }
+ catch (Exception ex)
+ {
+ Kp2aLog.Log("couldn't open from remote " + ioc.Path);
+ Kp2aLog.Log(ex.ToString());
+
+ _cacheSupervisor.CouldntOpenFromRemote(ioc, ex);
+ return File.OpenRead(cachedFilePath);
+ }
+ }
+
+ private Stream OpenFileForReadWhenLocalChanges(IOConnectionInfo ioc, string cachedFilePath)
+ {
+ //file is cached but has local modifications
+ //try to upload the changes if remote file doesn't have changes as well:
+ var hash = Calculate(ioc);
+
+ if (File.ReadAllText(BaseVersionFilePath(ioc)) == hash)
+ {
+ //no changes in remote file -> upload
+ using (Stream localData = File.OpenRead(CachedFilePath(ioc)))
+ {
+ TryUpdateRemoteFile(localData, ioc, true, hash);
+ }
+ }
+ else
+ {
+ //conflict: both files changed.
+ //signal that we're loading from local
+ _cacheSupervisor.NotifyOpenFromLocalDueToConflict(ioc);
+ }
+ return File.OpenRead(cachedFilePath);
+ }
+
+ private string Calculate(IOConnectionInfo ioc)
+ {
+ MemoryStream remoteData = new MemoryStream();
+ string hash;
+ using (
+ HashingStreamEx hashingRemoteStream = new HashingStreamEx(_cachedStorage.OpenFileForRead(ioc), false,
+ new SHA256Managed()))
+ {
+ hashingRemoteStream.CopyTo(remoteData);
+ hashingRemoteStream.Close();
+ hash = MemUtil.ByteArrayToHexString(hashingRemoteStream.Hash);
+ }
+ return hash;
+ }
+
+ private Stream OpenFileForReadWhenNoLocalChanges(IOConnectionInfo ioc, string cachedFilePath)
+ {
+ //open stream:
+ using (Stream file = _cachedStorage.OpenFileForRead(ioc))
+ {
+ //copy to cache:
+ //note: we might use the file version to check if it's already in the cache and if copying is required.
+ //However, this is safer.
+ string fileHash;
+ using (HashingStreamEx cachedFile = new HashingStreamEx(File.Create(cachedFilePath), true, new SHA256Managed()))
+ {
+ file.CopyTo(cachedFile);
+ cachedFile.Close();
+ fileHash = MemUtil.ByteArrayToHexString(cachedFile.Hash);
+ }
+ //save hash in cache files:
+ File.WriteAllText(VersionFilePath(ioc), fileHash);
+ File.WriteAllText(BaseVersionFilePath(ioc), fileHash);
+
+ return File.OpenRead(cachedFilePath);
+ }
+
+ }
+
+ private void TryUpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash)
+ {
+ try
+ {
+ //try to write to remote:
+ using (
+ IWriteTransaction remoteTrans = _cachedStorage.OpenWriteTransaction(ioc, useFileTransaction))
+ {
+ Stream remoteStream = remoteTrans.OpenFile();
+ cachedData.CopyTo(remoteStream);
+ remoteStream.Close();
+ remoteTrans.CommitWrite();
+ }
+ //success. Update base-version of cache:
+ File.WriteAllText(VersionFilePath(ioc), hash);
+ }
+ catch (Exception e)
+ {
+ Kp2aLog.Log("couldn't save to remote " + ioc.Path);
+ Kp2aLog.Log(e.ToString());
+ //notify the supervisor so it might display a warning or schedule a retry
+ _cacheSupervisor.CouldntSaveToRemote(ioc, e);
+ }
+ }
+
+ private class CachedWriteTransaction: IWriteTransaction
+ {
+ private class CachedWriteMemoryStream : MemoryStream
+ {
+ private readonly IOConnectionInfo ioc;
+ private readonly CachingFileStorage _cachingFileStorage;
+ private readonly bool _useFileTransaction;
+
+ public CachedWriteMemoryStream(IOConnectionInfo ioc, CachingFileStorage cachingFileStorage, bool useFileTransaction)
+ {
+ this.ioc = ioc;
+ _cachingFileStorage = cachingFileStorage;
+ _useFileTransaction = useFileTransaction;
+ }
+
+
+ public override void Close()
+ {
+ //write file to cache:
+ //(note: this might overwrite local changes. It's assumed that a sync operation or check was performed before
+ string hash;
+ using (var hashingStream = new HashingStreamEx(File.Create(_cachingFileStorage.CachedFilePath(ioc)), true, new SHA256Managed()))
+ {
+ Position = 0;
+ CopyTo(hashingStream);
+
+ hashingStream.Close();
+ hash = MemUtil.ByteArrayToHexString(hashingStream.Hash);
+ }
+
+ File.WriteAllText(_cachingFileStorage.VersionFilePath(ioc), hash);
+ //update file on remote. This might overwrite changes there as well, see above.
+ Position = 0;
+ _cachingFileStorage.TryUpdateRemoteFile(this, ioc, _useFileTransaction, hash);
+
+ base.Close();
+ }
+
+ }
+
+ private readonly IOConnectionInfo _ioc;
+ private readonly bool _useFileTransaction;
+ private readonly CachingFileStorage _cachingFileStorage;
+ private MemoryStream _memoryStream;
+ private bool _committed;
+
+ public CachedWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction, CachingFileStorage cachingFileStorage)
+ {
+ _ioc = ioc;
+ _useFileTransaction = useFileTransaction;
+ _cachingFileStorage = cachingFileStorage;
+ }
+
+ public void Dispose()
+ {
+ if (!_committed)
+ _memoryStream.Dispose();
+ }
+
+ public Stream OpenFile()
+ {
+ _memoryStream = new CachedWriteMemoryStream(_ioc, _cachingFileStorage, _useFileTransaction);
+ return _memoryStream;
+ }
+
+ public void CommitWrite()
+ {
+ //the transaction is committed in the stream's Close
+ _committed = true;
+ }
+
+ }
+
+
+ public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
+ {
+ //create a transaction which writes to memory stream
+ //on close: write to cache. If possible, write to online
+ //update versions
+ return new CachedWriteTransaction(ioc, useFileTransaction, this);
+ }
+
+ public bool CompleteIoId()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool? FileExists()
+ {
+ throw new NotImplementedException();
+ }
+
+ public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
+ {
+ return UrlUtil.StripExtension(
+ UrlUtil.GetFileName(ioc.Path));
+ }
+ }
+}
diff --git a/src/Kp2aBusinessLogic/Io/IoUtil.cs b/src/Kp2aBusinessLogic/Io/IoUtil.cs
new file mode 100644
index 00000000..c4a83a79
--- /dev/null
+++ b/src/Kp2aBusinessLogic/Io/IoUtil.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Java.IO;
+
+namespace keepass2android.Io
+{
+ public static class IoUtil
+ {
+ public static bool DeleteDir(File dir, bool contentsOnly=false)
+ {
+ if (dir != null && dir.IsDirectory)
+ {
+ String[] children = dir.List();
+ for (int i = 0; i < children.Length; i++)
+ {
+ bool success = DeleteDir(new File(dir, children[i]));
+ if (!success)
+ {
+ return false;
+ }
+ }
+ }
+
+ if (contentsOnly)
+ return true;
+
+ // The directory is now empty so delete it
+ return dir.Delete();
+ }
+
+
+ }
+}
diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj
index a39cd67f..9b110957 100644
--- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj
+++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj
@@ -52,7 +52,9 @@
+
+
diff --git a/src/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs
index dd3cf5c5..93e2a542 100644
--- a/src/Kp2aBusinessLogic/database/Database.cs
+++ b/src/Kp2aBusinessLogic/database/Database.cs
@@ -124,7 +124,9 @@ namespace keepass2android
try
{
IFileStorage fileStorage = _app.GetFileStorage(iocInfo);
+ var fileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
pwDatabase.Open(fileStorage.OpenFileForRead(iocInfo), fileStorage.GetFilenameWithoutPathAndExt(iocInfo), iocInfo, compositeKey, status);
+ LastFileVersion = fileVersion;
}
catch (Exception)
{
@@ -140,8 +142,6 @@ namespace keepass2android
status.UpdateSubMessage("");
- LastFileVersion = _app.GetFileStorage(iocInfo).GetCurrentFileVersionFast(iocInfo);
-
Root = pwDatabase.RootGroup;
PopulateGlobals(Root);
diff --git a/src/Kp2aUnitTests/Kp2aUnitTests.csproj b/src/Kp2aUnitTests/Kp2aUnitTests.csproj
index 0ad98d38..a6d9e58d 100644
--- a/src/Kp2aUnitTests/Kp2aUnitTests.csproj
+++ b/src/Kp2aUnitTests/Kp2aUnitTests.csproj
@@ -66,6 +66,7 @@
+
diff --git a/src/Kp2aUnitTests/MainActivity.cs b/src/Kp2aUnitTests/MainActivity.cs
index cce4f45a..0a1a684b 100644
--- a/src/Kp2aUnitTests/MainActivity.cs
+++ b/src/Kp2aUnitTests/MainActivity.cs
@@ -19,8 +19,9 @@ namespace Kp2aUnitTests
TestRunner runner = new TestRunner();
// Run all tests from this assembly
//runner.AddTests(Assembly.GetExecutingAssembly());
- //runner.AddTests(new List { typeof(TestSaveDb)});
- runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadEditSaveWithSyncKdbp"));
+ runner.AddTests(new List { typeof(TestCachingFileStorage) });
+ //runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote"));
+ //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly"));
//runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles"));
return runner;
}
diff --git a/src/Kp2aUnitTests/TestCachingFileStorage.cs b/src/Kp2aUnitTests/TestCachingFileStorage.cs
new file mode 100644
index 00000000..581c72ed
--- /dev/null
+++ b/src/Kp2aUnitTests/TestCachingFileStorage.cs
@@ -0,0 +1,358 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Android.App;
+using KeePassLib.Serialization;
+using KeePassLib.Utility;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using keepass2android.Io;
+
+namespace Kp2aUnitTests
+{
+ [TestClass]
+ class TestCachingFileStorage: TestBase
+ {
+ private TestFileStorage _testFileStorage;
+ private CachingFileStorage _fileStorage;
+ private static readonly string CachingTestFile = DefaultDirectory + "cachingTestFile.txt";
+ private string _defaultCacheFileContents = "default contents";
+ private TestCacheSupervisor _testCacheSupervisor;
+
+ class TestCacheSupervisor: ICacheSupervisor
+ {
+ public bool CouldntOpenFromRemoteCalled { get; set; }
+ public bool CouldntSaveToRemoteCalled { get; set; }
+ public bool NotifyOpenFromLocalDueToConflictCalled { get; set; }
+
+
+ public void CouldntSaveToRemote(IOConnectionInfo ioc, Exception e)
+ {
+ CouldntSaveToRemoteCalled = true;
+ }
+
+ public void CouldntOpenFromRemote(IOConnectionInfo ioc, Exception ex)
+ {
+ CouldntOpenFromRemoteCalled = true;
+ }
+
+ public void NotifyOpenFromLocalDueToConflict(IOConnectionInfo ioc)
+ {
+ NotifyOpenFromLocalDueToConflictCalled = true;
+ }
+ }
+
+ public class TestFileStorage: IFileStorage
+ {
+ private BuiltInFileStorage _builtIn = new BuiltInFileStorage();
+
+ public bool Offline { get; set; }
+
+
+ public void DeleteFile(IOConnectionInfo ioc)
+ {
+ if (Offline)
+ throw new IOException("offline");
+ _builtIn.DeleteFile(ioc);
+ }
+
+ public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
+ {
+ if (Offline)
+ return false;
+ return _builtIn.CheckForFileChangeFast(ioc, previousFileVersion);
+ }
+
+ public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
+ {
+ if (Offline)
+ throw new IOException("offline");
+ return _builtIn.GetCurrentFileVersionFast(ioc);
+ }
+
+ public Stream OpenFileForRead(IOConnectionInfo ioc)
+ {
+ if (Offline)
+ throw new IOException("offline");
+ return _builtIn.OpenFileForRead(ioc);
+ }
+
+ public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
+ {
+ if (Offline)
+ throw new IOException("offline");
+ return new TestFileTransaction(ioc, useFileTransaction, Offline);
+ }
+
+ public class TestFileTransaction : IWriteTransaction
+ {
+ private readonly bool _offline;
+ private readonly FileTransactionEx _transaction;
+
+ public TestFileTransaction(IOConnectionInfo ioc, bool useFileTransaction, bool offline)
+ {
+ _offline = offline;
+ _transaction = new FileTransactionEx(ioc, useFileTransaction);
+ }
+
+ public void Dispose()
+ {
+
+ }
+
+ public Stream OpenFile()
+ {
+ if (_offline)
+ throw new IOException("offline");
+ return _transaction.OpenWrite();
+ }
+
+ public void CommitWrite()
+ {
+ if (_offline)
+ throw new IOException("offline");
+ _transaction.CommitWrite();
+ }
+ }
+
+ public bool CompleteIoId()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool? FileExists()
+ {
+ throw new NotImplementedException();
+ }
+
+ public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
+ {
+ return _builtIn.GetFilenameWithoutPathAndExt(ioc);
+ }
+ }
+
+ ///
+ /// Tests correct behavior in case that either remote or cache are not available
+ ///
+ [TestMethod]
+ public void TestMakeAccessibleWhenOffline()
+ {
+ SetupFileStorage();
+
+ //read the file once. Should now be in the cache.
+ MemoryStream fileContents = ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ //check it's the correct data:
+ Assert.AreEqual(MemoryStreamToString(fileContents), _defaultCacheFileContents);
+
+ //let the base file storage go offline:
+ _testFileStorage.Offline = true;
+
+ //now try to read the file again:
+ MemoryStream fileContents2 = ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ AssertEqual(fileContents, fileContents2);
+
+ Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+
+
+ }
+
+ private string MemoryStreamToString(MemoryStream stream)
+ {
+ stream.Position = 0;
+ StreamReader r = new StreamReader(stream);
+ return r.ReadToEnd();
+ }
+
+ ///
+ /// tests correct behaviour after modifiying the local cache (with the remote file being unchanged) and remote being
+ /// either unavailable or available
+ ///
+ [TestMethod]
+ public void TestSyncOnLoadWhenLocalFileChanged()
+ {
+ SetupFileStorage();
+
+
+ //read the file once. Should now be in the cache.
+ ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+
+ //let the base file storage go offline:
+ _testFileStorage.Offline = true;
+
+ //write something to the cache:
+ string newContent = "new content";
+ WriteContentToCacheFile(newContent);
+
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsTrue(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+ _testCacheSupervisor.CouldntSaveToRemoteCalled = false;
+
+ //now try to read the file again:
+ MemoryStream fileContents2 = ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ Assert.IsTrue(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+ _testCacheSupervisor.CouldntOpenFromRemoteCalled = false;
+
+ //should return the written content:
+ Assert.AreEqual(MemoryStreamToString(fileContents2), newContent);
+
+ //now go online and read again. This should trigger a sync and the modified data must be returned
+ _testFileStorage.Offline = false;
+ MemoryStream fileContents3 = ReadToMemoryStream(_fileStorage, CachingTestFile);
+ Assert.AreEqual(MemoryStreamToString(fileContents3), newContent);
+
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+
+ //ensure the data on the remote was synced:
+ MemoryStream fileContents4 = ReadToMemoryStream(_testFileStorage, CachingTestFile);
+ Assert.AreEqual(MemoryStreamToString(fileContents4), newContent);
+ }
+
+ ///
+ /// tests correct behaviour after modifiying both the local cache and the remote file
+ ///
+ [TestMethod]
+ public void TestLoadLocalWhenBothFilesChanged()
+ {
+ SetupFileStorage();
+
+ //read the file once. Should now be in the cache.
+ ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+
+ //let the base file storage go offline:
+ _testFileStorage.Offline = true;
+
+ //write something to the cache:
+ string newLocalContent = "new local content";
+ WriteContentToCacheFile(newLocalContent);
+
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsTrue(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+ _testCacheSupervisor.CouldntSaveToRemoteCalled = false;
+
+ //write something to the remote file:
+ File.WriteAllText(CachingTestFile, "new remote content");
+
+ //go online again:
+ _testFileStorage.Offline = false;
+
+ //now try to read the file again:
+ MemoryStream fileContents2 = ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ //should return the local content:
+ Assert.AreEqual(MemoryStreamToString(fileContents2), newLocalContent);
+
+ //but a notification about the conflict should be made:
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+ Assert.IsTrue(_testCacheSupervisor.NotifyOpenFromLocalDueToConflictCalled);
+ _testCacheSupervisor.NotifyOpenFromLocalDueToConflictCalled = false;
+
+
+
+ }
+
+
+
+ [TestMethod]
+ public void TestSaveToRemote()
+ {
+ SetupFileStorage();
+
+ //read the file once. Should now be in the cache.
+ ReadToMemoryStream(_fileStorage, CachingTestFile);
+
+ //write something to the cache:
+ string newContent = "new content";
+ WriteContentToCacheFile(newContent);
+
+ Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
+ Assert.IsFalse(_testCacheSupervisor.CouldntSaveToRemoteCalled);
+
+ Assert.AreEqual(newContent, File.ReadAllText(CachingTestFile));
+ }
+
+ private void WriteContentToCacheFile(string newContent)
+ {
+
+ using (var trans = _fileStorage.OpenWriteTransaction(IocForCacheFile, true))
+ {
+ StreamWriter sw = new StreamWriter(trans.OpenFile());
+ sw.Write(newContent);
+ sw.Flush();
+ sw.Close();
+ trans.CommitWrite();
+ }
+ }
+
+ protected IOConnectionInfo IocForCacheFile
+ {
+ get { return new IOConnectionInfo() { Path = CachingTestFile }; }
+ }
+
+ private void SetupFileStorage()
+ {
+ _testFileStorage = new TestFileStorage();
+ _testCacheSupervisor = new TestCacheSupervisor();
+ _fileStorage = new CachingFileStorage(_testFileStorage, Application.Context.CacheDir.Path, _testCacheSupervisor);
+ _fileStorage.ClearCache();
+ File.WriteAllText(CachingTestFile, _defaultCacheFileContents);
+ }
+
+ private static MemoryStream ReadToMemoryStream(IFileStorage fileStorage, string filename)
+ {
+ Stream fileStream = fileStorage.OpenFileForRead(new IOConnectionInfo() {Path = filename});
+ MemoryStream fileContents = new MemoryStream();
+ fileStream.CopyTo(fileContents);
+ fileStream.Close();
+ return fileContents;
+ }
+
+
+ static bool StreamEquals(Stream stream1, Stream stream2)
+ {
+ const int bufferSize = 2048;
+ byte[] buffer1 = new byte[bufferSize]; //buffer size
+ byte[] buffer2 = new byte[bufferSize];
+ while (true)
+ {
+ int count1 = stream1.Read(buffer1, 0, bufferSize);
+ int count2 = stream2.Read(buffer2, 0, bufferSize);
+
+ if (count1 != count2)
+ return false;
+
+ if (count1 == 0)
+ return true;
+
+ // You might replace the following with an efficient "memcmp"
+ if (!buffer1.Take(count1).SequenceEqual(buffer2.Take(count2)))
+ return false;
+ }
+ }
+
+ private void AssertEqual(MemoryStream s1, MemoryStream s2)
+ {
+ s1.Seek(0,0);
+ s2.Seek(0, 0);
+ Assert.AreEqual(s1.Length, s2.Length);
+ Assert.AreEqual(0, s1.Position);
+ Assert.AreEqual(0, s2.Position);
+ Assert.IsTrue(StreamEquals(s1, s2));
+ }
+
+ //todo test delete
+ }
+}
diff --git a/src/Kp2aUnitTests/TestSaveDb.cs b/src/Kp2aUnitTests/TestSaveDb.cs
index b3316575..3a8d3b84 100644
--- a/src/Kp2aUnitTests/TestSaveDb.cs
+++ b/src/Kp2aUnitTests/TestSaveDb.cs
@@ -232,26 +232,18 @@ namespace Kp2aUnitTests
}
-
+ /*
[TestMethod]
public void TestSaveAsWhenReadOnly()
{
Assert.Fail("TODO: Test ");
}
-
- [TestMethod]
- public void TestReloadWhenCancelSync()
- {
- //when a change is detected and the user cancels saving, the app should display the "file was modified - reload?" question.
- Assert.Fail("TODO: Test ");
- }
-
[TestMethod]
public void TestSaveAsWhenSyncError()
{
Assert.Fail("TODO: Test ");
- }
+ }*/
[TestMethod]
public void TestLoadAndSave_TestIdenticalFiles()
diff --git a/src/keepass2android/AttachmentContentProvider.cs b/src/keepass2android/AttachmentContentProvider.cs
index 59b0378d..6326ef96 100644
--- a/src/keepass2android/AttachmentContentProvider.cs
+++ b/src/keepass2android/AttachmentContentProvider.cs
@@ -11,7 +11,8 @@ namespace keepass2android
///
[ContentProvider(new[]{"keepass2android."+AppNames.PackagePart+".provider"})]
public class AttachmentContentProvider : ContentProvider {
-
+ public const string AttachmentCacheSubDir = "AttachmentCache";
+
private const String ClassName = "AttachmentContentProvider";
// The authority is the symbolic name for the provider class
@@ -37,7 +38,8 @@ namespace keepass2android
// E.g.
// 'content://keepass2android.provider/Test.txt'
// Take this and build the path to the file
- String fileLocation = Context.CacheDir + File.Separator
+
+ String fileLocation = Context.CacheDir + File.Separator + AttachmentCacheSubDir + File.Separator
+ uri.LastPathSegment;
// Create & return a ParcelFileDescriptor pointing to the file
diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs
index fe7923ec..48125078 100644
--- a/src/keepass2android/EntryActivity.cs
+++ b/src/keepass2android/EntryActivity.cs
@@ -35,6 +35,7 @@ using KeePassLib.Security;
using Android.Webkit;
using Android.Graphics;
using Java.IO;
+using keepass2android.Io;
namespace keepass2android
{
@@ -230,7 +231,7 @@ namespace keepass2android
ISharedPreferences prefs = PreferenceManager.GetDefaultSharedPreferences(this);
string binaryDirectory = prefs.GetString(GetString(Resource.String.BinaryDirectory_key), GetString(Resource.String.BinaryDirectory_default));
if (writeToCacheDirectory)
- binaryDirectory = CacheDir.Path;
+ binaryDirectory = CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir;
string filepart = key;
if (writeToCacheDirectory)
@@ -528,30 +529,16 @@ namespace keepass2android
public void ClearCache() {
try {
- File dir = CacheDir;
- if (dir != null && dir.IsDirectory) {
- DeleteDir(dir);
+ File dir = new File(CacheDir.Path + File.Separator + AttachmentContentProvider.AttachmentCacheSubDir);
+ if (dir.IsDirectory) {
+ IoUtil.DeleteDir(dir);
}
} catch (Exception) {
}
}
- public static bool DeleteDir(File dir) {
- if (dir != null && dir.IsDirectory) {
- String[] children = dir.List();
- for (int i = 0; i < children.Length; i++) {
- bool success = DeleteDir(new File(dir, children[i]));
- if (!success) {
- return false;
- }
- }
- }
-
- // The directory is now empty so delete it
- return dir.Delete();
- }
-
+
public override bool OnOptionsItemSelected(IMenuItem item) {
switch ( item.ItemId ) {
case Resource.Id.menu_donate:
diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj
index 0a63549a..84ba4e08 100644
--- a/src/keepass2android/keepass2android.csproj
+++ b/src/keepass2android/keepass2android.csproj
@@ -150,7 +150,9 @@
False
-
+
+ Designer
+
False