using System; using System.Collections.Generic; using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Models; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; using Plugin.Fingerprint; using Plugin.Fingerprint.Abstractions; using Xamarin.Essentials; using Xamarin.Forms; namespace Bit.App.Services { public class MobilePlatformUtilsService : IPlatformUtilsService { private static readonly Random _random = new Random(); private const int DialogPromiseExpiration = 600000; // 10 minutes private readonly IDeviceActionService _deviceActionService; private readonly IMessagingService _messagingService; private readonly IBroadcasterService _broadcasterService; private readonly Dictionary, DateTime>> _showDialogResolves = new Dictionary, DateTime>>(); public MobilePlatformUtilsService( IDeviceActionService deviceActionService, IMessagingService messagingService, IBroadcasterService broadcasterService) { _deviceActionService = deviceActionService; _messagingService = messagingService; _broadcasterService = broadcasterService; } public void Init() { _broadcasterService.Subscribe(nameof(MobilePlatformUtilsService), (message) => { if (message.Command == "showDialogResolve") { var details = message.Data as Tuple; var dialogId = details.Item1; var confirmed = details.Item2; if (_showDialogResolves.ContainsKey(dialogId)) { var resolveObj = _showDialogResolves[dialogId].Item1; resolveObj.TrySetResult(confirmed); } // Clean up old tasks var deleteIds = new HashSet(); foreach (var item in _showDialogResolves) { var age = DateTime.UtcNow - item.Value.Item2; if (age.TotalMilliseconds > DialogPromiseExpiration) { deleteIds.Add(item.Key); } } foreach (var id in deleteIds) { _showDialogResolves.Remove(id); } } }); } public Core.Enums.DeviceType GetDevice() { return _deviceActionService.DeviceType; } public string GetDeviceString() { return DeviceInfo.Model; } public ClientType GetClientType() { return ClientType.Mobile; } public bool IsViewOpen() { return false; } public void LaunchUri(string uri, Dictionary options = null) { if ((uri.StartsWith("http://") || uri.StartsWith("https://")) && Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri)) { try { Browser.OpenAsync(uri, BrowserLaunchMode.External); } catch (FeatureNotSupportedException) { } } else { var launched = false; if (GetDevice() == Core.Enums.DeviceType.Android && uri.StartsWith("androidapp://")) { launched = _deviceActionService.LaunchApp(uri); } if (!launched && (options?.ContainsKey("page") ?? false)) { (options["page"] as Page).DisplayAlert(null, "", ""); // TODO } } } public void SaveFile() { // TODO } public string GetApplicationVersion() { return AppInfo.VersionString; } public bool SupportsDuo() { return true; } public bool SupportsFido2() { return _deviceActionService.SupportsFido2(); } public void ShowToast(string type, string title, string text, Dictionary options = null) { ShowToast(type, title, new string[] { text }, options); } public void ShowToast(string type, string title, string[] text, Dictionary options = null) { if (text.Length > 0) { var longDuration = options != null && options.ContainsKey("longDuration") ? (bool)options["longDuration"] : false; _deviceActionService.Toast(text[0], longDuration); } } public Task ShowDialogAsync(string text, string title = null, string confirmText = null, string cancelText = null, string type = null) { var dialogId = 0; lock (_random) { dialogId = _random.Next(0, int.MaxValue); } _messagingService.Send("showDialog", new DialogDetails { Text = text, Title = title, ConfirmText = confirmText, CancelText = cancelText, Type = type, DialogId = dialogId }); var tcs = new TaskCompletionSource(); _showDialogResolves.Add(dialogId, new Tuple, DateTime>(tcs, DateTime.UtcNow)); return tcs.Task; } public async Task ShowPasswordDialogAsync(string title, string body, Func> validator) { return (await ShowPasswordDialogAndGetItAsync(title, body, validator)).valid; } public async Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func> validator) { var password = await _deviceActionService.DisplayPromptAync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, null, AppResources.Submit, AppResources.Cancel, password: true); if (password == null) { return (password, false); } var valid = await validator(password); if (!valid) { await ShowDialogAsync(AppResources.InvalidMasterPassword, null, AppResources.Ok); } return (password, valid); } public bool IsDev() { return Core.Utilities.CoreHelpers.InDebugMode(); } public bool IsSelfHost() { return false; } public async Task ReadFromClipboardAsync(Dictionary options = null) { return await Clipboard.GetTextAsync(); } public async Task SupportsBiometricAsync() { try { return await CrossFingerprint.Current.IsAvailableAsync(); } catch { return false; } } public async Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null) { try { if (text == null) { text = AppResources.BiometricsDirection; if (Device.RuntimePlatform == Device.iOS) { var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection; } } var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text) { CancelTitle = AppResources.Cancel, FallbackTitle = fallbackText }; var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest); if (result.Authenticated) { return true; } if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested) { fallback?.Invoke(); } } catch { } return false; } public long GetActiveTime() { return _deviceActionService.GetActiveTime(); } } }