attachments page

This commit is contained in:
Kyle Spearrin 2019-05-10 23:43:35 -04:00
parent 34fd9b5842
commit 29b37219c2
16 changed files with 536 additions and 90 deletions

View file

@ -100,6 +100,7 @@
<Compile Include="Services\CryptoPrimitiveService.cs" /> <Compile Include="Services\CryptoPrimitiveService.cs" />
<Compile Include="Services\DeviceActionService.cs" /> <Compile Include="Services\DeviceActionService.cs" />
<Compile Include="Services\LocalizeService.cs" /> <Compile Include="Services\LocalizeService.cs" />
<Compile Include="Utilities\AndroidHelpers.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidAsset Include="Assets\FontAwesome.ttf" /> <AndroidAsset Include="Assets\FontAwesome.ttf" />

View file

@ -2,6 +2,15 @@
using Android.Content.PM; using Android.Content.PM;
using Android.Runtime; using Android.Runtime;
using Android.OS; using Android.OS;
using Bit.Core;
using System.Linq;
using Bit.App.Abstractions;
using Bit.Core.Utilities;
using Bit.Core.Abstractions;
using System.IO;
using System;
using Android.Content;
using Bit.Droid.Utilities;
namespace Bit.Droid namespace Bit.Droid
{ {
@ -14,8 +23,14 @@ namespace Bit.Droid
[Register("com.x8bit.bitwarden.MainActivity")] [Register("com.x8bit.bitwarden.MainActivity")]
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{ {
private IDeviceActionService _deviceActionService;
private IMessagingService _messagingService;
protected override void OnCreate(Bundle savedInstanceState) protected override void OnCreate(Bundle savedInstanceState)
{ {
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
TabLayoutResource = Resource.Layout.Tabbar; TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar; ToolbarResource = Resource.Layout.Toolbar;
@ -25,11 +40,63 @@ namespace Bit.Droid
LoadApplication(new App.App()); LoadApplication(new App.App());
} }
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
[GeneratedEnum] Permission[] grantResults) [GeneratedEnum] Permission[] grantResults)
{
if(requestCode == Constants.SelectFilePermissionRequestCode)
{
if(grantResults.Any(r => r != Permission.Granted))
{
_messagingService.Send("selectFileCameraPermissionDenied");
}
await _deviceActionService.SelectFileAsync();
}
else
{ {
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
base.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
} }
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if(requestCode == Constants.SelectFileRequestCode && resultCode == Result.Ok)
{
Android.Net.Uri uri = null;
string fileName = null;
if(data != null && data.Data != null)
{
uri = data.Data;
fileName = AndroidHelpers.GetFileName(ApplicationContext, uri);
}
else
{
// camera
var root = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
uri = Android.Net.Uri.FromFile(file);
fileName = $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
}
if(uri == null)
{
return;
}
try
{
using(var stream = ContentResolver.OpenInputStream(uri))
using(var memoryStream = new MemoryStream())
{
stream.CopyTo(memoryStream);
_messagingService.Send("selectFileResult",
new Tuple<byte[], string>(memoryStream.ToArray(), fileName ?? "unknown_file_name"));
}
}
catch(Java.IO.FileNotFoundException)
{
return;
}
}
}
} }
} }

View file

@ -54,7 +54,8 @@ namespace Bit.Droid
var secureStorageService = new SecureStorageService(); var secureStorageService = new SecureStorageService();
var cryptoPrimitiveService = new CryptoPrimitiveService(); var cryptoPrimitiveService = new CryptoPrimitiveService();
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage); var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
var deviceActionService = new DeviceActionService(mobileStorageService); var deviceActionService = new DeviceActionService(mobileStorageService, messagingService,
broadcasterService);
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService); broadcasterService);

View file

