diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index e0358a9a6..cab965c6c 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -287,8 +287,10 @@ - - + + + + @@ -296,6 +298,8 @@ + + @@ -324,6 +328,7 @@ + diff --git a/src/Android/Autofill/AutofillFieldMetadata.cs b/src/Android/Autofill/AutofillFieldMetadata.cs new file mode 100644 index 000000000..1eb9294a4 --- /dev/null +++ b/src/Android/Autofill/AutofillFieldMetadata.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Android.Service.Autofill; +using Android.Views; +using Android.Views.Autofill; +using static Android.App.Assist.AssistStructure; +using Android.Text; + +namespace Bit.Android.Autofill +{ + public class AutofillFieldMetadata + { + private List _autofillHints; + private string[] _autofillOptions; + + public AutofillFieldMetadata(ViewNode view) + { + _autofillOptions = view.GetAutofillOptions(); + Id = view.Id; + AutofillId = view.AutofillId; + AutofillType = view.AutofillType; + InputType = view.InputType; + IsFocused = view.IsFocused; + AutofillHints = AutofillHelper.FilterForSupportedHints(view.GetAutofillHints())?.ToList() ?? new List(); + } + + public SaveDataType SaveType { get; set; } = SaveDataType.Generic; + public List AutofillHints + { + get { return _autofillHints; } + set + { + _autofillHints = value; + UpdateSaveTypeFromHints(); + } + } + public int Id { get; private set; } + public AutofillId AutofillId { get; private set; } + public AutofillType AutofillType { get; private set; } + public InputTypes InputType { get; private set; } + public bool IsFocused { get; private set; } + + /** + * When the {@link ViewNode} is a list that the user needs to choose a string from (i.e. a + * spinner), this is called to return the index of a specific item in the list. + */ + public int GetAutofillOptionIndex(string value) + { + for(var i = 0; i < _autofillOptions.Length; i++) + { + if(_autofillOptions[i].Equals(value)) + { + return i; + } + } + + return -1; + } + + private void UpdateSaveTypeFromHints() + { + SaveType = SaveDataType.Generic; + if(_autofillHints == null) + { + return; + } + + foreach(var hint in _autofillHints) + { + switch(hint) + { + case View.AutofillHintCreditCardExpirationDate: + case View.AutofillHintCreditCardExpirationDay: + case View.AutofillHintCreditCardExpirationMonth: + case View.AutofillHintCreditCardExpirationYear: + case View.AutofillHintCreditCardNumber: + case View.AutofillHintCreditCardSecurityCode: + SaveType |= SaveDataType.CreditCard; + break; + case View.AutofillHintEmailAddress: + SaveType |= SaveDataType.EmailAddress; + break; + case View.AutofillHintPhone: + case View.AutofillHintName: + SaveType |= SaveDataType.Generic; + break; + case View.AutofillHintPassword: + SaveType |= SaveDataType.Password; + SaveType &= ~SaveDataType.EmailAddress; + SaveType &= ~SaveDataType.Username; + break; + case View.AutofillHintPostalAddress: + case View.AutofillHintPostalCode: + SaveType |= SaveDataType.Address; + break; + case View.AutofillHintUsername: + SaveType |= SaveDataType.Username; + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/Android/Autofill/AutofillFieldMetadataCollection.cs b/src/Android/Autofill/AutofillFieldMetadataCollection.cs new file mode 100644 index 000000000..449a3f0d3 --- /dev/null +++ b/src/Android/Autofill/AutofillFieldMetadataCollection.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Android.Service.Autofill; +using Android.Views; +using Android.Views.Autofill; + +namespace Bit.Android.Autofill +{ + public class AutofillFieldMetadataCollection + { + private int _size = 0; + + public List Ids { get; private set; } = new List(); + public List AutofillIds { get; private set; } = new List(); + public SaveDataType SaveType { get; private set; } = SaveDataType.Generic; + public List AutofillHints { get; private set; } = new List(); + public List FocusedAutofillHints { get; private set; } = new List(); + public List Feilds { get; private set; } + public IDictionary IdToFieldMap { get; private set; } = + new Dictionary(); + public IDictionary> AutofillHintsToFieldsMap { get; private set; } = + new Dictionary>(); + + public void Add(AutofillFieldMetadata data) + { + _size++; + SaveType |= data.SaveType; + Ids.Add(data.Id); + AutofillIds.Add(data.AutofillId); + IdToFieldMap.Add(data.Id, data); + + if((data.AutofillHints?.Count ?? 0) > 0) + { + AutofillHints.AddRange(data.AutofillHints); + if(data.IsFocused) + { + FocusedAutofillHints.AddRange(data.AutofillHints); + } + + foreach(var hint in data.AutofillHints) + { + if(!AutofillHintsToFieldsMap.ContainsKey(hint)) + { + AutofillHintsToFieldsMap.Add(hint, new List()); + } + + AutofillHintsToFieldsMap[hint].Add(data); + } + } + } + } +} \ No newline at end of file diff --git a/src/Android/Autofill/AutofillFrameworkService.cs b/src/Android/Autofill/AutofillFrameworkService.cs new file mode 100644 index 000000000..123b85fe4 --- /dev/null +++ b/src/Android/Autofill/AutofillFrameworkService.cs @@ -0,0 +1,80 @@ +using Android; +using Android.App; +using Android.OS; +using Android.Runtime; +using Android.Service.Autofill; +using Android.Views; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Android.Autofill +{ + [Service(Permission = Manifest.Permission.BindAutofillService, Label = "bitwarden")] + [IntentFilter(new string[] { "android.service.autofill.AutofillService" })] + [MetaData("android.autofill", Resource = "@xml/autofillservice")] + [Register("com.x8bit.bitwarden.Autofill.AutofillFrameworkService")] + public class AutofillFrameworkService : global::Android.Service.Autofill.AutofillService + { + public override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) + { + var structure = request.FillContexts?.LastOrDefault()?.Structure; + if(structure == null) + { + return; + } + + var clientState = request.ClientState; + + var parser = new StructureParser(structure); + parser.ParseForFill(); + + // build response + var responseBuilder = new FillResponse.Builder(); + + var username1 = new FilledAutofillField { TextValue = "username1" }; + var password1 = new FilledAutofillField { TextValue = "pass1" }; + var login1 = new Dictionary + { + { View.AutofillHintUsername, username1 }, + { View.AutofillHintPassword, password1 } + }; + var coll = new FilledAutofillFieldCollection("Login 1 Name", login1); + + var username2 = new FilledAutofillField { TextValue = "username2" }; + var password2 = new FilledAutofillField { TextValue = "pass2" }; + var login2 = new Dictionary + { + { View.AutofillHintUsername, username2 }, + { View.AutofillHintPassword, password2 } + }; + var col2 = new FilledAutofillFieldCollection("Login 2 Name", login2); + + var clientFormDataMap = new Dictionary + { + { "login-1-guid", coll }, + { "login-2-guid", col2 } + }; + + var response = AutofillHelper.NewResponse(this, false, parser.AutofillFields, clientFormDataMap); + // end build response + + callback.OnSuccess(response); + } + + public override void OnSaveRequest(SaveRequest request, SaveCallback callback) + { + var structure = request.FillContexts?.LastOrDefault()?.Structure; + if(structure == null) + { + return; + } + + var clientState = request.ClientState; + + var parser = new StructureParser(structure); + parser.ParseForSave(); + var filledAutofillFieldCollection = parser.GetClientFormData(); + //SaveFilledAutofillFieldCollection(filledAutofillFieldCollection); + } + } +} diff --git a/src/Android/Autofill/AutofillHelper.cs b/src/Android/Autofill/AutofillHelper.cs new file mode 100644 index 000000000..bfc5e7313 --- /dev/null +++ b/src/Android/Autofill/AutofillHelper.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using Android.Content; +using Android.Service.Autofill; +using Android.Views; +using Android.Widget; + +namespace Bit.Android.Autofill +{ + public static class AutofillHelper + { + /** + * Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the + * client View. + */ + public static Dataset NewDataset(Context context, AutofillFieldMetadataCollection autofillFields, + FilledAutofillFieldCollection filledAutofillFieldCollection, bool datasetAuth) + { + var datasetName = filledAutofillFieldCollection.DatasetName; + if(datasetName != null) + { + Dataset.Builder datasetBuilder; + if(datasetAuth) + { + datasetBuilder = new Dataset.Builder( + NewRemoteViews(context.PackageName, datasetName, "username", Resource.Drawable.fa_lock)); + //IntentSender sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName); + //datasetBuilder.SetAuthentication(sender); + } + else + { + datasetBuilder = new Dataset.Builder( + NewRemoteViews(context.PackageName, datasetName, "username", Resource.Drawable.user)); + } + + var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder); + if(setValueAtLeastOnce) + { + return datasetBuilder.Build(); + } + } + + return null; + } + + public static RemoteViews NewRemoteViews(string packageName, string text, string subtext, int iconId) + { + var views = new RemoteViews(packageName, Resource.Layout.autofill_listitem); + views.SetTextViewText(Resource.Id.text, text); + views.SetTextViewText(Resource.Id.text2, subtext); + views.SetImageViewResource(Resource.Id.icon, iconId); + return views; + } + + /** + * Wraps autofill data in a Response object (essentially a series of Datasets) which can then + * be sent back to the client View. + */ + public static FillResponse NewResponse(Context context, bool datasetAuth, + AutofillFieldMetadataCollection autofillFields, + IDictionary clientFormDataMap) + { + var responseBuilder = new FillResponse.Builder(); + if(clientFormDataMap != null) + { + foreach(var datasetName in clientFormDataMap.Keys) + { + if(clientFormDataMap.ContainsKey(datasetName)) + { + var dataset = NewDataset(context, autofillFields, clientFormDataMap[datasetName], datasetAuth); + if(dataset != null) + { + responseBuilder.AddDataset(dataset); + } + } + } + } + + if(autofillFields.SaveType != SaveDataType.Generic) + { + responseBuilder.SetSaveInfo( + new SaveInfo.Builder(autofillFields.SaveType, autofillFields.AutofillIds.ToArray()).Build()); + return responseBuilder.Build(); + } + else + { + //Log.d(TAG, "These fields are not meant to be saved by autofill."); + return null; + } + } + + public static string[] FilterForSupportedHints(string[] hints) + { + if((hints?.Length ?? 0) == 0) + { + return new string[0]; + } + + var filteredHints = new string[hints.Length]; + var i = 0; + foreach(var hint in hints) + { + if(IsValidHint(hint)) + { + filteredHints[i++] = hint; + } + } + + var finalFilteredHints = new string[i]; + Array.Copy(filteredHints, 0, finalFilteredHints, 0, i); + return finalFilteredHints; + } + + public static bool IsValidHint(string hint) + { + switch(hint) + { + case View.AutofillHintCreditCardExpirationDate: + case View.AutofillHintCreditCardExpirationDay: + case View.AutofillHintCreditCardExpirationMonth: + case View.AutofillHintCreditCardExpirationYear: + case View.AutofillHintCreditCardNumber: + case View.AutofillHintCreditCardSecurityCode: + case View.AutofillHintEmailAddress: + case View.AutofillHintPhone: + case View.AutofillHintName: + case View.AutofillHintPassword: + case View.AutofillHintPostalAddress: + case View.AutofillHintPostalCode: + case View.AutofillHintUsername: + return true; + default: + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Android/Autofill/FilledAutofillField.cs b/src/Android/Autofill/FilledAutofillField.cs new file mode 100644 index 000000000..fe2ab14ad --- /dev/null +++ b/src/Android/Autofill/FilledAutofillField.cs @@ -0,0 +1,81 @@ +using static Android.App.Assist.AssistStructure; + +namespace Bit.Android.Autofill +{ + public class FilledAutofillField + { + /** + * Does not need to be serialized into persistent storage, so it's not exposed. + */ + private string[] _autofillHints = null; + + public FilledAutofillField() { } + + public FilledAutofillField(ViewNode viewNode) + { + _autofillHints = AutofillHelper.FilterForSupportedHints(viewNode.GetAutofillHints()); + var autofillValue = viewNode.AutofillValue; + if(autofillValue != null) + { + if(autofillValue.IsList) + { + var autofillOptions = viewNode.GetAutofillOptions(); + int index = autofillValue.ListValue; + if(autofillOptions != null && autofillOptions.Length > 0) + { + TextValue = autofillOptions[index]; + } + } + else if(autofillValue.IsDate) + { + DateValue = autofillValue.DateValue; + } + else if(autofillValue.IsText) + { + // Using toString of AutofillValue.getTextValue in order to save it to + // SharedPreferences. + TextValue = autofillValue.TextValue; + } + } + } + + public string TextValue { get; set; } + public long? DateValue { get; set; } + public bool? ToggleValue { get; set; } + + public string[] GetAutofillHints() + { + return _autofillHints; + } + + public bool IsNull() + { + return TextValue == null && DateValue == null && ToggleValue == null; + } + + public override bool Equals(object o) + { + if(this == o) + return true; + + if(o == null || GetType() != o.GetType()) + return false; + + var that = (FilledAutofillField)o; + if(TextValue != null ? !TextValue.Equals(that.TextValue) : that.TextValue != null) + return false; + if(DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null) + return false; + + return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null; + } + + public override int GetHashCode() + { + var result = TextValue != null ? TextValue.GetHashCode() : 0; + result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0); + result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0); + return result; + } + } +} \ No newline at end of file diff --git a/src/Android/Autofill/FilledAutofillFieldCollection.cs b/src/Android/Autofill/FilledAutofillFieldCollection.cs new file mode 100644 index 000000000..11c563c89 --- /dev/null +++ b/src/Android/Autofill/FilledAutofillFieldCollection.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using Android.Service.Autofill; +using Android.Views; +using Android.Views.Autofill; + +namespace Bit.Android.Autofill +{ + public class FilledAutofillFieldCollection + { + public FilledAutofillFieldCollection() + : this(null, new Dictionary()) + { + } + + public FilledAutofillFieldCollection(string datasetName, IDictionary hintMap) + { + HintMap = hintMap; + DatasetName = datasetName; + } + + public IDictionary HintMap { get; private set; } + public string DatasetName { get; set; } + + /** + * Adds a {@code FilledAutofillField} to the collection, indexed by all of its hints. + */ + public void Add(FilledAutofillField filledAutofillField) + { + if(filledAutofillField == null) + { + throw new ArgumentNullException(nameof(filledAutofillField)); + } + + var autofillHints = filledAutofillField.GetAutofillHints(); + foreach(var hint in autofillHints) + { + HintMap.Add(hint, filledAutofillField); + } + } + + /** + * Populates a {@link Dataset.Builder} with appropriate values for each {@link AutofillId} + * in a {@code AutofillFieldMetadataCollection}. + * + * In other words, it constructs an autofill + * {@link Dataset.Builder} by applying saved values (from this {@code FilledAutofillFieldCollection}) + * to Views specified in a {@code AutofillFieldMetadataCollection}, which represents the current + * page the user is on. + */ + public bool ApplyToFields(AutofillFieldMetadataCollection autofillFieldMetadataCollection, + Dataset.Builder datasetBuilder) + { + var setValueAtLeastOnce = false; + var allHints = autofillFieldMetadataCollection.AutofillHints; + for(var hintIndex = 0; hintIndex < allHints.Count; hintIndex++) + { + var hint = allHints[hintIndex]; + if(!autofillFieldMetadataCollection.AutofillHintsToFieldsMap.ContainsKey(hint)) + { + continue; + } + + var fillableAutofillFields = autofillFieldMetadataCollection.AutofillHintsToFieldsMap[hint]; + for(var autofillFieldIndex = 0; autofillFieldIndex < fillableAutofillFields.Count; autofillFieldIndex++) + { + if(!HintMap.ContainsKey(hint)) + { + continue; + } + + var filledAutofillField = HintMap[hint]; + var autofillFieldMetadata = fillableAutofillFields[autofillFieldIndex]; + var autofillId = autofillFieldMetadata.AutofillId; + var autofillType = autofillFieldMetadata.AutofillType; + switch(autofillType) + { + case AutofillType.List: + int listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue); + if(listValue != -1) + { + datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue)); + setValueAtLeastOnce = true; + } + break; + case AutofillType.Date: + var dateValue = filledAutofillField.DateValue; + if(dateValue != null) + { + datasetBuilder.SetValue(autofillId, AutofillValue.ForDate(dateValue.Value)); + setValueAtLeastOnce = true; + } + break; + case AutofillType.Text: + var textValue = filledAutofillField.TextValue; + if(textValue != null) + { + datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue)); + setValueAtLeastOnce = true; + } + break; + case AutofillType.Toggle: + var toggleValue = filledAutofillField.ToggleValue; + if(toggleValue != null) + { + datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value)); + setValueAtLeastOnce = true; + } + break; + case AutofillType.None: + default: + break; + } + } + } + + return setValueAtLeastOnce; + } + + /** + * Takes in a list of autofill hints (`autofillHints`), usually associated with a View or set of + * Views. Returns whether any of the filled fields on the page have at least 1 of these + * `autofillHint`s. + */ + public bool HelpsWithHints(List autofillHints) + { + for(var i = 0; i < autofillHints.Count; i++) + { + if(HintMap.ContainsKey(autofillHints[i]) && !HintMap[autofillHints[i]].IsNull()) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Android/Autofill/StructureParser.cs b/src/Android/Autofill/StructureParser.cs new file mode 100644 index 000000000..20ea1ecf1 --- /dev/null +++ b/src/Android/Autofill/StructureParser.cs @@ -0,0 +1,73 @@ +using static Android.App.Assist.AssistStructure; +using Android.App.Assist; + +namespace Bit.Android.Autofill +{ + public class StructureParser + { + private readonly AssistStructure _structure; + private FilledAutofillFieldCollection _filledAutofillFieldCollection; + + public StructureParser(AssistStructure structure) + { + _structure = structure; + } + + public AutofillFieldMetadataCollection AutofillFields { get; private set; } + = new AutofillFieldMetadataCollection(); + + public void ParseForFill() + { + Parse(true); + } + + public void ParseForSave() + { + Parse(false); + } + + /** + * Traverse AssistStructure and add ViewNode metadata to a flat list. + */ + private void Parse(bool forFill) + { + _filledAutofillFieldCollection = new FilledAutofillFieldCollection(); + + for(var i = 0; i < _structure.WindowNodeCount; i++) + { + var node = _structure.GetWindowNodeAt(i); + var view = node.RootViewNode; + ParseLocked(forFill, view); + } + } + + private void ParseLocked(bool forFill, ViewNode viewNode) + { + var autofillHints = viewNode.GetAutofillHints(); + var autofillType = viewNode.AutofillType; + var inputType = viewNode.InputType; + var isEditText = viewNode.ClassName == "android.widget.EditText"; + if(isEditText || (autofillHints?.Length ?? 0) > 0) + { + if(forFill) + { + AutofillFields.Add(new AutofillFieldMetadata(viewNode)); + } + else + { + _filledAutofillFieldCollection.Add(new FilledAutofillField(viewNode)); + } + } + + for(var i = 0; i < viewNode.ChildCount; i++) + { + ParseLocked(forFill, viewNode.GetChildAt(i)); + } + } + + public FilledAutofillFieldCollection GetClientFormData() + { + return _filledAutofillFieldCollection; + } + } +} \ No newline at end of file diff --git a/src/Android/AutofillFrameworkService.cs b/src/Android/AutofillFrameworkService.cs deleted file mode 100644 index 07ae9a617..000000000 --- a/src/Android/AutofillFrameworkService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Android; -using Android.App; -using Android.OS; -using Android.Runtime; -using Android.Service.Autofill; -using Android.Util; - -namespace Bit.Android -{ - [Service(Permission = Manifest.Permission.BindAutofillService, Label = "bitwarden")] - [IntentFilter(new string[] { "android.service.autofill.AutofillService" })] - [MetaData("android.autofill", Resource = "@xml/autofillservice")] - [Register("com.x8bit.bitwarden.AutofillFrameworkService")] - public class AutofillFrameworkService : global::Android.Service.Autofill.AutofillService - { - public override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) - { - var structure = request.FillContexts[request.FillContexts.Count - 1].Structure; - var data = request.ClientState; - } - - public override void OnSaveRequest(SaveRequest request, SaveCallback callback) - { - var context = request.FillContexts; - var structure = context[context.Count - 1].Structure; - var data = request.ClientState; - - } - } -} diff --git a/src/Android/AutofillFrameworkService_OLD.cs b/src/Android/AutofillFrameworkService_OLD.cs deleted file mode 100644 index cbdb69141..000000000 --- a/src/Android/AutofillFrameworkService_OLD.cs +++ /dev/null @@ -1,630 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using Android.App; -using Android.Content; -using Android.OS; -using Android.Runtime; -using Android.Service.Autofill; -using Android.Views; -using Android.Widget; -using Android.Views.Autofill; -using static Android.App.Assist.AssistStructure; -using Android.Text; -using Android.App.Assist; - -namespace Bit.Android -{ - //[Service(Permission = global::Android.Manifest.Permission.BindAutofillService, Label = "bitwarden")] - //[IntentFilter(new string[] { "android.service.autofill.AutofillService" })] - //[MetaData("android.autofill", Resource = "@xml/autofillservice")] - public class AutofillFrameworkService_OLD : global::Android.Service.Autofill.AutofillService - { - public override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, - FillCallback callback) - { - var structure = request.FillContexts?.LastOrDefault()?.Structure; - if(structure == null) - { - return; - } - - var clientState = request.ClientState; - - var parser = new StructureParser(structure); - parser.ParseForFill(); - - // build response - var responseBuilder = new FillResponse.Builder(); - - var username1 = new FilledAutofillField { TextValue = "username1" }; - var password1 = new FilledAutofillField { TextValue = "pass1" }; - var login1 = new Dictionary - { - { View.AutofillHintUsername, username1 }, - { View.AutofillHintPassword, password1 } - }; - var coll = new FilledAutofillFieldCollection("Login 1 Name", login1); - - var username2 = new FilledAutofillField { TextValue = "username2" }; - var password2 = new FilledAutofillField { TextValue = "pass2" }; - var login2 = new Dictionary - { - { View.AutofillHintUsername, username2 }, - { View.AutofillHintPassword, password2 } - }; - var col2 = new FilledAutofillFieldCollection("Login 2 Name", login2); - - var clientFormDataMap = new Dictionary - { - { "login-1-guid", coll }, - { "login-2-guid", col2 } - }; - - var response = AutofillHelper.NewResponse(this, false, parser.AutofillFields, clientFormDataMap); - // end build response - - callback.OnSuccess(response); - } - - public override void OnSaveRequest(SaveRequest request, SaveCallback callback) - { - var structure = request.FillContexts?.LastOrDefault()?.Structure; - if(structure == null) - { - return; - } - - var clientState = request.ClientState; - - var parser = new StructureParser(structure); - parser.ParseForSave(); - var filledAutofillFieldCollection = parser.GetClientFormData(); - //SaveFilledAutofillFieldCollection(filledAutofillFieldCollection); - } - } - - /////////////////// Helper Classes /////////////////////// - - public class StructureParser - { - private readonly AssistStructure _structure; - private FilledAutofillFieldCollection _filledAutofillFieldCollection; - - public StructureParser(AssistStructure structure) - { - _structure = structure; - } - - public AutofillFieldMetadataCollection AutofillFields { get; private set; } - = new AutofillFieldMetadataCollection(); - - public void ParseForFill() - { - Parse(true); - } - - public void ParseForSave() - { - Parse(false); - } - - /** - * Traverse AssistStructure and add ViewNode metadata to a flat list. - */ - private void Parse(bool forFill) - { - _filledAutofillFieldCollection = new FilledAutofillFieldCollection(); - - for(var i = 0; i < _structure.WindowNodeCount; i++) - { - var node = _structure.GetWindowNodeAt(i); - var view = node.RootViewNode; - ParseLocked(forFill, view); - } - } - - private void ParseLocked(bool forFill, ViewNode viewNode) - { - var autofillHints = viewNode.GetAutofillHints(); - var autofillType = viewNode.AutofillType; - var inputType = viewNode.InputType; - var isEditText = viewNode.ClassName == "android.widget.EditText"; - if(isEditText || (autofillHints?.Length ?? 0) > 0) - { - if(forFill) - { - AutofillFields.Add(new AutofillFieldMetadata(viewNode)); - } - else - { - _filledAutofillFieldCollection.Add(new FilledAutofillField(viewNode)); - } - } - - for(var i = 0; i < viewNode.ChildCount; i++) - { - ParseLocked(forFill, viewNode.GetChildAt(i)); - } - } - - public FilledAutofillFieldCollection GetClientFormData() - { - return _filledAutofillFieldCollection; - } - } - - public class AutofillFieldMetadataCollection - { - private int _size = 0; - - public List Ids { get; private set; } = new List(); - public List AutofillIds { get; private set; } = new List(); - public SaveDataType SaveType { get; private set; } = SaveDataType.Generic; - public List AutofillHints { get; private set; } = new List(); - public List FocusedAutofillHints { get; private set; } = new List(); - public List Feilds { get; private set; } - public IDictionary IdToFieldMap { get; private set; } = - new Dictionary(); - public IDictionary> AutofillHintsToFieldsMap { get; private set; } = - new Dictionary>(); - - public void Add(AutofillFieldMetadata data) - { - _size++; - SaveType |= data.SaveType; - Ids.Add(data.Id); - AutofillIds.Add(data.AutofillId); - IdToFieldMap.Add(data.Id, data); - - if((data.AutofillHints?.Count ?? 0) > 0) - { - AutofillHints.AddRange(data.AutofillHints); - if(data.IsFocused) - { - FocusedAutofillHints.AddRange(data.AutofillHints); - } - - foreach(var hint in data.AutofillHints) - { - if(!AutofillHintsToFieldsMap.ContainsKey(hint)) - { - AutofillHintsToFieldsMap.Add(hint, new List()); - } - - AutofillHintsToFieldsMap[hint].Add(data); - } - } - } - } - - public class AutofillFieldMetadata - { - private List _autofillHints; - private string[] _autofillOptions; - - public AutofillFieldMetadata(ViewNode view) - { - _autofillOptions = view.GetAutofillOptions(); - Id = view.Id; - AutofillId = view.AutofillId; - AutofillType = view.AutofillType; - InputType = view.InputType; - IsFocused = view.IsFocused; - AutofillHints = AutofillHelper.FilterForSupportedHints(view.GetAutofillHints())?.ToList() ?? new List(); - } - - public SaveDataType SaveType { get; set; } = SaveDataType.Generic; - public List AutofillHints - { - get { return _autofillHints; } - set - { - _autofillHints = value; - UpdateSaveTypeFromHints(); - } - } - public int Id { get; private set; } - public AutofillId AutofillId { get; private set; } - public AutofillType AutofillType { get; private set; } - public InputTypes InputType { get; private set; } - public bool IsFocused { get; private set; } - - /** - * When the {@link ViewNode} is a list that the user needs to choose a string from (i.e. a - * spinner), this is called to return the index of a specific item in the list. - */ - public int GetAutofillOptionIndex(string value) - { - for(var i = 0; i < _autofillOptions.Length; i++) - { - if(_autofillOptions[i].Equals(value)) - { - return i; - } - } - - return -1; - } - - private void UpdateSaveTypeFromHints() - { - SaveType = SaveDataType.Generic; - if(_autofillHints == null) - { - return; - } - - foreach(var hint in _autofillHints) - { - switch(hint) - { - case View.AutofillHintCreditCardExpirationDate: - case View.AutofillHintCreditCardExpirationDay: - case View.AutofillHintCreditCardExpirationMonth: - case View.AutofillHintCreditCardExpirationYear: - case View.AutofillHintCreditCardNumber: - case View.AutofillHintCreditCardSecurityCode: - SaveType |= SaveDataType.CreditCard; - break; - case View.AutofillHintEmailAddress: - SaveType |= SaveDataType.EmailAddress; - break; - case View.AutofillHintPhone: - case View.AutofillHintName: - SaveType |= SaveDataType.Generic; - break; - case View.AutofillHintPassword: - SaveType |= SaveDataType.Password; - SaveType &= ~SaveDataType.EmailAddress; - SaveType &= ~SaveDataType.Username; - break; - case View.AutofillHintPostalAddress: - case View.AutofillHintPostalCode: - SaveType |= SaveDataType.Address; - break; - case View.AutofillHintUsername: - SaveType |= SaveDataType.Username; - break; - } - } - } - } - - public class FilledAutofillField - { - /** - * Does not need to be serialized into persistent storage, so it's not exposed. - */ - private string[] _autofillHints = null; - - public FilledAutofillField() { } - - public FilledAutofillField(ViewNode viewNode) - { - _autofillHints = AutofillHelper.FilterForSupportedHints(viewNode.GetAutofillHints()); - var autofillValue = viewNode.AutofillValue; - if(autofillValue != null) - { - if(autofillValue.IsList) - { - var autofillOptions = viewNode.GetAutofillOptions(); - int index = autofillValue.ListValue; - if(autofillOptions != null && autofillOptions.Length > 0) - { - TextValue = autofillOptions[index]; - } - } - else if(autofillValue.IsDate) - { - DateValue = autofillValue.DateValue; - } - else if(autofillValue.IsText) - { - // Using toString of AutofillValue.getTextValue in order to save it to - // SharedPreferences. - TextValue = autofillValue.TextValue; - } - } - } - - public string TextValue { get; set; } - public long? DateValue { get; set; } - public bool? ToggleValue { get; set; } - - public string[] GetAutofillHints() - { - return _autofillHints; - } - - public bool IsNull() - { - return TextValue == null && DateValue == null && ToggleValue == null; - } - - public override bool Equals(object o) - { - if(this == o) - return true; - - if(o == null || GetType() != o.GetType()) - return false; - - var that = (FilledAutofillField)o; - if(TextValue != null ? !TextValue.Equals(that.TextValue) : that.TextValue != null) - return false; - if(DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null) - return false; - - return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null; - } - - public override int GetHashCode() - { - var result = TextValue != null ? TextValue.GetHashCode() : 0; - result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0); - result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0); - return result; - } - } - - public class FilledAutofillFieldCollection - { - public FilledAutofillFieldCollection() - : this(null, new Dictionary()) - { - } - - public FilledAutofillFieldCollection(string datasetName, IDictionary hintMap) - { - HintMap = hintMap; - DatasetName = datasetName; - } - - public IDictionary HintMap { get; private set; } - public string DatasetName { get; set; } - - /** - * Adds a {@code FilledAutofillField} to the collection, indexed by all of its hints. - */ - public void Add(FilledAutofillField filledAutofillField) - { - if(filledAutofillField == null) - { - throw new ArgumentNullException(nameof(filledAutofillField)); - } - - var autofillHints = filledAutofillField.GetAutofillHints(); - foreach(var hint in autofillHints) - { - HintMap.Add(hint, filledAutofillField); - } - } - - /** - * Populates a {@link Dataset.Builder} with appropriate values for each {@link AutofillId} - * in a {@code AutofillFieldMetadataCollection}. - * - * In other words, it constructs an autofill - * {@link Dataset.Builder} by applying saved values (from this {@code FilledAutofillFieldCollection}) - * to Views specified in a {@code AutofillFieldMetadataCollection}, which represents the current - * page the user is on. - */ - public bool ApplyToFields(AutofillFieldMetadataCollection autofillFieldMetadataCollection, - Dataset.Builder datasetBuilder) - { - var setValueAtLeastOnce = false; - var allHints = autofillFieldMetadataCollection.AutofillHints; - for(var hintIndex = 0; hintIndex < allHints.Count; hintIndex++) - { - var hint = allHints[hintIndex]; - if(!autofillFieldMetadataCollection.AutofillHintsToFieldsMap.ContainsKey(hint)) - { - continue; - } - - var fillableAutofillFields = autofillFieldMetadataCollection.AutofillHintsToFieldsMap[hint]; - for(var autofillFieldIndex = 0; autofillFieldIndex < fillableAutofillFields.Count; autofillFieldIndex++) - { - if(!HintMap.ContainsKey(hint)) - { - continue; - } - - var filledAutofillField = HintMap[hint]; - var autofillFieldMetadata = fillableAutofillFields[autofillFieldIndex]; - var autofillId = autofillFieldMetadata.AutofillId; - var autofillType = autofillFieldMetadata.AutofillType; - switch(autofillType) - { - case AutofillType.List: - int listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue); - if(listValue != -1) - { - datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue)); - setValueAtLeastOnce = true; - } - break; - case AutofillType.Date: - var dateValue = filledAutofillField.DateValue; - if(dateValue != null) - { - datasetBuilder.SetValue(autofillId, AutofillValue.ForDate(dateValue.Value)); - setValueAtLeastOnce = true; - } - break; - case AutofillType.Text: - var textValue = filledAutofillField.TextValue; - if(textValue != null) - { - datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue)); - setValueAtLeastOnce = true; - } - break; - case AutofillType.Toggle: - var toggleValue = filledAutofillField.ToggleValue; - if(toggleValue != null) - { - datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value)); - setValueAtLeastOnce = true; - } - break; - case AutofillType.None: - default: - break; - } - } - } - - return setValueAtLeastOnce; - } - - /** - * Takes in a list of autofill hints (`autofillHints`), usually associated with a View or set of - * Views. Returns whether any of the filled fields on the page have at least 1 of these - * `autofillHint`s. - */ - public bool HelpsWithHints(List autofillHints) - { - for(var i = 0; i < autofillHints.Count; i++) - { - if(HintMap.ContainsKey(autofillHints[i]) && !HintMap[autofillHints[i]].IsNull()) - { - return true; - } - } - - return false; - } - } - - public static class AutofillHelper - { - /** - * Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the - * client View. - */ - public static Dataset NewDataset(Context context, AutofillFieldMetadataCollection autofillFields, - FilledAutofillFieldCollection filledAutofillFieldCollection, bool datasetAuth) - { - var datasetName = filledAutofillFieldCollection.DatasetName; - if(datasetName != null) - { - Dataset.Builder datasetBuilder; - if(datasetAuth) - { - datasetBuilder = new Dataset.Builder( - NewRemoteViews(context.PackageName, datasetName, "username", Resource.Drawable.fa_lock)); - //IntentSender sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName); - //datasetBuilder.SetAuthentication(sender); - } - else - { - datasetBuilder = new Dataset.Builder( - NewRemoteViews(context.PackageName, datasetName, "username", Resource.Drawable.user)); - } - - var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder); - if(setValueAtLeastOnce) - { - return datasetBuilder.Build(); - } - } - - return null; - } - - public static RemoteViews NewRemoteViews(string packageName, string text, string subtext, int iconId) - { - var views = new RemoteViews(packageName, Resource.Layout.autofill_listitem); - views.SetTextViewText(Resource.Id.text, text); - views.SetTextViewText(Resource.Id.text2, subtext); - views.SetImageViewResource(Resource.Id.icon, iconId); - return views; - } - - /** - * Wraps autofill data in a Response object (essentially a series of Datasets) which can then - * be sent back to the client View. - */ - public static FillResponse NewResponse(Context context, bool datasetAuth, - AutofillFieldMetadataCollection autofillFields, - IDictionary clientFormDataMap) - { - var responseBuilder = new FillResponse.Builder(); - if(clientFormDataMap != null) - { - foreach(var datasetName in clientFormDataMap.Keys) - { - if(clientFormDataMap.ContainsKey(datasetName)) - { - var dataset = NewDataset(context, autofillFields, clientFormDataMap[datasetName], datasetAuth); - if(dataset != null) - { - responseBuilder.AddDataset(dataset); - } - } - } - } - - if(autofillFields.SaveType != SaveDataType.Generic) - { - responseBuilder.SetSaveInfo( - new SaveInfo.Builder(autofillFields.SaveType, autofillFields.AutofillIds.ToArray()).Build()); - return responseBuilder.Build(); - } - else - { - //Log.d(TAG, "These fields are not meant to be saved by autofill."); - return null; - } - } - - public static string[] FilterForSupportedHints(string[] hints) - { - if((hints?.Length ?? 0) == 0) - { - return new string[0]; - } - - var filteredHints = new string[hints.Length]; - var i = 0; - foreach(var hint in hints) - { - if(IsValidHint(hint)) - { - filteredHints[i++] = hint; - } - } - - var finalFilteredHints = new string[i]; - Array.Copy(filteredHints, 0, finalFilteredHints, 0, i); - return finalFilteredHints; - } - - public static bool IsValidHint(string hint) - { - switch(hint) - { - case View.AutofillHintCreditCardExpirationDate: - case View.AutofillHintCreditCardExpirationDay: - case View.AutofillHintCreditCardExpirationMonth: - case View.AutofillHintCreditCardExpirationYear: - case View.AutofillHintCreditCardNumber: - case View.AutofillHintCreditCardSecurityCode: - case View.AutofillHintEmailAddress: - case View.AutofillHintPhone: - case View.AutofillHintName: - case View.AutofillHintPassword: - case View.AutofillHintPostalAddress: - case View.AutofillHintPostalCode: - case View.AutofillHintUsername: - return true; - default: - return false; - } - } - } -} \ No newline at end of file