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);