Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,31 @@ jobs:
path: native-publish/*
if-no-files-found: error

linux-arm64:
name: Build Linux ARM64
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build release binaries
run: cargo build --workspace --release --locked
- name: Test Rust workspace
run: cargo test --workspace --locked
- name: Prepare artifact
run: |
mkdir -p dist
cp target/release/sensitivity dist/sensitivity-linux-aarch64
cp target/release/sensitivity-gui dist/sensitivity-gui-linux-aarch64
- uses: actions/upload-artifact@v7
with:
name: sensitivity-Linux-arm64
path: dist/*
if-no-files-found: error

msrv:
name: Minimum Rust 1.97
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.97.1
Expand All @@ -169,7 +191,7 @@ jobs:

fuzz-target:
name: Fuzz target compiles
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
Expand Down
60 changes: 58 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:
jobs:
preflight:
name: Release preflight
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@1.97.1
Expand All @@ -37,8 +37,11 @@ jobs:
matrix:
include:
- name: linux-x86_64
os: ubuntu-22.04
os: ubuntu-24.04
archive: sensitivity-linux-x86_64.tar.gz
- name: linux-aarch64
os: ubuntu-24.04-arm
archive: sensitivity-linux-aarch64.tar.gz
- name: windows-x86_64
os: windows-latest
archive: sensitivity-windows-x86_64.zip
Expand Down Expand Up @@ -123,6 +126,57 @@ jobs:
rm package/Sensitivity.app/Contents/Info.plist.bak
fi
tar -C package -czf "${{ matrix.archive }}" .
- name: Package Linux desktop formats
if: matrix.name == 'linux-x86_64'
shell: bash
run: |
mkdir -p deb-root/DEBIAN deb-root/usr/bin deb-root/usr/share/applications deb-root/usr/share/icons/hicolor/512x512/apps
cp target/release/sensitivity deb-root/usr/bin/sensitivity
cp target/release/sensitivity-gui deb-root/usr/bin/sensitivity-gui
cp assets/sensitivity-icon.png deb-root/usr/share/icons/hicolor/512x512/apps/sensitivity.png
cat > deb-root/usr/share/applications/sensitivity.desktop <<'EOF'
[Desktop Entry]
Type=Application
Name=Sensitivity
Comment=Direct USB Xiaomi Recovery tool
Exec=sensitivity-gui
Icon=sensitivity
Terminal=false
Categories=Utility;System;
EOF
cat > deb-root/DEBIAN/control <<EOF
Package: sensitivity
Version: $(cargo metadata --format-version 1 --no-deps | sed -n 's/.*"name":"sensitivity","version":"\([^"]*\)".*/\1/p')
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Chromatic <feedback@chromatic.hu>
Description: Sensitivity Xiaomi Recovery tool
Direct USB recovery validation, download, and flashing for Xiaomi devices.
EOF
dpkg-deb --build --root-owner-group deb-root sensitivity-linux-x86_64.deb
mkdir -p Sensitivity.AppDir/usr/bin Sensitivity.AppDir/usr/share/icons/hicolor/512x512/apps
cp target/release/sensitivity target/release/sensitivity-gui Sensitivity.AppDir/usr/bin/
cp assets/sensitivity-icon.png Sensitivity.AppDir/usr/share/icons/hicolor/512x512/apps/sensitivity.png
cat > Sensitivity.AppDir/AppRun <<'EOF'
#!/bin/sh
exec "$(dirname "$0")/usr/bin/sensitivity-gui" "$@"
EOF
chmod +x Sensitivity.AppDir/AppRun
cat > Sensitivity.AppDir/sensitivity.desktop <<'EOF'
[Desktop Entry]
Type=Application
Name=Sensitivity
Comment=Direct USB Xiaomi Recovery tool
Exec=sensitivity-gui
Icon=sensitivity
Terminal=false
Categories=Utility;System;
EOF
cp Sensitivity.AppDir/sensitivity.desktop Sensitivity.AppDir/usr/share/applications/
curl -L --fail --retry 3 -o appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool.AppImage
APPIMAGE_EXTRACT_AND_RUN=1 ./appimagetool.AppImage Sensitivity.AppDir sensitivity-linux-x86_64.AppImage
- name: Package Windows
if: matrix.runtime != ''
shell: pwsh
Expand Down Expand Up @@ -165,6 +219,8 @@ jobs:
name: ${{ matrix.name }}
path: |
${{ matrix.archive }}
sensitivity-*.AppImage
sensitivity-*.deb
sensitivity-*-setup.exe
if-no-files-found: error

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ concurrency:
jobs:
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
security-events: write
packages: read
Expand Down Expand Up @@ -67,7 +67,7 @@ jobs:
dependency-review:
name: Dependency review
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: read
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ It speaks the Mi Assistant ADB-like protocol directly over USB, validates offici

