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 05457bb1..a02f98b2 100644 --- a/src/KeePassLib2Android/KeePassLib2Android.csproj +++ b/src/KeePassLib2Android/KeePassLib2Android.csproj @@ -20,7 +20,7 @@ full False bin\Debug - DEBUG; + TRACE;DEBUG; prompt 4 False diff --git a/src/KeePassLib2Android/PwEntry.cs b/src/KeePassLib2Android/PwEntry.cs index b26bec6f..228eebf3 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; } /// @@ -333,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(); @@ -351,6 +384,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; @@ -365,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; @@ -417,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; } @@ -461,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; @@ -494,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; @@ -514,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; @@ -844,6 +883,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..be00d0fb 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; @@ -42,6 +47,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 +60,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 +158,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 +182,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 +196,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; } /// @@ -344,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; @@ -358,6 +397,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; @@ -398,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)); @@ -413,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; @@ -673,6 +717,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; } @@ -683,7 +739,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; } @@ -715,7 +771,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; @@ -740,7 +796,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(); @@ -823,42 +879,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) { @@ -866,7 +922,7 @@ namespace KeePassLib { if(listStorage.UCount != uInitialResults) break; // Match - SearchEvalAdd(sp, strTag, rx, pe, listStorage); + SearchEvalAdd(sp, strTag, rx, pe, listStorage, resultContexts, SearchContextTags); } } @@ -880,28 +936,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() @@ -1422,6 +1509,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/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/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/Kp2aBusinessLogic/SearchDbHelper.cs b/src/Kp2aBusinessLogic/SearchDbHelper.cs index b3c77733..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; @@ -40,9 +41,9 @@ namespace keepass2android { SearchParameters sp = new SearchParameters {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 @@ -56,7 +57,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/Kp2aBusinessLogic/database/Database.cs b/src/Kp2aBusinessLogic/database/Database.cs index a5cdf677..e939857c 100644 --- a/src/Kp2aBusinessLogic/database/Database.cs +++ b/src/Kp2aBusinessLogic/database/Database.cs @@ -160,9 +160,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 eb52839c..41602145 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; @@ -151,7 +152,7 @@ namespace keepass2android Finish(); } } - + private String getDateTime(DateTime dt) { return dt.ToString ("g", CultureInfo.CurrentUICulture); @@ -178,16 +179,12 @@ namespace keepass2android extraGroup.RemoveAllViews(); } bool hasExtraFields = false; - foreach (KeyValuePair pair in Entry.Strings) + foreach (var view in from pair in Entry.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; } @@ -276,8 +273,8 @@ namespace keepass2android return Android.Net.Uri.Parse("content://" + AttachmentContentProvider.Authority + "/" + filename); } - return fileUri; - } + return fileUri; + } void OpenBinaryFile(Android.Net.Uri uri) { @@ -323,17 +320,17 @@ namespace keepass2android builder.SetMessage(GetString(Resource.String.SaveAttachmentDialog_text)); builder.SetPositiveButton(GetString(Resource.String.SaveAttachmentDialog_save), (dlgSender, dlgEvt) => - { + { WriteBinaryToFile(btnSender.Text, false); }); builder.SetNegativeButton(GetString(Resource.String.SaveAttachmentDialog_open), (dlgSender, dlgEvt) => - { + { Android.Net.Uri newUri = WriteBinaryToFile(btnSender.Text, true); - if (newUri != null) - { + if (newUri != null) + { OpenBinaryFile(newUri); - } + } }); Dialog dialog = builder.Create(); diff --git a/src/keepass2android/EntryEditActivity.cs b/src/keepass2android/EntryEditActivity.cs index 9fd89b12..aeae3d8c 100644 --- a/src/keepass2android/EntryEditActivity.cs +++ b/src/keepass2android/EntryEditActivity.cs @@ -118,7 +118,7 @@ namespace keepass2android entryId = new PwUuid(MemUtil.HexStringToByteArray(uuidBytes)); State.ParentGroup = null; - if (entryId == PwUuid.Zero) + if (entryId.EqualsValue(PwUuid.Zero)) { String groupId = i.GetStringExtra(KeyParent); @@ -487,12 +487,12 @@ namespace keepass2android builder.SetMessage(GetString(Resource.String.AskOverwriteBinary)); builder.SetPositiveButton(GetString(Resource.String.AskOverwriteBinary_yes), (dlgSender, dlgEvt) => - { + { AddBinary(filename, true); }); builder.SetNegativeButton(GetString(Resource.String.AskOverwriteBinary_no), (dlgSender, dlgEvt) => - { + { AddBinary(filename, false); }); @@ -532,9 +532,9 @@ namespace keepass2android try { byte[] vBytes = File.ReadAllBytes(filename); - ProtectedBinary pb = new ProtectedBinary(false, vBytes); + ProtectedBinary pb = new ProtectedBinary(false, vBytes); State.Entry.Binaries.Set(strItem, pb); - } + } catch(Exception exAttach) { Toast.MakeText(this, GetString(Resource.String.AttachFailed)+" "+exAttach.Message, ToastLength.Long).Show(); @@ -562,13 +562,13 @@ namespace keepass2android builder.SetMessage(GetString(Resource.String.AskDiscardChanges)); builder.SetPositiveButton(GetString(Android.Resource.String.Yes), (dlgSender, dlgEvt) => - { - Finish(); + { + Finish(); }); builder.SetNegativeButton(GetString(Android.Resource.String.No), (dlgSender, dlgEvt) => - { + { }); diff --git a/src/keepass2android/GroupBaseActivity.cs b/src/keepass2android/GroupBaseActivity.cs index e54468ff..17f959b5 100644 --- a/src/keepass2android/GroupBaseActivity.cs +++ b/src/keepass2android/GroupBaseActivity.cs @@ -194,7 +194,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; } @@ -238,8 +243,9 @@ namespace keepass2android SetResult(KeePass.ExitLock); 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" + /> + - + 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 93028dc8..d01c711b 100644 --- a/src/keepass2android/icons/DrawableFactory.cs +++ b/src/keepass2android/icons/DrawableFactory.cs @@ -53,13 +53,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); } return GetIconDrawable (res, icon); } - private static void InitBlank (Resources res) + private static void InitBlank (Resources res) { if (_blank == null) { _blank = res.GetDrawable (Resource.Drawable.ic99_blank); @@ -85,7 +85,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; diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index eb5404c8..18478ec4 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -85,6 +85,7 @@ + diff --git a/src/keepass2android/search/SearchProvider.cs b/src/keepass2android/search/SearchProvider.cs new file mode 100644 index 00000000..b806fede --- /dev/null +++ b/src/keepass2android/search/SearchProvider.cs @@ -0,0 +1,307 @@ +/* +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"; + + private Database _db; + + 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() + { + _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 (_db.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 = _db.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 = _db.DrawableFactory.GetIconDrawable(App.Context.Resources, _db.KpDatabase, 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_comment; + 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 ceca0626..56920186 100644 --- a/src/keepass2android/search/SearchResults.cs +++ b/src/keepass2android/search/SearchResults.cs @@ -22,13 +22,14 @@ using Android.OS; using Android.Widget; using keepass2android.view; using KeePassLib; +using Android.Support.V4.App; namespace keepass2android.search { /// /// Activity to show search results /// - [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 @@ -44,24 +45,44 @@ namespace keepass2android.search } SetResult(KeePass.ExitNormal); - + + ProcessIntent(Intent); + } + + protected override void OnNewIntent(Intent intent) + { + ProcessIntent(intent); + } + + private void ProcessIntent(Intent intent) + { _db = App.Kp2a.GetDb(); + // Likely the app has been killed exit the activity if ( ! _db.Open ) { Finish(); } - Query(getSearch(Intent)); - + if (intent.Action == Intent.ActionView) + { + var entryIntent = new Intent(this, typeof(EntryActivity)); + entryIntent.PutExtra(EntryActivity.KeyEntry, 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 { - Group = _db.Search (searchParams); + Group = _db.Search (searchParams, null); } catch (Exception e) { Toast.MakeText(this,e.Message, ToastLength.Long).Show(); Finish(); @@ -111,8 +132,8 @@ namespace keepass2android.search Finish(); return true; } - return false; + return false; + } } - } }