attachments on view page abd device actions

This commit is contained in:
Kyle Spearrin 2019-04-29 16:09:27 -04:00
parent a5bf16a415
commit 1f4bdb04ee
10 changed files with 264 additions and 4 deletions

View File

@ -46,7 +46,6 @@ namespace Bit.Droid
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db")); var liteDbStorage = new LiteDbStorageService(Path.Combine(documentsPath, "bitwarden.db"));
liteDbStorage.InitAsync(); liteDbStorage.InitAsync();
var deviceActionService = new DeviceActionService();
var localizeService = new LocalizeService(); var localizeService = new LocalizeService();
var broadcasterService = new BroadcasterService(); var broadcasterService = new BroadcasterService();
var messagingService = new MobileBroadcasterMessagingService(broadcasterService); var messagingService = new MobileBroadcasterMessagingService(broadcasterService);
@ -54,6 +53,7 @@ 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 platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService, var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, messagingService,
broadcasterService); broadcasterService);

View File

@ -26,5 +26,14 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:networkSecurityConfig="@xml/network_security_config"> 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> </application>
</manifest> </manifest>

View File

@ -1,6 +1,14 @@
using System.Threading.Tasks; using System;
using System.IO;
using System.Threading.Tasks;
using Android.App; using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Support.V4.Content;
using Android.Webkit;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Plugin.CurrentActivity; using Plugin.CurrentActivity;
@ -8,9 +16,16 @@ namespace Bit.Droid.Services
{ {
public class DeviceActionService : IDeviceActionService public class DeviceActionService : IDeviceActionService
{ {
private readonly IStorageService _storageService;
private ProgressDialog _progressDialog; private ProgressDialog _progressDialog;
private Android.Widget.Toast _toast; private Android.Widget.Toast _toast;
public DeviceActionService(IStorageService storageService)
{
_storageService = storageService;
}
public DeviceType DeviceType => DeviceType.Android; public DeviceType DeviceType => DeviceType.Android;
public void Toast(string text, bool longDuration = false) public void Toast(string text, bool longDuration = false)
@ -61,5 +76,100 @@ namespace Bit.Droid.Services
} }
return Task.FromResult(0); 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;
}
}
} }
} }

View File

@ -10,5 +10,8 @@ namespace Bit.App.Abstractions
bool LaunchApp(string appName); bool LaunchApp(string appName);
Task ShowLoadingAsync(string text); Task ShowLoadingAsync(string text);
Task HideLoadingAsync(); Task HideLoadingAsync();
bool OpenFile(byte[] fileData, string id, string fileName);
bool CanOpenFile(string fileName);
Task ClearCacheAsync();
} }
} }

View File

@ -73,6 +73,7 @@ namespace Bit.App.Pages
nameof(IsSecureNote), nameof(IsSecureNote),
nameof(ShowUris), nameof(ShowUris),
nameof(ShowFields), nameof(ShowFields),
nameof(ShowAttachments),
nameof(ShowTotp), nameof(ShowTotp),
nameof(ColoredPassword), nameof(ColoredPassword),
nameof(ShowIdentityAddress), nameof(ShowIdentityAddress),
@ -253,10 +254,44 @@ namespace Bit.App.Pages
if(Cipher.OrganizationId == null && !CanAccessPremium) if(Cipher.OrganizationId == null && !CanAccessPremium)
{ {
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired); 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 _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
await Task.Delay(2000); // TODO: download try
{
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(attachment, Cipher.OrganizationId);
await _deviceActionService.HideLoadingAsync(); 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) private async void CopyAsync(string id, string text = null)

View File

@ -35,5 +35,6 @@ namespace Bit.Core.Abstractions
Task UpdateLastUsedDateAsync(string id); Task UpdateLastUsedDateAsync(string id);
Task UpsertAsync(CipherData cipher); Task UpsertAsync(CipherData cipher);
Task UpsertAsync(List<CipherData> cipher); Task UpsertAsync(List<CipherData> cipher);
Task<byte[]> DownloadAndDecryptAttachmentAsync(AttachmentView attachment, string organizationId);
} }
} }

View File

@ -9,5 +9,6 @@
public static string DefaultUriMatch = "defaultUriMatch"; public static string DefaultUriMatch = "defaultUriMatch";
public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy"; public static string DisableAutoTotpCopyKey = "disableAutoTotpCopy";
public static string EnvironmentUrlsKey = "environmentUrls"; public static string EnvironmentUrlsKey = "environmentUrls";
public static string LastFileCacheClearKey = "lastFileCacheClear";
} }
} }

View File

@ -20,5 +20,17 @@ namespace Bit.Core.Models.View
public string SizeName { get; set; } public string SizeName { get; set; }
public string FileName { get; set; } public string FileName { get; set; }
public SymmetricCryptoKey Key { 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;
}
}
} }
} }

View File

@ -678,6 +678,27 @@ namespace Bit.Core.Services
await DeleteAttachmentAsync(id, attachmentId); 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 // Helpers
private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId, private async Task ShareAttachmentWithServerAsync(AttachmentView attachmentView, string cipherId,

View File

@ -1,9 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.iOS.Core.Views; using Bit.iOS.Core.Views;
using CoreGraphics; using CoreGraphics;
@ -14,9 +17,16 @@ namespace Bit.iOS.Services
{ {
public class DeviceActionService : IDeviceActionService public class DeviceActionService : IDeviceActionService
{ {
private readonly IStorageService _storageService;
private Toast _toast; private Toast _toast;
private UIAlertController _progressAlert; private UIAlertController _progressAlert;
public DeviceActionService(IStorageService storageService)
{
_storageService = storageService;
}
public DeviceType DeviceType => DeviceType.iOS; public DeviceType DeviceType => DeviceType.iOS;
public bool LaunchApp(string appName) public bool LaunchApp(string appName)
@ -82,6 +92,57 @@ namespace Bit.iOS.Services
return result.Task; 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() private UIViewController GetPresentedViewController()
{ {
var window = UIApplication.SharedApplication.KeyWindow; var window = UIApplication.SharedApplication.KeyWindow;
@ -99,5 +160,12 @@ namespace Bit.iOS.Services
return vc != null && (vc is UITabBarController || return vc != null && (vc is UITabBarController ||
(vc.ChildViewControllers?.Any(c => c is UITabBarController) ?? false)); (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");
}
} }
} }