noid-privacy/Modules/Privacy/Public/Restore-Bloatware.ps1

293 lines
15 KiB
PowerShell

function Restore-Bloatware {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$BackupPath
)
try {
# List of apps that CANNOT be restored via winget (no package available in msstore catalog)
# These will be removed during Apply, but user must reinstall manually from Microsoft Store
# Verified 2025-12-08: These specific apps have no winget msstore package
# Note: Xbox.TCUI, XboxSpeechToTextOverlay, and Solitaire are intentionally NOT removed during Apply
# (they cannot be reinstalled via winget once removed - must use Store manually)
$nonRestorableApps = @(
[PSCustomObject]@{ AppName = "Microsoft.Getstarted"; DisplayName = "Tips" }
# Microsoft.MicrosoftSolitaireCollection - NOW SKIPPED during Apply (not listed here)
[PSCustomObject]@{ AppName = "Microsoft.People"; DisplayName = "People" }
)
Write-Log -Level INFO -Message "Checking for removed apps to restore via winget..." -Module "Privacy"
$restoreInfoPath = Join-Path $BackupPath "REMOVED_APPS_WINGET.json"
if (-not (Test-Path $restoreInfoPath)) {
Write-Log -Level INFO -Message "No removed apps restore info found at $restoreInfoPath - skipping app restore" -Module "Privacy"
return [PSCustomObject]@{
Success = $true
NonRestorableApps = @()
}
}
$restoreInfo = Get-Content $restoreInfoPath -Raw | ConvertFrom-Json
$apps = @($restoreInfo.Apps)
if (-not $apps -or $apps.Count -eq 0) {
Write-Log -Level INFO -Message "Removed apps list is empty - nothing to restore" -Module "Privacy"
return [PSCustomObject]@{
Success = $true
NonRestorableApps = @()
}
}
# Filter out non-restorable apps before attempting restore
$nonRestorableAppNames = $nonRestorableApps.AppName
$skippedNonRestorableApps = @($apps | Where-Object { $nonRestorableAppNames -contains $_.AppName })
$appsToRestore = @($apps | Where-Object { $nonRestorableAppNames -notcontains $_.AppName })
$appsWithWinget = $appsToRestore | Where-Object { $_.WingetId -and $_.WingetId -ne "" }
$appsWithoutWinget = $appsToRestore | Where-Object { -not $_.WingetId -or $_.WingetId -eq "" }
if (-not $appsWithWinget -or $appsWithWinget.Count -eq 0) {
Write-Log -Level INFO -Message "No apps with valid WingetId to restore - skipping winget restore" -Module "Privacy"
# Map skipped apps to display names
$skippedDisplayNames = @()
foreach ($skipped in $skippedNonRestorableApps) {
$displayName = ($nonRestorableApps | Where-Object { $_.AppName -eq $skipped.AppName }).DisplayName
if ($displayName) { $skippedDisplayNames += $displayName }
}
return [PSCustomObject]@{
Success = $true
NonRestorableApps = $skippedDisplayNames
}
}
$wingetCmd = Get-Command winget -ErrorAction SilentlyContinue
if (-not $wingetCmd) {
Write-Log -Level WARNING -Message "winget not found - cannot automatically restore removed apps" -Module "Privacy"
# Map skipped apps to display names
$skippedDisplayNames = @()
foreach ($skipped in $skippedNonRestorableApps) {
$displayName = ($nonRestorableApps | Where-Object { $_.AppName -eq $skipped.AppName }).DisplayName
if ($displayName) { $skippedDisplayNames += $displayName }
}
return [PSCustomObject]@{
Success = $true
NonRestorableApps = $skippedDisplayNames
}
}
# Force reset winget sources to ensure msstore is available
try {
Write-Log -Level INFO -Message "Resetting winget sources to ensure msstore availability..." -Module "Privacy"
Start-Process -FilePath "winget" -ArgumentList @("source", "reset", "--force") -Wait -NoNewWindow -ErrorAction SilentlyContinue | Out-Null
} catch { $null = $null } # Ignore winget reset errors
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " RESTORING REMOVED APPS VIA WINGET" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Apps scheduled for reinstall: $($appsWithWinget.Count)" -ForegroundColor Green
Write-Host ""
# Filter out hidden Xbox system components from "manual reinstall" list
# These are handled automatically by Gaming Services / Xbox Game Bar install where possible
$hiddenXboxApps = @(
"Microsoft.Xbox.TCUI",
"Microsoft.XboxSpeechToTextOverlay"
)
$appsManualReinstall = @($appsWithoutWinget | Where-Object { $hiddenXboxApps -notcontains $_.AppName })
$appsHandledByGamingServices = @($appsWithoutWinget | Where-Object { $hiddenXboxApps -contains $_.AppName })
if ($appsManualReinstall -and $appsManualReinstall.Count -gt 0) {
Write-Host " NOTE: $($appsManualReinstall.Count) app(s) cannot be auto-restored (system components)" -ForegroundColor Yellow
Write-Log -Level INFO -Message ("Apps without WingetId (manual reinstall required): " + ($appsManualReinstall.AppName -join ", ")) -Module "Privacy"
}
if ($appsHandledByGamingServices.Count -gt 0) {
Write-Log -Level INFO -Message "Hidden Xbox components will be restored via Gaming Services: $($appsHandledByGamingServices.AppName -join ", ")" -Module "Privacy"
}
$successCount = 0
$failCount = 0
# SPECIAL HANDLING: Xbox/Gaming apps require Gaming Services to be installed first
# This prevents user prompts when opening Gaming App for the first time
$gamingApps = @($appsWithWinget | Where-Object { $_.AppName -match "Xbox|Gaming" })
if ($gamingApps.Count -gt 0) {
# CRITICAL: Remove Deprovisioned registry keys for Xbox framework components
# These keys block Windows from reinstalling these apps even via Gaming Services
Write-Host " [>] Removing Xbox deprovisioned blocks (if any)..." -ForegroundColor Cyan
$deprovisionedPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\Deprovisioned"
$xboxDeprovisionedApps = @(
"Microsoft.Xbox.TCUI_8wekyb3d8bbwe",
"Microsoft.XboxSpeechToTextOverlay_8wekyb3d8bbwe",
"Microsoft.XboxGamingOverlay_8wekyb3d8bbwe",
"Microsoft.XboxIdentityProvider_8wekyb3d8bbwe",
"Microsoft.GamingApp_8wekyb3d8bbwe"
)
foreach ($appKey in $xboxDeprovisionedApps) {
$keyPath = Join-Path $deprovisionedPath $appKey
if (Test-Path $keyPath) {
try {
Remove-Item -Path $keyPath -Force -ErrorAction Stop
Write-Log -Level SUCCESS -Message "Removed deprovisioned block: $appKey" -Module "Privacy"
Write-Host " [OK] Unblocked: $appKey" -ForegroundColor Green
}
catch {
Write-Log -Level WARNING -Message "Failed to remove deprovisioned key for $appKey : $_" -Module "Privacy"
}
}
}
Write-Host " [>] Detected Xbox/Gaming apps - installing Gaming Services first..." -ForegroundColor Cyan
Write-Log -Level INFO -Message "Installing Gaming Services (framework) to prevent user prompts" -Module "Privacy"
try {
# Gaming Services Store ID: 9MWPM2CQNLHN
$proc = Start-Process -FilePath "winget" -ArgumentList @("install", "--id", "9MWPM2CQNLHN", "--accept-package-agreements", "--accept-source-agreements", "--silent") -Wait -NoNewWindow -PassThru -ErrorAction Stop
# Check for Success (0) OR Already Installed (-1978335189 / 0x8A15002B)
if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq -1978335189) {
if ($proc.ExitCode -eq -1978335189) {
Write-Host " [OK] Gaming Services (already installed)" -ForegroundColor Green
Write-Log -Level SUCCESS -Message "Gaming Services already present - Xbox framework ready" -Module "Privacy"
} else {
Write-Host " [OK] Gaming Services installed" -ForegroundColor Green
Write-Log -Level SUCCESS -Message "Gaming Services installed" -Module "Privacy"
}
}
else {
Write-Host " [WARN] Gaming Services install had issues - Gaming apps may prompt on first launch" -ForegroundColor Yellow
Write-Log -Level WARNING -Message "Gaming Services install failed (ExitCode: $($proc.ExitCode)) - continuing anyway" -Module "Privacy"
}
}
catch {
Write-Log -Level WARNING -Message "Could not install Gaming Services: $_" -Module "Privacy"
}
Write-Host ""
}
foreach ($app in $appsWithWinget) {
$id = $app.WingetId
$name = $app.AppName
Write-Host " [>] Installing $name ($id)..." -ForegroundColor White
try {
# STEP 1: Check if app exists in winget catalog first (avoid unnecessary install attempts)
$searchStdout = Join-Path $env:TEMP "winget_search_$([guid]::NewGuid()).txt"
$searchStderr = Join-Path $env:TEMP "winget_search_err_$([guid]::NewGuid()).txt"
$searchProc = Start-Process -FilePath "winget" `
-ArgumentList @("search", "--id", $id, "--exact") `
-Wait -NoNewWindow -PassThru `
-RedirectStandardOutput $searchStdout `
-RedirectStandardError $searchStderr `
-ErrorAction Stop
# Cleanup temp files
Remove-Item $searchStdout, $searchStderr -Force -ErrorAction SilentlyContinue
# ExitCode -1978335212 = No package found
if ($searchProc.ExitCode -eq -1978335212 -or $searchProc.ExitCode -ne 0) {
Write-Host " [SKIP] $name (not available in winget catalog)" -ForegroundColor DarkGray
Write-Log -Level INFO -Message "App not available in winget catalog: $name ($id) - skipping" -Module "Privacy"
$failCount++ # Count as "failed" for summary, but not a real error
continue
}
# STEP 2: App exists - proceed with installation
$proc = Start-Process -FilePath "winget" -ArgumentList @("install", "--id", $id, "--exact", "--source", "msstore", "--accept-package-agreements", "--accept-source-agreements", "--silent") -Wait -NoNewWindow -PassThru -ErrorAction Stop
if ($proc.ExitCode -eq 0 -or $proc.ExitCode -eq -1978335189) {
if ($proc.ExitCode -eq -1978335189) {
Write-Host " [OK] $name (already installed)" -ForegroundColor Green
Write-Log -Level SUCCESS -Message "App already installed (winget): $name ($id)" -Module "Privacy"
} else {
Write-Host " [OK] $name" -ForegroundColor Green
Write-Log -Level SUCCESS -Message "Restored app via winget: $name ($id)" -Module "Privacy"
}
$successCount++
}
else {
# Installation failed despite app being available
Write-Host " [FAIL] $name (ExitCode: $($proc.ExitCode))" -ForegroundColor Yellow
Write-Log -Level WARNING -Message "Failed to restore app via winget: $name ($id) ExitCode=$($proc.ExitCode)" -Module "Privacy"
$failCount++
}
}
catch {
Write-Host " [FAIL] $name (exception)" -ForegroundColor Red
Write-Log -Level WARNING -Message "Exception when restoring app via winget: $name ($id) - $_" -Module "Privacy"
$failCount++
}
}
Write-Host ""
Write-Host " Winget restore summary: $successCount succeeded, $failCount failed" -ForegroundColor Cyan
# Collect ALL non-restorable apps for user notification:
# 1. Apps explicitly in $nonRestorableApps list (known to not be in winget catalog)
# 2. Apps that have no WingetId (excluding Xbox system components handled by Gaming Services)
$allNonRestorableDisplayNames = @()
# Add apps from $nonRestorableApps that were actually removed
foreach ($skipped in $skippedNonRestorableApps) {
$displayName = ($nonRestorableApps | Where-Object { $_.AppName -eq $skipped.AppName }).DisplayName
if ($displayName) { $allNonRestorableDisplayNames += $displayName }
}
# Add apps without WingetId (that are not Xbox system components)
foreach ($app in $appsManualReinstall) {
# Use AppName as display name since we don't have a mapping
$allNonRestorableDisplayNames += $app.AppName
}
# Add hidden Xbox system components that are still missing after Gaming Services / Xbox Game Bar restore
foreach ($app in $appsHandledByGamingServices) {
$pkg = $null
try {
$pkg = Get-AppxPackage -AllUsers -Name $app.AppName -ErrorAction SilentlyContinue
}
catch {
$pkg = $null
}
if (-not $pkg) {
switch ($app.AppName) {
"Microsoft.XboxSpeechToTextOverlay" {
$allNonRestorableDisplayNames += "Xbox Speech-to-Text Overlay (install/repair Gaming Services + Xbox Game Bar from Microsoft Store)"
}
"Microsoft.Xbox.TCUI" {
$allNonRestorableDisplayNames += "Xbox Game UI (Xbox.TCUI - install/repair Gaming Services + Xbox Game Bar from Microsoft Store)"
}
}
}
}
if ($failCount -gt 0) {
return [PSCustomObject]@{
Success = $false
NonRestorableApps = $allNonRestorableDisplayNames
}
}
return [PSCustomObject]@{
Success = $true
NonRestorableApps = $allNonRestorableDisplayNames
}
}
catch {
Write-Log -Level ERROR -Message "Failed to restore apps via winget: $_" -Module "Privacy"
return [PSCustomObject]@{
Success = $false
NonRestorableApps = @()
}
}
}