+ SynchronizeCachedDatabase.cs: Synchronizes the local cache with the remote file. Applies merging if necessary.

+ Tests (not yet complete)
This commit is contained in:
Philipp Crocoll 2013-08-01 22:20:39 +02:00
parent 3cfb2c17e6
commit c0520c055f
21 changed files with 587 additions and 186 deletions

View File

@ -105,7 +105,8 @@ namespace KeePassLib.Serialization
// Not implemented and ignored in Mono < 2.10 // Not implemented and ignored in Mono < 2.10
try try
{ {
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore); //deactivated. No longer supported in Mono 4.8?
//request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.NoCacheNoStore);
} }
catch(NotImplementedException) { } catch(NotImplementedException) { }
catch(Exception) { Debug.Assert(false); } catch(Exception) { Debug.Assert(false); }

View File

@ -1,4 +1,5 @@
using System; using System;
using Android.App;
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
using KeePassLib.Serialization; using KeePassLib.Serialization;
@ -69,6 +70,7 @@ namespace keepass2android
IProgressDialog CreateProgressDialog(Context ctx); IProgressDialog CreateProgressDialog(Context ctx);
IFileStorage GetFileStorage(IOConnectionInfo iocInfo); IFileStorage GetFileStorage(IOConnectionInfo iocInfo);
void TriggerReload(Context context);
} }
} }

View File

