diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index ffdb7370d..3e6672900 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -230,15 +230,8 @@ namespace Bit.Droid.Services string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension); if(mimeType == null) { - if(extension == "json") - { - // Explicit support for json since older versions of Android don't recognize the extension - mimeType = "text/json"; - } - else - { - return false; - } + // Unable to identify so fall back to generic "any" type + mimeType = "*/*"; } var intent = new Intent(Intent.ActionCreateDocument); diff --git a/src/App/Pages/Vault/ViewPage.xaml.cs b/src/App/Pages/Vault/ViewPage.xaml.cs index e2a2051ad..bc0c81c0d 100644 --- a/src/App/Pages/Vault/ViewPage.xaml.cs +++ b/src/App/Pages/Vault/ViewPage.xaml.cs @@ -1,4 +1,5 @@ -using Bit.App.Resources; +using System; +using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Utilities; using System.Collections.Generic; @@ -76,6 +77,18 @@ namespace Bit.App.Pages } }); } + else if(message.Command == "selectSaveFileResult") + { + Device.BeginInvokeOnMainThread(() => + { + var data = message.Data as Tuple; + if(data == null) + { + return; + } + _vm.SaveFileSelected(data.Item1, data.Item2); + }); + } }); await LoadOnAppearedAsync(_scrollView, true, async () => { diff --git a/src/App/Pages/Vault/ViewPageViewModel.cs b/src/App/Pages/Vault/ViewPageViewModel.cs index 8e984d4c2..6a2b05110 100644 --- a/src/App/Pages/Vault/ViewPageViewModel.cs +++ b/src/App/Pages/Vault/ViewPageViewModel.cs @@ -34,6 +34,8 @@ namespace Bit.App.Pages private bool _totpLow; private DateTime? _totpInterval = null; private string _previousCipherId; + private byte[] _attachmentData; + private string _attachmentFilename; public ViewPageViewModel() { @@ -405,10 +407,19 @@ namespace Bit.App.Pages return; } } + + var canOpenFile = true; if(!_deviceActionService.CanOpenFile(attachment.FileName)) { - await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile); - return; + if(Device.RuntimePlatform == Device.iOS) + { + // iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false + // for any reason we want to be sure to catch it here. + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile); + return; + } + + canOpenFile = false; } await _deviceActionService.ShowLoadingAsync(AppResources.Downloading); @@ -421,10 +432,23 @@ namespace Bit.App.Pages await _platformUtilsService.ShowDialogAsync(AppResources.UnableToDownloadFile); return; } - if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName)) + + if(Device.RuntimePlatform == Device.Android) { - await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile); - return; + if(canOpenFile) + { + // We can open this attachment directly, so give the user the option to open or save + PromptOpenOrSave(data, attachment); + } + else + { + // We can't open this attachment so go directly to save + SaveAttachment(data, attachment); + } + } + else + { + OpenAttachment(data, attachment); } } catch @@ -433,6 +457,59 @@ namespace Bit.App.Pages } } + public async void PromptOpenOrSave(byte[] data, AttachmentView attachment) + { + var selection = await Page.DisplayActionSheet(attachment.FileName, AppResources.Cancel, null, + AppResources.Open, AppResources.Save); + if(selection == AppResources.Open) + { + OpenAttachment(data, attachment); + } + else if(selection == AppResources.Save) + { + SaveAttachment(data, attachment); + } + } + + public async void OpenAttachment(byte[] data, AttachmentView attachment) + { + if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName)) + { + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile); + return; + } + } + + public async void SaveAttachment(byte[] data, AttachmentView attachment) + { + _attachmentData = data; + _attachmentFilename = attachment.FileName; + if(!_deviceActionService.SaveFile(_attachmentData, null, _attachmentFilename, null)) + { + ClearAttachmentData(); + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment); + } + } + + public async void SaveFileSelected(string contentUri, string filename) + { + if(_deviceActionService.SaveFile(_attachmentData, null, filename ?? _attachmentFilename, contentUri)) + { + ClearAttachmentData(); + _platformUtilsService.ShowToast("success", null, AppResources.SaveAttachmentSuccess); + return; + } + + ClearAttachmentData(); + await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment); + } + + private void ClearAttachmentData() + { + _attachmentData = null; + _attachmentFilename = null; + } + private async void CopyAsync(string id, string text = null) { string name = null; diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 2caea6fab..1cb7ef024 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2841,5 +2841,23 @@ namespace Bit.App.Resources { return ResourceManager.GetString("PasswordGeneratorPolicyInEffect", resourceCulture); } } + + public static string Open { + get { + return ResourceManager.GetString("Open", resourceCulture); + } + } + + public static string UnableToSaveAttachment { + get { + return ResourceManager.GetString("UnableToSaveAttachment", resourceCulture); + } + } + + public static string SaveAttachmentSuccess { + get { + return ResourceManager.GetString("SaveAttachmentSuccess", resourceCulture); + } + } } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 3ae080eba..c95d3e8cb 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1615,4 +1615,14 @@ One or more organization policies are affecting your generator settings + + Open + Button text for an open operation (verb). + + + There was a problem saving this attachment. If the problem persists, you can save it from the web vault. + + + Attachment saved successfully + \ No newline at end of file