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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

## [Unreleased]

### Added

- 可选的显示器端 ADB Keeper,在端口停止监听时重新启用网络 ADB。
- Redmi G Pro 27U 2026 / HyperOS 3 的 `com.example.autoreboot` 白名单兼容构建、双机重启验证和安装脚本。

### Security

- 文档明确说明 `WRITE_SECURE_SETTINGS`、长期开放 5555 和固件专用包身份的风险边界。

## [0.1.0] - 2026-08-21

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Set-ExecutionPolicy -Scope Process Bypass
- 默认日志不记录识别正文;设置 `logRecognizedText: true` 才会记录内容。
- `force-stop` / `pm enable` / `pm disable-user` 会改变显示器上的语音助手行为。只有经过验证的 profile 才应启用这些操作。
- `keepAndroidAwakeWhilePowered` 可缓解部分固件待机后关闭 ADB,但会增加 Android 系统待机功耗;通用 profile 默认关闭。
- Redmi G Pro 27U 2026 固件若在完整重启后复位 ADB,可选装设备端 [ADB Keeper](companion/adb-keeper/README.md)。它需要一次性 ADB 授权,并会让 5555 长期存在于可信局域网。
- 本方法依赖厂商未屏蔽的系统日志。日志被裁剪、权限收紧或语音服务变更后,可能无法适配。
- 仅支持 Windows 文本注入;Linux/macOS 尚未实现。

Expand All @@ -162,6 +163,7 @@ Set-ExecutionPolicy -Scope Process Bypass

- [前置条件与兼容性判断](docs/prerequisites.md)
- [Redmi G Pro 27U 2026 完整教程](docs/redmi-g-pro-27u-2026.md)
- [显示器端 ADB Keeper](companion/adb-keeper/README.md)
- [适配其他 Android 智能显示器](docs/adapt-new-device.md)
- [配置字段说明](docs/configuration.md)
- [故障排查](docs/troubleshooting.md)
Expand Down
2 changes: 2 additions & 0 deletions companion/adb-keeper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build/
/out/
59 changes: 59 additions & 0 deletions companion/adb-keeper/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.github.androiddisplayvoicebridge.adbkeeper">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />

<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />

<application
android:allowBackup="false"
android:directBootAware="true"
android:label="ADB Keeper"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.Light">

<activity
android:name="io.github.androiddisplayvoicebridge.adbkeeper.MainActivity"
android:exported="true"
android:permission="android.permission.DUMP" />

<receiver
android:name="io.github.androiddisplayvoicebridge.adbkeeper.BootReceiver"
android:directBootAware="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="mitv.action.STR_BOOT_COMPLETED" />
<action android:name="mitv.intent.action.STARTAPPS_PRIORITY_HIGH_PENDING" />
<action android:name="mitv.intent.action.STARTAPPS_PRIORITY_MIDDLE_PENDING" />
<action android:name="mitv.intent.action.STARTAPPS_PRIORITY_NORMAL_PENDING" />
</intent-filter>
</receiver>

<receiver
android:name="io.github.androiddisplayvoicebridge.adbkeeper.CheckReceiver"
android:directBootAware="true"
android:exported="false" />

<!-- A diagnostic hook callable by the ADB shell, not ordinary apps. -->
<receiver
android:name="io.github.androiddisplayvoicebridge.adbkeeper.ShellReceiver"
android:exported="true"
android:permission="android.permission.DUMP">
<intent-filter>
<action android:name="io.github.androiddisplayvoicebridge.adbkeeper.SCHEDULE" />
</intent-filter>
</receiver>
</application>
</manifest>
97 changes: 97 additions & 0 deletions companion/adb-keeper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ADB Keeper companion

ADB Keeper is an optional on-display companion for Android/HyperOS displays whose
firmware turns network ADB off after a reboot. It checks the display's local TCP
port 5555 after boot and every 15 minutes. It only toggles the Android ADB setting
when the port is not listening, so an active connection is not periodically reset.

This companion is not a security bypass. The display must have ADB enabled once,
the APK must be installed through an already-authorized ADB connection, and the
privileged settings permission must be granted explicitly from that connection.
Android's normal RSA host authorization remains in effect.

## Install and authorize

```powershell
$adb = 'adb.exe'
$serial = 'DISPLAY_IP:5555'
$apk = '.\companion\adb-keeper\out\ADB-Keeper-v0.1.4.apk'

& $adb -s $serial install -r $apk
& $adb -s $serial shell pm grant `
io.github.androiddisplayvoicebridge.adbkeeper `
android.permission.WRITE_SECURE_SETTINGS
& $adb -s $serial shell am start -n `
io.github.androiddisplayvoicebridge.adbkeeper/.MainActivity
```

The app intentionally has no launcher/TV-home icon. Some display firmwares block
unknown sideloaded launcher apps during boot. Keeping the companion headless avoids
that vendor launcher policy while its explicit diagnostic activity remains available
through ADB.

In addition to Android's standard boot broadcasts, the receiver recognizes Xiaomi
TV's staged `mitv.action.STR_BOOT_COMPLETED` and `STARTAPPS_PRIORITY_*` broadcasts.
Other brands can add their equivalent actions without changing the repair logic.

### Xiaomi/Redmi TV compatibility identity

Redmi G Pro 27U 2026 firmware `OS3.0.102.0.UFFMAMN` includes the unused compatibility
identity `com.example.autoreboot` in its boot-autostart allowlist. A device-specific
build can use it without weakening the allowlist for every sideloaded application:

```powershell
.\build.ps1 -ApplicationId com.example.autoreboot
```

This produces `ADB-Keeper-MiTV-v0.1.4.apk`. Confirm that the identity is not already
installed (`adb shell pm path com.example.autoreboot`) before using this variant. Its
component classes intentionally remain in the project's GitHub namespace.

Do not use `com.example.autorun1` on this firmware. Although it also appears in the
autostart allowlist, the package installer explicitly blacklists that identity.

Install one or more displays with [`install-mitv.ps1`](install-mitv.ps1). The
script uses `adb.exe` from `PATH`; use `-AdbPath` if Platform Tools is elsewhere:

```powershell
.\install-mitv.ps1 `
-Serial 192.168.1.101:5555,192.168.1.102:5555 `
-ApkPath .\out\ADB-Keeper-MiTV-v0.1.4.apk
```

On the tested firmware, network ADB briefly appears during boot, is reset by HyperOS,
and is restored by the companion roughly 1–2 minutes after power-on. This identity is
a firmware-specific compatibility mechanism, not a universal Xiaomi package name;
verify the device whitelist and perform a reboot test before documenting another model
as supported.

For a controlled recovery test, schedule a check before turning ADB off. The
receiver requires Android's `DUMP` permission, which the ADB shell has but normal
third-party apps do not:

```powershell
& $adb -s $serial shell am broadcast `
-a io.github.androiddisplayvoicebridge.adbkeeper.SCHEDULE `
--el delay_ms 30000 `
-p io.github.androiddisplayvoicebridge.adbkeeper
& $adb -s $serial shell settings put global adb_enabled 0
```

If `pm grant` is rejected, that firmware does not allow this non-root approach.
Uninstalling the APK removes the keeper; it does not remove the display's own ADB
or developer settings.

## Security note

Keeping TCP ADB available exposes port 5555 on the display's LAN interface. Use
this only on a trusted private network, do not forward port 5555 on the router,
and remove authorizations for computers you no longer control.

## Build

`build.ps1` uses `JAVA_HOME`, `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), Android
platform 34, and Build Tools 36.0.0. You can also pass `-JavaHome`, `-SdkRoot`,
and `-Keystore` explicitly. The signing keystore is deliberately stored outside
the repository. Keep it backed up: Android requires the same signing identity for
future in-place upgrades.
92 changes: 92 additions & 0 deletions companion/adb-keeper/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
param(
[string]$SdkRoot = $(if ($env:ANDROID_SDK_ROOT) { $env:ANDROID_SDK_ROOT } else { $env:ANDROID_HOME }),
[string]$JavaHome = $env:JAVA_HOME,
[string]$Keystore = (Join-Path $env:USERPROFILE '.android\adb-keeper-release.jks'),
[string]$ApplicationId = 'io.github.androiddisplayvoicebridge.adbkeeper'
)

$ErrorActionPreference = 'Stop'
$project = Split-Path -Parent $MyInvocation.MyCommand.Path
$build = Join-Path $project 'build'
$classes = Join-Path $build 'classes'
$dex = Join-Path $build 'dex'
$out = Join-Path $project 'out'
$androidJar = Join-Path $SdkRoot 'platforms\android-34\android.jar'
$tools = Join-Path $SdkRoot 'build-tools\36.0.0'
$javac = Join-Path $JavaHome 'bin\javac.exe'

foreach ($required in @($androidJar, $javac, (Join-Path $tools 'aapt2.exe'))) {
if (-not (Test-Path -LiteralPath $required)) {
throw "Missing build dependency: $required"
}
}

Remove-Item -LiteralPath $build -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $classes, $dex, $out -Force | Out-Null

$sources = Get-ChildItem -LiteralPath (Join-Path $project 'src') -Recurse -Filter '*.java'
& $javac -encoding UTF-8 -source 8 -target 8 -bootclasspath $androidJar -d $classes $sources.FullName
if ($LASTEXITCODE -ne 0) { throw 'javac failed' }

$classFiles = Get-ChildItem -LiteralPath $classes -Recurse -Filter '*.class'
& (Join-Path $tools 'd8.bat') --lib $androidJar --min-api 23 --output $dex $classFiles.FullName
if ($LASTEXITCODE -ne 0) { throw 'd8 failed' }

$unsigned = Join-Path $build 'adb-keeper-unsigned.apk'
$packageArgs = @()
if ($ApplicationId -ne 'io.github.androiddisplayvoicebridge.adbkeeper') {
$packageArgs = @('--rename-manifest-package', $ApplicationId)
}
& (Join-Path $tools 'aapt2.exe') link `
-I $androidJar `
--manifest (Join-Path $project 'AndroidManifest.xml') `
--min-sdk-version 23 `
--target-sdk-version 34 `
--version-code 5 `
--version-name '0.1.4' `
@packageArgs `
-o $unsigned
if ($LASTEXITCODE -ne 0) { throw 'aapt2 link failed' }

Push-Location $dex
try {
& (Join-Path $tools 'aapt.exe') add $unsigned 'classes.dex'
if ($LASTEXITCODE -ne 0) { throw 'aapt add failed' }
} finally {
Pop-Location
}

$aligned = Join-Path $build 'adb-keeper-aligned.apk'
& (Join-Path $tools 'zipalign.exe') -f 4 $unsigned $aligned
if ($LASTEXITCODE -ne 0) { throw 'zipalign failed' }

if (-not (Test-Path -LiteralPath $Keystore)) {
New-Item -ItemType Directory -Path (Split-Path -Parent $Keystore) -Force | Out-Null
& (Join-Path $JavaHome 'bin\keytool.exe') -genkeypair `
-keystore $Keystore `
-storepass 'android-display-voice-bridge' `
-keypass 'android-display-voice-bridge' `
-alias 'adb-keeper' `
-keyalg RSA `
-keysize 3072 `
-validity 10000 `
-dname 'CN=Android Display Voice Bridge, O=Community, C=CN'
if ($LASTEXITCODE -ne 0) { throw 'keytool failed' }
}

$variant = if ($ApplicationId -eq 'io.github.androiddisplayvoicebridge.adbkeeper') { '' } else { '-MiTV' }
$apk = Join-Path $out "ADB-Keeper$variant-v0.1.4.apk"
& (Join-Path $tools 'apksigner.bat') sign `
--ks $Keystore `
--ks-key-alias 'adb-keeper' `
--ks-pass 'pass:android-display-voice-bridge' `
--key-pass 'pass:android-display-voice-bridge' `
--out $apk `
$aligned
if ($LASTEXITCODE -ne 0) { throw 'apksigner failed' }

& (Join-Path $tools 'apksigner.bat') verify --verbose $apk
if ($LASTEXITCODE -ne 0) { throw 'APK verification failed' }

Get-FileHash -LiteralPath $apk -Algorithm SHA256
Write-Host "Built $apk"
46 changes: 46 additions & 0 deletions companion/adb-keeper/install-mitv.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
param(
[Parameter(Mandatory = $true)]
[string[]]$Serial,
[string]$ApkPath = (Join-Path $PSScriptRoot 'out\ADB-Keeper-MiTV-v0.1.4.apk'),
[string]$AdbPath = 'adb.exe'
)

$ErrorActionPreference = 'Stop'
$packageId = 'com.example.autoreboot'
$activity = 'io.github.androiddisplayvoicebridge.adbkeeper.MainActivity'

if (-not (Test-Path -LiteralPath $ApkPath)) {
throw "Missing APK: $ApkPath"
}

$adbCommand = Get-Command $AdbPath -ErrorAction SilentlyContinue
if (-not $adbCommand) {
throw "ADB was not found. Install Android Platform Tools or pass -AdbPath."
}
$AdbPath = $adbCommand.Source

foreach ($device in $Serial) {
Write-Host "Configuring $device"
& $AdbPath connect $device | Out-Host
if ((& $AdbPath -s $device get-state 2>$null) -ne 'device') {
throw "ADB device is not authorized or online: $device"
}

& $AdbPath -s $device install -r $ApkPath | Out-Host
if ($LASTEXITCODE -ne 0) { throw "APK install failed on $device" }

& $AdbPath -s $device shell pm grant $packageId `
android.permission.WRITE_SECURE_SETTINGS
if ($LASTEXITCODE -ne 0) { throw "WRITE_SECURE_SETTINGS grant failed on $device" }

& $AdbPath -s $device shell cmd deviceidle whitelist "+$packageId" | Out-Host
& $AdbPath -s $device shell cmd appops set $packageId RUN_IN_BACKGROUND allow
& $AdbPath -s $device shell cmd appops set $packageId RUN_ANY_IN_BACKGROUND allow
& $AdbPath -s $device shell am start -W -n "$packageId/$activity" | Out-Host

$permission = & $AdbPath -s $device shell dumpsys package $packageId |
Select-String 'WRITE_SECURE_SETTINGS: granted=true'
if (-not $permission) { throw "Permission verification failed on $device" }

Write-Host "$device configured. Reboot the display and allow up to two minutes for ADB recovery."
}
Loading
Loading