mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
attachments on view page abd device actions
This commit is contained in:
parent
a5bf16a415
commit
1f4bdb04ee
10 changed files with 264 additions and 4 deletions
|
@ -46,7 +46,6 @@ namespace Bit.Droid
|
|||
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
|
||||
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
|
||||
liteDbStorage.InitAsync();
|
||||
var deviceActionService = new DeviceActionService();
|
||||
var localizeService = new LocalizeService();
|
||||
var broadcasterService = new BroadcasterService();
|
||||
var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
|
||||
|
@ -54,6 +53,7 @@ namespace Bit.Droid
|
|||
var secureStorageService = new SecureStorageService();
|
||||
var cryptoPrimitiveService = new CryptoPrimitiveService();
|
||||
var mobileStorageService = new MobileStorageService(preferencesStorage, liteDbStorage);
|
||||
var deviceActionService = new DeviceActionService(mobileStorageService);
|
||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
|
||||
broadcasterService);
|
||||
|
||||
|
|
|
@ -26,5 +26,14 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="com.x8bit.bitwarden.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Support.V4.Content;
|
||||
using Android.Webkit;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Plugin.CurrentActivity;
|
||||
|
||||
|
@ -8,9 +16,16 @@ namespace Bit.Droid.Services
|
|||
{
|
||||
public class DeviceActionService : IDeviceActionService
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
private ProgressDialog _progressDialog;
|
||||
private Android.Widget.Toast _toast;
|
||||
|
||||
public DeviceActionService(IStorageService storageService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.Android;
|
||||
|
||||
public void Toast(string text, bool longDuration = false)
|
||||
|
@ -61,5 +76,100 @@ namespace Bit.Droid.Services
|
|||
}
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||
{
|
||||
if(!CanOpenFile(fileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var cachePath = activity.CacheDir;
|
||||
var filePath = Path.Combine(cachePath.Path, fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var file = new Java.IO.File(cachePath, fileName);
|
||||
if(!file.IsFile)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
var uri = FileProvider.GetUriForFile(activity.ApplicationContext,
|
||||
"com.x8bit.bitwarden.fileprovider", file);
|
||||
intent.SetDataAndType(uri, mimeType);
|
||||
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
|
||||
activity.StartActivity(intent);
|
||||
return true;
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CanOpenFile(string fileName)
|
||||
{
|
||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||
if(extension == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||
if(mimeType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||
var intent = new Intent(Intent.ActionView);
|
||||
intent.SetType(mimeType);
|
||||
var activities = activity.PackageManager.QueryIntentActivities(intent, PackageInfoFlags.MatchDefaultOnly);
|
||||
return (activities?.Count ?? 0) > 0;
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
||||
await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow);
|
||||
}
|
||||
catch(Exception) { }
|
||||
}
|
||||
|
||||
private bool DeleteDir(Java.IO.File dir)
|
||||
{
|
||||
if(dir != null && dir.IsDirectory)
|
||||
{
|
||||
var children = dir.List();
|
||||
for(int i = 0; i < children.Length; i++)
|
||||
{
|
||||
var success = DeleteDir(new Java.IO.File(dir, children[i]));
|
||||
if(!success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return dir.Delete();
|
||||
}
|
||||
else if(dir != null && dir.IsFile)
|
||||
{
|
||||
return dir.Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,5 +10,8 @@ namespace Bit.App.Abstractions
|
|||
bool LaunchApp(string appName);
|
||||
Task ShowLoadingAsync(string text);
|
||||
Task HideLoadingAsync();
|
||||
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||
bool CanOpenFile(string fileName);
|
||||
Task ClearCacheAsync();
|
||||
}
|
||||
}
|
|
@ -73,6 +73,7 @@ namespace Bit.App.Pages
|
|||
nameof(IsSecureNote),
|
||||
nameof(ShowUris),
|
||||
nameof(ShowFields),
|
||||
nameof(ShowAttachments),
|
||||
nameof(ShowTotp),
|
||||
nameof(ColoredPassword),
|
||||
nameof(ShowIdentityAddress),
|
||||
|
@ -253,10 +254,44 @@ namespace Bit.App.Pages
|
|||
if(Cipher.OrganizationId == null && !CanAccessPremium)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
||||
return;
|
||||
}
|
||||
if(attachment.FileSize >= 10485760) // 10 MB
|
||||
{
|
||||
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
||||
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
||||
AppResources.Yes, AppResources.No);
|
||||
if(!confirmed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!_deviceActionService.CanOpenFile(attachment.FileName))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||
return;
|
||||
}
|
||||
|
||||
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
||||
await Task.Delay(2000); // TODO: download
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
try
|
||||
{
|
||||
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment, Cipher.OrganizationId);
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
if(data == null)
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToDownloadFile);
|
||||
return;
|
||||
}
|
||||
if(!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName))
|
||||
{
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _deviceActionService.HideLoadingAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async void CopyAsync(string id, string text = null)
|
||||
|
|
|
@ -35,5 +35,6 @@ namespace Bit.Core.Abstractions
|
|||
Task UpdateLastUsedDateAsync(string id);
|
||||
Task UpsertAsync(CipherData cipher);
|
||||
Task UpsertAsync(List<CipherData> cipher);
|
||||
Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
|
||||
}
|
||||
}
|
|
@ -9,5 +9,6 @@
|
|||
public static string DefaultUriMatch = "defaultUriMatch";
|
||||
public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy";
|
||||
public static string EnvironmentUrlsKey = "environmentUrls";
|
||||
public static string LastFileCacheClearKey = "lastFileCacheClear";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,5 +20,17 @@ namespace Bit.Core.Models.View
|
|||
public string SizeName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public SymmetricCryptoKey Key { get; set; }
|
||||
|
||||
public long FileSize
|
||||
{
|
||||
get
|
||||
{
|
||||
if(!string.IsNullOrWhiteSpace(Size) && long.TryParse(Size, out var s))
|
||||
{
|
||||
return s;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -678,6 +678,27 @@ namespace Bit.Core.Services
|
|||
await DeleteAttachmentAsync(id, attachmentId);
|
||||
}
|
||||
|
||||
public async Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(new Uri(attachment.Url));
|
||||
if(!response.IsSuccessStatusCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var data = await response.Content.ReadAsByteArrayAsync();
|
||||
if(data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var key = attachment.Key ?? await _cryptoService.GetOrgKeyAsync(organizationId);
|
||||
return await _cryptoService.DecryptFromBytesAsync(data, key);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.iOS.Core.Views;
|
||||
using CoreGraphics;
|
||||
|
@ -14,9 +17,16 @@ namespace Bit.iOS.Services
|
|||
{
|
||||
public class DeviceActionService : IDeviceActionService
|
||||
{
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
private Toast _toast;
|
||||
private UIAlertController _progressAlert;
|
||||
|
||||
public DeviceActionService(IStorageService storageService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
public DeviceType DeviceType => DeviceType.iOS;
|
||||
|
||||
public bool LaunchApp(string appName)
|
||||
|
@ -82,6 +92,57 @@ namespace Bit.iOS.Services
|
|||
return result.Task;
|
||||
}
|
||||
|
||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||
{
|
||||
var filePath = Path.Combine(GetTempPath(), fileName);
|
||||
File.WriteAllBytes(filePath, fileData);
|
||||
var url = NSUrl.FromFilename(filePath);
|
||||
var viewer = UIDocumentInteractionController.FromUrl(url);
|
||||
var controller = GetVisibleViewController();
|
||||
var rect = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad ?
|
||||
new CGRect(100, 5, 320, 320) : controller.View.Frame;
|
||||
return viewer.PresentOpenInMenu(rect, controller.View, true);
|
||||
}
|
||||
|
||||
public bool CanOpenFile(string fileName)
|
||||
{
|
||||
// Not sure of a way to check this ahead of time on iOS
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task ClearCacheAsync()
|
||||
{
|
||||
var url = new NSUrl(GetTempPath());
|
||||
var tmpFiles = NSFileManager.DefaultManager.GetDirectoryContent(url, null,
|
||||
NSDirectoryEnumerationOptions.SkipsHiddenFiles, out NSError error);
|
||||
if(error == null && tmpFiles.Length > 0)
|
||||
{
|
||||
foreach(var item in tmpFiles)
|
||||
{
|
||||
NSFileManager.DefaultManager.Remove(item, out NSError itemError);
|
||||
}
|
||||
}
|
||||
await _storageService.SaveAsync(Constants.LastFileCacheClearKey, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
private UIViewController GetVisibleViewController(UIViewController controller = null)
|
||||
{
|
||||
controller = controller ?? UIApplication.SharedApplication.KeyWindow.RootViewController;
|
||||
if(controller.PresentedViewController == null)
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
if(controller.PresentedViewController is UINavigationController)
|
||||
{
|
||||
return ((UINavigationController)controller.PresentedViewController).VisibleViewController;
|
||||
}
|
||||
if(controller.PresentedViewController is UITabBarController)
|
||||
{
|
||||
return ((UITabBarController)controller.PresentedViewController).SelectedViewController;
|
||||
}
|
||||
return GetVisibleViewController(controller.PresentedViewController);
|
||||
}
|
||||
|
||||
private UIViewController GetPresentedViewController()
|
||||
{
|
||||
var window = UIApplication.SharedApplication.KeyWindow;
|
||||
|
@ -99,5 +160,12 @@ namespace Bit.iOS.Services
|
|||
return vc != null && (vc is UITabBarController ||
|
||||
(vc.ChildViewControllers?.Any(c => c is UITabBarController) ?? false));
|
||||
}
|
||||
|
||||
// ref: //https://developer.xamarin.com/guides/ios/application_fundamentals/working_with_the_file_system/
|
||||
public string GetTempPath()
|
||||
{
|
||||
var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
return Path.Combine(documents, "..", "tmp");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue