diff --git a/src/App/App.csproj b/src/App/App.csproj index fe91346d5..a33e55f32 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -71,6 +71,7 @@ + @@ -88,9 +89,7 @@ - - diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 36f99225a..4ee727a47 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -45,11 +45,11 @@ + - @@ -70,16 +70,12 @@ - - - - diff --git a/src/Core/MauiProgram.cs b/src/Core/MauiProgram.cs index d86e20525..ba2b313c7 100644 --- a/src/Core/MauiProgram.cs +++ b/src/Core/MauiProgram.cs @@ -1,9 +1,9 @@ -using CommunityToolkit.Maui; +using Camera.MAUI; +using CommunityToolkit.Maui; using FFImageLoading.Maui; using Microsoft.Extensions.Logging; using Microsoft.Maui.Controls.Compatibility.Hosting; using SkiaSharp.Views.Maui.Controls.Hosting; -using ZXing.Net.Maui.Controls; using AppEffects = Bit.App.Effects; namespace Bit.Core; @@ -20,7 +20,7 @@ public static class MauiProgram builder .UseMauiCommunityToolkit() .UseMauiCompatibility() - .UseBarcodeReader() + .UseMauiCameraView() .UseSkiaSharp() .UseFFImageLoading() .ConfigureEffects(effects => diff --git a/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs b/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs index c8308442f..a8e0d5f66 100644 --- a/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs +++ b/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Bit.App.Abstractions; +using Bit.App.Abstractions; using Bit.App.Models; using Bit.Core.Resources.Localization; using Bit.App.Utilities; @@ -9,8 +7,6 @@ using Bit.Core.Enums; using Bit.Core.Utilities; using Microsoft.Maui.Controls.PlatformConfiguration; using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; -using Microsoft.Maui.Controls; -using Microsoft.Maui; namespace Bit.App.Pages { @@ -62,14 +58,12 @@ namespace Bit.App.Pages _vm.CipherDetailsPage = cipherDetailsPage; _vm.Init(); SetActivityIndicator(); - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (_vm.EditMode && !_vm.CloneMode && Device.RuntimePlatform == Device.Android) + if (_vm.EditMode && !_vm.CloneMode && DeviceInfo.Platform == DevicePlatform.Android) { ToolbarItems.Add(_attachmentsItem); ToolbarItems.Add(_deleteItem); } - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.iOS) + if (DeviceInfo.Platform == DevicePlatform.iOS) { ToolbarItems.Add(_closeItem); if (_vm.EditMode && !_vm.CloneMode) @@ -267,7 +261,7 @@ namespace Bit.App.Pages { var page = new ScanPage(key => { - Device.BeginInvokeOnMainThread(async () => + MainThread.BeginInvokeOnMainThread(async () => { await Navigation.PopModalAsync(); await _vm.UpdateTotpKeyAsync(key); @@ -335,8 +329,7 @@ namespace Bit.App.Pages if (_vm.Cipher.Type == CipherType.Login && !_fromAutofill && !addLoginShown.GetValueOrDefault()) { await _stateService.SetAddSitePromptShownAsync(true); - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.iOS) + if (DeviceInfo.Platform == DevicePlatform.iOS) { if (_deviceActionService.SystemMajorVersion() < 12) { @@ -349,9 +342,9 @@ namespace Bit.App.Pages AppResources.BitwardenAutofillAlert2, AppResources.Ok); } } - else if (Device.RuntimePlatform == Device.Android && - !_autofillHandler.AutofillAccessibilityServiceRunning() && - !_autofillHandler.AutofillServiceEnabled()) + else if (DeviceInfo.Platform == DevicePlatform.Android && + !_autofillHandler.AutofillAccessibilityServiceRunning() && + !_autofillHandler.AutofillServiceEnabled()) { await DisplayAlert(AppResources.BitwardenAutofillService, AppResources.BitwardenAutofillServiceAlert2, AppResources.Ok); @@ -362,8 +355,7 @@ namespace Bit.App.Pages private void AdjustToolbar() { - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if ((_vm.EditMode || _vm.CloneMode) && Device.RuntimePlatform == Device.Android) + if ((_vm.EditMode || _vm.CloneMode) && DeviceInfo.Platform == DevicePlatform.Android) { if (_vm.Cipher == null) { diff --git a/src/Core/Pages/Vault/ScanPage.xaml b/src/Core/Pages/Vault/ScanPage.xaml index b22e53744..d7675d4c9 100644 --- a/src/Core/Pages/Vault/ScanPage.xaml +++ b/src/Core/Pages/Vault/ScanPage.xaml @@ -7,8 +7,8 @@ xmlns:u="clr-namespace:Bit.App.Utilities" xmlns:controls="clr-namespace:Bit.App.Controls" xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls" - xmlns:core="clr-namespace:Bit.Core" - xmlns:zxing="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI.Controls" + xmlns:core="clr-namespace:Bit.Core" + xmlns:maui="clr-namespace:Camera.MAUI;assembly=Camera.MAUI" x:Name="_page" Title="{Binding ScanQrPageTitle}"> @@ -34,18 +34,21 @@ - - + BindingContext as ScanPageViewModel; private readonly Action _callback; - private CancellationTokenSource _autofocusCts; - private Task _continuousAutofocusTask; private readonly Color _greenColor; private readonly SKColor _blueSKColor; private readonly SKColor _greenSKColor; @@ -27,10 +25,8 @@ namespace Bit.App.Pages { InitializeComponent(); _callback = callback; - ViewModel.InitScannerCommand = new Command(() => InitScanner()); - // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes - if (Device.RuntimePlatform == Device.Android) + if (DeviceInfo.Platform == DevicePlatform.Android) { ToolbarItems.RemoveAt(0); } @@ -54,125 +50,47 @@ namespace Bit.App.Pages 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 BarcodeReaderOptions - { - //UseNativeScanning = true, - //PossibleFormats = new List { ZXing.BarcodeFormat.QR_CODE }, - Formats = BarcodeFormat.QrCode, - 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; - } + if (_cameraView == null) { return; } - //_zxing.OnScanResult -= OnScanResult; - //_zxing.OnScanResult += OnScanResult; - // TODO: [MAUI-Migration] [Critical] - //_zxing.IsScanning = true; + ViewModel.StartCameraCommand?.Execute(this); - // 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) + if (_cameraView == null) { return; } - _autofocusCts?.Cancel(); - if (_continuousAutofocusTask != null) - { - await _continuousAutofocusTask; - } - // TODO: [MAUI-Migration] [Critical] - //_zxing.IsScanning = false; - //_zxing.OnScanResult -= OnScanResult; + _cameraView.BarCodeDetectionEnabled = false; + + await _cameraView.StopCameraAsync(); _pageIsActive = false; } - // TODO: [MAUI-Migration] [Critical] - private async void _zxing_BarcodesDetected(System.Object sender, ZXing.Net.Maui.BarcodeDetectionEventArgs e) + private async void CameraViewOnBarcodeDetected(object sender, BarcodeEventArgs e) { try { - if (!e.Results.Any()) + if (!e.Result.Any()) { return; } - var result = e.Results[0]; + var result = e.Result[0]; // Stop analysis until we navigate away so we don't keep reading barcodes - // TODO: [MAUI-Migration] [Critical] - //_zxing.IsAnalyzing = false; - var text = result?.Value; + _cameraView.BarCodeDetectionEnabled = false; + + var text = result?.Text; if (!string.IsNullOrWhiteSpace(text)) { if (text.StartsWith("otpauth://totp")) { + if (_qrcodeFound) { return; } //To avoid duplicate barcode detected events await QrCodeFoundAsync(); _callback(text); return; @@ -185,6 +103,7 @@ namespace Bit.App.Pages { if (part.StartsWith("secret=")) { + if (_qrcodeFound) { return; } //To avoid duplicate barcode detected events await QrCodeFoundAsync(); var subResult = part.Substring(7); if (!string.IsNullOrEmpty(subResult)) @@ -196,7 +115,11 @@ namespace Bit.App.Pages } } } - _callback(null); + + if (!_qrcodeFound) + { + _callback(null); + } } catch (Exception ex) { @@ -209,8 +132,6 @@ namespace Bit.App.Pages _qrcodeFound = true; Vibration.Vibrate(); await Task.Delay(1000); - // TODO: [MAUI-Migration] [Critical] - //_zxing.IsScanning = false; } private async void Close_Clicked(object sender, System.EventArgs e) diff --git a/src/Core/Pages/Vault/ScanPageViewModel.cs b/src/Core/Pages/Vault/ScanPageViewModel.cs index b817b095e..04f761866 100644 --- a/src/Core/Pages/Vault/ScanPageViewModel.cs +++ b/src/Core/Pages/Vault/ScanPageViewModel.cs @@ -1,16 +1,12 @@ -using System; -using System.Threading.Tasks; +using System.Collections.ObjectModel; using System.Windows.Input; using Bit.App.Abstractions; using Bit.Core.Resources.Localization; using Bit.App.Utilities; using Bit.Core.Abstractions; -using Bit.Core.Services; using Bit.Core.Utilities; - -using Microsoft.Maui.ApplicationModel; -using Microsoft.Maui.Controls; -using Microsoft.Maui; +using Camera.MAUI; +using Camera.MAUI.ZXingHelper; namespace Bit.App.Pages { @@ -25,6 +21,7 @@ namespace Bit.App.Pages public ScanPageViewModel() { ToggleScanModeCommand = new AsyncCommand(ToggleScanMode, onException: HandleException); + StartCameraCommand = new Command(StartCamera); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _logger = ServiceContainer.Resolve(); @@ -35,29 +32,80 @@ namespace Bit.App.Pages { try { - await Device.InvokeOnMainThreadAsync(async () => + await MainThread.InvokeOnMainThreadAsync(async () => { var hasCameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera()); HasCameraPermission = hasCameraPermission == PermissionStatus.Granted; ShowScanner = hasCameraPermission == PermissionStatus.Granted; }); - if (!HasCameraPermission) + BarCodeOptions = new BarcodeDecodeOptions { - return; - } - InitScannerCommand.Execute(null); + AutoRotate = false, //shouldn't be needed for QRCodes + PossibleFormats = { ZXing.BarcodeFormat.QR_CODE }, + ReadMultipleCodes = false, //runs slower when true and we only need one + TryHarder = false, //runs slower when true + TryInverted = true + }; + TriggerPropertyChanged(nameof(BarCodeOptions)); } - catch (System.Exception ex) + catch (Exception ex) { HandleException(ex); } } - public ICommand ToggleScanModeCommand { get; set; } - public ICommand InitScannerCommand { get; set; } + private CameraInfo _camera = null; + public CameraInfo Camera + { + get => _camera; + set + { + _camera = value; + TriggerPropertyChanged(nameof(Camera)); + + StartCameraCommand?.Execute(this); + } + } + private ObservableCollection _cameras = new(); + public ObservableCollection Cameras + { + get => _cameras; + set + { + _cameras = value; + TriggerPropertyChanged(nameof(Cameras)); + } + } + public int NumCameras + { + set + { + if (value > 0) + { + Camera = Cameras.First(); + } + } + } + + public BarcodeDecodeOptions BarCodeOptions { get; set; } + public bool AutoStartPreview { get; set; } = false; + + public ICommand StartCameraCommand { get; set; } + public ICommand ToggleScanModeCommand { get; set; } + + private bool _hasCameraPermission = false; + public bool HasCameraPermission + { + get => _hasCameraPermission; + set + { + _hasCameraPermission = value; + + StartCameraCommand?.Execute(this); + } + } - public bool HasCameraPermission { get; set; } public string ScanQrPageTitle => ShowScanner ? AppResources.ScanQrTitle : AppResources.AuthenticatorKeyScanner; public string CameraInstructionTop => ShowScanner ? AppResources.PointYourCameraAtTheQRCode : AppResources.OnceTheKeyIsSuccessfullyEntered; public string TotpAuthenticationKey @@ -81,6 +129,23 @@ namespace Bit.App.Pages }); } + private void StartCamera() + { + if (HasCameraPermission && Camera != null) + { + // Note: If we need to improve performance on Android we can use _cameraView.StartCamera() directly on ScanPage.xaml.cs + // this allows us to set a specific smaller resolution that should help performance and time to scan. + // (The supported resolutions are available in the Camera object) + // This solution would likely replace the "AutoStartPreview" logic in this Command. + + //Setting AutoStartPreview to false and then true should trigger the CameraView to start + AutoStartPreview = false; + TriggerPropertyChanged(nameof(AutoStartPreview)); + AutoStartPreview = true; + TriggerPropertyChanged(nameof(AutoStartPreview)); + } + } + private async Task ToggleScanMode() { var cameraPermission = await PermissionManager.CheckAndRequestPermissionAsync(new Permissions.Camera()); @@ -95,7 +160,6 @@ namespace Bit.App.Pages return; } ShowScanner = !ShowScanner; - InitScannerCommand.Execute(null); } public FormattedString ToggleScanModeLabel @@ -119,7 +183,7 @@ namespace Bit.App.Pages private void HandleException(Exception ex) { - Microsoft.Maui.ApplicationModel.MainThread.InvokeOnMainThreadAsync(async () => + MainThread.InvokeOnMainThreadAsync(async () => { await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);