diff --git a/.github/scripts/screenshots.ps1 b/.github/scripts/screenshots.ps1
index 485dc6d..bd91e3a 100644
--- a/.github/scripts/screenshots.ps1
+++ b/.github/scripts/screenshots.ps1
@@ -819,6 +819,62 @@ if (Test-Path $stockBoot) {
Write-Host "root: stock boot sent, Magisk image pulled and checked, flashed to boot"
}
+# ---------------------------------------------------------------- phone
+# A phone in Android with Fastboot Studio Companion: read without root, then through su (the
+# bootloader's own AVB state wins over the property a hiding module changed). The boot backup
+# on the phone is copied and checked against its SHA256SUMS; then the same phone without the
+# module offers to get it.
+$adbCompanion = Join-Path $appDir 'fake-adb.companion'
+Set-Content $adbMode 'android'
+Remove-Item $adbCompanion -ErrorAction SilentlyContinue
+function Text-Of([string] $id) { return (By-Id $root $id).Current.Name }
+function Phone-Buttons([string] $label) {
+ $cond = New-Object System.Windows.Automation.PropertyCondition($UIA::ControlTypeProperty, $Type::Button)
+ return @((By-Id $root 'phone_backups').FindAll($Scope::Descendants, $cond) | Where-Object { $_.Current.Name -eq $label })
+}
+# The page scrolls as a whole on a small screen; 0 is the top, 100 the backups table.
+function Phone-Scroll([double] $percent) {
+ $scroll = (By-Id $root 'phone_page').GetCurrentPattern([System.Windows.Automation.ScrollPattern]::Pattern)
+ if ($scroll.Current.VerticallyScrollable) { $scroll.SetScrollPercent(-1, $percent) }
+ Start-Sleep -Milliseconds 400
+}
+Go 'phone_tab' | Out-Null
+Wait-For { Shown $root 'phone_companion_version' } 60 "the Companion's report" | Out-Null
+$phoneBackups = Wait-For { By-Id $root 'phone_backups' } 10 "the phone's backups"
+Wait-For { (Rows $phoneBackups).Count -eq 3 } 20 "three backups on the phone" | Out-Null
+if ((Text-Of 'phone_companion_version') -notmatch '2\.4\.0') { Fail "the Companion version is '$(Text-Of 'phone_companion_version')'" }
+if ((Text-Of 'phone_avb') -ne 'AVB orange') { Fail "AVB shows '$(Text-Of 'phone_avb')', not the bootloader's orange" }
+if ((Text-Of 'phone_soc') -ne 'Qualcomm') { Fail "the chip maker shows '$(Text-Of 'phone_soc')'" }
+Start-Sleep -Milliseconds 600
+Save-Window ('{0:D2}a-phone' -f ($i - 1))
+
+$copyLabel = 'Copy and verify'
+$copyButtons = Wait-For { $b = Phone-Buttons $copyLabel; if ($b.Count -eq 3) { $b } } 10 "a copy button on each backup"
+$copyButtons[2].GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke()
+$copied = Join-Path $saveRoot 'Backups\Sample_A64_FBE0SAMPLE02_20260830-091233'
+Wait-For { (Phone-Buttons $copyLabel).Count -eq 2 -and (Phone-Buttons 'Open').Count -eq 1 } 60 "the boot backup to be copied and checked" | Out-Null
+Phone-Scroll 100
+if (-not (Test-Path (Join-Path $copied 'SHA256SUMS'))) { Fail "the backup was not copied to $copied" }
+Push-Location $copied
+foreach ($line in Get-Content 'SHA256SUMS') {
+ $hash, $file = $line -split ' ', 2
+ if ((Get-FileHash $file -Algorithm SHA256).Hash.ToLower() -ne $hash) { Pop-Location; Fail "$file does not match SHA256SUMS" }
+}
+Pop-Location
+if (@(Get-Content $fakeAdbLog | Where-Object { $_ -match 'pull \| -a \| /sdcard/FastbootStudio/Backups/' }).Count -ne 1) { Fail "the backup was not pulled exactly once" }
+Start-Sleep -Milliseconds 600
+Save-Window ('{0:D2}b-phone-copied' -f ($i - 1))
+
+Set-Content $adbCompanion 'missing'
+Press 'phone_read_again'
+Phone-Scroll 0
+Wait-For { Shown $root 'phone_companion_get' } 40 "the page to offer the Companion" | Out-Null
+if ($null -ne (Shown $root 'phone_companion_version')) { Fail "the Companion still shows after it was removed" }
+Start-Sleep -Milliseconds 600
+Save-Window ('{0:D2}c-phone-no-companion' -f ($i - 1))
+Remove-Item $adbMode, $adbCompanion -ErrorAction SilentlyContinue
+Write-Host "phone: read with the Companion (AVB from the bootloader, 3 conflicts), boot backup copied and checked, then without it"
+
# ---------------------------------------------------------------- terminal
# A command typed in the Terminal runs with the bundled fastboot and shows its output; one
# that erases a boot-chain partition asks for the name first, and answering No runs nothing.
@@ -915,6 +971,15 @@ if ($Arabic) {
Remove-Item $adbMode -ErrorAction SilentlyContinue
}
+ # The Phone page, right to left, with the Companion's report.
+ Set-Content $adbMode 'android'
+ Go 'phone_tab' | Out-Null
+ Wait-For { Shown $root 'phone_companion_version' } 60 "the Companion's report (Arabic)" | Out-Null
+ Wait-For { (Rows (By-Id $root 'phone_backups')).Count -eq 3 } 20 "the phone's backups (Arabic)" | Out-Null
+ Start-Sleep -Milliseconds 600
+ Save-Window 'ar-11-phone'
+ Remove-Item $adbMode -ErrorAction SilentlyContinue
+
if (Test-Path $superPart) {
Go 'images_tab' | Out-Null
Open-Image $superPart
@@ -972,6 +1037,13 @@ Start-Sleep -Seconds 3
Save-Window 'light-02-device'
Go 'flash_tab' | Out-Null
Save-Window 'light-03-flash'
+Set-Content $adbMode 'android'
+Go 'phone_tab' | Out-Null
+Wait-For { Shown $root 'phone_companion_version' } 60 "the Companion's report (light)" | Out-Null
+Wait-For { (Rows (By-Id $root 'phone_backups')).Count -eq 3 } 20 "the phone's backups (light)" | Out-Null
+Start-Sleep -Milliseconds 600
+Save-Window 'light-06-phone'
+Remove-Item $adbMode -ErrorAction SilentlyContinue
Go 'terminal_tab' | Out-Null
Terminal-Run 'fastboot devices'
Wait-For { (Terminal-Output) -match 'FBE0SAMPLE01' } 30 "fastboot devices in the terminal (light)" | Out-Null
diff --git a/BackupUI.cs b/BackupUI.cs
index ae70eb1..3cebb4c 100644
--- a/BackupUI.cs
+++ b/BackupUI.cs
@@ -37,6 +37,9 @@ static class BackupUI
static string saveRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Fastboot Studio");
+ /// The folder backups are saved to; the Phone page copies the Companion's backups there too.
+ public static string SaveRoot => saveRoot;
+
static string lastOutputFolder;
static volatile bool busy;
@@ -1047,7 +1050,7 @@ static void pullSelected()
});
}
- static void openInExplorer(string folder)
+ internal static void openInExplorer(string folder)
{
try
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 432b6ef..d4f673c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@
+## v2.5.0
+- صفحة «الجوال» جديدة: مواصفات الجوال وحمايته وهو شغّال على أندرويد
+- تتعرف على إضافة Fastboot Studio Companion: حالة AVB من البوت لودر وتعارض المودات
+- النسخ الاحتياطية اللي على الجوال: انقلها للكمبيوتر وتحقق منها بضغطة
+- إعادة التشغيل للبوت لودر أو fastbootd أو الريكفري من نفس الصفحة
+
## v2.4.0
- صفحة «روت» جديدة: Magisk و KernelSU و APatch في أربع خطوات
- فحص صورة الإقلاع قبل التفليش: يمنع init_boot في boot وصورة boot في vendor_boot
diff --git a/FastbootEnhance.csproj b/FastbootEnhance.csproj
index 3cd0ad5..a1429fe 100644
--- a/FastbootEnhance.csproj
+++ b/FastbootEnhance.csproj
@@ -15,7 +15,7 @@
- 2.4.0
+ 2.5.0
Fastboot Studio
Fastboot Studio
IFOKNR; LibXZR
diff --git a/MainWindow.xaml b/MainWindow.xaml
index ebd8396..a617cfd 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -220,6 +220,225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1246,7 +1465,7 @@
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 5f73072..1a38231 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -11,7 +11,7 @@ namespace FastbootEnhance
public partial class MainWindow : Window
{
public static MainWindow THIS;
- const string version = "2.4.0";
+ const string version = "2.5.0";
public MainWindow()
{
@@ -54,6 +54,7 @@ public MainWindow()
wireLanguages();
wireThemes();
RootUI.init();
+ PhoneUI.init();
TerminalUI.init();
// Closing mid-flash kills the worker between partitions and can leave the phone
diff --git a/PhoneUI.cs b/PhoneUI.cs
new file mode 100644
index 0000000..a6af0e0
--- /dev/null
+++ b/PhoneUI.cs
@@ -0,0 +1,563 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Input;
+using FastbootEnhance.Core.Adb;
+
+namespace FastbootEnhance
+{
+ ///
+ /// The Phone page: a phone running Android, read over adb while the page is open. The
+ /// properties, kernel, battery and the Companion's backups on /sdcard need no root; with the
+ /// Fastboot Studio Companion module (through su) the page also shows the bootloader's own AVB
+ /// state, the root manager, storage wear and the conflicts between root modules. Backups can
+ /// be copied to the PC and checked against their SHA256SUMS.
+ ///
+ static class PhoneUI
+ {
+ static MainWindow W => MainWindow.THIS;
+
+ const string CompanionReleases = "https://github.com/ifoknr/FastbootStudio-Companion/releases/latest";
+
+ // Long enough to tap "Allow" when the root manager asks; a refusal comes back at once.
+ static readonly TimeSpan CompanionTimeout = TimeSpan.FromSeconds(20);
+ static readonly TimeSpan CopyTimeout = TimeSpan.FromHours(3);
+
+ static readonly ObservableCollection backupRows = new ObservableCollection();
+ static readonly AutoResetEvent wake = new AutoResetEvent(false);
+
+ // What a backup copied in this run of the app turned out to be, by folder name.
+ static readonly Dictionary> checkedCopies = new Dictionary>();
+
+ static volatile bool pageVisible;
+ static volatile bool readAgain;
+ static int copying;
+ static string shownSerial;
+ static string shownModel;
+ static bool wideLayout = true;
+ static bool laidOut;
+
+ static void log(string line) => LogStore.Append("phone: " + line);
+
+ static void ui(Action action) => W.Dispatcher.Invoke(action);
+
+ static string backupsOnPc => Path.Combine(BackupUI.SaveRoot, "Backups");
+
+ public static void init()
+ {
+ W.phone_backups.ItemsSource = backupRows;
+ W.phone_backups.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(rowButton));
+ // The table shows every row and the page scrolls; the list would keep the wheel to itself.
+ W.phone_backups.PreviewMouseWheel += delegate (object sender, MouseWheelEventArgs e)
+ {
+ e.Handled = true;
+ W.phone_page.RaiseEvent(new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.MouseWheelEvent, Source = sender });
+ };
+
+ W.phone_reboot_recovery.Click += delegate { reboot("recovery", Properties.Resources.phone_reboot_recovery); };
+ W.phone_reboot_fastbootd.Click += delegate { reboot("fastboot", "fastbootd"); };
+ W.phone_reboot_bootloader.Click += delegate { reboot("bootloader", Properties.Resources.phone_bootloader); };
+ W.phone_read_again.Click += delegate { readNow(); };
+ W.phone_companion_retry.Click += delegate { readNow(); };
+ W.phone_companion_get.Click += delegate { openLink(CompanionReleases); };
+ W.phone_backups_open.Click += delegate { BackupUI.openInExplorer(backupsOnPc); };
+
+ W.phone_grid.SizeChanged += delegate { layout(); };
+
+ W.main_tabs.SelectionChanged += delegate (object sender, SelectionChangedEventArgs e)
+ {
+ if (e.OriginalSource != W.main_tabs)
+ return;
+ pageVisible = W.main_tabs.SelectedItem == W.phone_tab;
+ if (pageVisible)
+ wake.Set();
+ };
+
+ Thread poller = new Thread(poll) { IsBackground = true, Name = "phone poller" };
+ poller.Start();
+ }
+
+ static void readNow()
+ {
+ readAgain = true;
+ wake.Set();
+ }
+
+ // ---------------------------------------------------------------- reading the phone
+
+ static void poll()
+ {
+ while (true)
+ {
+ wake.WaitOne(TimeSpan.FromSeconds(2.5));
+ if (!pageVisible || Volatile.Read(ref copying) > 0)
+ continue;
+
+ List devices;
+ try
+ {
+ Adb.Result list = Adb.Run("devices -l", Adb.ShortCommand);
+ devices = AdbDevice.ParseList(list.Output);
+ }
+ catch (FileNotFoundException e)
+ {
+ log("adb.exe is missing: " + e.FileName);
+ ui(delegate { showEmpty(Properties.Resources.phone_empty_title, e.Message); });
+ return;
+ }
+ catch (Exception e)
+ {
+ log("adb poll failed: " + e.Message);
+ continue;
+ }
+
+ AdbDevice phone = devices.FirstOrDefault(d => d.State == "device");
+ if (phone == null)
+ {
+ AdbDevice waiting = devices.FirstOrDefault(d => d.Unauthorized);
+ shownSerial = null;
+ ui(delegate
+ {
+ if (waiting != null)
+ showEmpty(Properties.Resources.phone_empty_title, string.Format(Properties.Resources.phone_unauthorized, waiting.Serial));
+ else
+ showEmpty(Properties.Resources.phone_empty_title, Properties.Resources.phone_empty_text);
+ });
+ continue;
+ }
+
+ if (phone.Serial == shownSerial && !readAgain)
+ continue;
+ readAgain = false;
+ read(phone);
+ }
+ }
+
+ /// Reads the phone, without root first and then through the Companion. Blocks.
+ static void read(AdbDevice phone)
+ {
+ string serial = phone.Serial;
+ bool samePhone = serial == shownSerial;
+ shownSerial = serial;
+ shownModel = phone.Model ?? serial;
+ if (!samePhone)
+ ui(delegate { showEmpty(string.Format(Properties.Resources.phone_reading, shownModel), ""); });
+
+ Adb.Result basic = Adb.Run(AdbCommand.Shell(serial, PhoneReport.Command), Adb.ShortCommand);
+ if (!basic.Succeeded && string.IsNullOrWhiteSpace(basic.Output))
+ {
+ log(serial + " not read: " + basic.Problem);
+ shownSerial = null;
+ return;
+ }
+ PhoneReport report = PhoneReport.Parse(basic.Output);
+ ui(delegate { show(serial, report, true); });
+
+ // The Companion lives under /data/adb, which only root can reach. Ask su only when the
+ // phone has one; Magisk shows a prompt the first time, KernelSU and APatch answer from
+ // their own allow list.
+ Adb.Result su = Adb.Run(AdbCommand.Shell(serial, "command -v su"), Adb.ShortCommand);
+ bool hasSu = su.Succeeded && su.Output.Trim().Length > 0;
+ string output = null;
+ if (hasSu)
+ {
+ Adb.Result companion = Adb.Run(AdbCommand.Shell(serial,
+ AdbCommand.AsRoot(PhoneReport.CompanionCommand, RootAccess.Su)), CompanionTimeout);
+ output = companion.Output;
+ }
+ report.ApplyCompanion(hasSu, output);
+ log(serial + " (" + (report.Model ?? "?") + "): Android " + (report.Android ?? "?") + ", kernel " + (report.Kernel ?? "?")
+ + ", " + report.Backups.Count + " backups, Companion " + report.Companion
+ + (report.Companion == CompanionState.Ready ? " " + report.CompanionVersion + ", " + report.Conflicts.Count + " conflicts" : ""));
+ ui(delegate
+ {
+ if (shownSerial == serial)
+ show(serial, report, false);
+ });
+
+ // The boot image check on the Device page uses the same phone's kernel and patch level.
+ AdbFacts.ReadOnce(serial);
+ }
+
+ // ---------------------------------------------------------------- showing it
+
+ ///
+ /// Three cards side by side when there is room for kernel strings on one line; otherwise
+ /// the Companion card goes under the other two, across the page.
+ ///
+ static void layout()
+ {
+ // A gap between the two widths, so the scroll bar coming and going cannot flip it back and forth.
+ double width = W.phone_grid.ActualWidth;
+ bool wide = width >= 1100 || (wideLayout && width >= 1040);
+ if (wide == wideLayout && laidOut)
+ return;
+ wideLayout = wide;
+ laidOut = true;
+ W.phone_gap_column.Width = new GridLength(wide ? 16 : 0);
+ W.phone_third_column.Width = wide ? new GridLength(1, GridUnitType.Star) : new GridLength(0);
+ Grid.SetRow(W.phone_companion_card, wide ? 0 : 2);
+ Grid.SetColumn(W.phone_companion_card, wide ? 4 : 0);
+ Grid.SetColumnSpan(W.phone_companion_card, wide ? 1 : 3);
+ W.phone_companion_card.Margin = new Thickness(0, 0, 0, wide ? 0 : 16);
+ }
+
+ static void showEmpty(string title, string text)
+ {
+ W.phone_page.Visibility = Visibility.Collapsed;
+ W.phone_header_actions.Visibility = Visibility.Collapsed;
+ W.phone_empty.Visibility = Visibility.Visible;
+ W.phone_empty_title.Text = title;
+ W.phone_empty_text.Text = text;
+ W.phone_empty_text.Visibility = string.IsNullOrEmpty(text) ? Visibility.Collapsed : Visibility.Visible;
+ }
+
+ static void show(string serial, PhoneReport phone, bool companionPending)
+ {
+ W.phone_empty.Visibility = Visibility.Collapsed;
+ W.phone_page.Visibility = Visibility.Visible;
+ W.phone_header_actions.Visibility = Visibility.Visible;
+ W.phone_serial.Text = serial;
+
+ showDevice(phone);
+ showSecurity(phone, companionPending);
+ showCompanion(phone, companionPending);
+ showBackups(phone);
+ }
+
+ static void showDevice(PhoneReport phone)
+ {
+ List rows = new List();
+ string model = phone.Model;
+ if (model != null && phone.Brand != null && model.IndexOf(phone.Brand, StringComparison.OrdinalIgnoreCase) < 0)
+ model = phone.Brand + " " + model;
+ add(rows, Properties.Resources.phone_model, model);
+ add(rows, Properties.Resources.phone_chip, join(phone.Platform, phone.Arch));
+ add(rows, Properties.Resources.phone_android, phone.Android == null ? null
+ : phone.Android + (phone.Sdk != null ? " (SDK " + phone.Sdk + ")" : ""));
+ add(rows, Properties.Resources.phone_patch, phone.Patch);
+ add(rows, Properties.Resources.phone_slot, join(
+ phone.Slot != null ? phone.Slot + " · A/B" : Properties.Resources.phone_slot_none,
+ phone.Dynamic == true ? Properties.Resources.phone_dynamic : null));
+
+ List battery = new List();
+ if (phone.BatteryHealth != null)
+ battery.Add(string.Format(Properties.Resources.phone_battery_health, phone.BatteryHealth));
+ if (phone.BatteryCycles != null)
+ battery.Add(string.Format(Properties.Resources.phone_battery_cycles, phone.BatteryCycles));
+ if (battery.Count == 0 && phone.BatteryLevel != null)
+ battery.Add(string.Format(Properties.Resources.phone_battery_charge, phone.BatteryLevel));
+ add(rows, Properties.Resources.phone_battery, battery.Count > 0 ? string.Join(" · ", battery) : null);
+
+ if (phone.StorageWear != null)
+ {
+ string type = phone.StorageType == "ufs" ? "UFS" : phone.StorageType == "emmc" ? "eMMC" : phone.StorageType ?? "";
+ int wear = phone.StorageWear.Value;
+ string text = wear >= 11 ? string.Format(Properties.Resources.phone_storage_past, type)
+ : string.Format(Properties.Resources.phone_storage_wear, type, (wear - 1) * 10, wear * 10);
+ if (phone.StorageEol >= 2)
+ text += " · " + Properties.Resources.phone_storage_spare;
+ add(rows, Properties.Resources.phone_storage, text);
+ }
+ W.phone_device_rows.ItemsSource = rows;
+
+ W.phone_soc.Text = phone.Soc ?? "";
+ W.phone_soc_chip.Visibility = phone.Soc != null ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ static void showSecurity(PhoneReport phone, bool companionPending)
+ {
+ bool ready = phone.Companion == CompanionState.Ready;
+ List rows = new List();
+ add(rows, Properties.Resources.phone_kernel, phone.Kernel);
+ add(rows, "KMI", phone.Kmi);
+ add(rows, Properties.Resources.phone_bootloader, phone.DeviceState == "unlocked" ? Properties.Resources.phone_unlocked
+ : phone.DeviceState == "locked" ? Properties.Resources.phone_locked : phone.DeviceState);
+ add(rows, "SELinux", phone.Selinux);
+ add(rows, Properties.Resources.phone_root, ready ? phone.Root ?? Properties.Resources.phone_root_none
+ : phone.Companion == CompanionState.NoRoot && !companionPending ? Properties.Resources.phone_root_none
+ : Properties.Resources.phone_root_unknown);
+ W.phone_security_rows.ItemsSource = rows;
+
+ if (phone.Avb != null)
+ {
+ bool green = phone.Avb == "green";
+ W.phone_avb.Text = "AVB " + phone.Avb;
+ W.phone_avb.Foreground = Palette.Get(green ? "Ok" : "Warn");
+ W.phone_avb_chip.Background = Palette.Get(green ? "AccentSoft" : "WarnSoft");
+ W.phone_avb_chip.BorderBrush = Palette.Get(green ? "AccentLine" : "WarnLine");
+ W.phone_avb_chip.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ W.phone_avb_chip.Visibility = Visibility.Collapsed;
+ }
+
+ if (ready && phone.Spoofed)
+ {
+ W.phone_avb_note.Text = Properties.Resources.phone_spoofed;
+ W.phone_avb_note.Foreground = Palette.Get("Warn");
+ W.phone_avb_note.Visibility = Visibility.Visible;
+ }
+ else if (!ready && phone.Avb != null)
+ {
+ W.phone_avb_note.Text = Properties.Resources.phone_avb_from_props;
+ W.phone_avb_note.Foreground = Palette.Get("Dim");
+ W.phone_avb_note.Visibility = Visibility.Visible;
+ }
+ else
+ {
+ W.phone_avb_note.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ static void showCompanion(PhoneReport phone, bool pending)
+ {
+ List conflicts = new List();
+ string text;
+ string dot = "Faint";
+ bool get = false, retry = false;
+
+ if (pending)
+ {
+ text = Properties.Resources.phone_companion_reading;
+ }
+ else
+ {
+ switch (phone.Companion)
+ {
+ case CompanionState.Ready:
+ if (phone.Conflicts.Count == 0)
+ {
+ text = string.Format(Properties.Resources.phone_conflicts_none, phone.ModuleCount ?? 0);
+ dot = "Ok";
+ }
+ else
+ {
+ text = string.Format(Properties.Resources.phone_conflicts_some, phone.Conflicts.Count);
+ dot = "Warn";
+ }
+ conflicts.AddRange(phone.Conflicts.Select(describe));
+ break;
+ case CompanionState.NotInstalled:
+ text = Properties.Resources.phone_companion_missing;
+ get = true;
+ break;
+ case CompanionState.Refused:
+ text = Properties.Resources.phone_companion_refused;
+ dot = "Warn";
+ retry = true;
+ break;
+ default:
+ text = Properties.Resources.phone_companion_no_root;
+ break;
+ }
+ }
+
+ W.phone_companion_text.Text = text;
+ W.phone_companion_dot.Fill = Palette.Get(dot);
+ W.phone_companion_get.Visibility = get ? Visibility.Visible : Visibility.Collapsed;
+ W.phone_companion_retry.Visibility = retry ? Visibility.Visible : Visibility.Collapsed;
+ W.phone_conflicts.ItemsSource = conflicts;
+
+ bool ready = !pending && phone.Companion == CompanionState.Ready && phone.CompanionVersion != null;
+ W.phone_companion_version.Text = ready
+ ? "v" + phone.CompanionVersion + (phone.CompanionChannel == "beta" ? " " + Properties.Resources.phone_companion_beta : "")
+ : "";
+ W.phone_companion_chip.Visibility = ready ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ static ConflictRow describe(ModuleConflict conflict)
+ {
+ string modules = string.Join(Properties.Resources.phone_and, conflict.Modules);
+ switch (conflict.Kind)
+ {
+ case ConflictKind.File:
+ return new ConflictRow(conflict.Count > 1
+ ? string.Format(Properties.Resources.phone_conflict_files, modules, conflict.Count)
+ : string.Format(Properties.Resources.phone_conflict_file, modules),
+ string.Join("\n", conflict.Paths.Take(2)) + (conflict.Count > 2 ? "\n…" : ""), true);
+ case ConflictKind.Replace:
+ return new ConflictRow(string.Format(Properties.Resources.phone_conflict_replace, conflict.By, modules),
+ conflict.Paths.FirstOrDefault() ?? "", true);
+ default:
+ return new ConflictRow(string.Format(Properties.Resources.phone_conflict_prop, modules),
+ conflict.Key + " = " + string.Join(" / ", conflict.Values.Select(v => v.Value).Distinct()), false);
+ }
+ }
+
+ static void showBackups(PhoneReport phone)
+ {
+ backupRows.Clear();
+ foreach (PhoneBackup backup in phone.Backups)
+ {
+ string type, brush;
+ switch (backup.Set)
+ {
+ case BackupSet.Boot: type = Properties.Resources.phone_set_boot; brush = "Accent"; break;
+ case BackupSet.Critical: type = Properties.Resources.phone_set_critical; brush = "Warn"; break;
+ default: type = Properties.Resources.phone_set_full; brush = "Ok"; break;
+ }
+ PhoneBackupRow row = new PhoneBackupRow(backup, type, Palette.Get(brush),
+ backup.Kb != null ? Helper.byte2AUnit(backup.Kb.Value * 1024) : "",
+ backup.Taken?.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture) ?? "");
+ showOnPc(row);
+ backupRows.Add(row);
+ }
+
+ W.phone_backups_summary.Text = string.Format(Properties.Resources.phone_backups_summary,
+ phone.Backups.Count, Helper.byte2AUnit(phone.BackupsKb * 1024));
+ DateTime? last = phone.Backups.Select(b => b.Taken).FirstOrDefault(t => t != null);
+ W.phone_backups_last.Text = last != null
+ ? string.Format(Properties.Resources.phone_backups_last, last.Value.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture))
+ : "";
+ W.phone_backups_last_chip.Visibility = last != null ? Visibility.Visible : Visibility.Collapsed;
+ W.phone_backups_empty.Visibility = phone.Backups.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ /// Whether the backup is in the Backup page's folder on this PC, and what its check said.
+ static void showOnPc(PhoneBackupRow row)
+ {
+ string local = Path.Combine(backupsOnPc, row.Name);
+ bool there = Directory.Exists(local) && File.Exists(Path.Combine(local, Sha256Sums.FileName));
+ row.Copied = there;
+ KeyValuePair result;
+ lock (checkedCopies)
+ {
+ if (there && checkedCopies.TryGetValue(row.Name, out result))
+ {
+ row.SetState(result.Key, result.Value);
+ return;
+ }
+ }
+ row.SetState(there ? Properties.Resources.phone_pc_yes : Properties.Resources.phone_pc_no, there ? "Ok" : "Dim");
+ }
+
+ // ---------------------------------------------------------------- actions
+
+ static void rowButton(object sender, RoutedEventArgs e)
+ {
+ PhoneBackupRow row = (e.OriginalSource as Button)?.Tag as PhoneBackupRow;
+ if (row == null)
+ return;
+ e.Handled = true;
+ if (row.Copied)
+ BackupUI.openInExplorer(Path.Combine(backupsOnPc, row.Name));
+ else
+ copy(row);
+ }
+
+ /// adb pull of the backup folder (it is on /sdcard: no root), then every file checked against SHA256SUMS.
+ static void copy(PhoneBackupRow row)
+ {
+ string serial = shownSerial;
+ if (serial == null)
+ return;
+ string target = backupsOnPc;
+ string local = Path.Combine(target, row.Name);
+ row.Idle = false;
+ row.SetState(Properties.Resources.phone_pc_copying, "Accent");
+ Interlocked.Increment(ref copying);
+
+ string state = null, brush = null;
+ Helper.offloadAndRun(delegate
+ {
+ try
+ {
+ Directory.CreateDirectory(target);
+ Adb.Result pulled = Adb.Run(AdbCommand.Pull(serial, row.Backup.Path, target), CopyTimeout);
+ if (!pulled.Succeeded || !Directory.Exists(local))
+ {
+ state = string.Format(Properties.Resources.phone_pc_failed, pulled.Problem);
+ brush = "Danger";
+ log("copy of " + row.Backup.Path + " failed: " + pulled.Problem);
+ return;
+ }
+ ui(delegate { row.SetState(Properties.Resources.phone_pc_checking, "Accent"); });
+ VerifyReport report = Sha256Sums.Verify(local);
+ if (report.AllGood)
+ {
+ state = Properties.Resources.phone_pc_ok;
+ brush = "Ok";
+ }
+ else
+ {
+ state = string.Format(Properties.Resources.phone_pc_bad, Math.Max(1, report.Bad.Count), report.Total);
+ brush = "Danger";
+ }
+ log("copied " + row.Backup.Path + " to " + local + ": " + report.Good.Count + " of " + report.Total + " files match"
+ + (report.Bad.Count > 0 ? " (" + string.Join(", ", report.Bad) + ")" : ""));
+ }
+ catch (Exception e)
+ {
+ state = string.Format(Properties.Resources.phone_pc_failed, e.Message);
+ brush = "Danger";
+ log("copy of " + row.Backup.Path + " failed: " + e);
+ }
+ }, delegate
+ {
+ Interlocked.Decrement(ref copying);
+ lock (checkedCopies)
+ checkedCopies[row.Name] = new KeyValuePair(state, brush);
+ row.Copied = Directory.Exists(local) && File.Exists(Path.Combine(local, Sha256Sums.FileName));
+ row.SetState(state, brush);
+ row.Idle = true;
+ });
+ }
+
+ static void reboot(string target, string name)
+ {
+ string serial = shownSerial;
+ if (serial == null)
+ return;
+ if (!Helper.confirm(string.Format(Properties.Resources.phone_reboot_confirm, shownModel ?? serial, name), Properties.Resources.nav_phone))
+ return;
+ string error = null;
+ Helper.offloadAndRun(delegate
+ {
+ Adb.Result result = Adb.Run(AdbCommand.Reboot(serial, target), Adb.ShortCommand);
+ log("adb reboot " + target + " (" + serial + ") -> " + result.ExitCode);
+ if (!result.Succeeded)
+ error = result.Problem;
+ }, delegate
+ {
+ if (error != null)
+ ThemedDialog.Show(string.Format(Properties.Resources.phone_reboot_failed, error), Properties.Resources.nav_phone,
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+ wake.Set();
+ });
+ }
+
+ static void openLink(string url)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
+ }
+ catch (Exception e)
+ {
+ log("cannot open " + url + ": " + e.Message);
+ }
+ }
+
+ static void add(List rows, string name, string value)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ rows.Add(new PhoneRow(name, value));
+ }
+
+ static string join(params string[] parts)
+ {
+ string[] present = parts.Where(p => !string.IsNullOrWhiteSpace(p)).ToArray();
+ return present.Length > 0 ? string.Join(" · ", present) : null;
+ }
+ }
+}
diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs
index 7f0e38b..5ff1dde 100644
--- a/Properties/Resources.Designer.cs
+++ b/Properties/Resources.Designer.cs
@@ -5394,5 +5394,681 @@ public static string root_image_patched {
return ResourceManager.GetString("root_image_patched", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to Phone.
+ ///
+ public static string nav_phone {
+ get {
+ return ResourceManager.GetString("nav_phone", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The phone while it runs Android: its specs, its security and the backups on it, with more from Fastboot Studio Companion..
+ ///
+ public static string phone_subtitle {
+ get {
+ return ResourceManager.GetString("phone_subtitle", resourceCulture);
+ }
+ }
+
+
+ ///
+ /// Looks up a localized string similar to Recovery.
+ ///
+ public static string phone_reboot_recovery {
+ get {
+ return ResourceManager.GetString("phone_reboot_recovery", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Restart to bootloader.
+ ///
+ public static string phone_reboot_bootloader {
+ get {
+ return ResourceManager.GetString("phone_reboot_bootloader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Read again.
+ ///
+ public static string phone_read_again {
+ get {
+ return ResourceManager.GetString("phone_read_again", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No phone in Android.
+ ///
+ public static string phone_empty_title {
+ get {
+ return ResourceManager.GetString("phone_empty_title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connect the phone with USB debugging on, and allow this computer when the phone asks. A phone in the bootloader shows on the Device page..
+ ///
+ public static string phone_empty_text {
+ get {
+ return ResourceManager.GetString("phone_empty_text", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} is waiting: allow USB debugging for this computer on the phone's screen..
+ ///
+ public static string phone_unauthorized {
+ get {
+ return ResourceManager.GetString("phone_unauthorized", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Reading {0}….
+ ///
+ public static string phone_reading {
+ get {
+ return ResourceManager.GetString("phone_reading", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Device.
+ ///
+ public static string phone_card_device {
+ get {
+ return ResourceManager.GetString("phone_card_device", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Model.
+ ///
+ public static string phone_model {
+ get {
+ return ResourceManager.GetString("phone_model", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Chip.
+ ///
+ public static string phone_chip {
+ get {
+ return ResourceManager.GetString("phone_chip", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Android.
+ ///
+ public static string phone_android {
+ get {
+ return ResourceManager.GetString("phone_android", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Security patch.
+ ///
+ public static string phone_patch {
+ get {
+ return ResourceManager.GetString("phone_patch", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Slot.
+ ///
+ public static string phone_slot {
+ get {
+ return ResourceManager.GetString("phone_slot", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Battery.
+ ///
+ public static string phone_battery {
+ get {
+ return ResourceManager.GetString("phone_battery", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Storage.
+ ///
+ public static string phone_storage {
+ get {
+ return ResourceManager.GetString("phone_storage", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Kernel and security.
+ ///
+ public static string phone_card_security {
+ get {
+ return ResourceManager.GetString("phone_card_security", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Kernel.
+ ///
+ public static string phone_kernel {
+ get {
+ return ResourceManager.GetString("phone_kernel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Bootloader.
+ ///
+ public static string phone_bootloader {
+ get {
+ return ResourceManager.GetString("phone_bootloader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Root.
+ ///
+ public static string phone_root {
+ get {
+ return ResourceManager.GetString("phone_root", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Locked.
+ ///
+ public static string phone_locked {
+ get {
+ return ResourceManager.GetString("phone_locked", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unlocked.
+ ///
+ public static string phone_unlocked {
+ get {
+ return ResourceManager.GetString("phone_unlocked", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to None found.
+ ///
+ public static string phone_root_none {
+ get {
+ return ResourceManager.GetString("phone_root_none", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Known with the Companion.
+ ///
+ public static string phone_root_unknown {
+ get {
+ return ResourceManager.GetString("phone_root_unknown", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A hiding module changes ro.boot.verifiedbootstate. The state shown here is the bootloader's own..
+ ///
+ public static string phone_spoofed {
+ get {
+ return ResourceManager.GetString("phone_spoofed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to AVB as the phone's properties say; a hiding module can change them. The Companion reads the bootloader's own..
+ ///
+ public static string phone_avb_from_props {
+ get {
+ return ResourceManager.GetString("phone_avb_from_props", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Boot images are checked against this kernel before flashing..
+ ///
+ public static string phone_boot_checked {
+ get {
+ return ResourceManager.GetString("phone_boot_checked", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}% health.
+ ///
+ public static string phone_battery_health {
+ get {
+ return ResourceManager.GetString("phone_battery_health", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} cycles.
+ ///
+ public static string phone_battery_cycles {
+ get {
+ return ResourceManager.GetString("phone_battery_cycles", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0}% charged.
+ ///
+ public static string phone_battery_charge {
+ get {
+ return ResourceManager.GetString("phone_battery_charge", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} · {1}–{2}% worn.
+ ///
+ public static string phone_storage_wear {
+ get {
+ return ResourceManager.GetString("phone_storage_wear", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} · past its rated life.
+ ///
+ public static string phone_storage_past {
+ get {
+ return ResourceManager.GetString("phone_storage_past", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to spare blocks running low.
+ ///
+ public static string phone_storage_spare {
+ get {
+ return ResourceManager.GetString("phone_storage_spare", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No A/B slots.
+ ///
+ public static string phone_slot_none {
+ get {
+ return ResourceManager.GetString("phone_slot_none", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to dynamic.
+ ///
+ public static string phone_dynamic {
+ get {
+ return ResourceManager.GetString("phone_dynamic", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Beta.
+ ///
+ public static string phone_companion_beta {
+ get {
+ return ResourceManager.GetString("phone_companion_beta", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Asking the root manager for the Companion's report… allow Shell if it asks..
+ ///
+ public static string phone_companion_reading {
+ get {
+ return ResourceManager.GetString("phone_companion_reading", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The Companion is a root module for Magisk, KernelSU or APatch, and this phone has no root. Everything here was read without it..
+ ///
+ public static string phone_companion_no_root {
+ get {
+ return ResourceManager.GetString("phone_companion_no_root", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Not installed on this phone. With it, this page also shows the bootloader's own AVB state, the root manager, storage wear and the conflicts between your modules..
+ ///
+ public static string phone_companion_missing {
+ get {
+ return ResourceManager.GetString("phone_companion_missing", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Get the Companion.
+ ///
+ public static string phone_companion_get {
+ get {
+ return ResourceManager.GetString("phone_companion_get", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Root was refused or not answered. Allow Shell in your root manager, then read again..
+ ///
+ public static string phone_companion_refused {
+ get {
+ return ResourceManager.GetString("phone_companion_refused", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} modules, no conflicts between them.
+ ///
+ public static string phone_conflicts_none {
+ get {
+ return ResourceManager.GetString("phone_conflicts_none", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} conflicts between modules.
+ ///
+ public static string phone_conflicts_some {
+ get {
+ return ResourceManager.GetString("phone_conflicts_some", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Same file: {0}.
+ ///
+ public static string phone_conflict_file {
+ get {
+ return ResourceManager.GetString("phone_conflict_file", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {1} same files: {0}.
+ ///
+ public static string phone_conflict_files {
+ get {
+ return ResourceManager.GetString("phone_conflict_files", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} replaces the folder and hides {1}.
+ ///
+ public static string phone_conflict_replace {
+ get {
+ return ResourceManager.GetString("phone_conflict_replace", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Same property: {0}.
+ ///
+ public static string phone_conflict_prop {
+ get {
+ return ResourceManager.GetString("phone_conflict_prop", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to and .
+ ///
+ public static string phone_and {
+ get {
+ return ResourceManager.GetString("phone_and", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Backups on the phone.
+ ///
+ public static string phone_card_backups {
+ get {
+ return ResourceManager.GetString("phone_card_backups", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} backups · {1}.
+ ///
+ public static string phone_backups_summary {
+ get {
+ return ResourceManager.GetString("phone_backups_summary", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Last: {0}.
+ ///
+ public static string phone_backups_last {
+ get {
+ return ResourceManager.GetString("phone_backups_last", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Open the backups folder on the PC.
+ ///
+ public static string phone_backups_open {
+ get {
+ return ResourceManager.GetString("phone_backups_open", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No backups on the phone yet. The Companion makes them from its Backup tab, into /sdcard/FastbootStudio/Backups..
+ ///
+ public static string phone_backups_empty {
+ get {
+ return ResourceManager.GetString("phone_backups_empty", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copied over adb without root, then every file is checked against SHA256SUMS. A partition can be flashed back from them on the Device page..
+ ///
+ public static string phone_backups_note {
+ get {
+ return ResourceManager.GetString("phone_backups_note", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Backup.
+ ///
+ public static string phone_col_backup {
+ get {
+ return ResourceManager.GetString("phone_col_backup", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Type.
+ ///
+ public static string phone_col_type {
+ get {
+ return ResourceManager.GetString("phone_col_type", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Files.
+ ///
+ public static string phone_col_files {
+ get {
+ return ResourceManager.GetString("phone_col_files", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Size.
+ ///
+ public static string phone_col_size {
+ get {
+ return ResourceManager.GetString("phone_col_size", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Date.
+ ///
+ public static string phone_col_date {
+ get {
+ return ResourceManager.GetString("phone_col_date", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to On the PC.
+ ///
+ public static string phone_col_pc {
+ get {
+ return ResourceManager.GetString("phone_col_pc", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Full.
+ ///
+ public static string phone_set_full {
+ get {
+ return ResourceManager.GetString("phone_set_full", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Critical.
+ ///
+ public static string phone_set_critical {
+ get {
+ return ResourceManager.GetString("phone_set_critical", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Boot.
+ ///
+ public static string phone_set_boot {
+ get {
+ return ResourceManager.GetString("phone_set_boot", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No.
+ ///
+ public static string phone_pc_no {
+ get {
+ return ResourceManager.GetString("phone_pc_no", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copied.
+ ///
+ public static string phone_pc_yes {
+ get {
+ return ResourceManager.GetString("phone_pc_yes", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copying….
+ ///
+ public static string phone_pc_copying {
+ get {
+ return ResourceManager.GetString("phone_pc_copying", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Checking….
+ ///
+ public static string phone_pc_checking {
+ get {
+ return ResourceManager.GetString("phone_pc_checking", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ✓ Copied and matching.
+ ///
+ public static string phone_pc_ok {
+ get {
+ return ResourceManager.GetString("phone_pc_ok", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ✗ {0} of {1} files do not match.
+ ///
+ public static string phone_pc_bad {
+ get {
+ return ResourceManager.GetString("phone_pc_bad", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy failed: {0}.
+ ///
+ public static string phone_pc_failed {
+ get {
+ return ResourceManager.GetString("phone_pc_failed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Copy and verify.
+ ///
+ public static string phone_copy {
+ get {
+ return ResourceManager.GetString("phone_copy", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Open.
+ ///
+ public static string phone_open {
+ get {
+ return ResourceManager.GetString("phone_open", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Restart {0} to {1}?.
+ ///
+ public static string phone_reboot_confirm {
+ get {
+ return ResourceManager.GetString("phone_reboot_confirm", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The phone did not restart: {0}.
+ ///
+ public static string phone_reboot_failed {
+ get {
+ return ResourceManager.GetString("phone_reboot_failed", resourceCulture);
+ }
+ }
}
}
diff --git a/Properties/Resources.ar.resx b/Properties/Resources.ar.resx
index b8aa2ba..f0db04b 100644
--- a/Properties/Resources.ar.resx
+++ b/Properties/Resources.ar.resx
@@ -1960,4 +1960,229 @@ adb shell يحتاج أمر بعده، مثل adb shell getprop. وزر إيقا
معدّلة بـ {0}
+
+ الجوال
+
+
+ الجوال وهو شغّال على أندرويد: مواصفاته وحمايته والنسخ الاحتياطية اللي عليه، والمزيد من Fastboot Studio Companion.
+
+
+ ريكفري
+
+
+ إعادة التشغيل للبوت لودر
+
+
+ اقرأ من جديد
+
+
+ ما فيه جوال شغّال على أندرويد
+
+
+ وصّل الجوال وتصحيح USB شغّال، ووافق على الكمبيوتر لما يسألك الجوال. الجوال اللي في البوت لودر يطلع في صفحة الجهاز.
+
+
+ {0} ينتظر: وافق على تصحيح USB لهذا الكمبيوتر من شاشة الجوال.
+
+
+ جاري قراءة {0}…
+
+
+ الجهاز
+
+
+ الموديل
+
+
+ المعالج
+
+
+ أندرويد
+
+
+ مستوى الحماية
+
+
+ السلوت
+
+
+ البطارية
+
+
+ الذاكرة الداخلية
+
+
+ الكيرنل والحماية
+
+
+ الكيرنل
+
+
+ البوت لودر
+
+
+ الروت
+
+
+ مقفول
+
+
+ مفتوح
+
+
+ ما فيه
+
+
+ يبان مع الإضافة
+
+
+ فيه مود إخفاء يغيّر ro.boot.verifiedbootstate. الحالة هنا من البوت لودر نفسه.
+
+
+ حالة AVB حسب خصائص الجوال، وممكن مود إخفاء يغيّرها. الإضافة تقرأها من البوت لودر نفسه.
+
+
+ صور boot تنفحص على هذا الكيرنل قبل التفليش.
+
+
+ صحة {0}%
+
+
+ {0} دورة
+
+
+ مشحونة {0}%
+
+
+ {0} · استهلاك {1}–{2}%
+
+
+ {0} · تجاوزت عمرها المقدّر
+
+
+ الاحتياطي قارب يخلص
+
+
+ بدون سلوتات A/B
+
+
+ ديناميكي
+
+
+ تجريبي
+
+
+ جاري طلب تقرير الإضافة من مدير الروت… وافق لـ Shell لو سألك.
+
+
+ الإضافة موديول روت لـ Magisk أو KernelSU أو APatch، وهذا الجوال ما فيه روت. كل اللي هنا انقرا بدونها.
+
+
+ مو مثبتة على هذا الجوال. لو ثبتها تعرض الصفحة كمان حالة AVB من البوت لودر نفسه، ومدير الروت، واستهلاك الذاكرة، والتعارض بين المودات.
+
+
+ حمّل الإضافة
+
+
+ الروت انرفض أو ما أحد رد. اسمح لـ Shell في مدير الروت، وبعدين اقرأ من جديد.
+
+
+ {0} مودات وما بينها تعارض
+
+
+ {0} تعارضات بين المودات
+
+
+ نفس الملف: {0}
+
+
+ {1} ملفات مشتركة: {0}
+
+
+ {0} يستبدل المجلد ويخفي {1}
+
+
+ نفس الخاصية: {0}
+
+
+ و
+
+
+ النسخ الاحتياطية على الجوال
+
+
+ {0} نسخ · {1}
+
+
+ آخر نسخة: {0}
+
+
+ افتح مجلد النسخ في الكمبيوتر
+
+
+ ما فيه نسخ على الجوال للحين. الإضافة تسويها من تبويب النسخ الاحتياطي، في /sdcard/FastbootStudio/Backups.
+
+
+ النقل عن طريق adb بدون روت، وبعده يتحقق البرنامج من كل ملف بـ SHA256SUMS. تقدر ترجّع أي بارتشن منها بالتفليش من صفحة الجهاز.
+
+
+ النسخة
+
+
+ النوع
+
+
+ الملفات
+
+
+ الحجم
+
+
+ التاريخ
+
+
+ على الكمبيوتر
+
+
+ كامل
+
+
+ المهمة
+
+
+ الإقلاع
+
+
+ لا
+
+
+ منقولة
+
+
+ جاري النقل…
+
+
+ جاري التحقق…
+
+
+ ✓ منقولة ومطابقة
+
+
+ ✗ {0} من {1} ملفات غير مطابقة
+
+
+ فشل النقل: {0}
+
+
+ انقلها وتحقق
+
+
+ افتح
+
+
+ تعيد تشغيل {0} إلى {1}؟
+
+
+ الجوال ما أعاد التشغيل: {0}
+
diff --git a/Properties/Resources.resx b/Properties/Resources.resx
index 861f09f..31f0b4e 100644
--- a/Properties/Resources.resx
+++ b/Properties/Resources.resx
@@ -1960,4 +1960,229 @@ The bootloader must be unlocked. If the phone does not start afterwards, flash t
patched with {0}
+
+ Phone
+
+
+ The phone while it runs Android: its specs, its security and the backups on it, with more from Fastboot Studio Companion.
+
+
+ Recovery
+
+
+ Restart to bootloader
+
+
+ Read again
+
+
+ No phone in Android
+
+
+ Connect the phone with USB debugging on, and allow this computer when the phone asks. A phone in the bootloader shows on the Device page.
+
+
+ {0} is waiting: allow USB debugging for this computer on the phone's screen.
+
+
+ Reading {0}…
+
+
+ Device
+
+
+ Model
+
+
+ Chip
+
+
+ Android
+
+
+ Security patch
+
+
+ Slot
+
+
+ Battery
+
+
+ Storage
+
+
+ Kernel and security
+
+
+ Kernel
+
+
+ Bootloader
+
+
+ Root
+
+
+ Locked
+
+
+ Unlocked
+
+
+ None found
+
+
+ Known with the Companion
+
+
+ A hiding module changes ro.boot.verifiedbootstate. The state shown here is the bootloader's own.
+
+
+ AVB as the phone's properties say; a hiding module can change them. The Companion reads the bootloader's own.
+
+
+ Boot images are checked against this kernel before flashing.
+
+
+ {0}% health
+
+
+ {0} cycles
+
+
+ {0}% charged
+
+
+ {0} · {1}–{2}% worn
+
+
+ {0} · past its rated life
+
+
+ spare blocks running low
+
+
+ No A/B slots
+
+
+ dynamic
+
+
+ Beta
+
+
+ Asking the root manager for the Companion's report… allow Shell if it asks.
+
+
+ The Companion is a root module for Magisk, KernelSU or APatch, and this phone has no root. Everything here was read without it.
+
+
+ Not installed on this phone. With it, this page also shows the bootloader's own AVB state, the root manager, storage wear and the conflicts between your modules.
+
+
+ Get the Companion
+
+
+ Root was refused or not answered. Allow Shell in your root manager, then read again.
+
+
+ {0} modules, no conflicts between them
+
+
+ {0} conflicts between modules
+
+
+ Same file: {0}
+
+
+ {1} same files: {0}
+
+
+ {0} replaces the folder and hides {1}
+
+
+ Same property: {0}
+
+
+ and
+
+
+ Backups on the phone
+
+
+ {0} backups · {1}
+
+
+ Last: {0}
+
+
+ Open the backups folder on the PC
+
+
+ No backups on the phone yet. The Companion makes them from its Backup tab, into /sdcard/FastbootStudio/Backups.
+
+
+ Copied over adb without root, then every file is checked against SHA256SUMS. A partition can be flashed back from them on the Device page.
+
+
+ Backup
+
+
+ Type
+
+
+ Files
+
+
+ Size
+
+
+ Date
+
+
+ On the PC
+
+
+ Full
+
+
+ Critical
+
+
+ Boot
+
+
+ No
+
+
+ Copied
+
+
+ Copying…
+
+
+ Checking…
+
+
+ ✓ Copied and matching
+
+
+ ✗ {0} of {1} files do not match
+
+
+ Copy failed: {0}
+
+
+ Copy and verify
+
+
+ Open
+
+
+ Restart {0} to {1}?
+
+
+ The phone did not restart: {0}
+
\ No newline at end of file
diff --git a/README.ar.md b/README.ar.md
index cc70f44..2372fca 100644
--- a/README.ar.md
+++ b/README.ar.md
@@ -21,6 +21,10 @@
> 🆕 **نسخة جديدة.** Fastboot Studio هو الإصدار الجديد من Fastboot Enhance: واجهة جديدة بالكامل، وصفحات جديدة، وحماية أكثر للجوال.
>
+> **الجديد في 2.5:**
+> - صفحة **الجوال** للجوال وهو شغّال على أندرويد: المواصفات والكيرنل والحماية والبطارية، ومع إضافة
+> Companion حالة AVB من البوت لودر نفسه وتعارض المودات ونسخها الاحتياطية.
+>
> **الجديد في 2.4:**
> - صفحة **روت**: Magisk أو KernelSU أو APatch في أربع خطوات، وتختار لك الصورة الصحيحة (boot أو init_boot).
> - فحص صور الإقلاع قبل التفليش: الكيرنل والـ KMI ومستوى الحماية والروت، ويوقفك عن الأغلاط اللي تخرّب الجوال.
@@ -107,6 +111,23 @@
**الفائدة:** بدون أوامر ولا أخطاء كتابة في أسماء الأقسام، وتشوف كل معلومات الجوال قبل ما تلمس أي شيء.
+### 📲 الجوال
+
+الجوال وهو شغّال على أندرويد، عن طريق adb: افتح الصفحة ووصّل الجوال.
+
+- **الجهاز:** الموديل والمعالج وأندرويد والـ SDK ومستوى الحماية والسلوت (A/B وديناميكي)،
+ وصحة البطارية وعدد الدورات، واستهلاك الذاكرة الداخلية (eMMC أو UFS).
+- **الكيرنل والحماية:** الكيرنل والـ KMI، البوت لودر مقفول أو مفتوح، SELinux، مدير الروت وحالة AVB.
+- **[Fastboot Studio Companion](https://github.com/ifoknr/FastbootStudio-Companion):** لو الموديول
+ مثبت (Magisk أو KernelSU أو APatch) تسأله الصفحة عن طريق su وتعرض إصداره، وحالة AVB مثل ما
+ قالها **البوت لودر** نفسه (وتنبهك لو فيه مود إخفاء يغيّر الخاصية)، وكل **تعارض بين المودات**:
+ نفس الملف من مودين، مجلد يستبدله مود ويخفيه عن مود ثاني، خاصية متكررة بقيمتين.
+ وبدون الموديول تقول لك وش بيضيف وتعطيك رابطه.
+- **النسخ الاحتياطية على الجوال:** نسخ الإضافة في `/sdcard/FastbootStudio/Backups` بنوعها
+ (كامل، المهمة، الإقلاع) وحجمها وتاريخها. زر **انقلها وتحقق** ينقلها للكمبيوتر (بدون روت)
+ ويتحقق من كل ملف بـ `SHA256SUMS`.
+- إعادة التشغيل لـ **البوت لودر** أو **fastbootd** أو **الريكفري** من أعلى الصفحة.
+
### ⚡ التفليش
diff --git a/README.md b/README.md
index 1a6c26a..32a66c0 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,10 @@ nothing to install.
> 🆕 **A new version.** Fastboot Studio is the new generation of Fastboot Enhance: a new
> interface from top to bottom, new pages, and more protection for the phone.
>
+> **New in 2.5:**
+> - A **Phone** page for the phone in Android: specs, kernel, security and battery; with the
+> Companion module, the bootloader's own AVB state, module conflicts and its backups.
+>
> **New in 2.4:**
> - A **Root** page: Magisk, KernelSU or APatch in four steps, with the right image (boot or init_boot) picked for you.
> - Boot images are read before flashing: kernel, KMI, security patch and root, with a stop on the usual bricks.
@@ -122,6 +126,25 @@ phone reports before you touch anything.
+### 📲 Phone
+
+The phone while it runs Android, over adb: open the page and plug it in.
+
+- **Device:** model, chip, Android and SDK, security patch, slot (A/B, dynamic), battery
+ health and cycles, and storage wear (eMMC or UFS).
+- **Kernel and security:** kernel and KMI, bootloader locked or unlocked, SELinux, root
+ manager and the AVB state.
+- **[Fastboot Studio Companion](https://github.com/ifoknr/FastbootStudio-Companion):** when the
+ module is installed (Magisk, KernelSU or APatch), the page asks it through su and shows its
+ version, the AVB state as the **bootloader** reported it (a hiding module that changes the
+ property is pointed out), and every **conflict between your modules**: the same file from
+ two modules, a folder one module replaces and hides from another, a property set twice.
+ Without the module the page says what it would add and links to it.
+- **Backups on the phone:** the Companion's backups in `/sdcard/FastbootStudio/Backups`, with
+ their kind (full, critical, boot), size and date. **Copy and verify** brings one to the PC
+ (no root needed) and checks every file against its `SHA256SUMS`.
+- Restart to the **bootloader**, **fastbootd** or **recovery** from the header.
+
### ⚡ Flash
diff --git a/Themes/Styles.xaml b/Themes/Styles.xaml
index 84faf79..c01af82 100644
--- a/Themes/Styles.xaml
+++ b/Themes/Styles.xaml
@@ -8,6 +8,7 @@
M7,2 H17 A2,2 0 0 1 19,4 V20 A2,2 0 0 1 17,22 H7 A2,2 0 0 1 5,20 V4 A2,2 0 0 1 7,2 Z M10,18 H14
+ M7,2 H17 A2,2 0 0 1 19,4 V20 A2,2 0 0 1 17,22 H7 A2,2 0 0 1 5,20 V4 A2,2 0 0 1 7,2 Z M9,7 H15 M9,11 H15 M9,15 H12.5
M13,2 L4,14 H11 L10,22 L19,10 H12 Z
M21,8 L12,3 L3,8 V16 L12,21 L21,16 Z M3,8 L12,13 L21,8 M12,13 V21
M4,5 H20 M4,12 H15 M4,19 H11
@@ -492,7 +493,7 @@
-
+
@@ -539,7 +540,7 @@
RenderOptions.BitmapScalingMode="HighQuality"/>
-
+
File names read left to right; the "empty" label follows the page.
public FlowDirection ImageFlow => Partition.HasImage ? FlowDirection.LeftToRight : Languages.Flow;
}
+
+ ///
+ /// A name and a value on the Phone page. Codes and versions are drawn left to right in the
+ /// mono font; a value with Arabic words in it (a battery or slot line) keeps the page's
+ /// direction and font, so it does not come out backwards.
+ ///
+ sealed class PhoneRow
+ {
+ public PhoneRow(string name, string value)
+ {
+ bool words = false;
+ foreach (char c in value)
+ words |= c >= '\u0600' && c <= '\u06FF';
+ Name = name;
+ Value = value;
+ Flow = words ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
+ Font = (FontFamily)Application.Current.FindResource(words ? "UiFont" : "Mono");
+ }
+
+ public string Name { get; }
+ public string Value { get; }
+ public FlowDirection Flow { get; }
+ public FontFamily Font { get; }
+ }
+
+ /// A conflict between root modules, on the Phone page.
+ sealed class ConflictRow
+ {
+ public ConflictRow(string title, string detail, bool serious)
+ {
+ Title = title;
+ Detail = detail;
+ Brush = Palette.Get(serious ? "Danger" : "Warn");
+ }
+
+ public string Title { get; }
+ public string Detail { get; }
+ public Brush Brush { get; }
+ }
+
+ /// A backup on the phone, and whether it is on this PC. Updated while it is copied.
+ sealed class PhoneBackupRow : INotifyPropertyChanged
+ {
+ string onPc;
+ Brush onPcBrush;
+ bool copied;
+ bool idle = true;
+
+ public PhoneBackupRow(Core.Adb.PhoneBackup backup, string type, Brush typeBrush, string size, string date)
+ {
+ Backup = backup;
+ Type = type;
+ TypeBrush = typeBrush;
+ Size = size;
+ Date = date;
+ }
+
+ public Core.Adb.PhoneBackup Backup { get; }
+ public string Name => Backup.Name;
+ public string Type { get; }
+ public Brush TypeBrush { get; }
+ public int Files => Backup.Files;
+ public string Size { get; }
+ public string Date { get; }
+
+ public string OnPc => onPc;
+ public Brush OnPcBrush => onPcBrush;
+
+ /// The PC copy exists, so the row offers to open it instead of copying.
+ public bool Copied
+ {
+ get { return copied; }
+ set
+ {
+ copied = value;
+ Raise(nameof(CopyVisibility));
+ Raise(nameof(OpenVisibility));
+ }
+ }
+
+ public bool Idle
+ {
+ get { return idle; }
+ set
+ {
+ idle = value;
+ Raise(nameof(Idle));
+ }
+ }
+
+ public Visibility CopyVisibility => copied ? Visibility.Collapsed : Visibility.Visible;
+ public Visibility OpenVisibility => copied ? Visibility.Visible : Visibility.Collapsed;
+
+ public void SetState(string text, string brushKey)
+ {
+ onPc = text;
+ onPcBrush = Palette.Get(brushKey);
+ Raise(nameof(OnPc));
+ Raise(nameof(OnPcBrush));
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ void Raise(string name)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+ }
+ }
}
diff --git a/docs/screenshots/01-payload-basic-properties.png b/docs/screenshots/01-payload-basic-properties.png
index 69c0a14..5941a49 100644
Binary files a/docs/screenshots/01-payload-basic-properties.png and b/docs/screenshots/01-payload-basic-properties.png differ
diff --git a/docs/screenshots/02-payload-partitions.png b/docs/screenshots/02-payload-partitions.png
index 8616585..e583c2e 100644
Binary files a/docs/screenshots/02-payload-partitions.png and b/docs/screenshots/02-payload-partitions.png differ
diff --git a/docs/screenshots/03-payload-dynamic-partition-metadata.png b/docs/screenshots/03-payload-dynamic-partition-metadata.png
index 08a5b4e..9a8462e 100644
Binary files a/docs/screenshots/03-payload-dynamic-partition-metadata.png and b/docs/screenshots/03-payload-dynamic-partition-metadata.png differ
diff --git a/docs/screenshots/04-device-list.png b/docs/screenshots/04-device-list.png
index df69618..d5b1094 100644
Binary files a/docs/screenshots/04-device-list.png and b/docs/screenshots/04-device-list.png differ
diff --git a/docs/screenshots/05-usb-check.png b/docs/screenshots/05-usb-check.png
index 5d92e0b..cd1391c 100644
Binary files a/docs/screenshots/05-usb-check.png and b/docs/screenshots/05-usb-check.png differ
diff --git a/docs/screenshots/06-device-basic-properties.png b/docs/screenshots/06-device-basic-properties.png
index 081499e..3f5227b 100644
Binary files a/docs/screenshots/06-device-basic-properties.png and b/docs/screenshots/06-device-basic-properties.png differ
diff --git a/docs/screenshots/07-device-partitions.png b/docs/screenshots/07-device-partitions.png
index 54b7029..0ab4975 100644
Binary files a/docs/screenshots/07-device-partitions.png and b/docs/screenshots/07-device-partitions.png differ
diff --git a/docs/screenshots/08-confirm-erase.png b/docs/screenshots/08-confirm-erase.png
index 94e5b64..983d785 100644
Binary files a/docs/screenshots/08-confirm-erase.png and b/docs/screenshots/08-confirm-erase.png differ
diff --git a/docs/screenshots/09-create-missing.png b/docs/screenshots/09-create-missing.png
index 17ba47d..8bd0f90 100644
Binary files a/docs/screenshots/09-create-missing.png and b/docs/screenshots/09-create-missing.png differ
diff --git a/docs/screenshots/10-flash.png b/docs/screenshots/10-flash.png
index 3718b2e..70a9a7b 100644
Binary files a/docs/screenshots/10-flash.png and b/docs/screenshots/10-flash.png differ
diff --git a/docs/screenshots/11-slot-switch-blocked.png b/docs/screenshots/11-slot-switch-blocked.png
index d1a88f2..fee5f05 100644
Binary files a/docs/screenshots/11-slot-switch-blocked.png and b/docs/screenshots/11-slot-switch-blocked.png differ
diff --git a/docs/screenshots/12-critical-confirm.png b/docs/screenshots/12-critical-confirm.png
index bcf9036..cf4c07b 100644
Binary files a/docs/screenshots/12-critical-confirm.png and b/docs/screenshots/12-critical-confirm.png differ
diff --git a/docs/screenshots/13-vbmeta-question.png b/docs/screenshots/13-vbmeta-question.png
index 23fb3b3..32e2678 100644
Binary files a/docs/screenshots/13-vbmeta-question.png and b/docs/screenshots/13-vbmeta-question.png differ
diff --git a/docs/screenshots/14-super-space.png b/docs/screenshots/14-super-space.png
index 4a2104a..f617d86 100644
Binary files a/docs/screenshots/14-super-space.png and b/docs/screenshots/14-super-space.png differ
diff --git a/docs/screenshots/15-super-full.png b/docs/screenshots/15-super-full.png
index bb8a1bb..c450f5e 100644
Binary files a/docs/screenshots/15-super-full.png and b/docs/screenshots/15-super-full.png differ
diff --git a/docs/screenshots/15a-boot-check-init-boot.png b/docs/screenshots/15a-boot-check-init-boot.png
index 3f672c8..9f862ba 100644
Binary files a/docs/screenshots/15a-boot-check-init-boot.png and b/docs/screenshots/15a-boot-check-init-boot.png differ
diff --git a/docs/screenshots/15b-boot-check-vendor-boot.png b/docs/screenshots/15b-boot-check-vendor-boot.png
index f44fd18..7cfc3cb 100644
Binary files a/docs/screenshots/15b-boot-check-vendor-boot.png and b/docs/screenshots/15b-boot-check-vendor-boot.png differ
diff --git a/docs/screenshots/16-boot-once-init-boot.png b/docs/screenshots/16-boot-once-init-boot.png
index 479d498..5d7ef9d 100644
Binary files a/docs/screenshots/16-boot-once-init-boot.png and b/docs/screenshots/16-boot-once-init-boot.png differ
diff --git a/docs/screenshots/17-boot-once-started.png b/docs/screenshots/17-boot-once-started.png
index c8094b6..7fd8dee 100644
Binary files a/docs/screenshots/17-boot-once-started.png and b/docs/screenshots/17-boot-once-started.png differ
diff --git a/docs/screenshots/18-backup-fastboot.png b/docs/screenshots/18-backup-fastboot.png
index 40ac93e..15b92fa 100644
Binary files a/docs/screenshots/18-backup-fastboot.png and b/docs/screenshots/18-backup-fastboot.png differ
diff --git a/docs/screenshots/19-backup-fastbootd-question.png b/docs/screenshots/19-backup-fastbootd-question.png
index d128d1b..0bb0889 100644
Binary files a/docs/screenshots/19-backup-fastbootd-question.png and b/docs/screenshots/19-backup-fastbootd-question.png differ
diff --git a/docs/screenshots/20-backup-recovery-booted.png b/docs/screenshots/20-backup-recovery-booted.png
index 9bb88af..708a19f 100644
Binary files a/docs/screenshots/20-backup-recovery-booted.png and b/docs/screenshots/20-backup-recovery-booted.png differ
diff --git a/docs/screenshots/21-backup-files.png b/docs/screenshots/21-backup-files.png
index 7134a57..28606b4 100644
Binary files a/docs/screenshots/21-backup-files.png and b/docs/screenshots/21-backup-files.png differ
diff --git a/docs/screenshots/22-backup-partitions.png b/docs/screenshots/22-backup-partitions.png
index dc8e070..8a6ffc4 100644
Binary files a/docs/screenshots/22-backup-partitions.png and b/docs/screenshots/22-backup-partitions.png differ
diff --git a/docs/screenshots/23-backup-done.png b/docs/screenshots/23-backup-done.png
index 2792583..42a7282 100644
Binary files a/docs/screenshots/23-backup-done.png and b/docs/screenshots/23-backup-done.png differ
diff --git a/docs/screenshots/24-images-empty.png b/docs/screenshots/24-images-empty.png
index 91a6e27..a491d5c 100644
Binary files a/docs/screenshots/24-images-empty.png and b/docs/screenshots/24-images-empty.png differ
diff --git a/docs/screenshots/25-images-super.png b/docs/screenshots/25-images-super.png
index 0813838..49734f5 100644
Binary files a/docs/screenshots/25-images-super.png and b/docs/screenshots/25-images-super.png differ
diff --git a/docs/screenshots/26-images-extracted.png b/docs/screenshots/26-images-extracted.png
index 25e716d..4901116 100644
Binary files a/docs/screenshots/26-images-extracted.png and b/docs/screenshots/26-images-extracted.png differ
diff --git a/docs/screenshots/27-images-details.png b/docs/screenshots/27-images-details.png
index 7753a3c..aa03767 100644
Binary files a/docs/screenshots/27-images-details.png and b/docs/screenshots/27-images-details.png differ
diff --git a/docs/screenshots/28-images-qualcomm-pieces.png b/docs/screenshots/28-images-qualcomm-pieces.png
index 26bb3de..0aed1da 100644
Binary files a/docs/screenshots/28-images-qualcomm-pieces.png and b/docs/screenshots/28-images-qualcomm-pieces.png differ
diff --git a/docs/screenshots/28a-images-boot.png b/docs/screenshots/28a-images-boot.png
index 845ed1b..af15c2d 100644
Binary files a/docs/screenshots/28a-images-boot.png and b/docs/screenshots/28a-images-boot.png differ
diff --git a/docs/screenshots/29-super-empty.png b/docs/screenshots/29-super-empty.png
index 7517328..ad23486 100644
Binary files a/docs/screenshots/29-super-empty.png and b/docs/screenshots/29-super-empty.png differ
diff --git a/docs/screenshots/30-super-imported.png b/docs/screenshots/30-super-imported.png
index 58a21c9..0d720b9 100644
Binary files a/docs/screenshots/30-super-imported.png and b/docs/screenshots/30-super-imported.png differ
diff --git a/docs/screenshots/31-super-built.png b/docs/screenshots/31-super-built.png
index 6c8f105..ef792b2 100644
Binary files a/docs/screenshots/31-super-built.png and b/docs/screenshots/31-super-built.png differ
diff --git a/docs/screenshots/32-root-patched.png b/docs/screenshots/32-root-patched.png
index b63c649..3b97b54 100644
Binary files a/docs/screenshots/32-root-patched.png and b/docs/screenshots/32-root-patched.png differ
diff --git a/docs/screenshots/33-root-flashed.png b/docs/screenshots/33-root-flashed.png
index ef12f7b..fc2fdfb 100644
Binary files a/docs/screenshots/33-root-flashed.png and b/docs/screenshots/33-root-flashed.png differ
diff --git a/docs/screenshots/33a-phone.png b/docs/screenshots/33a-phone.png
new file mode 100644
index 0000000..9cb7fc1
Binary files /dev/null and b/docs/screenshots/33a-phone.png differ
diff --git a/docs/screenshots/33b-phone-copied.png b/docs/screenshots/33b-phone-copied.png
new file mode 100644
index 0000000..fcc691c
Binary files /dev/null and b/docs/screenshots/33b-phone-copied.png differ
diff --git a/docs/screenshots/33c-phone-no-companion.png b/docs/screenshots/33c-phone-no-companion.png
new file mode 100644
index 0000000..e6a6a4a
Binary files /dev/null and b/docs/screenshots/33c-phone-no-companion.png differ
diff --git a/docs/screenshots/34-terminal.png b/docs/screenshots/34-terminal.png
index 774aff5..d8b22f7 100644
Binary files a/docs/screenshots/34-terminal.png and b/docs/screenshots/34-terminal.png differ
diff --git a/docs/screenshots/35-logs.png b/docs/screenshots/35-logs.png
index 6dcd3e5..3f2ebf0 100644
Binary files a/docs/screenshots/35-logs.png and b/docs/screenshots/35-logs.png differ
diff --git a/docs/screenshots/36-about.png b/docs/screenshots/36-about.png
index 1efe78f..42c2f31 100644
Binary files a/docs/screenshots/36-about.png and b/docs/screenshots/36-about.png differ
diff --git a/docs/screenshots/ar-01-payload.png b/docs/screenshots/ar-01-payload.png
index f368b7d..c7983ff 100644
Binary files a/docs/screenshots/ar-01-payload.png and b/docs/screenshots/ar-01-payload.png differ
diff --git a/docs/screenshots/ar-02-device.png b/docs/screenshots/ar-02-device.png
index f75c3d3..16d62cb 100644
Binary files a/docs/screenshots/ar-02-device.png and b/docs/screenshots/ar-02-device.png differ
diff --git a/docs/screenshots/ar-03-flash.png b/docs/screenshots/ar-03-flash.png
index c2205d6..6ab0ae9 100644
Binary files a/docs/screenshots/ar-03-flash.png and b/docs/screenshots/ar-03-flash.png differ
diff --git a/docs/screenshots/ar-04-backup-files.png b/docs/screenshots/ar-04-backup-files.png
index a05f9fd..45c133a 100644
Binary files a/docs/screenshots/ar-04-backup-files.png and b/docs/screenshots/ar-04-backup-files.png differ
diff --git a/docs/screenshots/ar-05-backup-partitions.png b/docs/screenshots/ar-05-backup-partitions.png
index bc77763..73e1ef8 100644
Binary files a/docs/screenshots/ar-05-backup-partitions.png and b/docs/screenshots/ar-05-backup-partitions.png differ
diff --git a/docs/screenshots/ar-06-about.png b/docs/screenshots/ar-06-about.png
index c4bff35..d4c184d 100644
Binary files a/docs/screenshots/ar-06-about.png and b/docs/screenshots/ar-06-about.png differ
diff --git a/docs/screenshots/ar-07-dialog.png b/docs/screenshots/ar-07-dialog.png
index 881e38f..fe64cbd 100644
Binary files a/docs/screenshots/ar-07-dialog.png and b/docs/screenshots/ar-07-dialog.png differ
diff --git a/docs/screenshots/ar-08-images-super.png b/docs/screenshots/ar-08-images-super.png
index 67a3c4f..3221a7b 100644
Binary files a/docs/screenshots/ar-08-images-super.png and b/docs/screenshots/ar-08-images-super.png differ
diff --git a/docs/screenshots/ar-09-backup-fastboot.png b/docs/screenshots/ar-09-backup-fastboot.png
index 229bcac..9b29ef4 100644
Binary files a/docs/screenshots/ar-09-backup-fastboot.png and b/docs/screenshots/ar-09-backup-fastboot.png differ
diff --git a/docs/screenshots/ar-10-super.png b/docs/screenshots/ar-10-super.png
index e0845fd..e995293 100644
Binary files a/docs/screenshots/ar-10-super.png and b/docs/screenshots/ar-10-super.png differ
diff --git a/docs/screenshots/ar-11-phone.png b/docs/screenshots/ar-11-phone.png
new file mode 100644
index 0000000..8340f22
Binary files /dev/null and b/docs/screenshots/ar-11-phone.png differ
diff --git a/docs/screenshots/light-01-payload.png b/docs/screenshots/light-01-payload.png
index f9dc24d..09999fe 100644
Binary files a/docs/screenshots/light-01-payload.png and b/docs/screenshots/light-01-payload.png differ
diff --git a/docs/screenshots/light-02-device.png b/docs/screenshots/light-02-device.png
index cf078a7..145b5ee 100644
Binary files a/docs/screenshots/light-02-device.png and b/docs/screenshots/light-02-device.png differ
diff --git a/docs/screenshots/light-03-flash.png b/docs/screenshots/light-03-flash.png
index 782ac5e..da97bc9 100644
Binary files a/docs/screenshots/light-03-flash.png and b/docs/screenshots/light-03-flash.png differ
diff --git a/docs/screenshots/light-04-terminal.png b/docs/screenshots/light-04-terminal.png
index 98d24d4..a5c0831 100644
Binary files a/docs/screenshots/light-04-terminal.png and b/docs/screenshots/light-04-terminal.png differ
diff --git a/docs/screenshots/light-05-about.png b/docs/screenshots/light-05-about.png
index 0fe93c0..56e2ad6 100644
Binary files a/docs/screenshots/light-05-about.png and b/docs/screenshots/light-05-about.png differ
diff --git a/docs/screenshots/light-06-phone.png b/docs/screenshots/light-06-phone.png
new file mode 100644
index 0000000..73f7aec
Binary files /dev/null and b/docs/screenshots/light-06-phone.png differ
diff --git a/packaging/xda/thread.bbcode b/packaging/xda/thread.bbcode
index d402e37..7cf32dd 100644
--- a/packaging/xda/thread.bbcode
+++ b/packaging/xda/thread.bbcode
@@ -1,11 +1,12 @@
[CENTER][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/banner.png[/IMG]
[SIZE=6][B]Fastboot Studio[/B][/SIZE]
-[SIZE=4]fastboot, OTA flashing, rooting, payload dumper, super images, backups and a terminal — in one Windows app[/SIZE]
+[SIZE=4]fastboot, OTA flashing, rooting, a phone page, payload dumper, super images, backups and a terminal — in one Windows app[/SIZE]
[URL='https://github.com/ifoknr/FastbootStudio/releases/latest'][B]⬇ Download[/B][/URL] · [URL='https://github.com/ifoknr/FastbootStudio']Source (GitHub)[/URL] · [URL='https://github.com/ifoknr/FastbootStudio/blob/master/CHANGELOG.md']Changelog[/URL] · [URL='https://t.me/IFOKNR1']Telegram[/URL]
-[B]New in v2.4.0:[/B] a Root page (Magisk, KernelSU, APatch in four steps) and a boot image check that stops the usual bricks[/CENTER]
+[B]New in v2.5.0:[/B] a Phone page — your phone in Android, its security and its backups, with the Companion root module
+[B]v2.4.0:[/B] a Root page (Magisk, KernelSU, APatch in four steps) and a boot image check that stops the usual bricks[/CENTER]
[SIZE=5][B]What is it?[/B][/SIZE]
Fastboot Studio is a free, open-source (MIT) rewrite of [URL='https://github.com/libxzr/FastbootEnhance']Fastboot Enhance[/URL] by LibXZR. It keeps everything the original did and adds what people who flash phones reach for every day: a safe OTA flasher, image tools that replace simg2img / lpunpack / lpmake, a super image builder, and backups of partitions and files over adb.
@@ -22,10 +23,20 @@ Everything runs inside the app: no Python, no Linux tools, nothing to install. a
[*][B]Boot once[/B] (fastboot boot) — refuses init_boot and vendor_boot images, which cannot start on their own, and goes through the bootloader when you are in fastbootd
[*][B]Super space[/B] — what each slot and the COW partitions take in super, and how much is free
[*][B]Boot image check[/B] — before boot, init_boot, recovery or vendor_boot is flashed, the image is unpacked (gzip, lz4, zstd, xz) and read: kernel version and KMI, security patch, and root (Magisk, KernelSU, APatch, SuSFS). An init_boot image into boot, a boot image into vendor_boot or a file that is not a boot image needs the partition name typed; a kernel built for another KMI than the phone runs (a boot-loop) or an older security patch is flagged first
-[*][B]Phone in Android[/B] — the device page notices it over adb and notes its kernel and security patch for the checks above. With the [URL='https://github.com/ifoknr/FastbootStudio-Companion']Fastboot Studio Companion[/URL] root module it also shows the root manager, the real AVB state and conflicts between root modules
+[*][B]Phone in Android[/B] — the device page notices it over adb and notes its kernel and security patch for the checks above (more on the Phone page below)
[*][B]USB check[/B] — phone plugged in but not listed? It tells you why: a missing or broken driver (with the steps to fix it), Qualcomm EDL (9008), MediaTek BROM/preloader, Unisoc or Samsung download mode, or a phone still in Android (and reboots it to the bootloader)
[/LIST]
+[B]📲 Phone[/B]
+The phone while it runs Android, over adb — open the page and plug it in.
+[LIST]
+[*][B]Device[/B] — model, chip, Android and SDK, security patch, slot (A/B, dynamic), battery health and cycles, storage wear (eMMC / UFS)
+[*][B]Kernel and security[/B] — kernel and KMI, bootloader locked or unlocked, SELinux, root manager, AVB state
+[*][B]With the [URL='https://github.com/ifoknr/FastbootStudio-Companion']Fastboot Studio Companion[/URL] root module[/B] (Magisk, KernelSU, APatch) — the page recognises it and asks it through su: the AVB state as the [B]bootloader[/B] reported it (and a warning when a hiding module changed the property), the root manager, storage wear, and every [B]conflict between your modules[/B]: the same file from two modules, a folder one module replaces and hides from another, a property set twice. Without the module it tells you what it adds and links to it
+[*][B]Backups on the phone[/B] — the Companion's backups with their kind (full, critical, boot), size and date; [B]Copy and verify[/B] brings one to the PC without root and checks every file against its SHA256SUMS
+[*]Restart to bootloader, fastbootd or recovery from the page
+[/LIST]
+
[B]⚡ Flash[/B]
[LIST]
[*]Flashes a full OTA (payload.bin or the OTA zip) in fastbootd
@@ -108,6 +119,8 @@ The stock image is kept in Documents\Fastboot Studio\Root, to undo it.
[SPOILER="Flash"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/10-flash.png[/IMG]
[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/14-super-space.png[/IMG]
[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/15a-boot-check-init-boot.png[/IMG][/SPOILER]
+[SPOILER="Phone"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/33a-phone.png[/IMG]
+[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/33b-phone-copied.png[/IMG][/SPOILER]
[SPOILER="Root"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/32-root-patched.png[/IMG][/SPOILER]
[SPOILER="Payload Dumper"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/02-payload-partitions.png[/IMG][/SPOILER]
[SPOILER="Image Tools"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/25-images-super.png[/IMG]
@@ -120,7 +133,8 @@ The stock image is kept in Documents\Fastboot Studio\Root, to undo it.
[SPOILER="Light theme"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/light-02-device.png[/IMG]
[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/light-04-terminal.png[/IMG][/SPOILER]
[SPOILER="Arabic interface"][IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/ar-02-device.png[/IMG]
-[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/ar-03-flash.png[/IMG][/SPOILER]
+[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/ar-03-flash.png[/IMG]
+[IMG]https://raw.githubusercontent.com/ifoknr/FastbootStudio/master/docs/screenshots/ar-11-phone.png[/IMG][/SPOILER]
[SIZE=5][B]Credits[/B][/SIZE]
[LIST]
diff --git a/src/FastbootEnhance.Core/Adb/PhoneReport.cs b/src/FastbootEnhance.Core/Adb/PhoneReport.cs
new file mode 100644
index 0000000..0ef8dda
--- /dev/null
+++ b/src/FastbootEnhance.Core/Adb/PhoneReport.cs
@@ -0,0 +1,638 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json;
+using FastbootEnhance.Core.Fastboot;
+using FastbootEnhance.Core.Images;
+
+namespace FastbootEnhance.Core.Adb
+{
+ /// What the Phone page could learn from the Fastboot Studio Companion module.
+ public enum CompanionState
+ {
+ /// The phone has no su, so the module (under /data/adb) cannot be reached.
+ NoRoot,
+
+ /// su said no, or nobody answered its prompt in time.
+ Refused,
+
+ /// Root works, and the module is not installed.
+ NotInstalled,
+
+ Ready,
+ }
+
+ /// Which partitions a backup holds, guessed from its file names.
+ public enum BackupSet
+ {
+ /// Boot images only: boot, init_boot, vendor_boot, dtbo, vbmeta, recovery.
+ Boot,
+
+ /// The partitions that can brick the phone or lose its identity (PartitionSafety).
+ Critical,
+
+ /// Everything else as well.
+ Full,
+ }
+
+ public enum ConflictKind
+ {
+ /// Two or more modules put the same file in place; only one copy is used.
+ File,
+
+ /// A module replaces a whole folder, hiding what other modules add there.
+ Replace,
+
+ /// Modules set the same system property to different values; the last one wins.
+ Prop,
+ }
+
+ /// One conflict between root modules, as "fbs modules" reports it.
+ public sealed class ModuleConflict
+ {
+ public ConflictKind Kind { get; internal set; }
+
+ /// The modules involved, by name (their id when they have no name).
+ public List Modules { get; } = new List();
+
+ /// File: the first shared paths. Replace: the replaced folder.
+ public List Paths { get; } = new List();
+
+ /// File: how many paths are shared in all (Paths holds at most 12).
+ public int Count { get; internal set; }
+
+ /// Replace: the module that replaces the folder.
+ public string By { get; internal set; }
+
+ /// Prop: the property.
+ public string Key { get; internal set; }
+
+ /// Prop: each module's value, in the module's name.
+ public List> Values { get; } = new List>();
+ }
+
+ /// A backup the Companion made, in /sdcard/FastbootStudio/Backups.
+ public sealed class PhoneBackup
+ {
+ /// The folder name, the same as the Backup page uses: model_serial_yyyyMMdd-HHmmss.
+ public string Name { get; internal set; }
+
+ /// Lines in SHA256SUMS: the images that were saved.
+ public int Files { get; internal set; }
+
+ public long? Kb { get; internal set; }
+
+ /// From the end of the name, in the phone's local time.
+ public DateTime? Taken { get; internal set; }
+
+ public List Images { get; } = new List();
+
+ public BackupSet Set { get; internal set; }
+
+ public string Path => PhoneReport.BackupsFolder + "/" + Name;
+ }
+
+ ///
+ /// The phone as the Phone page shows it. A first adb shell command needs no root and reads
+ /// the properties, the kernel, the battery and the backups folder; the Companion module,
+ /// reached through su, adds the rest (the bootloader's own AVB state, the root manager,
+ /// storage wear and the conflicts between modules).
+ ///
+ public sealed class PhoneReport
+ {
+ public const string BackupsFolder = "/sdcard/FastbootStudio/Backups";
+ public const string CompanionPath = "/data/adb/modules/fastboot_studio_companion/bin/fbs";
+
+ /// key=value lines, then one "backup=name|files|kb|image image …" line per backup.
+ public const string Command =
+ "g() { echo \"$1=$(getprop $2)\"; }; " +
+ "g model ro.product.model; g brand ro.product.brand; g device ro.product.device; " +
+ "g android ro.build.version.release; g sdk ro.build.version.sdk; g patch ro.build.version.security_patch; " +
+ "g platform ro.board.platform; g hardware ro.hardware; g soc_maker ro.soc.manufacturer; g soc_model ro.soc.model; " +
+ "g arch ro.product.cpu.abi; g slot ro.boot.slot_suffix; g dynamic ro.boot.dynamic_partitions; " +
+ "g avb ro.boot.verifiedbootstate; g device_state ro.boot.vbmeta.device_state; g locked ro.boot.flash.locked; " +
+ "echo \"kernel=$(uname -r)\"; echo \"selinux=$(getenforce 2>/dev/null)\"; " +
+ "b=/sys/class/power_supply/battery; " +
+ "for k in capacity cycle_count charge_full charge_full_design; do echo \"battery_$k=$(cat $b/$k 2>/dev/null)\"; done; " +
+ "for d in " + BackupsFolder + "/*/; do [ -d \"$d\" ] || continue; n=${d%/}; n=${n##*/}; " +
+ "f=$(cat \"${d}SHA256SUMS\" 2>/dev/null | wc -l); k=$(du -sk \"$d\" 2>/dev/null); " +
+ "echo \"backup=$n|$f|${k%%[!0-9]*}|$(ls \"$d\" | tr '\\n' ' ')\"; done";
+
+ ///
+ /// Through su: the Companion's info and modules reports, separated by a line "@@", or
+ /// "fbs-missing" without the module. No single quotes, so AdbCommand.AsRoot can wrap it.
+ ///
+ public const string CompanionCommand =
+ "f=" + CompanionPath + "; if [ -f $f ]; then sh $f info; echo @@; sh $f modules; else echo fbs-missing; fi";
+
+ public const string CompanionMissing = "fbs-missing";
+
+ // ---------------------------------------------------------------- the phone
+
+ public string Model { get; private set; }
+ public string Brand { get; private set; }
+ public string Device { get; private set; }
+ public string Android { get; private set; }
+ public string Sdk { get; private set; }
+ public string Patch { get; private set; }
+ public string Platform { get; private set; }
+
+ /// MediaTek, Qualcomm, Unisoc, Samsung, Google, or null.
+ public string Soc { get; private set; }
+
+ public string Arch { get; private set; }
+
+ /// "_a" or "_b"; null on a phone without A/B slots.
+ public string Slot { get; private set; }
+
+ public bool? Dynamic { get; private set; }
+
+ // ---------------------------------------------------------------- kernel and security
+
+ public string Kernel { get; private set; }
+ public string Kmi { get; private set; }
+ public string Selinux { get; private set; }
+
+ /// green, yellow or orange. From the Companion, it is the bootloader's own word.
+ public string Avb { get; private set; }
+
+ /// locked or unlocked.
+ public string DeviceState { get; private set; }
+
+ /// The Companion found ro.boot.verifiedbootstate saying something else than the bootloader.
+ public bool Spoofed { get; private set; }
+
+ /// KernelSU, Magisk or APatch, from the Companion.
+ public string Root { get; private set; }
+
+ // ---------------------------------------------------------------- health
+
+ public int? BatteryLevel { get; private set; }
+
+ /// Full charge now against the design capacity, in percent.
+ public int? BatteryHealth { get; private set; }
+
+ public int? BatteryCycles { get; private set; }
+
+ /// ufs or emmc, from the Companion.
+ public string StorageType { get; private set; }
+
+ /// The worst of the two wear estimates, 1 (0-10% used) to 11 (past its rated life).
+ public int? StorageWear { get; private set; }
+
+ /// Pre-EOL: 1 normal, 2 warning (spare blocks running low), 3 urgent.
+ public int? StorageEol { get; private set; }
+
+ // ---------------------------------------------------------------- the Companion
+
+ public CompanionState Companion { get; private set; } = CompanionState.NoRoot;
+
+ /// "2.4.0".
+ public string CompanionVersion { get; private set; }
+
+ /// beta or stable.
+ public string CompanionChannel { get; private set; }
+
+ /// Installed modules, enabled or not; null when the Companion was not read.
+ public int? ModuleCount { get; private set; }
+
+ public List Conflicts { get; } = new List();
+
+ public List Backups { get; } = new List();
+
+ public long BackupsKb => Backups.Sum(b => b.Kb ?? 0);
+
+ /// Reads the output of .
+ public static PhoneReport Parse(string output)
+ {
+ PhoneReport report = new PhoneReport();
+ Dictionary values = new Dictionary();
+ foreach (string raw in (output ?? "").Split('\n'))
+ {
+ string line = raw.TrimEnd('\r');
+ int eq = line.IndexOf('=');
+ if (eq <= 0)
+ continue;
+ string key = line.Substring(0, eq).Trim();
+ string value = line.Substring(eq + 1).Trim();
+ if (key == "backup")
+ {
+ PhoneBackup backup = ParseBackup(value);
+ if (backup != null)
+ report.Backups.Add(backup);
+ }
+ else if (value.Length > 0)
+ {
+ values[key] = value;
+ }
+ }
+
+ report.Model = Get(values, "model");
+ report.Brand = Get(values, "brand");
+ report.Device = Get(values, "device");
+ report.Android = Get(values, "android");
+ report.Sdk = Get(values, "sdk");
+ report.Patch = Get(values, "patch");
+ report.Platform = Get(values, "platform");
+ report.Soc = SocVendor(Get(values, "soc_maker"), Get(values, "platform"), Get(values, "hardware"));
+ report.Arch = Get(values, "arch");
+ report.Slot = Get(values, "slot");
+ report.Dynamic = Bool(Get(values, "dynamic"));
+ report.Avb = Get(values, "avb");
+ report.DeviceState = Get(values, "device_state");
+ if (report.DeviceState == null)
+ {
+ bool? locked = Bool(Get(values, "locked"));
+ if (locked != null)
+ report.DeviceState = locked.Value ? "locked" : "unlocked";
+ }
+ report.SetKernel(Get(values, "kernel"));
+ report.Selinux = Get(values, "selinux");
+ report.BatteryLevel = Int(Get(values, "battery_capacity"));
+ report.BatteryCycles = Int(Get(values, "battery_cycle_count"));
+ report.BatteryHealth = Health(Long(Get(values, "battery_charge_full")), Long(Get(values, "battery_charge_full_design")));
+
+ report.Backups.Sort((x, y) => Nullable.Compare(y.Taken, x.Taken));
+ return report;
+ }
+
+ ///
+ /// Reads the output of . hasSu is whether the phone has su
+ /// at all; without it the command is not run and output is ignored.
+ ///
+ public void ApplyCompanion(bool hasSu, string output)
+ {
+ if (!hasSu)
+ {
+ Companion = CompanionState.NoRoot;
+ return;
+ }
+ output = output ?? "";
+ if (output.Split('\n').Any(l => l.Trim() == CompanionMissing))
+ {
+ Companion = CompanionState.NotInstalled;
+ return;
+ }
+
+ int split = output.IndexOf("\n@@", StringComparison.Ordinal);
+ string info = split >= 0 ? output.Substring(0, split) : output;
+ string modules = split >= 0 ? output.Substring(split + 3) : null;
+ try
+ {
+ int start = info.IndexOf("{\"fbs\"", StringComparison.Ordinal);
+ if (start < 0)
+ {
+ Companion = CompanionState.Refused;
+ return;
+ }
+ using (JsonDocument doc = JsonDocument.Parse(info.Substring(start).Trim()))
+ ApplyInfo(doc.RootElement);
+ Companion = CompanionState.Ready;
+ }
+ catch (JsonException)
+ {
+ Companion = CompanionState.Refused;
+ return;
+ }
+
+ if (modules == null)
+ return;
+ try
+ {
+ int start = modules.IndexOf('{');
+ if (start >= 0)
+ {
+ using (JsonDocument doc = JsonDocument.Parse(modules.Substring(start).Trim()))
+ ApplyModules(doc.RootElement);
+ }
+ }
+ catch (JsonException)
+ {
+ // A module list the app cannot read leaves the rest of the report as it is.
+ }
+ }
+
+ void ApplyInfo(JsonElement info)
+ {
+ CompanionVersion = Str(info, "fbs");
+ CompanionChannel = Str(info, "channel");
+ Model = Str(info, "model") ?? Model;
+ Brand = Str(info, "brand") ?? Brand;
+ Device = Str(info, "device") ?? Device;
+ Android = Str(info, "android") ?? Android;
+ Sdk = Str(info, "sdk") ?? Sdk;
+ Patch = Str(info, "patch") ?? Patch;
+ Platform = Str(info, "platform") ?? Platform;
+ string soc = Str(info, "soc");
+ if (soc != null && soc != "unknown")
+ Soc = SocName(soc);
+ Arch = Str(info, "arch") ?? Arch;
+ Slot = Str(info, "slot") ?? Slot;
+ Dynamic = BoolOf(info, "dynamic") ?? Dynamic;
+ // The KMI comes from the kernel, written as the rest of the app does ("android13-5.15").
+ SetKernel(Str(info, "kernel") ?? Kernel);
+ Selinux = Str(info, "selinux") ?? Selinux;
+ string root = Str(info, "root");
+ Root = root == "unknown" ? null : root;
+
+ JsonElement boot;
+ if (info.TryGetProperty("boot", out boot) && boot.ValueKind == JsonValueKind.Object)
+ {
+ // The bootloader's own words: a hiding module may have changed the properties.
+ Avb = Str(boot, "vbstate") ?? Avb;
+ string state = Str(boot, "device_state");
+ bool? locked = BoolOf(boot, "locked");
+ if (state != null)
+ DeviceState = state;
+ else if (locked != null)
+ DeviceState = locked.Value ? "locked" : "unlocked";
+ Spoofed = BoolOf(boot, "spoofed") ?? false;
+ }
+
+ JsonElement battery;
+ if (info.TryGetProperty("battery", out battery) && battery.ValueKind == JsonValueKind.Object)
+ {
+ BatteryLevel = IntOf(battery, "level") ?? BatteryLevel;
+ BatteryHealth = IntOf(battery, "health") ?? BatteryHealth;
+ BatteryCycles = IntOf(battery, "cycles") ?? BatteryCycles;
+ }
+
+ JsonElement storage;
+ if (info.TryGetProperty("storage", out storage) && storage.ValueKind == JsonValueKind.Object)
+ {
+ StorageType = Str(storage, "type");
+ int? a = Hex(Str(storage, "life_a")), b = Hex(Str(storage, "life_b"));
+ // 0 means "not defined"; the worse of the two is what matters.
+ int? wear = new[] { a, b }.Where(v => v > 0).Max();
+ StorageWear = wear;
+ int? eol = Hex(Str(storage, "eol"));
+ StorageEol = eol > 0 ? eol : null;
+ }
+ }
+
+ void ApplyModules(JsonElement root)
+ {
+ Dictionary names = new Dictionary();
+ JsonElement modules;
+ if (root.TryGetProperty("modules", out modules) && modules.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement module in modules.EnumerateArray())
+ {
+ string id = Str(module, "id");
+ if (id != null)
+ names[id] = Str(module, "name") ?? id;
+ }
+ ModuleCount = names.Count;
+ }
+ string Name(string id) => id != null && names.TryGetValue(id, out string name) ? name : id;
+
+ JsonElement conflicts;
+ if (!root.TryGetProperty("conflicts", out conflicts) || conflicts.ValueKind != JsonValueKind.Array)
+ return;
+ foreach (JsonElement item in conflicts.EnumerateArray())
+ {
+ ModuleConflict conflict = new ModuleConflict();
+ switch (Str(item, "kind"))
+ {
+ case "file":
+ conflict.Kind = ConflictKind.File;
+ conflict.Modules.AddRange(Strings(item, "modules").Select(Name));
+ conflict.Paths.AddRange(Strings(item, "paths"));
+ conflict.Count = IntOf(item, "count") ?? conflict.Paths.Count;
+ break;
+ case "replace":
+ conflict.Kind = ConflictKind.Replace;
+ conflict.By = Name(Str(item, "by"));
+ conflict.Modules.AddRange(Strings(item, "modules").Select(Name));
+ string path = Str(item, "path");
+ if (path != null)
+ conflict.Paths.Add(path);
+ conflict.Count = conflict.Paths.Count;
+ break;
+ case "prop":
+ conflict.Kind = ConflictKind.Prop;
+ conflict.Key = Str(item, "key");
+ JsonElement values;
+ if (item.TryGetProperty("values", out values) && values.ValueKind == JsonValueKind.Array)
+ {
+ foreach (JsonElement value in values.EnumerateArray())
+ {
+ string module = Name(Str(value, "module"));
+ conflict.Values.Add(new KeyValuePair(module, Str(value, "value") ?? ""));
+ if (module != null && !conflict.Modules.Contains(module))
+ conflict.Modules.Add(module);
+ }
+ }
+ break;
+ default:
+ continue;
+ }
+ Conflicts.Add(conflict);
+ }
+ }
+
+ void SetKernel(string release)
+ {
+ Kernel = release;
+ Kmi = release != null ? KernelInfo.FromRelease(release)?.Kmi : null;
+ }
+
+ // ---------------------------------------------------------------- backups
+
+ /// "name|files|kb|boot_a.img SHA256SUMS backup-info.txt …".
+ static PhoneBackup ParseBackup(string value)
+ {
+ string[] parts = value.Split(new[] { '|' }, 4);
+ if (parts.Length < 3 || parts[0].Length == 0 || parts[0].IndexOf('/') >= 0 || parts[0].StartsWith(".", StringComparison.Ordinal))
+ return null;
+ PhoneBackup backup = new PhoneBackup
+ {
+ Name = parts[0],
+ Files = Int(parts[1]) ?? 0,
+ Kb = Long(parts[2]),
+ Taken = DateFromName(parts[0]),
+ };
+ if (parts.Length > 3)
+ {
+ foreach (string file in parts[3].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (file.EndsWith(".img", StringComparison.OrdinalIgnoreCase))
+ backup.Images.Add(file.Substring(0, file.Length - 4));
+ }
+ }
+ backup.Set = SetOf(backup.Images);
+ return backup;
+ }
+
+ static readonly HashSet BootSet = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "boot", "init_boot", "vendor_boot", "vendor_kernel_boot", "dtbo", "recovery", "vbmeta", "vbmeta_system", "vbmeta_vendor",
+ };
+
+ /// The Companion's three sets: boot images, the critical partitions, or everything.
+ public static BackupSet SetOf(IEnumerable partitions)
+ {
+ bool critical = false, any = false;
+ foreach (string name in partitions)
+ {
+ any = true;
+ if (BootSet.Contains(PartitionSafety.BaseName(name)))
+ continue;
+ if (PartitionSafety.IsBrickRisk(name))
+ critical = true;
+ else
+ return BackupSet.Full;
+ }
+ return !any ? BackupSet.Full : critical ? BackupSet.Critical : BackupSet.Boot;
+ }
+
+ /// The date at the end of "model_serial_yyyyMMdd-HHmmss".
+ public static DateTime? DateFromName(string name)
+ {
+ int underscore = name?.LastIndexOf('_') ?? -1;
+ DateTime taken;
+ if (underscore >= 0 && DateTime.TryParseExact(name.Substring(underscore + 1), "yyyyMMdd-HHmmss",
+ CultureInfo.InvariantCulture, DateTimeStyles.None, out taken))
+ return taken;
+ return null;
+ }
+
+ // ---------------------------------------------------------------- helpers
+
+ /// The chip maker from ro.soc.manufacturer, or guessed from the platform name.
+ public static string SocVendor(string maker, string platform, string hardware)
+ {
+ string m = (maker ?? "").ToLowerInvariant();
+ if (m.StartsWith("mediatek", StringComparison.Ordinal) || m.StartsWith("mtk", StringComparison.Ordinal)) return "MediaTek";
+ if (m.StartsWith("qti", StringComparison.Ordinal) || m.StartsWith("qualcomm", StringComparison.Ordinal)) return "Qualcomm";
+ if (m.StartsWith("spreadtrum", StringComparison.Ordinal) || m.StartsWith("unisoc", StringComparison.Ordinal)) return "Unisoc";
+ if (m.StartsWith("samsung", StringComparison.Ordinal)) return "Samsung";
+ if (m.StartsWith("google", StringComparison.Ordinal)) return "Google";
+ foreach (string raw in new[] { platform, hardware })
+ {
+ string p = (raw ?? "").ToLowerInvariant();
+ if (p.Length == 0)
+ continue;
+ if (p.StartsWith("mt", StringComparison.Ordinal) && p.Length > 2 && char.IsDigit(p[2])) return "MediaTek";
+ if (p.StartsWith("msm", StringComparison.Ordinal) || p.StartsWith("sdm", StringComparison.Ordinal)
+ || (p.StartsWith("sm", StringComparison.Ordinal) && p.Length > 2 && char.IsDigit(p[2])) || QualcommCodenames.Contains(p))
+ return "Qualcomm";
+ if (p.StartsWith("ums", StringComparison.Ordinal) || p.StartsWith("sp9", StringComparison.Ordinal) || p.StartsWith("s9863", StringComparison.Ordinal))
+ return "Unisoc";
+ if (p.StartsWith("exynos", StringComparison.Ordinal) || p.StartsWith("universal", StringComparison.Ordinal) || p.StartsWith("s5e", StringComparison.Ordinal))
+ return "Samsung";
+ if ((p.StartsWith("gs", StringComparison.Ordinal) && p.Length > 2 && char.IsDigit(p[2])) || p.StartsWith("zuma", StringComparison.Ordinal))
+ return "Google";
+ }
+ return null;
+ }
+
+ static readonly HashSet QualcommCodenames = new HashSet
+ {
+ "qcom", "lahaina", "kona", "taro", "kalama", "pineapple", "sun", "holi", "bengal", "lito", "trinket",
+ "atoll", "parrot", "crow", "blair", "monaco", "niobe", "cliffs", "volcano", "pitti",
+ };
+
+ static string SocName(string companion)
+ {
+ switch (companion)
+ {
+ case "mediatek": return "MediaTek";
+ case "qualcomm": return "Qualcomm";
+ case "unisoc": return "Unisoc";
+ case "samsung": return "Samsung";
+ case "google": return "Google";
+ default: return companion;
+ }
+ }
+
+ static int? Health(long? full, long? design)
+ {
+ if (full == null || design == null || design <= 0 || full <= 0)
+ return null;
+ return (int)(full.Value * 100 / design.Value);
+ }
+
+ static string Get(Dictionary values, string key)
+ {
+ string value;
+ return values.TryGetValue(key, out value) ? value : null;
+ }
+
+ static int? Int(string value)
+ {
+ int parsed;
+ return int.TryParse((value ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : (int?)null;
+ }
+
+ static long? Long(string value)
+ {
+ long parsed;
+ return long.TryParse((value ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : (long?)null;
+ }
+
+ static int? Hex(string value)
+ {
+ value = (value ?? "").Trim();
+ if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ value = value.Substring(2);
+ int parsed;
+ return value.Length > 0 && int.TryParse(value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out parsed) ? parsed : (int?)null;
+ }
+
+ static bool? Bool(string value)
+ {
+ switch ((value ?? "").Trim().ToLowerInvariant())
+ {
+ case "1": case "true": case "yes": return true;
+ case "0": case "false": case "no": return false;
+ default: return null;
+ }
+ }
+
+ static string Str(JsonElement element, string name)
+ {
+ JsonElement value;
+ if (!element.TryGetProperty(name, out value))
+ return null;
+ string text = value.ValueKind == JsonValueKind.String ? value.GetString()
+ : value.ValueKind == JsonValueKind.Number ? value.GetRawText()
+ : null;
+ return string.IsNullOrWhiteSpace(text) ? null : text.Trim();
+ }
+
+ static int? IntOf(JsonElement element, string name)
+ {
+ JsonElement value;
+ int parsed;
+ if (!element.TryGetProperty(name, out value))
+ return null;
+ if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out parsed))
+ return parsed;
+ return value.ValueKind == JsonValueKind.String ? Int(value.GetString()) : null;
+ }
+
+ static bool? BoolOf(JsonElement element, string name)
+ {
+ JsonElement value;
+ if (!element.TryGetProperty(name, out value))
+ return null;
+ if (value.ValueKind == JsonValueKind.True) return true;
+ if (value.ValueKind == JsonValueKind.False) return false;
+ return value.ValueKind == JsonValueKind.String ? Bool(value.GetString()) : null;
+ }
+
+ static IEnumerable Strings(JsonElement element, string name)
+ {
+ JsonElement value;
+ if (!element.TryGetProperty(name, out value) || value.ValueKind != JsonValueKind.Array)
+ yield break;
+ foreach (JsonElement item in value.EnumerateArray())
+ {
+ if (item.ValueKind == JsonValueKind.String && !string.IsNullOrEmpty(item.GetString()))
+ yield return item.GetString();
+ }
+ }
+ }
+}
diff --git a/src/FastbootEnhance.Core/FastbootEnhance.Core.csproj b/src/FastbootEnhance.Core/FastbootEnhance.Core.csproj
index c5fca54..cba4335 100644
--- a/src/FastbootEnhance.Core/FastbootEnhance.Core.csproj
+++ b/src/FastbootEnhance.Core/FastbootEnhance.Core.csproj
@@ -22,6 +22,8 @@
+
+
diff --git a/tests/FastbootEnhance.Core.Tests/PhoneReportTests.cs b/tests/FastbootEnhance.Core.Tests/PhoneReportTests.cs
new file mode 100644
index 0000000..50bc987
--- /dev/null
+++ b/tests/FastbootEnhance.Core.Tests/PhoneReportTests.cs
@@ -0,0 +1,255 @@
+using System.Diagnostics;
+using FastbootEnhance.Core.Adb;
+
+namespace FastbootEnhance.Core.Tests
+{
+ public class PhoneReportTests
+ {
+ // What PhoneReport.Command prints on a MediaTek phone with two Companion backups.
+ const string Basic =
+ "model=Demo Phone\nbrand=Demo\ndevice=demo\nandroid=14\nsdk=34\npatch=2026-08-05\n" +
+ "platform=mt6789\nhardware=mt6789\nsoc_maker=Mediatek\nsoc_model=MT6789\narch=arm64-v8a\n" +
+ "slot=_a\ndynamic=true\navb=green\ndevice_state=locked\nlocked=0\n" +
+ "kernel=5.15.148-android13-8-00017-gabc123\nselinux=Enforcing\n" +
+ "battery_capacity=78\nbattery_cycle_count=312\nbattery_charge_full=4550000\nbattery_charge_full_design=5000000\n" +
+ "backup=Demo_Phone_DEMO0123456789_20260830-091233|4|114688|SHA256SUMS backup-info.txt boot_a.img init_boot_a.img vbmeta_a.img dtbo_a.img \n" +
+ "backup=Demo_Phone_DEMO0123456789_20261010-184012|22|10066329|SHA256SUMS backup-info.txt boot_a.img super.img nvram.img \n" +
+ "backup=Demo_Phone_DEMO0123456789_20260921-184012|11|352256|SHA256SUMS boot_a.img nvram.img seccfg.img persist.img \n";
+
+ // "fbs info", "@@" and "fbs modules" from the Companion's own test phone (tests/fixture.sh):
+ // a hiding module set ro.boot.verifiedbootstate to green while the bootloader said orange.
+ const string Companion =
+ "{\"fbs\":\"2.4.0\",\"channel\":\"beta\",\"model\":\"Demo Phone\",\"brand\":\"Demo\",\"device\":\"demo\",\"product\":\"demo_mt6789\"," +
+ "\"android\":\"14\",\"sdk\":\"34\",\"patch\":\"2026-08-05\",\"build\":\"DEMO-14.0.1\",\"platform\":\"mt6789\",\"soc\":\"mediatek\"," +
+ "\"arch\":\"arm64-v8a\",\"slot\":\"_a\",\"dynamic\":true,\"treble\":true,\"vndk\":\"34\"," +
+ "\"boot\":{\"vbstate\":\"orange\",\"device_state\":\"unlocked\",\"locked\":false,\"spoofed\":true}," +
+ "\"kernel\":\"5.15.148-android13-8-00017-gabc123\",\"kmi\":\"5.15-android13\",\"selinux\":\"Enforcing\",\"root\":\"KernelSU\"," +
+ "\"boot_reason\":\"reboot,userrequested\",\"battery\":{\"level\":78,\"health\":91,\"cycles\":312,\"temp_dc\":334}," +
+ "\"memory\":{\"total_kb\":7864320,\"avail_kb\":2725000},\"storage\":{\"type\":\"emmc\",\"life_a\":\"0x02\",\"life_b\":\"0x01\",\"eol\":\"0x01\"}}\n" +
+ "@@\n" +
+ "{\"root\":\"/data/adb/modules\",\"modules\":[" +
+ "{\"id\":\"addon\",\"name\":\"Addon\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"debloat\",\"name\":\"Debloat\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"fonts_1\",\"name\":\"Fonts 1\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"fonts_2\",\"name\":\"Fonts 2\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"hosts_a\",\"name\":\"Hosts A\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"hosts_b\",\"name\":\"Hosts \\\"B\\\"\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"nomount\",\"name\":\"No mount\",\"version\":\"v1.0\",\"enabled\":true}]," +
+ "\"conflicts\":[" +
+ "{\"kind\":\"file\",\"modules\":[\"fonts_1\",\"fonts_2\"],\"count\":20,\"paths\":[\"/system/fonts/Demo1.ttf\",\"/system/fonts/Demo10.ttf\"]}," +
+ "{\"kind\":\"file\",\"modules\":[\"hosts_a\",\"hosts_b\"],\"count\":2,\"paths\":[\"/system/etc/hosts\",\"/system/vendor/lib/libdemo.so\"]}," +
+ "{\"kind\":\"replace\",\"path\":\"/system/app/Foo\",\"by\":\"debloat\",\"modules\":[\"addon\"]}," +
+ "{\"kind\":\"prop\",\"key\":\"ro.demo.x\",\"values\":[{\"module\":\"hosts_a\",\"value\":\"1\"},{\"module\":\"debloat\",\"value\":\"2\"},{\"module\":\"nomount\",\"value\":\"1\"}]}]}\n";
+
+ [Fact]
+ public void Reads_the_phone_without_root()
+ {
+ PhoneReport phone = PhoneReport.Parse(Basic);
+ Assert.Equal("Demo Phone", phone.Model);
+ Assert.Equal("14", phone.Android);
+ Assert.Equal("34", phone.Sdk);
+ Assert.Equal("2026-08-05", phone.Patch);
+ Assert.Equal("MediaTek", phone.Soc);
+ Assert.Equal("mt6789", phone.Platform);
+ Assert.Equal("arm64-v8a", phone.Arch);
+ Assert.Equal("_a", phone.Slot);
+ Assert.True(phone.Dynamic);
+ Assert.Equal("android13-5.15", phone.Kmi);
+ Assert.Equal("Enforcing", phone.Selinux);
+ // Without the Companion, the properties are all there is.
+ Assert.Equal("green", phone.Avb);
+ Assert.Equal("locked", phone.DeviceState);
+ Assert.False(phone.Spoofed);
+ Assert.Null(phone.Root);
+ Assert.Equal(78, phone.BatteryLevel);
+ Assert.Equal(312, phone.BatteryCycles);
+ Assert.Equal(91, phone.BatteryHealth);
+ Assert.Equal(CompanionState.NoRoot, phone.Companion);
+ }
+
+ [Fact]
+ public void Lists_backups_newest_first_and_tells_their_kind()
+ {
+ PhoneReport phone = PhoneReport.Parse(Basic);
+ Assert.Equal(3, phone.Backups.Count);
+ Assert.Equal(new[] { "20261010-184012", "20260921-184012", "20260830-091233" },
+ phone.Backups.Select(b => b.Name.Substring(b.Name.LastIndexOf('_') + 1)).ToArray());
+
+ PhoneBackup full = phone.Backups[0];
+ Assert.Equal(BackupSet.Full, full.Set);
+ Assert.Equal(22, full.Files);
+ Assert.Equal(10066329, full.Kb);
+ Assert.Equal(new DateTime(2026, 10, 10, 18, 40, 12), full.Taken);
+ Assert.Equal(PhoneReport.BackupsFolder + "/Demo_Phone_DEMO0123456789_20261010-184012", full.Path);
+
+ Assert.Equal(BackupSet.Critical, phone.Backups[1].Set);
+ Assert.Equal(BackupSet.Boot, phone.Backups[2].Set);
+ Assert.Equal(new[] { "boot_a", "init_boot_a", "vbmeta_a", "dtbo_a" }, phone.Backups[2].Images);
+ Assert.Equal(10066329 + 352256 + 114688, phone.BackupsKb);
+ }
+
+ [Fact]
+ public void Skips_backup_lines_that_could_leave_the_folder()
+ {
+ PhoneReport phone = PhoneReport.Parse("backup=..|1|4|a.img\nbackup=a/b|1|4|a.img\nbackup=|1|4|\nbackup=x_y|2\n");
+ Assert.Empty(phone.Backups);
+ }
+
+ [Fact]
+ public void The_companion_overrides_the_properties_a_hiding_module_changed()
+ {
+ PhoneReport phone = PhoneReport.Parse(Basic);
+ phone.ApplyCompanion(true, Companion);
+
+ Assert.Equal(CompanionState.Ready, phone.Companion);
+ Assert.Equal("2.4.0", phone.CompanionVersion);
+ Assert.Equal("beta", phone.CompanionChannel);
+ Assert.Equal("orange", phone.Avb);
+ Assert.Equal("unlocked", phone.DeviceState);
+ Assert.True(phone.Spoofed);
+ Assert.Equal("KernelSU", phone.Root);
+ Assert.Equal(91, phone.BatteryHealth);
+ Assert.Equal("emmc", phone.StorageType);
+ Assert.Equal(2, phone.StorageWear); // the worse of 0x02 and 0x01
+ Assert.Equal(1, phone.StorageEol);
+ Assert.Equal(7, phone.ModuleCount);
+ // The backups came from the first command and stay.
+ Assert.Equal(3, phone.Backups.Count);
+ }
+
+ [Fact]
+ public void Reads_every_kind_of_module_conflict_by_module_name()
+ {
+ PhoneReport phone = PhoneReport.Parse("");
+ phone.ApplyCompanion(true, Companion);
+ Assert.Equal(4, phone.Conflicts.Count);
+
+ ModuleConflict fonts = phone.Conflicts[0];
+ Assert.Equal(ConflictKind.File, fonts.Kind);
+ Assert.Equal(new[] { "Fonts 1", "Fonts 2" }, fonts.Modules);
+ Assert.Equal(20, fonts.Count);
+ Assert.Equal("/system/fonts/Demo1.ttf", fonts.Paths[0]);
+
+ Assert.Equal(new[] { "Hosts A", "Hosts \"B\"" }, phone.Conflicts[1].Modules);
+
+ ModuleConflict replace = phone.Conflicts[2];
+ Assert.Equal(ConflictKind.Replace, replace.Kind);
+ Assert.Equal("Debloat", replace.By);
+ Assert.Equal(new[] { "Addon" }, replace.Modules);
+ Assert.Equal("/system/app/Foo", replace.Paths.Single());
+
+ ModuleConflict prop = phone.Conflicts[3];
+ Assert.Equal(ConflictKind.Prop, prop.Kind);
+ Assert.Equal("ro.demo.x", prop.Key);
+ Assert.Equal(new[] { "Hosts A", "Debloat", "No mount" }, prop.Modules);
+ Assert.Equal("2", prop.Values[1].Value);
+ }
+
+ [Fact]
+ public void Tells_a_missing_module_from_a_refused_root()
+ {
+ PhoneReport none = PhoneReport.Parse(Basic);
+ none.ApplyCompanion(false, "");
+ Assert.Equal(CompanionState.NoRoot, none.Companion);
+
+ PhoneReport missing = PhoneReport.Parse(Basic);
+ missing.ApplyCompanion(true, "fbs-missing\n");
+ Assert.Equal(CompanionState.NotInstalled, missing.Companion);
+
+ PhoneReport refused = PhoneReport.Parse(Basic);
+ refused.ApplyCompanion(true, "Permission denied\n");
+ Assert.Equal(CompanionState.Refused, refused.Companion);
+ Assert.Equal("green", refused.Avb);
+
+ PhoneReport broken = PhoneReport.Parse(Basic);
+ broken.ApplyCompanion(true, "{\"fbs\":\"2.4.0\",\"model\":");
+ Assert.Equal(CompanionState.Refused, broken.Companion);
+ }
+
+ [Fact]
+ public void A_module_list_it_cannot_read_keeps_the_rest()
+ {
+ PhoneReport phone = PhoneReport.Parse(Basic);
+ phone.ApplyCompanion(true, Companion.Substring(0, Companion.IndexOf("@@", StringComparison.Ordinal) + 3) + "{\"modules\":[");
+ Assert.Equal(CompanionState.Ready, phone.Companion);
+ Assert.Equal("KernelSU", phone.Root);
+ Assert.Empty(phone.Conflicts);
+ }
+
+ [Theory]
+ [InlineData("Mediatek", "", "", "MediaTek")]
+ [InlineData("QTI", "", "", "Qualcomm")]
+ [InlineData("", "kalama", "qcom", "Qualcomm")]
+ [InlineData("", "sm8550", "", "Qualcomm")]
+ [InlineData("", "mt6878", "", "MediaTek")]
+ [InlineData("", "ums9230", "", "Unisoc")]
+ [InlineData("", "exynos2100", "", "Samsung")]
+ [InlineData("", "zuma", "", "Google")]
+ [InlineData("", "", "", null)]
+ public void Names_the_chip_maker(string maker, string platform, string hardware, string expected)
+ {
+ Assert.Equal(expected, PhoneReport.SocVendor(maker, platform, hardware));
+ }
+
+ [Fact]
+ public void Kinds_of_backup()
+ {
+ Assert.Equal(BackupSet.Boot, PhoneReport.SetOf(new[] { "boot_a", "init_boot_a", "vendor_boot_a", "vbmeta_system_a" }));
+ Assert.Equal(BackupSet.Critical, PhoneReport.SetOf(new[] { "nvram", "persist", "seccfg", "boot_a" }));
+ Assert.Equal(BackupSet.Full, PhoneReport.SetOf(new[] { "nvram", "super" }));
+ Assert.Equal(BackupSet.Full, PhoneReport.SetOf(new string[0]));
+ }
+
+ [Fact]
+ public void Commands_fit_where_they_go()
+ {
+ // The Companion's command goes through su -c '…', which cannot carry single quotes.
+ Assert.Equal("su -c '" + PhoneReport.CompanionCommand + "'", AdbCommand.AsRoot(PhoneReport.CompanionCommand, RootAccess.Su));
+ Assert.DoesNotContain("\n", PhoneReport.Command);
+ }
+
+ /// Runs the real command in sh, with getprop answered by a stub and a backups folder made here.
+ [Fact]
+ public void The_command_runs_in_a_posix_shell()
+ {
+ if (!OperatingSystem.IsLinux() || !File.Exists("/bin/sh"))
+ return;
+ string root = Path.Combine(Path.GetTempPath(), "fbs-phone-" + Guid.NewGuid().ToString("N"));
+ string backup = Path.Combine(root, "Demo_Phone_SER1_20260921-184012");
+ Directory.CreateDirectory(backup);
+ try
+ {
+ File.WriteAllText(Path.Combine(backup, "SHA256SUMS"), new string('a', 64) + " boot_a.img\n" + new string('b', 64) + " nvram.img\n");
+ File.WriteAllBytes(Path.Combine(backup, "boot_a.img"), new byte[8192]);
+ File.WriteAllBytes(Path.Combine(backup, "nvram.img"), new byte[4096]);
+ string command = "getprop() { case $1 in ro.product.model) echo 'Demo Phone';; ro.board.platform) echo mt6789;; " +
+ "ro.boot.slot_suffix) echo _b;; esac; }; getenforce() { echo Permissive; }; " +
+ PhoneReport.Command.Replace(PhoneReport.BackupsFolder, root);
+
+ ProcessStartInfo start = new ProcessStartInfo("/bin/sh") { RedirectStandardOutput = true, UseShellExecute = false };
+ start.ArgumentList.Add("-c");
+ start.ArgumentList.Add(command);
+ using Process sh = Process.Start(start)!;
+ string output = sh.StandardOutput.ReadToEnd();
+ sh.WaitForExit();
+
+ PhoneReport phone = PhoneReport.Parse(output);
+ Assert.Equal("Demo Phone", phone.Model);
+ Assert.Equal("MediaTek", phone.Soc);
+ Assert.Equal("_b", phone.Slot);
+ Assert.Equal("Permissive", phone.Selinux);
+ Assert.NotNull(phone.Kernel);
+ PhoneBackup found = Assert.Single(phone.Backups);
+ Assert.Equal("Demo_Phone_SER1_20260921-184012", found.Name);
+ Assert.Equal(2, found.Files);
+ Assert.True(found.Kb > 0);
+ Assert.Equal(new[] { "boot_a", "nvram" }, found.Images.OrderBy(n => n).ToArray());
+ Assert.Equal(BackupSet.Critical, found.Set);
+ }
+ finally
+ {
+ Directory.Delete(root, true);
+ }
+ }
+ }
+}
diff --git a/tools/FastbootEnhance.FakeAdb/Program.cs b/tools/FastbootEnhance.FakeAdb/Program.cs
index bae786c..e4a8b7a 100644
--- a/tools/FastbootEnhance.FakeAdb/Program.cs
+++ b/tools/FastbootEnhance.FakeAdb/Program.cs
@@ -141,6 +141,30 @@ static int Shell(string command)
return 0;
}
+ // PhoneReport.Command: the Phone page's read without root, backups on /sdcard included.
+ if (command.Contains("soc_maker ro.soc.manufacturer"))
+ {
+ if (!Android)
+ return 1;
+ Console.Out.Write(PhoneBasic());
+ return 0;
+ }
+ // PhoneReport.CompanionCommand through su: "fbs info", "@@", "fbs modules".
+ if (command.StartsWith("su -c '", StringComparison.Ordinal) && command.Contains("fastboot_studio_companion/bin/fbs") && command.Contains(" info"))
+ {
+ switch (CompanionMode())
+ {
+ case "missing":
+ Console.Out.Write("fbs-missing\n");
+ return 0;
+ case "refused":
+ Console.Error.Write("Permission denied\n");
+ return 1;
+ }
+ Console.Out.Write(CompanionInfo + "\n@@\n" + CompanionModules + "\n");
+ return 0;
+ }
+
// DeviceFacts.Command: what any app can ask without root.
if (command.Contains("echo kernel=$(uname -r)"))
{
@@ -159,7 +183,7 @@ static int Shell(string command)
if (!Android)
return 127;
Console.Out.Write("kernel=5.10.198-android12-9-00001-gabcdef\npatch=2024-09-05\nvendor_patch=2024-09-05\n"
- + "model=Sample_A64\nandroid=14\ncompanion=2.3.0 beta\nroot=KernelSU\navb=orange\ndevice_state=unlocked\n"
+ + "model=Sample_A64\nandroid=14\ncompanion=2.4.0 beta\nroot=KernelSU\navb=orange\ndevice_state=unlocked\n"
+ "spoofed=false\nselinux=Enforcing\nkmi=5.10-android12\nconflicts=1\n");
return 0;
}
@@ -312,6 +336,8 @@ static int Pull(List paths)
Console.Error.WriteLine("adb: usage: pull [-a] REMOTE LOCAL");
return 1;
}
+ if (paths[0].StartsWith(BackupsFolder + "/", StringComparison.Ordinal))
+ return PullBackup(paths[0].Substring(BackupsFolder.Length + 1).TrimEnd('/'), paths[1]);
string remote = paths[0].TrimEnd('/');
string local = paths[1];
string parent = remote.Substring(0, Math.Max(1, remote.LastIndexOf('/')));
@@ -366,6 +392,111 @@ static void WriteFile(string path, long size)
file.SetLength(size);
}
+ // ---------------------------------------------------------------- the Phone page
+
+ const string BackupsFolder = "/sdcard/FastbootStudio/Backups";
+
+ // Three backups the Companion made, as on the mockup: full, critical and boot.
+ static readonly (string name, long kb, string[] images)[] Backups =
+ {
+ ("Sample_A64_FBE0SAMPLE02_20261010-184012", 10066329, new[]
+ {
+ "boot_a", "init_boot_a", "vendor_boot_a", "dtbo_a", "vbmeta_a", "vbmeta_system_a", "modem_a", "persist",
+ "modemst1", "modemst2", "fsg", "fsc", "devinfo", "misc", "metadata", "frp", "super", "abl_a", "xbl_a",
+ "tz_a", "aop_a", "keymaster_a",
+ }),
+ ("Sample_A64_FBE0SAMPLE02_20260921-184012", 352256, new[]
+ {
+ "persist", "modemst1", "modemst2", "fsg", "fsc", "devinfo", "frp", "abl_a", "xbl_a", "tz_a", "modem_a",
+ }),
+ ("Sample_A64_FBE0SAMPLE02_20260830-091233", 114688, new[] { "boot_a", "init_boot_a", "vendor_boot_a", "vbmeta_a" }),
+ };
+
+ /// "companion" (default), "missing" or "refused", from fake-adb.companion next to the executable.
+ static string CompanionMode()
+ {
+ string path = Path.Combine(AppContext.BaseDirectory, "fake-adb.companion");
+ return File.Exists(path) ? File.ReadAllText(path).Trim() : "companion";
+ }
+
+ static string PhoneBasic()
+ {
+ StringBuilder text = new StringBuilder(
+ "model=Sample_A64\nbrand=Sample\ndevice=sample\nandroid=14\nsdk=34\npatch=2024-09-05\n" +
+ "platform=taro\nhardware=qcom\nsoc_maker=QTI\nsoc_model=SM8450\narch=arm64-v8a\n" +
+ "slot=_a\ndynamic=true\navb=green\ndevice_state=locked\nlocked=1\n" +
+ "kernel=5.10.198-android12-9-00001-gabcdef\nselinux=Enforcing\n" +
+ "battery_capacity=78\nbattery_cycle_count=312\nbattery_charge_full=4550000\nbattery_charge_full_design=5000000\n");
+ foreach (var (name, kb, images) in Backups)
+ text.Append("backup=").Append(name).Append('|').Append(images.Length).Append('|').Append(kb).Append("|SHA256SUMS backup-info.txt ")
+ .Append(string.Join(" ", images.Select(i => i + ".img"))).Append(" \n");
+ return text.ToString();
+ }
+
+ // A hiding module set ro.boot.verifiedbootstate to green while the bootloader said orange.
+ const string CompanionInfo =
+ "{\"fbs\":\"2.4.0\",\"channel\":\"beta\",\"model\":\"Sample_A64\",\"brand\":\"Sample\",\"device\":\"sample\",\"product\":\"sample_a64\"," +
+ "\"android\":\"14\",\"sdk\":\"34\",\"patch\":\"2024-09-05\",\"build\":\"SAMPLE-14.0.1\",\"platform\":\"taro\",\"soc\":\"qualcomm\"," +
+ "\"arch\":\"arm64-v8a\",\"slot\":\"_a\",\"dynamic\":true,\"treble\":true,\"vndk\":\"34\"," +
+ "\"boot\":{\"vbstate\":\"orange\",\"device_state\":\"unlocked\",\"locked\":false,\"spoofed\":true}," +
+ "\"kernel\":\"5.10.198-android12-9-00001-gabcdef\",\"kmi\":\"5.10-android12\",\"selinux\":\"Enforcing\",\"root\":\"KernelSU\"," +
+ "\"boot_reason\":\"reboot\",\"battery\":{\"level\":78,\"health\":91,\"cycles\":312,\"temp_dc\":334}," +
+ "\"memory\":{\"total_kb\":7864320,\"avail_kb\":2725000},\"storage\":{\"type\":\"ufs\",\"life_a\":\"0x02\",\"life_b\":\"0x01\",\"eol\":\"0x01\"}}";
+
+ const string CompanionModules =
+ "{\"root\":\"/data/adb/modules\",\"modules\":[" +
+ "{\"id\":\"adblock\",\"name\":\"AdBlock Plus\",\"version\":\"v3.1\",\"enabled\":true}," +
+ "{\"id\":\"hosts\",\"name\":\"Systemless Hosts\",\"version\":\"v1.0\",\"enabled\":true}," +
+ "{\"id\":\"debloater\",\"name\":\"Debloater\",\"version\":\"v20.1\",\"enabled\":true}," +
+ "{\"id\":\"youtube\",\"name\":\"YouTube Patched\",\"version\":\"19.16\",\"enabled\":true}," +
+ "{\"id\":\"gpu\",\"name\":\"GPU Tweaks\",\"version\":\"v2\",\"enabled\":true}," +
+ "{\"id\":\"fastboot_studio_companion\",\"name\":\"Fastboot Studio Companion (Beta)\",\"version\":\"v2.4.0\",\"enabled\":true}]," +
+ "\"conflicts\":[" +
+ "{\"kind\":\"file\",\"modules\":[\"adblock\",\"hosts\"],\"count\":1,\"paths\":[\"/system/etc/hosts\"]}," +
+ "{\"kind\":\"replace\",\"path\":\"/system/app/YouTube\",\"by\":\"debloater\",\"modules\":[\"youtube\"]}," +
+ "{\"kind\":\"prop\",\"key\":\"debug.hwui.renderer\",\"values\":[{\"module\":\"gpu\",\"value\":\"skiavk\"},{\"module\":\"debloater\",\"value\":\"skiagl\"}]}]}";
+
+ /// Writes a backup folder as the Companion leaves it: small images, SHA256SUMS and backup-info.txt.
+ static int PullBackup(string name, string local)
+ {
+ var backup = Backups.FirstOrDefault(b => b.name == name);
+ if (backup.name == null)
+ {
+ Console.Error.WriteLine("adb: error: failed to stat remote object '" + BackupsFolder + "/" + name + "': No such file or directory");
+ return 1;
+ }
+ string folder = Path.Combine(local, name);
+ Directory.CreateDirectory(folder);
+ StringBuilder sums = new StringBuilder();
+ StringBuilder info = new StringBuilder("Fastboot Studio backup\ndate\t2026-10-10 18:40:12 +03:00\nserial\t" + Serial
+ + "\nmodel\tSample_A64\nproduct\tsample_a64\nmode\tcompanion\nsource\t/dev/block/by-name\n\npartition\tbytes\tsha256\n");
+ long bytes = 0;
+ foreach (string image in backup.images)
+ {
+ byte[] content = new byte[64 * 1024];
+ uint state = 2166136261;
+ foreach (char c in image)
+ state = (state ^ c) * 16777619;
+ for (int i = 0; i < content.Length; i += 4)
+ {
+ state ^= state << 13;
+ state ^= state >> 17;
+ state ^= state << 5;
+ BitConverter.TryWriteBytes(new Span(content, i, 4), state);
+ }
+ File.WriteAllBytes(Path.Combine(folder, image + ".img"), content);
+ string hash = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(content)).ToLowerInvariant();
+ sums.Append(hash).Append(" ").Append(image).Append(".img\n");
+ info.Append(image).Append('\t').Append(content.Length).Append('\t').Append(hash).Append('\n');
+ bytes += content.Length;
+ }
+ File.WriteAllText(Path.Combine(folder, "SHA256SUMS"), sums.ToString());
+ File.WriteAllText(Path.Combine(folder, "backup-info.txt"), info.ToString());
+ Console.Out.WriteLine(BackupsFolder + "/" + name + "/: " + (backup.images.Length + 2) + " files pulled, 0 skipped. 40.0 MB/s ("
+ + bytes + " bytes in 0.100s)");
+ return 0;
+ }
+
///
/// Appends a line to fake-adb.log. The app runs several copies at once (a device poller next to a
/// transfer), and CI counts these lines, so a write that finds the file in use waits