mirror of
https://github.com/bitwarden/android.git
synced 2024-12-24 18:08:26 +03:00
add support for card filling
This commit is contained in:
parent
4b24fe1bf4
commit
c45a77d538
8 changed files with 248 additions and 97 deletions
|
@ -2,21 +2,24 @@
|
|||
using System.Collections.Generic;
|
||||
using Android.Content;
|
||||
using Android.Service.Autofill;
|
||||
using Android.Views;
|
||||
using Android.Widget;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Bit.App.Abstractions;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Resources;
|
||||
|
||||
namespace Bit.Android.Autofill
|
||||
{
|
||||
public static class AutofillHelpers
|
||||
{
|
||||
public static async Task<List<IFilledItem>> GetFillItemsAsync(ICipherService service, string uri)
|
||||
public static async Task<List<IFilledItem>> GetFillItemsAsync(Parser parser, ICipherService service)
|
||||
{
|
||||
var items = new List<IFilledItem>();
|
||||
var ciphers = await service.GetAllAsync(uri);
|
||||
|
||||
if(parser.FieldCollection.FillableForLogin)
|
||||
{
|
||||
var ciphers = await service.GetAllAsync(parser.Uri);
|
||||
if(ciphers.Item1.Any() || ciphers.Item2.Any())
|
||||
{
|
||||
var allCiphers = ciphers.Item1.ToList();
|
||||
|
@ -26,18 +29,27 @@ namespace Bit.Android.Autofill
|
|||
items.Add(new CipherFilledItem(cipher));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(parser.FieldCollection.FillableForCard)
|
||||
{
|
||||
var ciphers = await service.GetAllAsync();
|
||||
foreach(var cipher in ciphers.Where(c => c.Type == App.Enums.CipherType.Card))
|
||||
{
|
||||
items.Add(new CipherFilledItem(cipher));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public static FillResponse BuildFillResponse(Context context, FieldCollection fields, List<IFilledItem> items)
|
||||
public static FillResponse BuildFillResponse(Context context, Parser parser, List<IFilledItem> items)
|
||||
{
|
||||
var responseBuilder = new FillResponse.Builder();
|
||||
if(items != null && items.Count > 0)
|
||||
{
|
||||
foreach(var item in items)
|
||||
{
|
||||
var dataset = BuildDataset(context, fields, item);
|
||||
var dataset = BuildDataset(context, parser.FieldCollection, item);
|
||||
if(dataset != null)
|
||||
{
|
||||
responseBuilder.AddDataset(dataset);
|
||||
|
@ -45,8 +57,8 @@ namespace Bit.Android.Autofill
|
|||
}
|
||||
}
|
||||
|
||||
AddSaveInfo(responseBuilder, fields);
|
||||
responseBuilder.SetIgnoredIds(fields.IgnoreAutofillIds.ToArray());
|
||||
AddSaveInfo(responseBuilder, parser.FieldCollection);
|
||||
responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
|
||||
return responseBuilder.Build();
|
||||
}
|
||||
|
||||
|
@ -64,8 +76,8 @@ namespace Bit.Android.Autofill
|
|||
public static FillResponse BuildAuthResponse(Context context, FieldCollection fields, string uri)
|
||||
{
|
||||
var responseBuilder = new FillResponse.Builder();
|
||||
var view = BuildListView(context.PackageName, "Auto-fill with bitwarden",
|
||||
"Vault is locked", Resource.Drawable.icon);
|
||||
var view = BuildListView(context.PackageName, AppResources.AutofillWithBitwarden,
|
||||
AppResources.VaultIsLocked, Resource.Drawable.icon);
|
||||
var intent = new Intent(context, typeof(MainActivity));
|
||||
intent.PutExtra("autofillFramework", true);
|
||||
intent.PutExtra("autofillFrameworkUri", uri);
|
||||
|
@ -87,36 +99,14 @@ namespace Bit.Android.Autofill
|
|||
|
||||
public static void AddSaveInfo(FillResponse.Builder responseBuilder, FieldCollection fields)
|
||||
{
|
||||
var saveInfo = new SaveInfo.Builder(SaveDataType.Password, fields.AutofillIds.ToArray()).Build();
|
||||
var saveType = fields.SaveType;
|
||||
if(saveType == SaveDataType.Generic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var saveInfo = new SaveInfo.Builder(saveType, fields.AutofillIds.ToArray()).Build();
|
||||
responseBuilder.SetSaveInfo(saveInfo);
|
||||
}
|
||||
|
||||
public static List<string> FilterForSupportedHints(string[] hints)
|
||||
{
|
||||
return hints?.Where(h => IsValidHint(h)).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,7 +34,8 @@ namespace Bit.Android.Autofill
|
|||
parser.Parse();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(parser.Uri) || parser.Uri == "androidapp://com.x8bit.bitwarden" ||
|
||||
parser.Uri == "androidapp://android" || !parser.FieldCollection.FillableForLogin)
|
||||
parser.Uri == "androidapp://android" ||
|
||||
(!parser.FieldCollection.FillableForLogin && !parser.FieldCollection.FillableForCard))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -58,8 +59,8 @@ namespace Bit.Android.Autofill
|
|||
}
|
||||
|
||||
// build response
|
||||
var items = await AutofillHelpers.GetFillItemsAsync(_cipherService, parser.Uri);
|
||||
var response = AutofillHelpers.BuildFillResponse(this, parser.FieldCollection, items);
|
||||
var items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
|
||||
var response = AutofillHelpers.BuildFillResponse(this, parser, items);
|
||||
callback.OnSuccess(response);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,17 @@ using System.Linq;
|
|||
using Bit.App.Models;
|
||||
using Bit.App.Enums;
|
||||
using Bit.App.Models.Page;
|
||||
using Android.Views;
|
||||
|
||||
namespace Bit.Android.Autofill
|
||||
{
|
||||
public class CipherFilledItem : IFilledItem
|
||||
{
|
||||
private Lazy<string> _password;
|
||||
private string _cardNumber;
|
||||
private Lazy<string> _cardExpMonth;
|
||||
private Lazy<string> _cardExpYear;
|
||||
private Lazy<string> _cardCode;
|
||||
|
||||
public CipherFilledItem(Cipher cipher)
|
||||
{
|
||||
|
@ -21,8 +26,24 @@ namespace Bit.Android.Autofill
|
|||
{
|
||||
case CipherType.Login:
|
||||
Subtitle = cipher.Login.Username?.Decrypt() ?? string.Empty;
|
||||
_password = new Lazy<string>(() => cipher.Login.Password?.Decrypt());
|
||||
Icon = Resource.Drawable.login;
|
||||
_password = new Lazy<string>(() => cipher.Login.Password?.Decrypt());
|
||||
break;
|
||||
case CipherType.Card:
|
||||
Subtitle = cipher.Card.Brand?.Decrypt();
|
||||
_cardNumber = cipher.Card.Number?.Decrypt();
|
||||
if(!string.IsNullOrWhiteSpace(_cardNumber) && _cardNumber.Length >= 4)
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(_cardNumber))
|
||||
{
|
||||
Subtitle += ", ";
|
||||
}
|
||||
Subtitle += ("*" + _cardNumber.Substring(_cardNumber.Length - 4));
|
||||
}
|
||||
Icon = Resource.Drawable.card;
|
||||
_cardCode = new Lazy<string>(() => cipher.Card.Code?.Decrypt());
|
||||
_cardExpMonth = new Lazy<string>(() => cipher.Card.ExpMonth?.Decrypt());
|
||||
_cardExpYear = new Lazy<string>(() => cipher.Card.ExpYear?.Decrypt());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -58,32 +79,68 @@ namespace Bit.Android.Autofill
|
|||
return false;
|
||||
}
|
||||
|
||||
var setValues = false;
|
||||
if(Type == CipherType.Login)
|
||||
{
|
||||
if(!fieldCollection.PasswordFields.Any() || string.IsNullOrWhiteSpace(_password.Value))
|
||||
if(fieldCollection.PasswordFields.Any() && !string.IsNullOrWhiteSpace(_password.Value))
|
||||
{
|
||||
return false;
|
||||
foreach(var f in fieldCollection.PasswordFields)
|
||||
{
|
||||
setValues = true;
|
||||
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_password.Value));
|
||||
}
|
||||
|
||||
foreach(var passwordField in fieldCollection.PasswordFields)
|
||||
{
|
||||
datasetBuilder.SetValue(passwordField.AutofillId, AutofillValue.ForText(_password.Value));
|
||||
}
|
||||
|
||||
if(fieldCollection.UsernameFields.Any() && !string.IsNullOrWhiteSpace(Subtitle))
|
||||
{
|
||||
foreach(var usernameField in fieldCollection.UsernameFields)
|
||||
foreach(var f in fieldCollection.UsernameFields)
|
||||
{
|
||||
datasetBuilder.SetValue(usernameField.AutofillId, AutofillValue.ForText(Subtitle));
|
||||
setValues = true;
|
||||
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(Subtitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(Type == CipherType.Card)
|
||||
{
|
||||
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardNumber) &&
|
||||
!string.IsNullOrWhiteSpace(_cardNumber))
|
||||
{
|
||||
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardNumber])
|
||||
{
|
||||
setValues = true;
|
||||
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardNumber));
|
||||
}
|
||||
}
|
||||
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardSecurityCode) &&
|
||||
!string.IsNullOrWhiteSpace(_cardCode.Value))
|
||||
{
|
||||
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardSecurityCode])
|
||||
{
|
||||
setValues = true;
|
||||
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardCode.Value));
|
||||
}
|
||||
}
|
||||
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationMonth) &&
|
||||
!string.IsNullOrWhiteSpace(_cardExpMonth.Value))
|
||||
{
|
||||
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardExpirationMonth])
|
||||
{
|
||||
setValues = true;
|
||||
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardExpMonth.Value));
|
||||
}
|
||||
}
|
||||
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationYear) &&
|
||||
!string.IsNullOrWhiteSpace(_cardExpYear.Value))
|
||||
{
|
||||
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardExpirationYear])
|
||||
{
|
||||
setValues = true;
|
||||
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardExpYear.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return setValues;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ namespace Bit.Android.Autofill
|
|||
Selected = node.IsSelected;
|
||||
Clickable = node.IsClickable;
|
||||
Visible = node.Visibility == ViewStates.Visible;
|
||||
Hints = AutofillHelpers.FilterForSupportedHints(node.GetAutofillHints());
|
||||
Hints = FilterForSupportedHints(node.GetAutofillHints());
|
||||
AutofillOptions = node.GetAutofillOptions()?.ToList();
|
||||
|
||||
if(node.AutofillValue != null)
|
||||
|
@ -168,5 +168,33 @@ namespace Bit.Android.Autofill
|
|||
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<string> FilterForSupportedHints(string[] hints)
|
||||
{
|
||||
return hints?.Where(h => IsValidHint(h)).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,24 @@ namespace Bit.Android.Autofill
|
|||
|
||||
public HashSet<int> Ids { get; private set; } = new HashSet<int>();
|
||||
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
|
||||
public SaveDataType SaveType { get; private set; } = SaveDataType.Generic;
|
||||
public List<string> Hints { get; private set; } = new List<string>();
|
||||
public List<string> FocusedHints { get; private set; } = new List<string>();
|
||||
public SaveDataType SaveType
|
||||
{
|
||||
get
|
||||
{
|
||||
if(FillableForLogin)
|
||||
{
|
||||
return SaveDataType.Password;
|
||||
}
|
||||
else if(FillableForCard)
|
||||
{
|
||||
return SaveDataType.CreditCard;
|
||||
}
|
||||
|
||||
return SaveDataType.Generic;
|
||||
}
|
||||
}
|
||||
public HashSet<string> Hints { get; private set; } = new HashSet<string>();
|
||||
public HashSet<string> FocusedHints { get; private set; } = new HashSet<string>();
|
||||
public List<Field> Fields { get; private set; } = new List<Field>();
|
||||
public IDictionary<int, Field> IdToFieldMap { get; private set; } =
|
||||
new Dictionary<int, Field>();
|
||||
|
@ -35,7 +50,11 @@ namespace Bit.Android.Autofill
|
|||
|
||||
if(Hints.Any())
|
||||
{
|
||||
_passwordFields = Fields.Where(f => f.Hints.Contains(View.AutofillHintPassword)).ToList();
|
||||
_passwordFields = new List<Field>();
|
||||
if(HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
|
||||
{
|
||||
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -59,15 +78,20 @@ namespace Bit.Android.Autofill
|
|||
return _usernameFields;
|
||||
}
|
||||
|
||||
_usernameFields = new List<Field>();
|
||||
if(Hints.Any())
|
||||
{
|
||||
_usernameFields = Fields
|
||||
.Where(f => f.Hints.Any(fh => fh == View.AutofillHintEmailAddress || fh == View.AutofillHintUsername))
|
||||
.ToList();
|
||||
if(HintToFieldsMap.ContainsKey(View.AutofillHintEmailAddress))
|
||||
{
|
||||
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintEmailAddress]);
|
||||
}
|
||||
if(HintToFieldsMap.ContainsKey(View.AutofillHintUsername))
|
||||
{
|
||||
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_usernameFields = new List<Field>();
|
||||
foreach(var passwordField in PasswordFields)
|
||||
{
|
||||
var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault();
|
||||
|
@ -82,7 +106,15 @@ namespace Bit.Android.Autofill
|
|||
}
|
||||
}
|
||||
|
||||
public bool FillableForLogin => UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused);
|
||||
public bool FillableForLogin => FocusedHintsContain(
|
||||
new string[] { View.AutofillHintUsername, View.AutofillHintEmailAddress, View.AutofillHintPassword }) ||
|
||||
UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused);
|
||||
public bool FillableForCard => FocusedHintsContain(
|
||||
new string[] { View.AutofillHintCreditCardNumber, View.AutofillHintCreditCardExpirationMonth,
|
||||
View.AutofillHintCreditCardExpirationYear, View.AutofillHintCreditCardSecurityCode});
|
||||
public bool FillableForIdentity => FocusedHintsContain(
|
||||
new string[] { View.AutofillHintName, View.AutofillHintPhone, View.AutofillHintPostalAddress,
|
||||
View.AutofillHintPostalCode });
|
||||
|
||||
public void Add(Field field)
|
||||
{
|
||||
|
@ -95,20 +127,19 @@ namespace Bit.Android.Autofill
|
|||
|
||||
Ids.Add(field.Id);
|
||||
Fields.Add(field);
|
||||
SaveType |= field.SaveType;
|
||||
AutofillIds.Add(field.AutofillId);
|
||||
IdToFieldMap.Add(field.Id, field);
|
||||
|
||||
if((field.Hints?.Count ?? 0) > 0)
|
||||
if(field.Hints != null)
|
||||
{
|
||||
Hints.AddRange(field.Hints);
|
||||
if(field.Focused)
|
||||
{
|
||||
FocusedHints.AddRange(field.Hints);
|
||||
}
|
||||
|
||||
foreach(var hint in field.Hints)
|
||||
{
|
||||
Hints.Add(hint);
|
||||
if(field.Focused)
|
||||
{
|
||||
FocusedHints.Add(hint);
|
||||
}
|
||||
|
||||
if(!HintToFieldsMap.ContainsKey(hint))
|
||||
{
|
||||
HintToFieldsMap.Add(hint, new List<Field>());
|
||||
|
@ -121,11 +152,8 @@ namespace Bit.Android.Autofill
|
|||
|
||||
public SavedItem GetSavedItem()
|
||||
{
|
||||
if(!Fields?.Any() ?? true)
|
||||
if(SaveType == SaveDataType.Password)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var passwordField = PasswordFields.FirstOrDefault(f => !string.IsNullOrWhiteSpace(f.TextValue));
|
||||
if(passwordField == null)
|
||||
{
|
||||
|
@ -149,5 +177,28 @@ namespace Bit.Android.Autofill
|
|||
|
||||
return savedItem;
|
||||
}
|
||||
else if(SaveType == SaveDataType.CreditCard)
|
||||
{
|
||||
var savedItem = new SavedItem
|
||||
{
|
||||
Type = App.Enums.CipherType.Card,
|
||||
Card = new SavedItem.CardItem
|
||||
{
|
||||
Number = HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardNumber) ?
|
||||
HintToFieldsMap[View.AutofillHintCreditCardNumber].FirstOrDefault(
|
||||
f => !string.IsNullOrWhiteSpace(f.TextValue))?.TextValue : null
|
||||
}
|
||||
};
|
||||
|
||||
return savedItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool FocusedHintsContain(IEnumerable<string> hints)
|
||||
{
|
||||
return hints.Any(h => FocusedHints.Contains(h));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -170,7 +170,7 @@ namespace Bit.Android
|
|||
}
|
||||
|
||||
var items = new List<IFilledItem> { new CipherFilledItem(cipher) };
|
||||
var response = AutofillHelpers.BuildFillResponse(this, parser.FieldCollection, items);
|
||||
var response = AutofillHelpers.BuildFillResponse(this, parser, items);
|
||||
var replyIntent = new Intent();
|
||||
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, response);
|
||||
SetResult(Result.Ok, replyIntent);
|
||||
|
|
18
src/App/Resources/AppResources.Designer.cs
generated
18
src/App/Resources/AppResources.Designer.cs
generated
|
@ -376,6 +376,15 @@ namespace Bit.App.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Auto-fill with bitwarden.
|
||||
/// </summary>
|
||||
public static string AutofillWithBitwarden {
|
||||
get {
|
||||
return ResourceManager.GetString("AutofillWithBitwarden", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Avoid Ambiguous Characters.
|
||||
/// </summary>
|
||||
|
@ -2860,6 +2869,15 @@ namespace Bit.App.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Vault is locked.
|
||||
/// </summary>
|
||||
public static string VaultIsLocked {
|
||||
get {
|
||||
return ResourceManager.GetString("VaultIsLocked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Verification Code.
|
||||
/// </summary>
|
||||
|
|
|
@ -1185,4 +1185,10 @@
|
|||
<data name="IconsUrl" xml:space="preserve">
|
||||
<value>Icons Server URL</value>
|
||||
</data>
|
||||
<data name="AutofillWithBitwarden" xml:space="preserve">
|
||||
<value>Auto-fill with bitwarden</value>
|
||||
</data>
|
||||
<data name="VaultIsLocked" xml:space="preserve">
|
||||
<value>Vault is locked</value>
|
||||
</data>
|
||||
</root>
|
Loading…
Reference in a new issue