mirror of
https://github.com/bitwarden/android.git
synced 2024-12-24 18:08:26 +03:00
attachments page
This commit is contained in:
parent
34fd9b5842
commit
29b37219c2
16 changed files with 536 additions and 90 deletions
|
@ -100,6 +100,7 @@
|
|||
<Compile Include="Services\CryptoPrimitiveService.cs" />
|
||||
<Compile Include="Services\DeviceActionService.cs" />
|
||||
<Compile Include="Services\LocalizeService.cs" />
|
||||
<Compile Include="Utilities\AndroidHelpers.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidAsset Include="Assets\FontAwesome.ttf" />
|
||||
|
|
|
@ -2,6 +2,15 @@
|
|||
using Android.Content.PM;
|
||||
using Android.Runtime;
|
||||
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
|
||||
{
|
||||
|
@ -14,8 +23,14 @@ namespace Bit.Droid
|
|||
[Register("com.x8bit.bitwarden.MainActivity")]
|
||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||
{
|
||||
private IDeviceActionService _deviceActionService;
|
||||
private IMessagingService _messagingService;
|
||||
|
||||
protected override void OnCreate(Bundle savedInstanceState)
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||
|
||||
TabLayoutResource = Resource.Layout.Tabbar;
|
||||
ToolbarResource = Resource.Layout.Toolbar;
|
||||
|
||||
|
@ -25,11 +40,63 @@ namespace Bit.Droid
|
|||
LoadApplication(new App.App());
|
||||
}
|
||||
|
||||
public override void OnRequestPermissionsResult(int requestCode, string[] permissions,
|
||||
public async override void OnRequestPermissionsResult(int requestCode, string[] permissions,
|
||||
[GeneratedEnum] Permission[] grantResults)
|
||||
{
|
||||
Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, 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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,8 @@ namespace Bit.Droid
|
|||
var secureStorageService = new SecureStorageService();
|
||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService);
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService, messagingService,
|
||||
broadcasterService);
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Android;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
using Android.Provider;
|
||||
using Android.Support.V4.App;
|
||||
using Android.Support.V4.Content;
|
||||
using Android.Webkit;
|
||||
using Android.Widget;
|
||||
|
@ -19,13 +24,28 @@ namespace Bit.Droid.Services
|
|||
public class DeviceActionService : IDeviceActionService
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
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;
|
||||
_messagingService = messagingService;
|
||||
_broadcasterService = broadcasterService;
|
||||
|
||||
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
|
||||
{
|
||||
if(message.Command == "selectFileCameraPermissionDenied")
|
||||
{
|
||||
_cameraPermissionsDenied = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.Android;
|
||||
|
@ -39,7 +59,7 @@ namespace Bit.Droid.Services
|
|||
_toast = null;
|
||||
}
|
||||
_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();
|
||||
}
|
||||
|
||||
|
@ -149,6 +169,54 @@ namespace Bit.Droid.Services
|
|||
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,
|
||||
string text = null, string okButtonText = null, string cancelButtonText = null)
|
||||
{
|
||||
|
@ -217,5 +285,35 @@ namespace Bit.Droid.Services
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
30
src/Android/Utilities/AndroidHelpers.cs
Normal file
30
src/Android/Utilities/AndroidHelpers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ namespace Bit.App.Abstractions
|
|||
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||
bool CanOpenFile(string fileName);
|
||||
Task ClearCacheAsync();
|
||||
Task SelectFileAsync();
|
||||
Task<string> DisplayPromptAync(string title = null, string description = null, string text = null,
|
||||
string okButtonText = null, string cancelButtonText = null);
|
||||
}
|
||||
|
|
|
@ -99,11 +99,12 @@ namespace Bit.App.Pages
|
|||
_vm.AddField();
|
||||
}
|
||||
|
||||
private void Attachments_Clicked(object sender, System.EventArgs e)
|
||||
private async void Attachments_Clicked(object sender, System.EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
// await Navigation.PushModalAsync();
|
||||
var page = new AttachmentsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
x:Class="Bit.App.Pages.AttachmentsPage"
|
||||
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:controls="clr-namespace:Bit.App.Controls"
|
||||
x:DataType="pages:AttachmentsPageViewModel"
|
||||
|
@ -21,6 +22,7 @@
|
|||
<ResourceDictionary>
|
||||
<u:InverseBoolConverter x:Key="inverseBool" />
|
||||
<u:IsNotNullConverter x:Key="notNull" />
|
||||
<u:IsNullConverter x:Key="null" />
|
||||
</ResourceDictionary>
|
||||
</ContentPage.Resources>
|
||||
|
||||
|
@ -28,22 +30,30 @@
|
|||
<StackLayout Spacing="20">
|
||||
<StackLayout StyleClass="box">
|
||||
<StackLayout StyleClass="box-row"
|
||||
IsVisible="{Binding HasCollections, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoCollectionsToList}" />
|
||||
IsVisible="{Binding HasAttachments, Converter={StaticResource inverseBool}}">
|
||||
<Label Text="{u:I18n NoAttachments}" />
|
||||
</StackLayout>
|
||||
<controls:RepeaterView ItemsSource="{Binding Collections}" IsVisible="{Binding HasCollections}">
|
||||
<controls:RepeaterView ItemsSource="{Binding Attachments}" IsVisible="{Binding HasAttachments}">
|
||||
<controls:RepeaterView.ItemTemplate>
|
||||
<DataTemplate x:DataType="pages:CollectionViewModel">
|
||||
<DataTemplate x:DataType="views:AttachmentView">
|
||||
<StackLayout Spacing="0" Padding="0">
|
||||
<StackLayout StyleClass="box-row, box-row-switch">
|
||||
<StackLayout Orientation="Horizontal" StyleClass="box-row" Spacing="10">
|
||||
<Label
|
||||
Text="{Binding Collection.Name}"
|
||||
StyleClass="box-label, box-label-regular"
|
||||
HorizontalOptions="StartAndExpand" />
|
||||
<Switch
|
||||
IsToggled="{Binding Checked}"
|
||||
Text="{Binding FileName, Mode=OneWay}"
|
||||
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=""
|
||||
Command="{Binding BindingContext.DeleteAttachmentCommand, Source={x:Reference _page}}"
|
||||
CommandParameter="{Binding .}"
|
||||
VerticalOptions="Center" />
|
||||
</StackLayout>
|
||||
<BoxView StyleClass="box-row-separator" />
|
||||
</StackLayout>
|
||||
|
@ -51,6 +61,34 @@
|
|||
</controls:RepeaterView.ItemTemplate>
|
||||
</controls:RepeaterView>
|
||||
</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>
|
||||
</ScrollView>
|
||||
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
using Xamarin.Forms;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
public partial class AttachmentsPage : BaseContentPage
|
||||
{
|
||||
private AttachmentsPageViewModel _vm;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
public AttachmentsPage(string cipherId)
|
||||
{
|
||||
InitializeComponent();
|
||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||
_vm = BindingContext as AttachmentsPageViewModel;
|
||||
_vm.Page = this;
|
||||
_vm.CipherId = cipherId;
|
||||
|
@ -18,20 +23,38 @@ namespace Bit.App.Pages
|
|||
protected override async void 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()
|
||||
{
|
||||
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())
|
||||
{
|
||||
await _vm.SubmitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void ChooseFile_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
if(DoOnce())
|
||||
{
|
||||
await _vm.ChooseFileAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ using Bit.Core.Utilities;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xamarin.Forms;
|
||||
|
||||
namespace Bit.App.Pages
|
||||
{
|
||||
|
@ -15,65 +16,103 @@ namespace Bit.App.Pages
|
|||
{
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private CipherView _cipher;
|
||||
private Cipher _cipherDomain;
|
||||
private bool _hasCollections;
|
||||
private bool _hasAttachments;
|
||||
private bool _hasUpdatedKey;
|
||||
private bool _canAccessAttachments;
|
||||
private string _fileName;
|
||||
|
||||
public AttachmentsPageViewModel()
|
||||
{
|
||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||
_collectionService = ServiceContainer.Resolve<ICollectionService>("collectionService");
|
||||
Collections = new ExtendedObservableCollection<CollectionViewModel>();
|
||||
PageTitle = AppResources.Collections;
|
||||
_userService = ServiceContainer.Resolve<IUserService>("userService");
|
||||
Attachments = new ExtendedObservableCollection<AttachmentView>();
|
||||
DeleteAttachmentCommand = new Command<AttachmentView>(DeleteAsync);
|
||||
PageTitle = AppResources.Attachments;
|
||||
}
|
||||
|
||||
public string CipherId { get; set; }
|
||||
public ExtendedObservableCollection<CollectionViewModel> Collections { get; set; }
|
||||
public bool HasCollections
|
||||
public CipherView Cipher
|
||||
{
|
||||
get => _hasCollections;
|
||||
set => SetProperty(ref _hasCollections, value);
|
||||
get => _cipher;
|
||||
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);
|
||||
var collectionIds = _cipherDomain.CollectionIds;
|
||||
_cipher = await _cipherDomain.DecryptAsync();
|
||||
var allCollections = await _collectionService.GetAllDecryptedAsync();
|
||||
var collections = allCollections
|
||||
.Where(c => !c.ReadOnly && c.OrganizationId == _cipher.OrganizationId)
|
||||
.Select(c => new CollectionViewModel
|
||||
Cipher = await _cipherDomain.DecryptAsync();
|
||||
LoadAttachments();
|
||||
_hasUpdatedKey = await _cryptoService.HasEncKeyAsync();
|
||||
var canAccessPremium = await _userService.CanAccessPremiumAsync();
|
||||
_canAccessAttachments = canAccessPremium || Cipher.OrganizationId != null;
|
||||
if(!_canAccessAttachments)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
}
|
||||
else if(!_hasUpdatedKey)
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.UpdateKey,
|
||||
AppResources.FeatureUnavailable, AppResources.LearnMore, AppResources.Cancel);
|
||||
if(confirmed)
|
||||
{
|
||||
Collection = c,
|
||||
Checked = collectionIds.Contains(c.Id)
|
||||
}).ToList();
|
||||
Collections.ResetWithRange(collections);
|
||||
HasCollections = Collections.Any();
|
||||
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/update-encryption-key/");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SubmitAsync()
|
||||
{
|
||||
if(!Collections.Any(c => c.Checked))
|
||||
if(!_hasUpdatedKey)
|
||||
{
|
||||
await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection,
|
||||
AppResources.Ok);
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UpdateKey,
|
||||
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;
|
||||
}
|
||||
|
||||
_cipherDomain.CollectionIds = new HashSet<string>(
|
||||
Collections.Where(c => c.Checked).Select(c => c.Collection.Id));
|
||||
try
|
||||
{
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
|
||||
await _cipherService.SaveCollectionsWithServerAsync(_cipherDomain);
|
||||
_cipherDomain = await _cipherService.SaveAttachmentRawWithServerAsync(
|
||||
_cipherDomain, FileName, FileData);
|
||||
Cipher = await _cipherDomain.DecryptAsync();
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.ItemUpdated);
|
||||
await Page.Navigation.PopModalAsync();
|
||||
_platformUtilsService.ShowToast("success", null, AppResources.AttachementAdded);
|
||||
LoadAttachments();
|
||||
FileData = null;
|
||||
FileName = null;
|
||||
return true;
|
||||
}
|
||||
catch(ApiException e)
|
||||
|
@ -83,5 +122,44 @@ namespace Bit.App.Pages
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,6 +112,11 @@ namespace Bit.App.Pages
|
|||
await _deviceActionService.HideLoadingAsync();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -105,11 +105,12 @@ namespace Bit.App.Pages
|
|||
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())
|
||||
{
|
||||
// await Navigation.PushModalAsync();
|
||||
var page = new AttachmentsPage(_vm.CipherId);
|
||||
await Navigation.PushModalAsync(new NavigationPage(page));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,5 +12,7 @@
|
|||
public static string LastFileCacheClearKey = "lastFileCacheClear";
|
||||
public static string AccessibilityAutofillPasswordFieldKey = "accessibilityAutofillPasswordField";
|
||||
public static string AccessibilityAutofillPersistNotificationKey = "accessibilityAutofillPersistNotification";
|
||||
public const int SelectFileRequestCode = 42;
|
||||
public const int SelectFilePermissionRequestCode = 43;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -249,7 +249,7 @@ namespace Bit.Core.Services
|
|||
public Task DeleteCipherAttachmentAsync(string id, string attachmentId)
|
||||
{
|
||||
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,
|
||||
|
|
|
@ -519,22 +519,11 @@ namespace Bit.Core.Services
|
|||
var encFileName = await _cryptoService.EncryptAsync(filename, key);
|
||||
var dataEncKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
var encData = await _cryptoService.EncryptToBytesAsync(data, dataEncKey.Item1);
|
||||
|
||||
CipherResponse response;
|
||||
try
|
||||
{
|
||||
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
|
||||
{
|
||||
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
|
||||
fd.Add(new StringContent(string.Empty), "key", dataEncKey.Item2.EncryptedString);
|
||||
response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd);
|
||||
}
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
throw new Exception(e.Error.GetSingleMessage());
|
||||
}
|
||||
|
||||
var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks);
|
||||
var fd = new MultipartFormDataContent(boundary);
|
||||
fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key");
|
||||
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
|
||||
var response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd);
|
||||
var userId = await _userService.GetUserIdAsync();
|
||||
var cData = new CipherData(response, userId, cipher.CollectionIds);
|
||||
await UpsertAsync(cData);
|
||||
|
@ -670,12 +659,13 @@ namespace Bit.Core.Services
|
|||
try
|
||||
{
|
||||
await _apiService.DeleteCipherAttachmentAsync(id, attachmentId);
|
||||
await DeleteAttachmentAsync(id, attachmentId);
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
throw new Exception(e.Error.GetSingleMessage());
|
||||
await DeleteAttachmentAsync(id, attachmentId);
|
||||
throw e;
|
||||
}
|
||||
await DeleteAttachmentAsync(id, attachmentId);
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId)
|
||||
|
@ -716,20 +706,11 @@ namespace Bit.Core.Services
|
|||
var encFileName = await _cryptoService.EncryptAsync(attachmentView.FileName, key);
|
||||
var dataEncKey = await _cryptoService.MakeEncKeyAsync(key);
|
||||
var encData = await _cryptoService.EncryptToBytesAsync(decBytes, dataEncKey.Item1);
|
||||
|
||||
try
|
||||
{
|
||||
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch(ApiException e)
|
||||
{
|
||||
throw new Exception(e.Error.GetSingleMessage());
|
||||
}
|
||||
var boundary = string.Concat("--BWMobileFormBoundary", DateTime.UtcNow.Ticks);
|
||||
var fd = new MultipartFormDataContent(boundary);
|
||||
fd.Add(new StringContent(dataEncKey.Item2.EncryptedString), "key");
|
||||
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
|
||||
await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId);
|
||||
}
|
||||
|
||||
private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
|
@ -12,6 +13,8 @@ using Bit.Core.Enums;
|
|||
using Bit.iOS.Core.Views;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using MobileCoreServices;
|
||||
using Photos;
|
||||
using UIKit;
|
||||
|
||||
namespace Bit.iOS.Services
|
||||
|
@ -19,13 +22,16 @@ namespace Bit.iOS.Services
|
|||
public class DeviceActionService : IDeviceActionService
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
private readonly IMessagingService _messagingService;
|
||||
private Toast _toast;
|
||||
private UIAlertController _progressAlert;
|
||||
|
||||
public DeviceActionService(IStorageService storageService)
|
||||
public DeviceActionService(
|
||||
IStorageService storageService,
|
||||
IMessagingService messagingService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
_messagingService = messagingService;
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.iOS;
|
||||
|
@ -126,6 +132,45 @@ namespace Bit.iOS.Services
|
|||
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,
|
||||
string text = null, string okButtonText = null, string cancelButtonText = null)
|
||||
{
|
||||
|
@ -152,6 +197,80 @@ namespace Bit.iOS.Services
|
|||
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)
|
||||
{
|
||||
controller = controller ?? UIApplication.SharedApplication.KeyWindow.RootViewController;
|
||||
|
|
Loading…
Reference in a new issue