diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 3cd1af98f..d868ce376 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -66,6 +66,13 @@
+
+
+
+
+
+
+
@@ -365,5 +372,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs
new file mode 100644
index 000000000..bc539bbc1
--- /dev/null
+++ b/src/Android/Autofill/AutofillHelpers.cs
@@ -0,0 +1,203 @@
+using System.Collections.Generic;
+using Android.Content;
+using Android.Service.Autofill;
+using Android.Widget;
+using System.Linq;
+using Android.App;
+using System.Threading.Tasks;
+using Bit.App.Resources;
+using Bit.Core.Enums;
+using Android.Views.Autofill;
+using Bit.Core.Abstractions;
+
+namespace Bit.Droid.Autofill
+{
+ public static class AutofillHelpers
+ {
+ private static int _pendingIntentId = 0;
+
+ // These browser work natively with the autofill framework
+ public static HashSet TrustedBrowsers = new HashSet
+ {
+ "org.mozilla.focus",
+ "org.mozilla.klar",
+ "com.duckduckgo.mobile.android",
+ };
+
+ // These browsers work using the compatibility shim for the autofill framework
+ public static HashSet CompatBrowsers = new HashSet
+ {
+ "org.mozilla.firefox",
+ "org.mozilla.firefox_beta",
+ "com.microsoft.emmx",
+ "com.android.chrome",
+ "com.chrome.beta",
+ "com.android.browser",
+ "com.brave.browser",
+ "com.opera.browser",
+ "com.opera.browser.beta",
+ "com.opera.mini.native",
+ "com.chrome.dev",
+ "com.chrome.canary",
+ "com.google.android.apps.chrome",
+ "com.google.android.apps.chrome_dev",
+ "com.yandex.browser",
+ "com.sec.android.app.sbrowser",
+ "com.sec.android.app.sbrowser.beta",
+ "org.codeaurora.swe.browser",
+ "com.amazon.cloud9",
+ "mark.via.gp",
+ "org.bromite.bromite",
+ "org.chromium.chrome",
+ "com.kiwibrowser.browser",
+ "com.ecosia.android",
+ "com.opera.mini.native.beta",
+ "org.mozilla.fennec_aurora",
+ "com.qwant.liberty",
+ "com.opera.touch",
+ "org.mozilla.fenix",
+ "org.mozilla.reference.browser",
+ "org.mozilla.rocket",
+ };
+
+ // The URLs are blacklisted from autofilling
+ public static HashSet BlacklistedUris = new HashSet
+ {
+ "androidapp://android",
+ "androidapp://com.x8bit.bitwarden",
+ "androidapp://com.oneplus.applocker",
+ };
+
+ public static async Task> GetFillItemsAsync(Parser parser, ICipherService cipherService)
+ {
+ if(parser.FieldCollection.FillableForLogin)
+ {
+ var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri);
+ if(ciphers.Item1.Any() || ciphers.Item2.Any())
+ {
+ var allCiphers = ciphers.Item1.ToList();
+ allCiphers.AddRange(ciphers.Item2.ToList());
+ return allCiphers.Select(c => new FilledItem(c)).ToList();
+ }
+ }
+ else if(parser.FieldCollection.FillableForCard)
+ {
+ var ciphers = await cipherService.GetAllDecryptedAsync();
+ return ciphers.Where(c => c.Type == CipherType.Card).Select(c => new FilledItem(c)).ToList();
+ }
+ return new List();
+ }
+
+ public static FillResponse BuildFillResponse(Parser parser, List items, bool locked)
+ {
+ var responseBuilder = new FillResponse.Builder();
+ if(items != null && items.Count > 0)
+ {
+ foreach(var item in items)
+ {
+ var dataset = BuildDataset(parser.ApplicationContext, parser.FieldCollection, item);
+ if(dataset != null)
+ {
+ responseBuilder.AddDataset(dataset);
+ }
+ }
+ }
+ responseBuilder.AddDataset(BuildVaultDataset(parser.ApplicationContext, parser.FieldCollection,
+ parser.Uri, locked));
+ AddSaveInfo(parser, responseBuilder, parser.FieldCollection);
+ responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
+ return responseBuilder.Build();
+ }
+
+ public static Dataset BuildDataset(Context context, FieldCollection fields, FilledItem filledItem)
+ {
+ var datasetBuilder = new Dataset.Builder(
+ BuildListView(filledItem.Name, filledItem.Subtitle, filledItem.Icon, context));
+ if(filledItem.ApplyToFields(fields, datasetBuilder))
+ {
+ return datasetBuilder.Build();
+ }
+ return null;
+ }
+
+ public static Dataset BuildVaultDataset(Context context, FieldCollection fields, string uri, bool locked)
+ {
+ var intent = new Intent(context, typeof(MainActivity));
+ intent.PutExtra("autofillFramework", true);
+ if(fields.FillableForLogin)
+ {
+ intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Login);
+ }
+ else if(fields.FillableForCard)
+ {
+ intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Card);
+ }
+ else if(fields.FillableForIdentity)
+ {
+ intent.PutExtra("autofillFrameworkFillType", (int)CipherType.Identity);
+ }
+ else
+ {
+ return null;
+ }
+ intent.PutExtra("autofillFrameworkUri", uri);
+ var pendingIntent = PendingIntent.GetActivity(context, ++_pendingIntentId, intent,
+ PendingIntentFlags.CancelCurrent);
+
+ var view = BuildListView(
+ AppResources.AutofillWithBitwarden,
+ locked ? AppResources.VaultIsLocked : AppResources.GoToMyVault,
+ Resource.Drawable.icon,
+ context);
+
+ var datasetBuilder = new Dataset.Builder(view);
+ datasetBuilder.SetAuthentication(pendingIntent.IntentSender);
+
+ // Dataset must have a value set. We will reset this in the main activity when the real item is chosen.
+ foreach(var autofillId in fields.AutofillIds)
+ {
+ datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
+ }
+ return datasetBuilder.Build();
+ }
+
+ public static RemoteViews BuildListView(string text, string subtext, int iconId, Context context)
+ {
+ var packageName = context.PackageName;
+ var view = new RemoteViews(packageName, Resource.Layout.autofill_listitem);
+ view.SetTextViewText(Resource.Id.text, text);
+ view.SetTextViewText(Resource.Id.text2, subtext);
+ view.SetImageViewResource(Resource.Id.icon, iconId);
+ return view;
+ }
+
+ public static void AddSaveInfo(Parser parser, FillResponse.Builder responseBuilder, FieldCollection fields)
+ {
+ // Docs state that password fields cannot be reliably saved in Compat mode since they will show as
+ // masked values.
+ var compatBrowser = CompatBrowsers.Contains(parser.PackageName);
+ if(compatBrowser && fields.SaveType == SaveDataType.Password)
+ {
+ return;
+ }
+
+ var requiredIds = fields.GetRequiredSaveFields();
+ if(fields.SaveType == SaveDataType.Generic || requiredIds.Length == 0)
+ {
+ return;
+ }
+
+ var saveBuilder = new SaveInfo.Builder(fields.SaveType, requiredIds);
+ var optionalIds = fields.GetOptionalSaveIds();
+ if(optionalIds.Length > 0)
+ {
+ saveBuilder.SetOptionalIds(optionalIds);
+ }
+ if(compatBrowser)
+ {
+ saveBuilder.SetFlags(SaveFlags.SaveOnAllViewsInvisible);
+ }
+ responseBuilder.SetSaveInfo(saveBuilder.Build());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs
new file mode 100644
index 000000000..38341ed3e
--- /dev/null
+++ b/src/Android/Autofill/AutofillService.cs
@@ -0,0 +1,113 @@
+using Android;
+using Android.App;
+using Android.Content;
+using Android.OS;
+using Android.Runtime;
+using Android.Service.Autofill;
+using Android.Widget;
+using Bit.Core;
+using Bit.Core.Abstractions;
+using Bit.Core.Enums;
+using Bit.Core.Utilities;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Bit.Droid.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 : Android.Service.Autofill.AutofillService
+ {
+ private ICipherService _cipherService;
+ //private ILockService _lockService;
+
+ public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)
+ {
+ var structure = request.FillContexts?.LastOrDefault()?.Structure;
+ if(structure == null)
+ {
+ return;
+ }
+
+ var parser = new Parser(structure, ApplicationContext);
+ parser.Parse();
+
+ if(!parser.ShouldAutofill)
+ {
+ return;
+ }
+
+ /*
+ if(_lockService == null)
+ {
+ _lockService = ServiceContainer.Resolve("lockService");
+ }
+ */
+
+ List items = null;
+ var locked = true; // TODO
+ if(!locked)
+ {
+ if(_cipherService == null)
+ {
+ _cipherService = ServiceContainer.Resolve("cipherService");
+ }
+ items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
+ }
+
+ // build response
+ var response = AutofillHelpers.BuildFillResponse(parser, items, locked);
+ callback.OnSuccess(response);
+ }
+
+ public override void OnSaveRequest(SaveRequest request, SaveCallback callback)
+ {
+ var structure = request.FillContexts?.LastOrDefault()?.Structure;
+ if(structure == null)
+ {
+ return;
+ }
+
+ var parser = new Parser(structure, ApplicationContext);
+ 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)savedItem.Type);
+ switch(savedItem.Type)
+ {
+ case CipherType.Login:
+ intent.PutExtra("autofillFrameworkName", parser.Uri
+ .Replace(Constants.AndroidAppProtocol, string.Empty)
+ .Replace("https://", string.Empty)
+ .Replace("http://", string.Empty));
+ intent.PutExtra("autofillFrameworkUri", parser.Uri);
+ intent.PutExtra("autofillFrameworkUsername", savedItem.Login.Username);
+ intent.PutExtra("autofillFrameworkPassword", savedItem.Login.Password);
+ break;
+ case CipherType.Card:
+ intent.PutExtra("autofillFrameworkCardName", savedItem.Card.Name);
+ intent.PutExtra("autofillFrameworkCardNumber", savedItem.Card.Number);
+ intent.PutExtra("autofillFrameworkCardExpMonth", savedItem.Card.ExpMonth);
+ intent.PutExtra("autofillFrameworkCardExpYear", savedItem.Card.ExpYear);
+ intent.PutExtra("autofillFrameworkCardCode", savedItem.Card.Code);
+ break;
+ default:
+ Toast.MakeText(this, "Unable to save this type of form.", ToastLength.Short).Show();
+ return;
+ }
+ StartActivity(intent);
+ }
+ }
+}
diff --git a/src/Android/Autofill/Field.cs b/src/Android/Autofill/Field.cs
new file mode 100644
index 000000000..34a6d664c
--- /dev/null
+++ b/src/Android/Autofill/Field.cs
@@ -0,0 +1,195 @@
+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;
+using static Android.Views.ViewStructure;
+
+namespace Bit.Droid.Autofill
+{
+ public class Field
+ {
+ private List _hints;
+
+ public Field(ViewNode node)
+ {
+ Id = node.Id;
+ TrackingId = $"{node.Id}_{node.GetHashCode()}";
+ IdEntry = node.IdEntry;
+ AutofillId = node.AutofillId;
+ AutofillType = node.AutofillType;
+ InputType = node.InputType;
+ Focused = node.IsFocused;
+ Selected = node.IsSelected;
+ Clickable = node.IsClickable;
+ Visible = node.Visibility == ViewStates.Visible;
+ Hints = FilterForSupportedHints(node.GetAutofillHints());
+ Hint = node.Hint;
+ AutofillOptions = node.GetAutofillOptions()?.ToList();
+ HtmlInfo = node.HtmlInfo;
+ Node = node;
+
+ if(node.AutofillValue != null)
+ {
+ if(node.AutofillValue.IsList)
+ {
+ var autofillOptions = node.GetAutofillOptions();
+ if(autofillOptions != null && autofillOptions.Length > 0)
+ {
+ ListValue = node.AutofillValue.ListValue;
+ TextValue = autofillOptions[node.AutofillValue.ListValue];
+ }
+ }
+ else if(node.AutofillValue.IsDate)
+ {
+ DateValue = node.AutofillValue.DateValue;
+ }
+ else if(node.AutofillValue.IsText)
+ {
+ TextValue = node.AutofillValue.TextValue;
+ }
+ else if(node.AutofillValue.IsToggle)
+ {
+ ToggleValue = node.AutofillValue.ToggleValue;
+ }
+ }
+ }
+
+ public SaveDataType SaveType { get; set; } = SaveDataType.Generic;
+ public List Hints
+ {
+ get => _hints;
+ set
+ {
+ _hints = value;
+ UpdateSaveTypeFromHints();
+ }
+ }
+ public string Hint { get; set; }
+ public int Id { get; private set; }
+ public string TrackingId { get; private set; }
+ public string IdEntry { get; set; }
+ public AutofillId AutofillId { get; private set; }
+ public AutofillType AutofillType { get; private set; }
+ public InputTypes InputType { get; private set; }
+ public bool Focused { get; private set; }
+ public bool Selected { get; private set; }
+ 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 int? ListValue { get; set; }
+ public bool? ToggleValue { get; set; }
+ public HtmlInfo HtmlInfo { get; private set; }
+ public ViewNode Node { get; private set; }
+
+ 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;
+ }
+
+ private static List FilterForSupportedHints(string[] hints)
+ {
+ return hints?.Where(h => IsValidHint(h)).ToList() ?? new List();
+ }
+
+ 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;
+ }
+ }
+
+ private void UpdateSaveTypeFromHints()
+ {
+ SaveType = SaveDataType.Generic;
+ if(_hints == null)
+ {
+ return;
+ }
+
+ foreach(var hint in _hints)
+ {
+ 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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Android/Autofill/FieldCollection.cs b/src/Android/Autofill/FieldCollection.cs
new file mode 100644
index 000000000..84e6f48db
--- /dev/null
+++ b/src/Android/Autofill/FieldCollection.cs
@@ -0,0 +1,342 @@
+using System.Collections.Generic;
+using Android.Service.Autofill;
+using Android.Views.Autofill;
+using System.Linq;
+using Android.Text;
+using Android.Views;
+
+namespace Bit.Droid.Autofill
+{
+ public class FieldCollection
+ {
+ private List _passwordFields = null;
+ private List _usernameFields = null;
+ private HashSet _ignoreSearchTerms = new HashSet { "search", "find", "recipient", "edit" };
+ private HashSet _passwordTerms = new HashSet { "password", "pswd" };
+
+ public List AutofillIds { get; private set; } = new List();
+ public SaveDataType SaveType
+ {
+ get
+ {
+ if(FillableForLogin)
+ {
+ return SaveDataType.Password;
+ }
+ else if(FillableForCard)
+ {
+ return SaveDataType.CreditCard;
+ }
+
+ return SaveDataType.Generic;
+ }
+ }
+ public HashSet Hints { get; private set; } = new HashSet();
+ public HashSet FocusedHints { get; private set; } = new HashSet();
+ public HashSet FieldTrackingIds { get; private set; } = new HashSet();
+ public List Fields { get; private set; } = new List();
+ public IDictionary> HintToFieldsMap { get; private set; } =
+ new Dictionary>();
+ public List IgnoreAutofillIds { get; private set; } = new List();
+
+ public List PasswordFields
+ {
+ get
+ {
+ if(_passwordFields != null)
+ {
+ return _passwordFields;
+ }
+ if(Hints.Any())
+ {
+ _passwordFields = new List();
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
+ {
+ _passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
+ }
+ }
+ else
+ {
+ _passwordFields = Fields.Where(f => FieldIsPassword(f)).ToList();
+ if(!_passwordFields.Any())
+ {
+ _passwordFields = Fields.Where(f => FieldHasPasswordTerms(f)).ToList();
+ }
+ }
+ return _passwordFields;
+ }
+ }
+
+ public List UsernameFields
+ {
+ get
+ {
+ if(_usernameFields != null)
+ {
+ return _usernameFields;
+ }
+ _usernameFields = new List();
+ if(Hints.Any())
+ {
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintEmailAddress))
+ {
+ _usernameFields.AddRange(HintToFieldsMap[View.AutofillHintEmailAddress]);
+ }
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintUsername))
+ {
+ _usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
+ }
+ }
+ else
+ {
+ foreach(var passwordField in PasswordFields)
+ {
+ var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId)
+ .LastOrDefault();
+ if(usernameField != null)
+ {
+ _usernameFields.Add(usernameField);
+ }
+ }
+ }
+ return _usernameFields;
+ }
+ }
+
+ 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 bool Fillable => FillableForLogin || FillableForCard || FillableForIdentity;
+
+ public void Add(Field field)
+ {
+ if(field == null || FieldTrackingIds.Contains(field.TrackingId))
+ {
+ return;
+ }
+
+ _passwordFields = _usernameFields = null;
+ FieldTrackingIds.Add(field.TrackingId);
+ Fields.Add(field);
+ AutofillIds.Add(field.AutofillId);
+
+ if(field.Hints != null)
+ {
+ foreach(var hint in field.Hints)
+ {
+ Hints.Add(hint);
+ if(field.Focused)
+ {
+ FocusedHints.Add(hint);
+ }
+ if(!HintToFieldsMap.ContainsKey(hint))
+ {
+ HintToFieldsMap.Add(hint, new List());
+ }
+ HintToFieldsMap[hint].Add(field);
+ }
+ }
+ }
+
+ public SavedItem GetSavedItem()
+ {
+ if(SaveType == SaveDataType.Password)
+ {
+ var passwordField = PasswordFields.FirstOrDefault(f => !string.IsNullOrWhiteSpace(f.TextValue));
+ if(passwordField == null)
+ {
+ return null;
+ }
+
+ var savedItem = new SavedItem
+ {
+ Type = Core.Enums.CipherType.Login,
+ Login = new SavedItem.LoginItem
+ {
+ Password = GetFieldValue(passwordField)
+ }
+ };
+
+ var usernameField = Fields.TakeWhile(f => f.AutofillId != passwordField.AutofillId).LastOrDefault();
+ savedItem.Login.Username = GetFieldValue(usernameField);
+ return savedItem;
+ }
+ else if(SaveType == SaveDataType.CreditCard)
+ {
+ var savedItem = new SavedItem
+ {
+ Type = Core.Enums.CipherType.Card,
+ Card = new SavedItem.CardItem
+ {
+ Number = GetFieldValue(View.AutofillHintCreditCardNumber),
+ Name = GetFieldValue(View.AutofillHintName),
+ ExpMonth = GetFieldValue(View.AutofillHintCreditCardExpirationMonth, true),
+ ExpYear = GetFieldValue(View.AutofillHintCreditCardExpirationYear),
+ Code = GetFieldValue(View.AutofillHintCreditCardSecurityCode)
+ }
+ };
+ return savedItem;
+ }
+ return null;
+ }
+
+ public AutofillId[] GetOptionalSaveIds()
+ {
+ if(SaveType == SaveDataType.Password)
+ {
+ return UsernameFields.Select(f => f.AutofillId).ToArray();
+ }
+ else if(SaveType == SaveDataType.CreditCard)
+ {
+ var fieldList = new List();
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardSecurityCode))
+ {
+ fieldList.AddRange(HintToFieldsMap[View.AutofillHintCreditCardSecurityCode]);
+ }
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationYear))
+ {
+ fieldList.AddRange(HintToFieldsMap[View.AutofillHintCreditCardExpirationYear]);
+ }
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationMonth))
+ {
+ fieldList.AddRange(HintToFieldsMap[View.AutofillHintCreditCardExpirationMonth]);
+ }
+ if(HintToFieldsMap.ContainsKey(View.AutofillHintName))
+ {
+ fieldList.AddRange(HintToFieldsMap[View.AutofillHintName]);
+ }
+ return fieldList.Select(f => f.AutofillId).ToArray();
+ }
+ return new AutofillId[0];
+ }
+
+ public AutofillId[] GetRequiredSaveFields()
+ {
+ if(SaveType == SaveDataType.Password)
+ {
+ return PasswordFields.Select(f => f.AutofillId).ToArray();
+ }
+ else if(SaveType == SaveDataType.CreditCard && HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardNumber))
+ {
+ return HintToFieldsMap[View.AutofillHintCreditCardNumber].Select(f => f.AutofillId).ToArray();
+ }
+ return new AutofillId[0];
+ }
+
+ private bool FocusedHintsContain(IEnumerable hints)
+ {
+ return hints.Any(h => FocusedHints.Contains(h));
+ }
+
+ private string GetFieldValue(string hint, bool monthValue = false)
+ {
+ if(HintToFieldsMap.ContainsKey(hint))
+ {
+ foreach(var field in HintToFieldsMap[hint])
+ {
+ var val = GetFieldValue(field, monthValue);
+ if(!string.IsNullOrWhiteSpace(val))
+ {
+ return val;
+ }
+ }
+ }
+ return null;
+ }
+
+ private string GetFieldValue(Field field, bool monthValue = false)
+ {
+ if(field == null)
+ {
+ return null;
+ }
+ if(!string.IsNullOrWhiteSpace(field.TextValue))
+ {
+ if(field.AutofillType == AutofillType.List && field.ListValue.HasValue && monthValue)
+ {
+ if(field.AutofillOptions.Count == 13)
+ {
+ return field.ListValue.ToString();
+ }
+ else if(field.AutofillOptions.Count == 12)
+ {
+ return (field.ListValue + 1).ToString();
+ }
+ }
+ return field.TextValue;
+ }
+ else if(field.DateValue.HasValue)
+ {
+ return field.DateValue.Value.ToString();
+ }
+ else if(field.ToggleValue.HasValue)
+ {
+ return field.ToggleValue.Value.ToString();
+ }
+ return null;
+ }
+
+ private bool FieldIsPassword(Field f)
+ {
+ var inputTypePassword = f.InputType.HasFlag(InputTypes.TextVariationPassword) ||
+ f.InputType.HasFlag(InputTypes.TextVariationVisiblePassword) ||
+ f.InputType.HasFlag(InputTypes.TextVariationWebPassword);
+
+ // For whatever reason, multi-line input types are coming through with TextVariationPassword flags
+ if(inputTypePassword && f.InputType.HasFlag(InputTypes.TextVariationPassword) &&
+ f.InputType.HasFlag(InputTypes.TextFlagMultiLine))
+ {
+ inputTypePassword = false;
+ }
+
+ if(!inputTypePassword && f.HtmlInfo != null && f.HtmlInfo.Tag == "input" &&
+ (f.HtmlInfo.Attributes?.Any() ?? false))
+ {
+ foreach(var a in f.HtmlInfo.Attributes)
+ {
+ var key = a.First as Java.Lang.String;
+ var val = a.Second as Java.Lang.String;
+ if(key != null && val != null && key.ToString() == "type" && val.ToString() == "password")
+ {
+ return true;
+ }
+ }
+ }
+
+ return inputTypePassword && !ValueContainsAnyTerms(f.IdEntry, _ignoreSearchTerms) &&
+ !ValueContainsAnyTerms(f.Hint, _ignoreSearchTerms);
+ }
+
+ private bool FieldHasPasswordTerms(Field f)
+ {
+ return ValueContainsAnyTerms(f.IdEntry, _passwordTerms) || ValueContainsAnyTerms(f.Hint, _passwordTerms);
+ }
+
+ private bool ValueContainsAnyTerms(string value, HashSet terms)
+ {
+ if(string.IsNullOrWhiteSpace(value))
+ {
+ return false;
+ }
+ var lowerValue = value.ToLowerInvariant();
+ return terms.Any(t => lowerValue.Contains(t));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Android/Autofill/FilledItem.cs b/src/Android/Autofill/FilledItem.cs
new file mode 100644
index 000000000..ce2aa2671
--- /dev/null
+++ b/src/Android/Autofill/FilledItem.cs
@@ -0,0 +1,224 @@
+using Android.Service.Autofill;
+using Android.Views.Autofill;
+using System.Linq;
+using Bit.Core.Enums;
+using Android.Views;
+using Bit.Core.Models.View;
+
+namespace Bit.Droid.Autofill
+{
+ public class FilledItem
+ {
+ private string _password;
+ private string _cardName;
+ private string _cardNumber;
+ private string _cardExpMonth;
+ private string _cardExpYear;
+ private string _cardCode;
+ private string _idPhone;
+ private string _idEmail;
+ private string _idUsername;
+ private string _idAddress;
+ private string _idPostalCode;
+
+ public FilledItem(CipherView cipher)
+ {
+ Name = cipher.Name;
+ Type = cipher.Type;
+ Subtitle = cipher.SubTitle;
+
+ switch(Type)
+ {
+ case CipherType.Login:
+ Icon = Resource.Drawable.login;
+ _password = cipher.Login.Password;
+ break;
+ case CipherType.Card:
+ _cardNumber = cipher.Card.Number;
+ Icon = Resource.Drawable.card;
+ _cardName = cipher.Card.CardholderName;
+ _cardCode = cipher.Card.Code;
+ _cardExpMonth = cipher.Card.ExpMonth;
+ _cardExpYear = cipher.Card.ExpYear;
+ break;
+ case CipherType.Identity:
+ Icon = Resource.Drawable.id;
+ _idPhone = cipher.Identity.Phone;
+ _idEmail = cipher.Identity.Email;
+ _idUsername = cipher.Identity.Username;
+ _idAddress = cipher.Identity.FullAddress;
+ _idPostalCode = cipher.Identity.PostalCode;
+ break;
+ default:
+ Icon = Resource.Drawable.login;
+ break;
+ }
+ }
+
+ public string Name { get; set; }
+ public string Subtitle { get; set; } = string.Empty;
+ public int Icon { get; set; } = Resource.Drawable.login;
+ public CipherType Type { get; set; }
+
+ public bool ApplyToFields(FieldCollection fieldCollection, Dataset.Builder datasetBuilder)
+ {
+ if(!fieldCollection?.Fields.Any() ?? true)
+ {
+ return false;
+ }
+
+ var setValues = false;
+ if(Type == CipherType.Login)
+ {
+ if(fieldCollection.PasswordFields.Any() && !string.IsNullOrWhiteSpace(_password))
+ {
+ foreach(var f in fieldCollection.PasswordFields)
+ {
+ var val = ApplyValue(f, _password);
+ if(val != null)
+ {
+ setValues = true;
+ datasetBuilder.SetValue(f.AutofillId, val);
+ }
+ }
+ }
+ if(fieldCollection.UsernameFields.Any() && !string.IsNullOrWhiteSpace(Subtitle))
+ {
+ foreach(var f in fieldCollection.UsernameFields)
+ {
+ var val = ApplyValue(f, Subtitle);
+ if(val != null)
+ {
+ setValues = true;
+ datasetBuilder.SetValue(f.AutofillId, val);
+ }
+ }
+ }
+ }
+ else if(Type == CipherType.Card)
+ {
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintCreditCardNumber,
+ _cardNumber))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintCreditCardSecurityCode,
+ _cardCode))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection,
+ Android.Views.View.AutofillHintCreditCardExpirationMonth, _cardExpMonth, true))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintCreditCardExpirationYear,
+ _cardExpYear))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintName, _cardName))
+ {
+ setValues = true;
+ }
+ }
+ else if(Type == CipherType.Identity)
+ {
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintPhone, _idPhone))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintEmailAddress, _idEmail))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintUsername,
+ _idUsername))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintPostalAddress,
+ _idAddress))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintPostalCode,
+ _idPostalCode))
+ {
+ setValues = true;
+ }
+ if(ApplyValue(datasetBuilder, fieldCollection, Android.Views.View.AutofillHintName, Subtitle))
+ {
+ setValues = true;
+ }
+ }
+ return setValues;
+ }
+
+ private static bool ApplyValue(Dataset.Builder builder, FieldCollection fieldCollection,
+ string hint, string value, bool monthValue = false)
+ {
+ bool setValues = false;
+ if(fieldCollection.HintToFieldsMap.ContainsKey(hint) && !string.IsNullOrWhiteSpace(value))
+ {
+ foreach(var f in fieldCollection.HintToFieldsMap[hint])
+ {
+ var val = ApplyValue(f, value, monthValue);
+ if(val != null)
+ {
+ setValues = true;
+ builder.SetValue(f.AutofillId, val);
+ }
+ }
+ }
+ return setValues;
+ }
+
+ private static AutofillValue ApplyValue(Field field, string value, bool monthValue = false)
+ {
+ switch(field.AutofillType)
+ {
+ case AutofillType.Date:
+ if(long.TryParse(value, out long dateValue))
+ {
+ return AutofillValue.ForDate(dateValue);
+ }
+ break;
+ case AutofillType.List:
+ if(field.AutofillOptions != null)
+ {
+ if(monthValue && int.TryParse(value, out int monthIndex))
+ {
+ if(field.AutofillOptions.Count == 13)
+ {
+ return AutofillValue.ForList(monthIndex);
+ }
+ else if(field.AutofillOptions.Count >= monthIndex)
+ {
+ return AutofillValue.ForList(monthIndex - 1);
+ }
+ }
+ for(var i = 0; i < field.AutofillOptions.Count; i++)
+ {
+ if(field.AutofillOptions[i].Equals(value))
+ {
+ return AutofillValue.ForList(i);
+ }
+ }
+ }
+ break;
+ case AutofillType.Text:
+ return AutofillValue.ForText(value);
+ case AutofillType.Toggle:
+ if(bool.TryParse(value, out bool toggleValue))
+ {
+ return AutofillValue.ForToggle(toggleValue);
+ }
+ break;
+ default:
+ break;
+ }
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Android/Autofill/Parser.cs b/src/Android/Autofill/Parser.cs
new file mode 100644
index 000000000..228bb7817
--- /dev/null
+++ b/src/Android/Autofill/Parser.cs
@@ -0,0 +1,130 @@
+using static Android.App.Assist.AssistStructure;
+using Android.App.Assist;
+using System.Collections.Generic;
+using Bit.Core;
+using Android.Content;
+
+namespace Bit.Droid.Autofill
+{
+ public class Parser
+ {
+ public static HashSet _excludedPackageIds = new HashSet
+ {
+ "android"
+ };
+ private readonly AssistStructure _structure;
+ private string _uri;
+ private string _packageName;
+ private string _webDomain;
+
+ public Parser(AssistStructure structure, Context applicationContext)
+ {
+ _structure = structure;
+ ApplicationContext = applicationContext;
+ }
+
+ public Context ApplicationContext { get; set; }
+ public FieldCollection FieldCollection { get; private set; } = new FieldCollection();
+
+ public string Uri
+ {
+ get
+ {
+ if(!string.IsNullOrWhiteSpace(_uri))
+ {
+ return _uri;
+ }
+ var webDomainNull = string.IsNullOrWhiteSpace(WebDomain);
+ if(webDomainNull && string.IsNullOrWhiteSpace(PackageName))
+ {
+ _uri = null;
+ }
+ else if(!webDomainNull)
+ {
+ _uri = string.Concat("http://", WebDomain);
+ }
+ else
+ {
+ _uri = string.Concat(Constants.AndroidAppProtocol, PackageName);
+ }
+ return _uri;
+ }
+ }
+
+ public string PackageName
+ {
+ get => _packageName;
+ set
+ {
+ if(string.IsNullOrWhiteSpace(value))
+ {
+ _packageName = _uri = null;
+ }
+ _packageName = value;
+ }
+ }
+
+ public string WebDomain
+ {
+ get => _webDomain;
+ set
+ {
+ if(string.IsNullOrWhiteSpace(value))
+ {
+ _webDomain = _uri = null;
+ }
+ _webDomain = value;
+ }
+ }
+
+ public bool ShouldAutofill => !string.IsNullOrWhiteSpace(Uri) &&
+ !AutofillHelpers.BlacklistedUris.Contains(Uri) && FieldCollection != null && FieldCollection.Fillable;
+
+ public void Parse()
+ {
+ for(var i = 0; i < _structure.WindowNodeCount; i++)
+ {
+ var node = _structure.GetWindowNodeAt(i);
+ ParseNode(node.RootViewNode);
+ }
+ if(!AutofillHelpers.TrustedBrowsers.Contains(PackageName) &&
+ !AutofillHelpers.CompatBrowsers.Contains(PackageName))
+ {
+ WebDomain = null;
+ }
+ }
+
+ private void ParseNode(ViewNode node)
+ {
+ SetPackageAndDomain(node);
+ var hints = node.GetAutofillHints();
+ var isEditText = node.ClassName == "android.widget.EditText" || node?.HtmlInfo?.Tag == "input";
+ if(isEditText || (hints?.Length ?? 0) > 0)
+ {
+ FieldCollection.Add(new Field(node));
+ }
+ else
+ {
+ FieldCollection.IgnoreAutofillIds.Add(node.AutofillId);
+ }
+
+ for(var i = 0; i < node.ChildCount; i++)
+ {
+ ParseNode(node.GetChildAt(i));
+ }
+ }
+
+ private void SetPackageAndDomain(ViewNode node)
+ {
+ if(string.IsNullOrWhiteSpace(PackageName) && !string.IsNullOrWhiteSpace(node.IdPackage) &&
+ !_excludedPackageIds.Contains(node.IdPackage))
+ {
+ PackageName = node.IdPackage;
+ }
+ if(string.IsNullOrWhiteSpace(WebDomain) && !string.IsNullOrWhiteSpace(node.WebDomain))
+ {
+ WebDomain = node.WebDomain;
+ }
+ }
+ }
+}
diff --git a/src/Android/Autofill/SavedItem.cs b/src/Android/Autofill/SavedItem.cs
new file mode 100644
index 000000000..482bc3a5c
--- /dev/null
+++ b/src/Android/Autofill/SavedItem.cs
@@ -0,0 +1,26 @@
+using Bit.Core.Enums;
+
+namespace Bit.Droid.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/Resources/Resource.designer.cs b/src/Android/Resources/Resource.designer.cs
index 73524a27e..7028622c9 100644
--- a/src/Android/Resources/Resource.designer.cs
+++ b/src/Android/Resources/Resource.designer.cs
@@ -5825,26 +5825,26 @@ namespace Bit.Droid
// aapt resource value: 0x7f02005a
public const int avd_hide_password = 2130837594;
- // aapt resource value: 0x7f020146
- public const int avd_hide_password_1 = 2130837830;
-
// aapt resource value: 0x7f020147
- public const int avd_hide_password_2 = 2130837831;
+ public const int avd_hide_password_1 = 2130837831;
// aapt resource value: 0x7f020148
- public const int avd_hide_password_3 = 2130837832;
+ public const int avd_hide_password_2 = 2130837832;
+
+ // aapt resource value: 0x7f020149
+ public const int avd_hide_password_3 = 2130837833;
// aapt resource value: 0x7f02005b
public const int avd_show_password = 2130837595;
- // aapt resource value: 0x7f020149
- public const int avd_show_password_1 = 2130837833;
-
// aapt resource value: 0x7f02014a
- public const int avd_show_password_2 = 2130837834;
+ public const int avd_show_password_1 = 2130837834;
// aapt resource value: 0x7f02014b
- public const int avd_show_password_3 = 2130837835;
+ public const int avd_show_password_2 = 2130837835;
+
+ // aapt resource value: 0x7f02014c
+ public const int avd_show_password_3 = 2130837836;
// aapt resource value: 0x7f02005c
public const int card = 2130837596;
@@ -6411,142 +6411,145 @@ namespace Bit.Droid
public const int ic_vol_type_tv_light = 2130837783;
// aapt resource value: 0x7f020118
- public const int id = 2130837784;
+ public const int icon = 2130837784;
// aapt resource value: 0x7f020119
- public const int @lock = 2130837785;
+ public const int id = 2130837785;
// aapt resource value: 0x7f02011a
- public const int login = 2130837786;
+ public const int @lock = 2130837786;
// aapt resource value: 0x7f02011b
- public const int logo = 2130837787;
+ public const int login = 2130837787;
// aapt resource value: 0x7f02011c
- public const int more = 2130837788;
+ public const int logo = 2130837788;
// aapt resource value: 0x7f02011d
- public const int mr_button_connected_dark = 2130837789;
+ public const int more = 2130837789;
// aapt resource value: 0x7f02011e
- public const int mr_button_connected_light = 2130837790;
+ public const int mr_button_connected_dark = 2130837790;
// aapt resource value: 0x7f02011f
- public const int mr_button_connecting_dark = 2130837791;
+ public const int mr_button_connected_light = 2130837791;
// aapt resource value: 0x7f020120
- public const int mr_button_connecting_light = 2130837792;
+ public const int mr_button_connecting_dark = 2130837792;
// aapt resource value: 0x7f020121
- public const int mr_button_dark = 2130837793;
+ public const int mr_button_connecting_light = 2130837793;
// aapt resource value: 0x7f020122
- public const int mr_button_light = 2130837794;
+ public const int mr_button_dark = 2130837794;
// aapt resource value: 0x7f020123
- public const int mr_dialog_close_dark = 2130837795;
+ public const int mr_button_light = 2130837795;
// aapt resource value: 0x7f020124
- public const int mr_dialog_close_light = 2130837796;
+ public const int mr_dialog_close_dark = 2130837796;
// aapt resource value: 0x7f020125
- public const int mr_dialog_material_background_dark = 2130837797;
+ public const int mr_dialog_close_light = 2130837797;
// aapt resource value: 0x7f020126
- public const int mr_dialog_material_background_light = 2130837798;
+ public const int mr_dialog_material_background_dark = 2130837798;
// aapt resource value: 0x7f020127
- public const int mr_group_collapse = 2130837799;
+ public const int mr_dialog_material_background_light = 2130837799;
// aapt resource value: 0x7f020128
- public const int mr_group_expand = 2130837800;
+ public const int mr_group_collapse = 2130837800;
// aapt resource value: 0x7f020129
- public const int mr_media_pause_dark = 2130837801;
+ public const int mr_group_expand = 2130837801;
// aapt resource value: 0x7f02012a
- public const int mr_media_pause_light = 2130837802;
+ public const int mr_media_pause_dark = 2130837802;
// aapt resource value: 0x7f02012b
- public const int mr_media_play_dark = 2130837803;
+ public const int mr_media_pause_light = 2130837803;
// aapt resource value: 0x7f02012c
- public const int mr_media_play_light = 2130837804;
+ public const int mr_media_play_dark = 2130837804;
// aapt resource value: 0x7f02012d
- public const int mr_media_stop_dark = 2130837805;
+ public const int mr_media_play_light = 2130837805;
// aapt resource value: 0x7f02012e
- public const int mr_media_stop_light = 2130837806;
+ public const int mr_media_stop_dark = 2130837806;
// aapt resource value: 0x7f02012f
- public const int mr_vol_type_audiotrack_dark = 2130837807;
+ public const int mr_media_stop_light = 2130837807;
// aapt resource value: 0x7f020130
- public const int mr_vol_type_audiotrack_light = 2130837808;
+ public const int mr_vol_type_audiotrack_dark = 2130837808;
// aapt resource value: 0x7f020131
- public const int mtrl_snackbar_background = 2130837809;
+ public const int mr_vol_type_audiotrack_light = 2130837809;
// aapt resource value: 0x7f020132
- public const int mtrl_tabs_default_indicator = 2130837810;
+ public const int mtrl_snackbar_background = 2130837810;
// aapt resource value: 0x7f020133
- public const int navigation_empty_icon = 2130837811;
+ public const int mtrl_tabs_default_indicator = 2130837811;
// aapt resource value: 0x7f020134
- public const int notification_action_background = 2130837812;
+ public const int navigation_empty_icon = 2130837812;
// aapt resource value: 0x7f020135
- public const int notification_bg = 2130837813;
+ public const int notification_action_background = 2130837813;
// aapt resource value: 0x7f020136
- public const int notification_bg_low = 2130837814;
+ public const int notification_bg = 2130837814;
// aapt resource value: 0x7f020137
- public const int notification_bg_low_normal = 2130837815;
+ public const int notification_bg_low = 2130837815;
// aapt resource value: 0x7f020138
- public const int notification_bg_low_pressed = 2130837816;
+ public const int notification_bg_low_normal = 2130837816;
// aapt resource value: 0x7f020139
- public const int notification_bg_normal = 2130837817;
+ public const int notification_bg_low_pressed = 2130837817;
// aapt resource value: 0x7f02013a
- public const int notification_bg_normal_pressed = 2130837818;
+ public const int notification_bg_normal = 2130837818;
// aapt resource value: 0x7f02013b
- public const int notification_icon_background = 2130837819;
-
- // aapt resource value: 0x7f020144
- public const int notification_template_icon_bg = 2130837828;
-
- // aapt resource value: 0x7f020145
- public const int notification_template_icon_low_bg = 2130837829;
+ public const int notification_bg_normal_pressed = 2130837819;
// aapt resource value: 0x7f02013c
- public const int notification_tile_bg = 2130837820;
+ public const int notification_icon_background = 2130837820;
+
+ // aapt resource value: 0x7f020145
+ public const int notification_template_icon_bg = 2130837829;
+
+ // aapt resource value: 0x7f020146
+ public const int notification_template_icon_low_bg = 2130837830;
// aapt resource value: 0x7f02013d
- public const int notify_panel_notification_icon_bg = 2130837821;
+ public const int notification_tile_bg = 2130837821;
// aapt resource value: 0x7f02013e
- public const int refresh = 2130837822;
+ public const int notify_panel_notification_icon_bg = 2130837822;
// aapt resource value: 0x7f02013f
- public const int shield = 2130837823;
+ public const int refresh = 2130837823;
// aapt resource value: 0x7f020140
- public const int splash_screen = 2130837824;
+ public const int shield = 2130837824;
// aapt resource value: 0x7f020141
- public const int tooltip_frame_dark = 2130837825;
+ public const int splash_screen = 2130837825;
// aapt resource value: 0x7f020142
- public const int tooltip_frame_light = 2130837826;
+ public const int tooltip_frame_dark = 2130837826;
// aapt resource value: 0x7f020143
- public const int yubikey = 2130837827;
+ public const int tooltip_frame_light = 2130837827;
+
+ // aapt resource value: 0x7f020144
+ public const int yubikey = 2130837828;
static Drawable()
{
diff --git a/src/Android/Resources/drawable-hdpi/icon.png b/src/Android/Resources/drawable-hdpi/icon.png
new file mode 100644
index 000000000..8b4107e4a
Binary files /dev/null and b/src/Android/Resources/drawable-hdpi/icon.png differ
diff --git a/src/Android/Resources/drawable-xhdpi/icon.png b/src/Android/Resources/drawable-xhdpi/icon.png
new file mode 100644
index 000000000..fcd1018f5
Binary files /dev/null and b/src/Android/Resources/drawable-xhdpi/icon.png differ
diff --git a/src/Android/Resources/drawable-xxhdpi/icon.png b/src/Android/Resources/drawable-xxhdpi/icon.png
new file mode 100644
index 000000000..b16b09f43
Binary files /dev/null and b/src/Android/Resources/drawable-xxhdpi/icon.png differ
diff --git a/src/Android/Resources/drawable-xxxhdpi/icon.png b/src/Android/Resources/drawable-xxxhdpi/icon.png
new file mode 100644
index 000000000..6fa399d2a
Binary files /dev/null and b/src/Android/Resources/drawable-xxxhdpi/icon.png differ
diff --git a/src/Android/Resources/drawable/icon.png b/src/Android/Resources/drawable/icon.png
new file mode 100644
index 000000000..b2634a56c
Binary files /dev/null and b/src/Android/Resources/drawable/icon.png differ
diff --git a/src/App/App.csproj b/src/App/App.csproj
index f53176aa9..02479f122 100644
--- a/src/App/App.csproj
+++ b/src/App/App.csproj
@@ -78,7 +78,7 @@
AppResources.Designer.cs
- ResXFileCodeGenerator
+ PublicResXFileCodeGenerator
MSBuild:UpdateDesignTimeXaml