Skip to content

Commit 779d8d9

Browse files
authored
Merge branch 'main' into FullScreenEvents
2 parents 84774e8 + 6133ad1 commit 779d8d9

File tree

20 files changed

+300
-130
lines changed

20 files changed

+300
-130
lines changed

.github/workflows/dotnet-build.yml

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -287,26 +287,25 @@ jobs:
287287
name: packages
288288
path: ./packages
289289

290-
- name: Remove MediaElement package if that is not being released
291-
if: startsWith(github.ref, 'refs/tags/') && !endsWith(github.ref, '-mediaelement')
290+
- name: Remove packages for MediaElement-only release
291+
if: startsWith(github.ref, 'refs/tags/') && endsWith(github.ref, '-mediaelement')
292292
run: |
293-
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_ -notmatch "CommunityToolkit.Maui.MediaElement" } | Remove-Item -Force
293+
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_.Name -notmatch "CommunityToolkit\.Maui\.MediaElement" } | Remove-Item -Force
294294
295-
- name: Remove Camera package if that is not being released
296-
if: startsWith(github.ref, 'refs/tags/') && !endsWith(github.ref, '-camera')
295+
- name: Remove packages for Camera-only release
296+
if: startsWith(github.ref, 'refs/tags/') && endsWith(github.ref, '-camera')
297297
run: |
298-
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_ -notmatch "CommunityToolkit.Maui.Camera" } | Remove-Item -Force
298+
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_.Name -notmatch "CommunityToolkit\.Maui\.Camera" } | Remove-Item -Force
299299
300-
- name: Remove Maps package if that is not being released
301-
if: startsWith(github.ref, 'refs/tags/') && !endsWith(github.ref, '-maps')
300+
- name: Remove packages for Maps-only release
301+
if: startsWith(github.ref, 'refs/tags/') && endsWith(github.ref, '-maps')
302302
run: |
303-
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_ -notmatch "CommunityToolkit.Maui.Maps" } | Remove-Item -Force
303+
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_.Name -notmatch "CommunityToolkit\.Maui\.Maps" } | Remove-Item -Force
304304
305-
- name: Remove Core & Main package if that is not being released
305+
- name: Remove packages for Core & Main release
306306
if: startsWith(github.ref, 'refs/tags/') && (!endsWith(github.ref, '-mediaelement') && !endsWith(github.ref, '-camera') && !endsWith(github.ref, '-maps'))
307307
run: |
308-
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_ -notmatch "CommunityToolkit.Maui" } | Remove-Item -Force
309-
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_ -notmatch "CommunityToolkit.Maui.Core" } | Remove-Item -Force
308+
Get-ChildItem -Path "${{ github.workspace }}/packages" -Recurse -Filter "*.nupkg" | Where-Object { $_.Name -match "CommunityToolkit\.Maui\.(Camera|MediaElement|Maps)" } | Remove-Item -Force
310309
311310
- name: Install Signing Tool
312311
run: dotnet tool install --tool-path ./tools sign --version 0.9.1-beta.23356.1
@@ -349,7 +348,7 @@ jobs:
349348
dotnet-version: ${{ env.TOOLKIT_NET_VERSION }}
350349
dotnet-quality: 'ga'
351350

352-
- name: Download signed packages for ${{ matrix.platform }}
351+
- name: Download signed packages
353352
uses: actions/download-artifact@v4
354353
with:
355354
name: signed-packages

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<NuGetAuditMode>all</NuGetAuditMode>
1717

1818
<!-- MAUI Specific -->
19-
<MauiPackageVersion>9.0.70</MauiPackageVersion>
19+
<MauiPackageVersion>9.0.80</MauiPackageVersion>
2020
<NextMauiPackageVersion>10.0.0</NextMauiPackageVersion>
2121
<MauiStrictXamlCompilation>true</MauiStrictXamlCompilation>
2222
<SkipValidateMauiImplicitPackageReferences>true</SkipValidateMauiImplicitPackageReferences>

samples/CommunityToolkit.Maui.Sample/Platforms/Windows/Package.appxmanifest

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757

5858
<Capabilities>
5959
<rescap:Capability Name="runFullTrust" />
60+
<DeviceCapability Name="microphone"/>
6061
</Capabilities>
6162

6263
</Package>

samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88

99
namespace CommunityToolkit.Maui.Sample.ViewModels.Essentials;
1010

11-
public partial class SpeechToTextViewModel : BaseViewModel
11+
public partial class SpeechToTextViewModel : BaseViewModel, IAsyncDisposable
1212
{
1313
const string defaultLanguage = "en-US";
14-
const string defaultLanguage_android = "en";
15-
const string defaultLanguage_tizen = "en_US";
1614

1715
readonly ITextToSpeech textToSpeech;
1816
readonly ISpeechToText speechToText;
@@ -55,7 +53,7 @@ async Task SetLocales(CancellationToken token)
5553
Locales.Add(locale);
5654
}
5755

58-
CurrentLocale = Locales.FirstOrDefault(x => x.Language is defaultLanguage or defaultLanguage_android or defaultLanguage_tizen) ?? Locales.FirstOrDefault();
56+
CurrentLocale = Locales.FirstOrDefault();
5957
}
6058

6159
[RelayCommand]
@@ -148,4 +146,9 @@ void HandleLocalesCollectionChanged(object? sender, NotifyCollectionChangedEvent
148146
{
149147
OnPropertyChanged(nameof(CurrentLocale));
150148
}
149+
150+
public async ValueTask DisposeAsync()
151+
{
152+
await speechToText.DisposeAsync();
153+
}
151154
}

src/CommunityToolkit.Maui.Camera/Interfaces/ICameraView.shared.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public interface ICameraView : IView
5555
/// To customize the behavior of the camera when capturing an image, consider overriding the behavior through
5656
/// <c>CameraViewHandler.CommandMapper.ReplaceMapping(nameof(ICameraView.CaptureImage), ADD YOUR METHOD);</c>.
5757
/// </remarks>
58-
ValueTask CaptureImage(CancellationToken token);
58+
Task<Stream> CaptureImage(CancellationToken token);
5959

6060
/// <summary>
6161
/// Starts the camera preview.

src/CommunityToolkit.Maui.Camera/Primitives/CameraViewDefaults.shared.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace CommunityToolkit.Maui.Core;
77

8-
/// <summary>Default Values for <see cref="ICameraView"/>"/></summary>
8+
/// <summary>Default Values for <see cref="ICameraView"/></summary>
99
[SupportedOSPlatform("windows10.0.10240.0")]
1010
[SupportedOSPlatform("android21.0")]
1111
[SupportedOSPlatform("ios")]
@@ -61,4 +61,4 @@ internal static ICommand CreateStopCameraPreviewCommand(BindableObject bindable)
6161
var cameraView = (CameraView)bindable;
6262
return new Command(_ => cameraView.StopCameraPreview());
6363
}
64-
}
64+
}

src/CommunityToolkit.Maui.Camera/Views/CameraView.shared.cs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace CommunityToolkit.Maui.Views;
1313
[SupportedOSPlatform("android21.0")]
1414
[SupportedOSPlatform("ios")]
1515
[SupportedOSPlatform("maccatalyst")]
16-
public partial class CameraView : View, ICameraView
16+
public partial class CameraView : View, ICameraView, IDisposable
1717
{
1818
static readonly BindablePropertyKey isAvailablePropertyKey =
1919
BindableProperty.CreateReadOnly(nameof(IsAvailable), typeof(bool), typeof(CameraView), CameraViewDefaults.IsAvailable);
@@ -65,22 +65,26 @@ public partial class CameraView : View, ICameraView
6565
/// Backing BindableProperty for the <see cref="CaptureImageCommand"/> property.
6666
/// </summary>
6767
public static readonly BindableProperty CaptureImageCommandProperty =
68-
BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command<CancellationToken>), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
68+
BindableProperty.CreateReadOnly(nameof(CaptureImageCommand), typeof(Command<CancellationToken>), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateCaptureImageCommand).BindableProperty;
6969

