mirror of
https://github.com/bitwarden/android.git
synced 2024-12-26 19:08:32 +03:00
[EC-519] Refactor Split DeviceActionService (#2081)
* EC-519 Refactored IDeviceActionService to be split into IFileService and IAutofillManager also some cleanups were made * EC-519 Fix format * EC-519 Fix merge to use the new AutofillHandler
This commit is contained in:
parent
d800e9a43e
commit
ba677a96aa
35 changed files with 883 additions and 798 deletions
|
@ -152,6 +152,8 @@
|
||||||
<Compile Include="Utilities\IntentExtensions.cs" />
|
<Compile Include="Utilities\IntentExtensions.cs" />
|
||||||
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
<Compile Include="Renderers\CustomPageRenderer.cs" />
|
||||||
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
||||||
|
<Compile Include="Services\FileService.cs" />
|
||||||
|
<Compile Include="Services\AutofillHandler.cs" />
|
||||||
<Compile Include="Constants.cs" />
|
<Compile Include="Constants.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -36,6 +36,7 @@ namespace Bit.Droid
|
||||||
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
|
||||||
{
|
{
|
||||||
private IDeviceActionService _deviceActionService;
|
private IDeviceActionService _deviceActionService;
|
||||||
|
private IFileService _fileService;
|
||||||
private IMessagingService _messagingService;
|
private IMessagingService _messagingService;
|
||||||
private IBroadcasterService _broadcasterService;
|
private IBroadcasterService _broadcasterService;
|
||||||
private IStateService _stateService;
|
private IStateService _stateService;
|
||||||
|
@ -59,6 +60,7 @@ namespace Bit.Droid
|
||||||
StrictMode.SetThreadPolicy(policy);
|
StrictMode.SetThreadPolicy(policy);
|
||||||
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
_broadcasterService = ServiceContainer.Resolve<IBroadcasterService>("broadcasterService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
|
@ -217,7 +219,7 @@ namespace Bit.Droid
|
||||||
{
|
{
|
||||||
_messagingService.Send("selectFileCameraPermissionDenied");
|
_messagingService.Send("selectFileCameraPermissionDenied");
|
||||||
}
|
}
|
||||||
await _deviceActionService.SelectFileAsync();
|
await _fileService.SelectFileAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -139,8 +139,9 @@ namespace Bit.Droid
|
||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var clipboardService = new ClipboardService(stateService);
|
var clipboardService = new ClipboardService(stateService);
|
||||||
var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
|
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
||||||
broadcasterService, () => ServiceContainer.Resolve<IEventService>("eventService"));
|
var fileService = new FileService(stateService, broadcasterService);
|
||||||
|
var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, new LazyResolve<IEventService>());
|
||||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
||||||
messagingService, broadcasterService);
|
messagingService, broadcasterService);
|
||||||
var biometricService = new BiometricService();
|
var biometricService = new BiometricService();
|
||||||
|
@ -159,6 +160,8 @@ namespace Bit.Droid
|
||||||
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
||||||
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
||||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||||
|
ServiceContainer.Register<IFileService>(fileService);
|
||||||
|
ServiceContainer.Register<IAutofillHandler>(autofillHandler);
|
||||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||||
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
ServiceContainer.Register<ICryptoFunctionService>("cryptoFunctionService", cryptoFunctionService);
|
||||||
|
|
210
src/Android/Services/AutofillHandler.cs
Normal file
210
src/Android/Services/AutofillHandler.cs
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Android.App;
|
||||||
|
using Android.App.Assist;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Provider;
|
||||||
|
using Android.Views.Autofill;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Droid.Autofill;
|
||||||
|
using Plugin.CurrentActivity;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Services
|
||||||
|
{
|
||||||
|
public class AutofillHandler : IAutofillHandler
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IMessagingService _messagingService;
|
||||||
|
private readonly IClipboardService _clipboardService;
|
||||||
|
private readonly LazyResolve<IEventService> _eventService;
|
||||||
|
|
||||||
|
public AutofillHandler(IStateService stateService,
|
||||||
|
IMessagingService messagingService,
|
||||||
|
IClipboardService clipboardService,
|
||||||
|
LazyResolve<IEventService> eventService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_messagingService = messagingService;
|
||||||
|
_clipboardService = clipboardService;
|
||||||
|
_eventService = eventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillServiceEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var afm = (AutofillManager)activity.GetSystemService(
|
||||||
|
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
||||||
|
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SupportsAutofillService()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
||||||
|
var manager = activity.GetSystemService(type) as AutofillManager;
|
||||||
|
return manager.IsAutofillSupported;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Autofill(CipherView cipher)
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
if (activity == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
||||||
|
{
|
||||||
|
if (cipher == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var structure = activity.Intent.GetParcelableExtra(
|
||||||
|
AutofillManager.ExtraAssistStructure) as AssistStructure;
|
||||||
|
if (structure == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var parser = new Parser(structure, activity.ApplicationContext);
|
||||||
|
parser.Parse();
|
||||||
|
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Canceled);
|
||||||
|
activity.Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var task = CopyTotpAsync(cipher);
|
||||||
|
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
|
||||||
|
var replyIntent = new Intent();
|
||||||
|
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
||||||
|
activity.SetResult(Result.Ok, replyIntent);
|
||||||
|
activity.Finish();
|
||||||
|
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var data = new Intent();
|
||||||
|
if (cipher?.Login == null)
|
||||||
|
{
|
||||||
|
data.PutExtra("canceled", "true");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var task = CopyTotpAsync(cipher);
|
||||||
|
data.PutExtra("uri", cipher.Login.Uri);
|
||||||
|
data.PutExtra("username", cipher.Login.Username);
|
||||||
|
data.PutExtra("password", cipher.Login.Password);
|
||||||
|
}
|
||||||
|
if (activity.Parent == null)
|
||||||
|
{
|
||||||
|
activity.SetResult(Result.Ok, data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
activity.Parent.SetResult(Result.Ok, data);
|
||||||
|
}
|
||||||
|
activity.Finish();
|
||||||
|
_messagingService.Send("finishMainActivity");
|
||||||
|
if (cipher != null)
|
||||||
|
{
|
||||||
|
var eventTask = _eventService.Value.CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CloseAutofill()
|
||||||
|
{
|
||||||
|
Autofill(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillAccessibilityServiceRunning()
|
||||||
|
{
|
||||||
|
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
|
||||||
|
Settings.Secure.EnabledAccessibilityServices);
|
||||||
|
return Application.Context.PackageName != null &&
|
||||||
|
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillAccessibilityOverlayPermitted()
|
||||||
|
{
|
||||||
|
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void DisableAutofillService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
||||||
|
var manager = activity.GetSystemService(type) as AutofillManager;
|
||||||
|
manager.DisableAutofillServices();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AutofillServicesEnabled()
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
||||||
|
{
|
||||||
|
// Android 5-6: Both accessibility & overlay are required or nothing happens
|
||||||
|
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
|
||||||
|
{
|
||||||
|
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
|
||||||
|
return AutofillAccessibilityServiceRunning();
|
||||||
|
}
|
||||||
|
// Android 8+: Either autofill or accessibility is required
|
||||||
|
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyTotpAsync(CipherView cipher)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
||||||
|
{
|
||||||
|
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
||||||
|
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
||||||
|
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
||||||
|
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
||||||
|
if (totp != null)
|
||||||
|
{
|
||||||
|
await _clipboardService.CopyTextAsync(totp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Android;
|
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.App.Assist;
|
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Nfc;
|
using Android.Nfc;
|
||||||
|
@ -14,20 +9,13 @@ using Android.Provider;
|
||||||
using Android.Text;
|
using Android.Text;
|
||||||
using Android.Text.Method;
|
using Android.Text.Method;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Android.Views.Autofill;
|
|
||||||
using Android.Views.InputMethods;
|
using Android.Views.InputMethods;
|
||||||
using Android.Webkit;
|
|
||||||
using Android.Widget;
|
using Android.Widget;
|
||||||
using AndroidX.Core.App;
|
|
||||||
using AndroidX.Core.Content;
|
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Droid.Autofill;
|
|
||||||
using Bit.Droid.Utilities;
|
using Bit.Droid.Utilities;
|
||||||
using Plugin.CurrentActivity;
|
using Plugin.CurrentActivity;
|
||||||
|
|
||||||
|
@ -35,38 +23,20 @@ namespace Bit.Droid.Services
|
||||||
{
|
{
|
||||||
public class DeviceActionService : IDeviceActionService
|
public class DeviceActionService : IDeviceActionService
|
||||||
{
|
{
|
||||||
private readonly IClipboardService _clipboardService;
|
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IBroadcasterService _broadcasterService;
|
|
||||||
private readonly Func<IEventService> _eventServiceFunc;
|
|
||||||
private AlertDialog _progressDialog;
|
private AlertDialog _progressDialog;
|
||||||
object _progressDialogLock = new object();
|
object _progressDialogLock = new object();
|
||||||
|
|
||||||
private bool _cameraPermissionsDenied;
|
|
||||||
private Toast _toast;
|
private Toast _toast;
|
||||||
private string _userAgent;
|
private string _userAgent;
|
||||||
|
|
||||||
public DeviceActionService(
|
public DeviceActionService(
|
||||||
IClipboardService clipboardService,
|
|
||||||
IStateService stateService,
|
IStateService stateService,
|
||||||
IMessagingService messagingService,
|
IMessagingService messagingService)
|
||||||
IBroadcasterService broadcasterService,
|
|
||||||
Func<IEventService> eventServiceFunc)
|
|
||||||
{
|
{
|
||||||
_clipboardService = clipboardService;
|
|
||||||
_stateService = stateService;
|
_stateService = stateService;
|
||||||
_messagingService = messagingService;
|
_messagingService = messagingService;
|
||||||
_broadcasterService = broadcasterService;
|
|
||||||
_eventServiceFunc = eventServiceFunc;
|
|
||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
|
|
||||||
{
|
|
||||||
if (message.Command == "selectFileCameraPermissionDenied")
|
|
||||||
{
|
|
||||||
_cameraPermissionsDenied = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DeviceUserAgent
|
public string DeviceUserAgent
|
||||||
|
@ -212,184 +182,6 @@ namespace Bit.Droid.Services
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OpenFile(byte[] fileData, string id, string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var intent = BuildOpenFileIntent(fileData, fileName);
|
|
||||||
if (intent == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
activity.StartActivity(intent);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanOpenFile(string fileName)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
|
||||||
if (intent == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
|
||||||
PackageInfoFlags.MatchDefaultOnly);
|
|
||||||
return (activities?.Count ?? 0) > 0;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
|
||||||
{
|
|
||||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
|
||||||
if (extension == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
|
|
||||||
if (contentUri != null)
|
|
||||||
{
|
|
||||||
var uri = Android.Net.Uri.Parse(contentUri);
|
|
||||||
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
|
||||||
// Using java bufferedOutputStream due to this issue:
|
|
||||||
// https://github.com/xamarin/xamarin-android/issues/3498
|
|
||||||
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
|
||||||
javaStream.Write(fileData);
|
|
||||||
javaStream.Flush();
|
|
||||||
javaStream.Close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prompt for location to save file
|
|
||||||
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
|
||||||
if (extension == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
|
||||||
if (mimeType == null)
|
|
||||||
{
|
|
||||||
// Unable to identify so fall back to generic "any" type
|
|
||||||
mimeType = "*/*";
|
|
||||||
}
|
|
||||||
|
|
||||||
var intent = new Intent(Intent.ActionCreateDocument);
|
|
||||||
intent.SetType(mimeType);
|
|
||||||
intent.AddCategory(Intent.CategoryOpenable);
|
|
||||||
intent.PutExtra(Intent.ExtraTitle, fileName);
|
|
||||||
|
|
||||||
activity.StartActivityForResult(intent, Core.Constants.SaveFileRequestCode);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ClearCacheAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
|
||||||
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
|
||||||
}
|
|
||||||
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 file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
|
||||||
if (!file.Exists())
|
|
||||||
{
|
|
||||||
file.ParentFile.Mkdirs();
|
|
||||||
file.CreateNewFile();
|
|
||||||
}
|
|
||||||
var outputFileUri = FileProvider.GetUriForFile(activity,
|
|
||||||
"com.x8bit.bitwarden.fileprovider", 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, Core.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,
|
||||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||||
|
@ -467,34 +259,6 @@ namespace Bit.Droid.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisableAutofillService()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
|
||||||
var manager = activity.GetSystemService(type) as AutofillManager;
|
|
||||||
manager.DisableAutofillServices();
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillServicesEnabled()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.M)
|
|
||||||
{
|
|
||||||
// Android 5-6: Both accessibility & overlay are required or nothing happens
|
|
||||||
return AutofillAccessibilityServiceRunning() && AutofillAccessibilityOverlayPermitted();
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SdkInt == BuildVersionCodes.N)
|
|
||||||
{
|
|
||||||
// Android 7: Only accessibility is required (overlay is optional when using quick-action tile)
|
|
||||||
return AutofillAccessibilityServiceRunning();
|
|
||||||
}
|
|
||||||
// Android 8+: Either autofill or accessibility is required
|
|
||||||
return AutofillServiceEnabled() || AutofillAccessibilityServiceRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetBuildNumber()
|
public string GetBuildNumber()
|
||||||
{
|
{
|
||||||
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
|
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
|
||||||
|
@ -526,25 +290,6 @@ namespace Bit.Droid.Services
|
||||||
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsAutofillService()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var type = Java.Lang.Class.FromType(typeof(AutofillManager));
|
|
||||||
var manager = activity.GetSystemService(type) as AutofillManager;
|
|
||||||
return manager.IsAutofillSupported;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SystemMajorVersion()
|
public int SystemMajorVersion()
|
||||||
{
|
{
|
||||||
return (int)Build.VERSION.SdkInt;
|
return (int)Build.VERSION.SdkInt;
|
||||||
|
@ -635,112 +380,6 @@ namespace Bit.Droid.Services
|
||||||
title, cancel, destruction, buttons);
|
title, cancel, destruction, buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Autofill(CipherView cipher)
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
if (activity == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
|
||||||
{
|
|
||||||
if (cipher == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var structure = activity.Intent.GetParcelableExtra(
|
|
||||||
AutofillManager.ExtraAssistStructure) as AssistStructure;
|
|
||||||
if (structure == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var parser = new Parser(structure, activity.ApplicationContext);
|
|
||||||
parser.Parse();
|
|
||||||
if ((!parser.FieldCollection?.Fields?.Any() ?? true) || string.IsNullOrWhiteSpace(parser.Uri))
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var task = CopyTotpAsync(cipher);
|
|
||||||
var dataset = AutofillHelpers.BuildDataset(activity, parser.FieldCollection, new FilledItem(cipher));
|
|
||||||
var replyIntent = new Intent();
|
|
||||||
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, dataset);
|
|
||||||
activity.SetResult(Result.Ok, replyIntent);
|
|
||||||
activity.Finish();
|
|
||||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var data = new Intent();
|
|
||||||
if (cipher?.Login == null)
|
|
||||||
{
|
|
||||||
data.PutExtra("canceled", "true");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var task = CopyTotpAsync(cipher);
|
|
||||||
data.PutExtra("uri", cipher.Login.Uri);
|
|
||||||
data.PutExtra("username", cipher.Login.Username);
|
|
||||||
data.PutExtra("password", cipher.Login.Password);
|
|
||||||
}
|
|
||||||
if (activity.Parent == null)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Ok, data);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.Parent.SetResult(Result.Ok, data);
|
|
||||||
}
|
|
||||||
activity.Finish();
|
|
||||||
_messagingService.Send("finishMainActivity");
|
|
||||||
if (cipher != null)
|
|
||||||
{
|
|
||||||
var eventTask = _eventServiceFunc().CollectAsync(EventType.Cipher_ClientAutofilled, cipher.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseAutofill()
|
|
||||||
{
|
|
||||||
Autofill(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Background()
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
if (activity.Intent?.GetBooleanExtra("autofillFramework", false) ?? false)
|
|
||||||
{
|
|
||||||
activity.SetResult(Result.Canceled);
|
|
||||||
activity.Finish();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
activity.MoveTaskToBack(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityServiceRunning()
|
|
||||||
{
|
|
||||||
var enabledServices = Settings.Secure.GetString(Application.Context.ContentResolver,
|
|
||||||
Settings.Secure.EnabledAccessibilityServices);
|
|
||||||
return Application.Context.PackageName != null &&
|
|
||||||
(enabledServices?.Contains(Application.Context.PackageName) ?? false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityOverlayPermitted()
|
|
||||||
{
|
|
||||||
return Accessibility.AccessibilityHelpers.OverlayPermitted();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasAutofillService()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenAccessibilityOverlayPermissionSettings()
|
public void OpenAccessibilityOverlayPermissionSettings()
|
||||||
{
|
{
|
||||||
|
@ -771,25 +410,6 @@ namespace Bit.Droid.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AutofillServiceEnabled()
|
|
||||||
{
|
|
||||||
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
|
||||||
var afm = (AutofillManager)activity.GetSystemService(
|
|
||||||
Java.Lang.Class.FromType(typeof(AutofillManager)));
|
|
||||||
return afm.IsEnabled && afm.HasEnabledAutofillServices;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenAccessibilitySettings()
|
public void OpenAccessibilitySettings()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -848,61 +468,6 @@ namespace Bit.Droid.Services
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 },
|
|
||||||
Core.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Intent RateIntentForUrl(string url, Activity activity)
|
private Intent RateIntentForUrl(string url, Activity activity)
|
||||||
{
|
{
|
||||||
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
|
||||||
|
@ -920,24 +485,6 @@ namespace Bit.Droid.Services
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyTotpAsync(CipherView cipher)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(cipher?.Login?.Totp))
|
|
||||||
{
|
|
||||||
var autoCopyDisabled = await _stateService.GetDisableAutoTotpCopyAsync();
|
|
||||||
var canAccessPremium = await _stateService.CanAccessPremiumAsync();
|
|
||||||
if ((canAccessPremium || cipher.OrganizationUseTotp) && !autoCopyDisabled.GetValueOrDefault())
|
|
||||||
{
|
|
||||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
|
||||||
var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
|
|
||||||
if (totp != null)
|
|
||||||
{
|
|
||||||
await _clipboardService.CopyTextAsync(totp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public float GetSystemFontSizeScale()
|
public float GetSystemFontSizeScale()
|
||||||
{
|
{
|
||||||
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
|
||||||
|
|
278
src/Android/Services/FileService.cs
Normal file
278
src/Android/Services/FileService.cs
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Android;
|
||||||
|
using Android.Content;
|
||||||
|
using Android.Content.PM;
|
||||||
|
using Android.OS;
|
||||||
|
using Android.Provider;
|
||||||
|
using Android.Webkit;
|
||||||
|
using AndroidX.Core.App;
|
||||||
|
using AndroidX.Core.Content;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Plugin.CurrentActivity;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Services
|
||||||
|
{
|
||||||
|
public class FileService : IFileService
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IBroadcasterService _broadcasterService;
|
||||||
|
|
||||||
|
private bool _cameraPermissionsDenied;
|
||||||
|
|
||||||
|
public FileService(IStateService stateService, IBroadcasterService broadcasterService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_broadcasterService = broadcasterService;
|
||||||
|
|
||||||
|
_broadcasterService.Subscribe(nameof(FileService), (message) =>
|
||||||
|
{
|
||||||
|
if (message.Command == "selectFileCameraPermissionDenied")
|
||||||
|
{
|
||||||
|
_cameraPermissionsDenied = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OpenFile(byte[] fileData, string id, string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var intent = BuildOpenFileIntent(fileData, fileName);
|
||||||
|
if (intent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
activity.StartActivity(intent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanOpenFile(string fileName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
var intent = BuildOpenFileIntent(new byte[0], string.Concat("opentest_", fileName));
|
||||||
|
if (intent == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var activities = activity.PackageManager.QueryIntentActivities(intent,
|
||||||
|
PackageInfoFlags.MatchDefaultOnly);
|
||||||
|
return (activities?.Count ?? 0) > 0;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent BuildOpenFileIntent(byte[] fileData, string fileName)
|
||||||
|
{
|
||||||
|
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||||
|
if (mimeType == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var activity = (MainActivity)CrossCurrentActivity.Current.Activity;
|
||||||
|
|
||||||
|
if (contentUri != null)
|
||||||
|
{
|
||||||
|
var uri = Android.Net.Uri.Parse(contentUri);
|
||||||
|
var stream = activity.ContentResolver.OpenOutputStream(uri);
|
||||||
|
// Using java bufferedOutputStream due to this issue:
|
||||||
|
// https://github.com/xamarin/xamarin-android/issues/3498
|
||||||
|
var javaStream = new Java.IO.BufferedOutputStream(stream);
|
||||||
|
javaStream.Write(fileData);
|
||||||
|
javaStream.Flush();
|
||||||
|
javaStream.Close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt for location to save file
|
||||||
|
var extension = MimeTypeMap.GetFileExtensionFromUrl(fileName.Replace(' ', '_').ToLower());
|
||||||
|
if (extension == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string mimeType = MimeTypeMap.Singleton.GetMimeTypeFromExtension(extension);
|
||||||
|
if (mimeType == null)
|
||||||
|
{
|
||||||
|
// Unable to identify so fall back to generic "any" type
|
||||||
|
mimeType = "*/*";
|
||||||
|
}
|
||||||
|
|
||||||
|
var intent = new Intent(Intent.ActionCreateDocument);
|
||||||
|
intent.SetType(mimeType);
|
||||||
|
intent.AddCategory(Intent.CategoryOpenable);
|
||||||
|
intent.PutExtra(Intent.ExtraTitle, fileName);
|
||||||
|
|
||||||
|
activity.StartActivityForResult(intent, Core.Constants.SaveFileRequestCode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", ex.GetType(), ex.StackTrace);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearCacheAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeleteDir(CrossCurrentActivity.Current.Activity.CacheDir);
|
||||||
|
await _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
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 file = new Java.IO.File(activity.FilesDir, "temp_camera_photo.jpg");
|
||||||
|
if (!file.Exists())
|
||||||
|
{
|
||||||
|
file.ParentFile.Mkdirs();
|
||||||
|
file.CreateNewFile();
|
||||||
|
}
|
||||||
|
var outputFileUri = FileProvider.GetUriForFile(activity,
|
||||||
|
"com.x8bit.bitwarden.fileprovider", 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, Core.Constants.SelectFileRequestCode);
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DeleteDir(Java.IO.File dir)
|
||||||
|
{
|
||||||
|
if (dir is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dir.IsFile)
|
||||||
|
{
|
||||||
|
return dir.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
Core.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
|
@ -8,44 +7,32 @@ namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
string DeviceUserAgent { get; }
|
string DeviceUserAgent { get; }
|
||||||
DeviceType DeviceType { get; }
|
DeviceType DeviceType { get; }
|
||||||
|
int SystemMajorVersion();
|
||||||
|
string SystemModel();
|
||||||
|
string GetBuildNumber();
|
||||||
|
|
||||||
void Toast(string text, bool longDuration = false);
|
void Toast(string text, bool longDuration = false);
|
||||||
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 SaveFile(byte[] fileData, string id, string fileName, string contentUri);
|
|
||||||
bool CanOpenFile(string fileName);
|
|
||||||
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, bool numericKeyboard = false,
|
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
|
||||||
bool autofocus = true, bool password = false);
|
bool autofocus = true, bool password = false);
|
||||||
void RateApp();
|
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
||||||
|
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
||||||
|
|
||||||
bool SupportsFaceBiometric();
|
bool SupportsFaceBiometric();
|
||||||
Task<bool> SupportsFaceBiometricAsync();
|
Task<bool> SupportsFaceBiometricAsync();
|
||||||
bool SupportsNfc();
|
bool SupportsNfc();
|
||||||
bool SupportsCamera();
|
bool SupportsCamera();
|
||||||
bool SupportsAutofillService();
|
bool SupportsFido2();
|
||||||
int SystemMajorVersion();
|
|
||||||
string SystemModel();
|
bool LaunchApp(string appName);
|
||||||
Task<string> DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
|
void RateApp();
|
||||||
Task<string> DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
|
|
||||||
void Autofill(CipherView cipher);
|
|
||||||
void CloseAutofill();
|
|
||||||
void Background();
|
|
||||||
bool AutofillAccessibilityServiceRunning();
|
|
||||||
bool AutofillAccessibilityOverlayPermitted();
|
|
||||||
bool HasAutofillService();
|
|
||||||
bool AutofillServiceEnabled();
|
|
||||||
void DisableAutofillService();
|
|
||||||
bool AutofillServicesEnabled();
|
|
||||||
string GetBuildNumber();
|
|
||||||
void OpenAccessibilitySettings();
|
void OpenAccessibilitySettings();
|
||||||
void OpenAccessibilityOverlayPermissionSettings();
|
void OpenAccessibilityOverlayPermissionSettings();
|
||||||
void OpenAutofillSettings();
|
void OpenAutofillSettings();
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
void CloseMainApp();
|
void CloseMainApp();
|
||||||
bool SupportsFido2();
|
|
||||||
float GetSystemFontSizeScale();
|
float GetSystemFontSizeScale();
|
||||||
Task OnAccountSwitchCompleteAsync();
|
Task OnAccountSwitchCompleteAsync();
|
||||||
Task SetScreenCaptureAllowedAsync();
|
Task SetScreenCaptureAllowedAsync();
|
||||||
|
|
|
@ -28,6 +28,7 @@ namespace Bit.App
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
|
@ -49,6 +50,7 @@ namespace Bit.App
|
||||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||||
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
|
|
||||||
|
@ -301,7 +303,7 @@ namespace Bit.App
|
||||||
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
var lastClear = await _stateService.GetLastFileCacheClearAsync();
|
||||||
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
|
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
|
||||||
{
|
{
|
||||||
var task = Task.Run(() => _deviceActionService.ClearCacheAsync());
|
var task = Task.Run(() => _fileService.ClearCacheAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace Bit.App.Pages
|
||||||
public class SendAddEditPageViewModel : BaseViewModel
|
public class SendAddEditPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
|
@ -51,6 +52,7 @@ namespace Bit.App.Pages
|
||||||
public SendAddEditPageViewModel()
|
public SendAddEditPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
|
@ -292,7 +294,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
public async Task ChooseFileAsync()
|
public async Task ChooseFileAsync()
|
||||||
{
|
{
|
||||||
await _deviceActionService.SelectFileAsync();
|
await _fileService.SelectFileAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearExpirationDate()
|
public void ClearExpirationDate()
|
||||||
|
|
|
@ -144,7 +144,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
|
|
||||||
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
|
||||||
if (MainPage)
|
if (MainPage)
|
||||||
{
|
{
|
||||||
groupedSends.Add(new SendGroupingsPageListGroup(
|
groupedSends.Add(new SendGroupingsPageListGroup(
|
||||||
|
|
|
@ -12,6 +12,7 @@ namespace Bit.App.Pages
|
||||||
public class AutofillServicesPageViewModel : BaseViewModel
|
public class AutofillServicesPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly MobileI18nService _i18nService;
|
private readonly MobileI18nService _i18nService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
|
@ -26,6 +27,7 @@ namespace Bit.App.Pages
|
||||||
public AutofillServicesPageViewModel()
|
public AutofillServicesPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
@ -173,7 +175,7 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_deviceActionService.DisableAutofillService();
|
_autofillHandler.DisableAutofillService();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +190,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
public async Task ToggleAccessibilityAsync()
|
public async Task ToggleAccessibilityAsync()
|
||||||
{
|
{
|
||||||
if (!_deviceActionService.AutofillAccessibilityServiceRunning())
|
if (!_autofillHandler.AutofillAccessibilityServiceRunning())
|
||||||
{
|
{
|
||||||
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
|
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
|
||||||
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
|
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
|
||||||
|
@ -213,9 +215,9 @@ namespace Bit.App.Pages
|
||||||
public void UpdateEnabled()
|
public void UpdateEnabled()
|
||||||
{
|
{
|
||||||
AutofillServiceToggled =
|
AutofillServiceToggled =
|
||||||
_deviceActionService.HasAutofillService() && _deviceActionService.AutofillServiceEnabled();
|
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
|
||||||
AccessibilityToggled = _deviceActionService.AutofillAccessibilityServiceRunning();
|
AccessibilityToggled = _autofillHandler.AutofillAccessibilityServiceRunning();
|
||||||
DrawOverToggled = _deviceActionService.AutofillAccessibilityOverlayPermitted();
|
DrawOverToggled = _autofillHandler.AutofillAccessibilityOverlayPermitted();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateInlineAutofillToggledAsync()
|
private async Task UpdateInlineAutofillToggledAsync()
|
||||||
|
|
|
@ -16,6 +16,7 @@ namespace Bit.App.Pages
|
||||||
public class ExportVaultPageViewModel : BaseViewModel
|
public class ExportVaultPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly II18nService _i18nService;
|
private readonly II18nService _i18nService;
|
||||||
private readonly IExportService _exportService;
|
private readonly IExportService _exportService;
|
||||||
|
@ -39,6 +40,7 @@ namespace Bit.App.Pages
|
||||||
public ExportVaultPageViewModel()
|
public ExportVaultPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
|
||||||
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
|
_exportService = ServiceContainer.Resolve<IExportService>("exportService");
|
||||||
|
@ -182,7 +184,7 @@ namespace Bit.App.Pages
|
||||||
_defaultFilename = _exportService.GetFileName(null, fileFormat);
|
_defaultFilename = _exportService.GetFileName(null, fileFormat);
|
||||||
_exportResult = Encoding.UTF8.GetBytes(data);
|
_exportResult = Encoding.UTF8.GetBytes(data);
|
||||||
|
|
||||||
if (!_deviceActionService.SaveFile(_exportResult, null, _defaultFilename, null))
|
if (!_fileService.SaveFile(_exportResult, null, _defaultFilename, null))
|
||||||
{
|
{
|
||||||
ClearResult();
|
ClearResult();
|
||||||
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
|
||||||
|
@ -220,7 +222,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
public async void SaveFileSelected(string contentUri, string filename)
|
public async void SaveFileSelected(string contentUri, string filename)
|
||||||
{
|
{
|
||||||
if (_deviceActionService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
|
if (_fileService.SaveFile(_exportResult, null, filename ?? _defaultFilename, contentUri))
|
||||||
{
|
{
|
||||||
ClearResult();
|
ClearResult();
|
||||||
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));
|
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms.PlatformConfiguration;
|
using Xamarin.Forms.PlatformConfiguration;
|
||||||
|
@ -9,12 +10,12 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
public partial class OptionsPage : BaseContentPage
|
public partial class OptionsPage : BaseContentPage
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly OptionsPageViewModel _vm;
|
private readonly OptionsPageViewModel _vm;
|
||||||
|
|
||||||
public OptionsPage()
|
public OptionsPage()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_vm = BindingContext as OptionsPageViewModel;
|
_vm = BindingContext as OptionsPageViewModel;
|
||||||
_vm.Page = this;
|
_vm.Page = this;
|
||||||
|
@ -25,7 +26,7 @@ namespace Bit.App.Pages
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ToolbarItems.RemoveAt(0);
|
ToolbarItems.RemoveAt(0);
|
||||||
_vm.ShowAndroidAutofillSettings = _deviceActionService.SupportsAutofillService();
|
_vm.ShowAndroidAutofillSettings = _autofillHandler.SupportsAutofillService();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,6 +20,7 @@ namespace Bit.App.Pages
|
||||||
private readonly ICryptoService _cryptoService;
|
private readonly ICryptoService _cryptoService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly IEnvironmentService _environmentService;
|
private readonly IEnvironmentService _environmentService;
|
||||||
private readonly IMessagingService _messagingService;
|
private readonly IMessagingService _messagingService;
|
||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
|
@ -74,6 +75,7 @@ namespace Bit.App.Pages
|
||||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
|
@ -454,7 +456,7 @@ namespace Bit.App.Pages
|
||||||
else if (await _platformUtilsService.SupportsBiometricAsync())
|
else if (await _platformUtilsService.SupportsBiometricAsync())
|
||||||
{
|
{
|
||||||
_biometric = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
_biometric = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
_deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
|
Device.RuntimePlatform == Device.Android ? "." : null);
|
||||||
}
|
}
|
||||||
if (_biometric == current)
|
if (_biometric == current)
|
||||||
{
|
{
|
||||||
|
@ -485,7 +487,7 @@ namespace Bit.App.Pages
|
||||||
autofillItems.Add(new SettingsPageListItem
|
autofillItems.Add(new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = AppResources.AutofillServices,
|
Name = AppResources.AutofillServices,
|
||||||
SubLabel = _deviceActionService.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
|
SubLabel = _autofillHandler.AutofillServicesEnabled() ? AppResources.On : AppResources.Off,
|
||||||
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
ExecuteAsync = () => Page.Navigation.PushModalAsync(new NavigationPage(new AutofillServicesPage(Page as SettingsPage)))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ namespace Bit.App.Pages
|
||||||
public class AttachmentsPageViewModel : BaseViewModel
|
public class AttachmentsPageViewModel : BaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IFileService _fileService;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly ICryptoService _cryptoService;
|
private readonly ICryptoService _cryptoService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
|
@ -34,6 +35,7 @@ namespace Bit.App.Pages
|
||||||
public AttachmentsPageViewModel()
|
public AttachmentsPageViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||||
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
@ -156,7 +158,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
_vaultTimeoutService.DelayLockAndLogoutMs = 60000;
|
_vaultTimeoutService.DelayLockAndLogoutMs = 60000;
|
||||||
}
|
}
|
||||||
await _deviceActionService.SelectFileAsync();
|
await _fileService.SelectFileAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void DeleteAsync(AttachmentView attachment)
|
private async void DeleteAsync(AttachmentView attachment)
|
||||||
|
|
|
@ -21,6 +21,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
private readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||||
|
@ -37,6 +38,7 @@ namespace Bit.App.Pages
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
|
@ -232,7 +234,7 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
|
if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
|
||||||
{
|
{
|
||||||
_deviceActionService.Autofill(cipher);
|
_autofillHandler.Autofill(cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
private readonly IAuditService _auditService;
|
private readonly IAuditService _auditService;
|
||||||
protected readonly IDeviceActionService _deviceActionService;
|
protected readonly IDeviceActionService _deviceActionService;
|
||||||
|
protected readonly IFileService _fileService;
|
||||||
protected readonly ILogger _logger;
|
protected readonly ILogger _logger;
|
||||||
protected readonly IPlatformUtilsService _platformUtilsService;
|
protected readonly IPlatformUtilsService _platformUtilsService;
|
||||||
private CipherView _cipher;
|
private CipherView _cipher;
|
||||||
|
@ -22,6 +23,7 @@ namespace Bit.App.Pages
|
||||||
public BaseCipherViewModel()
|
public BaseCipherViewModel()
|
||||||
{
|
{
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
||||||
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace Bit.App.Pages
|
||||||
private readonly AppOptions _appOptions;
|
private readonly AppOptions _appOptions;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
|
|
||||||
|
@ -40,6 +41,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||||
|
|
||||||
|
@ -350,8 +352,8 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (Device.RuntimePlatform == Device.Android &&
|
else if (Device.RuntimePlatform == Device.Android &&
|
||||||
!_deviceActionService.AutofillAccessibilityServiceRunning() &&
|
!_autofillHandler.AutofillAccessibilityServiceRunning() &&
|
||||||
!_deviceActionService.AutofillServiceEnabled())
|
!_autofillHandler.AutofillServiceEnabled())
|
||||||
{
|
{
|
||||||
await DisplayAlert(AppResources.BitwardenAutofillService,
|
await DisplayAlert(AppResources.BitwardenAutofillService,
|
||||||
AppResources.BitwardenAutofillServiceAlert2, AppResources.Ok);
|
AppResources.BitwardenAutofillServiceAlert2, AppResources.Ok);
|
||||||
|
|
|
@ -28,6 +28,7 @@ namespace Bit.App.Pages
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly ICustomFieldItemFactory _customFieldItemFactory;
|
private readonly ICustomFieldItemFactory _customFieldItemFactory;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
|
|
||||||
private bool _showNotesSeparator;
|
private bool _showNotesSeparator;
|
||||||
private bool _showPassword;
|
private bool _showPassword;
|
||||||
|
@ -78,6 +79,7 @@ namespace Bit.App.Pages
|
||||||
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
_policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||||
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
|
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
|
||||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
|
|
||||||
GeneratePasswordCommand = new Command(GeneratePassword);
|
GeneratePasswordCommand = new Command(GeneratePassword);
|
||||||
TogglePasswordCommand = new Command(TogglePassword);
|
TogglePasswordCommand = new Command(TogglePassword);
|
||||||
|
@ -508,7 +510,7 @@ namespace Bit.App.Pages
|
||||||
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
|
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
|
||||||
{
|
{
|
||||||
// Close and go back to app
|
// Close and go back to app
|
||||||
_deviceActionService.CloseAutofill();
|
_autofillHandler.CloseAutofill();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -493,7 +493,7 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
|
|
||||||
var canOpenFile = true;
|
var canOpenFile = true;
|
||||||
if (!_deviceActionService.CanOpenFile(attachment.FileName))
|
if (!_fileService.CanOpenFile(attachment.FileName))
|
||||||
{
|
{
|
||||||
if (Device.RuntimePlatform == Device.iOS)
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
{
|
{
|
||||||
|
@ -562,7 +562,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
public async void OpenAttachment(byte[] data, AttachmentView attachment)
|
public async void OpenAttachment(byte[] data, AttachmentView attachment)
|
||||||
{
|
{
|
||||||
if (!_deviceActionService.OpenFile(data, attachment.Id, attachment.FileName))
|
if (!_fileService.OpenFile(data, attachment.Id, attachment.FileName))
|
||||||
{
|
{
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
||||||
return;
|
return;
|
||||||
|
@ -573,7 +573,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
_attachmentData = data;
|
_attachmentData = data;
|
||||||
_attachmentFilename = attachment.FileName;
|
_attachmentFilename = attachment.FileName;
|
||||||
if (!_deviceActionService.SaveFile(_attachmentData, null, _attachmentFilename, null))
|
if (!_fileService.SaveFile(_attachmentData, null, _attachmentFilename, null))
|
||||||
{
|
{
|
||||||
ClearAttachmentData();
|
ClearAttachmentData();
|
||||||
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment);
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment);
|
||||||
|
@ -582,7 +582,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
public async void SaveFileSelected(string contentUri, string filename)
|
public async void SaveFileSelected(string contentUri, string filename)
|
||||||
{
|
{
|
||||||
if (_deviceActionService.SaveFile(_attachmentData, null, filename ?? _attachmentFilename, contentUri))
|
if (_fileService.SaveFile(_attachmentData, null, filename ?? _attachmentFilename, contentUri))
|
||||||
{
|
{
|
||||||
ClearAttachmentData();
|
ClearAttachmentData();
|
||||||
_platformUtilsService.ShowToast("success", null, AppResources.SaveAttachmentSuccess);
|
_platformUtilsService.ShowToast("success", null, AppResources.SaveAttachmentSuccess);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Bit.App.Abstractions;
|
|
||||||
using Bit.App.Controls;
|
using Bit.App.Controls;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
@ -12,7 +12,7 @@ namespace Bit.App.Pages
|
||||||
public partial class CiphersPage : BaseContentPage
|
public partial class CiphersPage : BaseContentPage
|
||||||
{
|
{
|
||||||
private readonly string _autofillUrl;
|
private readonly string _autofillUrl;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
|
|
||||||
private CiphersPageViewModel _vm;
|
private CiphersPageViewModel _vm;
|
||||||
private bool _hasFocused;
|
private bool _hasFocused;
|
||||||
|
@ -48,7 +48,7 @@ namespace Bit.App.Pages
|
||||||
{
|
{
|
||||||
NavigationPage.SetTitleView(this, _titleLayout);
|
NavigationPage.SetTitleView(this, _titleLayout);
|
||||||
}
|
}
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SearchBar SearchBar => _searchBar;
|
public SearchBar SearchBar => _searchBar;
|
||||||
|
@ -107,7 +107,7 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_deviceActionService.CloseAutofill();
|
_autofillHandler.CloseAutofill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ namespace Bit.App.Pages
|
||||||
private readonly ICipherService _cipherService;
|
private readonly ICipherService _cipherService;
|
||||||
private readonly ISearchService _searchService;
|
private readonly ISearchService _searchService;
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
|
private readonly IAutofillHandler _autofillHandler;
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
|
@ -37,6 +38,7 @@ namespace Bit.App.Pages
|
||||||
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
||||||
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
_searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
|
||||||
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
||||||
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
_organizationService = ServiceContainer.Resolve<IOrganizationService>("organizationService");
|
||||||
|
@ -196,7 +198,7 @@ namespace Bit.App.Pages
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_deviceActionService.Autofill(cipher);
|
_autofillHandler.Autofill(cipher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,7 +220,7 @@ namespace Bit.App.Pages
|
||||||
NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1);
|
NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
|
||||||
var hasFavorites = FavoriteCiphers?.Any() ?? false;
|
var hasFavorites = FavoriteCiphers?.Any() ?? false;
|
||||||
if (hasFavorites)
|
if (hasFavorites)
|
||||||
{
|
{
|
||||||
|
@ -400,7 +400,7 @@ namespace Bit.App.Pages
|
||||||
|
|
||||||
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
|
private void CreateCipherGroupedItems(List<GroupingsPageListGroup> groupedItems)
|
||||||
{
|
{
|
||||||
var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
|
var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
|
||||||
_totpTickCts?.Cancel();
|
_totpTickCts?.Cancel();
|
||||||
if (ShowTotp)
|
if (ShowTotp)
|
||||||
{
|
{
|
||||||
|
|
|
@ -72,8 +72,13 @@ namespace Bit.App.Services
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the device type on the server enum
|
||||||
|
/// </summary>
|
||||||
public Core.Enums.DeviceType GetDevice()
|
public Core.Enums.DeviceType GetDevice()
|
||||||
{
|
{
|
||||||
|
// Can't use Device.RuntimePlatform here because it gets called before Forms.Init() and throws.
|
||||||
|
// so we need to get the DeviceType ourselves
|
||||||
return _deviceActionService.DeviceType;
|
return _deviceActionService.DeviceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,11 +122,6 @@ namespace Bit.App.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveFile()
|
|
||||||
{
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetApplicationVersion()
|
public string GetApplicationVersion()
|
||||||
{
|
{
|
||||||
return AppInfo.VersionString;
|
return AppInfo.VersionString;
|
||||||
|
@ -208,11 +208,6 @@ namespace Bit.App.Services
|
||||||
return (password, valid);
|
return (password, valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsDev()
|
|
||||||
{
|
|
||||||
return Core.Utilities.CoreHelpers.InDebugMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsSelfHost()
|
public bool IsSelfHost()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -564,7 +564,7 @@ namespace Bit.App.Utilities
|
||||||
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
var sendService = ServiceContainer.Resolve<ISendService>("sendService");
|
||||||
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
var passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>(
|
||||||
"passwordGenerationService");
|
"passwordGenerationService");
|
||||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
var fileService = ServiceContainer.Resolve<IFileService>();
|
||||||
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
var policyService = ServiceContainer.Resolve<IPolicyService>("policyService");
|
||||||
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
var searchService = ServiceContainer.Resolve<ISearchService>("searchService");
|
||||||
var usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>(
|
var usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>(
|
||||||
|
@ -572,7 +572,7 @@ namespace Bit.App.Utilities
|
||||||
|
|
||||||
await Task.WhenAll(
|
await Task.WhenAll(
|
||||||
cipherService.ClearCacheAsync(),
|
cipherService.ClearCacheAsync(),
|
||||||
deviceActionService.ClearCacheAsync());
|
fileService.ClearCacheAsync());
|
||||||
tokenService.ClearCache();
|
tokenService.ClearCache();
|
||||||
cryptoService.ClearCache();
|
cryptoService.ClearCache();
|
||||||
settingsService.ClearCache();
|
settingsService.ClearCache();
|
||||||
|
|
16
src/Core/Abstractions/IAutofillHandler.cs
Normal file
16
src/Core/Abstractions/IAutofillHandler.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IAutofillHandler
|
||||||
|
{
|
||||||
|
bool AutofillServicesEnabled();
|
||||||
|
bool SupportsAutofillService();
|
||||||
|
void Autofill(CipherView cipher);
|
||||||
|
void CloseAutofill();
|
||||||
|
bool AutofillAccessibilityServiceRunning();
|
||||||
|
bool AutofillAccessibilityOverlayPermitted();
|
||||||
|
bool AutofillServiceEnabled();
|
||||||
|
void DisableAutofillService();
|
||||||
|
}
|
||||||
|
}
|
14
src/Core/Abstractions/IFileService.cs
Normal file
14
src/Core/Abstractions/IFileService.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IFileService
|
||||||
|
{
|
||||||
|
bool CanOpenFile(string fileName);
|
||||||
|
bool OpenFile(byte[] fileData, string id, string fileName);
|
||||||
|
bool SaveFile(byte[] fileData, string id, string fileName, string contentUri);
|
||||||
|
Task ClearCacheAsync();
|
||||||
|
Task SelectFileAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,15 +8,16 @@ namespace Bit.Core.Abstractions
|
||||||
public interface IPlatformUtilsService
|
public interface IPlatformUtilsService
|
||||||
{
|
{
|
||||||
string GetApplicationVersion();
|
string GetApplicationVersion();
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the device type on the server enum
|
||||||
|
/// </summary>
|
||||||
DeviceType GetDevice();
|
DeviceType GetDevice();
|
||||||
string GetDeviceString();
|
string GetDeviceString();
|
||||||
ClientType GetClientType();
|
ClientType GetClientType();
|
||||||
bool IsDev();
|
|
||||||
bool IsSelfHost();
|
bool IsSelfHost();
|
||||||
bool IsViewOpen();
|
bool IsViewOpen();
|
||||||
void LaunchUri(string uri, Dictionary<string, object> options = null);
|
void LaunchUri(string uri, Dictionary<string, object> options = null);
|
||||||
Task<string> ReadFromClipboardAsync(Dictionary<string, object> options = null);
|
Task<string> ReadFromClipboardAsync(Dictionary<string, object> options = null);
|
||||||
void SaveFile();
|
|
||||||
Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null,
|
Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null,
|
||||||
string cancelText = null, string type = null);
|
string cancelText = null, string type = null);
|
||||||
Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator);
|
Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator);
|
||||||
|
|
|
@ -2,8 +2,13 @@
|
||||||
|
|
||||||
namespace Bit.Core.Utilities
|
namespace Bit.Core.Utilities
|
||||||
{
|
{
|
||||||
public class LazyResolve<T> : Lazy<T>
|
public class LazyResolve<T> : Lazy<T> where T : class
|
||||||
{
|
{
|
||||||
|
public LazyResolve()
|
||||||
|
: base(() => ServiceContainer.Resolve<T>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public LazyResolve(string containerKey)
|
public LazyResolve(string containerKey)
|
||||||
: base(() => ServiceContainer.Resolve<T>(containerKey))
|
: base(() => ServiceContainer.Resolve<T>(containerKey))
|
||||||
{
|
{
|
||||||
|
|
22
src/iOS.Core/Services/AutofillHandler.cs
Normal file
22
src/iOS.Core/Services/AutofillHandler.cs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This handler is only needed on Android for now, now this class acts as a stub so that dependency injection doesn't break
|
||||||
|
/// </summary>
|
||||||
|
public class AutofillHandler : IAutofillHandler
|
||||||
|
{
|
||||||
|
public bool SupportsAutofillService() => false;
|
||||||
|
public bool AutofillServiceEnabled() => false;
|
||||||
|
public void Autofill(CipherView cipher) => throw new NotImplementedException();
|
||||||
|
public bool AutofillAccessibilityOverlayPermitted() => throw new NotImplementedException();
|
||||||
|
public bool AutofillAccessibilityServiceRunning() => throw new NotImplementedException();
|
||||||
|
public bool AutofillServicesEnabled() => throw new NotImplementedException();
|
||||||
|
public void CloseAutofill() => throw new NotImplementedException();
|
||||||
|
public void DisableAutofillService() => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,14 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Resources;
|
using Bit.App.Resources;
|
||||||
using Bit.Core.Abstractions;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
|
||||||
using Bit.iOS.Core.Utilities;
|
using Bit.iOS.Core.Utilities;
|
||||||
using Bit.iOS.Core.Views;
|
using Bit.iOS.Core.Views;
|
||||||
using CoreGraphics;
|
using CoreGraphics;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using LocalAuthentication;
|
using LocalAuthentication;
|
||||||
using MobileCoreServices;
|
|
||||||
using Photos;
|
|
||||||
using UIKit;
|
using UIKit;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
@ -22,20 +16,10 @@ namespace Bit.iOS.Core.Services
|
||||||
{
|
{
|
||||||
public class DeviceActionService : IDeviceActionService
|
public class DeviceActionService : IDeviceActionService
|
||||||
{
|
{
|
||||||
private readonly IStateService _stateService;
|
|
||||||
private readonly IMessagingService _messagingService;
|
|
||||||
private Toast _toast;
|
private Toast _toast;
|
||||||
private UIAlertController _progressAlert;
|
private UIAlertController _progressAlert;
|
||||||
private string _userAgent;
|
private string _userAgent;
|
||||||
|
|
||||||
public DeviceActionService(
|
|
||||||
IStateService stateService,
|
|
||||||
IMessagingService messagingService)
|
|
||||||
{
|
|
||||||
_stateService = stateService;
|
|
||||||
_messagingService = messagingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string DeviceUserAgent
|
public string DeviceUserAgent
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -120,91 +104,6 @@ namespace Bit.iOS.Core.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 bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
|
||||||
{
|
|
||||||
// OpenFile behavior is appropriate here as iOS prompts to save file
|
|
||||||
return OpenFile(fileData, id, fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 _stateService.SetLastFileCacheClearAsync(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) =>
|
|
||||||
{
|
|
||||||
if (SystemMajorVersion() < 11)
|
|
||||||
{
|
|
||||||
e.DocumentPicker.DidPickDocument += DocumentPicker_DidPickDocument;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
e.DocumentPicker.Delegate = new PickerDelegate(this);
|
|
||||||
}
|
|
||||||
controller.PresentViewController(e.DocumentPicker, true, null);
|
|
||||||
};
|
|
||||||
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,
|
||||||
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
bool numericKeyboard = false, bool autofocus = true, bool password = false)
|
||||||
|
@ -298,11 +197,6 @@ namespace Bit.iOS.Core.Services
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SupportsAutofillService()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int SystemMajorVersion()
|
public int SystemMajorVersion()
|
||||||
{
|
{
|
||||||
var versionParts = UIDevice.CurrentDevice.SystemVersion.Split('.');
|
var versionParts = UIDevice.CurrentDevice.SystemVersion.Split('.');
|
||||||
|
@ -391,46 +285,6 @@ namespace Bit.iOS.Core.Services
|
||||||
return result.Task;
|
return result.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Autofill(CipherView cipher)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CloseAutofill()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Background()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityServiceRunning()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasAutofillService()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillServiceEnabled()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DisableAutofillService()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillServicesEnabled()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetBuildNumber()
|
public string GetBuildNumber()
|
||||||
{
|
{
|
||||||
return NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString();
|
return NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString();
|
||||||
|
@ -479,78 +333,6 @@ namespace Bit.iOS.Core.Services
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
PickedDocument(e.Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectFileResult(byte[] data, string fileName)
|
|
||||||
{
|
|
||||||
_messagingService.Send("selectFileResult", new Tuple<byte[], string>(data, fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
@ -569,43 +351,6 @@ namespace Bit.iOS.Core.Services
|
||||||
(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");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void PickedDocument(NSUrl url)
|
|
||||||
{
|
|
||||||
url.StartAccessingSecurityScopedResource();
|
|
||||||
var doc = new UIDocument(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(url, NSFileCoordinatorReadingOptions.WithoutChanges,
|
|
||||||
out NSError error, (u) =>
|
|
||||||
{
|
|
||||||
var data = NSData.FromUrl(u).ToArray();
|
|
||||||
SelectFileResult(data, fileName ?? "unknown_file_name");
|
|
||||||
});
|
|
||||||
url.StopAccessingSecurityScopedResource();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AutofillAccessibilityOverlayPermitted()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenAccessibilityOverlayPermissionSettings()
|
public void OpenAccessibilityOverlayPermissionSettings()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
@ -629,21 +374,6 @@ namespace Bit.iOS.Core.Services
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PickerDelegate : UIDocumentPickerDelegate
|
|
||||||
{
|
|
||||||
private readonly DeviceActionService _deviceActionService;
|
|
||||||
|
|
||||||
public PickerDelegate(DeviceActionService deviceActionService)
|
|
||||||
{
|
|
||||||
_deviceActionService = deviceActionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl url)
|
|
||||||
{
|
|
||||||
_deviceActionService.PickedDocument(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenAppSettings()
|
public void OpenAppSettings()
|
||||||
{
|
{
|
||||||
var url = new NSUrl(UIApplication.OpenSettingsUrlString);
|
var url = new NSUrl(UIApplication.OpenSettingsUrlString);
|
||||||
|
|
213
src/iOS.Core/Services/FileService.cs
Normal file
213
src/iOS.Core/Services/FileService.cs
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.iOS.Core.Utilities;
|
||||||
|
using CoreGraphics;
|
||||||
|
using Foundation;
|
||||||
|
using MobileCoreServices;
|
||||||
|
using Photos;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Services
|
||||||
|
{
|
||||||
|
public class FileService : IFileService
|
||||||
|
{
|
||||||
|
private readonly IStateService _stateService;
|
||||||
|
private readonly IMessagingService _messagingService;
|
||||||
|
|
||||||
|
public FileService(IStateService stateService, IMessagingService messagingService)
|
||||||
|
{
|
||||||
|
_stateService = stateService;
|
||||||
|
_messagingService = messagingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = UIViewControllerExtensions.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 bool SaveFile(byte[] fileData, string id, string fileName, string contentUri)
|
||||||
|
{
|
||||||
|
// OpenFile behavior is appropriate here as iOS prompts to save file
|
||||||
|
return OpenFile(fileData, id, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _stateService.SetLastFileCacheClearAsync(DateTime.UtcNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SelectFileAsync()
|
||||||
|
{
|
||||||
|
var controller = UIViewControllerExtensions.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) =>
|
||||||
|
{
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0))
|
||||||
|
{
|
||||||
|
e.DocumentPicker.Delegate = new PickerDelegate(this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e.DocumentPicker.DidPickDocument += DocumentPicker_DidPickDocument;
|
||||||
|
}
|
||||||
|
controller.PresentViewController(e.DocumentPicker, true, null);
|
||||||
|
};
|
||||||
|
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.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
PickedDocument(e.Url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PickedDocument(NSUrl url)
|
||||||
|
{
|
||||||
|
url.StartAccessingSecurityScopedResource();
|
||||||
|
var doc = new UIDocument(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(url, NSFileCoordinatorReadingOptions.WithoutChanges,
|
||||||
|
out NSError error, (u) =>
|
||||||
|
{
|
||||||
|
var data = NSData.FromUrl(u).ToArray();
|
||||||
|
SelectFileResult(data, fileName ?? "unknown_file_name");
|
||||||
|
});
|
||||||
|
url.StopAccessingSecurityScopedResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectFileResult(byte[] data, string fileName)
|
||||||
|
{
|
||||||
|
_messagingService.Send("selectFileResult", new Tuple<byte[], string>(data, fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PickerDelegate : UIDocumentPickerDelegate
|
||||||
|
{
|
||||||
|
private readonly FileService _fileService;
|
||||||
|
|
||||||
|
public PickerDelegate(FileService fileService)
|
||||||
|
{
|
||||||
|
_fileService = fileService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void DidPickDocument(UIDocumentPickerViewController controller, NSUrl url)
|
||||||
|
{
|
||||||
|
_fileService.PickedDocument(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/iOS.Core/Utilities/UIViewControllerExtensions.cs
Normal file
31
src/iOS.Core/Utilities/UIViewControllerExtensions.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using System;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Utilities
|
||||||
|
{
|
||||||
|
public static class UIViewControllerExtensions
|
||||||
|
{
|
||||||
|
public static UIViewController GetVisibleViewController()
|
||||||
|
{
|
||||||
|
return GetVisibleViewController(UIApplication.SharedApplication.KeyWindow.RootViewController);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UIViewController GetVisibleViewController(this UIViewController controller)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -102,7 +102,8 @@ namespace Bit.iOS.Core.Utilities
|
||||||
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
|
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
|
||||||
var stateMigrationService =
|
var stateMigrationService =
|
||||||
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
|
||||||
var deviceActionService = new DeviceActionService(stateService, messagingService);
|
var deviceActionService = new DeviceActionService();
|
||||||
|
var fileService = new FileService(stateService, messagingService);
|
||||||
var clipboardService = new ClipboardService(stateService);
|
var clipboardService = new ClipboardService(stateService);
|
||||||
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
|
||||||
messagingService, broadcasterService);
|
messagingService, broadcasterService);
|
||||||
|
@ -121,6 +122,8 @@ namespace Bit.iOS.Core.Utilities
|
||||||
ServiceContainer.Register<IStateService>("stateService", stateService);
|
ServiceContainer.Register<IStateService>("stateService", stateService);
|
||||||
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
ServiceContainer.Register<IStateMigrationService>("stateMigrationService", stateMigrationService);
|
||||||
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
ServiceContainer.Register<IDeviceActionService>("deviceActionService", deviceActionService);
|
||||||
|
ServiceContainer.Register<IFileService>(fileService);
|
||||||
|
ServiceContainer.Register<IAutofillHandler>(new AutofillHandler());
|
||||||
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
ServiceContainer.Register<IClipboardService>("clipboardService", clipboardService);
|
||||||
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
ServiceContainer.Register<IPlatformUtilsService>("platformUtilsService", platformUtilsService);
|
||||||
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
ServiceContainer.Register<IBiometricService>("biometricService", biometricService);
|
||||||
|
|
|
@ -204,6 +204,9 @@
|
||||||
<Compile Include="Renderers\CollectionView\CollectionException.cs" />
|
<Compile Include="Renderers\CollectionView\CollectionException.cs" />
|
||||||
<Compile Include="Renderers\CollectionView\ExtendedGroupableItemsViewDelegator.cs" />
|
<Compile Include="Renderers\CollectionView\ExtendedGroupableItemsViewDelegator.cs" />
|
||||||
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
<Compile Include="Effects\NoEmojiKeyboardEffect.cs" />
|
||||||
|
<Compile Include="Services\FileService.cs" />
|
||||||
|
<Compile Include="Utilities\UIViewControllerExtensions.cs" />
|
||||||
|
<Compile Include="Services\AutofillHandler.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\App\App.csproj">
|
<ProjectReference Include="..\App\App.csproj">
|
||||||
|
|
Loading…
Reference in a new issue