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)
|
protected override void OnCreate(Bundle bundle)
|
||||||
{
|
{
|
||||||
base.OnCreate(bundle);
|
base.OnCreate(bundle);
|
||||||
_lastQueriedUri = Intent.GetStringExtra("uri");
|
LaunchMainActivity(Intent, 932473);
|
||||||
|
}
|
||||||
|
|
||||||
var intent = new Intent(this, typeof(MainActivity));
|
protected override void OnNewIntent(Intent intent)
|
||||||
intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop);
|
{
|
||||||
intent.PutExtra("uri", _lastQueriedUri);
|
base.OnNewIntent(intent);
|
||||||
StartActivityForResult(intent, 123);
|
LaunchMainActivity(intent, 489729);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
base.OnDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
|
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
|
||||||
{
|
{
|
||||||
base.OnActivityResult(requestCode, resultCode, data);
|
base.OnActivityResult(requestCode, resultCode, data);
|
||||||
|
if(data == null)
|
||||||
|
{
|
||||||
|
LastCredentials = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var uri = data.GetStringExtra("uri");
|
if(data.GetStringExtra("canceled") != null)
|
||||||
var username = data.GetStringExtra("username");
|
|
||||||
var password = data.GetStringExtra("password");
|
|
||||||
|
|
||||||
LastCredentials = new AutofillCredentials
|
|
||||||
{
|
{
|
||||||
Username = username,
|
LastCredentials = null;
|
||||||
Password = password,
|
}
|
||||||
Uri = uri,
|
else
|
||||||
LastUri = _lastQueriedUri
|
{
|
||||||
};
|
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
|
catch
|
||||||
{
|
{
|
||||||
|
@ -54,5 +72,18 @@ namespace Bit.Android
|
||||||
Finish();
|
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
|
public class AutofillService : AccessibilityService
|
||||||
{
|
{
|
||||||
private const int AutoFillNotificationId = 34573;
|
private const int AutoFillNotificationId = 34573;
|
||||||
private const string AndroidAppProtocol = "androidapp://";
|
|
||||||
private const string SystemUiPackage = "com.android.systemui";
|
private const string SystemUiPackage = "com.android.systemui";
|
||||||
private const string ChromePackage = "com.android.chrome";
|
private const string ChromePackage = "com.android.chrome";
|
||||||
private const string BrowserPackage = "com.android.browser";
|
private const string BrowserPackage = "com.android.browser";
|
||||||
|
private const string BitwardenPackage = "com.x8bit.bitwarden";
|
||||||
|
|
||||||
public override void OnAccessibilityEvent(AccessibilityEvent e)
|
public override void OnAccessibilityEvent(AccessibilityEvent e)
|
||||||
{
|
{
|
||||||
var eventType = e.EventType;
|
var eventType = e.EventType;
|
||||||
var packageName = e.PackageName;
|
var packageName = e.PackageName;
|
||||||
|
|
||||||
if(packageName == SystemUiPackage)
|
if(packageName == SystemUiPackage || packageName == BitwardenPackage)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -34,15 +34,14 @@ namespace Bit.Android
|
||||||
{
|
{
|
||||||
case EventTypes.WindowContentChanged:
|
case EventTypes.WindowContentChanged:
|
||||||
case EventTypes.WindowStateChanged:
|
case EventTypes.WindowStateChanged:
|
||||||
|
var cancelNotification = true;
|
||||||
var root = RootInActiveWindow;
|
var root = RootInActiveWindow;
|
||||||
var isChrome = root == null ? false : root.PackageName == ChromePackage;
|
var isChrome = root == null ? false : root.PackageName == ChromePackage;
|
||||||
var cancelNotification = true;
|
var avialablePasswordNodes = GetWindowNodes(root, e, n => AvailablePasswordField(n, isChrome));
|
||||||
var avialablePasswordNodes = GetNodeOrChildren(root, n => AvailablePasswordField(n, isChrome));
|
|
||||||
|
|
||||||
if(avialablePasswordNodes.Any() && AnyNodeOrChildren(root, n => n.WindowId == e.WindowId &&
|
if(avialablePasswordNodes.Any())
|
||||||
!(n.ViewIdResourceName != null && n.ViewIdResourceName.StartsWith(SystemUiPackage))))
|
|
||||||
{
|
{
|
||||||
var uri = string.Concat(AndroidAppProtocol, root.PackageName);
|
var uri = string.Concat(App.Constants.AndroidAppProtocol, root.PackageName);
|
||||||
if(isChrome)
|
if(isChrome)
|
||||||
{
|
{
|
||||||
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
|
var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
|
||||||
|
@ -57,23 +56,25 @@ namespace Bit.Android
|
||||||
uri = ExtractUriFromAddressField(uri, addressNode);
|
uri = ExtractUriFromAddressField(uri, addressNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var allEditTexts = GetNodeOrChildren(root, n => EditText(n));
|
if(NeedToAutofill(AutofillActivity.LastCredentials, uri))
|
||||||
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
|
|
||||||
|
|
||||||
if(AutofillActivity.LastCredentials != null && SameUri(AutofillActivity.LastCredentials.LastUri, uri))
|
|
||||||
{
|
{
|
||||||
|
var allEditTexts = GetWindowNodes(root, e, n => EditText(n));
|
||||||
|
var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault();
|
||||||
FillCredentials(usernameEditText, avialablePasswordNodes);
|
FillCredentials(usernameEditText, avialablePasswordNodes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AskFillPassword(uri, usernameEditText, avialablePasswordNodes);
|
NotifyToAutofill(uri);
|
||||||
cancelNotification = false;
|
cancelNotification = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AutofillActivity.LastCredentials = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(cancelNotification)
|
if(cancelNotification)
|
||||||
{
|
{
|
||||||
((NotificationManager)GetSystemService(NotificationService)).Cancel(AutoFillNotificationId);
|
var notificationManager = ((NotificationManager)GetSystemService(NotificationService));
|
||||||
|
notificationManager.Cancel(AutoFillNotificationId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -100,11 +101,21 @@ namespace Bit.Android
|
||||||
return uri;
|
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(creds == null)
|
||||||
if(Uri.TryCreate(uriString1, UriKind.RelativeOrAbsolute, out uri1) &&
|
{
|
||||||
Uri.TryCreate(uriString2, UriKind.RelativeOrAbsolute, out uri2) && uri1.Host == uri2.Host)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -124,8 +135,7 @@ namespace Bit.Android
|
||||||
return n.ClassName != null && n.ClassName.Contains("EditText");
|
return n.ClassName != null && n.ClassName.Contains("EditText");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AskFillPassword(string uri, AccessibilityNodeInfo usernameNode,
|
private void NotifyToAutofill(string uri)
|
||||||
IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
|
||||||
{
|
{
|
||||||
var intent = new Intent(this, typeof(AutofillActivity));
|
var intent = new Intent(this, typeof(AutofillActivity));
|
||||||
intent.PutExtra("uri", uri);
|
intent.PutExtra("uri", uri);
|
||||||
|
@ -134,10 +144,10 @@ namespace Bit.Android
|
||||||
|
|
||||||
var builder = new Notification.Builder(this);
|
var builder = new Notification.Builder(this);
|
||||||
builder.SetSmallIcon(Resource.Drawable.notification_sm)
|
builder.SetSmallIcon(Resource.Drawable.notification_sm)
|
||||||
.SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
|
|
||||||
.SetContentTitle("bitwarden Autofill Service")
|
.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.")
|
.SetTicker("Tap this notification to autofill a login from your bitwarden vault.")
|
||||||
|
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
|
||||||
.SetVisibility(NotificationVisibility.Secret)
|
.SetVisibility(NotificationVisibility.Secret)
|
||||||
.SetContentIntent(pendingIntent);
|
.SetContentIntent(pendingIntent);
|
||||||
|
|
||||||
|
@ -148,39 +158,37 @@ namespace Bit.Android
|
||||||
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
|
||||||
{
|
{
|
||||||
FillEditText(usernameNode, AutofillActivity.LastCredentials.Username);
|
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)
|
private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
|
||||||
{
|
{
|
||||||
|
if(editTextNode == null || value == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var bundle = new Bundle();
|
var bundle = new Bundle();
|
||||||
bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value);
|
bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value);
|
||||||
editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle);
|
editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AnyNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
|
private IEnumerable<AccessibilityNodeInfo> GetWindowNodes(AccessibilityNodeInfo n,
|
||||||
{
|
AccessibilityEvent e, Func<AccessibilityNodeInfo, bool> p)
|
||||||
return GetNodeOrChildren(n, p).Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n,
|
|
||||||
Func<AccessibilityNodeInfo, bool> p)
|
|
||||||
{
|
{
|
||||||
if(n != null)
|
if(n != null)
|
||||||
{
|
{
|
||||||
if(p(n))
|
if(n.WindowId == e.WindowId && !(n.ViewIdResourceName?.StartsWith(SystemUiPackage) ?? false) && p(n))
|
||||||
{
|
{
|
||||||
yield return n;
|
yield return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(int i = 0; i < n.ChildCount; i++)
|
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;
|
yield return node;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,9 +89,16 @@ namespace Bit.Android
|
||||||
private void ReturnCredentials(VaultListPageModel.Login login)
|
private void ReturnCredentials(VaultListPageModel.Login login)
|
||||||
{
|
{
|
||||||
Intent data = new Intent();
|
Intent data = new Intent();
|
||||||
data.PutExtra("uri", login.Uri.Value);
|
if(login == null)
|
||||||
data.PutExtra("username", login.Username);
|
{
|
||||||
data.PutExtra("password", login.Password.Value);
|
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)
|
if(Parent == null)
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:accessibilityEventTypes="typeAllMask"
|
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
|
||||||
android:accessibilityFeedbackType="feedbackSpoken"
|
android:accessibilityFeedbackType="feedbackSpoken"
|
||||||
android:accessibilityFlags="flagDefault"
|
android:accessibilityFlags="flagDefault"
|
||||||
android:notificationTimeout="100"
|
android:notificationTimeout="100"
|
||||||
android:canRetrieveWindowContent="true"
|
android:canRetrieveWindowContent="true"/>
|
||||||
android:canRequestEnhancedWebAccessibility="true"/>
|
|
|
@ -72,6 +72,7 @@
|
||||||
<Compile Include="Controls\FormPickerCell.cs" />
|
<Compile Include="Controls\FormPickerCell.cs" />
|
||||||
<Compile Include="Controls\FormEntryCell.cs" />
|
<Compile Include="Controls\FormEntryCell.cs" />
|
||||||
<Compile Include="Controls\PinControl.cs" />
|
<Compile Include="Controls\PinControl.cs" />
|
||||||
|
<Compile Include="Controls\VaultListViewCell.cs" />
|
||||||
<Compile Include="Enums\LockType.cs" />
|
<Compile Include="Enums\LockType.cs" />
|
||||||
<Compile Include="Enums\CipherType.cs" />
|
<Compile Include="Enums\CipherType.cs" />
|
||||||
<Compile Include="Enums\PushType.cs" />
|
<Compile Include="Enums\PushType.cs" />
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
{
|
{
|
||||||
public static class Constants
|
public static class Constants
|
||||||
{
|
{
|
||||||
|
public const string AndroidAppProtocol = "androidapp://";
|
||||||
|
|
||||||
public const string SettingFingerprintUnlockOn = "setting:fingerprintUnlockOn";
|
public const string SettingFingerprintUnlockOn = "setting:fingerprintUnlockOn";
|
||||||
public const string SettingPinUnlockOn = "setting:pinUnlockOn";
|
public const string SettingPinUnlockOn = "setting:pinUnlockOn";
|
||||||
public const string SettingLockSeconds = "setting:lockSeconds";
|
public const string SettingLockSeconds = "setting:lockSeconds";
|
||||||
|
|
|
@ -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 IConnectivity _connectivity;
|
||||||
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
private readonly IGoogleAnalyticsService _googleAnalyticsService;
|
||||||
private readonly ISettings _settings;
|
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>();
|
_loginService = Resolver.Resolve<ILoginService>();
|
||||||
_folderService = Resolver.Resolve<IFolderService>();
|
_folderService = Resolver.Resolve<IFolderService>();
|
||||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||||
|
@ -54,7 +59,16 @@ namespace Bit.App.Pages
|
||||||
usernameCell.Entry.Autocorrect = false;
|
usernameCell.Entry.Autocorrect = false;
|
||||||
|
|
||||||
var uriCell = new FormEntryCell(AppResources.URI, Keyboard.Url, nextElement: usernameCell.Entry);
|
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);
|
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 folderOptions = new List<string> { AppResources.FolderNone };
|
||||||
var folders = _folderService.GetAllAsync().GetAwaiter().GetResult()
|
var folders = _folderService.GetAllAsync().GetAwaiter().GetResult()
|
||||||
|
|
|
@ -12,49 +12,53 @@ using Bit.App.Utilities;
|
||||||
using PushNotification.Plugin.Abstractions;
|
using PushNotification.Plugin.Abstractions;
|
||||||
using Plugin.Settings.Abstractions;
|
using Plugin.Settings.Abstractions;
|
||||||
using Plugin.Connectivity.Abstractions;
|
using Plugin.Connectivity.Abstractions;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Bit.App.Pages
|
namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public class VaultAutofillListLoginsPage : ExtendedContentPage
|
public class VaultAutofillListLoginsPage : ExtendedContentPage
|
||||||
{
|
{
|
||||||
private readonly IFolderService _folderService;
|
|
||||||
private readonly ILoginService _loginService;
|
private readonly ILoginService _loginService;
|
||||||
private readonly IUserDialogs _userDialogs;
|
private readonly IUserDialogs _userDialogs;
|
||||||
private readonly IConnectivity _connectivity;
|
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly ISyncService _syncService;
|
|
||||||
private readonly IPushNotification _pushNotification;
|
|
||||||
private readonly IDeviceInfoService _deviceInfoService;
|
|
||||||
private readonly ISettings _settings;
|
|
||||||
private CancellationTokenSource _filterResultsCancellationTokenSource;
|
private CancellationTokenSource _filterResultsCancellationTokenSource;
|
||||||
private readonly DomainName _domainName;
|
private readonly DomainName _domainName;
|
||||||
|
private readonly string _uri;
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly bool _androidApp = false;
|
||||||
|
|
||||||
public VaultAutofillListLoginsPage(string uriString)
|
public VaultAutofillListLoginsPage(string uriString)
|
||||||
: base(true)
|
: base(true)
|
||||||
{
|
{
|
||||||
|
_uri = uriString;
|
||||||
Uri uri;
|
Uri uri;
|
||||||
if(Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out uri) &&
|
if(!Uri.TryCreate(uriString, UriKind.RelativeOrAbsolute, out uri) ||
|
||||||
DomainName.TryParse(uri.Host, out _domainName)) { }
|
!DomainName.TryParse(uri.Host, out _domainName))
|
||||||
|
{
|
||||||
_folderService = Resolver.Resolve<IFolderService>();
|
if(uriString != null && uriString.StartsWith(Constants.AndroidAppProtocol))
|
||||||
|
{
|
||||||
|
_androidApp = true;
|
||||||
|
_name = uriString.Substring(Constants.AndroidAppProtocol.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_name = _domainName.BaseDomain;
|
||||||
|
}
|
||||||
|
|
||||||
_loginService = Resolver.Resolve<ILoginService>();
|
_loginService = Resolver.Resolve<ILoginService>();
|
||||||
_connectivity = Resolver.Resolve<IConnectivity>();
|
|
||||||
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
_userDialogs = Resolver.Resolve<IUserDialogs>();
|
||||||
_clipboardService = Resolver.Resolve<IClipboardService>();
|
_clipboardService = Resolver.Resolve<IClipboardService>();
|
||||||
_syncService = Resolver.Resolve<ISyncService>();
|
|
||||||
_pushNotification = Resolver.Resolve<IPushNotification>();
|
|
||||||
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
|
|
||||||
_settings = Resolver.Resolve<ISettings>();
|
|
||||||
|
|
||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
public ExtendedObservableCollection<VaultListPageModel.Login> PresentationLogins { get; private set; }
|
public ExtendedObservableCollection<VaultListPageModel.Login> PresentationLogins { get; private set; }
|
||||||
= new ExtendedObservableCollection<VaultListPageModel.Login>();
|
= new ExtendedObservableCollection<VaultListPageModel.Login>();
|
||||||
|
public StackLayout NoDataStackLayout { get; set; }
|
||||||
public ListView ListView { get; set; }
|
public ListView ListView { get; set; }
|
||||||
|
public ActivityIndicator LoadingIndicator { get; set; }
|
||||||
|
|
||||||
private void Init()
|
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));
|
ToolbarItems.Add(new AddLoginToolBarItem(this));
|
||||||
|
|
||||||
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
|
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
|
||||||
{
|
{
|
||||||
ItemsSource = PresentationLogins,
|
ItemsSource = PresentationLogins,
|
||||||
HasUnevenRows = true,
|
HasUnevenRows = true,
|
||||||
ItemTemplate = new DataTemplate(() => new VaultListViewCell(this))
|
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
|
||||||
|
(VaultListPageModel.Login l) => MoreClickedAsync(l)))
|
||||||
};
|
};
|
||||||
|
|
||||||
if(Device.OS == TargetPlatform.iOS)
|
if(Device.OS == TargetPlatform.iOS)
|
||||||
|
@ -82,9 +110,16 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
ListView.ItemSelected += LoginSelected;
|
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()
|
protected override void OnAppearing()
|
||||||
|
@ -93,16 +128,27 @@ namespace Bit.App.Pages
|
||||||
_filterResultsCancellationTokenSource = FetchAndLoadVault();
|
_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()
|
private CancellationTokenSource FetchAndLoadVault()
|
||||||
{
|
{
|
||||||
var cts = new CancellationTokenSource();
|
var cts = new CancellationTokenSource();
|
||||||
_settings.AddOrUpdateValue(Constants.FirstVaultLoad, false);
|
|
||||||
|
|
||||||
if(PresentationLogins.Count > 0 && _syncService.SyncInProgress)
|
|
||||||
{
|
|
||||||
return cts;
|
|
||||||
}
|
|
||||||
|
|
||||||
_filterResultsCancellationTokenSource?.Cancel();
|
_filterResultsCancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
|
@ -110,12 +156,16 @@ namespace Bit.App.Pages
|
||||||
var logins = await _loginService.GetAllAsync();
|
var logins = await _loginService.GetAllAsync();
|
||||||
var filteredLogins = logins
|
var filteredLogins = logins
|
||||||
.Select(s => new VaultListPageModel.Login(s))
|
.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)
|
.OrderBy(s => s.Name)
|
||||||
.ThenBy(s => s.Username)
|
.ThenBy(s => s.Username);
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
PresentationLogins.ResetWithRange(filteredLogins);
|
Device.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
PresentationLogins.ResetWithRange(filteredLogins);
|
||||||
|
AdjustContent();
|
||||||
|
});
|
||||||
}, cts.Token);
|
}, cts.Token);
|
||||||
|
|
||||||
return cts;
|
return cts;
|
||||||
|
@ -127,12 +177,52 @@ namespace Bit.App.Pages
|
||||||
MessagingCenter.Send(Application.Current, "Autofill", login);
|
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);
|
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 class AddLoginToolBarItem : ToolbarItem
|
||||||
{
|
{
|
||||||
private readonly VaultAutofillListLoginsPage _page;
|
private readonly VaultAutofillListLoginsPage _page;
|
||||||
|
@ -147,32 +237,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
private void ClickedItem(object sender, EventArgs e)
|
private void ClickedItem(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_page.AddLogin();
|
_page.AddLoginAsync();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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); }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,8 @@ namespace Bit.App.Pages
|
||||||
ItemsSource = PresentationFolders,
|
ItemsSource = PresentationFolders,
|
||||||
HasUnevenRows = true,
|
HasUnevenRows = true,
|
||||||
GroupHeaderTemplate = new DataTemplate(() => new VaultListHeaderViewCell(this)),
|
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)
|
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
|
private class VaultListHeaderViewCell : ExtendedViewCell
|
||||||
{
|
{
|
||||||
public VaultListHeaderViewCell(VaultListLoginsPage page)
|
public VaultListHeaderViewCell(VaultListLoginsPage page)
|
||||||
|
|
|
@ -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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to Login updated..
|
/// Looks up a localized string similar to Login updated..
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Looks up a localized string similar to There are no logins in your vault for this website. Tap to add one..
|
/// Looks up a localized string similar to There are no logins in your vault for this website. Tap to add one..
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -757,4 +757,12 @@
|
||||||
<data name="Translations" xml:space="preserve">
|
<data name="Translations" xml:space="preserve">
|
||||||
<value>Translations</value>
|
<value>Translations</value>
|
||||||
</data>
|
</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>
|
</root>
|
Loading…
Reference in New Issue