@ -118,7 +118,7 @@ namespace keepass2android.Io
try try
{ {
if (!IsCached(ioc) if (!IsCached(ioc)
|| File.ReadAllText(VersionFilePath(ioc)) == File.ReadAllText(BaseVersionFilePath(ioc))) || GetLocalVersionHash(ioc) == GetBaseVersionHash(ioc))
{ {
return OpenFileForReadWhenNoLocalChanges(ioc, cachedFilePath); return OpenFileForReadWhenNoLocalChanges(ioc, cachedFilePath);
} }
@ -141,7 +141,7 @@ namespace keepass2android.Io
{ {
//file is cached but has local modifications //file is cached but has local modifications
//try to upload the changes if remote file doesn't have changes as well: //try to upload the changes if remote file doesn't have changes as well:
var hash = Calculate(ioc); var hash = CalculateHash(ioc);
if (File.ReadAllText(BaseVersionFilePath(ioc)) == hash) if (File.ReadAllText(BaseVersionFilePath(ioc)) == hash)
{ {
@ -160,18 +160,25 @@ namespace keepass2android.Io
return File.OpenRead(cachedFilePath); return File.OpenRead(cachedFilePath);
} }
private string Calculate(IOConnectionInfo ioc) public MemoryStream GetRemoteDataAndHash(IOConnectionInfo ioc, out string hash)
{ {
MemoryStream remoteData = new MemoryStream(); MemoryStream remoteData = new MemoryStream();
string hash;
using ( using (
HashingStreamEx hashingRemoteStream = new HashingStreamEx(_cachedStorage.OpenFileForRead(ioc), false, HashingStreamEx hashingRemoteStream = new HashingStreamEx(_cachedStorage.OpenFileForRead(ioc), false,
new SHA256Managed())) new SHA256Managed()))
{ {
hashingRemoteStream.CopyTo(remoteData); hashingRemoteStream.CopyTo(remoteData);
hashingRemoteStream.Close(); hashingRemoteStream.Close();
hash = MemUtil.ByteArrayToHexString(hashingRemoteStream.Hash); hash = MemUtil.ByteArrayToHexString(hashingRemoteStream.Hash);
} }
remoteData.Position = 0;
return remoteData;
}
private string CalculateHash(IOConnectionInfo ioc)
{
string hash;
GetRemoteDataAndHash(ioc, out hash);
return hash; return hash;
} }
@ -203,17 +210,7 @@ namespace keepass2android.Io
{ {
try try
{ {
//try to write to remote: UpdateRemoteFile(cachedData, ioc, useFileTransaction, hash);
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) catch (Exception e)
{ {
@ -224,6 +221,33 @@ namespace keepass2android.Io
} }
} }
private void UpdateRemoteFile(Stream cachedData, IOConnectionInfo ioc, bool useFileTransaction, string hash)
{
//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(BaseVersionFilePath(ioc), hash);
File.WriteAllText(VersionFilePath(ioc), hash);
}
public void UpdateRemoteFile(IOConnectionInfo ioc, bool useFileTransaction)
{
using (Stream cachedData = File.OpenRead(CachedFilePath(ioc)))
{
UpdateRemoteFile(cachedData, ioc, useFileTransaction, GetLocalVersionHash(ioc));
}
}
private class CachedWriteTransaction: IWriteTransaction private class CachedWriteTransaction: IWriteTransaction
{ {
private class CachedWriteMemoryStream : MemoryStream private class CachedWriteMemoryStream : MemoryStream
@ -231,6 +255,7 @@ namespace keepass2android.Io
private readonly IOConnectionInfo ioc; private readonly IOConnectionInfo ioc;
private readonly CachingFileStorage _cachingFileStorage; private readonly CachingFileStorage _cachingFileStorage;
private readonly bool _useFileTransaction; private readonly bool _useFileTransaction;
private bool _closed;
public CachedWriteMemoryStream(IOConnectionInfo ioc, CachingFileStorage cachingFileStorage, bool useFileTransaction) public CachedWriteMemoryStream(IOConnectionInfo ioc, CachingFileStorage cachingFileStorage, bool useFileTransaction)
{ {
@ -242,6 +267,8 @@ namespace keepass2android.Io
public override void Close() public override void Close()
{ {
if (_closed) return;
//write file to cache: //write file to cache:
//(note: this might overwrite local changes. It's assumed that a sync operation or check was performed before //(note: this might overwrite local changes. It's assumed that a sync operation or check was performed before
string hash; string hash;
@ -257,9 +284,20 @@ namespace keepass2android.Io
File.WriteAllText(_cachingFileStorage.VersionFilePath(ioc), hash); File.WriteAllText(_cachingFileStorage.VersionFilePath(ioc), hash);
//update file on remote. This might overwrite changes there as well, see above. //update file on remote. This might overwrite changes there as well, see above.
Position = 0; Position = 0;
_cachingFileStorage.TryUpdateRemoteFile(this, ioc, _useFileTransaction, hash); if (_cachingFileStorage.IsCached(ioc))
{
//if the file already is in the cache, it's ok if writing to remote fails.
_cachingFileStorage.TryUpdateRemoteFile(this, ioc, _useFileTransaction, hash);
}
else
{
//if not, we don't accept a failure (e.g. invalid credentials would always remain a problem)
_cachingFileStorage.UpdateRemoteFile(this, ioc, _useFileTransaction, hash);
}
base.Close(); base.Close();
_closed = true;
} }
} }
@ -280,7 +318,18 @@ namespace keepass2android.Io
public void Dispose() public void Dispose()
{ {
if (!_committed) if (!_committed)
_memoryStream.Dispose(); {
try
{
_memoryStream.Dispose();
}
catch (ObjectDisposedException e)
{
Kp2aLog.Log("Ignoring exception in Dispose: "+e);
}
}
} }
public Stream OpenFile() public Stream OpenFile()
@ -321,5 +370,32 @@ namespace keepass2android.Io
return UrlUtil.StripExtension( return UrlUtil.StripExtension(
UrlUtil.GetFileName(ioc.Path)); UrlUtil.GetFileName(ioc.Path));
} }
public string GetBaseVersionHash(IOConnectionInfo ioc)
{
return File.ReadAllText(BaseVersionFilePath(ioc));
}
public string GetLocalVersionHash(IOConnectionInfo ioc)
{
return File.ReadAllText(VersionFilePath(ioc));
}
public bool HasLocalChanges(IOConnectionInfo ioc)
{
return IsCached(ioc)
&& GetLocalVersionHash(ioc) != GetBaseVersionHash(ioc);
}
public Stream OpenRemoteForReadIfAvailable(IOConnectionInfo ioc)
{
try
{
return _cachedStorage.OpenFileForRead(ioc);
}
catch (Exception)
{
return File.OpenRead(CachedFilePath(ioc));
}
}
} }
} }

View File

@ -51,6 +51,7 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="database\SynchronizeCachedDatabase.cs" />
<Compile Include="Io\BuiltInFileStorage.cs" /> <Compile Include="Io\BuiltInFileStorage.cs" />
<Compile Include="Io\CachingFileStorage.cs" /> <Compile Include="Io\CachingFileStorage.cs" />
<Compile Include="Io\IFileStorage.cs" /> <Compile Include="Io\IFileStorage.cs" />

View File

@ -34,6 +34,11 @@ namespace keepass2android
yes, yes,
no, no,
YesSynchronize, YesSynchronize,
NoOverwrite NoOverwrite,
SynchronizingCachedDatabase,
DownloadingRemoteFile,
UploadingFile,
FilesInSync,
SynchronizedDatabaseSuccessfully
} }
} }

