Skip to content

Commit f1d65b0

Browse files
committed
feat: gate optimizations by system conditions and refresh the bloatware list
Introduce a reflection-driven condition system that evaluates every optimization and customize setting against a live system snapshot, so items that cannot work on the current machine (wrong Windows version, CPU/GPU brand, RAM, missing service or registry key) are flagged instead of applied blindly. Conditions: * Add Domain/Conditions: ICondition, ConditionBase, ConditionResult, ConditionState, discovery-time ConditionValidation and WindowsBuilds * Add built-in conditions: Windows 10 / Windows 11 / Windows 11 24H2+ build checks, Intel/AMD CPU brand, NVIDIA/AMD/Intel GPU brand, 16 GB RAM, Recall installed (24H2 + CoreAISpeech present), registry key exists and service exists * Add ConditionEvaluator service: caches ICondition instances, evaluates against the snapshot and fails open when detection is incomplete or the snapshot is unpopulated, so a failed check never hides a usable item * Declare conditions via [Optimization(Condition = ...)] and [CustomizeSetting(Condition = ...)]; both registries validate the type at discovery time (fail fast at startup) * BaseOptimization and BaseCustomizeSetting now extend ObservableObject and expose ConditionType, ConditionResult, IsConditionBlocked and a session-only IsConditionHidden ("show anyway") state; applied or hidden items are never blocked SystemInfo: * Add CpuVendor enum (Unknown/Intel/AMD) detected from the WMI manufacturer string, replacing the old free-text vendor * Raise SnapshotRefreshed after every refresh and add EnsureSnapshotAsync() so conditions always evaluate against a populated snapshot UI: * Add UiThread helper that marshals to the dispatcher but degrades to inline execution when no WPF Application exists (unit tests / headless hosts) * Optimization page evaluates conditions on load and re-evaluates on snapshot refresh and language change; unsupported items are hidden unless the user chooses to show them (new HideCondition command) * Customize page partitions unsupported settings into their own section with a header and count * CustomizeItemViewModel: move registry I/O (state, options, current value) off the UI thread, publish options before the selection value, and refresh from a single RefreshFromRegistryAsync reusing a new UiThread helper * Wire SystemInfoService into category page factories and replace GpuOptimizerPage with AIOptimizerPage Customize dropdowns: * Options now appends a memory-only "Custom" (or "Not set" via MissingValueSentinel) fallback when the live registry value is outside the declared options; it stays in sync with registry changes, is never persisted, and applying it is a safe no-op (no duplicate values on partial multi-binding matches) Optimizations: * Add new AI category: Disable Windows Recall (24H2+, gated on Recall being installed) and Disable Windows AI features * SecurityAndPrivacy: split DisableAdvertisingAndSuggestions into focused items — new DisableNewsAndInterests, HideMeetNowButton (Win10-only via Windows10Condition), DisableFindMyDevice and DisableDeliveryOptimization; drop the Dosvc/SysMain service and StorageSense writes; add Speech OneCore and Start_TrackProgs privacy keys; make DisableCortana Win10-only and gate DisableCopilot on Windows 11 * UserExperience: add Hide Recommended in Start (Win11) and Disable M365 ads in Settings * Performance: rename SvcHostSplit to ConsolidateServiceHosts with a new RAM-based description Bloatware list (Shared.cs): * SafeApps: drop apps no longer in Win11 images (BingSearch, GetHelp, FeedbackHub, QuickAssist, YourPhone, OutlookForWindows, LinkedIn, Paint3D, ZuneMusic, Sway, DrawboardPDF, XboxApp, StartExperiencesApp, WebExperience, Phone Link) and move them to CautionApps where still present; add Windows.AIHub and ~30 commonly pre-installed third-party apps (Netflix, TikTok, Instagram, LinkedIn, Candy Crush, Hulu, Sling, etc.) * CautionApps: add OEM support suites (HP, Dell, Lenovo, LG, CyberLink) and default handlers (OneDrive, Whiteboard, M365, Media Player, Widgets, Phone Link, Quick Assist, GetHelp) i18n: * Add keys for condition messages, new optimizations, renamed items and custom dropdown labels across all 14 locale files Tests: * Add ConditionTests, CustomizeItemViewModelTests and StubOptimization; extend BaseCustomizeSetting tests for the new Options fallback behaviour; update suites for the CpuVendor enum and new constructor signatures
1 parent ef56395 commit f1d65b0

