From 464f4ba300f9ea3b1a0b796857db46d8a79335af Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Tue, 30 Apr 2019 12:35:58 -0400 Subject: [PATCH] autofill service --- src/Android/Android.csproj | 55 +++ src/Android/Autofill/AutofillHelpers.cs | 203 +++++++++++ src/Android/Autofill/AutofillService.cs | 113 ++++++ src/Android/Autofill/Field.cs | 195 ++++++++++ src/Android/Autofill/FieldCollection.cs | 342 ++++++++++++++++++ src/Android/Autofill/FilledItem.cs | 224 ++++++++++++ src/Android/Autofill/Parser.cs | 130 +++++++ src/Android/Autofill/SavedItem.cs | 26 ++ src/Android/Resources/Resource.designer.cs | 123 ++++--- src/Android/Resources/drawable-hdpi/icon.png | Bin 0 -> 1301 bytes src/Android/Resources/drawable-xhdpi/icon.png | Bin 0 -> 1721 bytes .../Resources/drawable-xxhdpi/icon.png | Bin 0 -> 2748 bytes .../Resources/drawable-xxxhdpi/icon.png | Bin 0 -> 3679 bytes src/Android/Resources/drawable/icon.png | Bin 0 -> 941 bytes src/App/App.csproj | 2 +- 15 files changed, 1352 insertions(+), 61 deletions(-) create mode 100644 src/Android/Autofill/AutofillHelpers.cs create mode 100644 src/Android/Autofill/AutofillService.cs create mode 100644 src/Android/Autofill/Field.cs create mode 100644 src/Android/Autofill/FieldCollection.cs create mode 100644 src/Android/Autofill/FilledItem.cs create mode 100644 src/Android/Autofill/Parser.cs create mode 100644 src/Android/Autofill/SavedItem.cs create mode 100644 src/Android/Resources/drawable-hdpi/icon.png create mode 100644 src/Android/Resources/drawable-xhdpi/icon.png create mode 100644 src/Android/Resources/drawable-xxhdpi/icon.png create mode 100644 src/Android/Resources/drawable-xxxhdpi/icon.png create mode 100644 src/Android/Resources/drawable/icon.png 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 0000000000000000000000000000000000000000..8b4107e4ad21111ab69e9776fbe7e44459eff66a GIT binary patch literal 1301 zcmb7^`9ISS9LGOtuJ%RFoS7vel*;k&AU;eQ@x>@jVjDKsP}7*SwvcjsYdO9-<~}AV zGxUg(4y3-M^Q-14M{R62a`hd)^ZgILKfJEz5APq}*L;0Ely>Ou002MHg~JK5_z=~hfQe)>EBC_mfMw!Zj=a4FOwxd)L@@sT7l=3E4j zfQcfKtcVdL*b%FPR)?&`u&4s*AqElw1|**ql#cwa4(j*mjbB`jx3*?=3@0OU#XSw) zIq@b7Q>x|prZ&2wqd>KZF0SjKpJBegpPvXK`vQ93T*jSq4%M9aIHHkLHaaex)5sXK zTQklT%n2|4S#~>w+=nmFS<^(}XNu$^X9~_Pd@ryS4OuIf7(f4(^et(!e0#&42HP_3 z8QT}-p~aI!(DvF`(p&n&UyEMne94s{ljm{eT{6v=FUP(brl6djH)V0Tv}+K-?OAM9I3YP*@CapkH`^d#a&f*}K*`kS z&8c@X?Rk8|@K)viS)44lLWOZDs&4`k?SwnLDGG^7n>$EX$brf1$(ra^4_G#O>N<}u z;OS|r1$z$F?Buvqs^OwGKJ0JL-@PXb2T|1qA;)&oR1snSuO1;w@w{JC&$2(UR?E&y7!(KP7t@G=A_=JtB!X;XSyPNr|U1Y1g}eZ-%=5F zKQCoe;W7}+3D*jQ{-KQv%F};Lcp;W^OsVzz4SI#8s78ahTvny)* z=JDYV$om!Qwa{6CqWi1`RI`2rI1yXBG59$b_S@`ITu4=o{D`3I)AgnFGziZEqKdA`68x$y!=h zr=0RDZ0ZeLM1K&)F|*u;C`KVUTMK;9%Zhz}xU*fIGaVf$VbBKd)a@%F_oswLUeyA_ zjONw(Xou%UjUFZhCUegZc(NI0Ua<;*~FFDlgQwRK=P2c zIJ8@QfW*wCiZGkZpFv#=RS+<+t6L?a)<1;Ic5blE#yno;f$}=)`LLoPcS;=d(kUgM zM`?S7IwVe@|wr18i=cvuMV^ZOxktVbXDx09#d<(75CB@7O@tyJAiR%uvCRon{ zOMDqO3b^L+am0BaNungQ`8sAN)b1b1lNT3J0z;vn6os=I3rHh$K=3K5gpD0hOe=y8 zOtd9z^jKJgc|Wo6#{Bi;twz>qu5p=jb_pypr8Hc^dNm2dF0nqpH^dvYy;})taZ literal 0 HcmV?d00001 diff --git a/src/Android/Resources/drawable-xhdpi/icon.png b/src/Android/Resources/drawable-xhdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fcd1018f5ec1cc552a5b5922ee2cd83aad36774b GIT binary patch literal 1721 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 literal 0 HcmV?d00001 diff --git a/src/Android/Resources/drawable-xxxhdpi/icon.png b/src/Android/Resources/drawable-xxxhdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6fa399d2a97a5e18dbad5c6607c91e933a0e0426 GIT binary patch literal 3679 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 P00000NkvXXu0mjfMq#48 literal 0 HcmV?d00001 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