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 @@ + + - + + +