mirror of
https://github.com/bitwarden/mobile.git
synced 2024-09-27 03:52:57 +02:00
2a60ff62d8
* [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
305 lines
11 KiB
C#
305 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Bit.App.Utilities;
|
|
using Bit.Core.Abstractions;
|
|
using Bit.Core.Utilities;
|
|
using SkiaSharp;
|
|
using SkiaSharp.Views.Forms;
|
|
using Xamarin.CommunityToolkit.ObjectModel;
|
|
using Xamarin.Essentials;
|
|
using Xamarin.Forms;
|
|
using ZXing.Net.Mobile.Forms;
|
|
|
|
namespace Bit.App.Pages
|
|
{
|
|
public partial class ScanPage : BaseContentPage
|
|
{
|
|
private ScanPageViewModel ViewModel => BindingContext as ScanPageViewModel;
|
|
private readonly Action<string> _callback;
|
|
private CancellationTokenSource _autofocusCts;
|
|
private Task _continuousAutofocusTask;
|
|
private readonly Color _greenColor;
|
|
private readonly SKColor _blueSKColor;
|
|
private readonly SKColor _greenSKColor;
|
|
private readonly Stopwatch _stopwatch;
|
|
private bool _pageIsActive;
|
|
private bool _qrcodeFound;
|
|
private float _scale;
|
|
private ZXingScannerView _zxing;
|
|
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
|
|
|
|
public ScanPage(Action<string> callback)
|
|
{
|
|
InitializeComponent();
|
|
_callback = callback;
|
|
ViewModel.InitScannerCommand = new Command(() => InitScanner());
|
|
|
|
if (Device.RuntimePlatform == Device.Android)
|
|
{
|
|
ToolbarItems.RemoveAt(0);
|
|
}
|
|
|
|
_greenColor = ThemeManager.GetResourceColor("SuccessColor");
|
|
_greenSKColor = _greenColor.ToSKColor();
|
|
_blueSKColor = ThemeManager.GetResourceColor("PrimaryColor").ToSKColor();
|
|
_stopwatch = new Stopwatch();
|
|
_qrcodeFound = false;
|
|
}
|
|
|
|
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> { 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
|
|
// https://github.com/Redth/ZXing.Net.Mobile/issues/414
|
|
_autofocusCts?.Cancel();
|
|
_autofocusCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
|
|
|
|
var autofocusCts = _autofocusCts;
|
|
// this task is needed to be awaited OnDisappearing to avoid some crashes
|
|
// when changing the value of _zxing.IsScanning
|
|
_continuousAutofocusTask = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (!autofocusCts.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(2), autofocusCts.Token);
|
|
await Device.InvokeOnMainThreadAsync(() =>
|
|
{
|
|
if (!autofocusCts.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
_zxing.AutoFocus();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Value.Exception(ex);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
catch (TaskCanceledException) { }
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Value.Exception(ex);
|
|
}
|
|
}, autofocusCts.Token);
|
|
_pageIsActive = true;
|
|
AnimationLoopAsync();
|
|
}
|
|
|
|
private async Task StopScanner()
|
|
{
|
|
if (_zxing == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_autofocusCts?.Cancel();
|
|
if (_continuousAutofocusTask != null)
|
|
{
|
|
await _continuousAutofocusTask;
|
|
}
|
|
_zxing.IsScanning = false;
|
|
_zxing.OnScanResult -= OnScanResult;
|
|
_pageIsActive = false;
|
|
}
|
|
|
|
private async void OnScanResult(ZXing.Result result)
|
|
{
|
|
try
|
|
{
|
|
// Stop analysis until we navigate away so we don't keep reading barcodes
|
|
_zxing.IsAnalyzing = false;
|
|
var text = result?.Text;
|
|
if (!string.IsNullOrWhiteSpace(text))
|
|
{
|
|
if (text.StartsWith("otpauth://totp"))
|
|
{
|
|
await QrCodeFoundAsync();
|
|
_callback(text);
|
|
return;
|
|
}
|
|
else if (Uri.TryCreate(text, UriKind.Absolute, out Uri uri) &&
|
|
!string.IsNullOrWhiteSpace(uri?.Query))
|
|
{
|
|
var queryParts = uri.Query.Substring(1).ToLowerInvariant().Split('&');
|
|
foreach (var part in queryParts)
|
|
{
|
|
if (part.StartsWith("secret="))
|
|
{
|
|
await QrCodeFoundAsync();
|
|
var subResult = part.Substring(7);
|
|
if (!string.IsNullOrEmpty(subResult))
|
|
{
|
|
_callback(subResult.ToUpperInvariant());
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_callback(null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Value?.Exception(ex);
|
|
}
|
|
}
|
|
|
|
private async Task QrCodeFoundAsync()
|
|
{
|
|
_qrcodeFound = true;
|
|
Vibration.Vibrate();
|
|
await Task.Delay(1000);
|
|
_zxing.IsScanning = false;
|
|
}
|
|
|
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
|
{
|
|
if (DoOnce())
|
|
{
|
|
await Navigation.PopModalAsync();
|
|
}
|
|
}
|
|
|
|
private void AddAuthenticationKey_OnClicked(object sender, EventArgs e)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(ViewModel.TotpAuthenticationKey))
|
|
{
|
|
_callback(ViewModel.TotpAuthenticationKey);
|
|
return;
|
|
}
|
|
_callback(null);
|
|
}
|
|
|
|
private void ToggleScanMode_OnTapped(object sender, EventArgs e)
|
|
{
|
|
ViewModel.ToggleScanModeCommand.Execute(null);
|
|
if (!ViewModel.ShowScanner)
|
|
{
|
|
_authenticationKeyEntry.Focus();
|
|
}
|
|
}
|
|
|
|
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
|
|
{
|
|
var info = args.Info;
|
|
var surface = args.Surface;
|
|
var canvas = surface.Canvas;
|
|
var margins = 20;
|
|
var maxSquareSize = (Math.Min(info.Height, info.Width) * 0.9f - margins) * _scale;
|
|
var squareSize = maxSquareSize;
|
|
var lineSize = squareSize * 0.15f;
|
|
var startXPoint = (info.Width / 2) - (squareSize / 2);
|
|
var startYPoint = (info.Height / 2) - (squareSize / 2);
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
using (var strokePaint = new SKPaint
|
|
{
|
|
Color = _qrcodeFound ? _greenSKColor : _blueSKColor,
|
|
StrokeWidth = 9 * _scale,
|
|
StrokeCap = SKStrokeCap.Round,
|
|
})
|
|
{
|
|
canvas.Scale(1, 1);
|
|
//top left
|
|
canvas.DrawLine(startXPoint, startYPoint, startXPoint, startYPoint + lineSize, strokePaint);
|
|
canvas.DrawLine(startXPoint, startYPoint, startXPoint + lineSize, startYPoint, strokePaint);
|
|
//bot left
|
|
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint, startYPoint + squareSize - lineSize, strokePaint);
|
|
canvas.DrawLine(startXPoint, startYPoint + squareSize, startXPoint + lineSize, startYPoint + squareSize, strokePaint);
|
|
//top right
|
|
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize - lineSize, startYPoint, strokePaint);
|
|
canvas.DrawLine(startXPoint + squareSize, startYPoint, startXPoint + squareSize, startYPoint + lineSize, strokePaint);
|
|
//bot right
|
|
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize - lineSize, startYPoint + squareSize, strokePaint);
|
|
canvas.DrawLine(startXPoint + squareSize, startYPoint + squareSize, startXPoint + squareSize, startYPoint + squareSize - lineSize, strokePaint);
|
|
}
|
|
}
|
|
|
|
async Task AnimationLoopAsync()
|
|
{
|
|
try
|
|
{
|
|
_stopwatch.Start();
|
|
while (_pageIsActive)
|
|
{
|
|
var t = _stopwatch.Elapsed.TotalSeconds % 2 / 2;
|
|
_scale = (20 - (1 - (float)Math.Sin(4 * Math.PI * t))) / 20;
|
|
SkCanvasView.InvalidateSurface();
|
|
await Task.Delay(TimeSpan.FromSeconds(1.0 / 30));
|
|
if (_qrcodeFound && _scale > 0.98f)
|
|
{
|
|
_checkIcon.TextColor = _greenColor;
|
|
SkCanvasView.InvalidateSurface();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Value?.Exception(ex);
|
|
}
|
|
finally
|
|
{
|
|
_stopwatch?.Stop();
|
|
}
|
|
}
|
|
}
|
|
}
|