@ -1,9 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM; using Android.Content.PM;
using Android.OS;
using Android.Provider;
using Android.Support.V4.App;
using Android.Support.V4.Content; using Android.Support.V4.Content;
using Android.Webkit; using Android.Webkit;
using Android.Widget; using Android.Widget;
@ -19,13 +24,28 @@ namespace Bit.Droid.Services
public class DeviceActionService : IDeviceActionService public class DeviceActionService : IDeviceActionService
{ {
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private readonly IMessagingService _messagingService;
private readonly IBroadcasterService _broadcasterService;
private ProgressDialog _progressDialog; private ProgressDialog _progressDialog;
private Android.Widget.Toast _toast; private bool _cameraPermissionsDenied;
private Toast _toast;
public DeviceActionService(IStorageService storageService) public DeviceActionService(
IStorageService storageService,
IMessagingService messagingService,
IBroadcasterService broadcasterService)
{ {
_storageService = storageService; _storageService = storageService;
_messagingService = messagingService;
_broadcasterService = broadcasterService;
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
{
if(message.Command == "selectFileCameraPermissionDenied")
{
_cameraPermissionsDenied = true;
}
});
} }
public DeviceType DeviceType => DeviceType.Android; public DeviceType DeviceType => DeviceType.Android;
@ -39,7 +59,7 @@ namespace Bit.Droid.Services
_toast = null; _toast = null;
} }
_toast = Android.Widget.Toast.MakeText(CrossCurrentActivity.Current.Activity, text, _toast = Android.Widget.Toast.MakeText(CrossCurrentActivity.Current.Activity, text,
longDuration ? Android.Widget.ToastLength.Long : Android.Widget.ToastLength.Short); longDuration ? ToastLength.Long : ToastLength.Short);
_toast.Show(); _toast.Show();
} }
@ -149,6 +169,54 @@ namespace Bit.Droid.Services
catch(Exception) { } catch(Exception) { }
} }
public Task SelectFileAsync()
{
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
var hasStorageWritePermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.WriteExternalStorage);
var additionalIntents = new List<IParcelable>();
if(activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera))
{
var hasCameraPermission = !_cameraPermissionsDenied && HasPermission(Manifest.Permission.Camera);
if(!_cameraPermissionsDenied && !hasStorageWritePermission)
{
AskPermission(Manifest.Permission.WriteExternalStorage);
return Task.FromResult(0);
}
if(!_cameraPermissionsDenied && !hasCameraPermission)
{
AskPermission(Manifest.Permission.Camera);
return Task.FromResult(0);
}
if(!_cameraPermissionsDenied && hasCameraPermission && hasStorageWritePermission)
{
try
{
var root = new Java.IO.File(Android.OS.Environment.ExternalStorageDirectory, "bitwarden");
var file = new Java.IO.File(root, "temp_camera_photo.jpg");
if(!file.Exists())
{
file.ParentFile.Mkdirs();
file.CreateNewFile();
}
var outputFileUri = Android.Net.Uri.FromFile(file);
additionalIntents.AddRange(GetCameraIntents(outputFileUri));
}
catch(Java.IO.IOException) { }
}
}
var docIntent = new Intent(Intent.ActionOpenDocument);
docIntent.AddCategory(Intent.CategoryOpenable);
docIntent.SetType("*/*");
var chooserIntent = Intent.CreateChooser(docIntent, AppResources.FileSource);
if(additionalIntents.Count > 0)
{
chooserIntent.PutExtra(Intent.ExtraInitialIntents, additionalIntents.ToArray());
}
activity.StartActivityForResult(chooserIntent, Constants.SelectFileRequestCode);
return Task.FromResult(0);
}
public Task<string> DisplayPromptAync(string title = null, string description = null, public Task<string> DisplayPromptAync(string title = null, string description = null,
string text = null, string okButtonText = null, string cancelButtonText = null) string text = null, string okButtonText = null, string cancelButtonText = null)
{ {
@ -217,5 +285,35 @@ namespace Bit.Droid.Services
return false; return false;
} }
} }
private bool HasPermission(string permission)
{
return ContextCompat.CheckSelfPermission(
CrossCurrentActivity.Current.Activity, permission) == Permission.Granted;
}
private void AskPermission(string permission)
{
ActivityCompat.RequestPermissions(CrossCurrentActivity.Current.Activity, new string[] { permission },
Constants.SelectFilePermissionRequestCode);
}
private List<IParcelable> GetCameraIntents(Android.Net.Uri outputUri)
{
var intents = new List<IParcelable>();
var pm = CrossCurrentActivity.Current.Activity.PackageManager;
var captureIntent = new Intent(MediaStore.ActionImageCapture);
var listCam = pm.QueryIntentActivities(captureIntent, 0);
foreach(var res in listCam)
{
var packageName = res.ActivityInfo.PackageName;
var intent = new Intent(captureIntent);
intent.SetComponent(new ComponentName(packageName, res.ActivityInfo.Name));
intent.SetPackage(packageName);
intent.PutExtra(MediaStore.ExtraOutput, outputUri);
intents.Add(intent);
}
return intents;
}
} }
} }

