mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
refactor autofill classes. basic login support.
This commit is contained in:
parent
4f5e238685
commit
0e020924ff
12 changed files with 368 additions and 267 deletions
|
@ -287,10 +287,12 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="AutofillActivity.cs" />
|
<Compile Include="AutofillActivity.cs" />
|
||||||
<Compile Include="AutofillCredentials.cs" />
|
<Compile Include="AutofillCredentials.cs" />
|
||||||
<Compile Include="Autofill\AutofillFieldMetadata.cs" />
|
<Compile Include="Autofill\Field.cs" />
|
||||||
<Compile Include="Autofill\AutofillFieldMetadataCollection.cs" />
|
<Compile Include="Autofill\FieldCollection.cs" />
|
||||||
<Compile Include="Autofill\AutofillFrameworkService.cs" />
|
<Compile Include="Autofill\AutofillService.cs" />
|
||||||
<Compile Include="Autofill\AutofillHelper.cs" />
|
<Compile Include="Autofill\AutofillHelpers.cs" />
|
||||||
|
<Compile Include="Autofill\CipherFilledItem.cs" />
|
||||||
|
<Compile Include="Autofill\IFilledItem.cs" />
|
||||||
<Compile Include="Controls\CustomLabelRenderer.cs" />
|
<Compile Include="Controls\CustomLabelRenderer.cs" />
|
||||||
<Compile Include="Controls\CustomSearchBarRenderer.cs" />
|
<Compile Include="Controls\CustomSearchBarRenderer.cs" />
|
||||||
<Compile Include="Controls\CustomButtonRenderer.cs" />
|
<Compile Include="Controls\CustomButtonRenderer.cs" />
|
||||||
|
@ -298,8 +300,8 @@
|
||||||
<Compile Include="Controls\ExtendedButtonRenderer.cs" />
|
<Compile Include="Controls\ExtendedButtonRenderer.cs" />
|
||||||
<Compile Include="Controls\ExtendedTabbedPageRenderer.cs" />
|
<Compile Include="Controls\ExtendedTabbedPageRenderer.cs" />
|
||||||
<Compile Include="Controls\ExtendedTableViewRenderer.cs" />
|
<Compile Include="Controls\ExtendedTableViewRenderer.cs" />
|
||||||
<Compile Include="Autofill\FilledAutofillField.cs" />
|
<Compile Include="Autofill\FilledField.cs" />
|
||||||
<Compile Include="Autofill\FilledAutofillFieldCollection.cs" />
|
<Compile Include="Autofill\FilledFieldCollection.cs" />
|
||||||
<Compile Include="HockeyAppCrashManagerListener.cs" />
|
<Compile Include="HockeyAppCrashManagerListener.cs" />
|
||||||
<Compile Include="AutofillService.cs" />
|
<Compile Include="AutofillService.cs" />
|
||||||
<Compile Include="Controls\ExtendedEditorRenderer.cs" />
|
<Compile Include="Controls\ExtendedEditorRenderer.cs" />
|
||||||
|
@ -328,7 +330,7 @@
|
||||||
<Compile Include="Services\SqlService.cs" />
|
<Compile Include="Services\SqlService.cs" />
|
||||||
<Compile Include="SplashActivity.cs" />
|
<Compile Include="SplashActivity.cs" />
|
||||||
<Compile Include="PackageReplacedReceiver.cs" />
|
<Compile Include="PackageReplacedReceiver.cs" />
|
||||||
<Compile Include="Autofill\StructureParser.cs" />
|
<Compile Include="Autofill\Parser.cs" />
|
||||||
<Compile Include="Utilities.cs" />
|
<Compile Include="Utilities.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
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<int> Ids { get; private set; } = new List<int>();
|
|
||||||
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
|
|
||||||
public SaveDataType SaveType { get; private set; } = SaveDataType.Generic;
|
|
||||||
public List<string> AutofillHints { get; private set; } = new List<string>();
|
|
||||||
public List<string> FocusedAutofillHints { get; private set; } = new List<string>();
|
|
||||||
public List<AutofillFieldMetadata> Feilds { get; private set; }
|
|
||||||
public IDictionary<int, AutofillFieldMetadata> IdToFieldMap { get; private set; } =
|
|
||||||
new Dictionary<int, AutofillFieldMetadata>();
|
|
||||||
public IDictionary<string, List<AutofillFieldMetadata>> AutofillHintsToFieldsMap { get; private set; } =
|
|
||||||
new Dictionary<string, List<AutofillFieldMetadata>>();
|
|
||||||
|
|
||||||
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<AutofillFieldMetadata>());
|
|
||||||
}
|
|
||||||
|
|
||||||
AutofillHintsToFieldsMap[hint].Add(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
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<string, FilledAutofillField>
|
|
||||||
{
|
|
||||||
{ 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<string, FilledAutofillField>
|
|
||||||
{
|
|
||||||
{ View.AutofillHintUsername, username2 },
|
|
||||||
{ View.AutofillHintPassword, password2 }
|
|
||||||
};
|
|
||||||
var col2 = new FilledAutofillFieldCollection("Login 2 Name", login2);
|
|
||||||
|
|
||||||
var clientFormDataMap = new Dictionary<string, FilledAutofillFieldCollection>
|
|
||||||
{
|
|
||||||
{ "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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,37 +4,38 @@ using Android.Content;
|
||||||
using Android.Service.Autofill;
|
using Android.Service.Autofill;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Bit.Android.Autofill
|
namespace Bit.Android.Autofill
|
||||||
{
|
{
|
||||||
public static class AutofillHelper
|
public static class AutofillHelpers
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
|
* Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
|
||||||
* client View.
|
* client View.
|
||||||
*/
|
*/
|
||||||
public static Dataset NewDataset(Context context, AutofillFieldMetadataCollection autofillFields,
|
public static Dataset NewDataset(Context context, FieldCollection fields, IFilledItem filledItem, bool auth)
|
||||||
FilledAutofillFieldCollection filledAutofillFieldCollection, bool datasetAuth)
|
|
||||||
{
|
{
|
||||||
var datasetName = filledAutofillFieldCollection.DatasetName;
|
var itemName = filledItem.Name;
|
||||||
if(datasetName != null)
|
if(itemName != null)
|
||||||
{
|
{
|
||||||
Dataset.Builder datasetBuilder;
|
Dataset.Builder datasetBuilder;
|
||||||
if(datasetAuth)
|
if(auth)
|
||||||
{
|
{
|
||||||
datasetBuilder = new Dataset.Builder(
|
datasetBuilder = new Dataset.Builder(
|
||||||
NewRemoteViews(context.PackageName, datasetName, "username", Resource.Drawable.fa_lock));
|
NewRemoteViews(context.PackageName, itemName, filledItem.Subtitle, Resource.Drawable.fa_lock));
|
||||||
//IntentSender sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName);
|
//IntentSender sender = AuthActivity.getAuthIntentSenderForDataset(context, datasetName);
|
||||||
//datasetBuilder.SetAuthentication(sender);
|
//datasetBuilder.SetAuthentication(sender);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
datasetBuilder = new Dataset.Builder(
|
datasetBuilder = new Dataset.Builder(
|
||||||
NewRemoteViews(context.PackageName, datasetName, "username", Resource.Drawable.user));
|
NewRemoteViews(context.PackageName, itemName, filledItem.Subtitle, Resource.Drawable.user));
|
||||||
}
|
}
|
||||||
|
|
||||||
var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder);
|
var setValue = filledItem.ApplyToFields(fields, datasetBuilder);
|
||||||
if(setValueAtLeastOnce)
|
if(setValue)
|
||||||
{
|
{
|
||||||
return datasetBuilder.Build();
|
return datasetBuilder.Build();
|
||||||
}
|
}
|
||||||
|
@ -56,59 +57,43 @@ namespace Bit.Android.Autofill
|
||||||
* Wraps autofill data in a Response object (essentially a series of Datasets) which can then
|
* Wraps autofill data in a Response object (essentially a series of Datasets) which can then
|
||||||
* be sent back to the client View.
|
* be sent back to the client View.
|
||||||
*/
|
*/
|
||||||
public static FillResponse NewResponse(Context context, bool datasetAuth,
|
public static FillResponse NewResponse(Context context, bool auth, FieldCollection fields,
|
||||||
AutofillFieldMetadataCollection autofillFields,
|
IDictionary<string, IFilledItem> items)
|
||||||
IDictionary<string, FilledAutofillFieldCollection> clientFormDataMap)
|
|
||||||
{
|
{
|
||||||
var responseBuilder = new FillResponse.Builder();
|
var responseBuilder = new FillResponse.Builder();
|
||||||
if(clientFormDataMap != null)
|
if(items != null)
|
||||||
{
|
{
|
||||||
foreach(var datasetName in clientFormDataMap.Keys)
|
foreach(var datasetName in items.Keys)
|
||||||
{
|
{
|
||||||
if(clientFormDataMap.ContainsKey(datasetName))
|
var dataset = NewDataset(context, fields, items[datasetName], auth);
|
||||||
{
|
|
||||||
var dataset = NewDataset(context, autofillFields, clientFormDataMap[datasetName], datasetAuth);
|
|
||||||
if(dataset != null)
|
if(dataset != null)
|
||||||
{
|
{
|
||||||
responseBuilder.AddDataset(dataset);
|
responseBuilder.AddDataset(dataset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if(autofillFields.SaveType != SaveDataType.Generic)
|
if(true || fields.SaveType != SaveDataType.Generic)
|
||||||
{
|
{
|
||||||
responseBuilder.SetSaveInfo(
|
var info = new SaveInfo.Builder(fields.SaveType, fields.AutofillIds.ToArray()).Build();
|
||||||
new SaveInfo.Builder(autofillFields.SaveType, autofillFields.AutofillIds.ToArray()).Build());
|
responseBuilder.SetSaveInfo(info);
|
||||||
return responseBuilder.Build();
|
return responseBuilder.Build();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//Log.d(TAG, "These fields are not meant to be saved by autofill.");
|
Debug.WriteLine("These fields are not meant to be saved by autofill.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string[] FilterForSupportedHints(string[] hints)
|
public static List<string> FilterForSupportedHints(string[] hints)
|
||||||
{
|
{
|
||||||
if((hints?.Length ?? 0) == 0)
|
if(hints == null)
|
||||||
{
|
{
|
||||||
return new string[0];
|
return new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var filteredHints = new string[hints.Length];
|
return hints.Where(h => IsValidHint(h)).ToList();
|
||||||
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)
|
public static bool IsValidHint(string hint)
|
83
src/Android/Autofill/AutofillService.cs
Normal file
83
src/Android/Autofill/AutofillService.cs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
using Android;
|
||||||
|
using Android.App;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Runtime;
|
||||||
|
using Android.Service.Autofill;
|
||||||
|
using Android.Views;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using XLabs.Ioc;
|
||||||
|
|
||||||
|
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.AutofillService")]
|
||||||
|
public class AutofillService : global::Android.Service.Autofill.AutofillService
|
||||||
|
{
|
||||||
|
private ICipherService _cipherService;
|
||||||
|
|
||||||
|
public async 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 Parser(structure);
|
||||||
|
parser.ParseForFill();
|
||||||
|
|
||||||
|
if(!parser.FieldCollection.Fields.Any() || string.IsNullOrWhiteSpace(parser.Uri))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_cipherService == null)
|
||||||
|
{
|
||||||
|
_cipherService = Resolver.Resolve<ICipherService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// build response
|
||||||
|
var items = new Dictionary<string, IFilledItem>();
|
||||||
|
var ciphers = await _cipherService.GetAllAsync(parser.Uri);
|
||||||
|
if(ciphers.Item1.Any() || ciphers.Item2.Any())
|
||||||
|
{
|
||||||
|
var allCiphers = ciphers.Item1.ToList();
|
||||||
|
allCiphers.AddRange(ciphers.Item2.ToList());
|
||||||
|
foreach(var cipher in allCiphers)
|
||||||
|
{
|
||||||
|
items.Add(cipher.Id, new CipherFilledItem(cipher));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!items.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = AutofillHelpers.NewResponse(this, false, parser.FieldCollection, items);
|
||||||
|
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 Parser(structure);
|
||||||
|
parser.ParseForSave();
|
||||||
|
var filledAutofillFieldCollection = parser.GetClientFormData();
|
||||||
|
//SaveFilledAutofillFieldCollection(filledAutofillFieldCollection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
src/Android/Autofill/CipherFilledItem.cs
Normal file
71
src/Android/Autofill/CipherFilledItem.cs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Android.Service.Autofill;
|
||||||
|
using Android.Views;
|
||||||
|
using Android.Views.Autofill;
|
||||||
|
using System.Linq;
|
||||||
|
using Android.Text;
|
||||||
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Android.Autofill
|
||||||
|
{
|
||||||
|
public class CipherFilledItem : IFilledItem
|
||||||
|
{
|
||||||
|
private readonly Cipher _cipher;
|
||||||
|
|
||||||
|
public CipherFilledItem(Cipher cipher)
|
||||||
|
{
|
||||||
|
_cipher = cipher;
|
||||||
|
Name = cipher.Name?.Decrypt() ?? "--";
|
||||||
|
|
||||||
|
switch(cipher.Type)
|
||||||
|
{
|
||||||
|
case CipherType.Login:
|
||||||
|
Subtitle = _cipher.Login.Username?.Decrypt() ?? string.Empty;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Subtitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder)
|
||||||
|
{
|
||||||
|
if(_cipher.Type == CipherType.Login && _cipher.Login != null)
|
||||||
|
{
|
||||||
|
var passwordField = fieldCollection.Fields.FirstOrDefault(
|
||||||
|
f => f.InputType.HasFlag(InputTypes.TextVariationPassword));
|
||||||
|
if(passwordField == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var password = _cipher.Login.Password?.Decrypt();
|
||||||
|
if(string.IsNullOrWhiteSpace(password))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
datasetBuilder.SetValue(passwordField.AutofillId, AutofillValue.ForText(password));
|
||||||
|
|
||||||
|
var usernameField = fieldCollection.Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault();
|
||||||
|
if(usernameField != null)
|
||||||
|
{
|
||||||
|
if(!string.IsNullOrWhiteSpace(Subtitle))
|
||||||
|
{
|
||||||
|
datasetBuilder.SetValue(usernameField.AutofillId, AutofillValue.ForText(Subtitle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,29 +8,29 @@ using Android.Text;
|
||||||
|
|
||||||
namespace Bit.Android.Autofill
|
namespace Bit.Android.Autofill
|
||||||
{
|
{
|
||||||
public class AutofillFieldMetadata
|
public class Field
|
||||||
{
|
{
|
||||||
private List<string> _autofillHints;
|
private List<string> _hints;
|
||||||
private string[] _autofillOptions;
|
private string[] _autofillOptions;
|
||||||
|
|
||||||
public AutofillFieldMetadata(ViewNode view)
|
public Field(ViewNode view)
|
||||||
{
|
{
|
||||||
_autofillOptions = view.GetAutofillOptions();
|
_autofillOptions = view.GetAutofillOptions();
|
||||||
Id = view.Id;
|
Id = view.Id;
|
||||||
AutofillId = view.AutofillId;
|
AutofillId = view.AutofillId;
|
||||||
AutofillType = view.AutofillType;
|
AutofillType = view.AutofillType;
|
||||||
InputType = view.InputType;
|
InputType = view.InputType;
|
||||||
IsFocused = view.IsFocused;
|
Focused = view.IsFocused;
|
||||||
AutofillHints = AutofillHelper.FilterForSupportedHints(view.GetAutofillHints())?.ToList() ?? new List<string>();
|
Hints = AutofillHelpers.FilterForSupportedHints(view.GetAutofillHints())?.ToList() ?? new List<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SaveDataType SaveType { get; set; } = SaveDataType.Generic;
|
public SaveDataType SaveType { get; set; } = SaveDataType.Generic;
|
||||||
public List<string> AutofillHints
|
public List<string> Hints
|
||||||
{
|
{
|
||||||
get { return _autofillHints; }
|
get => _hints;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_autofillHints = value;
|
_hints = value;
|
||||||
UpdateSaveTypeFromHints();
|
UpdateSaveTypeFromHints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ namespace Bit.Android.Autofill
|
||||||
public AutofillId AutofillId { get; private set; }
|
public AutofillId AutofillId { get; private set; }
|
||||||
public AutofillType AutofillType { get; private set; }
|
public AutofillType AutofillType { get; private set; }
|
||||||
public InputTypes InputType { get; private set; }
|
public InputTypes InputType { get; private set; }
|
||||||
public bool IsFocused { get; private set; }
|
public bool Focused { get; private set; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the {@link ViewNode} is a list that the user needs to choose a string from (i.e. a
|
* When the {@link ViewNode} is a list that the user needs to choose a string from (i.e. a
|
||||||
|
@ -60,12 +60,12 @@ namespace Bit.Android.Autofill
|
||||||
private void UpdateSaveTypeFromHints()
|
private void UpdateSaveTypeFromHints()
|
||||||
{
|
{
|
||||||
SaveType = SaveDataType.Generic;
|
SaveType = SaveDataType.Generic;
|
||||||
if(_autofillHints == null)
|
if(_hints == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach(var hint in _autofillHints)
|
foreach(var hint in _hints)
|
||||||
{
|
{
|
||||||
switch(hint)
|
switch(hint)
|
||||||
{
|
{
|
53
src/Android/Autofill/FieldCollection.cs
Normal file
53
src/Android/Autofill/FieldCollection.cs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Android.Service.Autofill;
|
||||||
|
using Android.Views.Autofill;
|
||||||
|
|
||||||
|
namespace Bit.Android.Autofill
|
||||||
|
{
|
||||||
|
public class FieldCollection
|
||||||
|
{
|
||||||
|
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 List<Field> Fields { get; private set; } = new List<Field>();
|
||||||
|
public IDictionary<int, Field> IdToFieldMap { get; private set; } =
|
||||||
|
new Dictionary<int, Field>();
|
||||||
|
public IDictionary<string, List<Field>> HintToFieldsMap { get; private set; } =
|
||||||
|
new Dictionary<string, List<Field>>();
|
||||||
|
|
||||||
|
public void Add(Field field)
|
||||||
|
{
|
||||||
|
if(Ids.Contains(field.Id))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveType |= field.SaveType;
|
||||||
|
Ids.Add(field.Id);
|
||||||
|
Fields.Add(field);
|
||||||
|
AutofillIds.Add(field.AutofillId);
|
||||||
|
IdToFieldMap.Add(field.Id, field);
|
||||||
|
|
||||||
|
if((field.Hints?.Count ?? 0) > 0)
|
||||||
|
{
|
||||||
|
Hints.AddRange(field.Hints);
|
||||||
|
if(field.Focused)
|
||||||
|
{
|
||||||
|
FocusedHints.AddRange(field.Hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var hint in field.Hints)
|
||||||
|
{
|
||||||
|
if(!HintToFieldsMap.ContainsKey(hint))
|
||||||
|
{
|
||||||
|
HintToFieldsMap.Add(hint, new List<Field>());
|
||||||
|
}
|
||||||
|
|
||||||
|
HintToFieldsMap[hint].Add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +1,23 @@
|
||||||
using static Android.App.Assist.AssistStructure;
|
using System.Collections.Generic;
|
||||||
|
using static Android.App.Assist.AssistStructure;
|
||||||
|
|
||||||
namespace Bit.Android.Autofill
|
namespace Bit.Android.Autofill
|
||||||
{
|
{
|
||||||
public class FilledAutofillField
|
public class FilledField
|
||||||
{
|
{
|
||||||
/**
|
private IEnumerable<string> _hints = null;
|
||||||
* Does not need to be serialized into persistent storage, so it's not exposed.
|
|
||||||
*/
|
|
||||||
private string[] _autofillHints = null;
|
|
||||||
|
|
||||||
public FilledAutofillField() { }
|
public FilledField() { }
|
||||||
|
|
||||||
public FilledAutofillField(ViewNode viewNode)
|
public FilledField(ViewNode viewNode)
|
||||||
{
|
{
|
||||||
_autofillHints = AutofillHelper.FilterForSupportedHints(viewNode.GetAutofillHints());
|
_hints = AutofillHelpers.FilterForSupportedHints(viewNode.GetAutofillHints());
|
||||||
var autofillValue = viewNode.AutofillValue;
|
var autofillValue = viewNode.AutofillValue;
|
||||||
if(autofillValue != null)
|
if(autofillValue == null)
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(autofillValue.IsList)
|
if(autofillValue.IsList)
|
||||||
{
|
{
|
||||||
var autofillOptions = viewNode.GetAutofillOptions();
|
var autofillOptions = viewNode.GetAutofillOptions();
|
||||||
|
@ -37,15 +38,14 @@ namespace Bit.Android.Autofill
|
||||||
TextValue = autofillValue.TextValue;
|
TextValue = autofillValue.TextValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public string TextValue { get; set; }
|
public string TextValue { get; set; }
|
||||||
public long? DateValue { get; set; }
|
public long? DateValue { get; set; }
|
||||||
public bool? ToggleValue { get; set; }
|
public bool? ToggleValue { get; set; }
|
||||||
|
|
||||||
public string[] GetAutofillHints()
|
public IEnumerable<string> GetHints()
|
||||||
{
|
{
|
||||||
return _autofillHints;
|
return _hints;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsNull()
|
public bool IsNull()
|
||||||
|
@ -56,16 +56,25 @@ namespace Bit.Android.Autofill
|
||||||
public override bool Equals(object o)
|
public override bool Equals(object o)
|
||||||
{
|
{
|
||||||
if(this == o)
|
if(this == o)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if(o == null || GetType() != o.GetType())
|
if(o == null || GetType() != o.GetType())
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var that = (FilledAutofillField)o;
|
var that = o as FilledField;
|
||||||
if(TextValue != null ? !TextValue.Equals(that.TextValue) : that.TextValue != null)
|
if(TextValue != null ? !TextValue.Equals(that.TextValue) : that.TextValue != null)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if(DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null)
|
if(DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null;
|
return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null;
|
||||||
}
|
}
|
|
@ -3,39 +3,42 @@ using System.Collections.Generic;
|
||||||
using Android.Service.Autofill;
|
using Android.Service.Autofill;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Views.Autofill;
|
using Android.Views.Autofill;
|
||||||
|
using System.Linq;
|
||||||
|
using Android.Text;
|
||||||
|
|
||||||
namespace Bit.Android.Autofill
|
namespace Bit.Android.Autofill
|
||||||
{
|
{
|
||||||
public class FilledAutofillFieldCollection
|
public class FilledFieldCollection : IFilledItem
|
||||||
{
|
{
|
||||||
public FilledAutofillFieldCollection()
|
public FilledFieldCollection()
|
||||||
: this(null, new Dictionary<string, FilledAutofillField>())
|
: this(null, new Dictionary<string, FilledField>())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public FilledFieldCollection(string datasetName, IDictionary<string, FilledField> hintMap)
|
||||||
{
|
{
|
||||||
|
HintToFieldMap = hintMap;
|
||||||
|
Name = datasetName;
|
||||||
|
Subtitle = "username";
|
||||||
}
|
}
|
||||||
|
|
||||||
public FilledAutofillFieldCollection(string datasetName, IDictionary<string, FilledAutofillField> hintMap)
|
public IDictionary<string, FilledField> HintToFieldMap { get; private set; }
|
||||||
{
|
public string Name { get; set; }
|
||||||
HintMap = hintMap;
|
public string Subtitle { get; set; }
|
||||||
DatasetName = datasetName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDictionary<string, FilledAutofillField> HintMap { get; private set; }
|
|
||||||
public string DatasetName { get; set; }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a {@code FilledAutofillField} to the collection, indexed by all of its hints.
|
* Adds a {@code FilledAutofillField} to the collection, indexed by all of its hints.
|
||||||
*/
|
*/
|
||||||
public void Add(FilledAutofillField filledAutofillField)
|
public void Add(FilledField filledAutofillField)
|
||||||
{
|
{
|
||||||
if(filledAutofillField == null)
|
if(filledAutofillField == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(filledAutofillField));
|
throw new ArgumentNullException(nameof(filledAutofillField));
|
||||||
}
|
}
|
||||||
|
|
||||||
var autofillHints = filledAutofillField.GetAutofillHints();
|
var autofillHints = filledAutofillField.GetHints();
|
||||||
foreach(var hint in autofillHints)
|
foreach(var hint in autofillHints)
|
||||||
{
|
{
|
||||||
HintMap.Add(hint, filledAutofillField);
|
HintToFieldMap.Add(hint, filledAutofillField);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,35 +51,33 @@ namespace Bit.Android.Autofill
|
||||||
* to Views specified in a {@code AutofillFieldMetadataCollection}, which represents the current
|
* to Views specified in a {@code AutofillFieldMetadataCollection}, which represents the current
|
||||||
* page the user is on.
|
* page the user is on.
|
||||||
*/
|
*/
|
||||||
public bool ApplyToFields(AutofillFieldMetadataCollection autofillFieldMetadataCollection,
|
public bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder)
|
||||||
Dataset.Builder datasetBuilder)
|
|
||||||
{
|
{
|
||||||
var setValueAtLeastOnce = false;
|
var setValueAtLeastOnce = false;
|
||||||
var allHints = autofillFieldMetadataCollection.AutofillHints;
|
var allHints = fieldCollection.Hints;
|
||||||
for(var hintIndex = 0; hintIndex < allHints.Count; hintIndex++)
|
for(var hintIndex = 0; hintIndex < allHints.Count; hintIndex++)
|
||||||
{
|
{
|
||||||
var hint = allHints[hintIndex];
|
var hint = allHints[hintIndex];
|
||||||
if(!autofillFieldMetadataCollection.AutofillHintsToFieldsMap.ContainsKey(hint))
|
if(!fieldCollection.HintToFieldsMap.ContainsKey(hint))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fillableAutofillFields = autofillFieldMetadataCollection.AutofillHintsToFieldsMap[hint];
|
var fillableAutofillFields = fieldCollection.HintToFieldsMap[hint];
|
||||||
for(var autofillFieldIndex = 0; autofillFieldIndex < fillableAutofillFields.Count; autofillFieldIndex++)
|
for(var autofillFieldIndex = 0; autofillFieldIndex < fillableAutofillFields.Count; autofillFieldIndex++)
|
||||||
{
|
{
|
||||||
if(!HintMap.ContainsKey(hint))
|
if(!HintToFieldMap.ContainsKey(hint))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filledAutofillField = HintMap[hint];
|
var filledField = HintToFieldMap[hint];
|
||||||
var autofillFieldMetadata = fillableAutofillFields[autofillFieldIndex];
|
var fieldMetadata = fillableAutofillFields[autofillFieldIndex];
|
||||||
var autofillId = autofillFieldMetadata.AutofillId;
|
var autofillId = fieldMetadata.AutofillId;
|
||||||
var autofillType = autofillFieldMetadata.AutofillType;
|
switch(fieldMetadata.AutofillType)
|
||||||
switch(autofillType)
|
|
||||||
{
|
{
|
||||||
case AutofillType.List:
|
case AutofillType.List:
|
||||||
int listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue);
|
int listValue = fieldMetadata.GetAutofillOptionIndex(filledField.TextValue);
|
||||||
if(listValue != -1)
|
if(listValue != -1)
|
||||||
{
|
{
|
||||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
|
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
|
||||||
|
@ -84,7 +85,7 @@ namespace Bit.Android.Autofill
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AutofillType.Date:
|
case AutofillType.Date:
|
||||||
var dateValue = filledAutofillField.DateValue;
|
var dateValue = filledField.DateValue;
|
||||||
if(dateValue != null)
|
if(dateValue != null)
|
||||||
{
|
{
|
||||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate(dateValue.Value));
|
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate(dateValue.Value));
|
||||||
|
@ -92,7 +93,7 @@ namespace Bit.Android.Autofill
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AutofillType.Text:
|
case AutofillType.Text:
|
||||||
var textValue = filledAutofillField.TextValue;
|
var textValue = filledField.TextValue;
|
||||||
if(textValue != null)
|
if(textValue != null)
|
||||||
{
|
{
|
||||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
|
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
|
||||||
|
@ -100,7 +101,7 @@ namespace Bit.Android.Autofill
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case AutofillType.Toggle:
|
case AutofillType.Toggle:
|
||||||
var toggleValue = filledAutofillField.ToggleValue;
|
var toggleValue = filledField.ToggleValue;
|
||||||
if(toggleValue != null)
|
if(toggleValue != null)
|
||||||
{
|
{
|
||||||
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
|
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
|
||||||
|
@ -114,6 +115,16 @@ namespace Bit.Android.Autofill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!setValueAtLeastOnce)
|
||||||
|
{
|
||||||
|
var password = fieldCollection.Fields.FirstOrDefault(f => f.InputType == InputTypes.TextVariationPassword);
|
||||||
|
// datasetBuilder.SetValue(password.AutofillId, AutofillValue.ForText());
|
||||||
|
if(password != null)
|
||||||
|
{
|
||||||
|
var username = fieldCollection.Fields.TakeWhile(f => f.Id != password.Id).LastOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return setValueAtLeastOnce;
|
return setValueAtLeastOnce;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,15 +135,7 @@ namespace Bit.Android.Autofill
|
||||||
*/
|
*/
|
||||||
public bool HelpsWithHints(List<String> autofillHints)
|
public bool HelpsWithHints(List<String> autofillHints)
|
||||||
{
|
{
|
||||||
for(var i = 0; i < autofillHints.Count; i++)
|
return autofillHints.Any(h => HintToFieldMap.ContainsKey(h) && !HintToFieldMap[h].IsNull());
|
||||||
{
|
|
||||||
if(HintMap.ContainsKey(autofillHints[i]) && !HintMap[autofillHints[i]].IsNull())
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
12
src/Android/Autofill/IFilledItem.cs
Normal file
12
src/Android/Autofill/IFilledItem.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using Android.Service.Autofill;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Bit.Android.Autofill
|
||||||
|
{
|
||||||
|
public interface IFilledItem
|
||||||
|
{
|
||||||
|
string Name { get; set; }
|
||||||
|
string Subtitle { get; set; }
|
||||||
|
bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,18 +3,26 @@ using Android.App.Assist;
|
||||||
|
|
||||||
namespace Bit.Android.Autofill
|
namespace Bit.Android.Autofill
|
||||||
{
|
{
|
||||||
public class StructureParser
|
public class Parser
|
||||||
{
|
{
|
||||||
private readonly AssistStructure _structure;
|
private readonly AssistStructure _structure;
|
||||||
private FilledAutofillFieldCollection _filledAutofillFieldCollection;
|
private string _uri;
|
||||||
|
private FilledFieldCollection _filledAutofillFieldCollection;
|
||||||
|
|
||||||
public StructureParser(AssistStructure structure)
|
public Parser(AssistStructure structure)
|
||||||
{
|
{
|
||||||
_structure = structure;
|
_structure = structure;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AutofillFieldMetadataCollection AutofillFields { get; private set; }
|
public FieldCollection FieldCollection { get; private set; } = new FieldCollection();
|
||||||
= new AutofillFieldMetadataCollection();
|
public string Uri
|
||||||
|
{
|
||||||
|
get => _uri;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_uri = $"androidapp://{value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ParseForFill()
|
public void ParseForFill()
|
||||||
{
|
{
|
||||||
|
@ -31,7 +39,7 @@ namespace Bit.Android.Autofill
|
||||||
*/
|
*/
|
||||||
private void Parse(bool forFill)
|
private void Parse(bool forFill)
|
||||||
{
|
{
|
||||||
_filledAutofillFieldCollection = new FilledAutofillFieldCollection();
|
_filledAutofillFieldCollection = new FilledFieldCollection();
|
||||||
|
|
||||||
for(var i = 0; i < _structure.WindowNodeCount; i++)
|
for(var i = 0; i < _structure.WindowNodeCount; i++)
|
||||||
{
|
{
|
||||||
|
@ -51,11 +59,17 @@ namespace Bit.Android.Autofill
|
||||||
{
|
{
|
||||||
if(forFill)
|
if(forFill)
|
||||||
{
|
{
|
||||||
AutofillFields.Add(new AutofillFieldMetadata(viewNode));
|
var f = new Field(viewNode);
|
||||||
|
FieldCollection.Add(f);
|
||||||
|
|
||||||
|
if(Uri == null)
|
||||||
|
{
|
||||||
|
Uri = viewNode.IdPackage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_filledAutofillFieldCollection.Add(new FilledAutofillField(viewNode));
|
_filledAutofillFieldCollection.Add(new FilledField(viewNode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +79,7 @@ namespace Bit.Android.Autofill
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public FilledAutofillFieldCollection GetClientFormData()
|
public FilledFieldCollection GetClientFormData()
|
||||||
{
|
{
|
||||||
return _filledAutofillFieldCollection;
|
return _filledAutofillFieldCollection;
|
||||||
}
|
}
|
Loading…
Reference in a new issue