7070
/// <summary>
7171
/// Backing BindableProperty for the <see cref="StartCameraPreviewCommand"/> property.
7272
/// </summary>
7373
public static readonly BindableProperty StartCameraPreviewCommandProperty =
74-
BindableProperty.CreateReadOnly(nameof(StartCameraPreviewCommand), typeof(Command<CancellationToken>), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStartCameraPreviewCommand).BindableProperty;
74+
BindableProperty.CreateReadOnly(nameof(StartCameraPreviewCommand), typeof(Command<CancellationToken>), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStartCameraPreviewCommand).BindableProperty;
7575

7676
/// <summary>
7777
/// Backing BindableProperty for the <see cref="StopCameraPreviewCommand"/> property.
7878
/// </summary>
7979
public static readonly BindableProperty StopCameraPreviewCommandProperty =
80-
BindableProperty.CreateReadOnly(nameof(StopCameraPreviewCommand), typeof(ICommand), typeof(CameraView), default, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStopCameraPreviewCommand).BindableProperty;
80+
BindableProperty.CreateReadOnly(nameof(StopCameraPreviewCommand), typeof(ICommand), typeof(CameraView), null, BindingMode.OneWayToSource, defaultValueCreator: CameraViewDefaults.CreateStopCameraPreviewCommand).BindableProperty;
8181

82+
83+
readonly SemaphoreSlim captureImageSemaphoreSlim = new(1, 1);
8284
readonly WeakEventManager weakEventManager = new();
8385

86+
bool isDisposed;
87+
8488
/// <summary>
8589
/// Event that is raised when the camera capture fails.
8690
/// </summary>
@@ -188,7 +192,14 @@ bool ICameraView.IsBusy
188192
set => SetValue(isCameraBusyPropertyKey, value);
189193
}
190194

191-
private protected new CameraViewHandler Handler => (CameraViewHandler)(base.Handler ?? throw new InvalidOperationException("Unable to retrieve Handler"));
195+
new CameraViewHandler Handler => (CameraViewHandler)(base.Handler ?? throw new InvalidOperationException("Unable to retrieve Handler"));
196+
197+
/// <inheritdoc/>
198+
public void Dispose()
199+
{
200+
Dispose(disposing: true);
201+
GC.SuppressFinalize(this);
202+
}
192203

193204
/// <inheritdoc cref="ICameraView.GetAvailableCameras"/>
194205
public async ValueTask<IReadOnlyList<CameraInfo>> GetAvailableCameras(CancellationToken token)
@@ -207,8 +218,37 @@ public async ValueTask<IReadOnlyList<CameraInfo>> GetAvailableCameras(Cancellati
207218
}
208219

209220
/// <inheritdoc cref="ICameraView.CaptureImage"/>
210-
public ValueTask CaptureImage(CancellationToken token) =>
211-
Handler.CameraManager.TakePicture(token);
221+
public async Task<Stream> CaptureImage(CancellationToken token)
222+
{
223+
// Use SemaphoreSlim to ensure `MediaCaptured` and `MediaCaptureFailed` events are unsubscribed before calling `TakePicture` again
224+
// Without this SemaphoreSlim, previous calls to this method will fire `MediaCaptured` and/or `MediaCaptureFailed` events causing this method to return the wrong Stream or throw the wrong Exception
225+
await captureImageSemaphoreSlim.WaitAsync(token);
226+
227+
var mediaStreamTCS = new TaskCompletionSource<Stream>(TaskCreationOptions.RunContinuationsAsynchronously);
228+
229+
MediaCaptured += HandleMediaCaptured;
230+
MediaCaptureFailed += HandleMediaCapturedFailed;
231+
232+
try
233+
{
234+
await Handler.CameraManager.TakePicture(token);
235+
236+
var stream = await mediaStreamTCS.Task.WaitAsync(token);
237+
return stream;
238+
}
239+
finally
240+
{
241+
MediaCaptured -= HandleMediaCaptured;
242+
MediaCaptureFailed -= HandleMediaCapturedFailed;
243+
244+
// Release SemaphoreSlim after `MediaCaptured` and `MediaCaptureFailed` events are unsubscribed
245+
captureImageSemaphoreSlim.Release();
246+
}
247+
248+
void HandleMediaCaptured(object? sender, MediaCapturedEventArgs e) => mediaStreamTCS.SetResult(e.Media);
249+
250+
void HandleMediaCapturedFailed(object? sender, MediaCaptureFailedEventArgs e) => mediaStreamTCS.SetException(new CameraException(e.FailureReason));
251+
}
212252

213253
/// <inheritdoc cref="ICameraView.StartCameraPreview"/>
214254
public Task StartCameraPreview(CancellationToken token) =>
@@ -218,6 +258,20 @@ public Task StartCameraPreview(CancellationToken token) =>
218258
public void StopCameraPreview() =>
219259
Handler.CameraManager.StopCameraPreview();
220260

261+
/// <inheritdoc/>
262+
protected virtual void Dispose(bool disposing)
263+
{
264+
if (!isDisposed)
265+
{
266+
if (disposing)
267+
{
268+
captureImageSemaphoreSlim.Dispose();
269+
}
270+
271+
isDisposed = true;
272+
}
273+
}
274+
221275
void ICameraView.OnMediaCaptured(Stream imageData)
222276
{
223277
weakEventManager.HandleEvent(this, new MediaCapturedEventArgs(imageData), nameof(MediaCaptured));

src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.windows.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ public SpeechToTextState CurrentState
2828
public ValueTask DisposeAsync()
2929
{
3030
InternalStopListening();
31-
32-
offlineSpeechRecognizer?.Dispose();
33-
offlineSpeechRecognizer = null;
3431
return ValueTask.CompletedTask;
3532
}
3633

@@ -39,7 +36,6 @@ Task InternalStartListening(SpeechToTextOptions options, CancellationToken token
3936
Initialize(options);
4037

4138
offlineSpeechRecognizer.AudioStateChanged += OfflineSpeechRecognizer_StateChanged;
42-
offlineSpeechRecognizer.LoadGrammar(new DictationGrammar());
4339

4440
offlineSpeechRecognizer.InitialSilenceTimeout = TimeSpan.MaxValue;
4541
offlineSpeechRecognizer.BabbleTimeout = TimeSpan.MaxValue;
@@ -48,7 +44,12 @@ Task InternalStartListening(SpeechToTextOptions options, CancellationToken token
4844

4945
offlineSpeechRecognizer.RecognizeCompleted += OnRecognizeCompleted;
5046
offlineSpeechRecognizer.SpeechRecognized += OnSpeechRecognized;
51-
offlineSpeechRecognizer.RecognizeAsync(RecognizeMode.Multiple);
47+
48+
if (offlineSpeechRecognizer.AudioState == AudioState.Stopped)
49+
{
50+
offlineSpeechRecognizer.RecognizeAsync(RecognizeMode.Multiple);
51+
}
52+
5253
return Task.CompletedTask;
5354
}
5455

@@ -81,19 +82,26 @@ void InternalStopListening()
8182
{
8283
try
8384
{
84-
if (offlineSpeechRecognizer is not null)
85+
if (offlineSpeechRecognizer is not null && offlineSpeechRecognizer.AudioState != AudioState.Stopped)
8586
{
8687
offlineSpeechRecognizer.RecognizeAsyncStop();
87-
88-
offlineSpeechRecognizer.AudioStateChanged -= OfflineSpeechRecognizer_StateChanged;
89-
offlineSpeechRecognizer.RecognizeCompleted -= OnRecognizeCompleted;
90-
offlineSpeechRecognizer.SpeechRecognized -= OnSpeechRecognized;
9188
}
9289
}
9390
catch
9491
{
9592
// ignored. Recording may be already stopped
9693
}
94+
finally
95+
{
96+
if (offlineSpeechRecognizer is not null)
97+
{
98+
offlineSpeechRecognizer.AudioStateChanged -= OfflineSpeechRecognizer_StateChanged;
99+
offlineSpeechRecognizer.RecognizeCompleted -= OnRecognizeCompleted;
100+
offlineSpeechRecognizer.SpeechRecognized -= OnSpeechRecognized;
101+
offlineSpeechRecognizer?.Dispose();
102+
offlineSpeechRecognizer = null;
103+
}
104+
}
97105
}
98106

99107
[MemberNotNull(nameof(recognitionText), nameof(offlineSpeechRecognizer), nameof(speechToTextOptions))]
@@ -102,6 +110,7 @@ void Initialize(SpeechToTextOptions options)
102110
speechToTextOptions = options;
103111
recognitionText = string.Empty;
104112
offlineSpeechRecognizer = new SpeechRecognitionEngine(options.Culture);
113+
offlineSpeechRecognizer.LoadGrammarAsync(new DictationGrammar());
105114
}
106115

107116
void OfflineSpeechRecognizer_StateChanged(object? sender, AudioStateChangedEventArgs e)

src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.windows.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
using System.Diagnostics;
12
using System.Diagnostics.CodeAnalysis;
2-
using System.Globalization;
3-
using System.Speech.Recognition;
43
using Microsoft.Maui.ApplicationModel;
54
using Windows.Globalization;
65
using Windows.Media.SpeechRecognition;
@@ -35,8 +34,6 @@ public SpeechToTextState CurrentState
3534
public async ValueTask DisposeAsync()
3635
{
3736
await StopRecording(CancellationToken.None);
38-
speechRecognizer?.Dispose();
39-
speechRecognizer = null;
4037
}
4138

4239
async Task InternalStartListeningAsync(SpeechToTextOptions options, CancellationToken cancellationToken)
@@ -48,7 +45,10 @@ async Task InternalStartListeningAsync(SpeechToTextOptions options, Cancellation
4845
speechRecognizer.ContinuousRecognitionSession.Completed += OnCompleted;
4946
try
5047
{
51-
await speechRecognizer.ContinuousRecognitionSession.StartAsync().AsTask(cancellationToken);
48+
if (speechRecognizer.State == SpeechRecognizerState.Idle)
49+
{
50+
await speechRecognizer.ContinuousRecognitionSession.StartAsync().AsTask(cancellationToken);
51+
}
5252
}
5353
catch (Exception ex) when ((uint)ex.HResult is privacyStatementDeclinedCode)
5454
{
@@ -88,12 +88,8 @@ async Task StopRecording(CancellationToken cancellationToken)
8888
{
8989
try
9090
{
91-
if (speechRecognizer is not null)
91+
if (speechRecognizer is not null && speechRecognizer.State != SpeechRecognizerState.Idle)
9292
{
93-
speechRecognizer.StateChanged -= SpeechRecognizer_StateChanged;
94-
speechRecognizer.ContinuousRecognitionSession.ResultGenerated -= ResultGenerated;
95-
speechRecognizer.ContinuousRecognitionSession.Completed -= OnCompleted;
96-
9793
cancellationToken.ThrowIfCancellationRequested();
9894
await speechRecognizer.ContinuousRecognitionSession.StopAsync().AsTask(cancellationToken);
9995
}
@@ -102,6 +98,17 @@ async Task StopRecording(CancellationToken cancellationToken)
10298
{
10399
// ignored. Recording may be already stopped
104100
}
101+
finally
102+
{
103+
if (speechRecognizer is not null)
104+
{
105+
speechRecognizer.StateChanged -= SpeechRecognizer_StateChanged;
106+
speechRecognizer.ContinuousRecognitionSession.ResultGenerated -= ResultGenerated;
107+
speechRecognizer.ContinuousRecognitionSession.Completed -= OnCompleted;
108+
speechRecognizer?.Dispose();
109+
speechRecognizer = null;
110+
}
111+
}
105112
}
106113

107114
[MemberNotNull(nameof(recognitionText), nameof(speechRecognizer), nameof(speechToTextOptions))]

src/CommunityToolkit.Maui.Core/Platform/StatusBar/StatusBar.android.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ static void PlatformSetStyle(StatusBarStyle style)
8383
static void SetStatusBarAppearance(Activity activity, bool isLightStatusBars)
8484
{
8585
var window = activity.GetCurrentWindow();
86-
var windowController = WindowCompat.GetInsetsController(window, window.DecorView);
87-
windowController.AppearanceLightStatusBars = isLightStatusBars;
86+
if (WindowCompat.GetInsetsController(window, window.DecorView) is WindowInsetsControllerCompat windowController)
87+
{
88+
windowController.AppearanceLightStatusBars = isLightStatusBars;
89+
}
8890
}
8991
}

0 commit comments

Comments
 (0)