View file

@ -0,0 +1,30 @@
using Android.Content;
using Android.Provider;
namespace Bit.Droid.Utilities
{
public static class AndroidHelpers
{
public static string GetFileName(Context context, Android.Net.Uri uri)
{
string name = null;
string[] projection = { MediaStore.MediaColumns.DisplayName };
var metaCursor = context.ContentResolver.Query(uri, projection, null, null, null);
if(metaCursor != null)
{
try
{
if(metaCursor.MoveToFirst())
{
name = metaCursor.GetString(0);
}
}
finally
{
metaCursor.Close();
}
}
return name;
}
}
}

View file

@ -13,6 +13,7 @@ namespace Bit.App.Abstractions
bool OpenFile(byte[] fileData, string id, string fileName); bool OpenFile(byte[] fileData, string id, string fileName);
bool CanOpenFile(string fileName); bool CanOpenFile(string fileName);
Task ClearCacheAsync(); Task ClearCacheAsync();
Task SelectFileAsync();
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null, Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null); string okButtonText = null, string cancelButtonText = null);
} }

View file

@ -99,11 +99,12 @@ namespace Bit.App.Pages
_vm.AddField(); _vm.AddField();
} }
private void Attachments_Clicked(object sender, System.EventArgs e) private async void Attachments_Clicked(object sender, System.EventArgs e)
{ {
if(DoOnce()) if(DoOnce())
{ {
// await Navigation.PushModalAsync(); var page = new AttachmentsPage(_vm.CipherId);
await Navigation.PushModalAsync(new NavigationPage(page));
} }
} }

View file

@ -4,6 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Bit.App.Pages.AttachmentsPage" x:Class="Bit.App.Pages.AttachmentsPage"
xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:views="clr-namespace:Bit.Core.Models.View;assembly=BitwardenCore"
xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:u="clr-namespace:Bit.App.Utilities"
xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:controls="clr-namespace:Bit.App.Controls"
x:DataType="pages:AttachmentsPageViewModel" x:DataType="pages:AttachmentsPageViewModel"
@ -21,6 +22,7 @@
<ResourceDictionary> <ResourceDictionary>
<u:InverseBoolConverter x:Key="inverseBool" /> <u:InverseBoolConverter x:Key="inverseBool" />
<u:IsNotNullConverter x:Key="notNull" /> <u:IsNotNullConverter x:Key="notNull" />
<u:IsNullConverter x:Key="null" />
</ResourceDictionary> </ResourceDictionary>
</ContentPage.Resources> </ContentPage.Resources>
@ -28,22 +30,30 @@
<StackLayout Spacing="20"> <StackLayout Spacing="20">
<StackLayout StyleClass="box"> <StackLayout StyleClass="box">
<StackLayout StyleClass="box-row" <StackLayout StyleClass="box-row"
IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}"> IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}">
<Label Text="{u:I18n NoCollectionsToList}" /> <Label Text="{u:I18n NoAttachments}" />
</StackLayout> </StackLayout>
<controls:RepeaterView ItemsSource="{Binding Collections}" IsVisible="{Binding HasCollections}"> <controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}">
<controls:RepeaterView.ItemTemplate> <controls:RepeaterView.ItemTemplate>
<DataTemplate x:DataType="pages:CollectionViewModel"> <DataTemplate x:DataType="views:AttachmentView">
<StackLayout Spacing="0" Padding="0"> <StackLayout Spacing="0" Padding="0">
<StackLayout StyleClass="box-row, box-row-switch"> <StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
<Label <Label
Text="{Binding Collection.Name}" Text="{Binding FileName, Mode=OneWay}"
StyleClass="box-label, box-label-regular"
HorizontalOptions="StartAndExpand" />
<Switch
IsToggled="{Binding Checked}"
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> VerticalTextAlignment="Center"
HorizontalOptions="StartAndExpand" />
<Label
Text="{Binding SizeName, Mode=OneWay}"
StyleClass="box-sub-label"
HorizontalTextAlignment="End"
VerticalTextAlignment="Center" />
<controls:FaButton
StyleClass="box-row-button, box-row-button-platform"
Text="&#xf014;"
Command="{Binding BindingContext.DeleteAttachmentCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
VerticalOptions="Center" />
</StackLayout> </StackLayout>
<BoxView StyleClass="box-row-separator" /> <BoxView StyleClass="box-row-separator" />
</StackLayout> </StackLayout>
@ -51,6 +61,34 @@
</controls:RepeaterView.ItemTemplate> </controls:RepeaterView.ItemTemplate>
</controls:RepeaterView> </controls:RepeaterView>
</StackLayout> </StackLayout>
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row-header">
<Label Text="{u:I18n AddNewAttachment}"
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row">
<Label
IsVisible="{Binding FileName, Converter={StaticResource null}}"
Text="{u:I18n NoFileChosen}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
<Label
IsVisible="{Binding FileName, Converter={StaticResource notNull}}"
Text="{Binding FileName}"
LineBreakMode="CharacterWrap"
StyleClass="text-sm, text-muted"
HorizontalOptions="FillAndExpand"
HorizontalTextAlignment="Center" />
</StackLayout>
<Button Text="{u:I18n ChooseFile}" StyleClass="box-button-row"
Clicked="ChooseFile_Clicked"></Button>
<Label
Margin="0, 10, 0, 0"
Text="{u:I18n MaxFileSize}"
StyleClass="box-footer-label" />
</StackLayout>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>

View file

@ -1,14 +1,19 @@
using Xamarin.Forms; using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using System;
using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
public partial class AttachmentsPage : BaseContentPage public partial class AttachmentsPage : BaseContentPage
{ {
private AttachmentsPageViewModel _vm; private AttachmentsPageViewModel _vm;
private readonly IBroadcasterService _broadcasterService;
public AttachmentsPage(string cipherId) public AttachmentsPage(string cipherId)
{ {
InitializeComponent(); InitializeComponent();
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
_vm = BindingContext as AttachmentsPageViewModel; _vm = BindingContext as AttachmentsPageViewModel;
_vm.Page = this; _vm.Page = this;
_vm.CipherId = cipherId; _vm.CipherId = cipherId;
@ -18,20 +23,38 @@ namespace Bit.App.Pages
protected override async void OnAppearing() protected override async void OnAppearing()
{ {
base.OnAppearing(); base.OnAppearing();
await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync()); _broadcasterService.Subscribe(nameof(AttachmentsPage), async (message) =>
{
if(message.Command == "selectFileResult")
{
var data = message.Data as Tuple<byte[], string>;
_vm.FileData = data.Item1;
_vm.FileName = data.Item2;
}
});
await LoadOnAppearedAsync(_scrollView, true, () => _vm.InitAsync());
} }
protected override void OnDisappearing() protected override void OnDisappearing()
{ {
base.OnDisappearing(); base.OnDisappearing();
_broadcasterService.Unsubscribe(nameof(AttachmentsPage));
} }
private async void Save_Clicked(object sender, System.EventArgs e) private async void Save_Clicked(object sender, EventArgs e)
{ {
if(DoOnce()) if(DoOnce())
{ {
await _vm.SubmitAsync(); await _vm.SubmitAsync();
} }
} }
private async void ChooseFile_Clicked(object sender, EventArgs e)
{
if(DoOnce())
{
await _vm.ChooseFileAsync();
}
}
} }
} }

View file

