[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
This commit is contained in:
André Bispo 2022-12-06 16:30:46 +00:00 committed by GitHub
parent eaa4f193ce
commit 2a60ff62d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 28 deletions

View file

@ -263,12 +263,6 @@ namespace Bit.App.Pages
{ {
if (DoOnce()) if (DoOnce())
{ {
var cameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera());
if (cameraPermission != PermissionStatus.Granted)
{
return;
}
var page = new ScanPage(key => var page = new ScanPage(key =>
{ {
Device.BeginInvokeOnMainThread(async () => Device.BeginInvokeOnMainThread(async () =>
@ -277,6 +271,7 @@ namespace Bit.App.Pages
await _vm.UpdateTotpKeyAsync(key); await _vm.UpdateTotpKeyAsync(key);
}); });
}); });
await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page)); await Navigation.PushModalAsync(new Xamarin.Forms.NavigationPage(page));
} }
} }

View file

@ -34,16 +34,13 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<zxing:ZXingScannerView <ContentView
x:Name="_zxing" x:Name="_scannerContainer"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
AutomationId="zxingScannerView" AutomationId="zxingScannerView"
IsVisible="{Binding ShowScanner}" IsVisible="{Binding ShowScanner}"
Grid.Column="0" Grid.Column="0"
Grid.Row="0" Grid.Row="0"
Grid.RowSpan="3" Grid.RowSpan="3"/>
OnScanResult="OnScanResult"/>
<StackLayout <StackLayout
VerticalOptions="Center" VerticalOptions="Center"
HorizontalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"

View file

@ -8,8 +8,10 @@ using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using SkiaSharp; using SkiaSharp;
using SkiaSharp.Views.Forms; using SkiaSharp.Views.Forms;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials; using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
using ZXing.Net.Mobile.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@ -26,20 +28,15 @@ namespace Bit.App.Pages
private bool _pageIsActive; private bool _pageIsActive;
private bool _qrcodeFound; private bool _qrcodeFound;
private float _scale; private float _scale;
private ZXingScannerView _zxing;
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger"); private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
public ScanPage(Action<string> callback) public ScanPage(Action<string> callback)
{ {
_callback = callback;
InitializeComponent(); InitializeComponent();
_zxing.Options = new ZXing.Mobile.MobileBarcodeScanningOptions _callback = callback;
{ ViewModel.InitScannerCommand = new Command(() => InitScanner());
UseNativeScanning = true,
PossibleFormats = new List<ZXing.BarcodeFormat> { ZXing.BarcodeFormat.QR_CODE },
AutoRotate = false,
TryInverted = true
};
if (Device.RuntimePlatform == Device.Android) if (Device.RuntimePlatform == Device.Android)
{ {
ToolbarItems.RemoveAt(0); ToolbarItems.RemoveAt(0);
@ -55,6 +52,53 @@ namespace Bit.App.Pages
protected override void OnAppearing() protected override void OnAppearing()
{ {
base.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> { 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; _zxing.IsScanning = true;
// Fix for Autofocus, now it's done every 2 seconds so that the user does't have to do it // 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(); AnimationLoopAsync();
} }
protected override async void OnDisappearing() private async Task StopScanner()
{ {
if (_zxing == null)
{
return;
}
_autofocusCts?.Cancel(); _autofocusCts?.Cancel();
if (_continuousAutofocusTask != null) if (_continuousAutofocusTask != null)
{ {
await _continuousAutofocusTask; await _continuousAutofocusTask;
} }
_zxing.IsScanning = false; _zxing.IsScanning = false;
_zxing.OnScanResult -= OnScanResult;
_pageIsActive = false; _pageIsActive = false;
base.OnDisappearing();
} }
private async void OnScanResult(ZXing.Result result) private async void OnScanResult(ZXing.Result result)

View file

@ -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.App.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
using Xamarin.Essentials;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.App.Pages namespace Bit.App.Pages
@ -9,13 +17,46 @@ namespace Bit.App.Pages
{ {
private bool _showScanner = true; private bool _showScanner = true;
private string _totpAuthenticationKey; private string _totpAuthenticationKey;
private IPlatformUtilsService _platformUtilsService;
private IDeviceActionService _deviceActionService;
private ILogger _logger;
public ScanPageViewModel() public ScanPageViewModel()
{ {
ToggleScanModeCommand = new Command(() => ShowScanner = !ShowScanner); ToggleScanModeCommand = new AsyncCommand(ToggleScanMode, onException: HandleException);
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_logger = ServiceContainer.Resolve<ILogger>();
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 ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner;
public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered; public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered;
public string TotpAuthenticationKey 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 public FormattedString ToggleScanModeLabel
{ {
get get
@ -57,5 +115,15 @@ namespace Bit.App.Pages
return fs; return fs;
} }
} }
private void HandleException(Exception ex)
{
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
{
await _deviceActionService.HideLoadingAsync();
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
}).FireAndForget();
_logger.Exception(ex);
}
} }
} }

View file

@ -2128,6 +2128,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Enable camera permission to use the scanner.
/// </summary>
public static string EnableCamerPermissionToUseTheScanner {
get {
return ResourceManager.GetString("EnableCamerPermissionToUseTheScanner", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Enabled. /// Looks up a localized string similar to Enabled.
/// </summary> /// </summary>
@ -3544,7 +3553,7 @@ namespace Bit.App.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Enterprise Single Sign-On. /// Looks up a localized string similar to Enterprise single sign-on.
/// </summary> /// </summary>
public static string LogInSso { public static string LogInSso {
get { get {
@ -3589,7 +3598,7 @@ namespace Bit.App.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Log In with master password. /// Looks up a localized string similar to Log in with master password.
/// </summary> /// </summary>
public static string LogInWithMasterPassword { public static string LogInWithMasterPassword {
get { get {

View file

@ -2512,4 +2512,7 @@ Do you want to switch to this account?</value>
<data name="ThisRequestIsNoLongerValid" xml:space="preserve"> <data name="ThisRequestIsNoLongerValid" xml:space="preserve">
<value>This request is no longer valid</value> <value>This request is no longer valid</value>
</data> </data>
<data name="EnableCamerPermissionToUseTheScanner" xml:space="preserve">
<value>Enable camera permission to use the scanner</value>
</data>
</root> </root>