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 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);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
await _deviceActionService.HideLoadingAsync();
|
{
|
||||||
|
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)
|
private async void CopyAsync(string id, string text = null)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue