diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index dfe728ede..1ac400e27 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -152,6 +152,8 @@
+
+
diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs
index 751cd7f2a..cffc12ac0 100644
--- a/src/Android/MainActivity.cs
+++ b/src/Android/MainActivity.cs
@@ -36,6 +36,7 @@ namespace Bit.Droid
public class MainActivity : Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
private IDeviceActionService _deviceActionService;
+ private IFileService _fileService;
private IMessagingService _messagingService;
private IBroadcasterService _broadcasterService;
private IStateService _stateService;
@@ -59,6 +60,7 @@ namespace Bit.Droid
StrictMode.SetThreadPolicy(policy);
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _fileService = ServiceContainer.Resolve();
_messagingService = ServiceContainer.Resolve("messagingService");
_broadcasterService = ServiceContainer.Resolve("broadcasterService");
_stateService = ServiceContainer.Resolve("stateService");
@@ -217,7 +219,7 @@ namespace Bit.Droid
{
_messagingService.Send("selectFileCameraPermissionDenied");
}
- await _deviceActionService.SelectFileAsync();
+ await _fileService.SelectFileAsync();
}
else
{
diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs
index ef0b6f303..8bca7cafa 100644
--- a/src/Android/MainApplication.cs
+++ b/src/Android/MainApplication.cs
@@ -139,8 +139,9 @@ namespace Bit.Droid
var stateMigrationService =
new StateMigrationService(liteDbStorage, preferencesStorage, secureStorageService);
var clipboardService = new ClipboardService(stateService);
- var deviceActionService = new DeviceActionService(clipboardService, stateService, messagingService,
- broadcasterService, () => ServiceContainer.Resolve("eventService"));
+ var deviceActionService = new DeviceActionService(stateService, messagingService);
+ var fileService = new FileService(stateService, broadcasterService);
+ var autofillHandler = new AutofillHandler(stateService, messagingService, clipboardService, new LazyResolve());
var platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
var biometricService = new BiometricService();
@@ -159,6 +160,8 @@ namespace Bit.Droid
ServiceContainer.Register("stateMigrationService", stateMigrationService);
ServiceContainer.Register("clipboardService", clipboardService);
ServiceContainer.Register("deviceActionService", deviceActionService);
+ ServiceContainer.Register(fileService);
+ ServiceContainer.Register(autofillHandler);
ServiceContainer.Register("platformUtilsService", platformUtilsService);
ServiceContainer.Register("biometricService", biometricService);
ServiceContainer.Register("cryptoFunctionService", cryptoFunctionService);
diff --git a/src/Android/Services/AutofillHandler.cs b/src/Android/Services/AutofillHandler.cs
new file mode 100644
index 000000000..9cfa5ec9e
--- /dev/null
+++ b/src/Android/Services/AutofillHandler.cs
@@ -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 _eventService;
+
+ public AutofillHandler(IStateService stateService,
+ IMessagingService messagingService,
+ IClipboardService clipboardService,
+ LazyResolve 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("totpService");
+ var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
+ if (totp != null)
+ {
+ await _clipboardService.CopyTextAsync(totp);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs
index 452fb0b81..2ddee1245 100644
--- a/src/Android/Services/DeviceActionService.cs
+++ b/src/Android/Services/DeviceActionService.cs
@@ -1,11 +1,6 @@
using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
using System.Threading.Tasks;
-using Android;
using Android.App;
-using Android.App.Assist;
using Android.Content;
using Android.Content.PM;
using Android.Nfc;
@@ -14,20 +9,13 @@ using Android.Provider;
using Android.Text;
using Android.Text.Method;
using Android.Views;
-using Android.Views.Autofill;
using Android.Views.InputMethods;
-using Android.Webkit;
using Android.Widget;
-using AndroidX.Core.App;
-using AndroidX.Core.Content;
using Bit.App.Abstractions;
using Bit.App.Resources;
-using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
-using Bit.Core.Models.View;
using Bit.Core.Utilities;
-using Bit.Droid.Autofill;
using Bit.Droid.Utilities;
using Plugin.CurrentActivity;
@@ -35,38 +23,20 @@ namespace Bit.Droid.Services
{
public class DeviceActionService : IDeviceActionService
{
- private readonly IClipboardService _clipboardService;
private readonly IStateService _stateService;
private readonly IMessagingService _messagingService;
- private readonly IBroadcasterService _broadcasterService;
- private readonly Func _eventServiceFunc;
private AlertDialog _progressDialog;
object _progressDialogLock = new object();
- private bool _cameraPermissionsDenied;
private Toast _toast;
private string _userAgent;
public DeviceActionService(
- IClipboardService clipboardService,
IStateService stateService,
- IMessagingService messagingService,
- IBroadcasterService broadcasterService,
- Func eventServiceFunc)
+ IMessagingService messagingService)
{
- _clipboardService = clipboardService;
_stateService = stateService;
_messagingService = messagingService;
- _broadcasterService = broadcasterService;
- _eventServiceFunc = eventServiceFunc;
-
- _broadcasterService.Subscribe(nameof(DeviceActionService), (message) =>
- {
- if (message.Command == "selectFileCameraPermissionDenied")
- {
- _cameraPermissionsDenied = true;
- }
- });
}
public string DeviceUserAgent
@@ -212,184 +182,6 @@ namespace Bit.Droid.Services
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();
- 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 DisplayPromptAync(string title = null, string description = null,
string text = null, string okButtonText = null, string cancelButtonText = null,
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()
{
return Application.Context.ApplicationContext.PackageManager.GetPackageInfo(
@@ -526,25 +290,6 @@ namespace Bit.Droid.Services
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()
{
return (int)Build.VERSION.SdkInt;
@@ -635,112 +380,6 @@ namespace Bit.Droid.Services
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()
{
@@ -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()
{
try
@@ -848,61 +468,6 @@ namespace Bit.Droid.Services
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 GetCameraIntents(Android.Net.Uri outputUri)
- {
- var intents = new List();
- 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)
{
var intent = new Intent(Intent.ActionView, Android.Net.Uri.Parse($"{url}?id={activity.PackageName}"));
@@ -920,24 +485,6 @@ namespace Bit.Droid.Services
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("totpService");
- var totp = await totpService.GetCodeAsync(cipher.Login.Totp);
- if (totp != null)
- {
- await _clipboardService.CopyTextAsync(totp);
- }
- }
- }
- }
-
public float GetSystemFontSizeScale()
{
var activity = CrossCurrentActivity.Current?.Activity as MainActivity;
diff --git a/src/Android/Services/FileService.cs b/src/Android/Services/FileService.cs
new file mode 100644
index 000000000..c217f7a51
--- /dev/null
+++ b/src/Android/Services/FileService.cs
@@ -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();
+ 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 GetCameraIntents(Android.Net.Uri outputUri)
+ {
+ var intents = new List();
+ 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;
+ }
+ }
+}
diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs
index a314995f8..8f4a19a34 100644
--- a/src/App/Abstractions/IDeviceActionService.cs
+++ b/src/App/Abstractions/IDeviceActionService.cs
@@ -1,6 +1,5 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
-using Bit.Core.Models.View;
namespace Bit.App.Abstractions
{
@@ -8,44 +7,32 @@ namespace Bit.App.Abstractions
{
string DeviceUserAgent { get; }
DeviceType DeviceType { get; }
+ int SystemMajorVersion();
+ string SystemModel();
+ string GetBuildNumber();
+
void Toast(string text, bool longDuration = false);
- bool LaunchApp(string appName);
Task ShowLoadingAsync(string text);
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 DisplayPromptAync(string title = null, string description = null, string text = null,
string okButtonText = null, string cancelButtonText = null, bool numericKeyboard = false,
bool autofocus = true, bool password = false);
- void RateApp();
+ Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
+ Task DisplayActionSheetAsync(string title, string cancel, string destruction, params string[] buttons);
+
bool SupportsFaceBiometric();
Task SupportsFaceBiometricAsync();
bool SupportsNfc();
bool SupportsCamera();
- bool SupportsAutofillService();
- int SystemMajorVersion();
- string SystemModel();
- Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons);
- Task 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();
+ bool SupportsFido2();
+
+ bool LaunchApp(string appName);
+ void RateApp();
void OpenAccessibilitySettings();
void OpenAccessibilityOverlayPermissionSettings();
void OpenAutofillSettings();
long GetActiveTime();
void CloseMainApp();
- bool SupportsFido2();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync();
diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs
index 483711bfd..b78085a13 100644
--- a/src/App/App.xaml.cs
+++ b/src/App/App.xaml.cs
@@ -28,6 +28,7 @@ namespace Bit.App
private readonly ISyncService _syncService;
private readonly IAuthService _authService;
private readonly IDeviceActionService _deviceActionService;
+ private readonly IFileService _fileService;
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
private static bool _isResumed;
@@ -49,6 +50,7 @@ namespace Bit.App
_syncService = ServiceContainer.Resolve("syncService");
_authService = ServiceContainer.Resolve("authService");
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _fileService = ServiceContainer.Resolve();
_accountsManager = ServiceContainer.Resolve("accountsManager");
_pushNotificationService = ServiceContainer.Resolve();
@@ -301,7 +303,7 @@ namespace Bit.App
var lastClear = await _stateService.GetLastFileCacheClearAsync();
if ((DateTime.UtcNow - lastClear.GetValueOrDefault(DateTime.MinValue)).TotalDays >= 1)
{
- var task = Task.Run(() => _deviceActionService.ClearCacheAsync());
+ var task = Task.Run(() => _fileService.ClearCacheAsync());
}
}
diff --git a/src/App/Pages/Send/SendAddEditPageViewModel.cs b/src/App/Pages/Send/SendAddEditPageViewModel.cs
index 276b99a73..a0b55e2dc 100644
--- a/src/App/Pages/Send/SendAddEditPageViewModel.cs
+++ b/src/App/Pages/Send/SendAddEditPageViewModel.cs
@@ -19,6 +19,7 @@ namespace Bit.App.Pages
public class SendAddEditPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
+ private readonly IFileService _fileService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IMessagingService _messagingService;
private readonly IStateService _stateService;
@@ -51,6 +52,7 @@ namespace Bit.App.Pages
public SendAddEditPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _fileService = ServiceContainer.Resolve();
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
_messagingService = ServiceContainer.Resolve("messagingService");
_stateService = ServiceContainer.Resolve("stateService");
@@ -292,7 +294,7 @@ namespace Bit.App.Pages
public async Task ChooseFileAsync()
{
- await _deviceActionService.SelectFileAsync();
+ await _fileService.SelectFileAsync();
}
public void ClearExpirationDate()
diff --git a/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPageViewModel.cs b/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPageViewModel.cs
index ada402713..6fd3c0f16 100644
--- a/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPageViewModel.cs
+++ b/src/App/Pages/Send/SendGroupingsPage/SendGroupingsPageViewModel.cs
@@ -144,7 +144,7 @@ namespace Bit.App.Pages
{
await LoadDataAsync();
- var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
+ var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
if (MainPage)
{
groupedSends.Add(new SendGroupingsPageListGroup(
diff --git a/src/App/Pages/Settings/AutofillServicesPageViewModel.cs b/src/App/Pages/Settings/AutofillServicesPageViewModel.cs
index 5af80bcf2..1ec6f16c5 100644
--- a/src/App/Pages/Settings/AutofillServicesPageViewModel.cs
+++ b/src/App/Pages/Settings/AutofillServicesPageViewModel.cs
@@ -12,6 +12,7 @@ namespace Bit.App.Pages
public class AutofillServicesPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private readonly IStateService _stateService;
private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
@@ -26,6 +27,7 @@ namespace Bit.App.Pages
public AutofillServicesPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
_stateService = ServiceContainer.Resolve("stateService");
_i18nService = ServiceContainer.Resolve("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
@@ -173,7 +175,7 @@ namespace Bit.App.Pages
}
else
{
- _deviceActionService.DisableAutofillService();
+ _autofillHandler.DisableAutofillService();
}
}
@@ -188,7 +190,7 @@ namespace Bit.App.Pages
public async Task ToggleAccessibilityAsync()
{
- if (!_deviceActionService.AutofillAccessibilityServiceRunning())
+ if (!_autofillHandler.AutofillAccessibilityServiceRunning())
{
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
@@ -213,9 +215,9 @@ namespace Bit.App.Pages
public void UpdateEnabled()
{
AutofillServiceToggled =
- _deviceActionService.HasAutofillService() && _deviceActionService.AutofillServiceEnabled();
- AccessibilityToggled = _deviceActionService.AutofillAccessibilityServiceRunning();
- DrawOverToggled = _deviceActionService.AutofillAccessibilityOverlayPermitted();
+ _autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
+ AccessibilityToggled = _autofillHandler.AutofillAccessibilityServiceRunning();
+ DrawOverToggled = _autofillHandler.AutofillAccessibilityOverlayPermitted();
}
private async Task UpdateInlineAutofillToggledAsync()
diff --git a/src/App/Pages/Settings/ExportVaultPageViewModel.cs b/src/App/Pages/Settings/ExportVaultPageViewModel.cs
index ac8b3c7f6..403cf6e8c 100644
--- a/src/App/Pages/Settings/ExportVaultPageViewModel.cs
+++ b/src/App/Pages/Settings/ExportVaultPageViewModel.cs
@@ -16,6 +16,7 @@ namespace Bit.App.Pages
public class ExportVaultPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
+ private readonly IFileService _fileService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly II18nService _i18nService;
private readonly IExportService _exportService;
@@ -39,6 +40,7 @@ namespace Bit.App.Pages
public ExportVaultPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _fileService = ServiceContainer.Resolve();
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
_i18nService = ServiceContainer.Resolve("i18nService");
_exportService = ServiceContainer.Resolve("exportService");
@@ -182,7 +184,7 @@ namespace Bit.App.Pages
_defaultFilename = _exportService.GetFileName(null, fileFormat);
_exportResult = Encoding.UTF8.GetBytes(data);
- if (!_deviceActionService.SaveFile(_exportResult, null, _defaultFilename, null))
+ if (!_fileService.SaveFile(_exportResult, null, _defaultFilename, null))
{
ClearResult();
await _platformUtilsService.ShowDialogAsync(_i18nService.T("ExportVaultFailure"));
@@ -220,7 +222,7 @@ namespace Bit.App.Pages
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();
_platformUtilsService.ShowToast("success", null, _i18nService.T("ExportVaultSuccess"));
diff --git a/src/App/Pages/Settings/OptionsPage.xaml.cs b/src/App/Pages/Settings/OptionsPage.xaml.cs
index cb07027b2..8f608d600 100644
--- a/src/App/Pages/Settings/OptionsPage.xaml.cs
+++ b/src/App/Pages/Settings/OptionsPage.xaml.cs
@@ -1,5 +1,6 @@
using Bit.App.Abstractions;
using Bit.App.Resources;
+using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Xamarin.Forms;
using Xamarin.Forms.PlatformConfiguration;
@@ -9,12 +10,12 @@ namespace Bit.App.Pages
{
public partial class OptionsPage : BaseContentPage
{
- private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private readonly OptionsPageViewModel _vm;
public OptionsPage()
{
- _deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
InitializeComponent();
_vm = BindingContext as OptionsPageViewModel;
_vm.Page = this;
@@ -25,7 +26,7 @@ namespace Bit.App.Pages
if (Device.RuntimePlatform == Device.Android)
{
ToolbarItems.RemoveAt(0);
- _vm.ShowAndroidAutofillSettings = _deviceActionService.SupportsAutofillService();
+ _vm.ShowAndroidAutofillSettings = _autofillHandler.SupportsAutofillService();
}
else
{
diff --git a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
index 54f05fc49..f586e6e79 100644
--- a/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
+++ b/src/App/Pages/Settings/SettingsPage/SettingsPageViewModel.cs
@@ -20,6 +20,7 @@ namespace Bit.App.Pages
private readonly ICryptoService _cryptoService;
private readonly IStateService _stateService;
private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private readonly IEnvironmentService _environmentService;
private readonly IMessagingService _messagingService;
private readonly IVaultTimeoutService _vaultTimeoutService;
@@ -74,6 +75,7 @@ namespace Bit.App.Pages
_cryptoService = ServiceContainer.Resolve("cryptoService");
_stateService = ServiceContainer.Resolve("stateService");
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
_environmentService = ServiceContainer.Resolve("environmentService");
_messagingService = ServiceContainer.Resolve("messagingService");
_vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService");
@@ -454,7 +456,7 @@ namespace Bit.App.Pages
else if (await _platformUtilsService.SupportsBiometricAsync())
{
_biometric = await _platformUtilsService.AuthenticateBiometricAsync(null,
- _deviceActionService.DeviceType == Core.Enums.DeviceType.Android ? "." : null);
+ Device.RuntimePlatform == Device.Android ? "." : null);
}
if (_biometric == current)
{
@@ -485,7 +487,7 @@ namespace Bit.App.Pages
autofillItems.Add(new SettingsPageListItem
{
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)))
});
}
diff --git a/src/App/Pages/Vault/AttachmentsPageViewModel.cs b/src/App/Pages/Vault/AttachmentsPageViewModel.cs
index 6216f5ec9..02e9b2ae6 100644
--- a/src/App/Pages/Vault/AttachmentsPageViewModel.cs
+++ b/src/App/Pages/Vault/AttachmentsPageViewModel.cs
@@ -18,6 +18,7 @@ namespace Bit.App.Pages
public class AttachmentsPageViewModel : BaseViewModel
{
private readonly IDeviceActionService _deviceActionService;
+ private readonly IFileService _fileService;
private readonly ICipherService _cipherService;
private readonly ICryptoService _cryptoService;
private readonly IStateService _stateService;
@@ -34,6 +35,7 @@ namespace Bit.App.Pages
public AttachmentsPageViewModel()
{
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _fileService = ServiceContainer.Resolve();
_cipherService = ServiceContainer.Resolve("cipherService");
_cryptoService = ServiceContainer.Resolve("cryptoService");
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
@@ -156,7 +158,7 @@ namespace Bit.App.Pages
{
_vaultTimeoutService.DelayLockAndLogoutMs = 60000;
}
- await _deviceActionService.SelectFileAsync();
+ await _fileService.SelectFileAsync();
}
private async void DeleteAsync(AttachmentView attachment)
diff --git a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs
index 80e9caf09..525f7dea1 100644
--- a/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs
+++ b/src/App/Pages/Vault/AutofillCiphersPageViewModel.cs
@@ -21,6 +21,7 @@ namespace Bit.App.Pages
{
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private readonly ICipherService _cipherService;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
@@ -37,6 +38,7 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
_cipherService = ServiceContainer.Resolve("cipherService");
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
_stateService = ServiceContainer.Resolve("stateService");
_passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService");
_messagingService = ServiceContainer.Resolve("messagingService");
@@ -232,7 +234,7 @@ namespace Bit.App.Pages
}
if (autofillResponse == AppResources.Yes || autofillResponse == AppResources.YesAndSave)
{
- _deviceActionService.Autofill(cipher);
+ _autofillHandler.Autofill(cipher);
}
}
}
diff --git a/src/App/Pages/Vault/BaseCipherViewModel.cs b/src/App/Pages/Vault/BaseCipherViewModel.cs
index bd74befa5..871d8aa24 100644
--- a/src/App/Pages/Vault/BaseCipherViewModel.cs
+++ b/src/App/Pages/Vault/BaseCipherViewModel.cs
@@ -14,6 +14,7 @@ namespace Bit.App.Pages
{
private readonly IAuditService _auditService;
protected readonly IDeviceActionService _deviceActionService;
+ protected readonly IFileService _fileService;
protected readonly ILogger _logger;
protected readonly IPlatformUtilsService _platformUtilsService;
private CipherView _cipher;
@@ -22,6 +23,7 @@ namespace Bit.App.Pages
public BaseCipherViewModel()
{
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _fileService = ServiceContainer.Resolve();
_platformUtilsService = ServiceContainer.Resolve("platformUtilsService");
_auditService = ServiceContainer.Resolve("auditService");
_logger = ServiceContainer.Resolve("logger");
diff --git a/src/App/Pages/Vault/CipherAddEditPage.xaml.cs b/src/App/Pages/Vault/CipherAddEditPage.xaml.cs
index d84d4a733..60e6b667d 100644
--- a/src/App/Pages/Vault/CipherAddEditPage.xaml.cs
+++ b/src/App/Pages/Vault/CipherAddEditPage.xaml.cs
@@ -19,6 +19,7 @@ namespace Bit.App.Pages
private readonly AppOptions _appOptions;
private readonly IStateService _stateService;
private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IKeyConnectorService _keyConnectorService;
@@ -40,6 +41,7 @@ namespace Bit.App.Pages
{
_stateService = ServiceContainer.Resolve("stateService");
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
_vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService");
_keyConnectorService = ServiceContainer.Resolve("keyConnectorService");
@@ -350,8 +352,8 @@ namespace Bit.App.Pages
}
}
else if (Device.RuntimePlatform == Device.Android &&
- !_deviceActionService.AutofillAccessibilityServiceRunning() &&
- !_deviceActionService.AutofillServiceEnabled())
+ !_autofillHandler.AutofillAccessibilityServiceRunning() &&
+ !_autofillHandler.AutofillServiceEnabled())
{
await DisplayAlert(AppResources.BitwardenAutofillService,
AppResources.BitwardenAutofillServiceAlert2, AppResources.Ok);
diff --git a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
index 7be04f536..4f8ea42a1 100644
--- a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
+++ b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs
@@ -28,6 +28,7 @@ namespace Bit.App.Pages
private readonly IPolicyService _policyService;
private readonly ICustomFieldItemFactory _customFieldItemFactory;
private readonly IClipboardService _clipboardService;
+ private readonly IAutofillHandler _autofillHandler;
private bool _showNotesSeparator;
private bool _showPassword;
@@ -78,6 +79,7 @@ namespace Bit.App.Pages
_policyService = ServiceContainer.Resolve("policyService");
_customFieldItemFactory = ServiceContainer.Resolve("customFieldItemFactory");
_clipboardService = ServiceContainer.Resolve("clipboardService");
+ _autofillHandler = ServiceContainer.Resolve();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
@@ -508,7 +510,7 @@ namespace Bit.App.Pages
if (Page is CipherAddEditPage page && page.FromAutofillFramework)
{
// Close and go back to app
- _deviceActionService.CloseAutofill();
+ _autofillHandler.CloseAutofill();
}
else
{
diff --git a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs
index f6e7e3452..dcd994335 100644
--- a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs
+++ b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs
@@ -493,7 +493,7 @@ namespace Bit.App.Pages
}
var canOpenFile = true;
- if (!_deviceActionService.CanOpenFile(attachment.FileName))
+ if (!_fileService.CanOpenFile(attachment.FileName))
{
if (Device.RuntimePlatform == Device.iOS)
{
@@ -562,7 +562,7 @@ namespace Bit.App.Pages
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);
return;
@@ -573,7 +573,7 @@ namespace Bit.App.Pages
{
_attachmentData = data;
_attachmentFilename = attachment.FileName;
- if (!_deviceActionService.SaveFile(_attachmentData, null, _attachmentFilename, null))
+ if (!_fileService.SaveFile(_attachmentData, null, _attachmentFilename, null))
{
ClearAttachmentData();
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment);
@@ -582,7 +582,7 @@ namespace Bit.App.Pages
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();
_platformUtilsService.ShowToast("success", null, AppResources.SaveAttachmentSuccess);
diff --git a/src/App/Pages/Vault/CiphersPage.xaml.cs b/src/App/Pages/Vault/CiphersPage.xaml.cs
index db97763ab..610fb826c 100644
--- a/src/App/Pages/Vault/CiphersPage.xaml.cs
+++ b/src/App/Pages/Vault/CiphersPage.xaml.cs
@@ -1,8 +1,8 @@
using System;
using System.Linq;
-using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.App.Resources;
+using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using Xamarin.Forms;
@@ -12,7 +12,7 @@ namespace Bit.App.Pages
public partial class CiphersPage : BaseContentPage
{
private readonly string _autofillUrl;
- private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private CiphersPageViewModel _vm;
private bool _hasFocused;
@@ -48,7 +48,7 @@ namespace Bit.App.Pages
{
NavigationPage.SetTitleView(this, _titleLayout);
}
- _deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
}
public SearchBar SearchBar => _searchBar;
@@ -107,7 +107,7 @@ namespace Bit.App.Pages
}
else
{
- _deviceActionService.CloseAutofill();
+ _autofillHandler.CloseAutofill();
}
}
diff --git a/src/App/Pages/Vault/CiphersPageViewModel.cs b/src/App/Pages/Vault/CiphersPageViewModel.cs
index bbcc89d9f..aaa505d92 100644
--- a/src/App/Pages/Vault/CiphersPageViewModel.cs
+++ b/src/App/Pages/Vault/CiphersPageViewModel.cs
@@ -20,6 +20,7 @@ namespace Bit.App.Pages
private readonly ICipherService _cipherService;
private readonly ISearchService _searchService;
private readonly IDeviceActionService _deviceActionService;
+ private readonly IAutofillHandler _autofillHandler;
private readonly IStateService _stateService;
private readonly IPasswordRepromptService _passwordRepromptService;
private readonly IOrganizationService _organizationService;
@@ -37,6 +38,7 @@ namespace Bit.App.Pages
_cipherService = ServiceContainer.Resolve("cipherService");
_searchService = ServiceContainer.Resolve("searchService");
_deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ _autofillHandler = ServiceContainer.Resolve();
_stateService = ServiceContainer.Resolve("stateService");
_passwordRepromptService = ServiceContainer.Resolve("passwordRepromptService");
_organizationService = ServiceContainer.Resolve("organizationService");
@@ -196,7 +198,7 @@ namespace Bit.App.Pages
}
else
{
- _deviceActionService.Autofill(cipher);
+ _autofillHandler.Autofill(cipher);
}
}
}
diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
index cd626e2ee..45014b262 100644
--- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
+++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs
@@ -220,7 +220,7 @@ namespace Bit.App.Pages
NestedFolders = NestedFolders.GetRange(0, NestedFolders.Count - 1);
}
- var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
+ var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
var hasFavorites = FavoriteCiphers?.Any() ?? false;
if (hasFavorites)
{
@@ -400,7 +400,7 @@ namespace Bit.App.Pages
private void CreateCipherGroupedItems(List groupedItems)
{
- var uppercaseGroupNames = _deviceActionService.DeviceType == DeviceType.iOS;
+ var uppercaseGroupNames = Device.RuntimePlatform == Device.iOS;
_totpTickCts?.Cancel();
if (ShowTotp)
{
diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs
index 8c74d2e5f..bea6e52f1 100644
--- a/src/App/Services/MobilePlatformUtilsService.cs
+++ b/src/App/Services/MobilePlatformUtilsService.cs
@@ -72,8 +72,13 @@ namespace Bit.App.Services
});
}
+ ///
+ /// Gets the device type on the server enum
+ ///
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;
}
@@ -117,11 +122,6 @@ namespace Bit.App.Services
}
}
- public void SaveFile()
- {
- // TODO
- }
-
public string GetApplicationVersion()
{
return AppInfo.VersionString;
@@ -208,11 +208,6 @@ namespace Bit.App.Services
return (password, valid);
}
- public bool IsDev()
- {
- return Core.Utilities.CoreHelpers.InDebugMode();
- }
-
public bool IsSelfHost()
{
return false;
diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs
index e7dff41f2..07870b0e1 100644
--- a/src/App/Utilities/AppHelpers.cs
+++ b/src/App/Utilities/AppHelpers.cs
@@ -564,7 +564,7 @@ namespace Bit.App.Utilities
var sendService = ServiceContainer.Resolve("sendService");
var passwordGenerationService = ServiceContainer.Resolve(
"passwordGenerationService");
- var deviceActionService = ServiceContainer.Resolve("deviceActionService");
+ var fileService = ServiceContainer.Resolve();
var policyService = ServiceContainer.Resolve("policyService");
var searchService = ServiceContainer.Resolve("searchService");
var usernameGenerationService = ServiceContainer.Resolve(
@@ -572,7 +572,7 @@ namespace Bit.App.Utilities
await Task.WhenAll(
cipherService.ClearCacheAsync(),
- deviceActionService.ClearCacheAsync());
+ fileService.ClearCacheAsync());
tokenService.ClearCache();
cryptoService.ClearCache();
settingsService.ClearCache();
diff --git a/src/Core/Abstractions/IAutofillHandler.cs b/src/Core/Abstractions/IAutofillHandler.cs
new file mode 100644
index 000000000..84c9489b9
--- /dev/null
+++ b/src/Core/Abstractions/IAutofillHandler.cs
@@ -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();
+ }
+}
diff --git a/src/Core/Abstractions/IFileService.cs b/src/Core/Abstractions/IFileService.cs
new file mode 100644
index 000000000..6ddf5dabc
--- /dev/null
+++ b/src/Core/Abstractions/IFileService.cs
@@ -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();
+ }
+}
diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs
index e3c73d5ca..8bf2e0bed 100644
--- a/src/Core/Abstractions/IPlatformUtilsService.cs
+++ b/src/Core/Abstractions/IPlatformUtilsService.cs
@@ -8,15 +8,16 @@ namespace Bit.Core.Abstractions
public interface IPlatformUtilsService
{
string GetApplicationVersion();
+ ///
+ /// Gets the device type on the server enum
+ ///
DeviceType GetDevice();
string GetDeviceString();
ClientType GetClientType();
- bool IsDev();
bool IsSelfHost();
bool IsViewOpen();
void LaunchUri(string uri, Dictionary options = null);
Task ReadFromClipboardAsync(Dictionary options = null);
- void SaveFile();
Task ShowDialogAsync(string text, string title = null, string confirmText = null,
string cancelText = null, string type = null);
Task ShowPasswordDialogAsync(string title, string body, Func> validator);
diff --git a/src/Core/Utilities/LazyResolve.cs b/src/Core/Utilities/LazyResolve.cs
index 3fedfecd0..1e8a37ee1 100644
--- a/src/Core/Utilities/LazyResolve.cs
+++ b/src/Core/Utilities/LazyResolve.cs
@@ -2,8 +2,13 @@
namespace Bit.Core.Utilities
{
- public class LazyResolve : Lazy
+ public class LazyResolve : Lazy where T : class
{
+ public LazyResolve()
+ : base(() => ServiceContainer.Resolve())
+ {
+ }
+
public LazyResolve(string containerKey)
: base(() => ServiceContainer.Resolve(containerKey))
{
diff --git a/src/iOS.Core/Services/AutofillHandler.cs b/src/iOS.Core/Services/AutofillHandler.cs
new file mode 100644
index 000000000..ba12bed5e
--- /dev/null
+++ b/src/iOS.Core/Services/AutofillHandler.cs
@@ -0,0 +1,22 @@
+using System;
+using Bit.Core.Abstractions;
+using Bit.Core.Models.View;
+
+namespace Bit.iOS.Core.Services
+{
+ ///
+ /// This handler is only needed on Android for now, now this class acts as a stub so that dependency injection doesn't break
+ ///
+ 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();
+ }
+}
+
diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs
index 8f00f0b26..c2bec2753 100644
--- a/src/iOS.Core/Services/DeviceActionService.cs
+++ b/src/iOS.Core/Services/DeviceActionService.cs
@@ -1,20 +1,14 @@
using System;
-using System.IO;
using System.Linq;
-using System.Net;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
-using Bit.Core.Abstractions;
using Bit.Core.Enums;
-using Bit.Core.Models.View;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
using CoreGraphics;
using Foundation;
using LocalAuthentication;
-using MobileCoreServices;
-using Photos;
using UIKit;
using Xamarin.Forms;
@@ -22,20 +16,10 @@ namespace Bit.iOS.Core.Services
{
public class DeviceActionService : IDeviceActionService
{
- private readonly IStateService _stateService;
- private readonly IMessagingService _messagingService;
private Toast _toast;
private UIAlertController _progressAlert;
private string _userAgent;
- public DeviceActionService(
- IStateService stateService,
- IMessagingService messagingService)
- {
- _stateService = stateService;
- _messagingService = messagingService;
- }
-
public string DeviceUserAgent
{
get
@@ -120,91 +104,6 @@ namespace Bit.iOS.Core.Services
return result.Task;
}
- public bool OpenFile(byte[] fileData, string id, string fileName)
- {
- var filePath = Path.Combine(GetTempPath(), fileName);
- File.WriteAllBytes(filePath, fileData);
- var url = NSUrl.FromFilename(filePath);
- var viewer = UIDocumentInteractionController.FromUrl(url);
- var controller = GetVisibleViewController();
- var rect = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad ?
- new CGRect(100, 5, 320, 320) : controller.View.Frame;
- return viewer.PresentOpenInMenu(rect, controller.View, true);
- }
-
- public bool CanOpenFile(string fileName)
- {
- // Not sure of a way to check this ahead of time on iOS
- return true;
- }
-
- public 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 DisplayPromptAync(string title = null, string description = null,
string text = null, string okButtonText = null, string cancelButtonText = null,
bool numericKeyboard = false, bool autofocus = true, bool password = false)
@@ -298,11 +197,6 @@ namespace Bit.iOS.Core.Services
return true;
}
- public bool SupportsAutofillService()
- {
- return true;
- }
-
public int SystemMajorVersion()
{
var versionParts = UIDevice.CurrentDevice.SystemVersion.Split('.');
@@ -391,46 +285,6 @@ namespace Bit.iOS.Core.Services
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()
{
return NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString();
@@ -479,78 +333,6 @@ namespace Bit.iOS.Core.Services
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(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()
{
var window = UIApplication.SharedApplication.KeyWindow;
@@ -569,43 +351,6 @@ namespace Bit.iOS.Core.Services
(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()
{
throw new NotImplementedException();
@@ -629,21 +374,6 @@ namespace Bit.iOS.Core.Services
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()
{
var url = new NSUrl(UIApplication.OpenSettingsUrlString);
diff --git a/src/iOS.Core/Services/FileService.cs b/src/iOS.Core/Services/FileService.cs
new file mode 100644
index 000000000..bac060824
--- /dev/null
+++ b/src/iOS.Core/Services/FileService.cs
@@ -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(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);
+ }
+ }
+ }
+}
diff --git a/src/iOS.Core/Utilities/UIViewControllerExtensions.cs b/src/iOS.Core/Utilities/UIViewControllerExtensions.cs
new file mode 100644
index 000000000..c29025813
--- /dev/null
+++ b/src/iOS.Core/Utilities/UIViewControllerExtensions.cs
@@ -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);
+ }
+ }
+}
+
diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs
index 3d7b556ef..4d6a13c6c 100644
--- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs
+++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs
@@ -102,7 +102,8 @@ namespace Bit.iOS.Core.Utilities
var stateService = new StateService(mobileStorageService, secureStorageService, messagingService);
var stateMigrationService =
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 platformUtilsService = new MobilePlatformUtilsService(deviceActionService, clipboardService,
messagingService, broadcasterService);
@@ -121,6 +122,8 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Register("stateService", stateService);
ServiceContainer.Register("stateMigrationService", stateMigrationService);
ServiceContainer.Register("deviceActionService", deviceActionService);
+ ServiceContainer.Register(fileService);
+ ServiceContainer.Register(new AutofillHandler());
ServiceContainer.Register("clipboardService", clipboardService);
ServiceContainer.Register("platformUtilsService", platformUtilsService);
ServiceContainer.Register("biometricService", biometricService);
diff --git a/src/iOS.Core/iOS.Core.csproj b/src/iOS.Core/iOS.Core.csproj
index 76114db6a..a4515c9b8 100644
--- a/src/iOS.Core/iOS.Core.csproj
+++ b/src/iOS.Core/iOS.Core.csproj
@@ -204,6 +204,9 @@
+
+
+