diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs index df3fe14ea3..7dee92e068 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/CameraView/CameraViewPage.xaml.cs @@ -28,6 +28,21 @@ public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFi protected override async void OnAppearing() { base.OnAppearing(); + + var cameraPermissionsRequest = await Permissions.RequestAsync(); + var microphonePermissionsRequest = await Permissions.RequestAsync(); + + if (cameraPermissionsRequest is not PermissionStatus.Granted) + { + await Shell.Current.CurrentPage.DisplayAlertAsync("Camera permission is not granted.", "Please grant the permission to use this feature.", "OK"); + return; + } + + if (microphonePermissionsRequest is not PermissionStatus.Granted) + { + await Shell.Current.CurrentPage.DisplayAlertAsync("Microphone permission is not granted.", "Please grant the permission to use this feature.", "OK"); + return; + } var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3)); await BindingContext.RefreshCamerasCommand.ExecuteAsync(cancellationTokenSource.Token); @@ -124,6 +139,13 @@ async void SaveVideo(object? sender, EventArgs? e) } else { + var status = await Permissions.RequestAsync(); + if (status is not PermissionStatus.Granted) + { + await Shell.Current.CurrentPage.DisplayAlert("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK"); + return; + } + await fileSaver.SaveAsync("recording.mp4", videoRecordingStream); await videoRecordingStream.DisposeAsync(); videoRecordingStream = Stream.Null; diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs index 9f46379171..a3c25c7f37 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs @@ -288,4 +288,4 @@ async void DisplayPopup(object? sender, EventArgs? e) popupMediaElement.Stop(); popupMediaElement.Source = null; } -} +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs index aaf8e43e2e..febf791102 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs @@ -38,4 +38,4 @@ public partial class ConvertersGalleryViewModel() : BaseGalleryViewModel( SectionModel.Create(nameof(MultiMathExpressionConverter), "A converter that allows users to calculate multiple math expressions at runtime."), SectionModel.Create(nameof(TextCaseConverter), "A converter that allows users to convert the casing of an incoming string type binding. The Type property is used to define what kind of casing will be applied to the string."), SectionModel.Create(nameof(VariableMultiValueConverter), "A converter that allows you to combine multiple boolean bindings into a single binding."), -]); +]); \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs index ec47a3c448..df4e8c2754 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FileSaverViewModel.cs @@ -11,9 +11,29 @@ public partial class FileSaverViewModel(IFileSaver fileSaver) : BaseViewModel [ObservableProperty] public partial double Progress { get; set; } + static async Task ArePermissionsGranted() + { + var readPermissionStatus = await Permissions.RequestAsync(); + var writePermissionStatus = await Permissions.RequestAsync(); + + if (readPermissionStatus is PermissionStatus.Granted + && writePermissionStatus is PermissionStatus.Granted) + { + return true; + } + + await Shell.Current.CurrentPage.DisplayAlertAsync("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK"); + return false; + } + [RelayCommand] async Task SaveFile(CancellationToken cancellationToken) { + if (!await ArePermissionsGranted()) + { + return; + } + using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); try { @@ -32,6 +52,11 @@ async Task SaveFile(CancellationToken cancellationToken) [RelayCommand] async Task SaveFileStatic(CancellationToken cancellationToken) { + if (!await ArePermissionsGranted()) + { + return; + } + using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!")); var fileSaveResult = await FileSaver.SaveAsync("DCIM", "test.txt", stream, cancellationToken); if (fileSaveResult.IsSuccessful) @@ -47,6 +72,11 @@ async Task SaveFileStatic(CancellationToken cancellationToken) [RelayCommand] async Task SaveFileInstance(CancellationToken cancellationToken) { + if (!await ArePermissionsGranted()) + { + return; + } + using var client = new HttpClient(); const string communityToolkitNuGetUrl = "https://www.nuget.org/api/v2/package/CommunityToolkit.Maui/5.0.0"; diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs index fe6ff69a73..bbf4be84cd 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/FolderPickerViewModel.cs @@ -5,18 +5,34 @@ namespace CommunityToolkit.Maui.Sample.ViewModels.Essentials; -public partial class FolderPickerViewModel : BaseViewModel +public partial class FolderPickerViewModel(IFolderPicker folderPicker) : BaseViewModel { - readonly IFolderPicker folderPicker; + readonly IFolderPicker folderPicker = folderPicker; - public FolderPickerViewModel(IFolderPicker folderPicker) + static async Task ArePermissionsGranted() { - this.folderPicker = folderPicker; + var readPermissionStatus = await Permissions.RequestAsync(); + var writePermissionStatus = await Permissions.RequestAsync(); + + if (readPermissionStatus is PermissionStatus.Granted + && writePermissionStatus is PermissionStatus.Granted) + { + return true; + } + + await Shell.Current.CurrentPage.DisplayAlertAsync("Storage permission is not granted.", "Please grant the permission to use this feature.", "OK"); + + return false; } [RelayCommand] async Task PickFolder(CancellationToken cancellationToken) { + if (!await ArePermissionsGranted()) + { + return; + } + var folderPickerResult = await folderPicker.PickAsync(cancellationToken); if (folderPickerResult.IsSuccessful) { @@ -31,6 +47,11 @@ async Task PickFolder(CancellationToken cancellationToken) [RelayCommand] async Task PickFolderStatic(CancellationToken cancellationToken) { + if (!await ArePermissionsGranted()) + { + return; + } + var folderResult = await FolderPicker.PickAsync("DCIM", cancellationToken); if (folderResult.IsSuccessful) { @@ -46,6 +67,11 @@ async Task PickFolderStatic(CancellationToken cancellationToken) [RelayCommand] async Task PickFolderInstance(CancellationToken cancellationToken) { + if (!await ArePermissionsGranted()) + { + return; + } + var folderPickerInstance = new FolderPickerImplementation(); try { diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/OfflineSpeechToTextViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/OfflineSpeechToTextViewModel.cs index e02718a8cc..3af767e40a 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/OfflineSpeechToTextViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/OfflineSpeechToTextViewModel.cs @@ -24,10 +24,19 @@ public OfflineSpeechToTextViewModel() [ObservableProperty] public partial string? RecognitionText { get; set; } = "Welcome to .NET MAUI Community Toolkit!"; + static async Task ArePermissionsGranted(ISpeechToText speechToText) + { + var microphonePermissionStatus = await Permissions.RequestAsync(); + var isSpeechToTextRequestPermissionsGranted = await speechToText.RequestPermissions(CancellationToken.None); + + return microphonePermissionStatus is PermissionStatus.Granted + && isSpeechToTextRequestPermissionsGranted; + } + [RelayCommand] async Task StartListen() { - var isGranted = await speechToText.RequestPermissions(CancellationToken.None); + var isGranted = await ArePermissionsGranted(speechToText); if (!isGranted) { await Toast.Make("Permission not granted").Show(CancellationToken.None); diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs index 7b40e09e5b..c37405784f 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs @@ -41,6 +41,20 @@ public SpeechToTextViewModel(ITextToSpeech textToSpeech, [FromKeyedServices("Onl [ObservableProperty, NotifyCanExecuteChangedFor(nameof(StopListenCommand))] public partial bool CanStopListenExecute { get; set; } = false; + public async ValueTask DisposeAsync() + { + await speechToText.DisposeAsync(); + } + + static async Task ArePermissionsGranted(ISpeechToText speechToText) + { + var microphonePermissionStatus = await Permissions.RequestAsync(); + var isSpeechToTextPermissionsGranted = await speechToText.RequestPermissions(CancellationToken.None); + + return microphonePermissionStatus is PermissionStatus.Granted + && isSpeechToTextPermissionsGranted; + } + [RelayCommand] async Task SetLocales(CancellationToken token) { @@ -85,7 +99,7 @@ async Task StartListen() CanStartListenExecute = false; CanStopListenExecute = true; - var isGranted = await speechToText.RequestPermissions(CancellationToken.None); + var isGranted = await ArePermissionsGranted(speechToText); if (!isGranted) { await Toast.Make("Permission not granted").Show(CancellationToken.None); @@ -146,9 +160,4 @@ void HandleLocalesCollectionChanged(object? sender, NotifyCollectionChangedEvent { OnPropertyChanged(nameof(CurrentLocale)); } - - public async ValueTask DisposeAsync() - { - await speechToText.DisposeAsync(); - } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs index fd16ef81a8..4542a7fa8c 100644 --- a/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs @@ -24,29 +24,6 @@ partial class CameraManager( internal bool IsInitialized { get; private set; } - /// - /// Whether the user has granted the required permissions through the use of the API. - /// - /// Returns true if permission has been granted, false otherwise. - public async Task ArePermissionsGranted() - { - var cameraRequest = await Permissions.RequestAsync(); - var microphoneRequest = await Permissions.RequestAsync(); - if (cameraRequest is not PermissionStatus.Granted) - { - Trace.TraceInformation("Camera permission is not granted."); - return false; - } - - if (microphoneRequest is not PermissionStatus.Granted) - { - Trace.TraceInformation("Microphone permission is not granted."); - return false; - } - - return true; - } - /// /// Connects to the camera. /// diff --git a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs index f15859fc04..22d64c9bbd 100644 --- a/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs +++ b/src/CommunityToolkit.Maui.Camera/Handlers/CameraViewHandler.shared.cs @@ -84,7 +84,6 @@ protected override async void ConnectHandler(NativePlatformCameraPreviewView pla { base.ConnectHandler(platformView); - await CameraManager.ArePermissionsGranted(); await CameraManager.ConnectCamera(CancellationToken.None); await cameraProvider.RefreshAvailableCameras(CancellationToken.None); } diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs index 80de8e2073..fb5b3658d6 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.android.cs @@ -24,15 +24,6 @@ static async Task InternalSaveAsync(string initialPath, string fileName, AndroidUri? filePath = null; - if (!OperatingSystem.IsAndroidVersionAtLeast(33)) - { - var status = await Permissions.RequestAsync().WaitAsync(cancellationToken).ConfigureAwait(false); - if (status is not PermissionStatus.Granted) - { - throw new PermissionException("Storage permission is not granted."); - } - } - if (Android.OS.Environment.ExternalStorageDirectory is not null) { initialPath = initialPath.Replace(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, string.Empty, StringComparison.InvariantCulture); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs index 76d182a070..9032284d62 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FileSaver/FileSaverImplementation.tizen.cs @@ -7,12 +7,6 @@ public sealed partial class FileSaverImplementation : IFileSaver { async Task InternalSaveAsync(string initialPath, string fileName, Stream stream, IProgress? progress, CancellationToken cancellationToken) { - var status = await Permissions.RequestAsync().WaitAsync(cancellationToken); - if (status is not PermissionStatus.Granted) - { - throw new PermissionException("Storage permission is not granted."); - } - using var dialog = new FileFolderDialog(true, initialPath, fileName: fileName); var path = await dialog.Open().WaitAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs index c606d6cf68..961ed6c8b7 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.android.cs @@ -24,15 +24,6 @@ static async Task InternalPickAsync(string initialPath, CancellationToke Folder? folder = null; - if (!OperatingSystem.IsAndroidVersionAtLeast(33)) - { - var statusRead = await Permissions.RequestAsync().WaitAsync(cancellationToken).ConfigureAwait(false); - if (statusRead is not PermissionStatus.Granted) - { - throw new PermissionException("Storage permission is not granted."); - } - } - if (Android.OS.Environment.ExternalStorageDirectory is not null) { initialPath = initialPath.Replace(Android.OS.Environment.ExternalStorageDirectory.AbsolutePath, string.Empty, StringComparison.InvariantCulture); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs index 24a72f613f..fb29ed75f1 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/FolderPicker/FolderPickerImplementation.tizen.cs @@ -8,12 +8,6 @@ public sealed partial class FolderPickerImplementation : IFolderPicker { async Task InternalPickAsync(string initialPath, CancellationToken cancellationToken) { - var status = await Permissions.RequestAsync().WaitAsync(cancellationToken); - if (status is not PermissionStatus.Granted) - { - throw new PermissionException("Storage permission is not granted."); - } - using var dialog = new FileFolderDialog(false, initialPath); var path = await dialog.Open().WaitAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.shared.cs index a998fcb3bb..e28d506e41 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.shared.cs @@ -35,13 +35,6 @@ public event EventHandler StateChanged public async Task StartListenAsync(SpeechToTextOptions options, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - - var isPermissionGranted = await IsSpeechPermissionAuthorized(cancellationToken).ConfigureAwait(false); - if (!isPermissionGranted) - { - throw new PermissionException($"{nameof(Permissions)}.{nameof(Permissions.Microphone)} Not Granted"); - } - await InternalStartListening(options, cancellationToken); } @@ -75,11 +68,5 @@ public async Task RequestPermissions(CancellationToken cancellationToken = var status = await Permissions.RequestAsync().WaitAsync(cancellationToken).ConfigureAwait(false); return status is PermissionStatus.Granted; } - - static async Task IsSpeechPermissionAuthorized(CancellationToken cancellationToken) - { - var status = await Permissions.CheckStatusAsync().WaitAsync(cancellationToken).ConfigureAwait(false); - return status is PermissionStatus.Granted; - } #endif } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SharedSpeechToTextImplementation.macios.cs b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SharedSpeechToTextImplementation.macios.cs index 9871bea8f9..da55d39f47 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SharedSpeechToTextImplementation.macios.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SharedSpeechToTextImplementation.macios.cs @@ -41,12 +41,6 @@ public Task RequestPermissions(CancellationToken cancellationToken = defau return taskResult.Task.WaitAsync(cancellationToken); } - static Task IsSpeechPermissionAuthorized(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - return Task.FromResult(SFSpeechRecognizer.AuthorizationStatus is SFSpeechRecognizerAuthorizationStatus.Authorized); - } - static void InitializeAvAudioSession(out AVAudioSession sharedAvAudioSession) { sharedAvAudioSession = AVAudioSession.SharedInstance(); diff --git a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.shared.cs b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.shared.cs index 5d01647326..95cd305546 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.shared.cs @@ -36,12 +36,6 @@ public async Task StartListenAsync(SpeechToTextOptions options, CancellationToke { cancellationToken.ThrowIfCancellationRequested(); - var isPermissionGranted = await IsSpeechPermissionAuthorized(cancellationToken).ConfigureAwait(false); - if (!isPermissionGranted) - { - throw new PermissionException($"{nameof(Permissions)}.{nameof(Permissions.Microphone)} Not Granted"); - } - await InternalStartListeningAsync(options, cancellationToken).ConfigureAwait(false); } @@ -70,11 +64,5 @@ public async Task RequestPermissions(CancellationToken cancellationToken = var status = await Permissions.RequestAsync().WaitAsync(cancellationToken).ConfigureAwait(false); return status is PermissionStatus.Granted; } - - static async Task IsSpeechPermissionAuthorized(CancellationToken cancellationToken) - { - var status = await Permissions.CheckStatusAsync().WaitAsync(cancellationToken).ConfigureAwait(false); - return status is PermissionStatus.Granted; - } #endif } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Essentials/SpeechToTextTests.cs b/src/CommunityToolkit.Maui.UnitTests/Essentials/SpeechToTextTests.cs index 8e2ffd11d1..66a99dd8c9 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Essentials/SpeechToTextTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Essentials/SpeechToTextTests.cs @@ -21,7 +21,7 @@ public void SpeechToTextTestsSetDefaultUpdatesInstance() public async Task StartListenAsyncFailsOnNet() { SpeechToText.SetDefault(new SpeechToTextImplementation()); - await Assert.ThrowsAsync(() => SpeechToText.StartListenAsync(new SpeechToTextOptions { Culture = CultureInfo.CurrentCulture }, TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(() => SpeechToText.StartListenAsync(new SpeechToTextOptions { Culture = CultureInfo.CurrentCulture }, TestContext.Current.CancellationToken)); } [Fact(Timeout = (int)TestDuration.Long)] diff --git a/src/CommunityToolkit.Maui.UnitTests/Layouts/StateContainerTests.cs b/src/CommunityToolkit.Maui.UnitTests/Layouts/StateContainerTests.cs index 883e7a87ed..5e6a01c9b8 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Layouts/StateContainerTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Layouts/StateContainerTests.cs @@ -255,7 +255,7 @@ public async Task StateContainer_FuncAnimation_Timeout() var cancelledTokenSource = new CancellationTokenSource(TimeSpan.FromMicroseconds(1)); await Task.Delay(10, TestContext.Current.CancellationToken); - await Assert.ThrowsAsync(() => StateContainer.ChangeStateWithAnimation(layout, StateKey.Error, null, CustomAnimation, cancelledTokenSource.Token)); + await Assert.ThrowsAnyAsync(() => StateContainer.ChangeStateWithAnimation(layout, StateKey.Error, null, CustomAnimation, cancelledTokenSource.Token)); static Task CustomAnimation(VisualElement element, CancellationToken token) => element.RotateToAsync(0.75, 1000).WaitAsync(token); }