From f157329cab85267a0fdef52d2769c25edfce37df Mon Sep 17 00:00:00 2001 From: AlexVallat Date: Sat, 15 Jun 2013 20:12:46 +0100 Subject: [PATCH 1/6] Commit of initial kdbx load perf testing and optimisations --- .../Collections/ProtectedBinaryDictionary.cs | 6 + .../Collections/ProtectedStringDictionary.cs | 5 +- .../Interfaces/ITimeLogger.cs | 14 + .../KeePassLib2Android.csproj | 5 +- src/KeePassLib2Android/PwEntry.cs | 62 ++- src/KeePassLib2Android/PwGroup.cs | 73 +++- .../AsynchronousBufferedXmlReader.cs | 367 ++++++++++++++++++ .../Serialization/KdbxFile.Read.Streamed.cs | 10 +- .../Serialization/KdbxFile.Read.cs | 49 ++- src/KeePassLib2Android/Utility/TimeUtil.cs | 16 + src/keepass2android/Database.cs | 12 +- src/keepass2android/EntryActivity.cs | 14 +- 12 files changed, 587 insertions(+), 46 deletions(-) create mode 100644 src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs diff --git a/src/KeePassLib2Android/Collections/ProtectedBinaryDictionary.cs b/src/KeePassLib2Android/Collections/ProtectedBinaryDictionary.cs index c0df6707..0e8230ce 100644 --- a/src/KeePassLib2Android/Collections/ProtectedBinaryDictionary.cs +++ b/src/KeePassLib2Android/Collections/ProtectedBinaryDictionary.cs @@ -40,8 +40,14 @@ namespace KeePassLib.Collections IDeepCloneable, IEnumerable> { + /* private SortedDictionary m_vBinaries = new SortedDictionary(); + */ + + private Dictionary m_vBinaries = + new Dictionary(); + /// /// Get the number of binaries in this entry. diff --git a/src/KeePassLib2Android/Collections/ProtectedStringDictionary.cs b/src/KeePassLib2Android/Collections/ProtectedStringDictionary.cs index ef03ff78..dffede11 100644 --- a/src/KeePassLib2Android/Collections/ProtectedStringDictionary.cs +++ b/src/KeePassLib2Android/Collections/ProtectedStringDictionary.cs @@ -40,8 +40,9 @@ namespace KeePassLib.Collections IDeepCloneable, IEnumerable> { - private SortedDictionary m_vStrings = - new SortedDictionary(); + /*private SortedDictionary m_vStrings = + new SortedDictionary();*/ + private Dictionary m_vStrings = new Dictionary(); /// /// Get the number of strings in this entry. diff --git a/src/KeePassLib2Android/Interfaces/ITimeLogger.cs b/src/KeePassLib2Android/Interfaces/ITimeLogger.cs index 86a6b85d..361abe7a 100644 --- a/src/KeePassLib2Android/Interfaces/ITimeLogger.cs +++ b/src/KeePassLib2Android/Interfaces/ITimeLogger.cs @@ -101,5 +101,19 @@ namespace KeePassLib.Interfaces /// /// Update last modification time. void Touch(bool bModified); + + #region Set times lazily + // Passing xml datetime string to be parsed only on demand + + void SetLazyLastModificationTime(string xmlDateTime); + + void SetLazyCreationTime(string xmlDateTime); + + void SetLazyLastAccessTime(string xmlDateTime); + + void SetLazyExpiryTime(string xmlDateTime); + + void SetLazyLocationChanged(string xmlDateTime); + #endregion } } diff --git a/src/KeePassLib2Android/KeePassLib2Android.csproj b/src/KeePassLib2Android/KeePassLib2Android.csproj index b01ed8b8..a7fdc014 100644 --- a/src/KeePassLib2Android/KeePassLib2Android.csproj +++ b/src/KeePassLib2Android/KeePassLib2Android.csproj @@ -1,4 +1,4 @@ - + Debug @@ -20,7 +20,7 @@ full False bin\Debug - DEBUG; + TRACE;DEBUG; prompt 4 False @@ -113,6 +113,7 @@ + diff --git a/src/KeePassLib2Android/PwEntry.cs b/src/KeePassLib2Android/PwEntry.cs index b26bec6f..8e587b5a 100644 --- a/src/KeePassLib2Android/PwEntry.cs +++ b/src/KeePassLib2Android/PwEntry.cs @@ -40,6 +40,7 @@ namespace KeePassLib private PwUuid m_uuid = PwUuid.Zero; private PwGroup m_pParentGroup = null; private DateTime m_tParentGroupLastMod = PwDefs.DtDefaultNow; + private string m_tParentGroupLastModLazy; private ProtectedStringDictionary m_listStrings = new ProtectedStringDictionary(); private ProtectedBinaryDictionary m_listBinaries = new ProtectedBinaryDictionary(); @@ -56,6 +57,12 @@ namespace KeePassLib private DateTime m_tLastMod = PwDefs.DtDefaultNow; private DateTime m_tLastAccess = PwDefs.DtDefaultNow; private DateTime m_tExpire = PwDefs.DtDefaultNow; + + private string m_tCreationLazy; + private string m_tLastModLazy; + private string m_tLastAccessLazy; + private string m_tExpireLazy; + private bool m_bExpires = false; private ulong m_uUsageCount = 0; @@ -92,8 +99,13 @@ namespace KeePassLib /// public DateTime LocationChanged { - get { return m_tParentGroupLastMod; } - set { m_tParentGroupLastMod = value; } + get { return GetLazyTime(ref m_tParentGroupLastModLazy, ref m_tParentGroupLastMod); } + set { m_tParentGroupLastMod = value; m_tParentGroupLastModLazy = null; } + } + + public void SetLazyLocationChanged(string xmlDateTime) + { + m_tParentGroupLastModLazy = xmlDateTime; } /// @@ -195,8 +207,13 @@ namespace KeePassLib /// public DateTime CreationTime { - get { return m_tCreation; } - set { m_tCreation = value; } + get { return GetLazyTime(ref m_tCreationLazy, ref m_tCreation); } + set { m_tCreation = value; m_tCreationLazy = null; } + } + + public void SetLazyCreationTime(string xmlDateTime) + { + m_tCreationLazy = xmlDateTime; } /// @@ -204,8 +221,13 @@ namespace KeePassLib /// public DateTime LastAccessTime { - get { return m_tLastAccess; } - set { m_tLastAccess = value; } + get { return GetLazyTime(ref m_tLastAccessLazy, ref m_tLastAccess); } + set { m_tLastAccess = value; m_tLastAccessLazy = null; } + } + + public void SetLazyLastAccessTime(string xmlDateTime) + { + m_tLastAccessLazy = xmlDateTime; } /// @@ -213,8 +235,13 @@ namespace KeePassLib /// public DateTime LastModificationTime { - get { return m_tLastMod; } - set { m_tLastMod = value; } + get { return GetLazyTime(ref m_tLastModLazy, ref m_tLastMod); } + set { m_tLastMod = value; m_tLastModLazy = null; } + } + + public void SetLazyLastModificationTime(string xmlDateTime) + { + m_tLastModLazy = xmlDateTime; } /// @@ -223,8 +250,13 @@ namespace KeePassLib /// public DateTime ExpiryTime { - get { return m_tExpire; } - set { m_tExpire = value; } + get { return GetLazyTime(ref m_tExpireLazy, ref m_tExpire); } + set { m_tExpire = value; m_tExpireLazy = null; } + } + + public void SetLazyExpiryTime(string xmlDateTime) + { + m_tExpireLazy = xmlDateTime; } /// @@ -844,6 +876,16 @@ namespace KeePassLib } } } + + private DateTime GetLazyTime(ref string lazyTime, ref DateTime dateTime) + { + if (lazyTime != null) + { + dateTime = TimeUtil.DeserializeUtcOrDefault(lazyTime, dateTime); + lazyTime = null; + } + return dateTime; + } } public sealed class PwEntryComparer : IComparer diff --git a/src/KeePassLib2Android/PwGroup.cs b/src/KeePassLib2Android/PwGroup.cs index 791e0fbe..b5b20c25 100644 --- a/src/KeePassLib2Android/PwGroup.cs +++ b/src/KeePassLib2Android/PwGroup.cs @@ -42,6 +42,7 @@ namespace KeePassLib private PwObjectList m_listEntries = new PwObjectList(); private PwGroup m_pParentGroup = null; private DateTime m_tParentGroupLastMod = PwDefs.DtDefaultNow; + private string m_tParentGroupLastModLazy; private PwUuid m_uuid = PwUuid.Zero; private string m_strName = string.Empty; @@ -54,6 +55,12 @@ namespace KeePassLib private DateTime m_tLastMod = PwDefs.DtDefaultNow; private DateTime m_tLastAccess = PwDefs.DtDefaultNow; private DateTime m_tExpire = PwDefs.DtDefaultNow; + + private string m_tCreationLazy; + private string m_tLastModLazy; + private string m_tLastAccessLazy; + private string m_tExpireLazy; + private bool m_bExpires = false; private ulong m_uUsageCount = 0; @@ -146,8 +153,13 @@ namespace KeePassLib /// public DateTime LocationChanged { - get { return m_tParentGroupLastMod; } - set { m_tParentGroupLastMod = value; } + get { return GetLazyTime(ref m_tParentGroupLastModLazy, ref m_tParentGroupLastMod); } + set { m_tParentGroupLastMod = value; m_tParentGroupLastModLazy = null; } + } + + public void SetLazyLocationChanged(string xmlDateTime) + { + m_tParentGroupLastModLazy = xmlDateTime; } /// @@ -165,17 +177,13 @@ namespace KeePassLib /// public DateTime CreationTime { - get { return m_tCreation; } - set { m_tCreation = value; } + get { return GetLazyTime(ref m_tCreationLazy, ref m_tCreation); } + set { m_tCreation = value; m_tCreationLazy = null; } } - /// - /// The date/time when this group was last modified. - /// - public DateTime LastModificationTime + public void SetLazyCreationTime(string xmlDateTime) { - get { return m_tLastMod; } - set { m_tLastMod = value; } + m_tCreationLazy = xmlDateTime; } /// @@ -183,17 +191,42 @@ namespace KeePassLib /// public DateTime LastAccessTime { - get { return m_tLastAccess; } - set { m_tLastAccess = value; } + get { return GetLazyTime(ref m_tLastAccessLazy, ref m_tLastAccess); } + set { m_tLastAccess = value; m_tLastAccessLazy = null; } + } + + public void SetLazyLastAccessTime(string xmlDateTime) + { + m_tLastAccessLazy = xmlDateTime; } /// - /// The date/time when this group expires. + /// The date/time when this group was last modified. + /// + public DateTime LastModificationTime + { + get { return GetLazyTime(ref m_tLastModLazy, ref m_tLastMod); } + set { m_tLastMod = value; m_tLastModLazy = null; } + } + + public void SetLazyLastModificationTime(string xmlDateTime) + { + m_tLastModLazy = xmlDateTime; + } + + /// + /// The date/time when this group expires. Use the Expires property + /// to specify if the group does actually expire or not. /// public DateTime ExpiryTime { - get { return m_tExpire; } - set { m_tExpire = value; } + get { return GetLazyTime(ref m_tExpireLazy, ref m_tExpire); } + set { m_tExpire = value; m_tExpireLazy = null; } + } + + public void SetLazyExpiryTime(string xmlDateTime) + { + m_tExpireLazy = xmlDateTime; } /// @@ -1422,6 +1455,16 @@ namespace KeePassLib } m_listGroups.Clear(); } + + private DateTime GetLazyTime(ref string lazyTime, ref DateTime dateTime) + { + if (lazyTime != null) + { + dateTime = TimeUtil.DeserializeUtcOrDefault(lazyTime, m_tLastMod); + lazyTime = null; + } + return dateTime; + } } public sealed class PwGroupComparer : IComparer diff --git a/src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs b/src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs new file mode 100644 index 00000000..d10ba1cb --- /dev/null +++ b/src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Threading; +using System.Xml; +using System.Diagnostics; + +namespace KeePassLib.Serialization +{ + public class AsynchronousBufferedXmlReader : XmlReader + { + /// + /// An element which indicates the end of the XML document has been reached. + /// + private static readonly Element EndMarker = new Element(); + + /// + /// The next buffered element available for reading. + /// Volatility: only read/written to by non-buffering thread. Passed to the buffer thread as an initial parameter. + /// + Element mBufferQueueHead = new Element(); // Start off with the pre-document element. No content, yet. + + private readonly Thread mWorkerThread; + private readonly AutoResetEvent mWaitForBuffer = new AutoResetEvent(false); + /// + /// True while the reader thread is stalled waiting for buffering. + /// Volaitlity: Only written by read thread. Only read by buffer thread + /// + private volatile bool mWaitingForBuffer; + +#if TRACE + private Stopwatch mReadWaitTimer = new Stopwatch(); + private Stopwatch mBufferCompletedTimer = new Stopwatch(); +#endif + + /// + /// Testing helper method + /// + /// + /// + public static XmlReader FullyBuffer(Stream input) + { + var reader = new AsynchronousBufferedXmlReader(); + reader.ReadStreamWorker(input); + return reader; + } + + private AsynchronousBufferedXmlReader() + { + // Once the end is reached, it stays there. + EndMarker.NextElement = EndMarker; + } + + public AsynchronousBufferedXmlReader(Stream input) : this() + { + mWorkerThread = new Thread(ReadStreamWorker) { Name = GetType().Name }; + mWorkerThread.Start(input); + } + + #region Buffering + private void ReadStreamWorker(object state) + { + var input = (Stream)state; + + var xr = XmlReader.Create(input, KdbxFile.CreateStdXmlReaderSettings()); + + /// + /// The last buffered element available for reading. + /// + Element bufferQueueTail = mBufferQueueHead; + + /// + /// The element currently being buffered. Not yet available for reading. + /// + Element currentElement = null; + + while (xr.Read()) + { + switch (xr.NodeType) + { + case XmlNodeType.Element: + // Start a new element + if (currentElement != null) + { + // Add the previous current element to the tail of the buffer + bufferQueueTail.NextElement = currentElement; + bufferQueueTail = currentElement; + if (mWaitingForBuffer) mWaitForBuffer.Set(); // Signal that a new element is available in the buffer + } + + currentElement = new Element { Name = xr.Name }; + + // Process attributes - current optimisation, all elements have 0 or 1 attribute + if (xr.MoveToNextAttribute()) + { +#if DEBUG + Debug.Assert(xr.AttributeCount == 1); + currentElement.AttributeName = xr.Name; +#endif + currentElement.AttributeValue = xr.Value; + } + + currentElement.IsEmpty = xr.IsEmptyElement; + + break; + + case XmlNodeType.Text: + currentElement.Value = xr.Value; + currentElement.IsEmpty = true; // Mark as empty because it will have no end element written for it + break; + + case XmlNodeType.EndElement: + Debug.Assert(currentElement != null, "Ending an element that was never started"); + + // If this is an element with children (not one with a value) add an end element marker to the queue + if (currentElement.Value == null || currentElement.Name != xr.Name) + { + bufferQueueTail.NextElement = currentElement; + bufferQueueTail = currentElement; + if (mWaitingForBuffer) mWaitForBuffer.Set(); // Signal that a new element is available in the buffer + + currentElement = new Element { Name = xr.Name, IsEndElement = true }; + } + break; + } + } + + // Conclude the document, add the final element to the buffer and mark the ending + currentElement.NextElement = EndMarker; + bufferQueueTail.NextElement = currentElement; + bufferQueueTail = currentElement; + mWaitForBuffer.Set(); // Signal that final element is available in the buffer (regardless of wait flag, to avoid race condition) +#if TRACE + mBufferCompletedTimer.Start(); +#endif + } + #endregion + + private class Element + { + /// + /// Link to the next buffered element. + /// Volatility: Written to by buffer thread only. Read by both threads + /// + public volatile Element NextElement; + + public string Name; + + /// + /// If this element marks the end of an xml element with child nodes, the IsEndElement will be true, and Value must be null. + /// + public bool IsEndElement; + + /// + /// Set true if this represents an empty element + /// + public bool IsEmpty; + + /// + /// If Value is non-null, then there will be no corresponding Element with IsEndElement created. + /// + public string Value; + + // Currently KDBX has a maximum of one attribute per element, so no need for a dictionary here, and the name is only used for debug asserts +#if DEBUG + public string AttributeName; +#endif + public string AttributeValue; + } + + #region Custom XmlReader implementation for usage by KdbxFile only + public override bool Read() + { + Element nextElement; + while ((nextElement = mBufferQueueHead.NextElement) == null) + { +#if TRACE + mReadWaitTimer.Start(); +#endif + mWaitingForBuffer = true; + mWaitForBuffer.WaitOne(); + mWaitingForBuffer = false; + +#if TRACE + mReadWaitTimer.Stop(); +#endif + } + mBufferQueueHead = mBufferQueueHead.NextElement; + + +#if TRACE + if (mBufferQueueHead == EndMarker) + { + Debug.WriteLine(String.Format("Asynchronous Buffered XmlReader waited for a total of: {0}ms, buffer completed {1}ms ahead of read", mReadWaitTimer.ElapsedMilliseconds, mBufferCompletedTimer.ElapsedMilliseconds)); + } +#endif + return mBufferQueueHead != EndMarker; + } + + public override string ReadElementString() + { + var result = mBufferQueueHead.Value ?? String.Empty; // ReadElementString returns empty strings for null content + Read(); // Read element string always skips to the start of the next element + return result; + } + + public override XmlNodeType NodeType + { + get + { + return mBufferQueueHead.IsEndElement ? XmlNodeType.EndElement : XmlNodeType.Element; + } + } + + public override bool IsEmptyElement + { + get + { + return mBufferQueueHead.IsEmpty; + } + } + + public override string Name + { + get + { + return mBufferQueueHead.Name; + } + } + + public override bool HasAttributes + { + get + { + return mBufferQueueHead.AttributeValue != null; + } + } + + public override bool MoveToAttribute(string name) + { +#if DEBUG + Debug.Assert(mBufferQueueHead.AttributeName == name); +#endif + + return true; + } + + public override string Value + { + get + { + return mBufferQueueHead.AttributeValue; + } + } + + + public override bool MoveToElement() + { + return true; + } + #endregion + + #region Unimplemented XmlReader overrides + + public override int AttributeCount + { + get { throw new NotImplementedException(); } + } + + public override string BaseURI + { + get { throw new NotImplementedException(); } + } + + public override void Close() + { + throw new NotImplementedException(); + } + + public override int Depth + { + get { throw new NotImplementedException(); } + } + + public override bool EOF + { + get { throw new NotImplementedException(); } + } + + public override string GetAttribute(int i) + { + throw new NotImplementedException(); + } + + public override string GetAttribute(string name, string namespaceURI) + { + throw new NotImplementedException(); + } + + public override string GetAttribute(string name) + { + throw new NotImplementedException(); + } + + public override bool HasValue + { + get { throw new NotImplementedException(); } + } + + public override string LocalName + { + get { throw new NotImplementedException(); } + } + + public override string LookupNamespace(string prefix) + { + throw new NotImplementedException(); + } + + public override bool MoveToAttribute(string name, string ns) + { + throw new NotImplementedException(); + } + + public override bool MoveToFirstAttribute() + { + throw new NotImplementedException(); + } + + public override bool MoveToNextAttribute() + { + throw new NotImplementedException(); + } + + public override XmlNameTable NameTable + { + get { throw new NotImplementedException(); } + } + + public override string NamespaceURI + { + get { throw new NotImplementedException(); } + } + + public override string Prefix + { + get { throw new NotImplementedException(); } + } + + public override bool ReadAttributeValue() + { + throw new NotImplementedException(); + } + + public override ReadState ReadState + { + get { throw new NotImplementedException(); } + } + + public override void ResolveEntity() + { + throw new NotImplementedException(); + } + #endregion + } +} diff --git a/src/KeePassLib2Android/Serialization/KdbxFile.Read.Streamed.cs b/src/KeePassLib2Android/Serialization/KdbxFile.Read.Streamed.cs index c572f6ca..4502fb2b 100644 --- a/src/KeePassLib2Android/Serialization/KdbxFile.Read.Streamed.cs +++ b/src/KeePassLib2Android/Serialization/KdbxFile.Read.Streamed.cs @@ -497,19 +497,19 @@ namespace KeePassLib.Serialization Debug.Assert(tl != null); if(xr.Name == ElemLastModTime) - tl.LastModificationTime = ReadTime(xr); + tl.SetLazyLastModificationTime(ReadString(xr)); else if(xr.Name == ElemCreationTime) - tl.CreationTime = ReadTime(xr); + tl.SetLazyCreationTime(ReadString(xr)); else if(xr.Name == ElemLastAccessTime) - tl.LastAccessTime = ReadTime(xr); + tl.SetLazyLastAccessTime(ReadString(xr)); else if(xr.Name == ElemExpiryTime) - tl.ExpiryTime = ReadTime(xr); + tl.SetLazyExpiryTime(ReadString(xr)); else if(xr.Name == ElemExpires) tl.Expires = ReadBool(xr, false); else if(xr.Name == ElemUsageCount) tl.UsageCount = ReadULong(xr, 0); else if(xr.Name == ElemLocationChanged) - tl.LocationChanged = ReadTime(xr); + tl.SetLazyLocationChanged(ReadString(xr)); else ReadUnknown(xr); break; diff --git a/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs b/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs index 03456962..65c2aa55 100644 --- a/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs +++ b/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs @@ -70,6 +70,8 @@ namespace KeePassLib.Serialization Debug.Assert(sSource != null); if(sSource == null) throw new ArgumentNullException("sSource"); + var stopWatch = Stopwatch.StartNew(); + m_format = kdbFormat; m_slLogger = slLogger; @@ -127,7 +129,33 @@ namespace KeePassLib.Serialization } else m_randomStream = null; // No random stream for plain-text files - ReadXmlStreamed(readerStream, hashedStream); + Debug.WriteLine(String.Format("Crypto setup: {0}ms", stopWatch.ElapsedMilliseconds)); + stopWatch.Restart(); + + /* + var memStream = new MemoryStream((int)hashedStream.Length); + CopyStream(readerStream, memStream); + readerStream = memStream; + Debug.WriteLine(String.Format("CopyStream: {0}ms", stopWatch.ElapsedMilliseconds)); + + + stopWatch.Restart(); + */ + + //var bufferedXmlReader = AsynchronousBufferedXmlReader.FullyBuffer(readerStream); + //Debug.WriteLine(String.Format("ReadToBuffer: {0}ms", stopWatch.ElapsedMilliseconds)); + + if (Java.Lang.Runtime.GetRuntime().AvailableProcessors() > 1) + { + ReadDocumentStreamed(new AsynchronousBufferedXmlReader(readerStream), hashedStream); + Debug.WriteLine(String.Format("ReadDocumentStreamed: {0}ms multi-threaded", stopWatch.ElapsedMilliseconds)); + } + else + { + ReadXmlStreamed(readerStream, hashedStream); + Debug.WriteLine(String.Format("ReadXmlStreamed: {0}ms single-threaded", stopWatch.ElapsedMilliseconds)); + } + stopWatch.Restart(); // ReadXmlDom(readerStream); readerStream.Close(); @@ -138,9 +166,26 @@ namespace KeePassLib.Serialization { throw new CryptographicException(KLRes.FileCorrupted); } - finally { CommonCleanUpRead(sSource, hashedStream); } + finally + { + + CommonCleanUpRead(sSource, hashedStream); + Debug.WriteLine(String.Format("Close and Clean Up: {0}ms", stopWatch.ElapsedMilliseconds)); + } } + /* + public static void CopyStream(Stream input, Stream output) + { + byte[] buffer = new byte[32768]; + int read; + while ((read = input.Read(buffer, 0, buffer.Length)) > 0) + { + output.Write(buffer, 0, read); + } + output.Seek(0, SeekOrigin.Begin); + }*/ + private void CommonCleanUpRead(Stream sSource, HashingStreamEx hashedStream) { hashedStream.Close(); diff --git a/src/KeePassLib2Android/Utility/TimeUtil.cs b/src/KeePassLib2Android/Utility/TimeUtil.cs index 94aa4cd2..25c815e1 100644 --- a/src/KeePassLib2Android/Utility/TimeUtil.cs +++ b/src/KeePassLib2Android/Utility/TimeUtil.cs @@ -167,6 +167,22 @@ namespace KeePassLib.Utility return bResult; } + /// + /// Deserializes a UTC XML DateTime to a local time, or returns defaultValue if it could not be parsed + /// + public static DateTime DeserializeUtcOrDefault(string xmlDateTimeString, DateTime defaultValue) + { + try + { + return System.Xml.XmlConvert.ToDateTime(xmlDateTimeString, System.Xml.XmlDateTimeSerializationMode.Local); + } + catch(FormatException) + { + return defaultValue; + } + } + + private static DateTime? m_dtUnixRoot = null; public static DateTime ConvertUnixTime(double dtUnix) { diff --git a/src/keepass2android/Database.cs b/src/keepass2android/Database.cs index 08aecdf7..6e03e980 100644 --- a/src/keepass2android/Database.cs +++ b/src/keepass2android/Database.cs @@ -120,6 +120,8 @@ namespace keepass2android public void LoadData(Context ctx, IOConnectionInfo iocInfo, String password, String keyfile, UpdateStatus status) { + var stopWatch = System.Diagnostics.Stopwatch.StartNew(); + mIoc = iocInfo; KeePassLib.PwDatabase pwDatabase = new KeePassLib.PwDatabase(); @@ -137,9 +139,15 @@ namespace keepass2android throw new KeyFileException(); } } - + + System.Diagnostics.Debug.WriteLine(String.Format("LoadData Pre-open: {0}ms", stopWatch.ElapsedMilliseconds)); + stopWatch.Restart(); + pwDatabase.Open(iocInfo, key, status); + System.Diagnostics.Debug.WriteLine(String.Format("LoadData Open: {0}ms", stopWatch.ElapsedMilliseconds)); + stopWatch.Restart(); + if (iocInfo.IsLocalFile()) { mLastChangeDate = System.IO.File.GetLastWriteTimeUtc(iocInfo.Path); @@ -155,6 +163,8 @@ namespace keepass2android Loaded = true; pm = pwDatabase; searchHelper = new SearchDbHelper(ctx); + + System.Diagnostics.Debug.WriteLine(String.Format("LoadData Post-open: {0}ms", stopWatch.ElapsedMilliseconds)); } bool quickUnlockEnabled = false; diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index 779dd3b8..ff2f51d4 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -204,16 +204,12 @@ namespace keepass2android extraGroup.RemoveAllViews(); } bool hasExtraFields = false; - foreach (KeyValuePair pair in mEntry.Strings) + foreach (var view in from pair in mEntry.Strings where !PwDefs.IsStandardField(pair.Key) orderby pair.Key + select CreateEditSection(pair.Key, pair.Value.ReadString())) { - String key = pair.Key; - if (!PwDefs.IsStandardField(key)) - { - //View view = new EntrySection(this, null, key, pair.Value.ReadString()); - View view = CreateEditSection(key, pair.Value.ReadString()); - extraGroup.AddView(view); - hasExtraFields = true; - } + //View view = new EntrySection(this, null, key, pair.Value.ReadString()); + extraGroup.AddView(view); + hasExtraFields = true; } FindViewById(Resource.Id.entry_extra_strings_label).Visibility = hasExtraFields ? ViewStates.Visible : ViewStates.Gone; } From 4f49b073d8e78f7391796fdb98ea15cb38dbad86 Mon Sep 17 00:00:00 2001 From: AlexVallat Date: Wed, 19 Jun 2013 16:59:54 +0100 Subject: [PATCH 2/6] Remove multi-threaded kdbx loading path --- .../KeePassLib2Android.csproj | 1 - .../AsynchronousBufferedXmlReader.cs | 367 ------------------ .../Serialization/KdbxFile.Read.cs | 49 +-- src/keepass2android/Database.cs | 17 +- 4 files changed, 8 insertions(+), 426 deletions(-) delete mode 100644 src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs diff --git a/src/KeePassLib2Android/KeePassLib2Android.csproj b/src/KeePassLib2Android/KeePassLib2Android.csproj index a7fdc014..d925d883 100644 --- a/src/KeePassLib2Android/KeePassLib2Android.csproj +++ b/src/KeePassLib2Android/KeePassLib2Android.csproj @@ -113,7 +113,6 @@ - diff --git a/src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs b/src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs deleted file mode 100644 index d10ba1cb..00000000 --- a/src/KeePassLib2Android/Serialization/AsynchronousBufferedXmlReader.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.IO; -using System.Threading; -using System.Xml; -using System.Diagnostics; - -namespace KeePassLib.Serialization -{ - public class AsynchronousBufferedXmlReader : XmlReader - { - /// - /// An element which indicates the end of the XML document has been reached. - /// - private static readonly Element EndMarker = new Element(); - - /// - /// The next buffered element available for reading. - /// Volatility: only read/written to by non-buffering thread. Passed to the buffer thread as an initial parameter. - /// - Element mBufferQueueHead = new Element(); // Start off with the pre-document element. No content, yet. - - private readonly Thread mWorkerThread; - private readonly AutoResetEvent mWaitForBuffer = new AutoResetEvent(false); - /// - /// True while the reader thread is stalled waiting for buffering. - /// Volaitlity: Only written by read thread. Only read by buffer thread - /// - private volatile bool mWaitingForBuffer; - -#if TRACE - private Stopwatch mReadWaitTimer = new Stopwatch(); - private Stopwatch mBufferCompletedTimer = new Stopwatch(); -#endif - - /// - /// Testing helper method - /// - /// - /// - public static XmlReader FullyBuffer(Stream input) - { - var reader = new AsynchronousBufferedXmlReader(); - reader.ReadStreamWorker(input); - return reader; - } - - private AsynchronousBufferedXmlReader() - { - // Once the end is reached, it stays there. - EndMarker.NextElement = EndMarker; - } - - public AsynchronousBufferedXmlReader(Stream input) : this() - { - mWorkerThread = new Thread(ReadStreamWorker) { Name = GetType().Name }; - mWorkerThread.Start(input); - } - - #region Buffering - private void ReadStreamWorker(object state) - { - var input = (Stream)state; - - var xr = XmlReader.Create(input, KdbxFile.CreateStdXmlReaderSettings()); - - /// - /// The last buffered element available for reading. - /// - Element bufferQueueTail = mBufferQueueHead; - - /// - /// The element currently being buffered. Not yet available for reading. - /// - Element currentElement = null; - - while (xr.Read()) - { - switch (xr.NodeType) - { - case XmlNodeType.Element: - // Start a new element - if (currentElement != null) - { - // Add the previous current element to the tail of the buffer - bufferQueueTail.NextElement = currentElement; - bufferQueueTail = currentElement; - if (mWaitingForBuffer) mWaitForBuffer.Set(); // Signal that a new element is available in the buffer - } - - currentElement = new Element { Name = xr.Name }; - - // Process attributes - current optimisation, all elements have 0 or 1 attribute - if (xr.MoveToNextAttribute()) - { -#if DEBUG - Debug.Assert(xr.AttributeCount == 1); - currentElement.AttributeName = xr.Name; -#endif - currentElement.AttributeValue = xr.Value; - } - - currentElement.IsEmpty = xr.IsEmptyElement; - - break; - - case XmlNodeType.Text: - currentElement.Value = xr.Value; - currentElement.IsEmpty = true; // Mark as empty because it will have no end element written for it - break; - - case XmlNodeType.EndElement: - Debug.Assert(currentElement != null, "Ending an element that was never started"); - - // If this is an element with children (not one with a value) add an end element marker to the queue - if (currentElement.Value == null || currentElement.Name != xr.Name) - { - bufferQueueTail.NextElement = currentElement; - bufferQueueTail = currentElement; - if (mWaitingForBuffer) mWaitForBuffer.Set(); // Signal that a new element is available in the buffer - - currentElement = new Element { Name = xr.Name, IsEndElement = true }; - } - break; - } - } - - // Conclude the document, add the final element to the buffer and mark the ending - currentElement.NextElement = EndMarker; - bufferQueueTail.NextElement = currentElement; - bufferQueueTail = currentElement; - mWaitForBuffer.Set(); // Signal that final element is available in the buffer (regardless of wait flag, to avoid race condition) -#if TRACE - mBufferCompletedTimer.Start(); -#endif - } - #endregion - - private class Element - { - /// - /// Link to the next buffered element. - /// Volatility: Written to by buffer thread only. Read by both threads - /// - public volatile Element NextElement; - - public string Name; - - /// - /// If this element marks the end of an xml element with child nodes, the IsEndElement will be true, and Value must be null. - /// - public bool IsEndElement; - - /// - /// Set true if this represents an empty element - /// - public bool IsEmpty; - - /// - /// If Value is non-null, then there will be no corresponding Element with IsEndElement created. - /// - public string Value; - - // Currently KDBX has a maximum of one attribute per element, so no need for a dictionary here, and the name is only used for debug asserts -#if DEBUG - public string AttributeName; -#endif - public string AttributeValue; - } - - #region Custom XmlReader implementation for usage by KdbxFile only - public override bool Read() - { - Element nextElement; - while ((nextElement = mBufferQueueHead.NextElement) == null) - { -#if TRACE - mReadWaitTimer.Start(); -#endif - mWaitingForBuffer = true; - mWaitForBuffer.WaitOne(); - mWaitingForBuffer = false; - -#if TRACE - mReadWaitTimer.Stop(); -#endif - } - mBufferQueueHead = mBufferQueueHead.NextElement; - - -#if TRACE - if (mBufferQueueHead == EndMarker) - { - Debug.WriteLine(String.Format("Asynchronous Buffered XmlReader waited for a total of: {0}ms, buffer completed {1}ms ahead of read", mReadWaitTimer.ElapsedMilliseconds, mBufferCompletedTimer.ElapsedMilliseconds)); - } -#endif - return mBufferQueueHead != EndMarker; - } - - public override string ReadElementString() - { - var result = mBufferQueueHead.Value ?? String.Empty; // ReadElementString returns empty strings for null content - Read(); // Read element string always skips to the start of the next element - return result; - } - - public override XmlNodeType NodeType - { - get - { - return mBufferQueueHead.IsEndElement ? XmlNodeType.EndElement : XmlNodeType.Element; - } - } - - public override bool IsEmptyElement - { - get - { - return mBufferQueueHead.IsEmpty; - } - } - - public override string Name - { - get - { - return mBufferQueueHead.Name; - } - } - - public override bool HasAttributes - { - get - { - return mBufferQueueHead.AttributeValue != null; - } - } - - public override bool MoveToAttribute(string name) - { -#if DEBUG - Debug.Assert(mBufferQueueHead.AttributeName == name); -#endif - - return true; - } - - public override string Value - { - get - { - return mBufferQueueHead.AttributeValue; - } - } - - - public override bool MoveToElement() - { - return true; - } - #endregion - - #region Unimplemented XmlReader overrides - - public override int AttributeCount - { - get { throw new NotImplementedException(); } - } - - public override string BaseURI - { - get { throw new NotImplementedException(); } - } - - public override void Close() - { - throw new NotImplementedException(); - } - - public override int Depth - { - get { throw new NotImplementedException(); } - } - - public override bool EOF - { - get { throw new NotImplementedException(); } - } - - public override string GetAttribute(int i) - { - throw new NotImplementedException(); - } - - public override string GetAttribute(string name, string namespaceURI) - { - throw new NotImplementedException(); - } - - public override string GetAttribute(string name) - { - throw new NotImplementedException(); - } - - public override bool HasValue - { - get { throw new NotImplementedException(); } - } - - public override string LocalName - { - get { throw new NotImplementedException(); } - } - - public override string LookupNamespace(string prefix) - { - throw new NotImplementedException(); - } - - public override bool MoveToAttribute(string name, string ns) - { - throw new NotImplementedException(); - } - - public override bool MoveToFirstAttribute() - { - throw new NotImplementedException(); - } - - public override bool MoveToNextAttribute() - { - throw new NotImplementedException(); - } - - public override XmlNameTable NameTable - { - get { throw new NotImplementedException(); } - } - - public override string NamespaceURI - { - get { throw new NotImplementedException(); } - } - - public override string Prefix - { - get { throw new NotImplementedException(); } - } - - public override bool ReadAttributeValue() - { - throw new NotImplementedException(); - } - - public override ReadState ReadState - { - get { throw new NotImplementedException(); } - } - - public override void ResolveEntity() - { - throw new NotImplementedException(); - } - #endregion - } -} diff --git a/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs b/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs index 65c2aa55..03456962 100644 --- a/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs +++ b/src/KeePassLib2Android/Serialization/KdbxFile.Read.cs @@ -70,8 +70,6 @@ namespace KeePassLib.Serialization Debug.Assert(sSource != null); if(sSource == null) throw new ArgumentNullException("sSource"); - var stopWatch = Stopwatch.StartNew(); - m_format = kdbFormat; m_slLogger = slLogger; @@ -129,33 +127,7 @@ namespace KeePassLib.Serialization } else m_randomStream = null; // No random stream for plain-text files - Debug.WriteLine(String.Format("Crypto setup: {0}ms", stopWatch.ElapsedMilliseconds)); - stopWatch.Restart(); - - /* - var memStream = new MemoryStream((int)hashedStream.Length); - CopyStream(readerStream, memStream); - readerStream = memStream; - Debug.WriteLine(String.Format("CopyStream: {0}ms", stopWatch.ElapsedMilliseconds)); - - - stopWatch.Restart(); - */ - - //var bufferedXmlReader = AsynchronousBufferedXmlReader.FullyBuffer(readerStream); - //Debug.WriteLine(String.Format("ReadToBuffer: {0}ms", stopWatch.ElapsedMilliseconds)); - - if (Java.Lang.Runtime.GetRuntime().AvailableProcessors() > 1) - { - ReadDocumentStreamed(new AsynchronousBufferedXmlReader(readerStream), hashedStream); - Debug.WriteLine(String.Format("ReadDocumentStreamed: {0}ms multi-threaded", stopWatch.ElapsedMilliseconds)); - } - else - { - ReadXmlStreamed(readerStream, hashedStream); - Debug.WriteLine(String.Format("ReadXmlStreamed: {0}ms single-threaded", stopWatch.ElapsedMilliseconds)); - } - stopWatch.Restart(); + ReadXmlStreamed(readerStream, hashedStream); // ReadXmlDom(readerStream); readerStream.Close(); @@ -166,26 +138,9 @@ namespace KeePassLib.Serialization { throw new CryptographicException(KLRes.FileCorrupted); } - finally - { - - CommonCleanUpRead(sSource, hashedStream); - Debug.WriteLine(String.Format("Close and Clean Up: {0}ms", stopWatch.ElapsedMilliseconds)); - } + finally { CommonCleanUpRead(sSource, hashedStream); } } - /* - public static void CopyStream(Stream input, Stream output) - { - byte[] buffer = new byte[32768]; - int read; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - { - output.Write(buffer, 0, read); - } - output.Seek(0, SeekOrigin.Begin); - }*/ - private void CommonCleanUpRead(Stream sSource, HashingStreamEx hashedStream) { hashedStream.Close(); diff --git a/src/keepass2android/Database.cs b/src/keepass2android/Database.cs index 6e03e980..c8eee74d 100644 --- a/src/keepass2android/Database.cs +++ b/src/keepass2android/Database.cs @@ -120,8 +120,6 @@ namespace keepass2android public void LoadData(Context ctx, IOConnectionInfo iocInfo, String password, String keyfile, UpdateStatus status) { - var stopWatch = System.Diagnostics.Stopwatch.StartNew(); - mIoc = iocInfo; KeePassLib.PwDatabase pwDatabase = new KeePassLib.PwDatabase(); @@ -140,14 +138,8 @@ namespace keepass2android } } - System.Diagnostics.Debug.WriteLine(String.Format("LoadData Pre-open: {0}ms", stopWatch.ElapsedMilliseconds)); - stopWatch.Restart(); - pwDatabase.Open(iocInfo, key, status); - System.Diagnostics.Debug.WriteLine(String.Format("LoadData Open: {0}ms", stopWatch.ElapsedMilliseconds)); - stopWatch.Restart(); - if (iocInfo.IsLocalFile()) { mLastChangeDate = System.IO.File.GetLastWriteTimeUtc(iocInfo.Path); @@ -163,8 +155,6 @@ namespace keepass2android Loaded = true; pm = pwDatabase; searchHelper = new SearchDbHelper(ctx); - - System.Diagnostics.Debug.WriteLine(String.Format("LoadData Post-open: {0}ms", stopWatch.ElapsedMilliseconds)); } bool quickUnlockEnabled = false; @@ -197,7 +187,12 @@ namespace keepass2android public PwGroup Search(SearchParameters searchParams) { - return searchHelper.search(this, searchParams); + return Search(searchParams, null); + } + + public PwGroup Search(SearchParameters searchParams, IDictionary resultContexts) + { + return searchHelper.search(this, searchParams, resultContexts); } From 30cbc16051a78ee3c671fd00f87188ae765eb0a9 Mon Sep 17 00:00:00 2001 From: AlexVallat Date: Wed, 19 Jun 2013 18:44:35 +0100 Subject: [PATCH 3/6] Inline Search with suggestions --- src/KeePassLib2Android/PwGroup.cs | 92 ++++-- src/keepass2android/EntryEditActivity.cs | 2 +- src/keepass2android/GroupBaseActivity.cs | 8 +- .../Resources/menu-v11/group.xml | 6 + .../Resources/values/strings.xml | 2 + .../Resources/xml/searchable.xml | 5 + src/keepass2android/icons/DrawableFactory.cs | 6 +- src/keepass2android/keepass2android.csproj | 1 + src/keepass2android/search/SearchDbHelper.cs | 6 +- src/keepass2android/search/SearchProvider.cs | 309 ++++++++++++++++++ src/keepass2android/search/SearchResults.cs | 35 +- 11 files changed, 435 insertions(+), 37 deletions(-) create mode 100644 src/keepass2android/search/SearchProvider.cs diff --git a/src/KeePassLib2Android/PwGroup.cs b/src/KeePassLib2Android/PwGroup.cs index b5b20c25..fa0e2802 100644 --- a/src/KeePassLib2Android/PwGroup.cs +++ b/src/KeePassLib2Android/PwGroup.cs @@ -35,6 +35,11 @@ namespace KeePassLib /// public sealed class PwGroup : ITimeLogger, IStructureItem, IDeepCloneable { + private const int SearchContextStringMaxLength = 50; // Note, doesn't include elipsis, if added + public const string SearchContextUuid = "Uuid"; + public const string SearchContextParentGroup = "Parent Group"; + public const string SearchContextTags = "Tags"; + public const bool DefaultAutoTypeEnabled = true; public const bool DefaultSearchingEnabled = true; @@ -706,6 +711,18 @@ namespace KeePassLib /// be stored. public void SearchEntries(SearchParameters sp, PwObjectList listStorage, IStatusLogger slStatus) + { + } + + /// + /// Search this group and all subgroups for entries. + /// + /// Specifies the search method. + /// Entry list in which the search results will + /// be stored. + /// Dictionary that will be populated with text fragments indicating the context of why each entry (keyed by Uuid) was returned + public void SearchEntries(SearchParameters sp, PwObjectList listStorage, IDictionary resultContexts, + IStatusLogger slStatus) { if(sp == null) { Debug.Assert(false); return; } if(listStorage == null) { Debug.Assert(false); return; } @@ -716,7 +733,7 @@ namespace KeePassLib if((lTerms.Count <= 1) || sp.RegularExpression) { if(slStatus != null) uTotalEntries = GetEntriesCount(true); - SearchEntriesSingle(sp, listStorage, slStatus, ref uCurEntries, + SearchEntriesSingle(sp, listStorage, resultContexts , slStatus, ref uCurEntries, uTotalEntries); return; } @@ -748,7 +765,7 @@ namespace KeePassLib bNegate = (sp.SearchString.Length > 0); } - if(!pg.SearchEntriesSingle(sp, pgNew.Entries, slStatus, + if(!pg.SearchEntriesSingle(sp, pgNew.Entries, resultContexts, slStatus, ref uCurEntries, uTotalEntries)) { pg = null; @@ -773,7 +790,7 @@ namespace KeePassLib } private bool SearchEntriesSingle(SearchParameters spIn, - PwObjectList listStorage, IStatusLogger slStatus, + PwObjectList listStorage, IDictionary resultContexts, IStatusLogger slStatus, ref ulong uCurEntries, ulong uTotalEntries) { SearchParameters sp = spIn.Clone(); @@ -856,42 +873,42 @@ namespace KeePassLib if(strKey == PwDefs.TitleField) { if(bTitle) SearchEvalAdd(sp, kvp.Value.ReadString(), - rx, pe, listStorage); + rx, pe, listStorage, resultContexts, strKey); } else if(strKey == PwDefs.UserNameField) { if(bUserName) SearchEvalAdd(sp, kvp.Value.ReadString(), - rx, pe, listStorage); + rx, pe, listStorage, resultContexts, strKey); } else if(strKey == PwDefs.PasswordField) { if(bPassword) SearchEvalAdd(sp, kvp.Value.ReadString(), - rx, pe, listStorage); + rx, pe, listStorage, resultContexts, strKey); } else if(strKey == PwDefs.UrlField) { if(bUrl) SearchEvalAdd(sp, kvp.Value.ReadString(), - rx, pe, listStorage); + rx, pe, listStorage, resultContexts, strKey); } else if(strKey == PwDefs.NotesField) { if(bNotes) SearchEvalAdd(sp, kvp.Value.ReadString(), - rx, pe, listStorage); + rx, pe, listStorage, resultContexts, strKey); } else if(bOther) SearchEvalAdd(sp, kvp.Value.ReadString(), - rx, pe, listStorage); + rx, pe, listStorage, resultContexts, strKey); // An entry can match only once => break if we have added it if(listStorage.UCount > uInitialResults) break; } if(bUuids && (listStorage.UCount == uInitialResults)) - SearchEvalAdd(sp, pe.Uuid.ToHexString(), rx, pe, listStorage); + SearchEvalAdd(sp, pe.Uuid.ToHexString(), rx, pe, listStorage, resultContexts, SearchContextUuid); if(bGroupName && (listStorage.UCount == uInitialResults) && (pe.ParentGroup != null)) - SearchEvalAdd(sp, pe.ParentGroup.Name, rx, pe, listStorage); + SearchEvalAdd(sp, pe.ParentGroup.Name, rx, pe, listStorage, resultContexts, SearchContextParentGroup); if(bTags) { @@ -899,7 +916,7 @@ namespace KeePassLib { if(listStorage.UCount != uInitialResults) break; // Match - SearchEvalAdd(sp, strTag, rx, pe, listStorage); + SearchEvalAdd(sp, strTag, rx, pe, listStorage, resultContexts, SearchContextTags); } } @@ -913,28 +930,59 @@ namespace KeePassLib } private static void SearchEvalAdd(SearchParameters sp, string strDataField, - Regex rx, PwEntry pe, PwObjectList lResults) + Regex rx, PwEntry pe, PwObjectList lResults, IDictionary resultContexts, string contextFieldName) { bool bMatch = false; + int matchPos; - if(rx == null) - bMatch = (strDataField.IndexOf(sp.SearchString, - sp.ComparisonMode) >= 0); - else bMatch = rx.IsMatch(strDataField); + if (rx == null) + { + matchPos = strDataField.IndexOf(sp.SearchString, sp.ComparisonMode); + bMatch = matchPos >= 0; + } + else + { + var match = rx.Match(strDataField); + bMatch = match.Success; + matchPos = match.Index; + } if(!bMatch && (sp.DataTransformationFn != null)) { string strCmp = sp.DataTransformationFn(strDataField, pe); if(!object.ReferenceEquals(strCmp, strDataField)) { - if(rx == null) - bMatch = (strCmp.IndexOf(sp.SearchString, - sp.ComparisonMode) >= 0); - else bMatch = rx.IsMatch(strCmp); + if (rx == null) + { + matchPos = strCmp.IndexOf(sp.SearchString, sp.ComparisonMode); + bMatch = matchPos >= 0; + } + else + { + var match = rx.Match(strCmp); + bMatch = match.Success; + matchPos = match.Index; + } } } - if(bMatch) lResults.Add(pe); + if (bMatch) + { + lResults.Add(pe); + + if (resultContexts != null) + { + // Trim the value if necessary + var contextString = strDataField; + if (contextString.Length > SearchContextStringMaxLength) + { + // Start 10% before actual data, and don't run over + var startPos = Math.Min(matchPos - (SearchContextStringMaxLength / 10), contextString.Length - SearchContextStringMaxLength); + contextString = "… " + contextString.Substring(startPos, SearchContextStringMaxLength) + ((startPos + SearchContextStringMaxLength < contextString.Length) ? " …" : null); + } + resultContexts[pe.Uuid] = contextFieldName + ": " + contextString; + } + } } public List BuildEntryTagsList() diff --git a/src/keepass2android/EntryEditActivity.cs b/src/keepass2android/EntryEditActivity.cs index 46600af5..e6ca71b3 100644 --- a/src/keepass2android/EntryEditActivity.cs +++ b/src/keepass2android/EntryEditActivity.cs @@ -125,7 +125,7 @@ namespace keepass2android entryId = new KeePassLib.PwUuid(MemUtil.HexStringToByteArray(uuidBytes)); State.parentGroup = null; - if (entryId == PwUuid.Zero) + if (entryId.EqualsValue(PwUuid.Zero)) { String groupId = i.GetStringExtra(KEY_PARENT); diff --git a/src/keepass2android/GroupBaseActivity.cs b/src/keepass2android/GroupBaseActivity.cs index 87eeaf60..c7fceb7f 100644 --- a/src/keepass2android/GroupBaseActivity.cs +++ b/src/keepass2android/GroupBaseActivity.cs @@ -197,7 +197,12 @@ namespace keepass2android MenuInflater inflater = MenuInflater; inflater.Inflate(Resource.Menu.group, menu); + + var searchManager = (SearchManager)GetSystemService(Context.SearchService); + var searchView = (SearchView)menu.FindItem(Resource.Id.menu_search).ActionView; + searchView.SetSearchableInfo(searchManager.GetSearchableInfo(ComponentName)); + return true; } @@ -241,8 +246,9 @@ namespace keepass2android SetResult(KeePass.EXIT_LOCK); Finish(); return true; - + case Resource.Id.menu_search: + case Resource.Id.menu_search_advanced: OnSearchRequested(); return true; diff --git a/src/keepass2android/Resources/menu-v11/group.xml b/src/keepass2android/Resources/menu-v11/group.xml index 67ab22b0..ccaff7c9 100644 --- a/src/keepass2android/Resources/menu-v11/group.xml +++ b/src/keepass2android/Resources/menu-v11/group.xml @@ -20,6 +20,12 @@ android:icon="@android:drawable/ic_menu_search" android:title="@string/menu_search" android:showAsAction="ifRoom" + android:actionViewClass="android.widget.SearchView" + /> + User Name Extra string fields File attachments + Notes The ArcFour stream cipher is not supported. Keepass2Android cannot handle this uri. Error creating group. @@ -129,6 +130,7 @@ Open Rename Search + Advanced Search Go to URL Minus Never diff --git a/src/keepass2android/Resources/xml/searchable.xml b/src/keepass2android/Resources/xml/searchable.xml index 48b9492a..1730f33a 100644 --- a/src/keepass2android/Resources/xml/searchable.xml +++ b/src/keepass2android/Resources/xml/searchable.xml @@ -21,4 +21,9 @@ android:label="@string/search_label" android:hint="@string/search_hint" android:searchMode="showSearchLabelAsBadge" + android:searchSuggestAuthority="keepass2android.search.SearchProvider" + android:searchSuggestSelection=" ?" + android:searchSuggestThreshold="2" + android:searchSuggestIntentAction="android.intent.action.VIEW" + android:searchSuggestIntentData="content://keepass2android.EntryActivity" /> \ No newline at end of file diff --git a/src/keepass2android/icons/DrawableFactory.cs b/src/keepass2android/icons/DrawableFactory.cs index 0f7d3fe9..4825e153 100644 --- a/src/keepass2android/icons/DrawableFactory.cs +++ b/src/keepass2android/icons/DrawableFactory.cs @@ -59,13 +59,13 @@ namespace keepass2android public Drawable getIconDrawable (Resources res, PwDatabase db, PwIcon icon, PwUuid customIconId) { - if (customIconId != PwUuid.Zero) { + if (!customIconId.EqualsValue(PwUuid.Zero)) { return getIconDrawable (res, db, customIconId); } else { return getIconDrawable (res, icon); } } - + private static void initBlank (Resources res) { if (blank == null) { @@ -92,7 +92,7 @@ namespace keepass2android public Drawable getIconDrawable (Resources res, PwDatabase db, PwUuid icon) { initBlank (res); - if (icon == PwUuid.Zero) { + if (icon.EqualsValue(PwUuid.Zero)) { return blank; } Drawable draw = null; diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index ace5b061..2f291ec9 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -86,6 +86,7 @@ + diff --git a/src/keepass2android/search/SearchDbHelper.cs b/src/keepass2android/search/SearchDbHelper.cs index 41bea8db..f1888d24 100644 --- a/src/keepass2android/search/SearchDbHelper.cs +++ b/src/keepass2android/search/SearchDbHelper.cs @@ -50,9 +50,9 @@ namespace keepass2android SearchParameters sp = new SearchParameters(); sp.SearchString = str; - return search(database, sp); + return search(database, sp, null); } - public PwGroup search(Database database, SearchParameters sp) + public PwGroup search(Database database, SearchParameters sp, IDictionary resultContexts) { if(sp.RegularExpression) // Validate regular expression @@ -67,7 +67,7 @@ namespace keepass2android PwObjectList listResults = pgResults.Entries; - database.root.SearchEntries(sp, listResults, new NullStatusLogger()); + database.root.SearchEntries(sp, listResults, resultContexts, new NullStatusLogger()); return pgResults; diff --git a/src/keepass2android/search/SearchProvider.cs b/src/keepass2android/search/SearchProvider.cs new file mode 100644 index 00000000..858b113f --- /dev/null +++ b/src/keepass2android/search/SearchProvider.cs @@ -0,0 +1,309 @@ +/* +This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file is based on Keepassdroid, Copyright Brian Pellin. + + Keepass2Android is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Keepass2Android is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Keepass2Android. If not, see . + */ +using System; +using System.Linq; +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.Database; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Runtime; + +using KeePassLib; +using KeePassLib.Utility; +using System.Threading; +using System.Collections.Generic; + +namespace keepass2android.search +{ + [ContentProvider(new [] { SearchProvider.Authority })] + public class SearchProvider : ContentProvider + { + private enum UriMatches + { + NoMatch = UriMatcher.NoMatch, + GetIcon, + GetSuggestions + } + public const string Authority = "keepass2android.search.SearchProvider"; + + private const string GetIconPathQuery = "get_icon"; + private const string IconIdParameter = "IconId"; + private const string CustomIconUuidParameter = "CustomIconUuid"; + //public static readonly String AUTHORITY = "keepass2android.search.SearchProvider"; + //public static readonly Android.Net.Uri CONTENT_URI = Android.Net.Uri.Parse("content://" + AUTHORITY + "/dictionary"); + + private Database mDb; + + private static UriMatcher UriMatcher = BuildUriMatcher(); + + static UriMatcher BuildUriMatcher() + { + var matcher = new UriMatcher(UriMatcher.NoMatch); + + // to get definitions... + matcher.AddURI(Authority, GetIconPathQuery, (int)UriMatches.GetIcon); + matcher.AddURI(Authority, SearchManager.SuggestUriPathQuery, (int)UriMatches.GetSuggestions); + + return matcher; + } + + public override bool OnCreate() + { + mDb = App.getDB(); + return true; + } + + public override Android.Database.ICursor Query(Android.Net.Uri uri, string[] projection, string selection, string[] selectionArgs, string sortOrder) + { + if (mDb.Open) // Can't show suggestions if the database is locked! + { + switch ((UriMatches)UriMatcher.Match(uri)) + { + case UriMatches.GetSuggestions: + var searchString = selectionArgs[0]; + if (!String.IsNullOrEmpty(searchString)) + { + try + { + var resultsContexts = new Dictionary(); + var result = mDb.Search(new SearchParameters { SearchString = searchString }, resultsContexts ); + return new GroupCursor(result, resultsContexts); + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine("Failed to search for suggestions: " + e.Message); + } + } + break; + case UriMatches.GetIcon: + return null; // This will be handled by OpenAssetFile + + default: + return null; + //throw new ArgumentException("Unknown Uri: " + uri, "uri"); + } + } + + return null; + } + + public override ParcelFileDescriptor OpenFile(Android.Net.Uri uri, string mode) + { + switch ((UriMatches)UriMatcher.Match(uri)) + { + case UriMatches.GetIcon: + var iconId = (PwIcon)Enum.Parse(typeof(PwIcon), uri.GetQueryParameter(IconIdParameter)); + var customIconUuid = new PwUuid(MemUtil.HexStringToByteArray(uri.GetQueryParameter(CustomIconUuidParameter))); + + var iconDrawable = mDb.drawFactory.getIconDrawable(App.Context.Resources, mDb.pm, iconId, customIconUuid) as BitmapDrawable; + if (iconDrawable != null) + { + var pipe = ParcelFileDescriptor.CreatePipe(); + var outStream = new OutputStreamInvoker(new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])); + + ThreadPool.QueueUserWorkItem(state => + { + iconDrawable.Bitmap.Compress(Bitmap.CompressFormat.Png, 100, outStream); + outStream.Close(); + }); + + return pipe[0]; + } + + // Couldn't get an icon for some reason. + return null; + default: + throw new ArgumentException("Unknown Uri: " + uri, "uri"); + } + } + + public override string GetType(Android.Net.Uri uri) + { + switch ((UriMatches)UriMatcher.Match(uri)) + { + case UriMatches.GetSuggestions: + return SearchManager.SuggestMimeType; + case UriMatches.GetIcon: + return "image/png"; + + default: + throw new ArgumentException("Unknown Uri: " + uri, "uri"); + } + } + + #region Unimplemented + public override int Delete(Android.Net.Uri uri, string selection, string[] selectionArgs) + { + throw new NotImplementedException(); + } + public override Android.Net.Uri Insert(Android.Net.Uri uri, ContentValues values) + { + throw new NotImplementedException(); + } + public override int Update(Android.Net.Uri uri, ContentValues values, string selection, string[] selectionArgs) + { + throw new NotImplementedException(); + } + #endregion + + + private class GroupCursor : AbstractCursor + { + private static readonly string[] ColumnNames = new[] { Android.Provider.BaseColumns.Id, + SearchManager.SuggestColumnText1, + SearchManager.SuggestColumnText2, + SearchManager.SuggestColumnIcon1, + SearchManager.SuggestColumnIntentDataId, + }; + + private readonly PwGroup mGroup; + private readonly IDictionary mResultContexts; + + public GroupCursor(PwGroup group, IDictionary resultContexts) + { + System.Diagnostics.Debug.Assert(!group.Groups.Any(), "Expecting a flat list of groups"); + + mGroup = group; + mResultContexts = resultContexts; + } + + public override int Count + { + get { return (int)Math.Min(mGroup.GetEntriesCount(false), int.MaxValue); } + } + + public override string[] GetColumnNames() + { + return ColumnNames; + } + + public override FieldType GetType(int column) + { + switch (column) + { + case 0: // _ID + return FieldType.Integer; + default: + return base.GetType(column); // Ends up as string + } + } + + private PwEntry CurrentEntry + { + get + { + return mGroup.Entries.GetAt((uint)MPos); + } + } + + public override long GetLong(int column) + { + switch (column) + { + case 0: // _ID + return MPos; + default: + throw new FormatException(); + } + } + + public override string GetString(int column) + { + switch (column) + { + case 0: // _ID + return MPos.ToString(); + case 1: // SuggestColumnText1 + return CurrentEntry.Strings.ReadSafe(PwDefs.TitleField); + case 2: // SuggestColumnText2 + string context; + if (mResultContexts.TryGetValue(CurrentEntry.Uuid, out context)) + { + context = Internationalise(context); + return context; + } + return null; + case 3: // SuggestColumnIcon1 + var builder = new Android.Net.Uri.Builder(); + builder.Scheme(ContentResolver.SchemeContent); + builder.Authority(Authority); + builder.Path(GetIconPathQuery); + builder.AppendQueryParameter(IconIdParameter, CurrentEntry.IconId.ToString()); + builder.AppendQueryParameter(CustomIconUuidParameter, CurrentEntry.CustomIconUuid.ToHexString()); + return builder.Build().ToString(); + case 4: // SuggestColumnIntentDataId + return CurrentEntry.Uuid.ToHexString(); + default: + return null; + } + } + + private string Internationalise(string context) + { + // Some context names can be internationalised. + var splitPos = context.IndexOf(':'); + var rawName = context.Substring(0, splitPos); + int intlResourceId = 0; + switch (rawName) + { + case PwDefs.TitleField: + // We will already be showing Title, so ignore it entirely so it doesn't double-appear + return null; + case PwDefs.UserNameField: + intlResourceId = Resource.String.entry_user_name; + break; + case PwDefs.UrlField: + intlResourceId = Resource.String.entry_url; + break; + case PwDefs.NotesField: + intlResourceId = Resource.String.entry_notes; + break; + case PwGroup.SearchContextTags: + intlResourceId = Resource.String.entry_tags; + break; + default: + // Other fields aren't part of the default SearchParameters, so we won't ever get them as context anyway + break; + } + + if (intlResourceId > 0) + { + return App.Context.GetString(intlResourceId) + context.Substring(splitPos); + } + + return context; + } + + public override bool IsNull(int column) + { + return false; + } + + #region Data types appearing in no columns + public override int GetInt(int column) { throw new FormatException(); } + public override double GetDouble(int column) { throw new FormatException(); } + public override float GetFloat(int column) { throw new FormatException(); } + public override short GetShort(int column) { throw new FormatException(); } + #endregion + } + + } +} + diff --git a/src/keepass2android/search/SearchResults.cs b/src/keepass2android/search/SearchResults.cs index deef04df..b393f451 100644 --- a/src/keepass2android/search/SearchResults.cs +++ b/src/keepass2android/search/SearchResults.cs @@ -27,10 +27,11 @@ using Android.Views; using Android.Widget; using keepass2android.view; using KeePassLib; +using Android.Support.V4.App; namespace keepass2android.search { - [Activity (Label = "@string/app_name", Theme="@style/NoTitleBar")] + [Activity (Label = "@string/app_name", Theme="@style/NoTitleBar", LaunchMode=Android.Content.PM.LaunchMode.SingleTop)] [MetaData("android.app.searchable",Resource="@xml/searchable")] [IntentFilter(new[]{Intent.ActionSearch}, Categories=new[]{Intent.CategoryDefault})] public class SearchResults : GroupBaseActivity @@ -46,20 +47,40 @@ namespace keepass2android.search } SetResult(KeePass.EXIT_NORMAL); - + + ProcessIntent(Intent); + } + + protected override void OnNewIntent(Intent intent) + { + ProcessIntent(intent); + } + + private void ProcessIntent(Intent intent) + { mDb = App.getDB(); - + // Likely the app has been killed exit the activity - if ( ! mDb.Open ) { + if (!mDb.Open) + { Finish(); } - query(getSearch(Intent)); - + if (intent.Action == Intent.ActionView) + { + var entryIntent = new Intent(this, typeof(EntryActivity)); + entryIntent.PutExtra(EntryActivity.KEY_ENTRY, intent.Data.LastPathSegment); + Finish(); // Close this activity so that the entry activity is navigated to from the main activity, not this one. + StartActivity(entryIntent); + } + else + { + // Action may either by ActionSearch (from search widget) or null (if called from SearchActivity directly) + query(getSearch(intent)); + } } - private void query (SearchParameters searchParams) { try { From dea5a7152f4b8b96d44303db321973dd9675e4f9 Mon Sep 17 00:00:00 2001 From: AlexVallat Date: Thu, 20 Jun 2013 08:19:07 +0100 Subject: [PATCH 4/6] Merging in changes --- src/Kp2aBusinessLogic/SearchDbHelper.cs | 3 ++- src/Kp2aBusinessLogic/database/Database.cs | 4 ++-- src/keepass2android/EntryActivity.cs | 1 + src/keepass2android/Resources/values/strings.xml | 1 - src/keepass2android/search/SearchProvider.cs | 14 ++++++-------- src/keepass2android/search/SearchResults.cs | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Kp2aBusinessLogic/SearchDbHelper.cs b/src/Kp2aBusinessLogic/SearchDbHelper.cs index 3706a450..f26f76d6 100644 --- a/src/Kp2aBusinessLogic/SearchDbHelper.cs +++ b/src/Kp2aBusinessLogic/SearchDbHelper.cs @@ -15,8 +15,9 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file along with Keepass2Android. If not, see . */ using System; -using KeePassLib; +using System.Collections.Generic; using System.Text.RegularExpressions; +using KeePassLib; using KeePassLib.Collections; using KeePassLib.Interfaces; using KeePassLib.Utility; diff --git a/src/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs index f22926b8..0d2b25c2 100644 --- a/src/Kp2aBusinessLogic/database/Database.cs +++ b/src/Kp2aBusinessLogic/database/Database.cs @@ -145,9 +145,9 @@ namespace keepass2android } - public PwGroup Search(SearchParameters searchParams) + public PwGroup Search(SearchParameters searchParams, IDictionary resultContexts) { - return SearchHelper.Search(this, searchParams); + return SearchHelper.Search(this, searchParams, resultContexts); } diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index dae95ec7..825445dc 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -18,6 +18,7 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file using System; using System.Collections.Generic; using System.Text; +using System.Linq; using Android.App; using Android.Content; diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 9af3715b..7be2fbc7 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -61,7 +61,6 @@ User Name Extra string fields File attachments - Notes The ArcFour stream cipher is not supported. Keepass2Android cannot handle this uri. Error creating group. diff --git a/src/keepass2android/search/SearchProvider.cs b/src/keepass2android/search/SearchProvider.cs index 858b113f..b806fede 100644 --- a/src/keepass2android/search/SearchProvider.cs +++ b/src/keepass2android/search/SearchProvider.cs @@ -46,10 +46,8 @@ namespace keepass2android.search private const string GetIconPathQuery = "get_icon"; private const string IconIdParameter = "IconId"; private const string CustomIconUuidParameter = "CustomIconUuid"; - //public static readonly String AUTHORITY = "keepass2android.search.SearchProvider"; - //public static readonly Android.Net.Uri CONTENT_URI = Android.Net.Uri.Parse("content://" + AUTHORITY + "/dictionary"); - private Database mDb; + private Database _db; private static UriMatcher UriMatcher = BuildUriMatcher(); @@ -66,13 +64,13 @@ namespace keepass2android.search public override bool OnCreate() { - mDb = App.getDB(); + _db = App.Kp2a.GetDb(); return true; } public override Android.Database.ICursor Query(Android.Net.Uri uri, string[] projection, string selection, string[] selectionArgs, string sortOrder) { - if (mDb.Open) // Can't show suggestions if the database is locked! + if (_db.Open) // Can't show suggestions if the database is locked! { switch ((UriMatches)UriMatcher.Match(uri)) { @@ -83,7 +81,7 @@ namespace keepass2android.search try { var resultsContexts = new Dictionary(); - var result = mDb.Search(new SearchParameters { SearchString = searchString }, resultsContexts ); + var result = _db.Search(new SearchParameters { SearchString = searchString }, resultsContexts ); return new GroupCursor(result, resultsContexts); } catch (Exception e) @@ -112,7 +110,7 @@ namespace keepass2android.search var iconId = (PwIcon)Enum.Parse(typeof(PwIcon), uri.GetQueryParameter(IconIdParameter)); var customIconUuid = new PwUuid(MemUtil.HexStringToByteArray(uri.GetQueryParameter(CustomIconUuidParameter))); - var iconDrawable = mDb.drawFactory.getIconDrawable(App.Context.Resources, mDb.pm, iconId, customIconUuid) as BitmapDrawable; + var iconDrawable = _db.DrawableFactory.GetIconDrawable(App.Context.Resources, _db.KpDatabase, iconId, customIconUuid) as BitmapDrawable; if (iconDrawable != null) { var pipe = ParcelFileDescriptor.CreatePipe(); @@ -273,7 +271,7 @@ namespace keepass2android.search intlResourceId = Resource.String.entry_url; break; case PwDefs.NotesField: - intlResourceId = Resource.String.entry_notes; + intlResourceId = Resource.String.entry_comment; break; case PwGroup.SearchContextTags: intlResourceId = Resource.String.entry_tags; diff --git a/src/keepass2android/search/SearchResults.cs b/src/keepass2android/search/SearchResults.cs index e5a6ca75..56920186 100644 --- a/src/keepass2android/search/SearchResults.cs +++ b/src/keepass2android/search/SearchResults.cs @@ -82,7 +82,7 @@ namespace keepass2android.search private void Query (SearchParameters searchParams) { try { - Group = _db.Search (searchParams); + Group = _db.Search (searchParams, null); } catch (Exception e) { Toast.MakeText(this,e.Message, ToastLength.Long).Show(); Finish(); From a10d22010e92e51eb9f9971272eed49756b4cc47 Mon Sep 17 00:00:00 2001 From: AlexVallat Date: Thu, 20 Jun 2013 20:29:07 +0100 Subject: [PATCH 5/6] CloneDeep fixes --- src/KeePassLib2Android/PwEntry.cs | 5 +++++ src/KeePassLib2Android/PwGroup.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/KeePassLib2Android/PwEntry.cs b/src/KeePassLib2Android/PwEntry.cs index 8e587b5a..b54e5d2f 100644 --- a/src/KeePassLib2Android/PwEntry.cs +++ b/src/KeePassLib2Android/PwEntry.cs @@ -383,6 +383,11 @@ namespace KeePassLib peNew.m_tExpire = m_tExpire; peNew.m_bExpires = m_bExpires; peNew.m_uUsageCount = m_uUsageCount; + + peNew.m_tCreationLazy = m_tCreationLazy; + peNew.m_tLastModLazy = m_tLastModLazy; + peNew.m_tLastAccessLazy = m_tLastAccessLazy; + peNew.m_tExpireLazy = m_tExpireLazy; peNew.m_strOverrideUrl = m_strOverrideUrl; diff --git a/src/KeePassLib2Android/PwGroup.cs b/src/KeePassLib2Android/PwGroup.cs index fa0e2802..c7070d82 100644 --- a/src/KeePassLib2Android/PwGroup.cs +++ b/src/KeePassLib2Android/PwGroup.cs @@ -396,6 +396,11 @@ namespace KeePassLib pg.m_bExpires = m_bExpires; pg.m_uUsageCount = m_uUsageCount; + pg.m_tCreationLazy = m_tCreationLazy; + pg.m_tLastModLazy = m_tLastModLazy; + pg.m_tLastAccessLazy = m_tLastAccessLazy; + pg.m_tExpireLazy = m_tExpireLazy; + pg.m_bIsExpanded = m_bIsExpanded; pg.m_bVirtual = m_bVirtual; From 0b1f372a022e04beba18f1dc3209352f346899dc Mon Sep 17 00:00:00 2001 From: AlexVallat Date: Fri, 21 Jun 2013 17:56:50 +0100 Subject: [PATCH 6/6] Bug fixing --- src/KeePassLib2Android/PwEntry.cs | 24 +++++++++++--------- src/KeePassLib2Android/PwGroup.cs | 11 +++++---- src/keepass2android/Resources/menu/group.xml | 13 ++++++++--- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/KeePassLib2Android/PwEntry.cs b/src/KeePassLib2Android/PwEntry.cs index b54e5d2f..228eebf3 100644 --- a/src/KeePassLib2Android/PwEntry.cs +++ b/src/KeePassLib2Android/PwEntry.cs @@ -365,6 +365,7 @@ namespace KeePassLib peNew.m_uuid = m_uuid; // PwUuid is immutable peNew.m_pParentGroup = m_pParentGroup; peNew.m_tParentGroupLastMod = m_tParentGroupLastMod; + peNew.m_tParentGroupLastModLazy = m_tParentGroupLastModLazy; peNew.m_listStrings = m_listStrings.CloneDeep(); peNew.m_listBinaries = m_listBinaries.CloneDeep(); @@ -402,6 +403,7 @@ namespace KeePassLib peNew.m_uuid = m_uuid; // PwUuid is immutable peNew.m_tParentGroupLastMod = m_tParentGroupLastMod; + peNew.m_tParentGroupLastModLazy = m_tParentGroupLastModLazy; // Do not assign m_pParentGroup return peNew; @@ -454,7 +456,7 @@ namespace KeePassLib if((pwOpt & PwCompareOptions.IgnoreParentGroup) == PwCompareOptions.None) { if(m_pParentGroup != pe.m_pParentGroup) return false; - if(!bIgnoreLastMod && (m_tParentGroupLastMod != pe.m_tParentGroupLastMod)) + if(!bIgnoreLastMod && (LocationChanged != pe.LocationChanged)) return false; } @@ -498,10 +500,10 @@ namespace KeePassLib if(m_clrForeground != pe.m_clrForeground) return false; if(m_clrBackground != pe.m_clrBackground) return false; - if(m_tCreation != pe.m_tCreation) return false; - if(!bIgnoreLastMod && (m_tLastMod != pe.m_tLastMod)) return false; - if(!bIgnoreLastAccess && (m_tLastAccess != pe.m_tLastAccess)) return false; - if(m_tExpire != pe.m_tExpire) return false; + if(CreationTime != pe.CreationTime) return false; + if(!bIgnoreLastMod && (LastModificationTime != pe.LastModificationTime)) return false; + if(!bIgnoreLastAccess && (LastAccessTime != pe.LastAccessTime)) return false; + if(ExpiryTime != pe.ExpiryTime) return false; if(m_bExpires != pe.m_bExpires) return false; if(!bIgnoreLastAccess && (m_uUsageCount != pe.m_uUsageCount)) return false; @@ -531,14 +533,14 @@ namespace KeePassLib { Debug.Assert(peTemplate != null); if(peTemplate == null) throw new ArgumentNullException("peTemplate"); - if(bOnlyIfNewer && (peTemplate.m_tLastMod < m_tLastMod)) return; + if(bOnlyIfNewer && (peTemplate.LastModificationTime < LastModificationTime)) return; // Template UUID should be the same as the current one Debug.Assert(m_uuid.EqualsValue(peTemplate.m_uuid)); m_uuid = peTemplate.m_uuid; if(bAssignLocationChanged) - m_tParentGroupLastMod = peTemplate.m_tParentGroupLastMod; + m_tParentGroupLastMod = peTemplate.LocationChanged; m_listStrings = peTemplate.m_listStrings; m_listBinaries = peTemplate.m_listBinaries; @@ -551,10 +553,10 @@ namespace KeePassLib m_clrForeground = peTemplate.m_clrForeground; m_clrBackground = peTemplate.m_clrBackground; - m_tCreation = peTemplate.m_tCreation; - m_tLastMod = peTemplate.m_tLastMod; - m_tLastAccess = peTemplate.m_tLastAccess; - m_tExpire = peTemplate.m_tExpire; + m_tCreation = peTemplate.CreationTime; + m_tLastMod = peTemplate.LastModificationTime; + m_tLastAccess = peTemplate.LastAccessTime; + m_tExpire = peTemplate.ExpiryTime; m_bExpires = peTemplate.m_bExpires; m_uUsageCount = peTemplate.m_uUsageCount; diff --git a/src/KeePassLib2Android/PwGroup.cs b/src/KeePassLib2Android/PwGroup.cs index c7070d82..be00d0fb 100644 --- a/src/KeePassLib2Android/PwGroup.cs +++ b/src/KeePassLib2Android/PwGroup.cs @@ -382,6 +382,7 @@ namespace KeePassLib pg.m_listEntries = m_listEntries.CloneDeep(); pg.m_pParentGroup = m_pParentGroup; pg.m_tParentGroupLastMod = m_tParentGroupLastMod; + pg.m_tParentGroupLastModLazy = m_tParentGroupLastModLazy; pg.m_strName = m_strName; pg.m_strNotes = m_strNotes; @@ -441,7 +442,7 @@ namespace KeePassLib { Debug.Assert(pgTemplate != null); if(pgTemplate == null) throw new ArgumentNullException("pgTemplate"); - if(bOnlyIfNewer && (pgTemplate.m_tLastMod < m_tLastMod)) return; + if(bOnlyIfNewer && (pgTemplate.LastModificationTime < LastModificationTime)) return; // Template UUID should be the same as the current one Debug.Assert(m_uuid.EqualsValue(pgTemplate.m_uuid)); @@ -456,10 +457,10 @@ namespace KeePassLib m_pwIcon = pgTemplate.m_pwIcon; m_pwCustomIconID = pgTemplate.m_pwCustomIconID; - m_tCreation = pgTemplate.m_tCreation; - m_tLastMod = pgTemplate.m_tLastMod; - m_tLastAccess = pgTemplate.m_tLastAccess; - m_tExpire = pgTemplate.m_tExpire; + m_tCreation = pgTemplate.CreationTime; + m_tLastMod = pgTemplate.LastModificationTime; + m_tLastAccess = pgTemplate.LastAccessTime; + m_tExpire = pgTemplate.ExpiryTime; m_bExpires = pgTemplate.m_bExpires; m_uUsageCount = pgTemplate.m_uUsageCount; diff --git a/src/keepass2android/Resources/menu/group.xml b/src/keepass2android/Resources/menu/group.xml index 10063961..4a516d20 100644 --- a/src/keepass2android/Resources/menu/group.xml +++ b/src/keepass2android/Resources/menu/group.xml @@ -24,9 +24,16 @@ android:icon="@android:drawable/ic_lock_lock" android:title="@string/menu_lock" /> - +