Accessibility service WIP

This commit is contained in:
Kyle Spearrin 2017-01-30 19:26:39 -05:00
parent 0beb07c87e
commit 36c6c5a35e
12 changed files with 310 additions and 150 deletions

View File

@ -19,31 +19,49 @@ namespace Bit.Android
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
_lastQueriedUri = Intent.GetStringExtra("uri");
LaunchMainActivity(Intent, 932473);
}
var intent = new Intent(this, typeof(MainActivity));
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop);
intent.PutExtra("uri", _lastQueriedUri);
StartActivityForResult(intent, 123);
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
LaunchMainActivity(intent, 489729);
}
protected override void OnDestroy()
{
base.OnDestroy();
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
if(data == null)
{
LastCredentials = null;
return;
}
try
{
var uri = data.GetStringExtra("uri");
var username = data.GetStringExtra("username");
var password = data.GetStringExtra("password");
LastCredentials = new AutofillCredentials
if(data.GetStringExtra("canceled") != null)
{
Username = username,
Password = password,
Uri = uri,
LastUri = _lastQueriedUri
};
LastCredentials = null;
}
else
{
var uri = data.GetStringExtra("uri");
var username = data.GetStringExtra("username");
var password = data.GetStringExtra("password");
LastCredentials = new AutofillCredentials
{
Username = username,
Password = password,
Uri = uri,
LastUri = _lastQueriedUri
};
}
}
catch
{
@ -54,5 +72,18 @@ namespace Bit.Android
Finish();
}
}
private void LaunchMainActivity(Intent callingIntent, int requestCode)
{
_lastQueriedUri = callingIntent?.GetStringExtra("uri");
if(_lastQueriedUri == null)
{
return;
}
var intent = new Intent(this, typeof(MainActivity));
intent.PutExtra("uri", _lastQueriedUri);
StartActivityForResult(intent, requestCode);
}
}
}

View File