@ -8,6 +8,7 @@ using Bit.Core.Utilities;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -15,65 +16,103 @@ namespace Bit.App.Pages
{ {
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly ICipherService _cipherService; private readonly ICipherService _cipherService;
private readonly ICollectionService _collectionService; private readonly ICryptoService _cryptoService;
private readonly IUserService _userService;
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private CipherView _cipher; private CipherView _cipher;
private Cipher _cipherDomain; private Cipher _cipherDomain;
private bool _hasCollections; private bool _hasAttachments;
private bool _hasUpdatedKey;
private bool _canAccessAttachments;
private string _fileName;
public AttachmentsPageViewModel() public AttachmentsPageViewModel()
{ {
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService"); _deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService"); _cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService"); _userService = ServiceContainer.Resolve<IUserService>("userService");
Collections = new ExtendedObservableCollection<CollectionViewModel>(); Attachments = new ExtendedObservableCollection<AttachmentView>();
PageTitle = AppResources.Collections; DeleteAttachmentCommand = new Command<AttachmentView>(DeleteAsync);
PageTitle = AppResources.Attachments;
} }
public string CipherId { get; set; } public string CipherId { get; set; }
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; } public CipherView Cipher
public bool HasCollections
{ {
get => _hasCollections; get => _cipher;
set => SetProperty(ref _hasCollections, value); set => SetProperty(ref _cipher, value);
} }
public ExtendedObservableCollection<AttachmentView> Attachments { get; set; }
public bool HasAttachments
{
get => _hasAttachments;
set => SetProperty(ref _hasAttachments, value);
}
public string FileName
{
get => _fileName;
set => SetProperty(ref _fileName, value);
}
public byte[] FileData { get; set; }
public Command DeleteAttachmentCommand { get; set; }
public async Task LoadAsync() public async Task InitAsync()
{ {
_cipherDomain = await _cipherService.GetAsync(CipherId); _cipherDomain = await _cipherService.GetAsync(CipherId);
var collectionIds = _cipherDomain.CollectionIds; Cipher = await _cipherDomain.DecryptAsync();
_cipher = await _cipherDomain.DecryptAsync(); LoadAttachments();
var allCollections = await _collectionService.GetAllDecryptedAsync(); _hasUpdatedKey = await _cryptoService.HasEncKeyAsync();
var collections = allCollections var canAccessPremium = await _userService.CanAccessPremiumAsync();
.Where(c => !c.ReadOnly && c.OrganizationId == _cipher.OrganizationId) _canAccessAttachments = canAccessPremium || Cipher.OrganizationId != null;
.Select(c => new CollectionViewModel if(!_canAccessAttachments)
{ {
Collection = c, await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
Checked = collectionIds.Contains(c.Id) }
}).ToList(); else if(!_hasUpdatedKey)
Collections.ResetWithRange(collections); {
HasCollections = Collections.Any(); var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.UpdateKey,
AppResources.FeatureUnavailable, AppResources.LearnMore, AppResources.Cancel);
if(confirmed)
{
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/update-encryption-key/");
}
}
} }
public async Task<bool> SubmitAsync() public async Task<bool> SubmitAsync()
{ {
if(!Collections.Any(c => c.Checked)) if(!_hasUpdatedKey)
{ {
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection, await _platformUtilsService.ShowDialogAsync(AppResources.UpdateKey,
AppResources.Ok); AppResources.AnErrorHasOccurred);
return false;
}
if(FileData == null)
{
await _platformUtilsService.ShowDialogAsync(
string.Format(AppResources.ValidationFieldRequired, AppResources.File),
AppResources.AnErrorHasOccurred);
return false;
}
if(FileData.Length > 104857600) // 100 MB
{
await _platformUtilsService.ShowDialogAsync(AppResources.MaxFileSize,
AppResources.AnErrorHasOccurred);
return false; return false;
} }
_cipherDomain.CollectionIds = new HashSet<string>(
Collections.Where(c => c.Checked).Select(c => c.Collection.Id));
try try
{ {
await _deviceActionService.ShowLoadingAsync(AppResources.Saving); await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
await _cipherService.SaveCollectionsWithServerAsync(_cipherDomain); _cipherDomain = await _cipherService.SaveAttachmentRawWithServerAsync(
_cipherDomain, FileName, FileData);
Cipher = await _cipherDomain.DecryptAsync();
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.ItemUpdated); _platformUtilsService.ShowToast("success", null, AppResources.AttachementAdded);
await Page.Navigation.PopModalAsync(); LoadAttachments();
FileData = null;
FileName = null;
return true; return true;
} }
catch(ApiException e) catch(ApiException e)
@ -83,5 +122,44 @@ namespace Bit.App.Pages
} }
return false; return false;
} }
public async Task ChooseFileAsync()
{
await _deviceActionService.SelectFileAsync();
}
private async void DeleteAsync(AttachmentView attachment)
{
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete,
null, AppResources.Yes, AppResources.No);
if(!confirmed)
{
return;
}
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.Deleting);
await _cipherService.DeleteAttachmentWithServerAsync(Cipher.Id, attachment.Id);
await _deviceActionService.HideLoadingAsync();
_platformUtilsService.ShowToast("success", null, AppResources.AttachmentDeleted);
var attachmentToRemove = Cipher.Attachments.FirstOrDefault(a => a.Id == attachment.Id);
if(attachmentToRemove != null)
{
Cipher.Attachments.Remove(attachmentToRemove);
LoadAttachments();
}
}
catch(ApiException e)
{
await _deviceActionService.HideLoadingAsync();
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
}
}
private void LoadAttachments()
{
Attachments.ResetWithRange(Cipher.Attachments ?? new List<AttachmentView>());
HasAttachments = Cipher.HasAttachments;
}
} }
} }

