diff --git a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs index d24adaa70c..13c4481fdc 100644 --- a/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs +++ b/SoundSwitch.Common/Framework/Audio/Icon/AudioDeviceIconExtractor.cs @@ -1,77 +1,97 @@ -/******************************************************************** - * Copyright (C) 2015-2017 Antoine Aflalo - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - ********************************************************************/ - -#nullable enable -using System; - -using NAudio.CoreAudioApi; - -using Serilog; - -using SoundSwitch.Common.Framework.Icon; -using SoundSwitch.Common.Properties; - -namespace SoundSwitch.Common.Framework.Audio.Icon -{ - /// - /// Extracts icons for audio devices with DataFlow-specific fallback defaults. - /// Delegates caching and GDI reference counting to . - /// - public class AudioDeviceIconExtractor - { - private static readonly IconHandle DefaultSpeakersHandle = IconExtractor.CreatePermanent(Resources.defaultSpeakers); - private static readonly IconHandle DefaultMicrophoneHandle = IconExtractor.CreatePermanent(Resources.defaultMicrophone); - - /// - /// Extract an icon from an audio device icon path, falling back to a DataFlow-specific - /// default icon on failure. - /// - /// Audio device icon path (a .ico file or dllPath,iconIndex). - /// Data flow of the device, used to select the fallback icon. - /// When , extract a 32×32 icon; otherwise 16×16. - /// - /// An the caller must dispose when done. - /// - public static IconHandle ExtractIconFromPath(string path, DataFlow dataFlow, bool largeIcon) - { - try - { - return IconExtractor.ExtractFromPath(path, largeIcon); - } - catch (Exception e) - { - Log.Warning(e, "Can't extract icon from {path}", path); - return dataFlow switch - { - DataFlow.Capture => DefaultMicrophoneHandle.Acquire(), - DataFlow.Render => DefaultSpeakersHandle.Acquire(), - _ => throw new ArgumentOutOfRangeException() - }; - } - } - - /// - /// Extract the icon out of an . - /// - /// - /// - /// - /// An the caller must dispose when done. - /// - public static IconHandle ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIcon) - { - return ExtractIconFromPath(audioDevice.IconPath, audioDevice.DataFlow, largeIcon); - } - } -} +/******************************************************************** + * Copyright (C) 2015-2017 Antoine Aflalo + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + ********************************************************************/ + +#nullable enable +using System; + +using NAudio.CoreAudioApi; + +using Serilog; + +using SoundSwitch.Common.Framework.Icon; +using SoundSwitch.Common.Properties; + +namespace SoundSwitch.Common.Framework.Audio.Icon +{ + /// + /// Extracts icons for audio devices with DataFlow-specific fallback defaults. + /// Delegates caching and GDI reference counting to . + /// + public class AudioDeviceIconExtractor + { + private static readonly IconHandle DefaultSpeakersHandle = CreatePermanentDefaultIcon(() => Resources.defaultSpeakers, () => System.Drawing.SystemIcons.Application, nameof(Resources.defaultSpeakers)); + private static readonly IconHandle DefaultMicrophoneHandle = CreatePermanentDefaultIcon(() => Resources.defaultMicrophone, () => System.Drawing.SystemIcons.Information, nameof(Resources.defaultMicrophone)); + + /// + /// Creates a permanent icon handle from a bundled icon resource with a fallback option. + /// + /// Factory function that provides the primary bundled icon. + /// Factory function that provides a fallback system icon if the bundled icon fails to load. + /// The name of the resource being loaded, used for logging purposes. + /// A permanent that does not require disposal. + private static IconHandle CreatePermanentDefaultIcon(Func bundledIconFactory, Func fallbackIconFactory, string resourceName) + { + try + { + return IconExtractor.CreatePermanent(bundledIconFactory()); + } + catch (Exception e) + { + Log.Warning(e, "Can't load bundled fallback icon {resourceName}, using system icon fallback", resourceName); + return IconExtractor.CreatePermanent((System.Drawing.Icon)fallbackIconFactory().Clone()); + } + } + + /// + /// Extract an icon from an audio device icon path, falling back to a DataFlow-specific + /// default icon on failure. + /// + /// Audio device icon path (a .ico file or dllPath,iconIndex). + /// Data flow of the device, used to select the fallback icon. + /// When , extract a 32×32 icon; otherwise 16×16. + /// + /// An the caller must dispose when done. + /// + public static IconHandle ExtractIconFromPath(string path, DataFlow dataFlow, bool largeIcon) + { + try + { + return IconExtractor.ExtractFromPath(path, largeIcon); + } + catch (Exception e) + { + Log.Warning(e, "Can't extract icon from {path}", path); + return dataFlow switch + { + DataFlow.Capture => DefaultMicrophoneHandle.Acquire(), + DataFlow.Render => DefaultSpeakersHandle.Acquire(), + _ => throw new ArgumentOutOfRangeException() + }; + } + } + + /// + /// Extract the icon out of an . + /// + /// + /// + /// + /// An the caller must dispose when done. + /// + public static IconHandle ExtractIconFromAudioDevice(MMDevice audioDevice, bool largeIcon) + { + return ExtractIconFromPath(audioDevice.IconPath, audioDevice.DataFlow, largeIcon); + } + } +} diff --git a/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs new file mode 100644 index 0000000000..b74ac978ad --- /dev/null +++ b/SoundSwitch.Tests/AudioDeviceIconExtractorTests.cs @@ -0,0 +1,27 @@ +using FluentAssertions; + +using NAudio.CoreAudioApi; + +using NUnit.Framework; + +using SoundSwitch.Common.Framework.Audio.Icon; + +namespace SoundSwitch.Tests; + +/// +/// Tests for the class, verifying icon extraction +/// behavior for audio devices including fallback handling for invalid icon paths. +/// +[TestFixture] +public class AudioDeviceIconExtractorTests +{ + [TestCase(DataFlow.Render)] + [TestCase(DataFlow.Capture)] + public void ExtractIconFromPath_WhenPathIsInvalid_ReturnsFallbackIcon(DataFlow dataFlow) + { + using var iconHandle = AudioDeviceIconExtractor.ExtractIconFromPath("invalid-icon-path", dataFlow, false); + + iconHandle.Should().NotBeNull(); + iconHandle.Icon.Should().NotBeNull(); + } +}