@ -15,17 +15,17 @@ namespace Bit.Android
public class AutofillService : AccessibilityService
{
private const int AutoFillNotificationId = 34573;
private const string AndroidAppProtocol = "androidapp://";
private const string SystemUiPackage = "com.android.systemui";
private const string ChromePackage = "com.android.chrome";
private const string BrowserPackage = "com.android.browser";
private const string BitwardenPackage = "com.x8bit.bitwarden";
public override void OnAccessibilityEvent(AccessibilityEvent e)
{
var eventType = e.EventType;
var packageName = e.PackageName;
if(packageName == SystemUiPackage)
if(packageName == SystemUiPackage || packageName == BitwardenPackage)
{
return;
}
@ -34,15 +34,14 @@ namespace Bit.Android
{
case EventTypes.WindowContentChanged:
case EventTypes.WindowStateChanged:
var cancelNotification = true;
var root = RootInActiveWindow;
var isChrome = root == null ? false : root.PackageName == ChromePackage;
var cancelNotification = true;
var avialablePasswordNodes = GetNodeOrChildren(root, n => AvailablePasswordField(n, isChrome));
var avialablePasswordNodes = GetWindowNodes(root, e, n => AvailablePasswordField(n, isChrome));
if(avialablePasswordNodes.Any() && AnyNodeOrChildren(root, n => n.WindowId == e.WindowId &&
!(n.ViewIdResourceName != null && n.ViewIdResourceName.StartsWith(SystemUiPackage))))
if(avialablePasswordNodes.Any())
{
var uri = string.Concat(AndroidAppProtocol, root.PackageName);
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName);
if(isChrome)
{
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
@ -57,23 +56,25 @@ namespace Bit.Android
uri = ExtractUriFromAddressField(uri, addressNode);
}
var allEditTexts = GetNodeOrChildren(root, n => EditText(n));
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
if(AutofillActivity.LastCredentials != null && SameUri(AutofillActivity.LastCredentials.LastUri, uri))
if(NeedToAutofill(AutofillActivity.LastCredentials, uri))
{
var allEditTexts = GetWindowNodes(root, e, n => EditText(n));
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
FillCredentials(usernameEditText, avialablePasswordNodes);
}
else
{
AskFillPassword(uri, usernameEditText, avialablePasswordNodes);
NotifyToAutofill(uri);
cancelNotification = false;
}
AutofillActivity.LastCredentials = null;
}
if(cancelNotification)
{
((NotificationManager)GetSystemService(NotificationService)).Cancel(AutoFillNotificationId);
var notificationManager = ((NotificationManager)GetSystemService(NotificationService));
notificationManager.Cancel(AutoFillNotificationId);
}
break;
default:
@ -100,11 +101,21 @@ namespace Bit.Android
return uri;
}
private bool SameUri(string uriString1, string uriString2)
/// <summary>
/// Check to make sure it is ok to autofill still on the current screen
/// </summary>
private bool NeedToAutofill(AutofillCredentials creds, string currentUriString)
{
Uri uri1, uri2;
if(Uri.TryCreate(uriString1, UriKind.RelativeOrAbsolute, out uri1) &&
Uri.TryCreate(uriString2, UriKind.RelativeOrAbsolute, out uri2) && uri1.Host == uri2.Host)
if(creds == null)
{
return false;
}
Uri credsUri, lastUri, currentUri;
if(Uri.TryCreate(creds.Uri, UriKind.Absolute, out credsUri) &&
Uri.TryCreate(creds.LastUri, UriKind.Absolute, out lastUri) &&
Uri.TryCreate(currentUriString, UriKind.Absolute, out currentUri) &&
credsUri.Host == currentUri.Host && lastUri.Host == currentUri.Host)
{
return true;
}
@ -124,8 +135,7 @@ namespace Bit.Android
return n.ClassName != null && n.ClassName.Contains("EditText");
}
private void AskFillPassword(string uri, AccessibilityNodeInfo usernameNode,
IEnumerable<AccessibilityNodeInfo> passwordNodes)
private void NotifyToAutofill(string uri)
{
var intent = new Intent(this, typeof(AutofillActivity));
intent.PutExtra("uri", uri);
@ -134,10 +144,10 @@ namespace Bit.Android
var builder = new Notification.Builder(this);
builder.SetSmallIcon(Resource.Drawable.notification_sm)
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
.SetContentTitle("bitwarden Autofill Service")
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
.SetTicker("Tap this notification to autofill a login from your bitwarden vault.")
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetVisibility(NotificationVisibility.Secret)
.SetContentIntent(pendingIntent);
@ -148,39 +158,37 @@ namespace Bit.Android
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
{
FillEditText(usernameNode, AutofillActivity.LastCredentials.Username);
foreach(var pNode in passwordNodes)
foreach(var n in passwordNodes)
{
FillEditText(pNode, AutofillActivity.LastCredentials.Password);
FillEditText(n, AutofillActivity.LastCredentials.Password);
}
AutofillActivity.LastCredentials = null;
}
private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
{
if(editTextNode == null || value == null)
{
return;
}
var bundle = new Bundle();
bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value);
editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle);
}
private bool AnyNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
{
return GetNodeOrChildren(n, p).Any();
}
private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n,
Func<AccessibilityNodeInfo, bool> p)
private IEnumerable<AccessibilityNodeInfo> GetWindowNodes(AccessibilityNodeInfo n,
AccessibilityEvent e, Func<AccessibilityNodeInfo, bool> p)
{
if(n != null)
{
if(p(n))
if(n.WindowId == e.WindowId && !(n.ViewIdResourceName?.StartsWith(SystemUiPackage) ?? false) && p(n))
{
yield return n;
}
for(int i = 0; i < n.ChildCount; i++)
{
foreach(var node in GetNodeOrChildren(n.GetChild(i), p))
foreach(var node in GetWindowNodes(n.GetChild(i), e, p))
{
yield return node;
}

View File

@ -89,9 +89,16 @@ namespace Bit.Android
private void ReturnCredentials(VaultListPageModel.Login login)
{
Intent data = new Intent();
data.PutExtra("uri", login.Uri.Value);
data.PutExtra("username", login.Username);
data.PutExtra("password", login.Password.Value);
if(login == null)
{
data.PutExtra("canceled", "true");
}
else
{
data.PutExtra("uri", login.Uri.Value);
data.PutExtra("username", login.Username);
data.PutExtra("password", login.Password.Value);
}
if(Parent == null)
{

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackSpoken"
android:accessibilityFlags="flagDefault"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"
android:canRequestEnhancedWebAccessibility="true"/>
android:canRetrieveWindowContent="true"/>

View File

@ -72,6 +72,7 @@
<Compile Include="Controls\FormPickerCell.cs" />
<Compile Include="Controls\FormEntryCell.cs" />
<Compile Include="Controls\PinControl.cs" />
<Compile Include="Controls\VaultListViewCell.cs" />
<Compile Include="Enums\LockType.cs" />
<Compile Include="Enums\CipherType.cs" />
<Compile Include="Enums\PushType.cs" />

View File

@ -2,6 +2,8 @@
{
public static class Constants
{
public const string AndroidAppProtocol = "androidapp://";
public const string SettingFingerprintUnlockOn = "setting:fingerprintUnlockOn";
public const string SettingPinUnlockOn = "setting:pinUnlockOn";
public const string SettingLockSeconds = "setting:lockSeconds";

View File

@ -0,0 +1,40 @@
using Bit.App.Models.Page;
using System;
using Xamarin.Forms;
namespace Bit.App.Controls
{
public class VaultListViewCell : LabeledDetailCell
{
private Action<VaultListPageModel.Login> _moreClickedAction;
public static readonly BindableProperty LoginParameterProperty = BindableProperty.Create(nameof(LoginParameter),
typeof(VaultListPageModel.Login), typeof(VaultListViewCell), null);
public VaultListViewCell(Action<VaultListPageModel.Login> moreClickedAction)
{
_moreClickedAction = moreClickedAction;
SetBinding(LoginParameterProperty, new Binding("."));
Label.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Name);
Detail.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Username);
Button.Image = "more";
Button.Command = new Command(() => ShowMore());
Button.BackgroundColor = Color.Transparent;
BackgroundColor = Color.White;
}
public VaultListPageModel.Login LoginParameter
{
get { return GetValue(LoginParameterProperty) as VaultListPageModel.Login; }
set { SetValue(LoginParameterProperty, value); }
}
private void ShowMore()
{
_moreClickedAction?.Invoke(LoginParameter);
}
}
}

View File

@ -23,9 +23,14 @@ namespace Bit.App.Pages
private readonly IConnectivity _connectivity;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly ISettings _settings;
private readonly string _defaultUri;
private readonly string _defaultName;
public VaultAddLoginPage()
public VaultAddLoginPage(string defaultUri = null, string defaultName = null)
{
_defaultUri = defaultUri;
_defaultName = defaultName;
_loginService = Resolver.Resolve<ILoginService>();
_folderService = Resolver.Resolve<IFolderService>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
@ -54,7 +59,16 @@ namespace Bit.App.Pages
usernameCell.Entry.Autocorrect = false;
var uriCell = new FormEntryCell(AppResources.URI, Keyboard.Url, nextElement: usernameCell.Entry);
if(!string.IsNullOrWhiteSpace(_defaultUri))
{
uriCell.Entry.Text = _defaultUri;
}
var nameCell = new FormEntryCell(AppResources.Name, nextElement: uriCell.Entry);
if(!string.IsNullOrWhiteSpace(_defaultName))
{
nameCell.Entry.Text = _defaultName;
}
var folderOptions = new List<string> { AppResources.FolderNone };
var folders = _folderService.GetAllAsync().GetAwaiter().GetResult()

View File

@ -12,49 +12,53 @@ using Bit.App.Utilities;
using PushNotification.Plugin.Abstractions;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Collections.Generic;
using System.Threading;
using Bit.App.Models;
using System.Collections.Generic;
namespace Bit.App.Pages
{
public class VaultAutofillListLoginsPage : ExtendedContentPage
{
private readonly IFolderService _folderService;
private readonly ILoginService _loginService;
private readonly IUserDialogs _userDialogs;
private readonly IConnectivity _connectivity;
private readonly IClipboardService _clipboardService;
private readonly ISyncService _syncService;
private readonly IPushNotification _pushNotification;
private readonly IDeviceInfoService _deviceInfoService;
private readonly ISettings _settings;
private CancellationTokenSource _filterResultsCancellationTokenSource;
private readonly DomainName _domainName;
private readonly string _uri;
private readonly string _name;
private readonly bool _androidApp = false;
public VaultAutofillListLoginsPage(string uriString)
: base(true)
{
_uri = uriString;
Uri uri;
if(Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out uri) &&
DomainName.TryParse(uri.Host, out _domainName)) { }
if(!Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out uri) ||
!DomainName.TryParse(uri.Host, out _domainName))
{
if(uriString != null && uriString.StartsWith(Constants.AndroidAppProtocol))
{
_androidApp = true;
_name = uriString.Substring(Constants.AndroidAppProtocol.Length);
}
}
else
{
_name = _domainName.BaseDomain;
}
_folderService = Resolver.Resolve<IFolderService>();
_loginService = Resolver.Resolve<ILoginService>();
_connectivity = Resolver.Resolve<IConnectivity>();
_userDialogs = Resolver.Resolve<IUserDialogs>();
_clipboardService = Resolver.Resolve<IClipboardService>();
_syncService = Resolver.Resolve<ISyncService>();
_pushNotification = Resolver.Resolve<IPushNotification>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_settings = Resolver.Resolve<ISettings>();
Init();
}
public ExtendedObservableCollection<VaultListPageModel.Login> PresentationLogins { get; private set; }
= new ExtendedObservableCollection<VaultListPageModel.Login>();
public StackLayout NoDataStackLayout { get; set; }
public ListView ListView { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
private void Init()
{
@ -66,13 +70,37 @@ namespace Bit.App.Pages
}
});
var noDataLabel = new Label
{
Text = string.Format(AppResources.NoLoginsForUri, _name ?? "--"),
HorizontalTextAlignment = TextAlignment.Center,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"]
};
var addLoginButton = new ExtendedButton
{
Text = AppResources.AddALogin,
Command = new Command(() => AddLoginAsync()),
Style = (Style)Application.Current.Resources["btn-primaryAccent"]
};
NoDataStackLayout = new StackLayout
{
Children = { noDataLabel, addLoginButton },
VerticalOptions = LayoutOptions.CenterAndExpand,
Padding = new Thickness(20, 0),
Spacing = 20
};
ToolbarItems.Add(new AddLoginToolBarItem(this));
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{
ItemsSource = PresentationLogins,
HasUnevenRows = true,
ItemTemplate = new DataTemplate(() => new VaultListViewCell(this))
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Login l) => MoreClickedAsync(l)))
};
if(Device.OS == TargetPlatform.iOS)
@ -82,9 +110,16 @@ namespace Bit.App.Pages
ListView.ItemSelected += LoginSelected;
Title = AppResources.Logins;
Title = string.Format(AppResources.LoginsForUri, _name ?? "--");
Content = ListView;
LoadingIndicator = new ActivityIndicator
{
IsRunning = true,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
Content = LoadingIndicator;
}
protected override void OnAppearing()
@ -93,16 +128,27 @@ namespace Bit.App.Pages
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
protected override bool OnBackButtonPressed()
{
MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Login)null);
return true;
}
private void AdjustContent()
{
if(PresentationLogins.Count > 0)
{
Content = ListView;
}
else
{
Content = NoDataStackLayout;
}
}
private CancellationTokenSource FetchAndLoadVault()
{
var cts = new CancellationTokenSource();
_settings.AddOrUpdateValue(Constants.FirstVaultLoad, false);
if(PresentationLogins.Count > 0 && _syncService.SyncInProgress)
{
return cts;
}
_filterResultsCancellationTokenSource?.Cancel();
Task.Run(async () =>
@ -110,12 +156,16 @@ namespace Bit.App.Pages
var logins = await _loginService.GetAllAsync();
var filteredLogins = logins
.Select(s => new VaultListPageModel.Login(s))
.Where(s => s.BaseDomain != null && s.BaseDomain == _domainName.BaseDomain)
.Where(s => (_androidApp && _domainName == null && s.Uri.Value == _uri) ||
(_domainName != null && s.BaseDomain != null && s.BaseDomain == _domainName.BaseDomain))
.OrderBy(s => s.Name)
.ThenBy(s => s.Username)
.ToArray();
.ThenBy(s => s.Username);
PresentationLogins.ResetWithRange(filteredLogins);
Device.BeginInvokeOnMainThread(() =>
{
PresentationLogins.ResetWithRange(filteredLogins);
AdjustContent();
});
}, cts.Token);
return cts;
@ -127,12 +177,52 @@ namespace Bit.App.Pages
MessagingCenter.Send(Application.Current, "Autofill", login);
}
private async void AddLogin()
private async void AddLoginAsync()
{
var page = new VaultAddLoginPage();
var page = new VaultAddLoginPage(_uri, _name);
await Navigation.PushForDeviceAsync(page);
}
private async void MoreClickedAsync(VaultListPageModel.Login login)
{
var buttons = new List<string> { AppResources.View, AppResources.Edit };
if(!string.IsNullOrWhiteSpace(login.Password.Value))
{
buttons.Add(AppResources.CopyPassword);
}
if(!string.IsNullOrWhiteSpace(login.Username))
{
buttons.Add(AppResources.CopyUsername);
}
var selection = await DisplayActionSheet(login.Name, AppResources.Cancel, null, buttons.ToArray());
if(selection == AppResources.View)
{
var page = new VaultViewLoginPage(login.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.Edit)
{
var page = new VaultEditLoginPage(login.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.CopyPassword)
{
Copy(login.Password.Value, AppResources.Password);
}
else if(selection == AppResources.CopyUsername)
{
Copy(login.Username, AppResources.Username);
}
}
private void Copy(string copyText, string alertLabel)
{
_clipboardService.CopyToClipboard(copyText);
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
}
private class AddLoginToolBarItem : ToolbarItem
{
private readonly VaultAutofillListLoginsPage _page;
@ -147,32 +237,7 @@ namespace Bit.App.Pages
private void ClickedItem(object sender, EventArgs e)
{
_page.AddLogin();
}
}
private class VaultListViewCell : LabeledDetailCell
{
private VaultAutofillListLoginsPage _page;
public static readonly BindableProperty LoginParameterProperty = BindableProperty.Create(nameof(LoginParameter),
typeof(VaultListPageModel.Login), typeof(VaultListViewCell), null);
public VaultListViewCell(VaultAutofillListLoginsPage page)
{
_page = page;
SetBinding(LoginParameterProperty, new Binding("."));
Label.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Name);
Detail.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Username);
BackgroundColor = Color.White;
}
public VaultListPageModel.Login LoginParameter
{
get { return GetValue(LoginParameterProperty) as VaultListPageModel.Login; }
set { SetValue(LoginParameterProperty, value); }
_page.AddLoginAsync();
}
}
}

