diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index ee5cd9f7a..ab71b1e74 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -300,6 +300,7 @@ + @@ -307,8 +308,6 @@ - - diff --git a/src/Android/Autofill/AuthActivity.cs b/src/Android/Autofill/AuthActivity.cs index f08fceb6b..f177cb233 100644 --- a/src/Android/Autofill/AuthActivity.cs +++ b/src/Android/Autofill/AuthActivity.cs @@ -1,13 +1,9 @@ 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.Views; using Android.Widget; using Android.Support.V7.App; using Android.Views.Autofill; @@ -92,7 +88,7 @@ namespace Bit.Android.Autofill } var parser = new Parser(structure); - parser.ParseForFill(); + parser.Parse(); if(!parser.FieldCollection.Fields.Any() || string.IsNullOrWhiteSpace(parser.Uri)) { _replyIntent = null; diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index 395aaaf65..cadf5dcee 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -4,6 +4,8 @@ using Android.Content; using Android.OS; using Android.Runtime; using Android.Service.Autofill; +using Android.Widget; +using Bit.App; using Bit.App.Abstractions; using Bit.App.Enums; using System.Linq; @@ -29,7 +31,7 @@ namespace Bit.Android.Autofill } var parser = new Parser(structure); - parser.ParseForFill(); + parser.Parse(); if(!parser.FieldCollection.Fields.Any() || string.IsNullOrWhiteSpace(parser.Uri) || parser.Uri == "androidapp://com.x8bit.bitwarden" || parser.Uri == "androidapp://android") @@ -70,16 +72,32 @@ namespace Bit.Android.Autofill } var parser = new Parser(structure); - parser.ParseForSave(); + parser.Parse(); + + var savedItem = parser.FieldCollection.GetSavedItem(); + if(savedItem == null) + { + Toast.MakeText(this, "Unable to save this form.", ToastLength.Short).Show(); + return; + } var intent = new Intent(this, typeof(MainActivity)); + intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop); intent.PutExtra("autofillFramework", true); intent.PutExtra("autofillFrameworkSave", true); - intent.PutExtra("autofillFrameworkType", (int)CipherType.Login); - intent.PutExtra("autofillFrameworkUri", parser.Uri); - intent.PutExtra("autofillFrameworkUsername", "username"); - intent.PutExtra("autofillFrameworkPassword", "pass123"); - intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop); + intent.PutExtra("autofillFrameworkType", (int)savedItem.Type); + switch(savedItem.Type) + { + case CipherType.Login: + intent.PutExtra("autofillFrameworkName", parser.Uri.Replace(Constants.AndroidAppProtocol, string.Empty)); + intent.PutExtra("autofillFrameworkUri", parser.Uri); + intent.PutExtra("autofillFrameworkUsername", savedItem.Login.Username); + intent.PutExtra("autofillFrameworkPassword", savedItem.Login.Password); + break; + default: + Toast.MakeText(this, "Unable to save this type of form.", ToastLength.Short).Show(); + return; + } StartActivity(intent); } } diff --git a/src/Android/Autofill/CipherFilledItem.cs b/src/Android/Autofill/CipherFilledItem.cs index f9ae98696..56f4731c2 100644 --- a/src/Android/Autofill/CipherFilledItem.cs +++ b/src/Android/Autofill/CipherFilledItem.cs @@ -54,6 +54,11 @@ namespace Bit.Android.Autofill public bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder) { + if(!fieldCollection?.Fields.Any() ?? true) + { + return false; + } + if(Type == CipherType.Login) { var passwordField = fieldCollection.Fields.FirstOrDefault( diff --git a/src/Android/Autofill/Field.cs b/src/Android/Autofill/Field.cs index 198aa61c6..e83b4a43e 100644 --- a/src/Android/Autofill/Field.cs +++ b/src/Android/Autofill/Field.cs @@ -25,6 +25,26 @@ namespace Bit.Android.Autofill Visible = node.Visibility == ViewStates.Visible; Hints = AutofillHelpers.FilterForSupportedHints(node.GetAutofillHints()); AutofillOptions = node.GetAutofillOptions()?.ToList(); + + if(node.AutofillValue != null) + { + if(node.AutofillValue.IsList) + { + var autofillOptions = node.GetAutofillOptions(); + if(autofillOptions != null && autofillOptions.Length > 0) + { + TextValue = autofillOptions[node.AutofillValue.ListValue]; + } + } + else if(node.AutofillValue.IsDate) + { + DateValue = node.AutofillValue.DateValue; + } + else if(node.AutofillValue.IsText) + { + TextValue = node.AutofillValue.TextValue; + } + } } public SaveDataType SaveType { get; set; } = SaveDataType.Generic; @@ -47,6 +67,9 @@ namespace Bit.Android.Autofill public bool Clickable { get; private set; } public bool Visible { get; private set; } public List AutofillOptions { get; set; } + public string TextValue { get; set; } + public long? DateValue { get; set; } + public bool? ToggleValue { get; set; } public int GetAutofillOptionIndex(string value) { @@ -106,5 +129,44 @@ namespace Bit.Android.Autofill } } } + + public bool ValueIsNull() + { + return TextValue == null && DateValue == null && ToggleValue == null; + } + + public override bool Equals(object obj) + { + if(this == obj) + { + return true; + } + + if(obj == null || GetType() != obj.GetType()) + { + return false; + } + + var field = obj as Field; + if(TextValue != null ? !TextValue.Equals(field.TextValue) : field.TextValue != null) + { + return false; + } + + if(DateValue != null ? !DateValue.Equals(field.DateValue) : field.DateValue != null) + { + return false; + } + + return ToggleValue != null ? ToggleValue.Equals(field.ToggleValue) : field.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/FieldCollection.cs b/src/Android/Autofill/FieldCollection.cs index 632b294f0..46c667c89 100644 --- a/src/Android/Autofill/FieldCollection.cs +++ b/src/Android/Autofill/FieldCollection.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using Android.Service.Autofill; using Android.Views.Autofill; +using System.Linq; +using Android.Text; namespace Bit.Android.Autofill { @@ -49,5 +51,43 @@ namespace Bit.Android.Autofill } } } + + public SavedItem GetSavedItem() + { + if(!Fields?.Any() ?? true) + { + return null; + } + + var passwordField = Fields.FirstOrDefault( + f => f.InputType.HasFlag(InputTypes.TextVariationPassword) && !string.IsNullOrWhiteSpace(f.TextValue)); + if(passwordField == null) + { + passwordField = Fields.FirstOrDefault( + f => (f.IdEntry?.ToLower().Contains("password") ?? false) && !string.IsNullOrWhiteSpace(f.TextValue)); + } + + if(passwordField == null) + { + return null; + } + + var savedItem = new SavedItem + { + Type = App.Enums.CipherType.Login, + Login = new SavedItem.LoginItem + { + Password = passwordField.TextValue + } + }; + + var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault(); + if(usernameField != null && !string.IsNullOrWhiteSpace(usernameField.TextValue)) + { + savedItem.Login.Username = usernameField.TextValue; + } + + return savedItem; + } } } \ No newline at end of file diff --git a/src/Android/Autofill/FilledField.cs b/src/Android/Autofill/FilledField.cs deleted file mode 100644 index deffaddf0..000000000 --- a/src/Android/Autofill/FilledField.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using static Android.App.Assist.AssistStructure; - -namespace Bit.Android.Autofill -{ - public class FilledField - { - public FilledField() { } - - public FilledField(ViewNode node) - { - Hints = AutofillHelpers.FilterForSupportedHints(node.GetAutofillHints()); - - if(node.AutofillValue == null) - { - return; - } - - if(node.AutofillValue.IsList) - { - var autofillOptions = node.GetAutofillOptions(); - if(autofillOptions != null && autofillOptions.Length > 0) - { - TextValue = autofillOptions[node.AutofillValue.ListValue]; - } - } - else if(node.AutofillValue.IsDate) - { - DateValue = node.AutofillValue.DateValue; - } - else if(node.AutofillValue.IsText) - { - TextValue = node.AutofillValue.TextValue; - } - } - - public string TextValue { get; set; } - public long? DateValue { get; set; } - public bool? ToggleValue { get; set; } - public List Hints { get; set; } - - 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 field = o as FilledField; - if(TextValue != null ? !TextValue.Equals(field.TextValue) : field.TextValue != null) - { - return false; - } - - if(DateValue != null ? !DateValue.Equals(field.DateValue) : field.DateValue != null) - { - return false; - } - - return ToggleValue != null ? ToggleValue.Equals(field.ToggleValue) : field.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/FilledFieldCollection.cs b/src/Android/Autofill/FilledFieldCollection.cs deleted file mode 100644 index 4b0d57796..000000000 --- a/src/Android/Autofill/FilledFieldCollection.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using Android.Service.Autofill; -using Android.Views; -using Android.Views.Autofill; -using System.Linq; -using Android.Text; - -namespace Bit.Android.Autofill -{ - public class FilledFieldCollection : IFilledItem - { - public FilledFieldCollection() - : this(null, new Dictionary()) - { } - - public FilledFieldCollection(string datasetName, IDictionary hintMap) - { - HintToFieldMap = hintMap; - Name = datasetName; - Subtitle = "subtitle"; - Icon = Resource.Drawable.login; - } - - public IDictionary HintToFieldMap { get; private set; } - public string Name { get; set; } - public string Subtitle { get; set; } - public int Icon { get; set; } - - public void Add(FilledField filledField) - { - if(filledField == null) - { - throw new ArgumentNullException(nameof(filledField)); - } - - foreach(var hint in filledField.Hints) - { - HintToFieldMap.Add(hint, filledField); - } - } - - public bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder) - { - var setValue = false; - foreach(var hint in fieldCollection.Hints) - { - if(!fieldCollection.HintToFieldsMap.ContainsKey(hint)) - { - continue; - } - - var fillableFields = fieldCollection.HintToFieldsMap[hint]; - for(var i = 0; i < fillableFields.Count; i++) - { - if(!HintToFieldMap.ContainsKey(hint)) - { - continue; - } - - var field = fillableFields[i]; - var filledField = HintToFieldMap[hint]; - - switch(field.AutofillType) - { - case AutofillType.List: - int listValue = field.GetAutofillOptionIndex(filledField.TextValue); - if(listValue != -1) - { - datasetBuilder.SetValue(field.AutofillId, AutofillValue.ForList(listValue)); - setValue = true; - } - break; - case AutofillType.Date: - var dateValue = filledField.DateValue; - if(dateValue != null) - { - datasetBuilder.SetValue(field.AutofillId, AutofillValue.ForDate(dateValue.Value)); - setValue = true; - } - break; - case AutofillType.Text: - var textValue = filledField.TextValue; - if(textValue != null) - { - datasetBuilder.SetValue(field.AutofillId, AutofillValue.ForText(textValue)); - setValue = true; - } - break; - case AutofillType.Toggle: - var toggleValue = filledField.ToggleValue; - if(toggleValue != null) - { - datasetBuilder.SetValue(field.AutofillId, AutofillValue.ForToggle(toggleValue.Value)); - setValue = true; - } - break; - case AutofillType.None: - default: - break; - } - } - } - - return setValue; - } - - public bool HelpsWithHints(List autofillHints) - { - return autofillHints.Any(h => HintToFieldMap.ContainsKey(h) && !HintToFieldMap[h].IsNull()); - } - } -} \ No newline at end of file diff --git a/src/Android/Autofill/Parser.cs b/src/Android/Autofill/Parser.cs index 3deadb8b0..2ff0046e5 100644 --- a/src/Android/Autofill/Parser.cs +++ b/src/Android/Autofill/Parser.cs @@ -1,5 +1,6 @@ using static Android.App.Assist.AssistStructure; using Android.App.Assist; +using Bit.App; namespace Bit.Android.Autofill { @@ -14,7 +15,6 @@ namespace Bit.Android.Autofill } public FieldCollection FieldCollection { get; private set; } = new FieldCollection(); - public FilledFieldCollection FilledFieldCollection { get; private set; } = new FilledFieldCollection(); public string Uri { get => _uri; @@ -26,30 +26,20 @@ namespace Bit.Android.Autofill return; } - _uri = $"androidapp://{value}"; + _uri = string.Concat(Constants.AndroidAppProtocol, value); } } - public void ParseForFill() - { - Parse(true); - } - - public void ParseForSave() - { - Parse(false); - } - - private void Parse(bool forFill) + public void Parse() { for(var i = 0; i < _structure.WindowNodeCount; i++) { var node = _structure.GetWindowNodeAt(i); - ParseNode(forFill, node.RootViewNode); + ParseNode(node.RootViewNode); } } - private void ParseNode(bool forFill, ViewNode node) + private void ParseNode(ViewNode node) { var hints = node.GetAutofillHints(); var isEditText = node.ClassName == "android.widget.EditText"; @@ -59,20 +49,12 @@ namespace Bit.Android.Autofill { Uri = node.IdPackage; } - - if(forFill) - { - FieldCollection.Add(new Field(node)); - } - else - { - FilledFieldCollection.Add(new FilledField(node)); - } + FieldCollection.Add(new Field(node)); } for(var i = 0; i < node.ChildCount; i++) { - ParseNode(forFill, node.GetChildAt(i)); + ParseNode(node.GetChildAt(i)); } } } diff --git a/src/Android/Autofill/SavedItem.cs b/src/Android/Autofill/SavedItem.cs new file mode 100644 index 000000000..280335199 --- /dev/null +++ b/src/Android/Autofill/SavedItem.cs @@ -0,0 +1,26 @@ +using Bit.App.Enums; + +namespace Bit.Android.Autofill +{ + public class SavedItem + { + public CipherType Type { get; set; } + public LoginItem Login { get; set; } + public CardItem Card { get; set; } + + public class LoginItem + { + public string Username { get; set; } + public string Password { get; set; } + } + + public class CardItem + { + public string Name { get; set; } + public string Number { get; set; } + public string ExpMonth { get; set; } + public string ExpYear { get; set; } + public string Code { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index b7754b8b3..4816b4fe8 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -161,7 +161,7 @@ namespace Bit.Android } var parser = new Parser(structure); - parser.ParseForFill(); + parser.Parse(); if(!parser.FieldCollection.Fields.Any() || string.IsNullOrWhiteSpace(parser.Uri)) { SetResult(Result.Canceled); @@ -438,6 +438,7 @@ namespace Bit.Android if(Intent.GetBooleanExtra("autofillFrameworkSave", false)) { options.SaveType = (CipherType)Intent.GetIntExtra("autofillFrameworkType", 0); + options.SaveName = Intent.GetStringExtra("autofillFrameworkName"); options.SaveUsername = Intent.GetStringExtra("autofillFrameworkUsername"); options.SavePassword = Intent.GetStringExtra("autofillFrameworkPassword"); } diff --git a/src/App/Models/AppOptions.cs b/src/App/Models/AppOptions.cs index cbfe7efa5..221bf4320 100644 --- a/src/App/Models/AppOptions.cs +++ b/src/App/Models/AppOptions.cs @@ -8,6 +8,7 @@ namespace Bit.App.Models public bool FromAutofillFramework { get; set; } public string Uri { get; set; } public CipherType? SaveType { get; set; } + public string SaveName { get; set; } public string SaveUsername { get; set; } public string SavePassword { get; set; } } diff --git a/src/App/Pages/Vault/VaultAddCipherPage.cs b/src/App/Pages/Vault/VaultAddCipherPage.cs index 37bf0f786..43d711f40 100644 --- a/src/App/Pages/Vault/VaultAddCipherPage.cs +++ b/src/App/Pages/Vault/VaultAddCipherPage.cs @@ -38,7 +38,7 @@ namespace Bit.App.Pages private DateTime? _lastAction; public VaultAddCipherPage(AppOptions options) - : this(options.SaveType.Value, options.Uri, options.Uri, options.FromAutofillFramework, false) + : this(options.SaveType.Value, options.Uri, options.SaveName, options.FromAutofillFramework, false) { _defaultUsername = options.SaveUsername; _defaultPassword = options.SavePassword;