diff --git a/.github/scripts/screenshots.ps1 b/.github/scripts/screenshots.ps1 index a4391dd..bfa5ab1 100644 --- a/.github/scripts/screenshots.ps1 +++ b/.github/scripts/screenshots.ps1 @@ -72,6 +72,8 @@ Set-Content $fakeUsb @( $env:FASTBOOT_STUDIO_FAKE_USB = $fakeUsb # No update check by itself while the screens are captured; the update dialog is tested at the end. $env:FASTBOOT_STUDIO_UPDATE_FEED = 'off' +# No tour or What's new over the pages; both are captured on their own near the end. +$env:FASTBOOT_STUDIO_WELCOME = 'off' Remove-Item $saveRoot -Recurse -Force -ErrorAction SilentlyContinue function Save-Desktop([string] $name) { @@ -1171,6 +1173,130 @@ Stop-Process -Id $proc.Id -Force Remove-Item Env:FASTBOOT_STUDIO_THEME Write-Host "captured the light theme" +# ---------------------------------------------------------------- the tour, What's new and the help panel +# The first start: four steps, the page picked on the map opens at the end, and its ؟ opens +# the help at the side. Then the start of a new version: What's new. In English, then Arabic. +function Tour([string] $prefix, [string] $okLabel) { + $env:FASTBOOT_STUDIO_WELCOME = 'on' + $env:FASTBOOT_STUDIO_FAKE_SAMSUNG_DRIVER = 'missing' + Launch-App + $next = Wait-For { Shown $root 'welcome_next' } 30 "the tour ($prefix)" + Wait-For { Shown $root ('welcome_language_' + $(if ($prefix -eq 'ar') { 'ar' } else { 'en' })) } 10 "the language step ($prefix)" | Out-Null + Start-Sleep -Milliseconds 600 + Save-Window "$prefix-welcome-1-language" + Press 'welcome_next' + Wait-For { Shown $root 'welcome_page_samsung' } 10 "the page map ($prefix)" | Out-Null + Press 'welcome_page_samsung' + Start-Sleep -Milliseconds 600 + Save-Window "$prefix-welcome-2-pages" + Press 'welcome_next' + # The check reads the PC: the Samsung driver is "missing", the stand-in USB list has a phone without a driver. + Wait-For { Shown $root 'welcome_install_samsung' } 30 "the quick check ($prefix)" | Out-Null + Wait-For { Shown $root 'welcome_usb_check' } 30 "the USB line of the check ($prefix)" | Out-Null + Start-Sleep -Milliseconds 600 + Save-Window "$prefix-welcome-3-check" + Press 'welcome_next' + Start-Sleep -Milliseconds 800 + Save-Window "$prefix-welcome-4-safety" + Press 'welcome_next' + # The page picked on the map is open, and the tour is gone. + Wait-For { Shown $root 'samsung_open' } 10 "the Samsung page after the tour ($prefix)" | Out-Null + if ($null -ne (Shown $root 'welcome_next')) { Fail "the tour is still showing after Start ($prefix)" } + Press 'help_samsung' + Wait-For { Shown $root 'help_close' } 10 "the Samsung help ($prefix)" | Out-Null + Start-Sleep -Milliseconds 800 + Save-Window "$prefix-help-samsung" + # Another page while the help is open: its own help follows. + Go 'device_tab' | Out-Null + Start-Sleep -Milliseconds 800 + Save-Window "$prefix-help-device" + Press 'help_close' + Wait-For { $null -eq (Shown $root 'help_close') } 10 "the help to close ($prefix)" | Out-Null + if (Test-Path $crashLog) { Fail "the app recorded an unhandled exception in the tour ($prefix)" } + Stop-Process -Id $proc.Id -Force + Start-Sleep -Seconds 2 + + $env:FASTBOOT_STUDIO_WELCOME = 'whatsnew' + Launch-App + Wait-For { $n = Shown $root 'welcome_next'; $null -ne $n -and $n.Current.Name -eq $okLabel } 30 "What's new ($prefix)" | Out-Null + Start-Sleep -Milliseconds 600 + Save-Window "$prefix-whats-new" + Press 'welcome_next' + Wait-For { $null -eq (Shown $root 'welcome_next') } 10 "What's new to close ($prefix)" | Out-Null + if (Test-Path $crashLog) { Fail "the app recorded an unhandled exception in What's new ($prefix)" } + Stop-Process -Id $proc.Id -Force + Remove-Item Env:FASTBOOT_STUDIO_FAKE_SAMSUNG_DRIVER + $env:FASTBOOT_STUDIO_WELCOME = 'off' + Start-Sleep -Seconds 2 +} +Tour 'en' 'OK' +if ($Arabic) { + $env:FASTBOOT_STUDIO_LANG = 'ar' + Tour 'ar' 'تمام' + Remove-Item Env:FASTBOOT_STUDIO_LANG +} +Write-Host "tour, What's new and help: shown, the picked page opened, the help followed the page" + +# ---------------------------------------------------------------- Download mode (Odin, reading only) +# A stand-in phone answers the Odin protocol on a made-up COM9: the window finds it, reads its +# PIT and model, and restarts it. Then a port another program holds, which must fail clearly. +function Save-Dialog($dialog, [string] $name) { + [Win32]::SetForegroundWindow([IntPtr]$dialog.Current.NativeWindowHandle) | Out-Null + Start-Sleep -Milliseconds 800 + $b = $dialog.Current.BoundingRectangle + $bmp = New-Object System.Drawing.Bitmap ([int]$b.Width), ([int]$b.Height) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen([int]$b.Left, [int]$b.Top, 0, 0, $bmp.Size) + $path = Join-Path $Out "$name.png" + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() + Write-Host "saved $path ($([int]$b.Width)x$([int]$b.Height))" +} + +function Odin([string] $prefix, [string] $mode) { + $env:FASTBOOT_STUDIO_FAKE_ODIN = $mode + $env:FASTBOOT_STUDIO_FAKE_SAMSUNG_DRIVER = 'installed' + Launch-App + Go 'samsung_tab' | Out-Null + Press 'samsung_odin' + $dialog = Wait-For { Find-Dialog } 20 "the Download mode window ($prefix)" + $read = Wait-For { $b = By-Id $dialog 'odin_primary'; if ($null -ne $b -and $b.Current.IsEnabled) { $b } } 20 "the stand-in phone to be found ($prefix)" + Save-Dialog $dialog "$prefix-odin-1-ready" + $label = $read.Current.Name + $read.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() + if ($mode -eq 'phone') { + Wait-For { Shown $dialog 'odin_extra' } 30 "the reading to finish ($prefix)" | Out-Null + Save-Dialog $dialog "$prefix-odin-2-read" + } else { + Wait-For { $b = By-Id $dialog 'odin_primary'; $null -ne $b -and $b.Current.Name -ne $label -and -not $b.Current.IsOffscreen } 30 "the failure ($prefix)" | Out-Null + Save-Dialog $dialog "$prefix-odin-3-$mode" + } + $close = By-Id $dialog 'odin_secondary' + $close.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke() + Wait-For { $null -eq (Find-Dialog) } 10 "the Download mode window to close ($prefix)" | Out-Null + Go 'logs_tab' | Out-Null + $logText = (By-Id $root 'log_text').GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern).Current.Value + if ($mode -eq 'phone') { + if ($logText -notmatch 'odin: handshake LOKE') { Fail "the Odin handshake was not logged ($prefix)" } + if ($logText -notmatch 'odin: PIT, \d+ bytes') { Fail "the PIT was not read ($prefix)" } + if ($logText -notmatch 'odin: restart Normal') { Fail "the phone was not restarted ($prefix)" } + } elseif ($logText -notmatch 'odin: reading failed') { + Fail "the failure was not logged ($prefix)" + } + if (Test-Path $crashLog) { Fail "the app recorded an unhandled exception in the Download mode window ($prefix)" } + Stop-Process -Id $proc.Id -Force + Remove-Item Env:FASTBOOT_STUDIO_FAKE_ODIN, Env:FASTBOOT_STUDIO_FAKE_SAMSUNG_DRIVER + Start-Sleep -Seconds 2 +} +Odin 'en' 'phone' +Odin 'en' 'busy' +if ($Arabic) { + $env:FASTBOOT_STUDIO_LANG = 'ar' + Odin 'ar' 'phone' + Remove-Item Env:FASTBOOT_STUDIO_LANG +} +Write-Host "Download mode: the stand-in phone found, read and restarted; a busy port reported" + # ---------------------------------------------------------------- the update dialog # A test feed stands in for GitHub: a newer release whose zip is a local file. The dialog must # open by itself, download the zip, keep it only when its SHA-256 matches, and delete it otherwise. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4883e4..525ceaf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,51 @@ jobs: if (-not (Test-Path (Join-Path publish $f))) { throw "$f missing from publish output" } } + # The Samsung driver button keeps an installer only when Windows verifies Samsung's + # signature on it. Checked here with Samsung's real file; if Samsung cannot be reached, + # this only warns, since the check is about the app, not Samsung's servers. + - name: Samsung driver link and signature + shell: pwsh + run: | + $gen = 'tools/FastbootEnhance.SampleGen' + $neg = dotnet run --project $gen -c Release -- signer publish/adb.exe + Write-Host "adb.exe: $neg" + if (-not ($neg -match 'samsung=False')) { throw "adb.exe was taken for Samsung's driver" } + # A working check names Google on adb.exe; a broken one would name nobody. + if (-not ($neg -match 'O=Google LLC')) { throw "adb.exe's Google signature was not verified" } + $copy = Join-Path $env:RUNNER_TEMP 'locked-adb.exe' + Copy-Item publish/adb.exe $copy + Copy-Item publish/AdbWinApi.dll, publish/AdbWinUsbApi.dll $env:RUNNER_TEMP + $run = dotnet run --project $gen -c Release -- run-locked $copy version + Write-Host "run while held open: $run" + if (-not ($run -match 'exit=0')) { throw "a program held open for its check could not be started" } + $bad = Join-Path $env:RUNNER_TEMP 'unsigned.exe' + Copy-Item publish/adb.exe $bad + $bytes = [IO.File]::ReadAllBytes($bad); $bytes[1024] = $bytes[1024] -bxor 0xFF; [IO.File]::WriteAllBytes($bad, $bytes) + $tampered = dotnet run --project $gen -c Release -- signer $bad + Write-Host "tampered adb.exe: $tampered" + if (-not ($tampered -match 'no trusted signature')) { throw "a tampered file passed the signature check" } + try { + $page = Join-Path $env:RUNNER_TEMP 'samsung.html' + Invoke-WebRequest 'https://developer.samsung.com/android-usb-driver' -UseBasicParsing -TimeoutSec 60 -OutFile $page + $link = dotnet run --project $gen -c Release -- samsung-link $page + if ($LASTEXITCODE -ne 0) { Write-Warning "no download link found on Samsung's page"; $link = 'https://developer.samsung.com/SDP/file/a2fcaf64-7aa3-49f7-b06f-9e646590d528' } + Write-Host "link: $link" + $drv = Join-Path $env:RUNNER_TEMP 'samsung-driver.exe' + Invoke-WebRequest $link -UseBasicParsing -TimeoutSec 300 -OutFile $drv + } catch { + Write-Warning "Samsung could not be reached: $_" + exit 0 + } + $head = [IO.File]::ReadAllBytes($drv) + if ($head.Length -lt 5MB -or $head[0] -ne 0x4D -or $head[1] -ne 0x5A) { + Write-Warning "Samsung did not send the installer (got $($head.Length) bytes)" + exit 0 + } + $pos = dotnet run --project $gen -c Release -- signer $drv + Write-Host "Samsung's installer: $pos" + if (-not ($pos -match 'samsung=True')) { throw "Samsung's own installer did not pass the signature check" } + - uses: actions/upload-artifact@v4 with: name: FastbootStudio-win-x64 diff --git a/Assets/appname-dark.png b/Assets/appname-dark.png new file mode 100644 index 0000000..942e41c Binary files /dev/null and b/Assets/appname-dark.png differ diff --git a/Assets/appname-light.png b/Assets/appname-light.png new file mode 100644 index 0000000..0c63a56 Binary files /dev/null and b/Assets/appname-light.png differ diff --git a/Assets/signature-dark.png b/Assets/signature-dark.png new file mode 100644 index 0000000..12be4fe Binary files /dev/null and b/Assets/signature-dark.png differ diff --git a/Assets/signature-light.png b/Assets/signature-light.png new file mode 100644 index 0000000..a1d490f Binary files /dev/null and b/Assets/signature-light.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7951774..0ddf284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ +## v2.8.0 +- زر ؟ بكل صفحة: شرح وخطوات وتحذيرات بلغتك +- جولة ترحيب، و«الجديد» يطلع بعد كل تحديث +- تعريف سامسونج: يتحمّل من سامسونج ويتفحص توقيعه ويتثبّت +- ينبهك إذا boot مروّت، ويقترح تجرب الريكفري مؤقتاً أول +- سامسونج: حالة Knox، وخطوات Magisk أوضح (CSC أول مرة، HOME_CSC للتحديث) +- جديد: قراءة الجوال في وضع التحميل (أودين): الموديل والمنطقة وجدول PIT + +**English** +- A ? on every page: help, steps and dangers in your language +- A welcome tour, and What's new after each update +- Samsung driver: downloaded from Samsung, signature checked, installed +- A rooted boot image is named; a recovery suggests Boot once first +- Samsung: Knox status; clearer Magisk steps (CSC first, HOME_CSC to update) +- New: read a phone in Download mode (Odin): model, region and PIT + ## v2.7.1 - القوايم ما تبيّض وقت فحص MD5 أو الاستخراج، تبقى داكنة لين يخلص الشغل diff --git a/DriverUI.cs b/DriverUI.cs new file mode 100644 index 0000000..5bca642 --- /dev/null +++ b/DriverUI.cs @@ -0,0 +1,70 @@ +using FastbootEnhance.Core.Usb; +using Microsoft.Win32; +using System; +using System.IO; +using System.Linq; + +namespace FastbootEnhance +{ + /// + /// Samsung's USB driver: whether it is installed, and the window that downloads it from + /// Samsung, checks Samsung signed it and runs its installer. Download (Odin) mode needs it; + /// it is the same driver Odin itself uses, so installing it breaks nothing. + /// FASTBOOT_STUDIO_FAKE_SAMSUNG_DRIVER ("installed" or "missing") answers for tests. + /// + static class DriverUI + { + const string StandInVariable = "FASTBOOT_STUDIO_FAKE_SAMSUNG_DRIVER"; + + /// True when Windows' driver store holds the Download-mode driver, or the installer is listed in Programs. + public static bool SamsungInstalled() + { + string standIn = Environment.GetEnvironmentVariable(StandInVariable); + if (!string.IsNullOrEmpty(standIn)) + return string.Equals(standIn.Trim(), "installed", StringComparison.OrdinalIgnoreCase); + + try + { + string store = Path.Combine(Environment.SystemDirectory, "DriverStore", "FileRepository"); + if (Directory.Exists(store) && Directory.EnumerateDirectories(store) + .Any(d => SamsungDriver.IsDriverStoreFolder(Path.GetFileName(d)))) + return true; + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException) + { + } + + const string uninstall = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; + foreach (RegistryView view in new[] { RegistryView.Registry64, RegistryView.Registry32 }) + { + try + { + using (RegistryKey root = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view)) + using (RegistryKey list = root.OpenSubKey(uninstall)) + { + if (list == null) + continue; + foreach (string name in list.GetSubKeyNames()) + { + using (RegistryKey entry = list.OpenSubKey(name)) + { + if (SamsungDriver.IsProgramName(entry?.GetValue("DisplayName") as string)) + return true; + } + } + } + } + catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is System.Security.SecurityException) + { + } + } + return false; + } + + /// Opens the download-and-install window; runs when it closes. + public static void InstallSamsung(Action after = null) + { + DriverWindow.Show(after); + } + } +} diff --git a/DriverWindow.xaml b/DriverWindow.xaml new file mode 100644 index 0000000..0034608 --- /dev/null +++ b/DriverWindow.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +