Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/Essentials/src/Permissions/Permissions.windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Windows.ApplicationModel.Contacts;
using Windows.Devices.Enumeration;
using Windows.Devices.Geolocation;
using Windows.Media.Capture;

namespace Microsoft.Maui.ApplicationModel
{
Expand Down Expand Up @@ -173,6 +174,57 @@ public partial class Media : BasePlatformPermission

public partial class Microphone : BasePlatformPermission
{
/// <inheritdoc/>
protected override Func<IEnumerable<string>> RequiredDeclarations => () => ["microphone"];

/// <inheritdoc/>
public override Task<PermissionStatus> CheckStatusAsync()
{
EnsureDeclared();
return Task.FromResult(CheckStatus() switch
{
DeviceAccessStatus.Allowed => PermissionStatus.Granted,
DeviceAccessStatus.DeniedBySystem => PermissionStatus.Denied,
DeviceAccessStatus.DeniedByUser => PermissionStatus.Denied,
_ => PermissionStatus.Unknown,
});
}

/// <inheritdoc/>
public override async Task<PermissionStatus> RequestAsync()
{
EnsureDeclared();

// If already explicitly allowed, return that
var status = CheckStatus();
if (status == DeviceAccessStatus.Allowed)
return PermissionStatus.Granted;

try
{
var settings = new MediaCaptureInitializationSettings
{
StreamingCaptureMode = StreamingCaptureMode.Audio
};

using (var mediaCapture = new MediaCapture())
{
await mediaCapture.InitializeAsync(settings);
return PermissionStatus.Granted;
}
}
catch (UnauthorizedAccessException)
{
return PermissionStatus.Denied;
}
catch
{
return PermissionStatus.Unknown;
}
}

private DeviceAccessStatus CheckStatus()
=> DeviceAccessInformation.CreateFromDeviceClass(DeviceClass.AudioCapture).CurrentStatus;
}

public partial class NearbyWifiDevices : BasePlatformPermission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Microsoft.Maui.Media.SpeechOptions.Rate.get -> float?
Microsoft.Maui.Media.SpeechOptions.Rate.set -> void
*REMOVED*Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult!>!>!
Microsoft.Maui.Storage.IFilePicker.PickMultipleAsync(Microsoft.Maui.Storage.PickOptions? options = null) -> System.Threading.Tasks.Task<System.Collections.Generic.IEnumerable<Microsoft.Maui.Storage.FileResult?>!>!
~override Microsoft.Maui.ApplicationModel.Permissions.Microphone.CheckStatusAsync() -> System.Threading.Tasks.Task<Microsoft.Maui.ApplicationModel.PermissionStatus>
~override Microsoft.Maui.ApplicationModel.Permissions.Microphone.RequestAsync() -> System.Threading.Tasks.Task<Microsoft.Maui.ApplicationModel.PermissionStatus>
~override Microsoft.Maui.ApplicationModel.Permissions.Microphone.RequiredDeclarations.get -> System.Func<System.Collections.Generic.IEnumerable<string>>
static Microsoft.Maui.Authentication.WebAuthenticator.AuthenticateAsync(Microsoft.Maui.Authentication.WebAuthenticatorOptions! webAuthenticatorOptions, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.WebAuthenticatorResult!>!
static Microsoft.Maui.Authentication.WebAuthenticator.AuthenticateAsync(System.Uri! url, System.Uri! callbackUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.WebAuthenticatorResult!>!
static Microsoft.Maui.Authentication.WebAuthenticatorExtensions.AuthenticateAsync(this Microsoft.Maui.Authentication.IWebAuthenticator! webAuthenticator, System.Uri! url, System.Uri! callbackUrl, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<Microsoft.Maui.Authentication.WebAuthenticatorResult!>!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.HIGH_SAMPLING_RATE_SENSORS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<queries>
<!-- Email -->
<intent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@
<string>Access to your location is required for cool things to happen!</string>
<key>NSContactsUsageDescription</key>
<string>Contacts</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for microphone tests.</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@

<Capabilities>
<rescap:Capability Name="runFullTrust" />
<!-- Microphone device capability for Essentials.DeviceTests -->
<DeviceCapability Name="microphone" />
</Capabilities>

</Package>
2 changes: 2 additions & 0 deletions src/Essentials/test/DeviceTests/Platforms/iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,7 @@
<string>Access to your location is required for cool things to happen!</string>
<key>NSContactsUsageDescription</key>
<string>Contacts</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required for microphone tests.</string>
</dict>
</plist>
92 changes: 92 additions & 0 deletions src/Essentials/test/DeviceTests/Tests/Microphone_Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel;
using Xunit;

namespace Microsoft.Maui.Essentials.DeviceTests;

[Category("Permissions")]
public class Microphone_Tests
{
[Fact]
[Trait(Traits.InteractionType, Traits.InteractionTypes.Human)]
public async Task Request_Microphone_Denied_Returns_Denied()
{
// If microphone permission was previously granted, the OS won't show a new prompt.
// In that case we can't exercise the prompt flow, so skip the test.
var initial = await Permissions.CheckStatusAsync<Permissions.Microphone>();
if (initial == PermissionStatus.Granted)
return; // nothing to prompt; skip

// This test requires human interaction: When prompted, choose Deny for microphone access
var status = await MainThread.InvokeOnMainThreadAsync(Permissions.RequestAsync<Permissions.Microphone>);
Assert.Equal(PermissionStatus.Denied, status);

// And subsequent checks should reflect Denied
var check = await Permissions.CheckStatusAsync<Permissions.Microphone>();
Assert.Equal(PermissionStatus.Denied, check);
}

[Fact]
[Trait(Traits.InteractionType, Traits.InteractionTypes.Human)]
public async Task Request_Microphone_Allowed_Returns_Granted()
{
// If microphone permission was previously granted, the OS won't show a new prompt.
// In that case we can't exercise the prompt flow, so skip the test.
var initial = await Permissions.CheckStatusAsync<Permissions.Microphone>();
if (initial == PermissionStatus.Granted)
return; // nothing to prompt; skip

// This test requires human interaction: When prompted, choose Allow for microphone access
var status = await MainThread.InvokeOnMainThreadAsync(Permissions.RequestAsync<Permissions.Microphone>);
Assert.Equal(PermissionStatus.Granted, status);

// And subsequent checks should reflect Granted
var check = await Permissions.CheckStatusAsync<Permissions.Microphone>();
Assert.Equal(PermissionStatus.Granted, check);
}

[Fact]
[Trait(Traits.InteractionType, Traits.InteractionTypes.Human)]
public async Task Request_Microphone_Granted_Then_RequestAgain_Returns_Granted()
{
// Ensure we have the Granted state first (may require human interaction on first request)
var status = await Permissions.CheckStatusAsync<Permissions.Microphone>();
if (status != PermissionStatus.Granted)
{
// Human: choose Allow when prompted
status = await MainThread.InvokeOnMainThreadAsync(Permissions.RequestAsync<Permissions.Microphone>);
if (status != PermissionStatus.Granted)
return; // Can't proceed with this test if user denied; treat as skip
}

// Second request should short-circuit and still return Granted without a new prompt
var second = await MainThread.InvokeOnMainThreadAsync(Permissions.RequestAsync<Permissions.Microphone>);
Assert.Equal(PermissionStatus.Granted, second);

// Status check remains Granted
var check = await Permissions.CheckStatusAsync<Permissions.Microphone>();
Assert.Equal(PermissionStatus.Granted, check);
}

[Fact]
[Trait(Traits.InteractionType, Traits.InteractionTypes.Human)]
public async Task Request_Microphone_Denied_Then_RequestAgain_Returns_Denied()
{
// If already granted we can't meaningfully test denied flow again without application/OS settings reset
var initial = await Permissions.CheckStatusAsync<Permissions.Microphone>();
if (initial == PermissionStatus.Granted)
return; // skip

// First request: Human choose Deny when prompted (if prompt appears)
var first = await MainThread.InvokeOnMainThreadAsync(Permissions.RequestAsync<Permissions.Microphone>);
if (first != PermissionStatus.Denied)
return; // can't assert repeat deny path if user allowed or unknown

// Second request should return Denied again (idempotent) and not elevate permission
var second = await MainThread.InvokeOnMainThreadAsync(Permissions.RequestAsync<Permissions.Microphone>);
Assert.Equal(PermissionStatus.Denied, second);

var check = await Permissions.CheckStatusAsync<Permissions.Microphone>();
Assert.Equal(PermissionStatus.Denied, check);
}
}
Loading