mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +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\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" />
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
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);
|
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 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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
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 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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=""
|
||||||
|
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>
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
{
|
||||||
|
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,
|
_platformUtilsService.LaunchUri("https://help.bitwarden.com/article/update-encryption-key/");
|
||||||
Checked = collectionIds.Contains(c.Id)
|
}
|
||||||
}).ToList();
|
}
|
||||||
Collections.ResetWithRange(collections);
|
|
||||||
HasCollections = Collections.Any();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
{
|
fd.Add(new StreamContent(new MemoryStream(encData)), "data", encFileName.EncryptedString);
|
||||||
using(var fd = new MultipartFormDataContent(string.Concat("Upload----", DateTime.UtcNow)))
|
var response = await _apiService.PostCipherAttachmentAsync(cipher.Id, fd);
|
||||||
{
|
|
||||||
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 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);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
await DeleteAttachmentAsync(id, attachmentId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId)
|
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 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);
|
||||||
{
|
await _apiService.PostShareCipherAttachmentAsync(cipherId, attachmentView.Id, fd, organizationId);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri,
|
private bool CheckDefaultUriMatch(CipherView cipher, LoginUriView loginUri,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue