allow to fill single inputs (or autofill=off) with autofill (#9)
This commit is contained in:
@@ -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))
|
||||
{
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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));
|
||||
|
Reference in New Issue
Block a user