1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-31 23:53:25 +00:00
Files
mobile/src/App/Pages/Vault/VaultListCiphersPage.cs
2017-11-27 15:42:36 -05:00

400 lines
15 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
using Xamarin.Forms;
using XLabs.Ioc;
using Bit.App.Utilities;
using Plugin.Settings.Abstractions;
using Plugin.Connectivity.Abstractions;
using System.Threading;
using static Bit.App.Models.Page.VaultListPageModel;
using System.Collections.Generic;
namespace Bit.App.Pages
{
public class VaultListCiphersPage : ExtendedContentPage
{
private readonly ICipherService _cipherService;
private readonly IConnectivity _connectivity;
private readonly ISyncService _syncService;
private readonly IDeviceInfoService _deviceInfoService;
private readonly ISettings _settings;
private readonly IAppSettingsService _appSettingsService;
private readonly IGoogleAnalyticsService _googleAnalyticsService;
private readonly IDeviceActionService _deviceActionService;
private CancellationTokenSource _filterResultsCancellationTokenSource;
private readonly bool _favorites = false;
private readonly bool _folder = false;
private readonly string _folderId = null;
private readonly string _collectionId = null;
private readonly string _groupingName = null;
private readonly string _uri = null;
public VaultListCiphersPage(bool folder = false, string folderId = null,
string collectionId = null, string groupingName = null, bool favorites = false, string uri = null)
: base(true)
{
_folder = folder;
_folderId = folderId;
_collectionId = collectionId;
_favorites = favorites;
_groupingName = groupingName;
_uri = uri;
_cipherService = Resolver.Resolve<ICipherService>();
_connectivity = Resolver.Resolve<IConnectivity>();
_syncService = Resolver.Resolve<ISyncService>();
_deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
_settings = Resolver.Resolve<ISettings>();
_appSettingsService = Resolver.Resolve<IAppSettingsService>();
_googleAnalyticsService = Resolver.Resolve<IGoogleAnalyticsService>();
_deviceActionService = Resolver.Resolve<IDeviceActionService>();
Init();
}
public ExtendedObservableCollection<Section<Cipher>> PresentationSections { get; private set; }
= new ExtendedObservableCollection<Section<Cipher>>();
public Cipher[] Ciphers { get; set; } = new Cipher[] { };
public ListView ListView { get; set; }
public SearchBar Search { get; set; }
public ActivityIndicator LoadingIndicator { get; set; }
public StackLayout NoDataStackLayout { get; set; }
public StackLayout ResultsStackLayout { get; set; }
private AddCipherToolBarItem AddCipherItem { get; set; }
private void Init()
{
if(!string.IsNullOrWhiteSpace(_uri) || _folder || !string.IsNullOrWhiteSpace(_folderId))
{
AddCipherItem = new AddCipherToolBarItem(this, _folderId);
ToolbarItems.Add(AddCipherItem);
}
ListView = new ListView(ListViewCachingStrategy.RecycleElement)
{
IsGroupingEnabled = true,
ItemsSource = PresentationSections,
HasUnevenRows = true,
GroupHeaderTemplate = new DataTemplate(() => new SectionHeaderViewCell(nameof(Section<Cipher>.Name),
nameof(Section<Cipher>.Count))),
GroupShortNameBinding = new Binding(nameof(Section<Cipher>.Name)),
ItemTemplate = new DataTemplate(() => new VaultListViewCell(
(Cipher c) => Helpers.CipherMoreClickedAsync(this, c, !string.IsNullOrWhiteSpace(_uri))))
};
if(Device.RuntimePlatform == Device.iOS)
{
ListView.RowHeight = -1;
}
Search = new SearchBar
{
Placeholder = AppResources.SearchVault,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Button)),
CancelButtonColor = Color.FromHex("3c8dbc")
};
// Bug with search bar on android 7, ref https://bugzilla.xamarin.com/show_bug.cgi?id=43975
if(Device.RuntimePlatform == Device.Android && _deviceInfoService.Version >= 24)
{
Search.HeightRequest = 50;
}
var noDataLabel = new Label
{
Text = _favorites ? AppResources.NoFavorites : AppResources.NoItems,
HorizontalTextAlignment = TextAlignment.Center,
FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label)),
Style = (Style)Application.Current.Resources["text-muted"]
};
if(_folder || !string.IsNullOrWhiteSpace(_folderId))
{
noDataLabel.Text = AppResources.NoItemsFolder;
}
else if(!string.IsNullOrWhiteSpace(_collectionId))
{
noDataLabel.Text = AppResources.NoItemsCollection;
}
NoDataStackLayout = new StackLayout
{
Children = { noDataLabel },
VerticalOptions = LayoutOptions.CenterAndExpand,
Padding = new Thickness(20, 0),
Spacing = 20
};
if(string.IsNullOrWhiteSpace(_collectionId) && !_favorites)
{
NoDataStackLayout.Children.Add(new ExtendedButton
{
Text = AppResources.AddAnItem,
Command = new Command(() => Helpers.AddCipher(this, _folderId)),
Style = (Style)Application.Current.Resources["btn-primaryAccent"]
});
}
ResultsStackLayout = new StackLayout
{
Children = { Search, ListView },
Spacing = 0
};
if(!string.IsNullOrWhiteSpace(_groupingName))
{
Title = _groupingName;
}
else if(_favorites)
{
Title = AppResources.Favorites;
}
else
{
Title = AppResources.SearchVault;
}
LoadingIndicator = new ActivityIndicator
{
IsRunning = true,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center
};
Content = LoadingIndicator;
}
private void SearchBar_SearchButtonPressed(object sender, EventArgs e)
{
_filterResultsCancellationTokenSource = FilterResultsBackground(((SearchBar)sender).Text,
_filterResultsCancellationTokenSource);
}
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);
}
private CancellationTokenSource FilterResultsBackground(string searchFilter,
CancellationTokenSource previousCts)
{
var cts = new CancellationTokenSource();
Task.Run(async () =>
{
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;
}
private void FilterResults(string searchFilter, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if(string.IsNullOrWhiteSpace(searchFilter))
{
LoadSections(Ciphers, ct);
}
else
{
searchFilter = searchFilter.ToLower();
var filteredCiphers = Ciphers
.Where(s => s.Name.ToLower().Contains(searchFilter) ||
(s.Subtitle?.ToLower().Contains(searchFilter) ?? false))
.TakeWhile(s => !ct.IsCancellationRequested)
.ToArray();
ct.ThrowIfCancellationRequested();
LoadSections(filteredCiphers, ct);
}
}
protected override bool OnBackButtonPressed()
{
if(string.IsNullOrWhiteSpace(_uri))
{
return false;
}
_googleAnalyticsService.TrackExtensionEvent("BackClosed", _uri.StartsWith("http") ? "Website" : "App");
_deviceActionService.CloseAutofill();
return true;
}
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Subscribe<Application, bool>(Application.Current, "SyncCompleted", (sender, success) =>
{
if(success)
{
_filterResultsCancellationTokenSource = FetchAndLoadVault();
}
});
AddCipherItem?.InitEvents();
ListView.ItemSelected += CipherSelected;
Search.TextChanged += SearchBar_TextChanged;
Search.SearchButtonPressed += SearchBar_SearchButtonPressed;
_filterResultsCancellationTokenSource = FetchAndLoadVault();
if(string.IsNullOrWhiteSpace(_uri) && !_folder && string.IsNullOrWhiteSpace(_folderId) &&
string.IsNullOrWhiteSpace(_collectionId) && !_favorites)
{
Search.FocusWithDelay();
}
}
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Unsubscribe<Application, bool>(Application.Current, "SyncCompleted");
AddCipherItem?.Dispose();
ListView.ItemSelected -= CipherSelected;
Search.TextChanged -= SearchBar_TextChanged;
Search.SearchButtonPressed -= SearchBar_SearchButtonPressed;
}
private CancellationTokenSource FetchAndLoadVault()
{
var cts = new CancellationTokenSource();
if(PresentationSections.Count > 0 && _syncService.SyncInProgress)
{
return cts;
}
_filterResultsCancellationTokenSource?.Cancel();
Task.Run(async () =>
{
IEnumerable<Models.Cipher> ciphers;
if(_folder || !string.IsNullOrWhiteSpace(_folderId))
{
ciphers = await _cipherService.GetAllByFolderAsync(_folderId);
}
else if(!string.IsNullOrWhiteSpace(_collectionId))
{
ciphers = await _cipherService.GetAllByCollectionAsync(_collectionId);
}
else if(_favorites)
{
ciphers = await _cipherService.GetAllAsync(true);
}
else
{
ciphers = await _cipherService.GetAllAsync();
}
Ciphers = ciphers
.Select(s => new Cipher(s, _appSettingsService))
.OrderBy(s =>
{
// Sort numbers and letters before special characters
return !string.IsNullOrWhiteSpace(s.Name) && s.Name.Length > 0 &&
Char.IsDigit(s.Name[0]) ? 0 : Char.IsLetter(s.Name[0]) ? 1 : 2;
})
.ThenBy(s => s.Name)
.ThenBy(s => s.Subtitle)
.ToArray();
try
{
FilterResults(Search.Text, cts.Token);
}
catch(OperationCanceledException) { }
}, cts.Token);
return cts;
}
private void LoadSections(Cipher[] ciphers, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var sections = ciphers.GroupBy(c => c.NameGroup).Select(g => new Section<Cipher>(g.ToList(), g.Key));
ct.ThrowIfCancellationRequested();
Device.BeginInvokeOnMainThread(() =>
{
PresentationSections.ResetWithRange(sections);
if(PresentationSections.Count > 0 || !string.IsNullOrWhiteSpace(Search.Text))
{
Content = ResultsStackLayout;
}
else if(_syncService.SyncInProgress)
{
Content = LoadingIndicator;
}
else
{
Content = NoDataStackLayout;
}
});
}
private async void CipherSelected(object sender, SelectedItemChangedEventArgs e)
{
var cipher = e.SelectedItem as Cipher;
if(cipher == null)
{
return;
}
string selection = null;
if(!string.IsNullOrWhiteSpace(_uri))
{
selection = await DisplayActionSheet(AppResources.AutofillOrView, AppResources.Cancel, null,
AppResources.Autofill, AppResources.View);
}
if(selection == AppResources.View || string.IsNullOrWhiteSpace(_uri))
{
var page = new VaultViewCipherPage(cipher.Type, cipher.Id);
await Navigation.PushForDeviceAsync(page);
}
else if(selection == AppResources.Autofill)
{
if(_deviceInfoService.Version < 21)
{
Helpers.CipherMoreClickedAsync(this, cipher, !string.IsNullOrWhiteSpace(_uri));
}
else
{
_googleAnalyticsService.TrackExtensionEvent("AutoFilled",
_uri.StartsWith("http") ? "Website" : "App");
_deviceActionService.Autofill(cipher);
}
}
((ListView)sender).SelectedItem = null;
}
}
}