View File

@ -83,7 +83,8 @@ namespace Bit.App.Pages
ItemsSource = PresentationFolders,
HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new VaultListHeaderViewCell(this)),
ItemTemplate = new DataTemplate(() => new VaultListViewCell(this))
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Login l) => MoreClickedAsync(l)))
};
if(Device.OS == TargetPlatform.iOS)
@ -439,40 +440,6 @@ namespace Bit.App.Pages
}
}
private class VaultListViewCell : LabeledDetailCell
{
private VaultListLoginsPage _page;
public static readonly BindableProperty LoginParameterProperty = BindableProperty.Create(nameof(LoginParameter),
typeof(VaultListPageModel.Login), typeof(VaultListViewCell), null);
public VaultListViewCell(VaultListLoginsPage page)
{
_page = page;
SetBinding(LoginParameterProperty, new Binding("."));
Label.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Name);
Detail.SetBinding<VaultListPageModel.Login>(Label.TextProperty, s => s.Username);
Button.Image = "more";
Button.Command = new Command(() => ShowMore());
Button.BackgroundColor = Color.Transparent;
BackgroundColor = Color.White;
}
public VaultListPageModel.Login LoginParameter
{
get { return GetValue(LoginParameterProperty) as VaultListPageModel.Login; }
set { SetValue(LoginParameterProperty, value); }
}
private void ShowMore()
{
_page.MoreClickedAsync(LoginParameter);
}
}
private class VaultListHeaderViewCell : ExtendedViewCell
{
public VaultListHeaderViewCell(VaultListLoginsPage page)

View File

@ -1015,6 +1015,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Logins for {0}.
/// </summary>
public static string LoginsForUri {
get {
return ResourceManager.GetString("LoginsForUri", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Login updated..
/// </summary>
@ -1222,6 +1231,15 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to There are no logins in your vault for {0}..
/// </summary>
public static string NoLoginsForUri {
get {
return ResourceManager.GetString("NoLoginsForUri", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to There are no logins in your vault for this website. Tap to add one..
/// </summary>

View File

@ -757,4 +757,12 @@
<data name="Translations" xml:space="preserve">
<value>Translations</value>
</data>
<data name="LoginsForUri" xml:space="preserve">
<value>Logins for {0}</value>
<comment>This is used for the autofill service. ex. "Logins for twitter.com"</comment>
</data>
<data name="NoLoginsForUri" xml:space="preserve">
<value>There are no logins in your vault for {0}.</value>
<comment>This is used for the autofill service. ex. "There are no logins in your vault for twitter.com".</comment>
</data>
</root>