View file

@ -112,6 +112,11 @@ namespace Bit.App.Pages
await _deviceActionService.HideLoadingAsync(); await _deviceActionService.HideLoadingAsync();
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok);
} }
catch(System.Exception e)
{
await _deviceActionService.HideLoadingAsync();
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Message, AppResources.Ok);
}
return false; return false;
} }

View file

@ -105,11 +105,12 @@ namespace Bit.App.Pages
EditToolbarItem_Clicked(sender, e); EditToolbarItem_Clicked(sender, e);
} }
private void Attachments_Clicked(object sender, System.EventArgs e) private async void Attachments_Clicked(object sender, System.EventArgs e)
{ {
if(DoOnce()) if(DoOnce())
{ {
// await Navigation.PushModalAsync(); var page = new AttachmentsPage(_vm.CipherId);
await Navigation.PushModalAsync(new NavigationPage(page));
} }
} }

View file

@ -12,5 +12,7 @@
public static string LastFileCacheClearKey = "lastFileCacheClear"; public static string LastFileCacheClearKey = "lastFileCacheClear";
public static string AccessibilityAutofillPasswordFieldKey = "accessibilityAutofillPasswordField"; public static string AccessibilityAutofillPasswordFieldKey = "accessibilityAutofillPasswordField";
public static string AccessibilityAutofillPersistNotificationKey = "accessibilityAutofillPersistNotification"; public static string AccessibilityAutofillPersistNotificationKey = "accessibilityAutofillPersistNotification";
public const int SelectFileRequestCode = 42;
public const int SelectFilePermissionRequestCode = 43;
} }
} }

View file

@ -249,7 +249,7 @@ namespace Bit.Core.Services
public Task DeleteCipherAttachmentAsync(string id, string attachmentId) public Task DeleteCipherAttachmentAsync(string id, string attachmentId)
{ {
return SendAsync<object, object>(HttpMethod.Delete, return SendAsync<object, object>(HttpMethod.Delete,
string.Concat("/ciphers/", id, "/attachments/", attachmentId), null, true, false); string.Concat("/ciphers/", id, "/attachment/", attachmentId), null, true, false);
} }
public Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, public Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data,

View file