## Install

Download a release for Windows, Linux, or macOS from [GitHub Releases](https://github.com/Has-X/Sensitivity/releases). On Windows, choose the native `Sensitivity-Setup-x64.exe` or `Sensitivity-Setup-arm64.exe` for the processor architecture, then open Sensitivity from Start. Portable ZIPs are available too. The Windows app follows the system light or dark mode and accent colour, with Mica material where supported. The app and CLI support 34 languages; see the [localization guide](docs/LOCALIZATION.md) for the maintained list and translation rules. The separate `sensitivity-cli.exe` is for terminals and scripts. On Linux or macOS, open `sensitivity-gui`. Releases include SHA-256 checksums.
Download a release for Windows, Linux, or macOS from [GitHub Releases](https://github.com/Has-X/Sensitivity/releases). On Windows, choose the native `Sensitivity-Setup-x64.exe` or `Sensitivity-Setup-arm64.exe` for the processor architecture, then open Sensitivity from Start. Portable ZIPs are available too. Linux releases include x86_64 tarball, AppImage, and Debian packages, plus an ARM64 tarball. The Windows app follows the system light or dark mode and accent colour, with Mica material where supported. The app and CLI support 34 languages; see the [localization guide](docs/LOCALIZATION.md) for the maintained list and translation rules. The separate `sensitivity-cli.exe` is for terminals and scripts. On Linux or macOS, open `sensitivity-gui`. Releases include SHA-256 checksums.

To build or install from source:

Expand All @@ -45,20 +45,34 @@ In the desktop app, select the detected recovery and ROM, review validation and
For the CLI:

1. Boot the phone into stock recovery and choose **Connect with Mi Assistant**.
2. Connect it directly by USB and run the setup check:
2. Connect it directly by USB and run the setup check. On Windows, use the packaged CLI executable:

```powershell
.\sensitivity-cli.exe doctor
```

On Linux and macOS, use:

```console
sensitivity doctor
```

3. Read the detected device identity:
3. Read the detected device identity with the same platform-specific command:

```powershell
.\sensitivity-cli.exe info
```

```console
sensitivity info
```

4. Flash an official Recovery ROM:

```powershell
.\sensitivity-cli.exe flash C:\path\to\recovery-rom.zip
```

```console
sensitivity flash /path/to/recovery-rom.zip
```
Expand Down Expand Up @@ -92,19 +106,22 @@ The maintained documentation is available in the [Sensitivity Wiki](https://gith

Translation contributors should use the [English source and translator guide](docs/LOCALIZATION.md). It records the safety context for Windows, installer, portable GUI, and CLI messages.

Use `sensitivity help` or `sensitivity help <command>` for the complete command reference.
Use `sensitivity help` or `sensitivity help <command>` for the complete command reference. On Windows, use `sensitivity-cli.exe help` or `sensitivity-cli.exe help <command>`.

On Windows, the packaged command is `sensitivity-cli.exe` (or `sensitivity-cli` after the installer adds it to `PATH`). On Linux and macOS, the command is `sensitivity`. The examples below use the cross-platform name.

## Common commands

```console
sensitivity doctor # diagnose USB and local ADB coexistence
sensitivity devices # list matching USB interfaces without claiming them
sensitivity detect # verify the direct-USB protocol handshake
sensitivity detect # verify the recovery protocol handshake
sensitivity info # human-readable device information
sensitivity info --json # stable output for scripts
sensitivity completions bash # generate shell completion definitions
sensitivity list-allowed-roms # query packages accepted for this device
sensitivity download-latest # download and verify the latest approved ROM
sensitivity flash ROM.zip --validate-only # validate without starting sideload
sensitivity flash ROM.zip # validate and flash a local package
sensitivity flash-from-latest # download, validate, and flash
sensitivity reboot # leave recovery
Expand All @@ -120,14 +137,14 @@ The supported profiles are `global`, `eea`, `in`, `ru`, `id`, `tr`, `tw`, and `c

## ADB coexistence

Sensitivity uses direct USB and leaves a local Android Debug Bridge server untouched by default. If `adb` already owns the recovery interface, stop it only for this invocation:
Sensitivity leaves a local Android Debug Bridge server untouched by default. When that server already owns a Mi Recovery interface, Sensitivity reuses the matching recovery transport while keeping unrelated Android devices connected. It falls back to direct USB when no compatible recovery is available through ADB. To force direct USB for one invocation:

```console
sensitivity --adb-policy stop doctor
sensitivity --adb-policy stop flash ROM.zip
```

Sensitivity never occupies port 5037 and only asks the ADB server to stop when you choose that policy. The Windows app detects likely ownership conflicts, explains the impact, and asks before retrying.
Sensitivity only asks the ADB server to stop when you choose that policy. The Windows app lists Mi Recovery transports without presenting normal Android devices as recovery targets. Existing debugging sessions remain connected when the default policy is used.

Users and scripts moving from the older project should read [Migrating from MiAssistantFork](docs/MIGRATING_FROM_MAF.md).

Expand Down
29 changes: 18 additions & 11 deletions apps/windows/Sensitivity.WinUI/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ await RunBusyAsync(async cancellationToken =>
private async void DevicePicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_hasRecoveryDevice = DevicePicker.SelectedItem is UsbDevice;
StopAdbToggle.IsEnabled = DevicePicker.SelectedItem is not UsbDevice selected
|| !string.Equals(selected.Transport, "adb-server", StringComparison.Ordinal);
UpdateDeviceActionState();
ClearDeviceInfo();
if (DevicePicker.SelectedItem is UsbDevice)
Expand All @@ -258,9 +260,9 @@ await RunBusyAsync(async cancellationToken =>
DeviceInfo info;
try
{
info = await _backend.GetDeviceInfoAsync(device.Index, StopAdbToggle.IsOn, cancellationToken);
info = await _backend.GetDeviceInfoAsync(device.Index, ShouldStopAdb(device), cancellationToken);
}
catch (Exception error) when (!StopAdbToggle.IsOn
catch (Exception error) when (!ShouldStopAdb(device)
&& AutoResolveAdbToggle.IsOn
&& SensitivityBackend.IsUsbOwnershipError(error))
{
Expand Down Expand Up @@ -349,7 +351,7 @@ private async void ListAllowedRomsButton_Click(object sender, RoutedEventArgs e)
}
await RunBusyAsync(async cancellationToken =>
{
var result = await _backend.ListAllowedRomsAsync(device.Index, StopAdbToggle.IsOn, cancellationToken);
var result = await _backend.ListAllowedRomsAsync(device.Index, ShouldStopAdb(device), cancellationToken);
AllowedRomsText.Text = string.Join(Environment.NewLine, new[] { result.StandardOutput.Trim(), result.StandardError.Trim() }.Where(text => !string.IsNullOrWhiteSpace(text)));
if (!result.Succeeded) throw new InvalidOperationException(result.ErrorMessage);
}, L("status.fetching_allowed"));
Expand All @@ -364,7 +366,7 @@ private async void DownloadLatestButton_Click(object sender, RoutedEventArgs e)
}
await RunBusyAsync(async cancellationToken =>
{
var result = await _backend.DownloadLatestAsync(device.Index, DownloadDirectoryText.Text, StopAdbToggle.IsOn, cancellationToken);
var result = await _backend.DownloadLatestAsync(device.Index, DownloadDirectoryText.Text, ShouldStopAdb(device), cancellationToken);
if (!result.Succeeded) throw new InvalidOperationException(result.ErrorMessage);
var romPath = ExtractDownloadedRomPath(result.StandardOutput) ?? FindDownloadedRom();
if (romPath is not null)
Expand Down Expand Up @@ -458,6 +460,9 @@ private bool TrySelectDownloadedRom()
: null;
}

private bool ShouldStopAdb(UsbDevice device)
=> StopAdbToggle.IsOn && !string.Equals(device.Transport, "adb-server", StringComparison.Ordinal);

private async void FlashLatestButton_Click(object sender, RoutedEventArgs e)
{
if (DevicePicker.SelectedItem is not UsbDevice device)
Expand All @@ -470,7 +475,7 @@ private async void FlashLatestButton_Click(object sender, RoutedEventArgs e)
SetBusy(true, L("status.downloading_latest"));
try
{
var result = await _backend.FlashLatestAsync(device.Index, DownloadDirectoryText.Text, StopAdbToggle.IsOn, HandleBackendEventAsync, _operationCancellation.Token);
var result = await _backend.FlashLatestAsync(device.Index, DownloadDirectoryText.Text, ShouldStopAdb(device), HandleBackendEventAsync, _operationCancellation.Token);
if (!result.Succeeded) throw new InvalidOperationException(result.ErrorMessage);
FlashProgress.Value = 100;
ProgressPercentText.Text = "100%";
Expand Down Expand Up @@ -523,7 +528,7 @@ private async void StartFlashButton_Click(object sender, RoutedEventArgs e)
var result = await _backend.FlashAsync(
device.Index,
_romPath,
StopAdbToggle.IsOn,
ShouldStopAdb(device),
HandleBackendEventAsync,
_operationCancellation.Token);
if (_operationCancellation.IsCancellationRequested)
Expand Down Expand Up @@ -604,7 +609,7 @@ private async void RebootButton_Click(object sender, RoutedEventArgs e)
}
await RunBusyAsync(async cancellationToken =>
{
var result = await _backend.RebootAsync(device.Index, StopAdbToggle.IsOn, cancellationToken);
var result = await _backend.RebootAsync(device.Index, ShouldStopAdb(device), cancellationToken);
if (!result.Succeeded) throw new InvalidOperationException(result.ErrorMessage);
ShowStatus(L("status.reboot_requested"), L("status.reboot_detail"), InfoBarSeverity.Success);
}, L("status.sending_reboot"));
Expand All @@ -627,18 +632,20 @@ private async void EraseDataButton_Click(object sender, RoutedEventArgs e)
}
await RunBusyAsync(async cancellationToken =>
{
var result = await _backend.FormatDataAsync(device.Index, StopAdbToggle.IsOn, cancellationToken);
var result = await _backend.FormatDataAsync(device.Index, ShouldStopAdb(device), cancellationToken);
if (!result.Succeeded) throw new InvalidOperationException(result.ErrorMessage);
ShowStatus(L("status.erase_requested"), L("status.erase_detail"), InfoBarSeverity.Success);
}, L("status.erasing"));
}

private async void RunDoctorButton_Click(object sender, RoutedEventArgs e)
{
var index = (DevicePicker.SelectedItem as UsbDevice)?.Index ?? 0;
var selectedDevice = DevicePicker.SelectedItem as UsbDevice;
var index = selectedDevice?.Index ?? 0;
var stopAdb = selectedDevice is null ? StopAdbToggle.IsOn : ShouldStopAdb(selectedDevice);
await RunBusyAsync(async cancellationToken =>
{
var result = await _backend.RunDoctorAsync(index, StopAdbToggle.IsOn, cancellationToken);
var result = await _backend.RunDoctorAsync(index, stopAdb, cancellationToken);
DiagnosticsText.Text = string.Join(
Environment.NewLine,
new[] { result.StandardOutput.Trim(), result.StandardError.Trim() }
Expand All @@ -660,7 +667,7 @@ private async void DetectButton_Click(object sender, RoutedEventArgs e)

await RunBusyAsync(async cancellationToken =>
{
var result = await _backend.DetectAsync(device.Index, StopAdbToggle.IsOn, cancellationToken);
var result = await _backend.DetectAsync(device.Index, ShouldStopAdb(device), cancellationToken);
DiagnosticsText.Text = string.Join(
Environment.NewLine,
new[] { result.StandardOutput.Trim(), result.StandardError.Trim() }
Expand Down
18 changes: 17 additions & 1 deletion apps/windows/Sensitivity.WinUI/Models/BackendModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,27 @@ public sealed class UsbDevice
[JsonPropertyName("product_id")]
public int ProductId { get; set; }

[JsonPropertyName("transport")]
public string Transport { get; set; } = "usb";

[JsonPropertyName("transport_id")]
public long? TransportId { get; set; }

[JsonPropertyName("recovery_device")]
public string? RecoveryDevice { get; set; }

[JsonPropertyName("model")]
public string? Model { get; set; }

[JsonPropertyName("protocol")]
public int Protocol { get; set; }

[JsonIgnore]
public string DisplayName => $"{LocalizationService.Get("label.recovery_device")} {Index + 1} · {VendorId:x4}:{ProductId:x4} · USB {Bus}/{Address}";
public string DisplayName => Transport == "adb-server"
? string.IsNullOrWhiteSpace(RecoveryDevice) && string.IsNullOrWhiteSpace(Model)
? $"{LocalizationService.Get("label.recovery_device")} {Index + 1} · ADB"
: $"{LocalizationService.Get("label.recovery_device")} {Index + 1} · {(string.IsNullOrWhiteSpace(RecoveryDevice) ? Model : RecoveryDevice)} · ADB"
: $"{LocalizationService.Get("label.recovery_device")} {Index + 1} · {VendorId:x4}:{ProductId:x4} · USB {Bus}/{Address}";
}

public sealed class DeviceInfo
Expand Down
1 change: 1 addition & 0 deletions crates/gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ publish = false
anyhow = "1"
eframe = { version = "0.36.1", default-features = false, features = ["default_fonts", "glow", "persistence", "wayland", "x11"] }
rfd = "0.17.2"
reqwest = { version = "0.13", default-features = false, features = ["blocking", "rustls"] }
sensitivity = { version = "1.1.3", path = "../.." }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
Loading
Loading