Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,20 @@ public CameraViewPage(CameraViewViewModel viewModel, IFileSystem fileSystem, IFi
protected override async void OnAppearing()
{
base.OnAppearing();
var cameraRequest = await Permissions.RequestAsync<Permissions.Camera>();
var microphoneRequest = await Permissions.RequestAsync<Permissions.Microphone>();
if (cameraRequest is not PermissionStatus.Granted)
{
await Shell.Current.CurrentPage.DisplayAlert("Camera permission is not granted.", "Please grant the permission to use this feature.", "OK");
return;
}

if (microphoneRequest is not PermissionStatus.Granted)
{
await Shell.Current.CurrentPage.DisplayAlert("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);
}
Expand Down Expand Up @@ -124,6 +137,13 @@ async void SaveVideo(object? sender, EventArgs? e)
}
else
{
var status = await Permissions.RequestAsync<Permissions.StorageWrite>();
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,4 @@ async void DisplayPopup(object? sender, EventArgs? e)
popupMediaElement.Stop();
popupMediaElement.Source = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ public partial class ConvertersGalleryViewModel() : BaseGalleryViewModel(
SectionModel.Create<MultiMathExpressionConverterViewModel>(nameof(MultiMathExpressionConverter), "A converter that allows users to calculate multiple math expressions at runtime."),
SectionModel.Create<TextCaseConverterViewModel>(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<VariableMultiValueConverterViewModel>(nameof(VariableMultiValueConverter), "A converter that allows you to combine multiple boolean bindings into a single binding."),
]);
]);
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,29 @@ public partial class FileSaverViewModel(IFileSaver fileSaver) : BaseViewModel
[ObservableProperty]
public partial double Progress { get; set; }

static async Task<bool> RequestPermissions()
{
var readPermissionStatus = await Permissions.RequestAsync<Permissions.StorageRead>();
var writePermissionStatus = await Permissions.RequestAsync<Permissions.StorageWrite>();

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 RequestPermissions())
{
return;
}

using var stream = new MemoryStream(Encoding.Default.GetBytes("Hello from the Community Toolkit!"));
try
{
Expand All @@ -32,6 +52,11 @@ async Task SaveFile(CancellationToken cancellationToken)
[RelayCommand]
async Task SaveFileStatic(CancellationToken cancellationToken)
{
if (!await RequestPermissions())
{
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)
Expand All @@ -47,6 +72,11 @@ async Task SaveFileStatic(CancellationToken cancellationToken)
[RelayCommand]
async Task SaveFileInstance(CancellationToken cancellationToken)
{
if (!await RequestPermissions())
{
return;
}

using var client = new HttpClient();

const string communityToolkitNuGetUrl = "https://www.nuget.org/api/v2/package/CommunityToolkit.Maui/5.0.0";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> RequestPermissions()
{
this.folderPicker = folderPicker;
var readPermissionStatus = await Permissions.RequestAsync<Permissions.StorageRead>();
var writePermissionStatus = await Permissions.RequestAsync<Permissions.StorageWrite>();

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 RequestPermissions())
{
return;
}

var folderPickerResult = await folderPicker.PickAsync(cancellationToken);
if (folderPickerResult.IsSuccessful)
{
Expand All @@ -31,6 +47,11 @@ async Task PickFolder(CancellationToken cancellationToken)
[RelayCommand]
async Task PickFolderStatic(CancellationToken cancellationToken)
{
if (!await RequestPermissions())
{
return;
}

var folderResult = await FolderPicker.PickAsync("DCIM", cancellationToken);
if (folderResult.IsSuccessful)
{
Expand All @@ -46,6 +67,11 @@ async Task PickFolderStatic(CancellationToken cancellationToken)
[RelayCommand]
async Task PickFolderInstance(CancellationToken cancellationToken)
{
if (!await RequestPermissions())
{
return;
}

var folderPickerInstance = new FolderPickerImplementation();
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,19 @@ public OfflineSpeechToTextViewModel()
[ObservableProperty]
public partial string? RecognitionText { get; set; } = "Welcome to .NET MAUI Community Toolkit!";

static async Task<bool> RequestPermissions(ISpeechToText speechToText)
{
var microphonePermissionStatus = await Permissions.RequestAsync<Permissions.Microphone>();
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 RequestPermissions(speechToText);
if (!isGranted)
{
await Toast.Make("Permission not granted").Show(CancellationToken.None);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> RequestPermissions(ISpeechToText speechToText)
{
var microphonePermissionStatus = await Permissions.RequestAsync<Permissions.Microphone>();
var isSpeechToTextPermissionsGranted = await speechToText.RequestPermissions(CancellationToken.None);

return microphonePermissionStatus is PermissionStatus.Granted
&& isSpeechToTextPermissionsGranted;
}

[RelayCommand]
async Task SetLocales(CancellationToken token)
{
Expand Down Expand Up @@ -85,7 +99,7 @@ async Task StartListen()
CanStartListenExecute = false;
CanStopListenExecute = true;

var isGranted = await speechToText.RequestPermissions(CancellationToken.None);
var isGranted = await RequestPermissions(speechToText);
if (!isGranted)
{
await Toast.Make("Permission not granted").Show(CancellationToken.None);
Expand Down Expand Up @@ -146,9 +160,4 @@ void HandleLocalesCollectionChanged(object? sender, NotifyCollectionChangedEvent
{
OnPropertyChanged(nameof(CurrentLocale));
}

public async ValueTask DisposeAsync()
{
await speechToText.DisposeAsync();
}
}
23 changes: 0 additions & 23 deletions src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,15 @@
/// <exception cref="NullReferenceException">Thrown when no <see cref="CameraProvider"/> can be resolved.</exception>
/// <exception cref="InvalidOperationException">Thrown when there are no cameras available.</exception>
partial class CameraManager(
IMauiContext mauiContext,

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (macos-26)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'mauiContext' is unread.

Check warning on line 18 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Sample App using Latest .NET SDK (macos-26)

Parameter 'mauiContext' is unread.
ICameraView cameraView,

Check warning on line 19 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (macos-26)

Parameter 'cameraView' is unread.

Check warning on line 19 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (windows-latest)

Parameter 'cameraView' is unread.

Check warning on line 19 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'cameraView' is unread.

Check warning on line 19 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'cameraView' is unread.

Check warning on line 19 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'cameraView' is unread.

Check warning on line 19 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'cameraView' is unread.
ICameraProvider cameraProvider,

Check warning on line 20 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (macos-26)

Parameter 'cameraProvider' is unread.

Check warning on line 20 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Run Benchmarks (windows-latest)

Parameter 'cameraProvider' is unread.

Check warning on line 20 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'cameraProvider' is unread.

Check warning on line 20 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (windows-latest)

Parameter 'cameraProvider' is unread.

Check warning on line 20 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'cameraProvider' is unread.

Check warning on line 20 in src/CommunityToolkit.Maui.Camera/CameraManager.shared.cs

View workflow job for this annotation

GitHub Actions / Build Library (macos-26)

Parameter 'cameraProvider' is unread.
Action onLoaded) : IDisposable
{
internal Action OnLoaded { get; } = onLoaded;

internal bool IsInitialized { get; private set; }

/// <summary>
/// Whether the user has granted the required permissions through the use of the <see cref="Permissions"/> API.
/// </summary>
/// <returns>Returns <c>true</c> if permission has been granted, <c>false</c> otherwise.</returns>
public async Task<bool> ArePermissionsGranted()
{
var cameraRequest = await Permissions.RequestAsync<Permissions.Camera>();
var microphoneRequest = await Permissions.RequestAsync<Permissions.Microphone>();
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;
}

/// <summary>
/// Connects to the camera.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ static async Task<string> InternalSaveAsync(string initialPath, string fileName,

AndroidUri? filePath = null;

if (!OperatingSystem.IsAndroidVersionAtLeast(33))
{
var status = await Permissions.RequestAsync<Permissions.StorageWrite>().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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ public sealed partial class FileSaverImplementation : IFileSaver
{
async Task<string> InternalSaveAsync(string initialPath, string fileName, Stream stream, IProgress<double>? progress, CancellationToken cancellationToken)
{
var status = await Permissions.RequestAsync<Permissions.StorageRead>().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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,6 @@ static async Task<Folder> InternalPickAsync(string initialPath, CancellationToke

Folder? folder = null;

if (!OperatingSystem.IsAndroidVersionAtLeast(33))
{
var statusRead = await Permissions.RequestAsync<Permissions.StorageRead>().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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ public sealed partial class FolderPickerImplementation : IFolderPicker
{
async Task<Folder> InternalPickAsync(string initialPath, CancellationToken cancellationToken)
{
var status = await Permissions.RequestAsync<Permissions.StorageRead>().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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ public event EventHandler<SpeechToTextStateChangedEventArgs> 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);
}

Expand Down Expand Up @@ -75,11 +68,5 @@ public async Task<bool> RequestPermissions(CancellationToken cancellationToken =
var status = await Permissions.RequestAsync<Permissions.Microphone>().WaitAsync(cancellationToken).ConfigureAwait(false);
return status is PermissionStatus.Granted;
}

static async Task<bool> IsSpeechPermissionAuthorized(CancellationToken cancellationToken)
{
var status = await Permissions.CheckStatusAsync<Permissions.Microphone>().WaitAsync(cancellationToken).ConfigureAwait(false);
return status is PermissionStatus.Granted;
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ public Task<bool> RequestPermissions(CancellationToken cancellationToken = defau
return taskResult.Task.WaitAsync(cancellationToken);
}

static Task<bool> IsSpeechPermissionAuthorized(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(SFSpeechRecognizer.AuthorizationStatus is SFSpeechRecognizerAuthorizationStatus.Authorized);
}

static void InitializeAvAudioSession(out AVAudioSession sharedAvAudioSession)
{
sharedAvAudioSession = AVAudioSession.SharedInstance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -70,11 +64,5 @@ public async Task<bool> RequestPermissions(CancellationToken cancellationToken =
var status = await Permissions.RequestAsync<Permissions.Microphone>().WaitAsync(cancellationToken).ConfigureAwait(false);
return status is PermissionStatus.Granted;
}

static async Task<bool> IsSpeechPermissionAuthorized(CancellationToken cancellationToken)
{
var status = await Permissions.CheckStatusAsync<Permissions.Microphone>().WaitAsync(cancellationToken).ConfigureAwait(false);
return status is PermissionStatus.Granted;
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public void SpeechToTextTestsSetDefaultUpdatesInstance()
public async Task StartListenAsyncFailsOnNet()
{
SpeechToText.SetDefault(new SpeechToTextImplementation());
await Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => SpeechToText.StartListenAsync(new SpeechToTextOptions { Culture = CultureInfo.CurrentCulture }, TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<NotSupportedException>(() => SpeechToText.StartListenAsync(new SpeechToTextOptions { Culture = CultureInfo.CurrentCulture }, TestContext.Current.CancellationToken));
}

[Fact(Timeout = (int)TestDuration.Long)]
Expand Down
Loading
Loading