mirror of
https://github.com/bitwarden/mobile
synced 2025-01-28 17:29:18 +01:00
Accessibility service WIP
This commit is contained in:
parent
0beb07c87e
commit
36c6c5a35e
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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"/>
|
@ -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" />
|
||||
|
@ -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";
|
||||
|
40
src/App/Controls/VaultListViewCell.cs
Normal file
40
src/App/Controls/VaultListViewCell.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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)) { }
|
||||
|
||||
_folderService = Resolver.Resolve<IFolderService>();
|
||||
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;
|
||||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
18
src/App/Resources/AppResources.Designer.cs
generated
18
src/App/Resources/AppResources.Designer.cs
generated
@ -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>
|
||||
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user