diff --git a/.github/workflows/dotnet-build.yml b/.github/workflows/dotnet-build.yml
index c2f5e5db65..71dd470e25 100644
--- a/.github/workflows/dotnet-build.yml
+++ b/.github/workflows/dotnet-build.yml
@@ -37,11 +37,13 @@ env:
PathToCommunityToolkitCameraAnalyzersCsproj: 'src/CommunityToolkit.Maui.Camera.Analyzers/CommunityToolkit.Maui.Camera.Analyzers.csproj'
PathToCommunityToolkitMediaElementAnalyzersCsproj: 'src/CommunityToolkit.Maui.MediaElement.Analyzers/CommunityToolkit.Maui.MediaElement.Analyzers.csproj'
PathToCommunityToolkitSourceGeneratorsCsproj: 'src/CommunityToolkit.Maui.SourceGenerators/CommunityToolkit.Maui.SourceGenerators.csproj'
+ PathToCommunityToolkitMediaElementSourceGeneratorsCsproj: 'src/CommunityToolkit.Maui.MediaElement.SourceGenerators/CommunityToolkit.Maui.MediaElement.SourceGenerators.csproj'
PathToCommunityToolkitAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.Analyzers.CodeFixes/CommunityToolkit.Maui.Analyzers.CodeFixes.csproj'
PathToCommunityToolkitCameraAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.Camera.Analyzers.CodeFixes/CommunityToolkit.Maui.Camera.Analyzers.CodeFixes.csproj'
PathToCommunityToolkitMediaElementAnalyzersCodeFixCsproj: 'src/CommunityToolkit.Maui.MediaElement.Analyzers.CodeFixes/CommunityToolkit.Maui.MediaElement.Analyzers.CodeFixes.csproj'
PathToCommunityToolkitAnalyzersUnitTestProjectDirectory: 'src/CommunityToolkit.Maui.Analyzers.UnitTests'
PathToCommunityToolkitSourceGeneratorsUnitTestDirectory: 'src/CommunityToolkit.Maui.SourceGenerators.UnitTests'
+ PathToCommunityToolkitMediaElementSourceGeneratorsUnitTestDirectory: 'src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests'
PathToCommunityToolkitSourceGeneratorsUnitTestCsproj: 'src/CommunityToolkit.Maui.SourceGenerators.UnitTests/CommunityToolkit.Maui.SourceGenerators.UnitTests.csproj'
PathToCommunityToolkitAnalyzersBenchmarkCsproj: 'src/CommunityToolkit.Maui.Analyzers.Benchmarks/CommunityToolkit.Maui.Analyzers.Benchmarks.csproj'
CommunityToolkitLibrary_Xcode_Version: '26.1'
@@ -189,6 +191,9 @@ jobs:
- name: 'Build CommunityToolkit.Maui.SourceGenerators'
run: dotnet build ${{ env.PathToCommunityToolkitSourceGeneratorsCsproj }} -c Release
+ - name: 'Build CommunityToolkit.Maui.MediaElement.SourceGenerators'
+ run: dotnet build ${{ env.PathToCommunityToolkitMediaElementSourceGeneratorsCsproj }} -c Release
+
- name: 'Build CommunityToolkit.Maui.Camera'
run: dotnet build ${{ env.PathToCommunityToolkitCameraCsproj }} -c Release -p:PackageVersion=${{ env.NugetPackageVersionCamera }} -p:Version=${{ env.NugetPackageVersionCamera }}
@@ -215,6 +220,12 @@ jobs:
cd ${{ env.PathToCommunityToolkitSourceGeneratorsUnitTestDirectory }}
dotnet run -c Release --results-directory "${{ runner.temp }}" --coverage --coverage-output "${{ runner.temp }}/ut-sourcegenerators.cobertura.xml" --coverage-output-format cobertura --report-xunit
+ - name: Run CommunityToolkit MediaElement Source Generators UnitTests
+ if: runner.os == 'Windows'
+ run: |
+ cd ${{ env.PathToCommunityToolkitMediaElementSourceGeneratorsUnitTestDirectory }}
+ dotnet run -c Release --results-directory "${{ runner.temp }}" --coverage --coverage-output "${{ runner.temp }}/ut-mediaelement-sourcegenerators.cobertura.xml" --coverage-output-format cobertura --report-xunit
+
- name: Run CommunityToolkit UnitTests
run: |
cd ${{ env.PathToCommunityToolkitUnitTestProjectDirectory }}
diff --git a/Directory.Build.props b/Directory.Build.props
index 5e0169388e..a9dd7b9ebb 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -22,11 +22,11 @@ https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitati
all
+ SourceGen
10.0.30
true
true
true
- SourceGen
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/Android/SamsungBadgeProvider.cs b/samples/CommunityToolkit.Maui.Sample/Platforms/Android/SamsungBadgeProvider.cs
index f289f25bea..c0be174afb 100644
--- a/samples/CommunityToolkit.Maui.Sample/Platforms/Android/SamsungBadgeProvider.cs
+++ b/samples/CommunityToolkit.Maui.Sample/Platforms/Android/SamsungBadgeProvider.cs
@@ -13,7 +13,7 @@ class SamsungBadgeProvider : IBadgeProvider
public void SetCount(uint count)
{
- var contentUri = Android.Net.Uri.Parse(contentStringUri);
+ var contentUri = global::Android.Net.Uri.Parse(contentStringUri);
if (contentUri is null)
{
return;
diff --git a/src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitMediaElementInitializationAnalyzerTests.cs b/src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitMediaElementInitializationAnalyzerTests.cs
index da013837c2..6d72252aad 100644
--- a/src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitMediaElementInitializationAnalyzerTests.cs
+++ b/src/CommunityToolkit.Maui.Analyzers.UnitTests/UseCommunityToolkitMediaElementInitializationAnalyzerTests.cs
@@ -1,4 +1,4 @@
-using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core;
using CommunityToolkit.Maui.MediaElement.Analyzers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
@@ -34,7 +34,7 @@ public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp()
- .UseMauiCommunityToolkitMediaElement()
+ .UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false)
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
@@ -69,7 +69,7 @@ public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder ();
builder.UseMauiApp ()
- .UseMauiCommunityToolkitMediaElement ()
+ .UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false)
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
@@ -139,7 +139,7 @@ public static MauiApp CreateMauiApp()
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp()
#if ANDROID || IOS
- .UseMauiCommunityToolkitMediaElement()
+ .UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false)
#endif
.ConfigureFonts(fonts =>
{
diff --git a/src/CommunityToolkit.Maui.Analyzers/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Maui.Analyzers/AnalyzerReleases.Shipped.md
index c96143bb0d..4be37bea94 100644
--- a/src/CommunityToolkit.Maui.Analyzers/AnalyzerReleases.Shipped.md
+++ b/src/CommunityToolkit.Maui.Analyzers/AnalyzerReleases.Shipped.md
@@ -2,9 +2,9 @@
### New Rules
-Rule ID | Category | Severity | Notes
+Rule ID | Category | Severity | Notes
--------|----------|----------|-----------------------------------------------------
-MCT001 | Usage | Error | `.UseMauiCommunityToolkit()` Not Found on MauiAppBuilder
+MCT001 | Usage | Error | `.UseMauiCommunityToolkit()` Not Found on MauiAppBuilder
## Release 11.1.0
@@ -12,4 +12,4 @@ MCT001 | Usage | Error | `.UseMauiCommunityToolkit()` Not Found on MauiAp
Rule ID | Category | Severity | Notes
--------|----------|----------|-----------------------------------------------------
-MCT002 | Usage | Error | The value of MaximumRating must be between 1 and 10
\ No newline at end of file
+MCT002 | Usage | Error | The value of MaximumRating must be between 1 and 10
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.Designer.cs b/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.Designer.cs
index ef9d314a7e..3961bb34ac 100644
--- a/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.Designer.cs
+++ b/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.Designer.cs
@@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -11,32 +12,46 @@ namespace CommunityToolkit.Maui.MediaElement.Analyzers {
using System;
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.1.0.0")]
- [System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
- private static System.Resources.ResourceManager resourceMan;
+ private static global::System.Resources.ResourceManager resourceMan;
- private static System.Globalization.CultureInfo resourceCulture;
+ private static global::System.Globalization.CultureInfo resourceCulture;
- [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
- [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static System.Resources.ResourceManager ResourceManager {
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
get {
- if (object.Equals(null, resourceMan)) {
- System.Resources.ResourceManager temp = new System.Resources.ResourceManager("CommunityToolkit.Maui.MediaElement.Analyzers.Resources", typeof(Resources).Assembly);
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CommunityToolkit.Maui.MediaElement.Analyzers.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
- [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
- internal static System.Globalization.CultureInfo Culture {
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@@ -45,18 +60,27 @@ internal static System.Globalization.CultureInfo Culture {
}
}
+ ///
+ /// Looks up a localized string similar to `.UseMauiCommunityToolkitMediaElement()` must be chained to `.UseMauiApp<T>()`.
+ ///
internal static string InitalizationMessageFormat {
get {
return ResourceManager.GetString("InitalizationMessageFormat", resourceCulture);
}
}
+ ///
+ /// Looks up a localized string similar to `.UseMauiCommunityToolkitMediaElement()` is required to initalize .NET MAUI Community Toolkit MediaElement..
+ ///
internal static string InitializationErrorMessage {
get {
return ResourceManager.GetString("InitializationErrorMessage", resourceCulture);
}
}
+ ///
+ /// Looks up a localized string similar to `.UseMauiCommunityToolkitMediaElement()` Not Found on MauiAppBuilder.
+ ///
internal static string InitializationErrorTitle {
get {
return ResourceManager.GetString("InitializationErrorTitle", resourceCulture);
diff --git a/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.resx b/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.resx
index 245f8d4ec0..ccea8f3c5b 100644
--- a/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.resx
+++ b/src/CommunityToolkit.Maui.MediaElement.Analyzers/Resources.resx
@@ -1,5 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
text/microsoft-resx
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_EdgeCaseTests.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_EdgeCaseTests.cs
new file mode 100644
index 0000000000..4100faaeec
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_EdgeCaseTests.cs
@@ -0,0 +1,237 @@
+using Xunit;
+
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.AndroidMediaElementServiceConfigurationGeneratorTests;
+
+public class AndroidMediaElementForegroundServiceConfigurationGenerator_EdgeCaseTests : BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest
+{
+ [Fact]
+ public async Task GenerateConfiguration_WithMultipleMediaElementOptionsClasses_GeneratesOnce()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+ }
+
+ public class MyCustomMediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WithNestedNamespace_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ """
+ using CommunityToolkit.Maui.Core;
+
+ namespace Company.Product.App;
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WithBothPropertyAndMethod_GeneratesOnce()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+ }
+
+ public class TestConfiguration
+ {
+ public void Configure()
+ {
+ var options = new MediaElementOptions();
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WithEmptySource_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ """
+ namespace TestNamespace;
+
+ public class EmptyClass
+ {
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WithPropertyInDifferentClass_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+
+ namespace {{defaultTestNamespace}};
+
+ public class OtherOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_IntegrationTests.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_IntegrationTests.cs
new file mode 100644
index 0000000000..99dfac0e94
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_IntegrationTests.cs
@@ -0,0 +1,250 @@
+using Xunit;
+
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.AndroidMediaElementServiceConfigurationGeneratorTests;
+
+public class AndroidMediaElementForegroundServiceConfigurationGenerator_IntegrationTests : BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest
+{
+ [Fact]
+ public async Task GenerateConfiguration_RealWorldScenarioWithMauiProgram_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public static class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiApp();
+ return builder.Build();
+ }
+ }
+
+ public class App : Application
+ {
+ }
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+
+ public void SetDefaultAndroidViewType(int androidViewType) { }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_ComplexAppWithMultipleFiles_GeneratesCorrectly()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+ using System;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+ }
+
+ public static class MediaElementExtensions
+ {
+ public static MauiAppBuilder ConfigureMediaElement(this MauiAppBuilder builder)
+ {
+ var options = new MediaElementOptions();
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ return builder;
+ }
+ }
+
+ public static class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.ConfigureMediaElement();
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_PropertyAndMethodCombination_GeneratesCorrectly()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+ }
+
+ public class FirstConfiguration
+ {
+ public void Configure()
+ {
+ var options = new MediaElementOptions();
+ options.SetIsAndroidForegroundServiceEnabled(false);
+ }
+ }
+
+ public class SecondConfiguration
+ {
+ public void Configure()
+ {
+ var options = new MediaElementOptions();
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_OutputTests.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_OutputTests.cs
new file mode 100644
index 0000000000..f87a9d4c18
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_OutputTests.cs
@@ -0,0 +1,212 @@
+using Xunit;
+
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.AndroidMediaElementServiceConfigurationGeneratorTests;
+
+public class AndroidMediaElementForegroundServiceConfigurationGenerator_OutputTests : BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest
+{
+ [Fact]
+ public async Task GenerateConfiguration_WhenPropertySetToTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+ using CommunityToolkit.Maui.Core.Views;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenMethodSetToTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+ }
+
+ public class TestConfiguration
+ {
+ public void Configure()
+ {
+ var options = new MediaElementOptions();
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenPropertySetToFalse_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenMethodSetToFalse_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using CommunityToolkit.Maui.Core;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+ }
+
+ public class TestConfiguration
+ {
+ public void Configure()
+ {
+ var options = new MediaElementOptions();
+ options.SetIsAndroidForegroundServiceEnabled(false);
+ }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenNoMediaElementOptions_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using Microsoft.Maui.Controls;
+
+ namespace {{defaultTestNamespace}};
+
+ public class {{defaultTestClassName}} : View
+ {
+ public string Text { get; set; } = "";
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_UseMauiCommunityToolkitMediaElementTests.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_UseMauiCommunityToolkitMediaElementTests.cs
new file mode 100644
index 0000000000..4b27a99d0c
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/AndroidMediaElementForegroundServiceConfigurationGenerator_UseMauiCommunityToolkitMediaElementTests.cs
@@ -0,0 +1,868 @@
+using Xunit;
+
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.AndroidMediaElementServiceConfigurationGeneratorTests;
+
+///
+/// Unit tests for AndroidMediaElementServiceConfigurationGenerator to verify that the generator
+/// correctly detects when UseMauiCommunityToolkitMediaElement is called with isAndroidForegroundServiceEnabled set to true
+/// and generates the appropriate AndroidMediaElementServiceConfiguration.g.cs file.
+///
+public class AndroidMediaElementForegroundServiceConfigurationGenerator_UseMauiCommunityToolkitMediaElementTests : BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest
+{
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: true);
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithTrueAndOptionsTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: true, static options =>
+ {
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ });
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithFalseAndOptionsTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false, static options =>
+ {
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ });
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithFalse_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false);
+ return builder.Build();
+ }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithTrueAndOptions_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(
+ isAndroidForegroundServiceEnabled: true,
+ options: static opts =>
+ {
+ // Configure options here
+ });
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithBoolVariableTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ const bool enableForegroundService = true;
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: enableForegroundService);
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithPositionalArgument_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(true);
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenMultipleUseMauiCommunityToolkitMediaElementCallsWithTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class FirstConfiguration
+ {
+ public MauiAppBuilder Configure(MauiAppBuilder builder)
+ {
+ return builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: true);
+ }
+ }
+
+ public class SecondConfiguration
+ {
+ public MauiAppBuilder Configure(MauiAppBuilder builder)
+ {
+ return builder.UseMauiCommunityToolkitMediaElement(true);
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementNotCalled_GeneratesNoCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = false;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ return builder.Build();
+ }
+ }
+ """;
+
+ await VerifySourceGeneratorAsync(source);
+ }
+
+ [Fact]
+ public async Task GenerateConfiguration_WhenCombinedWithPropertyAndMethodAndExtensionMethodTrue_GeneratesCorrectCode()
+ {
+ const string source =
+ /* language=C#-test */
+ $$"""
+ using System;
+ using CommunityToolkit.Maui.Core;
+ using Microsoft.Maui.Hosting;
+
+ namespace {{defaultTestNamespace}};
+
+ public class MediaElementOptions
+ {
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled)
+ {
+ IsAndroidForegroundServiceEnabled = isEnabled;
+ }
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
+ }
+
+ public static class AppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(
+ this MauiAppBuilder builder,
+ bool isAndroidForegroundServiceEnabled,
+ Action? options = null)
+ {
+ var mediaElementOptions = new MediaElementOptions();
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
+ return builder;
+ }
+ }
+
+ public class MauiProgram
+ {
+ public static MauiApp CreateMauiApp()
+ {
+ var builder = MauiApp.CreateBuilder();
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: true);
+ return builder.Build();
+ }
+ }
+ """;
+
+ const string expectedGenerated =
+ /* language=C#-test */
+ """
+ //
+ // See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator
+ // This file provides assembly-level Android permissions and service attributes
+ // when Android Foreground Service is enabled for MediaElement.
+
+ #pragma warning disable
+ #nullable enable
+
+ #if ANDROID
+ using Android.App;
+
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
+ [assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
+ [assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
+ [assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
+
+ namespace CommunityToolkit.Maui.Android;
+
+ ///
+ /// Auto-generated configuration for Android MediaElement Service.
+ ///
+ ///
+ /// This file is auto-generated and provides assembly-level permissions
+ /// for MediaElement when is enabled
+ /// or when
+ /// is called with isAndroidForegroundServiceEnabled set to true.
+ ///
+ internal static class AndroidMediaElementServiceConfiguration
+ {
+ ///
+ /// Indicates that Android MediaElement Service configuration is required.
+ ///
+ public const bool IsRequired = true;
+ }
+ #endif
+ """;
+
+ await VerifySourceGeneratorAsync(source, expectedGenerated);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest.cs
new file mode 100644
index 0000000000..20844c5b67
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/AndroidMediaElementServiceConfigurationGeneratorTests/BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest.cs
@@ -0,0 +1,20 @@
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.AndroidMediaElementServiceConfigurationGeneratorTests;
+
+public abstract class BaseAndroidMediaElementForegroundServiceConfigurationGeneratorTest : BaseTest
+{
+ protected static Task VerifySourceGeneratorAsync(string source, string expectedGeneratedFile)
+ {
+ List<(string FileName, string GeneratedFile)> expectedGeneratedFilesList =
+ [
+ ("AndroidMediaElementServiceConfiguration.g.cs", expectedGeneratedFile)
+ ];
+
+ return VerifySourceGeneratorAsync(source, expectedGeneratedFilesList);
+ }
+
+ protected static Task VerifySourceGeneratorAsync(string source)
+ {
+ List<(string FileName, string GeneratedFile)> expectedGeneratedFilesList = [];
+ return VerifySourceGeneratorAsync(source, expectedGeneratedFilesList);
+ }
+}
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/BaseTest.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/BaseTest.cs
new file mode 100644
index 0000000000..1274cbd93d
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/BaseTest.cs
@@ -0,0 +1,102 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using Xunit;
+
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests;
+
+public abstract class BaseTest
+{
+ protected const string defaultTestClassName = "TestView";
+ protected const string defaultTestNamespace = "TestNamespace";
+
+ protected static async Task VerifySourceGeneratorAsync(string source, params List<(string FileName, string GeneratedFile)> expectedGeneratedFilesList)
+ where TSourceGenerator : IIncrementalGenerator, new()
+ {
+ const string sourceGeneratorNamespace = "CommunityToolkit.Maui.MediaElement.SourceGenerators";
+ var sourceGeneratorFullName = typeof(TSourceGenerator).FullName ?? throw new InvalidOperationException("Source Generator Type Path cannot be null");
+
+ var test = new MediaElementSourceGeneratorTest
+ {
+#if NET10_0
+ ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net100,
+#else
+#error ReferenceAssemblies must be updated to current version of .NET
+#endif
+ TestState =
+ {
+ Sources = { source },
+
+ AdditionalReferences =
+ {
+ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Hosting.MauiAppBuilder).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableProperty).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindingMode).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Microsoft.CodeAnalysis.Accessibility).Assembly.Location),
+ }
+ }
+ };
+
+ // Add minimal placeholders for the CommunityToolkit.Maui.Core namespaces so
+ // test sources that include `using CommunityToolkit.Maui.Core` or
+ // `using CommunityToolkit.Maui.Core.Views` do not produce missing-namespace
+ // diagnostics when the real assembly is not referenced.
+ test.TestState.Sources.Add(@"namespace CommunityToolkit.Maui.Core
+ {
+ // Placeholder type for unit tests
+ internal static class __CommunityToolkitMauiCorePlaceholder { }
+ }");
+
+ test.TestState.Sources.Add(@"namespace CommunityToolkit.Maui.Core.Views
+ {
+ // Placeholder type for unit tests
+ internal static class __CommunityToolkitMauiCoreViewsPlaceholder { }
+ }");
+
+ // Add minimal placeholders for Microsoft.Maui hosting types used in tests so
+ // test sources that rely on `MauiApp`, `MauiAppBuilder`, `UseMauiApp` or
+ // `Application` do not produce missing-type diagnostics when the real
+ // assemblies are not available in the test environment.
+ test.TestState.Sources.Add(@"namespace Microsoft.Maui.Hosting
+ {
+ public class MauiApp
+ {
+ public static MauiAppBuilder CreateBuilder() => new MauiAppBuilder();
+ }
+
+ public class MauiAppBuilder
+ {
+ public MauiApp Build() => new MauiApp();
+ }
+
+ public static class MauiAppBuilderExtensions
+ {
+ public static MauiAppBuilder UseMauiApp(this MauiAppBuilder builder) => builder;
+ }
+ }");
+
+ // Provide a simple Application placeholder so tests can declare `class App : Application`.
+ test.TestState.Sources.Add(@"public class Application { }");
+
+ foreach (var generatedFile in expectedGeneratedFilesList.Where(static x => !string.IsNullOrEmpty(x.GeneratedFile)))
+ {
+ var expectedGeneratedText = Microsoft.CodeAnalysis.Text.SourceText.From(generatedFile.GeneratedFile, System.Text.Encoding.UTF8);
+ var generatedFilePath = Path.Combine(sourceGeneratorNamespace, sourceGeneratorFullName, generatedFile.FileName);
+ test.TestState.GeneratedSources.Add((generatedFilePath, expectedGeneratedText));
+ }
+
+ await test.RunAsync(TestContext.Current.CancellationToken);
+ }
+
+ sealed class MediaElementSourceGeneratorTest : CSharpSourceGeneratorTest
+ where TSourceGenerator : IIncrementalGenerator, new()
+ {
+ protected override CompilationOptions CreateCompilationOptions()
+ {
+ var compilationOptions = base.CreateCompilationOptions();
+ return compilationOptions;
+ }
+ }
+}
+
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.csproj b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.csproj
new file mode 100644
index 0000000000..6b9c5d6258
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.csproj
@@ -0,0 +1,41 @@
+
+
+
+ $(NetVersion)
+ true
+ true
+ false
+ true
+ Exe
+ CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/GlobalUsings.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/GlobalUsings.cs
new file mode 100644
index 0000000000..2a619c58c8
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/GlobalUsings.cs
@@ -0,0 +1,6 @@
+global using System;
+global using System.Collections.Generic;
+global using System.Collections.Immutable;
+global using System.Threading.Tasks;
+global using Microsoft.CodeAnalysis;
+global using Xunit;
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/xunit.runner.json b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/xunit.runner.json
new file mode 100644
index 0000000000..86e7fbb26f
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests/xunit.runner.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json"
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators/CommunityToolkit.Maui.MediaElement.SourceGenerators.csproj b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators/CommunityToolkit.Maui.MediaElement.SourceGenerators.csproj
new file mode 100644
index 0000000000..8c7f024e1c
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators/CommunityToolkit.Maui.MediaElement.SourceGenerators.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0
+ false
+ true
+
+
+ true
+ true
+
+
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Maui.MediaElement.SourceGenerators/Generators/AndroidMediaElementForegroundServiceConfigurationGenerator.cs b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators/Generators/AndroidMediaElementForegroundServiceConfigurationGenerator.cs
new file mode 100644
index 0000000000..0d49632179
--- /dev/null
+++ b/src/CommunityToolkit.Maui.MediaElement.SourceGenerators/Generators/AndroidMediaElementForegroundServiceConfigurationGenerator.cs
@@ -0,0 +1,271 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace CommunityToolkit.Maui.MediaElement.SourceGenerators;
+
+///
+/// Source generator that detects when Android Foreground Service is enabled for MediaElement
+/// and generates the required assembly-level permissions.
+///
+[Generator]
+public class AndroidMediaElementForegroundServiceConfigurationGenerator : IIncrementalGenerator
+{
+ const string mediaElementOptionsClassName = "MediaElementOptions";
+ const string isAndroidForegroundServiceEnabledProperty = "IsAndroidForegroundServiceEnabled";
+ const string setDefaultAndroidForegroundServiceEnabledMethod = "SetIsAndroidForegroundServiceEnabled";
+ const string useMauiCommunityToolkitMediaElementMethod = "UseMauiCommunityToolkitMediaElement";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Detect MediaElementOptions to determine if Android Foreground Service is enabled
+ var mediaElementProvider = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (syntax, _) => IsMediaElementOptionsClass(syntax),
+ transform: (ctx, _) => GetConfigurationInfo(ctx))
+ .Where(info => info is not null)
+ .Collect();
+
+ var isEnabledFromOptions = mediaElementProvider
+ .Select((configs, _) => configs.Where(c => c is not null)
+ .Cast()
+ .Any(c => c.IsForegroundServiceEnabled));
+
+ var isEnabledFromInvocation = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (syntax, _) => IsSetDefaultAndroidForegroundServiceEnabledInvocation(syntax),
+ transform: (ctx, _) => GetForegroundServiceEnabledFromInvocation(ctx))
+ .Where(isEnabled => isEnabled)
+ .Collect()
+ .Select((results, _) => results.Any());
+
+ var isEnabledFromMediaElementInvocation = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: (syntax, _) => IsUseMauiCommunityToolkitMediaElementInvocation(syntax),
+ transform: (ctx, _) => GetForegroundServiceEnabledFromMediaElementInvocation(ctx))
+ .Where(isEnabled => isEnabled)
+ .Collect()
+ .Select((results, _) => results.Any());
+
+ var isForegroundServiceEnabled = isEnabledFromOptions
+ .Combine(isEnabledFromInvocation)
+ .Combine(isEnabledFromMediaElementInvocation)
+ .Select((pair, _) => pair.Left.Left || pair.Left.Right || pair.Right);
+
+ context.RegisterSourceOutput(isForegroundServiceEnabled, (spc, isEnabled) =>
+ {
+ if (isEnabled)
+ {
+ GeneratePermissions(spc);
+ }
+ });
+ }
+
+ static bool IsMediaElementOptionsClass(SyntaxNode node)
+ {
+ return node is ClassDeclarationSyntax classDecl &&
+ classDecl.Identifier.Text.Contains(mediaElementOptionsClassName);
+ }
+
+ static bool IsSetDefaultAndroidForegroundServiceEnabledInvocation(SyntaxNode node)
+ {
+ return node is InvocationExpressionSyntax invocation &&
+ GetInvocationMethodName(invocation) == setDefaultAndroidForegroundServiceEnabledMethod;
+ }
+
+ static bool IsUseMauiCommunityToolkitMediaElementInvocation(SyntaxNode node)
+ {
+ return node is InvocationExpressionSyntax invocation &&
+ GetInvocationMethodName(invocation) == useMauiCommunityToolkitMediaElementMethod;
+ }
+
+ static bool GetForegroundServiceEnabledFromInvocation(GeneratorSyntaxContext context)
+ {
+ if (context.Node is not InvocationExpressionSyntax invocation)
+ {
+ return false;
+ }
+
+ var semanticModel = context.SemanticModel;
+ var symbolInfo = semanticModel.GetSymbolInfo(invocation);
+ var methodSymbol = symbolInfo.Symbol as IMethodSymbol ?? symbolInfo.CandidateSymbols.OfType().FirstOrDefault();
+
+ if (methodSymbol is null || methodSymbol.Name != setDefaultAndroidForegroundServiceEnabledMethod)
+ {
+ return false;
+ }
+
+ if (methodSymbol.ContainingType?.Name != mediaElementOptionsClassName)
+ {
+ return false;
+ }
+
+ if (invocation.ArgumentList.Arguments.Count == 0)
+ {
+ return false;
+ }
+
+ var firstArg = invocation.ArgumentList.Arguments[0].Expression;
+ var constantValue = semanticModel.GetConstantValue(firstArg);
+
+ return constantValue.HasValue && constantValue.Value is true;
+ }
+
+ static bool GetForegroundServiceEnabledFromMediaElementInvocation(GeneratorSyntaxContext context)
+ {
+ if (context.Node is not InvocationExpressionSyntax invocation)
+ {
+ return false;
+ }
+
+ var semanticModel = context.SemanticModel;
+ var symbolInfo = semanticModel.GetSymbolInfo(invocation);
+ var methodSymbol = symbolInfo.Symbol as IMethodSymbol ?? symbolInfo.CandidateSymbols.OfType().FirstOrDefault();
+
+ if (methodSymbol is null || methodSymbol.Name != useMauiCommunityToolkitMediaElementMethod)
+ {
+ return false;
+ }
+
+ // Verify this is an extension method
+ if (!methodSymbol.IsExtensionMethod)
+ {
+ return false;
+ }
+
+ // Check if it has at least 2 parameters (this MauiAppBuilder, bool isAndroidForegroundServiceEnabled)
+ if (methodSymbol.Parameters.Length < 2)
+ {
+ return false;
+ }
+
+ // Verify the second parameter is a boolean
+ var secondParam = methodSymbol.Parameters[0];
+ if (secondParam.Type.SpecialType != SpecialType.System_Boolean)
+ {
+ return false;
+ }
+
+ // Get the second argument (first argument is the builder via extension method call)
+ if (invocation.ArgumentList.Arguments.Count < 1)
+ {
+ return false;
+ }
+
+ // In an extension method call like builder.UseMauiCommunityToolkitMediaElement(true, ...)
+ // The first argument in ArgumentList corresponds to the second parameter in the method signature
+ var firstArg = invocation.ArgumentList.Arguments[0].Expression;
+ var constantValue = semanticModel.GetConstantValue(firstArg);
+
+ return constantValue.HasValue && constantValue.Value is true;
+ }
+
+ static string? GetInvocationMethodName(InvocationExpressionSyntax invocation)
+ {
+ if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ return memberAccess.Name.Identifier.Text;
+ }
+
+ if (invocation.Expression is IdentifierNameSyntax identifier)
+ {
+ return identifier.Identifier.Text;
+ }
+
+ return null;
+ }
+
+ static ConfigurationInfo? GetConfigurationInfo(GeneratorSyntaxContext context)
+ {
+ if (context.Node is not ClassDeclarationSyntax classDecl)
+ {
+ return null;
+ }
+
+ var className = classDecl.Identifier.Text;
+
+ if (className.Contains(mediaElementOptionsClassName))
+ {
+ // Look for the IsAndroidForegroundServiceEnabled property
+ var propertyDecl = classDecl.Members
+ .OfType()
+ .FirstOrDefault(p => p.Identifier.Text == isAndroidForegroundServiceEnabledProperty);
+
+ if (propertyDecl?.Initializer?.Value is null)
+ {
+ return null;
+ }
+
+ // Use semantic analysis to get the constant value
+ var semanticModel = context.SemanticModel;
+ var constantValue = semanticModel.GetConstantValue(propertyDecl.Initializer.Value);
+ var isEnabled = constantValue.HasValue && constantValue.Value is true;
+
+ var symbol = semanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
+
+ return new ConfigurationInfo
+ {
+ ClassName = className,
+ Namespace = symbol?.ContainingNamespace.ToDisplayString() ?? "",
+ IsForegroundServiceEnabled = isEnabled,
+ ClassType = "MediaElementOptions"
+ };
+ }
+
+ return null;
+ }
+
+ static void GeneratePermissions(SourceProductionContext context)
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("// ");
+ sb.AppendLine("// See: CommunityToolkit.Maui.MediaElement.SourceGenerators.AndroidMediaElementServiceConfigurationGenerator");
+ sb.AppendLine("// This file provides assembly-level Android permissions and service attributes");
+ sb.AppendLine("// when Android Foreground Service is enabled for MediaElement.");
+ sb.AppendLine("");
+ sb.AppendLine("#pragma warning disable");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("");
+ sb.AppendLine("#if ANDROID");
+ sb.AppendLine("using Android.App;");
+ sb.AppendLine("");
+ sb.AppendLine("[assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]");
+ sb.AppendLine("[assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]");
+ sb.AppendLine("[assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]");
+ sb.AppendLine("[assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]");
+ sb.AppendLine("");
+ sb.AppendLine("namespace CommunityToolkit.Maui.Android;");
+ sb.AppendLine("");
+ sb.AppendLine("/// ");
+ sb.AppendLine("/// Auto-generated configuration for Android MediaElement Service.");
+ sb.AppendLine("/// ");
+ sb.AppendLine("/// ");
+ sb.AppendLine("/// This file is auto-generated and provides assembly-level permissions");
+ sb.AppendLine("/// for MediaElement when is enabled");
+ sb.AppendLine("/// or when ");
+ sb.AppendLine("/// is called with isAndroidForegroundServiceEnabled set to true.");
+ sb.AppendLine("/// ");
+ sb.AppendLine("internal static class AndroidMediaElementServiceConfiguration");
+ sb.AppendLine("{");
+ sb.AppendLine("\t/// ");
+ sb.AppendLine("\t/// Indicates that Android MediaElement Service configuration is required.");
+ sb.AppendLine("\t/// ");
+ sb.AppendLine("\tpublic const bool IsRequired = true;");
+ sb.AppendLine("}");
+ sb.Append("#endif");
+
+ var source = sb.ToString();
+ context.AddSource("AndroidMediaElementServiceConfiguration.g.cs", SourceText.From(source, Encoding.UTF8));
+ }
+
+ sealed class ConfigurationInfo
+ {
+ public string? ClassName { get; set; }
+ public string? Namespace { get; set; }
+ public bool IsForegroundServiceEnabled { get; set; }
+ public string? ClassType { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.MediaElement/AppBuilderExtensions.shared.cs b/src/CommunityToolkit.Maui.MediaElement/AppBuilderExtensions.shared.cs
index 57a0d3c49f..3797f1829c 100644
--- a/src/CommunityToolkit.Maui.MediaElement/AppBuilderExtensions.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/AppBuilderExtensions.shared.cs
@@ -20,12 +20,17 @@ public static class AppBuilderExtensions
/// Initializes the .NET MAUI Community Toolkit MediaElement Library
///
/// generated by .
- ///
+ /// Enables Android Foreground Service for .
+ /// When true, the following permissions are automatically added to the Android Manifest by CommunityToolkit.Maui.MediaElement: FOREGROUND_SERVICE, MEDIA_CONTENT_CONTROL, POST_NOTIFICATION, FOREGROUND_SERVICE_MEDIA_PLAYBACK
+ ///
+ /// .
/// initialized for .
- public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(this MauiAppBuilder builder, Action? options = null)
+ public static MauiAppBuilder UseMauiCommunityToolkitMediaElement(this MauiAppBuilder builder, bool isAndroidForegroundServiceEnabled, Action? options = null)
{
// Update the default MediaElementOptions for MediaElement if Action is not null
- options?.Invoke(new MediaElementOptions(builder));
+ var mediaElementOptions = new MediaElementOptions(builder);
+ options?.Invoke(mediaElementOptions);
+ mediaElementOptions.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(isAndroidForegroundServiceEnabled);
// Perform Handler configuration
builder.ConfigureMauiHandlers(h =>
diff --git a/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj b/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj
index 53043c36f8..b0099b921d 100644
--- a/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj
+++ b/src/CommunityToolkit.Maui.MediaElement/CommunityToolkit.Maui.MediaElement.csproj
@@ -58,6 +58,7 @@
+
diff --git a/src/CommunityToolkit.Maui.MediaElement/Handlers/MediaElementHandler.android.cs b/src/CommunityToolkit.Maui.MediaElement/Handlers/MediaElementHandler.android.cs
index bd1e00e6e7..9a1ed2e361 100644
--- a/src/CommunityToolkit.Maui.MediaElement/Handlers/MediaElementHandler.android.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/Handlers/MediaElementHandler.android.cs
@@ -24,7 +24,7 @@ protected override MauiMediaElement CreatePlatformView()
VirtualView,
Dispatcher.GetForCurrentThread() ?? throw new InvalidOperationException($"{nameof(IDispatcher)} cannot be null"));
- var (_, playerView) = MediaManager.CreatePlatformView(VirtualView.AndroidViewType);
+ var (_, playerView) = MediaManager.CreatePlatformView(VirtualView.AndroidViewType, VirtualView.IsAndroidForegroundServiceEnabled);
return new(Context, playerView);
}
diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
index 535154f893..a279578740 100644
--- a/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/MediaElement.shared.cs
@@ -128,6 +128,11 @@ internal event EventHandler StopRequested
///
public AndroidViewType AndroidViewType { get; init; } = MediaElementOptions.DefaultAndroidViewType;
+ ///
+ /// Gets or sets a value indicating whether Android Foreground Service is enabled.
+ ///
+ public bool IsAndroidForegroundServiceEnabled { get; init; } = MediaElementOptions.IsAndroidForegroundServiceEnabled;
+
///
/// Gets or sets the ratio used to display the video content.
///
diff --git a/src/CommunityToolkit.Maui.MediaElement/MediaElementOptions.shared.cs b/src/CommunityToolkit.Maui.MediaElement/MediaElementOptions.shared.cs
index 5a1bb58ce0..2144222f67 100644
--- a/src/CommunityToolkit.Maui.MediaElement/MediaElementOptions.shared.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/MediaElementOptions.shared.cs
@@ -1,8 +1,4 @@
-using CommunityToolkit.Maui.Core.Views;
-using Microsoft.Maui;
-using Microsoft.Maui.Hosting;
-
-namespace CommunityToolkit.Maui.Core;
+namespace CommunityToolkit.Maui.Core;
///
/// Construction options for MediaElement, for example, to create an Android SurfaceView or TextureView
@@ -21,6 +17,11 @@ internal MediaElementOptions(in MauiAppBuilder builder) : this()
this.builder = builder;
}
+ ///
+ /// Set Android Foreground Service for MediaElement on construction
+ ///
+ internal static bool IsAndroidForegroundServiceEnabled { get; private set; } = true;
+
///
/// Set Android View type for MediaElement as SurfaceView or TextureView on construction
///
@@ -30,4 +31,13 @@ internal MediaElementOptions(in MauiAppBuilder builder) : this()
/// Set Android View type for MediaElement as SurfaceView or TextureView on construction
///
public void SetDefaultAndroidViewType(AndroidViewType androidViewType) => DefaultAndroidViewType = androidViewType;
+
+ ///
+ /// Set Android Foreground Service for MediaElement on construction
+ ///
+ /// When true, the following permissions are automatically added to the Android Manifest by CommunityToolkit.Maui.MediaElement: FOREGROUND_SERVICE, MEDIA_CONTENT_CONTROL, POST_NOTIFICATION, FOREGROUND_SERVICE_MEDIA_PLAYBACK
+ ///
+ public void SetIsAndroidForegroundServiceEnabled(bool isEnabled) => IsAndroidForegroundServiceEnabled = isEnabled;
+
+ internal void UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(bool isEnabled) => IsAndroidForegroundServiceEnabled |= isEnabled;
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.android.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.android.cs
index b9799a3874..196463a476 100644
--- a/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.android.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/Views/MauiMediaElement.android.cs
@@ -8,11 +8,6 @@
using AndroidX.Media3.UI;
using CommunityToolkit.Maui.Views;
-[assembly: UsesPermission(Android.Manifest.Permission.ForegroundServiceMediaPlayback)]
-[assembly: UsesPermission(Android.Manifest.Permission.ForegroundService)]
-[assembly: UsesPermission(Android.Manifest.Permission.MediaContentControl)]
-[assembly: UsesPermission(Android.Manifest.Permission.PostNotifications)]
-
namespace CommunityToolkit.Maui.Core.Views;
///
@@ -44,7 +39,7 @@ public MauiMediaElement(nint ptr, JniHandleOwnership jni) : base(Platform.AppCon
public MauiMediaElement(Context context, PlayerView playerView) : base(context)
{
this.playerView = playerView;
- this.playerView.SetBackgroundColor(Android.Graphics.Color.Black);
+ this.playerView.SetBackgroundColor(global::Android.Graphics.Color.Black);
playerView.FullscreenButtonClick += OnFullscreenButtonClick;
var layout = new RelativeLayout.LayoutParams(LayoutParams.WrapContent, LayoutParams.WrapContent);
layout.AddRule(LayoutRules.CenterInParent);
@@ -73,7 +68,7 @@ public override void OnDetachedFromWindow()
///
///
///
- protected override void OnVisibilityChanged(Android.Views.View changedView, [GeneratedEnum] ViewStates visibility)
+ protected override void OnVisibilityChanged(global::Android.Views.View changedView, [GeneratedEnum] ViewStates visibility)
{
base.OnVisibilityChanged(changedView, visibility);
if (isFullScreen && visibility is ViewStates.Visible)
@@ -220,7 +215,7 @@ public static Activity CurrentActivity
}
}
- public static Android.Views.Window CurrentWindow
+ public static global::Android.Views.Window CurrentWindow
{
get
{
diff --git a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs
index ffc22e6afc..1728eae11c 100644
--- a/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs
+++ b/src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs
@@ -1,8 +1,10 @@
-using System.Diagnostics;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Android.Content;
using Android.Views;
using Android.Widget;
+using Android.Util;
+using Android.App;
using AndroidX.Media3.Common;
using AndroidX.Media3.Common.Text;
using AndroidX.Media3.Common.Util;
@@ -27,6 +29,7 @@ public partial class MediaManager : Java.Lang.Object, IPlayerListener
static readonly HttpClient client = new();
readonly SemaphoreSlim seekToSemaphoreSlim = new(1, 1);
+ bool isAndroidForegroundServiceEnabled = false;
double? previousSpeed;
float volumeBeforeMute = 1;
@@ -62,9 +65,8 @@ public void OnPlaybackParametersChanged(PlaybackParameters? playbackParameters)
public void UpdateNotifications()
{
- if (connection?.Binder?.Service is null)
+ if (connection?.Binder?.Service is null || !isAndroidForegroundServiceEnabled)
{
- System.Diagnostics.Trace.TraceInformation("Notification Service not running.");
return;
}
@@ -130,11 +132,11 @@ or PlaybackState.StateSkippingToQueueItem
/// The platform native counterpart of .
/// Thrown when is or when the platform view could not be created.
[MemberNotNull(nameof(Player), nameof(PlayerView), nameof(session))]
- public (PlatformMediaElement platformView, PlayerView PlayerView) CreatePlatformView(AndroidViewType androidViewType)
+ public (PlatformMediaElement platformView, PlayerView PlayerView) CreatePlatformView(AndroidViewType androidViewType, bool isAndroidServiceEnabled)
{
Player = new ExoPlayerBuilder(MauiContext.Context).Build() ?? throw new InvalidOperationException("Player cannot be null");
Player.AddListener(this);
-
+ this.isAndroidForegroundServiceEnabled = isAndroidServiceEnabled;
if (androidViewType is AndroidViewType.SurfaceView)
{
PlayerView = new PlayerView(MauiContext.Context)
@@ -156,7 +158,7 @@ or PlaybackState.StateSkippingToQueueItem
var xmlResource = resources.GetXml(Microsoft.Maui.Resource.Layout.textureview);
xmlResource.Read();
- var attributes = Android.Util.Xml.AsAttributeSet(xmlResource)!;
+ var attributes = Xml.AsAttributeSet(xmlResource)!;
PlayerView = new PlayerView(MauiContext.Context, attributes)
{
@@ -353,7 +355,7 @@ protected virtual async partial ValueTask PlatformUpdateSource()
return;
}
- if (connection is null)
+ if (connection is null && isAndroidForegroundServiceEnabled)
{
StartService();
}
@@ -381,10 +383,16 @@ protected virtual async partial ValueTask PlatformUpdateSource()
hasSetSource = true;
}
- if (hasSetSource && Player.PlayerError is null)
+ if (hasSetSource)
{
- MediaElement.MediaOpened();
- UpdateNotifications();
+ if (Player.PlayerError is null)
+ {
+ MediaElement.MediaOpened();
+ }
+ if (isAndroidForegroundServiceEnabled)
+ {
+ UpdateNotifications();
+ }
}
}
@@ -631,23 +639,30 @@ static async ValueTask GetByteCountFromStream(Stream stream, CancellationT
}
}
- [MemberNotNull(nameof(connection))]
void StartService()
{
- var intent = new Intent(Android.App.Application.Context, typeof(MediaControlsService));
+ if (!isAndroidForegroundServiceEnabled)
+ {
+ return;
+ }
+ var intent = new Intent(global::Android.App.Application.Context, typeof(MediaControlsService));
connection = new BoundServiceConnection(this);
connection.MediaControlsServiceTaskRemoved += HandleMediaControlsServiceTaskRemoved;
- Android.App.Application.Context.StartForegroundService(intent);
- Android.App.Application.Context.ApplicationContext?.BindService(intent, connection, Bind.AutoCreate);
+ global::Android.App.Application.Context.StartForegroundService(intent);
+ global::Android.App.Application.Context.ApplicationContext?.BindService(intent, connection, Bind.AutoCreate);
}
void StopService(in BoundServiceConnection boundServiceConnection)
{
+ if (!isAndroidForegroundServiceEnabled)
+ {
+ return;
+ }
boundServiceConnection.MediaControlsServiceTaskRemoved -= HandleMediaControlsServiceTaskRemoved;
var serviceIntent = new Intent(Platform.AppContext, typeof(MediaControlsService));
- Android.App.Application.Context.StopService(serviceIntent);
+ global::Android.App.Application.Context.StopService(serviceIntent);
Platform.AppContext.UnbindService(boundServiceConnection);
}
diff --git a/src/CommunityToolkit.Maui.MediaElement/readme.txt b/src/CommunityToolkit.Maui.MediaElement/readme.txt
index 0dd0ef18e7..161c385592 100644
--- a/src/CommunityToolkit.Maui.MediaElement/readme.txt
+++ b/src/CommunityToolkit.Maui.MediaElement/readme.txt
@@ -15,7 +15,7 @@ public static class MauiProgram
builder
.UseMauiApp()
// Initialize the .NET MAUI Community Toolkit MediaElement by adding the below line of code
- .UseMauiCommunityToolkitMediaElement()
+ .UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false)
// After initializing the .NET MAUI Community Toolkit, optionally add additional fonts
.ConfigureFonts(fonts =>
{
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/AndroidMediaElementForegroundServiceConfigurationBenchmarks.cs b/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/AndroidMediaElementForegroundServiceConfigurationBenchmarks.cs
new file mode 100644
index 0000000000..38ddd50b24
--- /dev/null
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/AndroidMediaElementForegroundServiceConfigurationBenchmarks.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using CommunityToolkit.Maui.MediaElement.SourceGenerators.UnitTests.AndroidMediaElementServiceConfigurationGeneratorTests;
+using CommunityToolkit.Maui.SourceGenerators.UnitTests.AttachedBindablePropertyAttributeSourceGeneratorTests;
+
+namespace CommunityToolkit.Maui.SourceGenerators.Benchmarks;
+
+[MemoryDiagnoser]
+public class AndroidMediaElementForegroundServiceConfigurationBenchmarks
+{
+ static readonly AndroidMediaElementForegroundServiceConfigurationGenerator_OutputTests outputTests = new();
+ static readonly AndroidMediaElementForegroundServiceConfigurationGenerator_EdgeCaseTests edgeCaseTests = new();
+ static readonly AndroidMediaElementForegroundServiceConfigurationGenerator_IntegrationTests integrationTests = new();
+ static readonly AndroidMediaElementForegroundServiceConfigurationGenerator_UseMauiCommunityToolkitMediaElementTests useMauiCommunityToolkitMediaElementTests = new();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WhenPropertySetToTrue_GeneratesCorrectCode()
+ => outputTests.GenerateConfiguration_WhenPropertySetToTrue_GeneratesCorrectCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WhenNoMediaElementOptions_GeneratesNoCode()
+ => outputTests.GenerateConfiguration_WhenNoMediaElementOptions_GeneratesNoCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WhenMethodSetToTrue_GeneratesCorrectCode()
+ => outputTests.GenerateConfiguration_WhenMethodSetToTrue_GeneratesCorrectCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_RealWorldScenarioWithMauiProgram_GeneratesCorrectCode()
+ => integrationTests.GenerateConfiguration_RealWorldScenarioWithMauiProgram_GeneratesCorrectCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_PropertyAndMethodCombination_GeneratesCorrectly()
+ => integrationTests.GenerateConfiguration_PropertyAndMethodCombination_GeneratesCorrectly();
+
+ [Benchmark]
+ public Task GenerateConfiguration_ComplexAppWithMultipleFiles_GeneratesCorrectly()
+ => integrationTests.GenerateConfiguration_ComplexAppWithMultipleFiles_GeneratesCorrectly();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WithNestedNamespace_GeneratesCorrectCode()
+ => edgeCaseTests.GenerateConfiguration_WithNestedNamespace_GeneratesCorrectCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WithEmptySource_GeneratesNoCode()
+ => edgeCaseTests.GenerateConfiguration_WithEmptySource_GeneratesNoCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WithMultipleMediaElementOptionsClasses_GeneratesOnce()
+ => edgeCaseTests.GenerateConfiguration_WithMultipleMediaElementOptionsClasses_GeneratesOnce();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WhenCombinedWithPropertyAndMethodAndExtensionMethodTrue_GeneratesCorrectCode()
+ => useMauiCommunityToolkitMediaElementTests.GenerateConfiguration_WhenCombinedWithPropertyAndMethodAndExtensionMethodTrue_GeneratesCorrectCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WhenMultipleUseMauiCommunityToolkitMediaElementCallsWithTrue_GeneratesCorrectCode()
+ => useMauiCommunityToolkitMediaElementTests.GenerateConfiguration_WhenMultipleUseMauiCommunityToolkitMediaElementCallsWithTrue_GeneratesCorrectCode();
+
+ [Benchmark]
+ public Task GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithFalse_GeneratesNoCode()
+ => useMauiCommunityToolkitMediaElementTests.GenerateConfiguration_WhenUseMauiCommunityToolkitMediaElementCalledWithFalse_GeneratesNoCode();
+
+
+}
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/CommunityToolkit.Maui.SourceGenerators.Benchmarks.csproj b/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/CommunityToolkit.Maui.SourceGenerators.Benchmarks.csproj
index cdad3351eb..31f9b814b4 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/CommunityToolkit.Maui.SourceGenerators.Benchmarks.csproj
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/CommunityToolkit.Maui.SourceGenerators.Benchmarks.csproj
@@ -12,7 +12,7 @@
+
-
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/Program.cs b/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/Program.cs
index fc7bc1b6db..321d2ace7d 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/Program.cs
+++ b/src/CommunityToolkit.Maui.SourceGenerators.Benchmarks/Program.cs
@@ -10,5 +10,6 @@ public static void Main(string[] args)
var config = DefaultConfig.Instance;
BenchmarkRunner.Run(config, args);
BenchmarkRunner.Run(config, args);
+ BenchmarkRunner.Run(config, args);
}
}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/CommunityToolkit.Maui.SourceGenerators.UnitTests.csproj b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/CommunityToolkit.Maui.SourceGenerators.UnitTests.csproj
index a48d4f9997..9d808d927a 100644
--- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/CommunityToolkit.Maui.SourceGenerators.UnitTests.csproj
+++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/CommunityToolkit.Maui.SourceGenerators.UnitTests.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/src/CommunityToolkit.Maui.UnitTests/BaseTest.cs b/src/CommunityToolkit.Maui.UnitTests/BaseTest.cs
index 7ee48febe3..0af90eefe9 100644
--- a/src/CommunityToolkit.Maui.UnitTests/BaseTest.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/BaseTest.cs
@@ -90,7 +90,7 @@ protected virtual void Dispose(bool isDisposing)
// Restore default MediaElementOptions
var mediaElementOptions = new MediaElementOptions();
mediaElementOptions.SetDefaultAndroidViewType(AndroidViewType.SurfaceView);
-
+ mediaElementOptions.SetIsAndroidForegroundServiceEnabled(false);
isDisposed = true;
}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/AppBuilderExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/AppBuilderExtensionsTests.cs
index 27ef4de017..985f67ec29 100644
--- a/src/CommunityToolkit.Maui.UnitTests/Extensions/AppBuilderExtensionsTests.cs
+++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/AppBuilderExtensionsTests.cs
@@ -148,7 +148,7 @@ void HandleShouldUseStatusBarBehaviorOnAndroidModalPageOptionCompleted(object? s
public void UseMauiCommunityToolkitMediaElement_ShouldUseSurfaceViewByDefault()
{
var builder = MauiApp.CreateBuilder();
- builder.UseMauiCommunityToolkitMediaElement();
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false);
MediaElementOptions.DefaultAndroidViewType.Should().Be(AndroidViewType.SurfaceView);
}
@@ -159,7 +159,7 @@ public void UseMauiCommunityToolkitMediaElement_ShouldSetDefaultAndroidViewType(
MediaElementOptions.DefaultAndroidViewType.Should().Be(AndroidViewType.SurfaceView);
var builder = MauiApp.CreateBuilder();
- builder.UseMauiCommunityToolkitMediaElement(static options =>
+ builder.UseMauiCommunityToolkitMediaElement(isAndroidForegroundServiceEnabled: false, static options =>
{
options.SetDefaultAndroidViewType(AndroidViewType.TextureView);
});
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementOptionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementOptionsTests.cs
new file mode 100644
index 0000000000..615b82c9ed
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/MediaElement/MediaElementOptionsTests.cs
@@ -0,0 +1,115 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Views;
+using FluentAssertions;
+using Xunit;
+
+namespace CommunityToolkit.Maui.UnitTests.Views;
+
+public class MediaElementOptionsTests : BaseViewTest
+{
+ [Fact]
+ public void MediaElementOptions_HasExpectedDefaults()
+ {
+ // Assert defaults on the options class
+ MediaElementOptions.DefaultAndroidViewType.Should().Be(AndroidViewType.SurfaceView);
+ MediaElementOptions.IsAndroidForegroundServiceEnabled.Should().BeFalse();
+ }
+
+ [Fact]
+ public void SetDefaultAndroidForegroundServiceEnabled_Updates_StaticDefault()
+ {
+ var options = new MediaElementOptions();
+
+ options.SetIsAndroidForegroundServiceEnabled(false);
+ MediaElementOptions.IsAndroidForegroundServiceEnabled.Should().BeFalse();
+
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ MediaElementOptions.IsAndroidForegroundServiceEnabled.Should().BeTrue();
+ }
+
+ [Fact]
+ public void SetDefaultAndroidViewType_Updates_StaticDefault()
+ {
+ var options = new MediaElementOptions();
+
+ options.SetDefaultAndroidViewType(AndroidViewType.TextureView);
+ MediaElementOptions.DefaultAndroidViewType.Should().Be(AndroidViewType.TextureView);
+
+ options.SetDefaultAndroidViewType(AndroidViewType.SurfaceView);
+ MediaElementOptions.DefaultAndroidViewType.Should().Be(AndroidViewType.SurfaceView);
+ }
+
+ [Fact]
+ public void InitializesFromMediaElementOptionsDefaults()
+ {
+ var options = new MediaElementOptions();
+
+ // change defaults then create a new MediaElement and verify it picked them up
+ options.SetDefaultAndroidViewType(AndroidViewType.TextureView);
+ options.SetIsAndroidForegroundServiceEnabled(false);
+
+ var mediaElement = new MediaElement();
+
+ mediaElement.AndroidViewType.Should().Be(AndroidViewType.TextureView);
+ mediaElement.IsAndroidForegroundServiceEnabled.Should().BeFalse();
+ }
+
+ [Fact]
+ public void MediaElementOptions_UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameterTrue_ShouldBeTrue()
+ {
+ var options = new MediaElementOptions();
+
+ // change defaults then create a new MediaElement and verify it picked them up
+ options.SetDefaultAndroidViewType(AndroidViewType.TextureView);
+ options.SetIsAndroidForegroundServiceEnabled(false);
+ options.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(true);
+
+ var mediaElement = new MediaElement();
+
+ mediaElement.AndroidViewType.Should().Be(AndroidViewType.TextureView);
+ mediaElement.IsAndroidForegroundServiceEnabled.Should().BeTrue();
+ }
+
+ [Fact]
+ public void MediaElementOptions_UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameterFalse_ShouldBeTrue()
+ {
+ var options = new MediaElementOptions();
+
+ // change defaults then create a new MediaElement and verify it picked them up
+ options.SetDefaultAndroidViewType(AndroidViewType.TextureView);
+ options.SetIsAndroidForegroundServiceEnabled(true);
+ options.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(false);
+
+ var mediaElement = new MediaElement();
+
+ mediaElement.AndroidViewType.Should().Be(AndroidViewType.TextureView);
+ mediaElement.IsAndroidForegroundServiceEnabled.Should().BeTrue();
+ }
+
+ [Fact]
+ public void MediaElementOptions_UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameterFalse_ShouldBeFalse()
+ {
+ var options = new MediaElementOptions();
+
+ // change defaults then create a new MediaElement and verify it picked them up
+ options.SetDefaultAndroidViewType(AndroidViewType.TextureView);
+ options.SetIsAndroidForegroundServiceEnabled(false);
+ options.UpdateIsAndroidForegroundServiceEnabledWithUseMauiCommunityToolkitMediaElementParameter(false);
+
+ var mediaElement = new MediaElement();
+
+ mediaElement.AndroidViewType.Should().Be(AndroidViewType.TextureView);
+ mediaElement.IsAndroidForegroundServiceEnabled.Should().BeFalse();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ var options = new MediaElementOptions();
+
+ // restore original state
+ options.SetDefaultAndroidViewType(MediaElementOptions.DefaultAndroidViewType);
+ options.SetIsAndroidForegroundServiceEnabled(false);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.slnx b/src/CommunityToolkit.Maui.slnx
index b9de2dfcf7..cd535b86ae 100644
--- a/src/CommunityToolkit.Maui.slnx
+++ b/src/CommunityToolkit.Maui.slnx
@@ -6,6 +6,7 @@
+
@@ -15,14 +16,18 @@
+
+
-
+
+
+