@ -519,22 +519,11 @@ namespace Bit.Core.Services
var encFileName = await _cryptoService.EncryptAsync(filename, key); var encFileName = await _cryptoService.EncryptAsync(filename, key);
var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); var dataEncKey = await _cryptoService.MakeEncKeyAsync(key);
var encData = await _cryptoService.EncryptToBytesAsync(data, dataEncKey.Item1); var encData = await _cryptoService.EncryptToBytesAsync(data, dataEncKey.Item1);
var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks);
CipherResponse response; var fd = new MultipartFormDataContent(boundary);
try fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key");
{
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
{
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString); fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString); var response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd);
response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd);
}
}
catch(ApiException e)
{
throw new Exception(e.Error.GetSingleMessage());
}
var userId = await _userService.GetUserIdAsync(); var userId = await _userService.GetUserIdAsync();
var cData = new CipherData(response, userId, cipher.CollectionIds); var cData = new CipherData(response, userId, cipher.CollectionIds);
await UpsertAsync(cData); await UpsertAsync(cData);
@ -670,12 +659,13 @@ namespace Bit.Core.Services
try try
{ {
await _apiService.DeleteCipherAttachmentAsync(id, attachmentId); await _apiService.DeleteCipherAttachmentAsync(id, attachmentId);
await DeleteAttachmentAsync(id, attachmentId);
} }
catch(ApiException e) catch(ApiException e)
{ {
throw new Exception(e.Error.GetSingleMessage());
}
await DeleteAttachmentAsync(id, attachmentId); await DeleteAttachmentAsync(id, attachmentId);
throw e;
}
} }
public async Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId) public async Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId)
@ -716,21 +706,12 @@ namespace Bit.Core.Services
var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key); var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key);
var dataEncKey = await _cryptoService.MakeEncKeyAsync(key); var dataEncKey = await _cryptoService.MakeEncKeyAsync(key);
var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1); var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1);
var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks);
try var fd = new MultipartFormDataContent(boundary);
{ fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key");
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
{
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString); fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString);
await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId); await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId);
} }
}
catch(ApiException e)
{
throw new Exception(e.Error.GetSingleMessage());
}
}
private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri, private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri,
List<CipherView> matchingLogins, List<CipherView> matchingFuzzyLogins, List<CipherView> matchingLogins, List<CipherView> matchingFuzzyLogins,

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
@ -12,6 +13,8 @@ using Bit.Core.Enums;
using Bit.iOS.Core.Views; using Bit.iOS.Core.Views;
using CoreGraphics; using CoreGraphics;
using Foundation; using Foundation;
using MobileCoreServices;
using Photos;
using UIKit; using UIKit;
namespace Bit.iOS.Services namespace Bit.iOS.Services
@ -19,13 +22,16 @@ namespace Bit.iOS.Services
public class DeviceActionService : IDeviceActionService public class DeviceActionService : IDeviceActionService
{ {
private readonly IStorageService _storageService; private readonly IStorageService _storageService;
private readonly IMessagingService _messagingService;
private Toast _toast; private Toast _toast;
private UIAlertController _progressAlert; private UIAlertController _progressAlert;
public DeviceActionService(IStorageService storageService) public DeviceActionService(
IStorageService storageService,
IMessagingService messagingService)
{ {
_storageService = storageService; _storageService = storageService;
_messagingService = messagingService;
} }
public DeviceType DeviceType => DeviceType.iOS; public DeviceType DeviceType => DeviceType.iOS;
@ -126,6 +132,45 @@ namespace Bit.iOS.Services
await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow); await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow);
} }
public Task SelectFileAsync()
{
var controller = GetVisibleViewController();
var picker = new UIDocumentMenuViewController(new string[] { UTType.Data }, UIDocumentPickerMode.Import);
picker.AddOption(AppResources.Camera, UIImage.FromBundle("camera"), UIDocumentMenuOrder.First, () =>
{
var imagePicker = new UIImagePickerController
{
SourceType = UIImagePickerControllerSourceType.Camera
};
imagePicker.FinishedPickingMedia += ImagePicker_FinishedPickingMedia;
imagePicker.Canceled += ImagePicker_Canceled;
controller.PresentModalViewController(imagePicker, true);
});
picker.AddOption(AppResources.Photos, UIImage.FromBundle("photo"), UIDocumentMenuOrder.First, () =>
{
var imagePicker = new UIImagePickerController
{
SourceType = UIImagePickerControllerSourceType.PhotoLibrary
};
imagePicker.FinishedPickingMedia += ImagePicker_FinishedPickingMedia;
imagePicker.Canceled += ImagePicker_Canceled;
controller.PresentModalViewController(imagePicker, true);
});
picker.DidPickDocumentPicker += (sender, e) =>
{
controller.PresentViewController(e.DocumentPicker, true, null);
e.DocumentPicker.DidPickDocument += DocumentPicker_DidPickDocument;
};
var root = UIApplication.SharedApplication.KeyWindow.RootViewController;
if(picker.PopoverPresentationController != null && root != null)
{
picker.PopoverPresentationController.SourceView = root.View;
picker.PopoverPresentationController.SourceRect = root.View.Bounds;
}
controller.PresentViewController(picker, true, null);
return Task.FromResult(0);
}
public Task<string> DisplayPromptAync(string title = null, string description = null, public Task<string> DisplayPromptAync(string title = null, string description = null,
string text = null, string okButtonText = null, string cancelButtonText = null) string text = null, string okButtonText = null, string cancelButtonText = null)
{ {
@ -152,6 +197,80 @@ namespace Bit.iOS.Services
return result.Task; return result.Task;
} }
private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
if(sender is UIImagePickerController picker)
{
string fileName = null;
if(e.Info.TryGetValue(UIImagePickerController.ReferenceUrl, out NSObject urlObj))
{
var result = PHAsset.FetchAssets(new NSUrl[] { (urlObj as NSUrl) }, null);
fileName = result?.firstObject?.ValueForKey(new NSString("filename"))?.ToString();
}
fileName = fileName ?? $"photo_{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.jpg";
var lowerFilename = fileName?.ToLowerInvariant();
byte[] data;
if(lowerFilename != null && (lowerFilename.EndsWith(".jpg") || lowerFilename.EndsWith(".jpeg")))
{
using(var imageData = e.OriginalImage.AsJPEG())
{
data = new byte[imageData.Length];
System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, data, 0,
Convert.ToInt32(imageData.Length));
}
}
else
{
using(var imageData = e.OriginalImage.AsPNG())
{
data = new byte[imageData.Length];
System.Runtime.InteropServices.Marshal.Copy(imageData.Bytes, data, 0,
Convert.ToInt32(imageData.Length));
}
}
SelectFileResult(data, fileName);
picker.DismissViewController(true, null);
}
}
private void ImagePicker_Canceled(object sender, EventArgs e)
{
if(sender is UIImagePickerController picker)
{
picker.DismissViewController(true, null);
}
}
private void DocumentPicker_DidPickDocument(object sender, UIDocumentPickedEventArgs e)
{
e.Url.StartAccessingSecurityScopedResource();
var doc = new UIDocument(e.Url);
var fileName = doc.LocalizedName;
if(string.IsNullOrWhiteSpace(fileName))
{
var path = doc.FileUrl?.ToString();
if(path != null)
{
path = WebUtility.UrlDecode(path);
var split = path.LastIndexOf('/');
fileName = path.Substring(split + 1);
}
}
var fileCoordinator = new NSFileCoordinator();
fileCoordinator.CoordinateRead(e.Url, NSFileCoordinatorReadingOptions.WithoutChanges,
out NSError error, (url) =>
{
var data = NSData.FromUrl(url).ToArray();
SelectFileResult(data, fileName ?? "unknown_file_name");
});
e.Url.StopAccessingSecurityScopedResource();
}
private void SelectFileResult(byte[] data, string fileName)
{
_messagingService.Send("selectFileResult", new Tuple<byte[], string>(data, fileName));
}
private UIViewController GetVisibleViewController(UIViewController controller = null) private UIViewController GetVisibleViewController(UIViewController controller = null)
{ {
controller = controller ?? UIApplication.SharedApplication.KeyWindow.RootViewController; controller = controller ?? UIApplication.SharedApplication.KeyWindow.RootViewController;