[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:
Federico Maccaroni 2022-10-11 18:19:32 -03:00 committed by GitHub
parent d800e9a43e
commit ba677a96aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 883 additions and 798 deletions

View file

@ -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>

View file

@ -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
{ {

View file

@ -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);

View 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);
}
}
}
}
}
}

View file

@ -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;

View 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;
}
}
}

View file

@ -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();

View file

@ -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());
} }
} }

View file

@ -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()

View file

@ -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(

View file

@ -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()

View file

@ -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"));

View file

@ -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
{ {

View file

@ -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)))
}); });
} }

View file

@ -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)

View file

@ -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);
} }
} }
} }

View file

@ -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");

View file

@ -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);

View file

@ -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
{ {

View file

@ -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);

View file

@ -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();
} }
} }

View file

@ -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);
} }
} }
} }

View file

@ -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)
{ {

View file

@ -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;

View file

@ -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();

View 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();
}
}

View 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();
}
}

View file

@ -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);

View file

@ -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))
{ {

View 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();
}
}

View file

@ -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);

View 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);
}
}
}
}

View 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);
}
}
}

View file

@ -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);

View file

@ -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">