View File

@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Android.App;
using Android.Content;
using KeePassLib.Serialization;
using keepass2android.Io;
namespace keepass2android
{
public class SynchronizeCachedDatabase: RunnableOnFinish
{
private readonly Context _context;
private readonly IKp2aApp _app;
public SynchronizeCachedDatabase(Context context, IKp2aApp app, OnFinish finish)
: base(finish)
{
_context = context;
_app = app;
}
public override void Run()
{
try
{
IOConnectionInfo ioc = _app.GetDb().Ioc;
IFileStorage fileStorage = _app.GetFileStorage(ioc);
if (!(fileStorage is CachingFileStorage))
{
throw new Exception("Cannot sync a non-cached database!");
}
StatusLogger.UpdateMessage(UiStringKey.SynchronizingCachedDatabase);
CachingFileStorage cachingFileStorage = (CachingFileStorage) fileStorage;
//download file from remote location and calculate hash:
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.DownloadingRemoteFile));
string hash;
//todo: catch filenotfound and upload then
MemoryStream remoteData = cachingFileStorage.GetRemoteDataAndHash(ioc, out hash);
//todo: what happens if something fails here?
//check if remote file was modified:
if (cachingFileStorage.GetBaseVersionHash(ioc) != hash)
{
//remote file is unmodified
if (cachingFileStorage.HasLocalChanges(ioc))
{
//conflict! need to merge
SaveDb saveDb = new SaveDb(_context, _app, new ActionOnFinish((success, result) =>
{
if (!success)
{
Finish(false, result);
}
else
{
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
}), false, remoteData);
saveDb.Run();
}
else
{
//only the remote file was modified -> reload database.
//note: it's best to lock the database and do a complete reload here (also better for UI consistency in case something goes wrong etc.)
_app.TriggerReload(_context);
Finish(true);
}
}
else
{
//remote file is unmodified
if (cachingFileStorage.HasLocalChanges(ioc))
{
//but we have local changes -> upload:
StatusLogger.UpdateSubMessage(_app.GetResourceString(UiStringKey.UploadingFile));
cachingFileStorage.UpdateRemoteFile(ioc, _app.GetBooleanPreference(PreferenceKey.UseFileTransactions));
StatusLogger.UpdateSubMessage("");
Finish(true, _app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
}
else
{
//files are in sync: just set the result
Finish(true, _app.GetResourceString(UiStringKey.FilesInSync));
}
}
}
catch (Exception e)
{
Finish(false, e.Message);
}
}
}
}

View File

