bitwarden-android/src/App/Pages/Vault/VaultListLoginsPage.cs

526 lines
19 KiB
C#
Raw Normal View History

2016-05-02 09:52:09 +03:00
using System;
using System.Linq;
2016-05-04 02:49:49 +03:00
using System.Threading.Tasks;
using Acr.UserDialogs;
2016-05-02 09:52:09 +03:00
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Models.Page;
using Bit.App.Resources;
2016-05-02 09:52:09 +03:00
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Utilities;
2016-06-28 07:55:53 +03:00
using PushNotification.Plugin.Abstractions;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Collections.Generic;
using System.Threading;
2016-05-02 09:52:09 +03:00
2016-05-03 00:50:16 +03:00
namespace Bit.App.Pages
2016-05-02 09:52:09 +03:00
{
2017-01-03 08:17:15 +03:00
public class VaultListLoginsPage : ExtendedContentPage
2016-05-02 09:52:09 +03:00
{
private readonly IFolderService _folderService;
2017-01-03 08:17:15 +03:00
private readonly ILoginService _loginService;
2016-05-04 02:49:49 +03:00
private readonly IUserDialogs _userDialogs;
private readonly IConnectivity _connectivity;
private readonly IClipboardService _clipboardService;
private readonly ISyncService _syncService;
2016-06-28 07:55:53 +03:00
private readonly IPushNotification _pushNotification;
2016-12-19 17:41:47 +03:00
private readonly IDeviceInfoService _deviceInfoService;
2016-06-28 07:55:53 +03:00
private readonly ISettings _settings;
2017-02-14 06:10:34 +03:00
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly bool _favorites;
private bool _loadExistingData;
private CancellationTokenSource _filterResultsCancellationTokenSource;
2016-05-02 09:52:09 +03:00
public VaultListLoginsPage(bool favorites, string uri = null)
: base(true)
2016-05-02 09:52:09 +03:00
{
_favorites = favorites;
2016-05-02 09:52:09 +03:00
_folderService = Resolver.Resolve<IFolderService>();
2017-01-03 08:17:15 +03:00
_loginService = Resolver.Resolve<ILoginService>();
_connectivity = Resolver.Resolve<IConnectivity>();
2016-05-04 02:49:49 +03:00
_userDialogs = Resolver.Resolve<IUserDialogs>();
_clipboardService = Resolver.Resolve<IClipboardService>();
_syncService = Resolver.Resolve<ISyncService>();
2016-06-28 07:55:53 +03:00
_pushNotification = Resolver.Resolve<IPushNotification>();
2016-12-19 17:41:47 +03:00
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
2016-06-28 07:55:53 +03:00
_settings = Resolver.Resolve<ISettings>();
2017-02-14 06:10:34 +03:00
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
2016-05-02 09:52:09 +03:00
var cryptoService = Resolver.Resolve<ICryptoService>();
_loadExistingData = !_settings.GetValueOrDefault(Constants.FirstVaultLoad, true) || !cryptoService.KeyChanged;
Uri = uri;
2016-05-04 02:49:49 +03:00
Init();
}
2016-07-11 08:12:31 +03:00
public ExtendedObservableCollection<VaultListPageModel.Folder> PresentationFolders { get; private set; }
= new ExtendedObservableCollection<VaultListPageModel.Folder>();
public ListView ListView { get; set; }
2017-01-03 08:17:15 +03:00
public VaultListPageModel.Login[] Logins { get; set; } = new VaultListPageModel.Login[] { };
public VaultListPageModel.Folder[] Folders { get; set; } = new VaultListPageModel.Folder[] { };
public SearchBar Search { get; set; }
public StackLayout NoDataStackLayout { get; set; }
public StackLayout ResultsStackLayout { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
public string Uri { get; set; }
2016-05-02 09:52:09 +03:00
2016-05-04 02:49:49 +03:00
private void Init()
{
2016-07-15 08:52:33 +03:00
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted", (sender, success) =>
{
if(success)
{
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
});
if(!_favorites)
{
2017-01-03 08:17:15 +03:00
ToolbarItems.Add(new AddLoginToolBarItem(this));
}
2016-05-02 09:52:09 +03:00
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{
IsGroupingEnabled = true,
2016-07-11 08:12:31 +03:00
ItemsSource = PresentationFolders,
HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new VaultListHeaderViewCell(this)),
2017-01-31 03:26:39 +03:00
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(VaultListPageModel.Login l) => MoreClickedAsync(l)))
};
2016-06-29 07:34:20 +03:00
2016-06-18 01:14:24 +03:00
if(Device.OS == TargetPlatform.iOS)
{
2016-07-11 08:12:31 +03:00
ListView.RowHeight = -1;
2016-06-18 01:14:24 +03:00
}
2016-06-29 07:34:20 +03:00
2017-01-03 08:17:15 +03:00
ListView.ItemSelected += LoginSelected;
2016-07-11 08:12:31 +03:00
Search = new SearchBar
2016-07-11 08:12:31 +03:00
{
2016-11-26 00:42:52 +03:00
Placeholder = AppResources.SearchVault,
2016-07-19 02:16:27 +03:00
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Button)),
CancelButtonColor = Color.FromHex("3c8dbc")
2016-07-11 08:12:31 +03:00
};
Search.TextChanged += SearchBar_TextChanged;
Search.SearchButtonPressed += SearchBar_SearchButtonPressed;
2016-12-19 17:41:47 +03:00
// Bug with searchbar on android 7, ref https://bugzilla.xamarin.com/show_bug.cgi?id=43975
if(Device.OS == TargetPlatform.Android && _deviceInfoService.Version >= 24)
{
Search.HeightRequest = 50;
}
2016-05-02 09:52:09 +03:00
Title = _favorites ? AppResources.Favorites : AppResources.MyVault;
ResultsStackLayout = new StackLayout
2016-07-11 08:12:31 +03:00
{
2017-02-16 03:55:52 +03:00
Children = { Search, ListView },
2016-07-11 08:12:31 +03:00
Spacing = 0
};
var noDataLabel = new Label
{
2017-01-03 08:17:15 +03:00
Text = _favorites ? AppResources.NoFavorites : AppResources.NoLogins,
HorizontalTextAlignment = TextAlignment.Center,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"]
};
NoDataStackLayout = new StackLayout
{
Children = { noDataLabel },
VerticalOptions = LayoutOptions.CenterAndExpand,
Padding = new Thickness(20, 0),
Spacing = 20
};
if(!_favorites)
{
2017-01-03 08:17:15 +03:00
var addLoginButton = new ExtendedButton
{
2017-01-03 08:17:15 +03:00
Text = AppResources.AddALogin,
Command = new Command(() => AddLogin()),
2016-08-27 06:53:50 +03:00
Style = (Style)Application.Current.Resources["btn-primaryAccent"]
};
2017-01-03 08:17:15 +03:00
NoDataStackLayout.Children.Add(addLoginButton);
}
LoadingIndicator = new ActivityIndicator
{
IsRunning = true,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
Content = LoadingIndicator;
2016-07-11 08:12:31 +03:00
}
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
{
_filterResultsCancellationTokenSource = FilterResultsBackground(((SearchBar)sender).Text,
_filterResultsCancellationTokenSource);
2016-07-11 08:12:31 +03:00
}
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
{
var oldLength = e.OldTextValue?.Length ?? 0;
var newLength = e.NewTextValue?.Length ?? 0;
if(oldLength < 2 && newLength < 2 && oldLength < newLength)
{
return;
}
_filterResultsCancellationTokenSource = FilterResultsBackground(e.NewTextValue,
_filterResultsCancellationTokenSource);
2016-07-11 08:12:31 +03:00
}
private CancellationTokenSource FilterResultsBackground(string searchFilter, CancellationTokenSource previousCts)
2016-07-11 08:12:31 +03:00
{
var cts = new CancellationTokenSource();
Task.Run(async () =>
2016-07-11 08:12:31 +03:00
{
if(!string.IsNullOrWhiteSpace(searchFilter))
{
await Task.Delay(300);
if(searchFilter != Search.Text)
{
return;
}
else
{
previousCts?.Cancel();
}
}
try
{
FilterResults(searchFilter, cts.Token);
}
catch(OperationCanceledException) { }
}, cts.Token);
return cts;
2016-05-02 09:52:09 +03:00
}
private void FilterResults(string searchFilter, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if(string.IsNullOrWhiteSpace(searchFilter))
{
2017-01-03 08:17:15 +03:00
LoadFolders(Logins, ct);
}
else
{
searchFilter = searchFilter.ToLower();
2017-01-03 08:17:15 +03:00
var filteredLogins = Logins
.Where(s => s.Name.ToLower().Contains(searchFilter) || s.Username.ToLower().Contains(searchFilter))
.TakeWhile(s => !ct.IsCancellationRequested)
.ToArray();
ct.ThrowIfCancellationRequested();
2017-01-03 08:17:15 +03:00
LoadFolders(filteredLogins, ct);
}
}
2016-07-15 08:52:33 +03:00
protected override void OnAppearing()
2016-05-02 09:52:09 +03:00
{
base.OnAppearing();
if(_loadExistingData)
{
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
2016-06-28 07:55:53 +03:00
if(_connectivity.IsConnected && Device.OS == TargetPlatform.iOS && !_favorites)
2016-06-28 07:55:53 +03:00
{
var pushPromptShow = _settings.GetValueOrDefault(Constants.PushInitialPromptShown, false);
Action registerAction = () =>
{
var lastPushRegistration = _settings.GetValueOrDefault<DateTime?>(Constants.PushLastRegistrationDate, null);
if(!pushPromptShow || !lastPushRegistration.HasValue
|| (DateTime.UtcNow - lastPushRegistration) > TimeSpan.FromDays(1))
{
_pushNotification.Register();
}
};
2016-07-02 02:16:47 +03:00
if(!pushPromptShow)
2016-06-28 07:55:53 +03:00
{
_settings.AddOrUpdateValue(Constants.PushInitialPromptShown, true);
_userDialogs.Alert(new AlertConfig
{
2016-11-26 00:42:52 +03:00
Message = AppResources.PushNotificationAlert,
Title = AppResources.EnableAutomaticSyncing,
2016-12-07 06:27:14 +03:00
OnAction = registerAction,
2016-11-26 00:42:52 +03:00
OkText = AppResources.OkGotIt
});
2016-06-28 07:55:53 +03:00
}
else
2016-07-02 02:16:47 +03:00
{
// Check push registration once per day
registerAction();
2016-07-02 02:16:47 +03:00
}
2016-06-28 07:55:53 +03:00
}
2016-05-04 02:49:49 +03:00
}
2016-05-02 09:52:09 +03:00
protected override bool OnBackButtonPressed()
{
if(string.IsNullOrWhiteSpace(Uri))
{
return false;
}
2017-02-14 06:10:34 +03:00
_googleAnalyticsService.TrackExtensionEvent("BackClosed", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", (VaultListPageModel.Login)null);
return true;
}
private void AdjustContent()
{
if(PresentationFolders.Count > 0 || !string.IsNullOrWhiteSpace(Search.Text))
{
Content = ResultsStackLayout;
}
else
{
Content = NoDataStackLayout;
}
}
private CancellationTokenSource FetchAndLoadVault()
2016-07-11 08:12:31 +03:00
{
var cts = new CancellationTokenSource();
_settings.AddOrUpdateValue(Constants.FirstVaultLoad, false);
_loadExistingData = true;
if(PresentationFolders.Count > 0 && _syncService.SyncInProgress)
{
return cts;
}
_filterResultsCancellationTokenSource?.Cancel();
2016-07-15 08:52:33 +03:00
Task.Run(async () =>
{
var foldersTask = _folderService.GetAllAsync();
2017-01-03 08:17:15 +03:00
var loginsTask = _favorites ? _loginService.GetAllAsync(true) : _loginService.GetAllAsync();
await Task.WhenAll(foldersTask, loginsTask);
2016-07-11 08:12:31 +03:00
var folders = await foldersTask;
2017-01-03 08:17:15 +03:00
var logins = await loginsTask;
2016-07-11 08:12:31 +03:00
Folders = folders
.Select(f => new VaultListPageModel.Folder(f))
.OrderBy(s => s.Name)
.ToArray();
2017-01-03 08:17:15 +03:00
Logins = logins
.Select(s => new VaultListPageModel.Login(s))
.OrderBy(s => s.Name)
.ThenBy(s => s.Username)
.ToArray();
2016-07-11 08:12:31 +03:00
try
{
FilterResults(Search.Text, cts.Token);
}
catch(OperationCanceledException) { }
}, cts.Token);
return cts;
2016-07-11 08:12:31 +03:00
}
2017-01-03 08:17:15 +03:00
private void LoadFolders(VaultListPageModel.Login[] logins, CancellationToken ct)
2016-05-04 02:49:49 +03:00
{
2016-07-11 08:12:31 +03:00
var folders = new List<VaultListPageModel.Folder>(Folders);
foreach(var folder in folders)
{
if(folder.Any())
{
folder.Clear();
}
2017-01-03 08:17:15 +03:00
var loginsToAdd = logins
.Where(s => s.FolderId == folder.Id)
.TakeWhile(s => !ct.IsCancellationRequested)
.ToList();
ct.ThrowIfCancellationRequested();
2017-01-03 08:17:15 +03:00
folder.AddRange(loginsToAdd);
2016-07-11 08:12:31 +03:00
}
2016-05-04 02:49:49 +03:00
2017-01-03 08:17:15 +03:00
var noneToAdd = logins
.Where(s => s.FolderId == null)
.TakeWhile(s => !ct.IsCancellationRequested)
.ToList();
ct.ThrowIfCancellationRequested();
var noneFolder = new VaultListPageModel.Folder(noneToAdd);
2016-07-11 08:12:31 +03:00
folders.Add(noneFolder);
var foldersToAdd = folders
.Where(f => f.Any())
.TakeWhile(s => !ct.IsCancellationRequested)
.ToList();
ct.ThrowIfCancellationRequested();
Device.BeginInvokeOnMainThread(() =>
{
PresentationFolders.ResetWithRange(foldersToAdd);
AdjustContent();
});
2016-05-02 09:52:09 +03:00
}
2017-01-03 08:17:15 +03:00
private async void LoginSelected(object sender, SelectedItemChangedEventArgs e)
2016-05-02 09:52:09 +03:00
{
2017-01-03 08:17:15 +03:00
var login = e.SelectedItem as VaultListPageModel.Login;
2017-02-14 06:10:34 +03:00
if(login == null)
{
return;
}
string selection = null;
if(!string.IsNullOrWhiteSpace(Uri))
{
2017-02-14 06:10:34 +03:00
selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
AppResources.Autofill, AppResources.View);
}
if(selection == AppResources.View || string.IsNullOrWhiteSpace(Uri))
{
var page = new VaultViewLoginPage(login.Id);
await Navigation.PushForDeviceAsync(page);
}
2017-02-14 06:10:34 +03:00
else if(selection == AppResources.Autofill)
{
if(Uri.StartsWith("http") && _deviceInfoService.Version < 21)
{
MoreClickedAsync(login);
}
2017-02-14 06:10:34 +03:00
else
{
_googleAnalyticsService.TrackExtensionEvent("AutoFilled", Uri.StartsWith("http") ? "Website" : "App");
MessagingCenter.Send(Application.Current, "Autofill", login);
}
}
2017-02-14 06:10:34 +03:00
((ListView)sender).SelectedItem = null;
2016-05-02 09:52:09 +03:00
}
2017-01-03 08:17:15 +03:00
private async void MoreClickedAsync(VaultListPageModel.Login login)
2016-05-02 09:52:09 +03:00
{
var buttons = new List<string> { AppResources.View, AppResources.Edit };
2017-01-03 08:17:15 +03:00
if(!string.IsNullOrWhiteSpace(login.Password.Value))
{
buttons.Add(AppResources.CopyPassword);
}
2017-01-03 08:17:15 +03:00
if(!string.IsNullOrWhiteSpace(login.Username))
{
buttons.Add(AppResources.CopyUsername);
}
2017-01-03 08:17:15 +03:00
if(!string.IsNullOrWhiteSpace(login.Uri.Value) && (login.Uri.Value.StartsWith("http://")
|| login.Uri.Value.StartsWith("https://")))
{
buttons.Add(AppResources.GoToWebsite);
}
2017-01-03 08:17:15 +03:00
var selection = await DisplayActionSheet(login.Name, AppResources.Cancel, null, buttons.ToArray());
2016-05-02 09:52:09 +03:00
if(selection == AppResources.View)
2016-05-04 02:49:49 +03:00
{
2017-01-03 08:17:15 +03:00
var page = new VaultViewLoginPage(login.Id);
await Navigation.PushForDeviceAsync(page);
2016-05-04 02:49:49 +03:00
}
else if(selection == AppResources.Edit)
{
2017-01-03 08:17:15 +03:00
var page = new VaultEditLoginPage(login.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.CopyPassword)
{
2017-01-03 08:17:15 +03:00
Copy(login.Password.Value, AppResources.Password);
}
else if(selection == AppResources.CopyUsername)
{
2017-01-03 08:17:15 +03:00
Copy(login.Username, AppResources.Username);
}
else if(selection == AppResources.GoToWebsite)
{
2017-01-03 08:17:15 +03:00
Device.OpenUri(new Uri(login.Uri.Value));
}
}
private void Copy(string copyText, string alertLabel)
{
_clipboardService.CopyToClipboard(copyText);
_userDialogs.Toast(string.Format(AppResources.ValueHasBeenCopied, alertLabel));
2016-05-04 02:49:49 +03:00
}
2017-01-03 08:17:15 +03:00
private async void AddLogin()
{
var page = new VaultAddLoginPage(Uri);
await Navigation.PushForDeviceAsync(page);
}
2017-01-03 08:17:15 +03:00
private class AddLoginToolBarItem : ToolbarItem
2016-05-04 02:49:49 +03:00
{
2017-01-03 08:17:15 +03:00
private readonly VaultListLoginsPage _page;
2016-05-04 02:49:49 +03:00
2017-01-03 08:17:15 +03:00
public AddLoginToolBarItem(VaultListLoginsPage page)
2016-05-04 02:49:49 +03:00
{
_page = page;
Text = AppResources.Add;
2016-06-14 05:09:16 +03:00
Icon = "plus";
2016-05-04 02:49:49 +03:00
Clicked += ClickedItem;
}
private void ClickedItem(object sender, EventArgs e)
2016-05-04 02:49:49 +03:00
{
2017-01-03 08:17:15 +03:00
_page.AddLogin();
2016-05-04 02:49:49 +03:00
}
}
2016-07-01 02:08:09 +03:00
private class VaultListHeaderViewCell : ExtendedViewCell
{
2017-01-03 08:17:15 +03:00
public VaultListHeaderViewCell(VaultListLoginsPage page)
{
var image = new Image
{
2016-07-30 07:46:17 +03:00
Source = "folder",
WidthRequest = 18,
HeightRequest = 18
};
var label = new Label
{
2016-08-23 01:59:15 +03:00
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label)),
2016-12-26 19:30:57 +03:00
Style = (Style)Application.Current.Resources["text-muted"],
VerticalTextAlignment = TextAlignment.Center
};
label.SetBinding<VaultListPageModel.Folder>(Label.TextProperty, s => s.Name);
var grid = new Grid
{
ColumnSpacing = 10,
Padding = new Thickness(16, 8, 0, 8)
};
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(18, GridUnitType.Absolute) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.Children.Add(image, 0, 0);
grid.Children.Add(label, 1, 0);
View = grid;
2016-07-01 02:08:09 +03:00
BackgroundColor = Color.FromHex("efeff4");
2016-05-04 02:49:49 +03:00
}
2016-05-02 09:52:09 +03:00
}
}
}