improve implementation of Oreo autofill (#9), now supporting all Android/W3cHints, using all Keepass fields (if hints match field name). Make hint comparison code clearer and always compare case insensitive

This commit is contained in:
Philipp Crocoll
2017-12-29 07:07:04 +01:00
parent fb018946b9
commit c8d56a237b
7 changed files with 289 additions and 166 deletions

View File

@@ -14,8 +14,8 @@ namespace keepass2android.services.AutofillBase
public class AutofillFieldMetadata
{
public SaveDataType SaveType { get; set; }
public string[] AutofillHints { get; set; }
public string[] AutofillCanonicalHints { get; set; }
public AutofillId AutofillId { get; }
public AutofillType AutofillType { get; }
@@ -29,14 +29,14 @@ namespace keepass2android.services.AutofillBase
AutofillOptions = view.GetAutofillOptions();
Focused = view.IsFocused;
var supportedHints = AutofillHintsHelper.FilterForSupportedHints(view.GetAutofillHints());
var storedHints = AutofillHintsHelper.ConvertToStoredHints(supportedHints);
SetHints(storedHints.ToArray());
var canonicalHints = AutofillHintsHelper.ConvertToCanonicalHints(supportedHints);
SetHints(canonicalHints.ToArray());
}
void SetHints(string[] value)
{
AutofillHints = value;
AutofillCanonicalHints = value;
UpdateSaveTypeFromHints();
}
@@ -62,11 +62,11 @@ namespace keepass2android.services.AutofillBase
{
//TODO future add savetypes for W3cHints
SaveType = 0;
if (AutofillHints == null)
if (AutofillCanonicalHints == null)
{
return;
}
foreach (var hint in AutofillHints)
foreach (var hint in AutofillCanonicalHints)
{
switch (hint)
{

View File

@@ -12,17 +12,19 @@ namespace keepass2android.services.AutofillBase
public class AutofillFieldMetadataCollection
{
List<AutofillId> AutofillIds = new List<AutofillId>();
Dictionary<string, List<AutofillFieldMetadata>> AutofillHintsToFieldsMap = new Dictionary<string, List<AutofillFieldMetadata>>();
public List<string> AllAutofillHints { get; }
public List<string> FocusedAutofillHints { get; }
Dictionary<string, List<AutofillFieldMetadata>> AutofillCanonicalHintsToFieldsMap = new Dictionary<string, List<AutofillFieldMetadata>>();
public List<string> AllAutofillCanonicalHints { get; }
public List<string> FocusedAutofillCanonicalHints { get; }
int Size = 0;
public SaveDataType SaveType { get; set; }
public AutofillFieldMetadataCollection()
{
SaveType = 0;
FocusedAutofillHints = new List<string>();
AllAutofillHints = new List<string>();
FocusedAutofillCanonicalHints = new List<string>();
AllAutofillCanonicalHints = new List<string>();
}
public void Add(AutofillFieldMetadata autofillFieldMetadata)
@@ -30,19 +32,19 @@ namespace keepass2android.services.AutofillBase
SaveType |= autofillFieldMetadata.SaveType;
Size++;
AutofillIds.Add(autofillFieldMetadata.AutofillId);
var hintsList = autofillFieldMetadata.AutofillHints;
AllAutofillHints.AddRange(hintsList);
var hintsList = autofillFieldMetadata.AutofillCanonicalHints;
AllAutofillCanonicalHints.AddRange(hintsList);
if (autofillFieldMetadata.Focused)
{
FocusedAutofillHints.AddRange(hintsList);
FocusedAutofillCanonicalHints.AddRange(hintsList);
}
foreach (var hint in autofillFieldMetadata.AutofillHints)
foreach (var hint in autofillFieldMetadata.AutofillCanonicalHints)
{
if (!AutofillHintsToFieldsMap.ContainsKey(hint))
if (!AutofillCanonicalHintsToFieldsMap.ContainsKey(hint))
{
AutofillHintsToFieldsMap.Add(hint, new List<AutofillFieldMetadata>());
AutofillCanonicalHintsToFieldsMap.Add(hint, new List<AutofillFieldMetadata>());
}
AutofillHintsToFieldsMap[hint].Add(autofillFieldMetadata);
AutofillCanonicalHintsToFieldsMap[hint].Add(autofillFieldMetadata);
}
}
@@ -51,9 +53,17 @@ namespace keepass2android.services.AutofillBase
return AutofillIds.ToArray();
}
public List<AutofillFieldMetadata> GetFieldsForHint(String hint)
/// <summary>
/// returns the fields for the given hint or an empty list.
/// </summary>
public List<AutofillFieldMetadata> GetFieldsForHint(String canonicalHint)
{
return AutofillHintsToFieldsMap[hint];
List<AutofillFieldMetadata> result;
if (!AutofillCanonicalHintsToFieldsMap.TryGetValue(canonicalHint, out result))
{
result = new List<AutofillFieldMetadata>();
}
return result;
}

View File

@@ -16,16 +16,76 @@ namespace keepass2android.services.AutofillBase
{
class AutofillHintsHelper
{
private static readonly HashSet<string> validHints = new HashSet<string>()
private static readonly HashSet<string> _allSupportedHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
View.AutofillHintUsername,
View.AutofillHintCreditCardExpirationDate,
View.AutofillHintCreditCardExpirationDay,
View.AutofillHintCreditCardExpirationMonth,
View.AutofillHintCreditCardExpirationYear,
View.AutofillHintCreditCardNumber,
View.AutofillHintCreditCardSecurityCode,
View.AutofillHintEmailAddress,
View.AutofillHintPhone,
View.AutofillHintName,
View.AutofillHintPassword,
View.AutofillHintPostalAddress,
View.AutofillHintPostalCode,
View.AutofillHintUsername,
W3cHints.HONORIFIC_PREFIX,
W3cHints.NAME,
W3cHints.GIVEN_NAME,
W3cHints.ADDITIONAL_NAME,
W3cHints.FAMILY_NAME,
W3cHints.HONORIFIC_SUFFIX,
W3cHints.USERNAME,
W3cHints.NEW_PASSWORD,
W3cHints.CURRENT_PASSWORD,
W3cHints.NEW_PASSWORD
W3cHints.ORGANIZATION_TITLE,
W3cHints.ORGANIZATION,
W3cHints.STREET_ADDRESS,
W3cHints.ADDRESS_LINE1,
W3cHints.ADDRESS_LINE2,
W3cHints.ADDRESS_LINE3,
W3cHints.ADDRESS_LEVEL4,
W3cHints.ADDRESS_LEVEL3,
W3cHints.ADDRESS_LEVEL2,
W3cHints.ADDRESS_LEVEL1,
W3cHints.COUNTRY,
W3cHints.COUNTRY_NAME,
W3cHints.POSTAL_CODE,
W3cHints.CC_NAME,
W3cHints.CC_GIVEN_NAME,
W3cHints.CC_ADDITIONAL_NAME,
W3cHints.CC_FAMILY_NAME,
W3cHints.CC_NUMBER,
W3cHints.CC_EXPIRATION,
W3cHints.CC_EXPIRATION_MONTH,
W3cHints.CC_EXPIRATION_YEAR,
W3cHints.CC_CSC,
W3cHints.CC_TYPE,
W3cHints.TRANSACTION_CURRENCY,
W3cHints.TRANSACTION_AMOUNT,
W3cHints.LANGUAGE,
W3cHints.BDAY,
W3cHints.BDAY_DAY,
W3cHints.BDAY_MONTH,
W3cHints.BDAY_YEAR,
W3cHints.SEX,
W3cHints.URL,
W3cHints.PHOTO,
W3cHints.TEL,
W3cHints.TEL_COUNTRY_CODE,
W3cHints.TEL_NATIONAL,
W3cHints.TEL_AREA_CODE,
W3cHints.TEL_LOCAL,
W3cHints.TEL_LOCAL_PREFIX,
W3cHints.TEL_LOCAL_SUFFIX,
W3cHints.TEL_EXTENSION,
W3cHints.EMAIL,
W3cHints.IMPP,
};
private static readonly Dictionary<string, string> hintReplacements= new Dictionary<string, string>()
private static readonly Dictionary<string, string> hintToCanonicalReplacement= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{W3cHints.EMAIL, View.AutofillHintEmailAddress},
{W3cHints.USERNAME, View.AutofillHintUsername},
@@ -41,9 +101,9 @@ namespace keepass2android.services.AutofillBase
};
public static bool IsValidHint(string hint)
public static bool IsSupportedHint(string hint)
{
return validHints.Contains(hint);
return _allSupportedHints.Contains(hint);
}
@@ -53,7 +113,7 @@ namespace keepass2android.services.AutofillBase
int i = 0;
foreach (var hint in hints)
{
if (IsValidHint(hint))
if (IsSupportedHint(hint))
{
filteredHints[i++] = hint;
}
@@ -69,16 +129,18 @@ namespace keepass2android.services.AutofillBase
public static List<string> ConvertToStoredHints(string[] supportedHints)
/// <summary>
/// transforms hints by replacing some W3cHints by their Android counterparts and transforming everything to lowercase
/// </summary>
public static List<string> ConvertToCanonicalHints(string[] supportedHints)
{
List<string> result = new List<string>();
foreach (string hint in supportedHints)
{
string storedHint = hint;
if (hintReplacements.ContainsKey(hint))
storedHint = hintReplacements[hint];
result.Add(storedHint);
string canonicalHint;
if (!hintToCanonicalReplacement.TryGetValue(hint, out canonicalHint))
canonicalHint = hint;
result.Add(canonicalHint.ToLower());
}
return result;

View File

@@ -1,25 +1,89 @@
using Android.App.Assist;
using System.Collections.Generic;
using Android.App.Assist;
using Android.Views.Autofill;
namespace keepass2android.services.AutofillBase.model
{
public class FilledAutofillField
{
public string TextValue { get; set; }
private string[] _autofillHints;
public string TextValue { get; set; }
public long? DateValue { get; set; }
public bool? ToggleValue { get; set; }
public string[] AutofillHints { get; set; }
public FilledAutofillField()
/// <summary>
/// returns the autofill hints for the filled field. These are always lowercased for simpler string comparison.
/// </summary>
public string[] AutofillHints
{
get
{
return _autofillHints;
}
set
{
_autofillHints = value;
for (int i = 0; i < _autofillHints.Length; i++)
_autofillHints[i] = _autofillHints[i].ToLower();
}
}
public bool Protected { get; set; }
public FilledAutofillField()
{}
public FilledAutofillField(AssistStructure.ViewNode viewNode)
{
string[] rawHints = AutofillHintsHelper.FilterForSupportedHints(viewNode.GetAutofillHints());
List<string> hintList = new List<string>();
string nextHint = null;
for (int i = 0; i < rawHints.Length; i++)
{
string hint = rawHints[i];
if (i < rawHints.Length - 1)
{
nextHint = rawHints[i + 1];
}
// First convert the compound W3C autofill hints
if (W3cHints.isW3cSectionPrefix(hint) && i < rawHints.Length - 1)
{
hint = rawHints[++i];
CommonUtil.logd($"Hint is a W3C section prefix; using {hint} instead");
if (i < rawHints.Length - 1)
{
nextHint = rawHints[i + 1];
}
}
if (W3cHints.isW3cTypePrefix(hint) && nextHint != null && W3cHints.isW3cTypeHint(nextHint))
{
hint = nextHint;
i++;
CommonUtil.logd($"Hint is a W3C type prefix; using {hint} instead");
}
if (W3cHints.isW3cAddressType(hint) && nextHint != null)
{
hint = nextHint;
i++;
CommonUtil.logd($"Hint is a W3C address prefix; using {hint} instead");
}
/*public FilledAutofillField(AssistStructure.ViewNode viewNode)
{
AutofillHintsHelper = AutofillHelper.FilterForSupportedHints(viewNode.GetAutofillHints());
// Then check if the "actual" hint is supported.
if (AutofillHintsHelper.IsSupportedHint(hint))
{
hintList.Add(hint);
}
else
{
CommonUtil.loge($"Invalid hint: {rawHints[i]}");
}
}
AutofillHints = hintList.ToArray();
//TODO port updated FilledAutofillField?
//TODO port updated FilledAutofillField
AutofillValue autofillValue = viewNode.AutofillValue;
if (autofillValue != null)
{
@@ -41,9 +105,9 @@ namespace keepass2android.services.AutofillBase.model
TextValue = autofillValue.TextValue;
}
}
}*/
}
public bool IsNull()
public bool IsNull()
{
return TextValue == null && DateValue == null && ToggleValue == null;
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Android.Service.Autofill;
using Android.Util;
using Android.Views;
@@ -12,121 +13,48 @@ namespace keepass2android.services.AutofillBase.model
/// </summary>
public class FilledAutofillFieldCollection
{
public Dictionary<string, FilledAutofillField> HintMap { get; set; }
public Dictionary<string, FilledAutofillField> HintMap { get; }
public string DatasetName { get; set; }
public FilledAutofillFieldCollection(Dictionary<string, FilledAutofillField> hintMap, string datasetName = "")
{
HintMap = hintMap;
//recreate hint map making sure we compare case insensitive
HintMap = BuildHintMap();
foreach (var p in hintMap)
HintMap.Add(p.Key, p.Value);
DatasetName = datasetName;
}
public FilledAutofillFieldCollection() : this(new Dictionary<string, FilledAutofillField>())
public FilledAutofillFieldCollection() : this(BuildHintMap())
{}
/// <summary>
private static Dictionary<string, FilledAutofillField> BuildHintMap()
{
return new Dictionary<string, FilledAutofillField>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Adds a filledAutofillField to the collection, indexed by all of its hints.
/// </summary>
/// <returns>The add.</returns>
/// <param name="filledAutofillField">Filled autofill field.</param>
public void Add(FilledAutofillField filledAutofillField)
{
string[] autofillHints = filledAutofillField.AutofillHints;
string nextHint = null;
for (int i = 0; i < autofillHints.Length; i++)
{
string hint = autofillHints[i];
if (i < autofillHints.Length - 1)
{
nextHint = autofillHints[i + 1];
}
// First convert the compound W3C autofill hints
if (isW3cSectionPrefix(hint) && i < autofillHints.Length - 1)
{
hint = autofillHints[++i];
CommonUtil.logd($"Hint is a W3C section prefix; using {hint} instead");
if (i < autofillHints.Length - 1)
{
nextHint = autofillHints[i + 1];
}
}
if (isW3cTypePrefix(hint) && nextHint != null && isW3cTypeHint(nextHint))
{
hint = nextHint;
i++;
CommonUtil.logd($"Hint is a W3C type prefix; using {hint} instead");
}
if (isW3cAddressType(hint) && nextHint != null)
{
hint = nextHint;
i++;
CommonUtil.logd($"Hint is a W3C address prefix; using {hint} instead");
}
// Then check if the "actual" hint is supported.
if (AutofillHintsHelper.IsValidHint(hint))
foreach (string hint in filledAutofillField.AutofillHints)
{
if (AutofillHintsHelper.IsSupportedHint(hint))
{
HintMap.Add(hint, filledAutofillField);
}
else
{
CommonUtil.loge($"Invalid hint: {autofillHints[i]}");
CommonUtil.loge($"Invalid hint: {hint}");
}
}
}
private static bool isW3cSectionPrefix(string hint)
{
return hint.StartsWith(W3cHints.PREFIX_SECTION);
}
private static bool isW3cAddressType(string hint)
{
switch (hint)
{
case W3cHints.SHIPPING:
case W3cHints.BILLING:
return true;
}
return false;
}
private static bool isW3cTypePrefix(string hint)
{
switch (hint)
{
case W3cHints.PREFIX_WORK:
case W3cHints.PREFIX_FAX:
case W3cHints.PREFIX_HOME:
case W3cHints.PREFIX_PAGER:
return true;
}
return false;
}
private static bool isW3cTypeHint(string hint)
{
switch (hint)
{
case W3cHints.TEL:
case W3cHints.TEL_COUNTRY_CODE:
case W3cHints.TEL_NATIONAL:
case W3cHints.TEL_AREA_CODE:
case W3cHints.TEL_LOCAL:
case W3cHints.TEL_LOCAL_PREFIX:
case W3cHints.TEL_LOCAL_SUFFIX:
case W3cHints.TEL_EXTENSION:
case W3cHints.EMAIL:
case W3cHints.IMPP:
return true;
}
Log.Warn(CommonUtil.Tag, "Invalid W3C type hint: " + hint);
return false;
}
/// <summary>
/// Populates a Dataset.Builder with appropriate values for each AutofillId
/// in a AutofillFieldMetadataCollection.
@@ -142,16 +70,10 @@ namespace keepass2android.services.AutofillBase.model
public bool ApplyToFields(AutofillFieldMetadataCollection autofillFieldMetadataCollection, Dataset.Builder datasetBuilder)
{
bool setValueAtLeastOnce = false;
List<string> allHints = autofillFieldMetadataCollection.AllAutofillHints;
for (int hintIndex = 0; hintIndex < allHints.Count; hintIndex++)
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
string hint = allHints[hintIndex];
List<AutofillFieldMetadata> fillableAutofillFields = autofillFieldMetadataCollection.GetFieldsForHint(hint);
if (fillableAutofillFields == null)
{
continue;
}
foreach (AutofillFieldMetadata autofillFieldMetadata in fillableAutofillFields)
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection.GetFieldsForHint(hint))
{
FilledAutofillField filledAutofillField;
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))

View File

@@ -1,4 +1,6 @@
namespace keepass2android.services.AutofillBase.model
using Android.Util;
namespace keepass2android.services.AutofillBase.model
{
public class W3cHints
{
@@ -70,5 +72,56 @@
private W3cHints()
{
}
public static bool isW3cSectionPrefix(string hint)
{
return hint.ToLower().StartsWith(W3cHints.PREFIX_SECTION);
}
public static bool isW3cAddressType(string hint)
{
switch (hint.ToLower())
{
case W3cHints.SHIPPING:
case W3cHints.BILLING:
return true;
}
return false;
}
public static bool isW3cTypePrefix(string hint)
{
switch (hint.ToLower())
{
case W3cHints.PREFIX_WORK:
case W3cHints.PREFIX_FAX:
case W3cHints.PREFIX_HOME:
case W3cHints.PREFIX_PAGER:
return true;
}
return false;
}
public static bool isW3cTypeHint(string hint)
{
switch (hint.ToLower())
{
case W3cHints.TEL:
case W3cHints.TEL_COUNTRY_CODE:
case W3cHints.TEL_NATIONAL:
case W3cHints.TEL_AREA_CODE:
case W3cHints.TEL_LOCAL:
case W3cHints.TEL_LOCAL_PREFIX:
case W3cHints.TEL_LOCAL_SUFFIX:
case W3cHints.TEL_EXTENSION:
case W3cHints.EMAIL:
case W3cHints.IMPP:
return true;
}
Log.Warn(CommonUtil.Tag, "Invalid W3C type hint: " + hint);
return false;
}
}
}

View File

@@ -41,32 +41,44 @@ namespace keepass2android.services.Kp2aAutofill
if (!App.Kp2a.GetDb().Loaded || (App.Kp2a.QuickLocked))
return null;
string username = App.Kp2a.GetDb().LastOpenedEntry.Entry.Strings.ReadSafe(PwDefs.UserNameField);
string password = App.Kp2a.GetDb().LastOpenedEntry.Entry.Strings.ReadSafe(PwDefs.PasswordField);
FilledAutofillField pwdField =
new FilledAutofillField
{
AutofillHints = new[] {View.AutofillHintPassword},
TextValue = password
};
FilledAutofillField userField = new FilledAutofillField
{
AutofillHints = new[] {View.AutofillHintUsername},
TextValue = username
};
FilledAutofillFieldCollection fieldCollection = new FilledAutofillFieldCollection();
fieldCollection.HintMap = new Dictionary<string, FilledAutofillField>();
fieldCollection.Add(userField);
fieldCollection.Add(pwdField);
fieldCollection.DatasetName = App.Kp2a.GetDb().LastOpenedEntry.Entry.Strings.ReadSafe(PwDefs.TitleField);
var pwEntry = App.Kp2a.GetDb().LastOpenedEntry.Entry;
foreach (string key in pwEntry.Strings.GetKeys())
{
FilledAutofillField field =
new FilledAutofillField
{
AutofillHints = new[] { GetCanonicalHintFromKp2aField(pwEntry, key) },
TextValue = pwEntry.Strings.ReadSafe(key),
Protected = pwEntry.Strings.Get(key).IsProtected
};
fieldCollection.Add(field);
}
//TODO add support for Keepass templates
//TODO add values like expiration?
//TODO if cc-exp is there, also set cc-exp-month etc.
fieldCollection.DatasetName = pwEntry.Strings.ReadSafe(PwDefs.TitleField);
return fieldCollection;
}
private static readonly Dictionary<string, string> keyToHint = new Dictionary<string, string>()
{
{PwDefs.UserNameField, View.AutofillHintUsername },
{PwDefs.PasswordField, View.AutofillHintPassword },
{PwDefs.UrlField, W3cHints.URL },
};
private string GetCanonicalHintFromKp2aField(PwEntry pwEntry, string key)
{
if (!keyToHint.TryGetValue(key, out string result))
result = key;
result = result.ToLower();
return result;
}
public override IAutofillIntentBuilder IntentBuilder => new Kp2aAutofillIntentBuilder();
}