From e5bef74151850815ff1ae5b1b29696c5837068af Mon Sep 17 00:00:00 2001 From: James Crutchley Date: Sun, 26 Oct 2025 21:57:59 -0700 Subject: [PATCH 1/6] Improve null safety and update dependencies Enhanced null safety across CameraManager.android.cs by adding null-conditional operators (`?`) to prevent potential null reference exceptions. Updated `CameraViewPage.xaml` to bind `Picker` to `SelectedCamera.Name` for accurate UI display. Upgraded `Xamarin.AndroidX.Camera` dependencies to version 1.5.0 and added `Xamarin.AndroidX.Activity.Ktx` for improved functionality and compatibility. Adjusted `CameraProvider.android.cs` to handle null values and ensure robust camera initialization. --- .../Views/CameraView/CameraViewPage.xaml | 2 +- .../CameraManager.android.cs | 48 +++++++++---------- .../CommunityToolkit.Maui.Camera.csproj | 8 ++-- .../Providers/CameraProvider.android.cs | 10 ++-- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index d90bcd224f..238a56aead 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml @@ -64,7 +64,7 @@ EnableModes(CameraInfo selectedCamera, CancellationTo } var extensionsManagerFuture = ExtensionsManager.GetInstanceAsync(context, cameraProviderInstance); - extensionsManagerFuture.AddListener(new Runnable(() => + extensionsManagerFuture?.AddListener(new Runnable(() => { var extensionsManager = (ExtensionsManager?)extensionsManagerFuture.Get(); if (extensionsManager is not null && extensionsManager.IsExtensionAvailable(cameraSelector, extensionMode)) @@ -455,10 +455,10 @@ void SetImageCaptureTargetRotation(int rotation) sealed class ImageCallBack(ICameraView cameraView) : ImageCapture.OnImageCapturedCallback { - public override void OnCaptureSuccess(IImageProxy image) + public override void OnCaptureSuccess(IImageProxy? image) { base.OnCaptureSuccess(image); - var img = image.Image; + var img = image?.Image; if (img is null) { @@ -471,7 +471,7 @@ public override void OnCaptureSuccess(IImageProxy image) if (buffer is null) { cameraView.OnMediaCapturedFailed("Unable to obtain a buffer for the image plane."); - image.Close(); + image?.Close(); return; } @@ -489,7 +489,7 @@ public override void OnCaptureSuccess(IImageProxy image) } finally { - image.Close(); + image?.Close(); } static Image.Plane? GetFirstPlane(Image.Plane[]? planes) @@ -503,10 +503,10 @@ public override void OnCaptureSuccess(IImageProxy image) } } - public override void OnError(ImageCaptureException exception) + public override void OnError(ImageCaptureException? exception) { base.OnError(exception); - cameraView.OnMediaCapturedFailed(exception.Message ?? "An unknown error occurred."); + cameraView.OnMediaCapturedFailed(exception?.Message ?? "An unknown error occurred."); } } @@ -514,13 +514,13 @@ sealed class ResolutionFilter(Android.Util.Size size) : Object, IResolutionFilte { public Android.Util.Size TargetSize { get; set; } = size; - public IList Filter(IList supportedSizes, int rotationDegrees) + public IList Filter(IList? supportedSizes, int rotationDegrees) { - var filteredList = supportedSizes + var filteredList = supportedSizes? .Where(size => size.Width <= TargetSize.Width && size.Height <= TargetSize.Height) .OrderByDescending(size => size.Width * size.Height).ToList(); - return filteredList.Count is 0 ? supportedSizes : filteredList; + return filteredList?.Count is 0 ? supportedSizes ?? [] : filteredList ?? []; } } diff --git a/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj b/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj index e318badb0f..6b498c4258 100644 --- a/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj +++ b/src/CommunityToolkit.Maui.Camera/CommunityToolkit.Maui.Camera.csproj @@ -52,10 +52,12 @@ - - - + + + + + diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index f908040cd0..f0cc100e91 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs @@ -43,7 +43,7 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) if (CameraCharacteristics.ScalerStreamConfigurationMap is not null) { - var streamConfigMap = camera2Info.GetCameraCharacteristic(CameraCharacteristics.ScalerStreamConfigurationMap) as StreamConfigurationMap; + var streamConfigMap = camera2Info?.GetCameraCharacteristic(CameraCharacteristics.ScalerStreamConfigurationMap) as StreamConfigurationMap; if (OperatingSystem.IsAndroidVersionAtLeast(23)) { @@ -68,13 +68,13 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) } var cameraInfo = new CameraInfo(name, - camera2Info.CameraId, + camera2Info?.CameraId ?? string.Empty, position, cameraXInfo.HasFlashUnit, - (cameraXInfo.ZoomState.Value as IZoomState)?.MinZoomRatio ?? 1.0f, - (cameraXInfo.ZoomState.Value as IZoomState)?.MaxZoomRatio ?? 1.0f, + (cameraXInfo.ZoomState?.Value as IZoomState)?.MinZoomRatio ?? 1.0f, + (cameraXInfo.ZoomState?.Value as IZoomState)?.MaxZoomRatio ?? 1.0f, supportedResolutions, - cameraXInfo.CameraSelector); + cameraXInfo?.CameraSelector ?? throw new InvalidOperationException("CameraSelector cannot be null")); availableCameras.Add(cameraInfo); } From 2377bcfabd9195894b0484d7b874828de9507cfc Mon Sep 17 00:00:00 2001 From: James Crutchley Date: Sun, 26 Oct 2025 23:54:58 -0700 Subject: [PATCH 2/6] Improve camera binding, resolution, and recording quality Updated CameraViewPage.xaml to fix data binding issues by modifying the `ItemDisplayBinding` property of the `Picker`. Enhanced CameraManager.android.cs with the following changes: - Improved resolution selection logic in `UpdateCaptureResolution` by prioritizing higher resolution over capture rate. - Enhanced image capture quality in `StartUseCase` by setting capture mode to maximize quality and applying the resolution selector. - Added audio support and proper initialization for video recording in `PlatformStartVideoRecording`. These changes improve the overall functionality and quality of the camera features. --- .../Pages/Views/CameraView/CameraViewPage.xaml | 2 +- src/CommunityToolkit.Maui.Camera/CameraManager.android.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml index 238a56aead..ed5edd3a93 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml @@ -64,7 +64,7 @@ Date: Mon, 27 Oct 2025 00:03:11 -0700 Subject: [PATCH 3/6] Update src/CommunityToolkit.Maui.Camera/CameraManager.android.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/CommunityToolkit.Maui.Camera/CameraManager.android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index f1fcab7259..5b0c0c2abb 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -520,7 +520,7 @@ sealed class ResolutionFilter(Android.Util.Size size) : Object, IResolutionFilte .Where(size => size.Width <= TargetSize.Width && size.Height <= TargetSize.Height) .OrderByDescending(size => size.Width * size.Height).ToList(); - return filteredList?.Count is 0 ? supportedSizes ?? [] : filteredList ?? []; + return filteredList is null || filteredList.Count is 0 ? supportedSizes ?? [] : filteredList; } } From c11f32b51b8975281115723d16a1779d18f6b4ec Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:22:19 -0700 Subject: [PATCH 4/6] Ignore the camera if `Camera2CameraInfo.From` returns `null` --- .../Providers/CameraProvider.android.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index f0cc100e91..1260b1654f 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs @@ -30,6 +30,14 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) foreach (var cameraXInfo in processCameraProvider.AvailableCameraInfos) { var camera2Info = Camera2CameraInfo.From(cameraXInfo); + if (camera2Info is null) + { + // `Camera2CameraInfo.From` should never return `null` + // According to the Android Docs, `Camera2CameraInfo.From` returns a `NonNull` + // `Camera2CameraInfo.From` returning a nullable `Camera2CameraInfo` object is likely just a C# binding mistake + // https://developer.android.com/reference/androidx/camera/camera2/interop/Camera2CameraInfo + continue; + } var (name, position) = cameraXInfo.LensFacing switch { @@ -43,7 +51,7 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) if (CameraCharacteristics.ScalerStreamConfigurationMap is not null) { - var streamConfigMap = camera2Info?.GetCameraCharacteristic(CameraCharacteristics.ScalerStreamConfigurationMap) as StreamConfigurationMap; + var streamConfigMap = camera2Info.GetCameraCharacteristic(CameraCharacteristics.ScalerStreamConfigurationMap) as StreamConfigurationMap; if (OperatingSystem.IsAndroidVersionAtLeast(23)) { @@ -68,13 +76,13 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) } var cameraInfo = new CameraInfo(name, - camera2Info?.CameraId ?? string.Empty, + camera2Info.CameraId, position, cameraXInfo.HasFlashUnit, (cameraXInfo.ZoomState?.Value as IZoomState)?.MinZoomRatio ?? 1.0f, (cameraXInfo.ZoomState?.Value as IZoomState)?.MaxZoomRatio ?? 1.0f, supportedResolutions, - cameraXInfo?.CameraSelector ?? throw new InvalidOperationException("CameraSelector cannot be null")); + cameraXInfo.CameraSelector ?? throw new InvalidOperationException($"Unable to retrieve {nameof(ICameraInfo.CameraSelector)}")); availableCameras.Add(cameraInfo); } From 098d9ac1bf7407dd6dfee707f90984819c75d296 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:38:58 -0700 Subject: [PATCH 5/6] Update Builder Patterns --- .../CameraManager.android.cs | 45 ++++++++++++++----- .../Providers/CameraProvider.android.cs | 2 +- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs index 5b0c0c2abb..81213d08f2 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.android.cs @@ -125,8 +125,12 @@ public async partial ValueTask UpdateCaptureResolution(Size resolution, Cancella resolutionSelector = new ResolutionSelector.Builder() .SetAllowedResolutionMode(ResolutionSelector.PreferHigherResolutionOverCaptureRate)? - .SetResolutionFilter(resolutionFilter)? - .Build(); + .SetResolutionFilter(resolutionFilter) + ?.Build() ?? throw new InvalidOperationException("Unable to Set Resolution Filter"); + + // `.SetResolutionFilter()` should never return null + // According to the Android docs, `ResolutionSelector.Builder.setResolutionFilter(ResolutionFilter)` returns a `NonNull` object + // `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake if (IsInitialized) { @@ -234,11 +238,21 @@ protected async Task StartUseCase(CancellationToken token) imageCapture = new ImageCapture.Builder() .SetCaptureMode(ImageCapture.CaptureModeMaximizeQuality)? - .SetResolutionSelector(resolutionSelector)? - .Build(); + .SetResolutionSelector(resolutionSelector) + ?.Build() ?? throw new InvalidOperationException("Unable to set resolution selector"); + + // `.SetResolutionFilter()` should never return null + // According to the Android docs, `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returns a `NonNull` object + // `ResolutionSelector.Builder.SetResolutionFilter(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake + // https://developer.android.com/reference/androidx/camera/core/resolutionselector/ResolutionSelector.Builder#setResolutionFilter(androidx.camera.core.resolutionselector.ResolutionFilter) var videoRecorderBuilder = new Recorder.Builder() - .SetExecutor(cameraExecutor); + .SetExecutor(cameraExecutor) ?? throw new InvalidOperationException("Unable to set video recorder executor"); + + // `.SetExecutor()` should never return null + // According to the Android docs, `ResolutionSelector.Builder.setExecutor(ResolutionFilter)` returns a `NonNull` object + // `ResolutionSelector.Builder.SetExecutor(ResolutionFilter)` returning a nullable object in .NET for Android is likely a C# Binding mistake + // https://developer.android.com/reference/androidx/camera/video/Recorder.Builder#setExecutor(java.util.concurrent.Executor) if (Quality.Highest is not null) { @@ -343,9 +357,14 @@ protected virtual async partial Task PlatformStartVideoRecording(Stream stream, var captureListener = new CameraConsumer(videoRecordingFinalizeTcs); var executor = ContextCompat.GetMainExecutor(context) ?? throw new CameraException($"Unable to retrieve {nameof(IExecutorService)}"); videoRecording = videoRecorder - .PrepareRecording(context, outputOptions)? - .WithAudioEnabled() - .Start(executor, captureListener); + .PrepareRecording(context, outputOptions) + ?.WithAudioEnabled() + .Start(executor, captureListener) ?? throw new InvalidOperationException("Unable to prepare recording"); + + // `.PrepareRecording()` should never return null + // According to the Android docs, `Recorder.prepareRecording(Context, eMediaSoreOutputOptions)` returns a `NonNull` object + // `Recorder.PrepareRecording(Context, eMediaSoreOutputOptions)` returning a nullable object in .NET for Android is likely a C# Binding mistake + // https://developer.android.com/reference/androidx/camera/video/Recorder#prepareRecording(android.content.Context,androidx.camera.video.MediaStoreOutputOptions) } protected virtual async partial Task PlatformStopVideoRecording(CancellationToken token) @@ -415,8 +434,10 @@ async Task EnableModes(CameraInfo selectedCamera, CancellationTo return; } - var extensionsManagerFuture = ExtensionsManager.GetInstanceAsync(context, cameraProviderInstance); - extensionsManagerFuture?.AddListener(new Runnable(() => + var extensionsManagerFuture = ExtensionsManager.GetInstanceAsync(context, cameraProviderInstance) + ?? throw new InvalidOperationException("Unable to get listenable future for camera provider");; + + extensionsManagerFuture.AddListener(new Runnable(() => { var extensionsManager = (ExtensionsManager?)extensionsManagerFuture.Get(); if (extensionsManager is not null && extensionsManager.IsExtensionAvailable(cameraSelector, extensionMode)) @@ -520,7 +541,9 @@ sealed class ResolutionFilter(Android.Util.Size size) : Object, IResolutionFilte .Where(size => size.Width <= TargetSize.Width && size.Height <= TargetSize.Height) .OrderByDescending(size => size.Width * size.Height).ToList(); - return filteredList is null || filteredList.Count is 0 ? supportedSizes ?? [] : filteredList; + return filteredList is null || filteredList.Count is 0 + ? supportedSizes ?? [] + : filteredList; } } diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index 1260b1654f..be5a81f994 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs @@ -34,7 +34,7 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) { // `Camera2CameraInfo.From` should never return `null` // According to the Android Docs, `Camera2CameraInfo.From` returns a `NonNull` - // `Camera2CameraInfo.From` returning a nullable `Camera2CameraInfo` object is likely just a C# binding mistake + // `Camera2CameraInfo.From` returning a nullable `Camera2CameraInfo` object is likely a C# binding mistake // https://developer.android.com/reference/androidx/camera/camera2/interop/Camera2CameraInfo continue; } From a8fdbd4f5486ff128bccc46628f874f1d1a3506a Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:41:36 -0700 Subject: [PATCH 6/6] Update CameraProvider.android.cs --- .../Providers/CameraProvider.android.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs index be5a81f994..c5a45ca1f2 100644 --- a/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs +++ b/src/CommunityToolkit.Maui.Camera/Providers/CameraProvider.android.cs @@ -76,7 +76,7 @@ public async partial ValueTask RefreshAvailableCameras(CancellationToken token) } var cameraInfo = new CameraInfo(name, - camera2Info.CameraId, + camera2Info.CameraId ?? throw new InvalidOperationException("Unable to retrieve Camera ID"), position, cameraXInfo.HasFlashUnit, (cameraXInfo.ZoomState?.Value as IZoomState)?.MinZoomRatio ?? 1.0f,