@ -34,14 +34,38 @@ namespace keepass2android
public class SaveDb : RunnableOnFinish { public class SaveDb : RunnableOnFinish {
private readonly IKp2aApp _app; private readonly IKp2aApp _app;
private readonly bool _dontSave; private readonly bool _dontSave;
/// <summary>
/// stream for reading the data from the original file. If this is set to a non-null value, we know we need to sync
/// </summary>
private readonly Stream _streamForOrigFile;
private readonly Context _ctx; private readonly Context _ctx;
private Thread _workerThread; private Thread _workerThread;
public SaveDb(Context ctx, IKp2aApp app, OnFinish finish, bool dontSave): base(finish) { public SaveDb(Context ctx, IKp2aApp app, OnFinish finish, bool dontSave)
: base(finish)
{
_ctx = ctx; _ctx = ctx;
_app = app; _app = app;
_dontSave = dontSave; _dontSave = dontSave;
} }
/// <summary>
/// Constructor for sync
/// </summary>
/// <param name="ctx"></param>
/// <param name="app"></param>
/// <param name="finish"></param>
/// <param name="dontSave"></param>
/// <param name="streamForOrigFile">Stream for reading the data from the (changed) original location</param>
public SaveDb(Context ctx, IKp2aApp app, OnFinish finish, bool dontSave, Stream streamForOrigFile)
: base(finish)
{
_ctx = ctx;
_app = app;
_dontSave = dontSave;
_streamForOrigFile = streamForOrigFile;
}
public SaveDb(Context ctx, IKp2aApp app, OnFinish finish) public SaveDb(Context ctx, IKp2aApp app, OnFinish finish)
: base(finish) : base(finish)
@ -63,18 +87,25 @@ namespace keepass2android
IOConnectionInfo ioc = _app.GetDb().Ioc; IOConnectionInfo ioc = _app.GetDb().Ioc;
IFileStorage fileStorage = _app.GetFileStorage(ioc); IFileStorage fileStorage = _app.GetFileStorage(ioc);
if ((!_app.GetBooleanPreference(PreferenceKey.CheckForFileChangesOnSave)) if (_streamForOrigFile == null)
|| (_app.GetDb().KpDatabase.HashOfFileOnDisk == null)) //first time saving
{ {
PerformSaveWithoutCheck(fileStorage, ioc); if ((!_app.GetBooleanPreference(PreferenceKey.CheckForFileChangesOnSave))
Finish(true); || (_app.GetDb().KpDatabase.HashOfFileOnDisk == null)) //first time saving
return; {
PerformSaveWithoutCheck(fileStorage, ioc);
Finish(true);
return;
}
} }
if (fileStorage.CheckForFileChangeFast(ioc, _app.GetDb().LastFileVersion) //first try to use the fast change detection if (
|| (FileHashChanged(ioc, _app.GetDb().KpDatabase.HashOfFileOnDisk))) //if that fails, hash the file and compare: (_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:
)
{ {
//ask user... //ask user...
@ -183,12 +214,28 @@ namespace keepass2android
pwImp.MemoryProtection = pwDatabase.MemoryProtection.CloneDeep(); pwImp.MemoryProtection = pwDatabase.MemoryProtection.CloneDeep();
pwImp.MasterKey = pwDatabase.MasterKey; pwImp.MasterKey = pwDatabase.MasterKey;
KdbxFile kdbx = new KdbxFile(pwImp); KdbxFile kdbx = new KdbxFile(pwImp);
kdbx.Load(fileStorage.OpenFileForRead(ioc), KdbpFile.GetFormatToUse(ioc), null); kdbx.Load(GetStreamForBaseFile(fileStorage, ioc), KdbpFile.GetFormatToUse(ioc), null);
pwDatabase.MergeIn(pwImp, PwMergeMethod.Synchronize, null); pwDatabase.MergeIn(pwImp, PwMergeMethod.Synchronize, null);
} }
private Stream GetStreamForBaseFile(IFileStorage fileStorage, IOConnectionInfo ioc)
{
//if we have the original file already available: use it
if (_streamForOrigFile != null)
return _streamForOrigFile;
//if the file storage caches, it might return the local data in case of a conflict. This would result in data loss
// so we need to ensure we get the data from remote (only if the remote file is available. if not, we won't overwrite anything)
CachingFileStorage cachingFileStorage = fileStorage as CachingFileStorage;
if (cachingFileStorage != null)
{
return cachingFileStorage.OpenRemoteForReadIfAvailable(ioc);
}
return fileStorage.OpenFileForRead(ioc);
}
private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc) private void PerformSaveWithoutCheck(IFileStorage fileStorage, IOConnectionInfo ioc)
{ {
StatusLogger.UpdateSubMessage(""); StatusLogger.UpdateSubMessage("");

View File

@ -53,6 +53,7 @@
<ItemGroup> <ItemGroup>
<None Include="Additions\AboutAdditions.txt" /> <None Include="Additions\AboutAdditions.txt" />
<None Include="Jars\AboutJars.txt" /> <None Include="Jars\AboutJars.txt" />
<LibraryProjectZip Include="project.zip" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<TransformFile Include="Transforms\EnumFields.xml" /> <TransformFile Include="Transforms\EnumFields.xml" />
@ -60,9 +61,4 @@
<TransformFile Include="Transforms\Metadata.xml" /> <TransformFile Include="Transforms\Metadata.xml" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Novell\Xamarin.Android.Bindings.targets" /> <Import Project="$(MSBuildExtensionsPath)\Novell\Xamarin.Android.Bindings.targets" />
<ItemGroup>
<LibraryProjectZip Include="..\java\KP2ASoftKeyboard\project.zip">
<Link>project.zip</Link>
</LibraryProjectZip>
</ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,5 @@
<metadata> <metadata>
<remove-node path="/api/package[@name='keepass2android.softkeyboard']/class[@name='KP2AKeyboard']" />
<!-- <!--
This sample removes the class: android.support.v4.content.AsyncTaskLoader.LoadTask: This sample removes the class: android.support.v4.content.AsyncTaskLoader.LoadTask:
<remove-node path="/api/package[@name='android.support.v4.content']/class[@name='AsyncTaskLoader.LoadTask']" /> <remove-node path="/api/package[@name='android.support.v4.content']/class[@name='AsyncTaskLoader.LoadTask']" />

View File

@ -15,6 +15,7 @@
<AndroidApplication>true</AndroidApplication> <AndroidApplication>true</AndroidApplication>
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile> <AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies> <GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -58,16 +59,19 @@
<ItemGroup> <ItemGroup>
<Compile Include="ProgressDialogStub.cs" /> <Compile Include="ProgressDialogStub.cs" />
<Compile Include="TestBase.cs" /> <Compile Include="TestBase.cs" />
<Compile Include="TestCacheSupervisor.cs" />
<Compile Include="TestDrawableFactory.cs" /> <Compile Include="TestDrawableFactory.cs" />
<Compile Include="TestCreateDb.cs" /> <Compile Include="TestCreateDb.cs" />
<Compile Include="MainActivity.cs" /> <Compile Include="MainActivity.cs" />
<Compile Include="Resources\Resource.Designer.cs" /> <Compile Include="Resources\Resource.Designer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestFileStorage.cs" />
<Compile Include="TestKp2aApp.cs" /> <Compile Include="TestKp2aApp.cs" />
<Compile Include="TestLoadDb.cs" /> <Compile Include="TestLoadDb.cs" />
<Compile Include="TestLoadDbCredentials.cs" /> <Compile Include="TestLoadDbCredentials.cs" />
<Compile Include="TestCachingFileStorage.cs" /> <Compile Include="TestCachingFileStorage.cs" />
<Compile Include="TestSaveDb.cs" /> <Compile Include="TestSaveDb.cs" />
<Compile Include="TestSynchronizeCachedDatabase.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Resources\AboutResources.txt" /> <None Include="Resources\AboutResources.txt" />
@ -96,6 +100,9 @@
<Name>MonoDroidUnitTesting</Name> <Name>MonoDroidUnitTesting</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<TransformFile Include="Properties\AndroidManifest.xml" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it. <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets. Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -18,8 +18,8 @@ namespace Kp2aUnitTests
{ {
TestRunner runner = new TestRunner(); TestRunner runner = new TestRunner();
// Run all tests from this assembly // Run all tests from this assembly
//runner.AddTests(Assembly.GetExecutingAssembly()); runner.AddTests(Assembly.GetExecutingAssembly());
runner.AddTests(new List<Type> { typeof(TestCachingFileStorage) }); //runner.AddTests(new List<Type> { typeof(TestSynchronizeCachedDatabase) });
//runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote")); //runner.AddTests(typeof(TestCachingFileStorage).GetMethod("TestSaveToRemote"));
//runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly")); //runner.AddTests(typeof(TestLoadDb).GetMethod("TestLoadKdbpWithPasswordOnly"));
//runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles")); //runner.AddTests(typeof(TestSaveDb).GetMethod("TestLoadKdbxAndSaveKdbp_TestIdenticalFiles"));

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="8" />
<application></application>
</manifest>

View File

@ -61,9 +61,9 @@ namespace Kp2aUnitTests
get { return DefaultDirectory + "savedWithDesktop/"; } get { return DefaultDirectory + "savedWithDesktop/"; }
} }
protected IKp2aApp LoadDatabase(string filename, string password, string keyfile) protected TestKp2aApp LoadDatabase(string filename, string password, string keyfile)
{ {
IKp2aApp app = new TestKp2aApp(); var app = CreateTestKp2aApp();
app.CreateNewDatabase(); app.CreateNewDatabase();
bool loadSuccesful = false; bool loadSuccesful = false;
LoadDb task = new LoadDb(app, new IOConnectionInfo() { Path = filename }, password, keyfile, new ActionOnFinish((success, message) => LoadDb task = new LoadDb(app, new IOConnectionInfo() { Path = filename }, password, keyfile, new ActionOnFinish((success, message) =>
@ -81,6 +81,12 @@ namespace Kp2aUnitTests
return app; return app;
} }
protected virtual TestKp2aApp CreateTestKp2aApp()
{
TestKp2aApp app = new TestKp2aApp();
return app;
}
protected void SaveDatabase(IKp2aApp app) protected void SaveDatabase(IKp2aApp app)
{ {
bool saveSuccesful = TrySaveDatabase(app); bool saveSuccesful = TrySaveDatabase(app);
@ -104,16 +110,16 @@ namespace Kp2aUnitTests
return saveSuccesful; return saveSuccesful;
} }
protected IKp2aApp SetupAppWithDefaultDatabase() protected TestKp2aApp SetupAppWithDefaultDatabase()
{ {
string filename = DefaultFilename; string filename = DefaultFilename;
return SetupAppWithDatabase(filename); return SetupAppWithDatabase(filename);
} }
protected IKp2aApp SetupAppWithDatabase(string filename) protected TestKp2aApp SetupAppWithDatabase(string filename)
{ {
IKp2aApp app = new TestKp2aApp(); TestKp2aApp app = CreateTestKp2aApp();
IOConnectionInfo ioc = new IOConnectionInfo {Path = filename}; IOConnectionInfo ioc = new IOConnectionInfo {Path = filename};
Database db = app.CreateNewDatabase(); Database db = app.CreateNewDatabase();

View File

@ -0,0 +1,29 @@
using System;
using KeePassLib.Serialization;
using keepass2android.Io;
namespace Kp2aUnitTests
{
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;
}
}
}

View File

@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -20,118 +19,6 @@ namespace Kp2aUnitTests
private string _defaultCacheFileContents = "default contents"; private string _defaultCacheFileContents = "default contents";
private TestCacheSupervisor _testCacheSupervisor; 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);
}
}
/// <summary> /// <summary>
/// Tests correct behavior in case that either remote or cache are not available /// Tests correct behavior in case that either remote or cache are not available
/// </summary> /// </summary>

View File

@ -0,0 +1,96 @@
using System;
using System.IO;
using KeePassLib.Serialization;
using keepass2android.Io;
namespace Kp2aUnitTests
{
internal 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);
}
}
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Android.App;
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
using KeePassLib.Serialization; using KeePassLib.Serialization;
@ -22,6 +23,7 @@ namespace Kp2aUnitTests
private YesNoCancelResult _yesNoCancelResult = YesNoCancelResult.Yes; private YesNoCancelResult _yesNoCancelResult = YesNoCancelResult.Yes;
private Dictionary<PreferenceKey, bool> _preferences = new Dictionary<PreferenceKey, bool>(); private Dictionary<PreferenceKey, bool> _preferences = new Dictionary<PreferenceKey, bool>();
public void SetShutdown() public void SetShutdown()
{ {
@ -40,7 +42,7 @@ namespace Kp2aUnitTests
public Database CreateNewDatabase() public Database CreateNewDatabase()
{ {
TestDrawableFactory testDrawableFactory = new TestDrawableFactory(); TestDrawableFactory testDrawableFactory = new TestDrawableFactory();
_db = new Database(testDrawableFactory, new TestKp2aApp()); _db = new Database(testDrawableFactory, this);
return _db; return _db;
} }
@ -98,6 +100,9 @@ namespace Kp2aUnitTests
public Handler UiThreadHandler { public Handler UiThreadHandler {
get { return null; } //ensure everything runs in the same thread. Otherwise the OnFinish-callback would run after the test has already finished (with failure) get { return null; } //ensure everything runs in the same thread. Otherwise the OnFinish-callback would run after the test has already finished (with failure)
} }
public IFileStorage FileStorage { get; set; }
public IProgressDialog CreateProgressDialog(Context ctx) public IProgressDialog CreateProgressDialog(Context ctx)
{ {
return new ProgressDialogStub(); return new ProgressDialogStub();
@ -105,7 +110,20 @@ namespace Kp2aUnitTests
public IFileStorage GetFileStorage(IOConnectionInfo iocInfo) public IFileStorage GetFileStorage(IOConnectionInfo iocInfo)
{ {
return new BuiltInFileStorage(); return FileStorage;
}
public bool TriggerReloadCalled;
public TestKp2aApp()
{
FileStorage = new BuiltInFileStorage();
}
public void TriggerReload(Context ctx)
{
TriggerReloadCalled = true;
} }
public void SetYesNoCancelResult(YesNoCancelResult yesNoCancelResult) public void SetYesNoCancelResult(YesNoCancelResult yesNoCancelResult)

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Android.App;
using Android.OS;
using KeePassLib;
using KeePassLib.Keys;
using KeePassLib.Serialization;
using KeePassLib.Utility;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using keepass2android;
using keepass2android.Io;
namespace Kp2aUnitTests
{
[TestClass]
internal class TestSynchronizeCachedDatabase : TestBase
{
private TestCacheSupervisor _testCacheSupervisor = new TestCacheSupervisor();
private TestFileStorage _testFileStorage = new TestFileStorage();
[TestMethod]
public void TestTodos()
{
Assert.IsFalse(true, "Wird immer ManagedTransform benutzt??");
Assert.IsFalse(true, "TODOs in SyncDb");
Assert.IsFalse(true, "FileNotFound");
Assert.IsFalse(true, "Test merge files");
}
protected override TestKp2aApp CreateTestKp2aApp()
{
TestKp2aApp app = base.CreateTestKp2aApp();
app.FileStorage = new CachingFileStorage(_testFileStorage, "/mnt/sdcard/kp2atest/cache/", _testCacheSupervisor);
return app;
}
/// <summary>
/// Tests that synchronizing works if
/// - no changes in remote and local db
/// - remote is offline -> error
/// - only local file was changed
/// </summary>
[TestMethod]
public void TestSimpleSyncCases()
{
//create the default database:
TestKp2aApp app = SetupAppWithDefaultDatabase();
IOConnection.DeleteFile(new IOConnectionInfo {Path = DefaultFilename});
//save it and reload it so we have a base version ("remote" and in the cache)
SaveDatabase(app);
app = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
string resultMessage;
bool wasSuccessful;
//sync without changes on any side:
Synchronize(app, out wasSuccessful, out resultMessage);
Assert.IsTrue(wasSuccessful);
Assert.AreEqual(resultMessage, app.GetResourceString(UiStringKey.FilesInSync));
//go offline:
_testFileStorage.Offline = true;
//sync when offline (->error)
Synchronize(app, out wasSuccessful, out resultMessage);
Assert.IsFalse(wasSuccessful);
Assert.AreEqual(resultMessage, "offline");
//modify the database by adding a group:
app.GetDb().KpDatabase.RootGroup.AddGroup(new PwGroup(true, true, "TestGroup", PwIcon.Apple), true);
//save the database again (will be saved locally only)
SaveDatabase(app);
Assert.IsTrue(_testCacheSupervisor.CouldntSaveToRemoteCalled);
_testCacheSupervisor.CouldntSaveToRemoteCalled = false;
//go online again:
_testFileStorage.Offline = false;
//sync with local changes only (-> upload):
Synchronize(app, out wasSuccessful, out resultMessage);
Assert.IsTrue(wasSuccessful);
Assert.AreEqual(resultMessage, app.GetResourceString(UiStringKey.SynchronizedDatabaseSuccessfully));
//ensure both files are identical and up to date now:
_testFileStorage.Offline = true;
var appOfflineLoaded = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
_testCacheSupervisor.CouldntOpenFromRemoteCalled = false;
_testFileStorage.Offline = false;
var appRemoteLoaded = LoadDatabase(DefaultFilename, DefaultPassword, DefaultKeyfile);
Assert.IsFalse(_testCacheSupervisor.CouldntOpenFromRemoteCalled);
AssertDatabasesAreEqual(app.GetDb().KpDatabase, appOfflineLoaded.GetDb().KpDatabase);
AssertDatabasesAreEqual(app.GetDb().KpDatabase, appRemoteLoaded.GetDb().KpDatabase);
}
private void Synchronize(TestKp2aApp app, out bool wasSuccessful, out string resultMessage)
{
bool success = false;
string result = null;
var sync = new SynchronizeCachedDatabase(Application.Context, app, new ActionOnFinish((_success, _result) =>
{
success = _success;
result = _result;
}));
sync.Run();
wasSuccessful = success;
resultMessage = result;
}
}
}

View File

@ -270,7 +270,7 @@
* External database changes are detected and merged when saving\n * External database changes are detected and merged when saving\n
* Improved loading performance\n * Improved loading performance\n
* Improved search toolbar with suggestions\n * Improved search toolbar with suggestions\n
* New App logo! * New App logo!\n
* Added support for .kdbp format for faster loading/saving\n * Added support for .kdbp format for faster loading/saving\n
* Improved editing of extra strings and hidden display when protected\n * Improved editing of extra strings and hidden display when protected\n
Thanks to Alex Vallat for his code contributions!\n Thanks to Alex Vallat for his code contributions!\n

View File

@ -125,33 +125,39 @@ namespace keepass2android
{ {
activity.SetResult(KeePass.ExitReloadDb); activity.SetResult(KeePass.ExitReloadDb);
activity.Finish(); activity.Finish();
//todo: return?
} }
AlertDialog.Builder builder = new AlertDialog.Builder(activity); AskForReload(activity);
builder.SetTitle(activity.GetString(Resource.String.AskReloadFile_title));
builder.SetMessage(activity.GetString(Resource.String.AskReloadFile));
builder.SetPositiveButton(activity.GetString(Android.Resource.String.Yes),
(dlgSender, dlgEvt) =>
{
_db.ReloadRequested = true;
activity.SetResult(KeePass.ExitReloadDb);
activity.Finish();
});
builder.SetNegativeButton(activity.GetString(Android.Resource.String.No), (dlgSender, dlgEvt) =>
{
});
Dialog dialog = builder.Create();
dialog.Show();
} }
} }
public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile) private void AskForReload(Activity activity)
{
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.SetTitle(activity.GetString(Resource.String.AskReloadFile_title));
builder.SetMessage(activity.GetString(Resource.String.AskReloadFile));
builder.SetPositiveButton(activity.GetString(Android.Resource.String.Yes),
(dlgSender, dlgEvt) =>
{
_db.ReloadRequested = true;
activity.SetResult(KeePass.ExitReloadDb);
activity.Finish();
});
builder.SetNegativeButton(activity.GetString(Android.Resource.String.No), (dlgSender, dlgEvt) =>
{
});
Dialog dialog = builder.Create();
dialog.Show();
}
public void StoreOpenedFileAsRecent(IOConnectionInfo ioc, string keyfile)
{ {
FileDbHelper.CreateFile(ioc, keyfile); FileDbHelper.CreateFile(ioc, keyfile);
} }
@ -250,6 +256,11 @@ namespace keepass2android
return new BuiltInFileStorage(); return new BuiltInFileStorage();
} }
public void TriggerReload(Context ctx)
{
AskForReload((Activity)ctx);
}
internal void OnTerminate() internal void OnTerminate()
{ {

View File

@ -644,6 +644,7 @@
<Import Project="$(MSBuildExtensionsPath)\Novell\Novell.MonoDroid.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Novell\Novell.MonoDroid.CSharp.targets" />
<ItemGroup> <ItemGroup>
<Folder Include="Assets\" /> <Folder Include="Assets\" />
<Folder Include="SupportLib\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\KeePassLib2Android\KeePassLib2Android.csproj"> <ProjectReference Include="..\KeePassLib2Android\KeePassLib2Android.csproj">
@ -672,9 +673,6 @@
</Properties> </Properties>
</MonoDevelop> </MonoDevelop>
</ProjectExtensions> </ProjectExtensions>
<ItemGroup>
<AndroidJavaLibrary Include="SupportLib\android-support-v4.jar" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidNativeLibrary Include="..\java\kp2akeytransform\libs\armeabi\libfinal-key.so"> <AndroidNativeLibrary Include="..\java\kp2akeytransform\libs\armeabi\libfinal-key.so">
<Link>libs\armeabi\libfinal-key.so</Link> <Link>libs\armeabi\libfinal-key.so</Link>