95 files changed

Lines changed: 4253 additions & 2131 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

optimizerDuck.Test/Domain/Conditions/ConditionTests.cs

Lines changed: 482 additions & 0 deletions
Large diffs are not rendered by default.

optimizerDuck.Test/Domain/Customize/BaseCustomizeSettingTests.cs

Lines changed: 275 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using optimizerDuck.Domain.Abstractions;
77
using optimizerDuck.Domain.Customize.Models;
88
using optimizerDuck.Domain.Optimizations.Models.Services;
9+
using optimizerDuck.Services.Configuration;
910
using optimizerDuck.Services.Optimization.Providers;
1011
using Xunit;
1112

@@ -32,22 +33,18 @@ private class TestCustomizeSetting : BaseCustomizeSetting
3233

3334
#region GetStateWithRetryAsync helpers
3435

35-
/// <summary>Always returns the same stable state.</summary>
3636
private sealed class StableStateSetting : BaseCustomizeSetting
3737
{
3838
public bool State { get; set; }
3939

4040
public override Task<bool> GetStateAsync() => Task.Run(() => State);
4141
}
4242

43-
/// <summary>Uses base GetStateAsync (no toggles → returns false).</summary>
4443
private sealed class EmptyTogglesSetting : BaseCustomizeSetting
4544
{
46-
// No RegistryToggles override → empty
47-
// No GetStateAsync override → uses base (empty toggles → false)
45+
// No overrides: base GetStateAsync reads empty toggles → returns false
4846
}
4947

50-
/// <summary>Alternates on every call — never stabilises.</summary>
5148
private sealed class OscillatingStateSetting : BaseCustomizeSetting
5249
{
5350
private int _callCount;
@@ -56,7 +53,6 @@ public override Task<bool> GetStateAsync() =>
5653
Task.Run(() => Interlocked.Increment(ref _callCount) % 2 == 1);
5754
}
5855

59-
/// <summary>Returns stable after a configurable delay.</summary>
6056
private sealed class DelayedStateSetting : BaseCustomizeSetting
6157
{
6258
public bool State { get; set; } = true;
@@ -72,7 +68,6 @@ public override async Task<bool> GetStateAsync()
7268
}
7369
}
7470

