allow to fill single inputs (or autofill=off) with autofill (#9)

This commit is contained in:
Philipp Crocoll
2017-12-31 10:52:40 +01:00
parent e4c6285fab
commit 1857dd72b9
6 changed files with 119 additions and 86 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.Service.Autofill;
using Android.Views.Autofill;
@@ -11,9 +12,9 @@ namespace keepass2android.services.AutofillBase
/// </summary>
public class AutofillFieldMetadataCollection
{
List<AutofillId> AutofillIds = new List<AutofillId>();
Dictionary<string, List<AutofillFieldMetadata>> AutofillCanonicalHintsToFieldsMap = new Dictionary<string, List<AutofillFieldMetadata>>();
readonly List<AutofillId> AutofillIds = new List<AutofillId>();
readonly Dictionary<string, List<AutofillFieldMetadata>> AutofillCanonicalHintsToFieldsMap = new Dictionary<string, List<AutofillFieldMetadata>>();
public List<string> AllAutofillCanonicalHints { get; }
public List<string> FocusedAutofillCanonicalHints { get; }
@@ -34,16 +35,21 @@ namespace keepass2android.services.AutofillBase
public void Add(AutofillFieldMetadata autofillFieldMetadata)
{
SaveType |= autofillFieldMetadata.SaveType;
var hintsList = autofillFieldMetadata.AutofillCanonicalHints;
if (!hintsList.Any())
return;
if (AutofillIds.Contains(autofillFieldMetadata.AutofillId))
return;
SaveType |= autofillFieldMetadata.SaveType;
Size++;
AutofillIds.Add(autofillFieldMetadata.AutofillId);
var hintsList = autofillFieldMetadata.AutofillCanonicalHints;
AllAutofillCanonicalHints.AddRange(hintsList);
if (autofillFieldMetadata.Focused)
{
FocusedAutofillCanonicalHints.AddRange(hintsList);
}
foreach (var hint in autofillFieldMetadata.AutofillCanonicalHints)
foreach (var hint in hintsList)
{
if (!AutofillCanonicalHintsToFieldsMap.ContainsKey(hint))
{

View File

@@ -14,34 +14,22 @@ namespace keepass2android.services.AutofillBase
/// </summary>
public class AutofillHelper
{
/// <summary>
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
/// client View.
/// </summary>
/// <returns>The dataset.</returns>
/// <param name="context">Context.</param>
/// <param name="autofillFields">Autofill fields.</param>
/// <param name="filledAutofillFieldCollection">Filled autofill field collection.</param>
/// <param name="datasetAuth">If set to <c>true</c> dataset auth.</param>
public static Dataset NewDataset(Context context,
AutofillFieldMetadataCollection autofillFields, FilledAutofillFieldCollection filledAutofillFieldCollection, bool datasetAuth, IAutofillIntentBuilder intentBuilder)
/// <summary>
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
/// client View.
/// </summary>
/// <returns>The dataset.</returns>
/// <param name="context">Context.</param>
/// <param name="autofillFields">Autofill fields.</param>
/// <param name="filledAutofillFieldCollection">Filled autofill field collection.</param>
public static Dataset NewDataset(Context context,
AutofillFieldMetadataCollection autofillFields, FilledAutofillFieldCollection filledAutofillFieldCollection, IAutofillIntentBuilder intentBuilder)
{
var datasetName = filledAutofillFieldCollection.DatasetName;
if (datasetName != null)
{
Dataset.Builder datasetBuilder;
if (datasetAuth)
{
datasetBuilder = new Dataset.Builder
(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
IntentSender sender = intentBuilder.GetAuthIntentSenderForDataset(context, datasetName);
datasetBuilder.SetAuthentication(sender);
}
else
{
datasetBuilder = new Dataset.Builder
(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
}
var datasetBuilder = new Dataset.Builder(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder);
if (setValueAtLeastOnce)
{
@@ -80,7 +68,7 @@ namespace keepass2android.services.AutofillBase
var filledAutofillFieldCollection = clientFormDataMap[datasetName];
if (filledAutofillFieldCollection != null)
{
var dataset = NewDataset(context, autofillFields, filledAutofillFieldCollection, datasetAuth, intentBuilder);
var dataset = NewDataset(context, autofillFields, filledAutofillFieldCollection, intentBuilder);
if (dataset != null)
{
responseBuilder.AddDataset(dataset);

View File

@@ -11,8 +11,7 @@ namespace keepass2android.services.AutofillBase
{
public interface IAutofillIntentBuilder
{
IntentSender GetAuthIntentSenderForResponse(Context context, string query);
IntentSender GetAuthIntentSenderForDataset(Context context, string dataset);
IntentSender GetAuthIntentSenderForResponse(Context context, string query, bool isManualRequest);
Intent GetRestartAppIntent(Context context);
int AppIconResource { get; }
@@ -33,7 +32,8 @@ namespace keepass2android.services.AutofillBase
public override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)
{
CommonUtil.logd( "onFillRequest");
bool isManual = (request.Flags & FillRequest.FlagManualRequest) != 0;
CommonUtil.logd( "onFillRequest " + (isManual ? "manual" : "auto"));
var structure = request.FillContexts[request.FillContexts.Count - 1].Structure;
//TODO support package signature verification as soon as this is supported in Keepass storage
@@ -50,7 +50,7 @@ namespace keepass2android.services.AutofillBase
var parser = new StructureParser(this, structure);
try
{
query = parser.ParseForFill();
query = parser.ParseForFill(isManual);
}
catch (Java.Lang.SecurityException e)
@@ -68,7 +68,7 @@ namespace keepass2android.services.AutofillBase
{
var responseBuilder = new FillResponse.Builder();
var sender = IntentBuilder.GetAuthIntentSenderForResponse(this, query);
var sender = IntentBuilder.GetAuthIntentSenderForResponse(this, query, isManual);
RemoteViews presentation = AutofillHelper.NewRemoteViews(PackageName, GetString(Resource.String.autofill_sign_in_prompt), AppNames.LauncherIcon);
var datasetBuilder = new Dataset.Builder(presentation);

View File

@@ -22,6 +22,7 @@ namespace keepass2android.services.AutofillBase
public static string ExtraQueryString => "EXTRA_QUERY_STRING";
public static string ExtraIsManualRequest => "EXTRA_IS_MANUAL_REQUEST";
public int RequestCodeQuery => 6245;
@@ -78,18 +79,18 @@ namespace keepass2android.services.AutofillBase
ReplyIntent = null;
}
protected void OnSuccess(FilledAutofillFieldCollection clientFormDataMap)
protected void OnSuccess(FilledAutofillFieldCollection clientFormDataMap, bool isManual)
{
var intent = Intent;
AssistStructure structure = (AssistStructure)intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure);
StructureParser parser = new StructureParser(this, structure);
parser.ParseForFill();
parser.ParseForFill(isManual);
AutofillFieldMetadataCollection autofillFields = parser.AutofillFields;
int partitionIndex = AutofillHintsHelper.GetPartitionIndex(autofillFields.FocusedAutofillCanonicalHints.First());
int partitionIndex = AutofillHintsHelper.GetPartitionIndex(autofillFields.FocusedAutofillCanonicalHints.FirstOrDefault());
FilledAutofillFieldCollection partitionData =
AutofillHintsHelper.FilterForPartition(clientFormDataMap, partitionIndex);
ReplyIntent = new Intent();
SetDatasetIntent(AutofillHelper.NewDataset(this, autofillFields, partitionData, false, IntentBuilder));
SetDatasetIntent(AutofillHelper.NewDataset(this, autofillFields, partitionData, IntentBuilder));
}
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
@@ -99,7 +100,7 @@ namespace keepass2android.services.AutofillBase
if (requestCode == RequestCodeQuery)
{
if (resultCode == ExpectedActivityResult)
OnSuccess(GetDataset(data));
OnSuccess(GetDataset(data), Intent.GetBooleanExtra(ExtraIsManualRequest, false));
else
OnFailure();
Finish();

View File

@@ -30,22 +30,23 @@ namespace keepass2android.services.AutofillBase
AutofillFields = new AutofillFieldMetadataCollection();
}
public string ParseForFill()
public string ParseForFill(bool isManual)
{
return Parse(true);
return Parse(true, isManual);
}
public string ParseForSave()
{
return Parse(false);
return Parse(false, true);
}
/// <summary>
/// Traverse AssistStructure and add ViewNode metadata to a flat list.
/// </summary>
/// <returns>The parse.</returns>
/// <param name="forFill">If set to <c>true</c> for fill.</param>
string Parse(bool forFill)
/// <summary>
/// Traverse AssistStructure and add ViewNode metadata to a flat list.
/// </summary>
/// <returns>The parse.</returns>
/// <param name="forFill">If set to <c>true</c> for fill.</param>
/// <param name="isManualRequest"></param>
string Parse(bool forFill, bool isManualRequest)
{
Log.Debug(CommonUtil.Tag, "Parsing structure for " + Structure.ActivityComponent);
var nodes = Structure.WindowNodeCount;
@@ -57,44 +58,52 @@ namespace keepass2android.services.AutofillBase
{
var node = Structure.GetWindowNodeAt(i);
var view = node.RootViewNode;
ParseLocked(forFill, view, ref webDomain);
ParseLocked(forFill, isManualRequest, view, ref webDomain);
}
if (AutofillFields.Empty)
{
var passwordFields = _editTextsWithoutHint
.Where(f =>
(!f.IdEntry?.ToLowerInvariant().Contains("search") ?? true) &&
(!f.Hint?.ToLowerInvariant().Contains("search") ?? true) &&
(
f.InputType.HasFlag(InputTypes.TextVariationPassword) ||
f.InputType.HasFlag(InputTypes.TextVariationVisiblePassword) ||
f.InputType.HasFlag(InputTypes.TextVariationWebPassword) ||
(f.HtmlInfo?.Attributes.Any(p => p.First.ToString() == "type" && p.Second.ToString() == "password") ?? false)
)
).ToList();
if (!_editTextsWithoutHint.Any())
.Where(IsPassword).ToList();
if (!passwordFields.Any())
{
passwordFields = _editTextsWithoutHint.Where(f =>
(f.IdEntry?.ToLowerInvariant().Contains("password") ?? false)
|| (f.Hint?.ToLowerInvariant().Contains("password") ?? false)).ToList();
passwordFields = _editTextsWithoutHint.Where(HasPasswordHint).ToList();
}
foreach (var passwordField in passwordFields)
{
AutofillFields.Add(new AutofillFieldMetadata(passwordField, new[] { View.AutofillHintPassword }));
AutofillFields.Add(new AutofillFieldMetadata(passwordField, new[] { View.AutofillHintPassword }));
var usernameField = _editTextsWithoutHint.TakeWhile(f => f.AutofillId != passwordField.AutofillId).LastOrDefault();
if (usernameField != null)
{
AutofillFields.Add(new AutofillFieldMetadata(usernameField, new[] {View.AutofillHintUsername}));
}
}
//for some pages with two-step login, we don't see a password field and don't display the autofill for non-manual requests. But if the user forces autofill,
//let's assume it is a username field:
if (isManualRequest && !passwordFields.Any() && _editTextsWithoutHint.Count == 1)
{
AutofillFields.Add(new AutofillFieldMetadata(_editTextsWithoutHint.First(), new[] { View.AutofillHintUsername }));
}
}
//force focused fields to be included in autofill fields when request was triggered manually. This allows to fill fields which are "off" or don't have a hint (in case there are hints)
if (isManualRequest)
{
foreach (AssistStructure.ViewNode editText in _editTextsWithoutHint)
{
if (editText.IsFocused)
{
AutofillFields.Add(new AutofillFieldMetadata(editText, new[] { IsPassword(editText) || HasPasswordHint(editText) ? View.AutofillHintPassword : View.AutofillHintUsername }));
break;
}
}
}
String packageName = Structure.ActivityComponent.PackageName;
@@ -115,7 +124,26 @@ namespace keepass2android.services.AutofillBase
return webDomain;
}
void ParseLocked(bool forFill, AssistStructure.ViewNode viewNode, ref string validWebdomain)
private static bool HasPasswordHint(AssistStructure.ViewNode f)
{
return (f.IdEntry?.ToLowerInvariant().Contains("password") ?? false)
|| (f.Hint?.ToLowerInvariant().Contains("password") ?? false);
}
private static bool IsPassword(AssistStructure.ViewNode f)
{
return
(!f.IdEntry?.ToLowerInvariant().Contains("search") ?? true) &&
(!f.Hint?.ToLowerInvariant().Contains("search") ?? true) &&
(
f.InputType.HasFlag(InputTypes.TextVariationPassword) ||
f.InputType.HasFlag(InputTypes.TextVariationVisiblePassword) ||
f.InputType.HasFlag(InputTypes.TextVariationWebPassword) ||
(f.HtmlInfo?.Attributes.Any(p => p.First.ToString() == "type" && p.Second.ToString() == "password") ?? false)
);
}
void ParseLocked(bool forFill, bool isManualRequest, AssistStructure.ViewNode viewNode, ref string validWebdomain)
{
String webDomain = viewNode.WebDomain;
if (webDomain != null)
@@ -134,7 +162,19 @@ namespace keepass2android.services.AutofillBase
}
}
if (viewNode.GetAutofillHints() != null && viewNode.GetAutofillHints().Length > 0)
string[] viewHints = viewNode.GetAutofillHints();
if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused &&
isManualRequest)
viewHints[0] = "on";
CommonUtil.logd("viewHints=" + viewHints);
CommonUtil.logd("class=" + viewNode.ClassName);
CommonUtil.logd("tag=" + (viewNode?.HtmlInfo?.Tag ?? "(null)"));
if (viewNode?.HtmlInfo?.Tag == "input")
{
foreach (var p in viewNode.HtmlInfo.Attributes)
CommonUtil.logd("attr="+p.First + "/" + p.Second);
}
if (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/)
{
if (forFill)
{
@@ -147,16 +187,21 @@ namespace keepass2android.services.AutofillBase
//ClientFormData.Add(new FilledAutofillField(viewNode));
}
}
else if (viewNode.ClassName == "android.widget.EditText" || viewNode?.HtmlInfo?.Tag == "input")
else
{
_editTextsWithoutHint.Add(viewNode);
if (viewNode.ClassName == "android.widget.EditText" || viewNode?.HtmlInfo?.Tag == "input")
{
_editTextsWithoutHint.Add(viewNode);
}
}
var childrenSize = viewNode.ChildCount;
if (childrenSize > 0)
{
for (int i = 0; i < childrenSize; i++)
{
ParseLocked(forFill, viewNode.GetChildAt(i), ref validWebdomain);
ParseLocked(forFill, isManualRequest, viewNode.GetChildAt(i), ref validWebdomain);
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
@@ -16,20 +15,14 @@ namespace keepass2android.services
class Kp2aAutofillIntentBuilder: IAutofillIntentBuilder
{
public IntentSender GetAuthIntentSenderForResponse(Context context, string query)
public IntentSender GetAuthIntentSenderForResponse(Context context, string query, bool isManualRequest)
{
Intent intent = new Intent(context, typeof(ChooseForAutofillActivity));
intent.PutExtra(ChooseForAutofillActivityBase.ExtraQueryString, query);
intent.PutExtra(ChooseForAutofillActivityBase.ExtraIsManualRequest, isManualRequest);
return PendingIntent.GetActivity(context, 0, intent, PendingIntentFlags.CancelCurrent).IntentSender;
}
public IntentSender GetAuthIntentSenderForDataset(Context context, string dataset)
{
//TODO implement
//return GetAuthIntentSenderForResponse(context, null);
throw new NotImplementedException();
}
public Intent GetRestartAppIntent(Context context)
{
var intent = new Intent(context, typeof(FileSelectActivity));