From 2a60ff62d894776ac33a8e08527ece70953e4910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bispo?= Date: Tue, 6 Dec 2022 16:30:46 +0000 Subject: [PATCH] [SG-601] Enhance experience when user denies camera access in Authenticator (#2205) * [SG-601] Handle camera denied permissions * [SG-601] Code format * [SG-601] PR Fixes * [SG-601] Change resource text to singular * [SG-601] Remove horizontal and vertical options * [SG-601] Add start and stop scanner methods * [SG-601] Remove parameter from ScanPage * [SG-601] Move initialization to viewmodel * [SG-601] Fix zxing scanning bug * [SG-601] Move RunUIThread inside of method --- src/App/Pages/Vault/CipherAddEditPage.xaml.cs | 7 +- src/App/Pages/Vault/ScanPage.xaml | 9 +-- src/App/Pages/Vault/ScanPage.xaml.cs | 71 +++++++++++++++--- src/App/Pages/Vault/ScanPageViewModel.cs | 74 ++++++++++++++++++- src/App/Resources/AppResources.Designer.cs | 13 +++- src/App/Resources/AppResources.resx | 3 + 6 files changed, 149 insertions(+), 28 deletions(-) diff --git a/src/App/Pages/Vault/CipherAddEditPage.xaml.cs b/src/App/Pages/Vault/CipherAddEditPage.xaml.cs index 60e6b667d..59a246529 100644 --- a/src/App/Pages/Vault/CipherAddEditPage.xaml.cs +++ b/src/App/Pages/Vault/CipherAddEditPage.xaml.cs @@ -263,12 +263,6 @@ namespace Bit.App.Pages { if (DoOnce()) { - var cameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera()); - if (cameraPermission != PermissionStatus.Granted) - { - return; - } - var page = new ScanPage(key => { Device.BeginInvokeOnMainThread(async () => @@ -277,6 +271,7 @@ namespace Bit.App.Pages await _vm.UpdateTotpKeyAsync(key); }); }); + await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page)); } } diff --git a/src/App/Pages/Vault/ScanPage.xaml b/src/App/Pages/Vault/ScanPage.xaml index 9c1b31346..c1ac0e7b7 100644 --- a/src/App/Pages/Vault/ScanPage.xaml +++ b/src/App/Pages/Vault/ScanPage.xaml @@ -34,16 +34,13 @@ - + Grid.RowSpan="3"/> _logger = new LazyResolve("logger"); public ScanPage(Action callback) { - _callback = callback; InitializeComponent(); - _zxing.Options = new ZXing.Mobile.MobileBarcodeScanningOptions - { - UseNativeScanning = true, - PossibleFormats = new List { ZXing.BarcodeFormat.QR_CODE }, - AutoRotate = false, - TryInverted = true - }; + _callback = callback; + ViewModel.InitScannerCommand = new Command(() => InitScanner()); + if (Device.RuntimePlatform == Device.Android) { ToolbarItems.RemoveAt(0); @@ -55,6 +52,53 @@ namespace Bit.App.Pages protected override void OnAppearing() { base.OnAppearing(); + StartScanner(); + } + + protected override void OnDisappearing() + { + StopScanner().FireAndForget(); + base.OnDisappearing(); + } + + // Fix known bug with DelayBetweenAnalyzingFrames & DelayBetweenContinuousScans: https://github.com/Redth/ZXing.Net.Mobile/issues/721 + private void InitScanner() + { + try + { + if (!ViewModel.HasCameraPermission || !ViewModel.ShowScanner || _zxing != null) + { + return; + } + + _zxing = new ZXingScannerView(); + _zxing.Options = new ZXing.Mobile.MobileBarcodeScanningOptions + { + UseNativeScanning = true, + PossibleFormats = new List { ZXing.BarcodeFormat.QR_CODE }, + AutoRotate = false, + TryInverted = true, + DelayBetweenAnalyzingFrames = 5, + DelayBetweenContinuousScans = 5 + }; + _scannerContainer.Content = _zxing; + StartScanner(); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } + } + + private void StartScanner() + { + if (_zxing == null) + { + return; + } + + _zxing.OnScanResult -= OnScanResult; + _zxing.OnScanResult += OnScanResult; _zxing.IsScanning = true; // Fix for Autofocus, now it's done every 2 seconds so that the user does't have to do it @@ -98,16 +142,21 @@ namespace Bit.App.Pages AnimationLoopAsync(); } - protected override async void OnDisappearing() + private async Task StopScanner() { + if (_zxing == null) + { + return; + } + _autofocusCts?.Cancel(); if (_continuousAutofocusTask != null) { await _continuousAutofocusTask; } _zxing.IsScanning = false; + _zxing.OnScanResult -= OnScanResult; _pageIsActive = false; - base.OnDisappearing(); } private async void OnScanResult(ZXing.Result result) diff --git a/src/App/Pages/Vault/ScanPageViewModel.cs b/src/App/Pages/Vault/ScanPageViewModel.cs index 00405756b..74b2d1a4c 100644 --- a/src/App/Pages/Vault/ScanPageViewModel.cs +++ b/src/App/Pages/Vault/ScanPageViewModel.cs @@ -1,6 +1,14 @@ -using Bit.App.Resources; +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Bit.App.Abstractions; +using Bit.App.Resources; using Bit.App.Utilities; using Bit.Core.Abstractions; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Essentials; using Xamarin.Forms; namespace Bit.App.Pages @@ -9,13 +17,46 @@ namespace Bit.App.Pages { private bool _showScanner = true; private string _totpAuthenticationKey; + private IPlatformUtilsService _platformUtilsService; + private IDeviceActionService _deviceActionService; + private ILogger _logger; public ScanPageViewModel() { - ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner); + ToggleScanModeCommand = new AsyncCommand(ToggleScanMode, onException: HandleException); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _logger = ServiceContainer.Resolve(); + InitAsync().FireAndForget(); } - public Command ToggleScanModeCommand { get; set; } + public async Task InitAsync() + { + try + { + await Device.InvokeOnMainThreadAsync(async () => + { + var hasCameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera()); + HasCameraPermission = hasCameraPermission == PermissionStatus.Granted; + ShowScanner = hasCameraPermission == PermissionStatus.Granted; + }); + + if (!HasCameraPermission) + { + return; + } + InitScannerCommand.Execute(null); + } + catch (System.Exception ex) + { + HandleException(ex); + } + } + + public ICommand ToggleScanModeCommand { get; set; } + public ICommand InitScannerCommand { get; set; } + + public bool HasCameraPermission { get; set; } public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner; public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered; public string TotpAuthenticationKey @@ -39,6 +80,23 @@ namespace Bit.App.Pages }); } + private async Task ToggleScanMode() + { + var cameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera()); + HasCameraPermission = cameraPermission == PermissionStatus.Granted; + if (!HasCameraPermission) + { + var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(AppResources.EnableCamerPermissionToUseTheScanner, title: string.Empty, confirmText: AppResources.Settings, cancelText: AppResources.NoThanks); + if (openAppSettingsResult) + { + _deviceActionService.OpenAppSettings(); + } + return; + } + ShowScanner = !ShowScanner; + InitScannerCommand.Execute(null); + } + public FormattedString ToggleScanModeLabel { get @@ -57,5 +115,15 @@ namespace Bit.App.Pages return fs; } } + + private void HandleException(Exception ex) + { + Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () => + { + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage); + }).FireAndForget(); + _logger.Exception(ex); + } } } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index ff83de8a7..ca0e23b08 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2128,6 +2128,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Enable camera permission to use the scanner. + /// + public static string EnableCamerPermissionToUseTheScanner { + get { + return ResourceManager.GetString("EnableCamerPermissionToUseTheScanner", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enabled. /// @@ -3544,7 +3553,7 @@ namespace Bit.App.Resources { } /// - /// Looks up a localized string similar to Enterprise Single Sign-On. + /// Looks up a localized string similar to Enterprise single sign-on. /// public static string LogInSso { get { @@ -3589,7 +3598,7 @@ namespace Bit.App.Resources { } /// - /// Looks up a localized string similar to Log In with master password. + /// Looks up a localized string similar to Log in with master password. /// public static string LogInWithMasterPassword { get { diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 3a651bb62..82ed75f6b 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2512,4 +2512,7 @@ Do you want to switch to this account? This request is no longer valid + + Enable camera permission to use the scanner +