75-
/// <summary>Setting with multiple toggles to test path dedup.</summary>
7671
private sealed class MultiToggleSetting : BaseCustomizeSetting
7772
{
7873
protected override IEnumerable<RegistryToggle> RegistryToggles =>
@@ -311,29 +306,25 @@ private static CustomizeRefreshScope GetRefreshScope(BaseCustomizeSetting s) =>
311306
private static bool GetNeedsPostAction(BaseCustomizeSetting s) =>
312307
(bool)NeedsPostActionProperty.GetValue(s)!;
313308

314-
/// <summary>Plain setting with no overrides - must be opt-in for post-action.</summary>
315309
private sealed class DefaultScopeSetting : BaseCustomizeSetting
316310
{
317311
public override Task<bool> GetStateAsync() => Task.FromResult(false);
318312
}
319313

320-
/// <summary>Setting that opts into the default explorer-level refresh.</summary>
321314
private sealed class DefaultExplorerScopeSetting : BaseCustomizeSetting
322315
{
323316
protected override CustomizeRefreshScope RefreshScope => CustomizeRefreshScope.Default;
324317

325318
public override Task<bool> GetStateAsync() => Task.FromResult(false);
326319
}
327320

328-
/// <summary>Setting that opts into the desktop-icon refresh.</summary>
329321
private sealed class DesktopIconsScopeSetting : BaseCustomizeSetting
330322
{
331323
protected override CustomizeRefreshScope RefreshScope => CustomizeRefreshScope.DesktopIcons;
332324

333325
public override Task<bool> GetStateAsync() => Task.FromResult(false);
334326
}
335327

336-
/// <summary>Setting that opts into the global HideIcons cache refresh.</summary>
337328
private sealed class HideDesktopIconsScopeSetting : BaseCustomizeSetting
338329
{
339330
protected override CustomizeRefreshScope RefreshScope =>
@@ -342,7 +333,6 @@ private sealed class HideDesktopIconsScopeSetting : BaseCustomizeSetting
342333
public override Task<bool> GetStateAsync() => Task.FromResult(false);
343334
}
344335

345-
/// <summary>Setting that opts into the taskbar refresh.</summary>
346336
private sealed class TaskbarScopeSetting : BaseCustomizeSetting
347337
{
348338
protected override CustomizeRefreshScope RefreshScope =>
@@ -351,7 +341,6 @@ private sealed class TaskbarScopeSetting : BaseCustomizeSetting
351341
public override Task<bool> GetStateAsync() => Task.FromResult(false);
352342
}
353343

354-
/// <summary>Setting that opts into a multi-flag custom scope.</summary>
355344
private sealed class MultiScopeSetting : BaseCustomizeSetting
356345
{
357346
protected override CustomizeRefreshScope RefreshScope =>
@@ -714,6 +703,279 @@ public async Task Dropdown_AutoWrite_WritesCorrectValue()
714703
CleanupTestKeys();
715704
}
716705

706+
#region Options (Custom option) tests
707+
708+
[Fact]
709+
public void Options_ValueInScope_ReturnsDeclaredOptionsOnly()
710+
{
711+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
712+
713+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 1));
714+
715+
var effective = setting.Options;
716+
717+
Assert.NotNull(effective);
718+
Assert.Equal(2, effective!.Count);
719+
Assert.DoesNotContain(effective, o => Equals(o.Value, 99));
720+
721+
CleanupTestKeys();
722+
}
723+
724+
[Fact]
725+
public void Options_ValueOutOfScope_AppendsCustomOptionWithRawValue()
726+
{
727+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
728+
729+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 99));
730+
731+
var effective = setting.Options;
732+
733+
Assert.NotNull(effective);
734+
Assert.Equal(3, effective!.Count);
735+
Assert.Equal(99, effective[2].Value);
736+
Assert.Equal(Loc.Instance[BaseCustomizeSetting.CustomOptionTranslationKey], effective[2].DisplayName);
737+
Assert.Null(effective[2].Bindings);
738+
Assert.Equal(99, setting.CurrentValue);
739+
740+
CleanupTestKeys();
741+
}
742+
743+
[Fact]
744+
public void Options_ValueReturnsInScope_RemovesCustomOption()
745+
{
746+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
747+
748+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 99));
749+
Assert.Equal(3, setting.Options!.Count);
750+
751+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 2));
752+
753+
var effective = setting.Options;
754+
755+
Assert.NotNull(effective);
756+
Assert.Equal(2, effective!.Count);
757+
Assert.DoesNotContain(effective, o => Equals(o.Value, 99));
758+
Assert.Equal(2, setting.CurrentValue);
759+
760+
CleanupTestKeys();
761+
}
762+
763+
[Fact]
764+
public void Options_ValueChangesOutOfScope_UpdatesCustomOptionValue()
765+
{
766+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
767+
768+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 99));
769+
Assert.Equal(99, setting.Options![2].Value);
770+
771+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 123));
772+
773+
var effective = setting.Options;
774+
775+
Assert.NotNull(effective);
776+
Assert.Equal(3, effective!.Count);
777+
Assert.Equal(123, effective[2].Value);
778+
Assert.Equal(123, setting.CurrentValue);
779+
780+
CleanupTestKeys();
781+
}
782+
783+
[Fact]
784+
public void Options_MissingValue_AddsCustomFallbackWithSentinel()
785+
{
786+
// Even when the registry value is missing entirely, the ComboBox must never
787+
// render empty: a synthetic fallback (with a stable non-null sentinel value so
788+
// WPF can select it) is appended, labeled distinctly from "Custom".
789+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
790+
791+
RegistryService.DeleteValue(new RegistryItem(TestKeyPath, "DropdownTest"));
792+
793+
var effective = setting.Options;
794+
795+
Assert.NotNull(effective);
796+
Assert.Equal(3, effective!.Count);
797+
Assert.Equal(
798+
Loc.Instance[BaseCustomizeSetting.CustomOptionNotSetTranslationKey],
799+
effective[2].DisplayName
800+
);
801+
Assert.Same(BaseCustomizeSetting.MissingValueSentinel, effective[2].Value);
802+
Assert.Null(effective[2].Bindings);
803+
Assert.Same(BaseCustomizeSetting.MissingValueSentinel, setting.CurrentValue);
804+
805+
CleanupTestKeys();
806+
}
807+
808+
[Fact]
809+
public async Task ApplyAsync_MissingValueSentinel_IsSafeNoOp()
810+
{
811+
// The sentinel must never leak into the registry: applying it resolves against
812+
// declared Options (where it never exists), so nothing is written and it throws.
813+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
814+
815+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 1));
816+
await setting.ApplyAsync(BaseCustomizeSetting.MissingValueSentinel);
817+
818+
var value = RegistryService.Read<int>(new RegistryItem(TestKeyPath, "DropdownTest"));
819+
Assert.Equal(1, value);
820+
821+
CleanupTestKeys();
822+
}
823+
824+
[Fact]
825+
public void Options_MissingValueThenDeclaredValue_RemovesCustomFallback()
826+
{
827+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
828+
829+
RegistryService.DeleteValue(new RegistryItem(TestKeyPath, "DropdownTest"));
830+
Assert.Equal(3, setting.Options!.Count);
831+
832+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 2));
833+
834+
var effective = setting.Options;
835+
836+
Assert.NotNull(effective);
837+
Assert.Equal(2, effective!.Count);
838+
Assert.Equal(2, setting.CurrentValue);
839+
840+
CleanupTestKeys();
841+
}
842+
843+
[Fact]
844+
public void Options_NonDropdownSetting_ReturnsDeclaredOptions()
845+
{
846+
var setting = new TestCustomizeSetting { OwnerType = typeof(TestCustomizeSetting) };
847+
848+
Assert.Null(setting.Options);
849+
}
850+
851+
[Fact]
852+
public void Options_PartialMultiBindingMatch_DoesNotDuplicateDeclaredValue()
853+
{
854+
var setting = new TestMultiBindingDropdown { OwnerType = typeof(TestMultiBindingDropdown) };
855+
856+
// Key A = 1 (matches "On" primary) but Key B = 0 → no full match; the raw primary
857+
// value (1) already equals the declared "On" value, so no "Custom" duplicate.
858+
RegistryService.Write(new RegistryItem(TestKeyPath, "Key1", 1));
859+
RegistryService.Write(new RegistryItem(@"HKCU\Software\TestOptimizerDuckMultiKey", "Key2", 0));
860+
861+
var effective = setting.Options;
862+
863+
Assert.NotNull(effective);
864+
Assert.Equal(2, effective!.Count);
865+
Assert.DoesNotContain(
866+
effective,
867+
o => o.DisplayName == Loc.Instance[BaseCustomizeSetting.CustomOptionTranslationKey]
868+
);
869+
Assert.Equal(1, setting.CurrentValue);
870+
871+
CleanupTestKeys();
872+
try
873+
{
874+
using var hkcu = Microsoft.Win32.Registry.CurrentUser;
875+
hkcu.DeleteSubKeyTree(@"Software\TestOptimizerDuckMultiKey", false);
876+
}
877+
catch { }
878+
}
879+
880+
[Fact]
881+
public void Options_CustomOptionIsMemoryOnly_NeverPersisted()
882+
{
883+
// The synthetic option is derived from live registry state and is never stored,
884+
// so re-reading it must always reflect the current registry value.
885+
var setting = new TestDropdownSetting { OwnerType = typeof(TestDropdownSetting) };
886+
887+
RegistryService.Write(new RegistryItem(TestKeyPath, "DropdownTest", 99));
888+
var first = setting.Options!;
889+
var second = setting.Options!;
890+
891+
Assert.Equal(99, first[2].Value);
892+
Assert.Equal(99, second[2].Value);
893+
Assert.NotSame(first, second);
894+
895+
CleanupTestKeys();
896+
}
897+
898+
#endregion
899+
900+
private sealed class TestNotSetOptionDropdown : BaseCustomizeSetting
901+
{
902+
private const string RegPath = @"HKCU\Software\TestOptimizerDuckCustomize";
903+
private const string RegName = "NotSetOptionTest";
904+
905+
public override CustomizeControlType ControlType => CustomizeControlType.Dropdown;
906+
907+
protected override IReadOnlyList<SettingOption>? GetOptions() =>
908+
[
909+
new SettingOption("Off", 0, [new RegistryBinding(RegPath, RegName, 0)]),
910+
new SettingOption("On", 1, [new RegistryBinding(RegPath, RegName, 1)]),
911+
new SettingOption("NotSet", "notset", [new RegistryBinding(RegPath, RegName, null)]),
912+
];
913+
}
914+
915+
[Fact]
916+
public void Options_DeclaredNullBindingOption_MissingValue_MatchesDeclaredNotSet()
917+
{
918+
var setting = new TestNotSetOptionDropdown { OwnerType = typeof(TestNotSetOptionDropdown) };
919+
920+
RegistryService.DeleteValue(new RegistryItem(TestKeyPath, "NotSetOptionTest"));
921+
922+
// The declared null-binding option matches the missing state (null == null),
923+
// so no synthetic fallback is appended; CurrentValue is the declared value.
924+
var effective = setting.Options;
925+
926+
Assert.NotNull(effective);
927+
Assert.Equal(3, effective!.Count);
928+
Assert.Equal("notset", setting.CurrentValue);
929+
Assert.DoesNotContain(
930+
effective,
931+
o => ReferenceEquals(o.Value, BaseCustomizeSetting.MissingValueSentinel)
932+
);
933+
934+
CleanupTestKeys();
935+
}
936+
937+
[Fact]
938+
public async Task ApplyAsync_DeclaredNullBindingOption_DeletesValueLikeNotSet()
939+
{
940+
var setting = new TestNotSetOptionDropdown { OwnerType = typeof(TestNotSetOptionDropdown) };
941+
942+
RegistryService.Write(new RegistryItem(TestKeyPath, "NotSetOptionTest", 1));
943+
Assert.Equal(1, setting.CurrentValue);
944+
945+
// Selecting the declared "NotSet" option deletes the registry value (binding null).
946+
await setting.ApplyAsync("notset");
947+
948+
var value = RegistryService.Read<object>(
949+
new RegistryItem(TestKeyPath, "NotSetOptionTest")
950+
);
951+
Assert.Null(value);
952+
Assert.Equal("notset", setting.CurrentValue);
953+
954+
CleanupTestKeys();
955+
}
956+
957+
[Fact]
958+
public async Task ApplyAsync_NullBindingOption_RoundTripsToDeclaredValue()
959+
{
960+
var setting = new TestNotSetOptionDropdown { OwnerType = typeof(TestNotSetOptionDropdown) };
961+
962+
// Not set → apply declared "On" → value written → apply "NotSet" → deleted again.
963+
RegistryService.DeleteValue(new RegistryItem(TestKeyPath, "NotSetOptionTest"));
964+
Assert.Equal("notset", setting.CurrentValue);
965+
966+
await setting.ApplyAsync(1);
967+
Assert.Equal(1, setting.CurrentValue);
968+
969+
await setting.ApplyAsync("notset");
970+
var value = RegistryService.Read<object>(
971+
new RegistryItem(TestKeyPath, "NotSetOptionTest")
972+
);
973+
Assert.Null(value);
974+
Assert.Equal("notset", setting.CurrentValue);
975+
976+
CleanupTestKeys();
977+
}
978+
717979
private class TestMultiBindingDropdown : BaseCustomizeSetting
718980
{
719981
private const string RegPathA = @"HKCU\Software\TestOptimizerDuckCustomize";

0 commit comments

Comments
 (0)