From d2547bc3e00a7f5d2e2739f7b3901b39cbbde406 Mon Sep 17 00:00:00 2001 From: Evilom <33251918+Evilom@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:26:02 +0800 Subject: [PATCH] feat: add reboot-persistent ADB keeper --- CHANGELOG.md | 9 ++ README.md | 2 + companion/adb-keeper/.gitignore | 2 + companion/adb-keeper/AndroidManifest.xml | 59 ++++++++ companion/adb-keeper/README.md | 97 +++++++++++++ companion/adb-keeper/build.ps1 | 92 +++++++++++++ companion/adb-keeper/install-mitv.ps1 | 46 +++++++ .../adbkeeper/AdbKeeper.java | 128 ++++++++++++++++++ .../adbkeeper/BootReceiver.java | 25 ++++ .../adbkeeper/CheckReceiver.java | 23 ++++ .../adbkeeper/MainActivity.java | 70 ++++++++++ .../adbkeeper/ShellReceiver.java | 16 +++ docs/redmi-g-pro-27u-2026.md | 36 ++++- docs/troubleshooting.md | 1 + 14 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 companion/adb-keeper/.gitignore create mode 100644 companion/adb-keeper/AndroidManifest.xml create mode 100644 companion/adb-keeper/README.md create mode 100644 companion/adb-keeper/build.ps1 create mode 100644 companion/adb-keeper/install-mitv.ps1 create mode 100644 companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/AdbKeeper.java create mode 100644 companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/BootReceiver.java create mode 100644 companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/CheckReceiver.java create mode 100644 companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/MainActivity.java create mode 100644 companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/ShellReceiver.java diff --git a/CHANGELOG.md b/CHANGELOG.md index babd2c9..a20191c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 1ef745b..c80b684 100644 --- a/README.md +++ b/README.md @@ -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 尚未实现。 @@ -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) diff --git a/companion/adb-keeper/.gitignore b/companion/adb-keeper/.gitignore new file mode 100644 index 0000000..a07d5ed --- /dev/null +++ b/companion/adb-keeper/.gitignore @@ -0,0 +1,2 @@ +/build/ +/out/ diff --git a/companion/adb-keeper/AndroidManifest.xml b/companion/adb-keeper/AndroidManifest.xml new file mode 100644 index 0000000..729e419 --- /dev/null +++ b/companion/adb-keeper/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/companion/adb-keeper/README.md b/companion/adb-keeper/README.md new file mode 100644 index 0000000..886d1fe --- /dev/null +++ b/companion/adb-keeper/README.md @@ -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. diff --git a/companion/adb-keeper/build.ps1 b/companion/adb-keeper/build.ps1 new file mode 100644 index 0000000..a73e47b --- /dev/null +++ b/companion/adb-keeper/build.ps1 @@ -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" diff --git a/companion/adb-keeper/install-mitv.ps1 b/companion/adb-keeper/install-mitv.ps1 new file mode 100644 index 0000000..06f4b54 --- /dev/null +++ b/companion/adb-keeper/install-mitv.ps1 @@ -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." +} diff --git a/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/AdbKeeper.java b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/AdbKeeper.java new file mode 100644 index 0000000..b046321 --- /dev/null +++ b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/AdbKeeper.java @@ -0,0 +1,128 @@ +package io.github.androiddisplayvoicebridge.adbkeeper; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.Log; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +final class AdbKeeper { + static final String TAG = "AdbKeeper"; + static final String ACTION_CHECK = + "io.github.androiddisplayvoicebridge.adbkeeper.CHECK"; + private static final int ADB_PORT = 5555; + private static final long CHECK_INTERVAL_MS = 15L * 60L * 1000L; + + private AdbKeeper() { + } + + static Result repairIfNeeded(Context context, boolean force) { + boolean wasListening = isLocalAdbListening(); + boolean repaired = false; + + try { + Settings.Global.putInt( + context.getContentResolver(), + Settings.Global.STAY_ON_WHILE_PLUGGED_IN, + 7); + + int enabled = Settings.Global.getInt( + context.getContentResolver(), Settings.Global.ADB_ENABLED, 0); + + if (force || !wasListening) { + if (enabled == 1) { + Settings.Global.putInt( + context.getContentResolver(), Settings.Global.ADB_ENABLED, 0); + SystemClock.sleep(500); + } + + Settings.Global.putInt( + context.getContentResolver(), Settings.Global.ADB_ENABLED, 1); + repaired = true; + SystemClock.sleep(2500); + } + + boolean listeningNow = isLocalAdbListening(); + Log.i(TAG, "check complete: before=" + wasListening + + ", repaired=" + repaired + ", after=" + listeningNow); + return new Result(wasListening, repaired, listeningNow, null); + } catch (SecurityException exception) { + Log.e(TAG, "WRITE_SECURE_SETTINGS has not been granted", exception); + return new Result(wasListening, repaired, false, + "缺少 WRITE_SECURE_SETTINGS 授权"); + } catch (RuntimeException exception) { + Log.e(TAG, "repair failed", exception); + return new Result(wasListening, repaired, false, exception.toString()); + } + } + + static void scheduleNextCheck(Context context, long initialDelayMs) { + AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (alarms == null) { + Log.w(TAG, "AlarmManager unavailable"); + return; + } + + Intent intent = new Intent(context, CheckReceiver.class).setAction(ACTION_CHECK); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, + 5555, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + alarms.setAndAllowWhileIdle( + AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + initialDelayMs, + pendingIntent); + } + + static void scheduleRegularCheck(Context context) { + scheduleNextCheck(context, CHECK_INTERVAL_MS); + } + + private static boolean isLocalAdbListening() { + Socket socket = new Socket(); + try { + socket.connect(new InetSocketAddress("127.0.0.1", ADB_PORT), 700); + return true; + } catch (IOException ignored) { + return false; + } finally { + try { + socket.close(); + } catch (IOException ignored) { + // Nothing to do. + } + } + } + + static final class Result { + final boolean listeningBefore; + final boolean repaired; + final boolean listeningAfter; + final String error; + + Result(boolean listeningBefore, boolean repaired, boolean listeningAfter, String error) { + this.listeningBefore = listeningBefore; + this.repaired = repaired; + this.listeningAfter = listeningAfter; + this.error = error; + } + + String describe() { + if (error != null) { + return "失败:" + error; + } + if (listeningAfter) { + return repaired ? "ADB 5555 已重新启动" : "ADB 5555 正常,无需修复"; + } + return "已写入 ADB 开关,但 5555 仍未监听"; + } + } +} diff --git a/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/BootReceiver.java b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/BootReceiver.java new file mode 100644 index 0000000..4fe8824 --- /dev/null +++ b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/BootReceiver.java @@ -0,0 +1,25 @@ +package io.github.androiddisplayvoicebridge.adbkeeper; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public final class BootReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, Intent intent) { + Log.i(AdbKeeper.TAG, "boot trigger received: " + intent.getAction()); + final PendingResult pending = goAsync(); + new Thread(new Runnable() { + @Override + public void run() { + try { + AdbKeeper.repairIfNeeded(context.getApplicationContext(), false); + AdbKeeper.scheduleNextCheck(context.getApplicationContext(), 45L * 1000L); + } finally { + pending.finish(); + } + } + }, "adb-keeper-boot").start(); + } +} diff --git a/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/CheckReceiver.java b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/CheckReceiver.java new file mode 100644 index 0000000..b645db6 --- /dev/null +++ b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/CheckReceiver.java @@ -0,0 +1,23 @@ +package io.github.androiddisplayvoicebridge.adbkeeper; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public final class CheckReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, Intent intent) { + final PendingResult pending = goAsync(); + new Thread(new Runnable() { + @Override + public void run() { + try { + AdbKeeper.repairIfNeeded(context.getApplicationContext(), false); + AdbKeeper.scheduleRegularCheck(context.getApplicationContext()); + } finally { + pending.finish(); + } + } + }, "adb-keeper-check").start(); + } +} diff --git a/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/MainActivity.java b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/MainActivity.java new file mode 100644 index 0000000..740915c --- /dev/null +++ b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/MainActivity.java @@ -0,0 +1,70 @@ +package io.github.androiddisplayvoicebridge.adbkeeper; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +public final class MainActivity extends Activity { + private TextView status; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setGravity(Gravity.CENTER); + int padding = dp(32); + layout.setPadding(padding, padding, padding, padding); + + TextView title = new TextView(this); + title.setText("ADB Keeper"); + title.setTextSize(28); + layout.addView(title); + + status = new TextView(this); + status.setText("正在检查……"); + status.setTextSize(18); + status.setPadding(0, dp(24), 0, dp(24)); + layout.addView(status); + + Button repair = new Button(this); + repair.setText("立即检查并修复"); + repair.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + runCheck(true); + } + }); + layout.addView(repair); + + setContentView(layout); + runCheck(false); + } + + private void runCheck(final boolean force) { + status.setText("正在检查……"); + new Thread(new Runnable() { + @Override + public void run() { + final AdbKeeper.Result result = + AdbKeeper.repairIfNeeded(getApplicationContext(), force); + AdbKeeper.scheduleRegularCheck(getApplicationContext()); + runOnUiThread(new Runnable() { + @Override + public void run() { + status.setText(result.describe()); + } + }); + } + }, "adb-keeper-ui").start(); + } + + private int dp(int value) { + return (int) (value * getResources().getDisplayMetrics().density + 0.5f); + } +} diff --git a/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/ShellReceiver.java b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/ShellReceiver.java new file mode 100644 index 0000000..50b2c5c --- /dev/null +++ b/companion/adb-keeper/src/io/github/androiddisplayvoicebridge/adbkeeper/ShellReceiver.java @@ -0,0 +1,16 @@ +package io.github.androiddisplayvoicebridge.adbkeeper; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public final class ShellReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + long delayMs = intent.getLongExtra("delay_ms", 30_000L); + delayMs = Math.max(5_000L, Math.min(delayMs, 10L * 60L * 1000L)); + AdbKeeper.scheduleNextCheck(context.getApplicationContext(), delayMs); + Log.i(AdbKeeper.TAG, "shell requested a check in " + delayMs + " ms"); + } +} diff --git a/docs/redmi-g-pro-27u-2026.md b/docs/redmi-g-pro-27u-2026.md index 6eac1a7..ab24247 100644 --- a/docs/redmi-g-pro-27u-2026.md +++ b/docs/redmi-g-pro-27u-2026.md @@ -129,7 +129,33 @@ Set-ExecutionPolicy -Scope Process Bypass adb -s 192.168.1.101:5555 shell settings put global stay_on_while_plugged_in 0 ``` -## 7. 手工修改配置 +## 7. 完整重启后 ADB 自动关闭 + +固件 `OS3.0.102.0.UFFMAMN` 会在启动早期主动停止普通侧载应用,并在网络 ADB +短暂出现后再次关闭它。`keepAndroidAwakeWhilePowered` 只能缓解待机,不能解决完整 +重启。 + +仓库提供可选的设备端 [ADB Keeper](../companion/adb-keeper/README.md)。该固件预留了 +未安装的 `com.example.autoreboot` 自启动兼容身份;MiTV 构建使用该身份后可以收到 +开机广播,在 HyperOS 二次复位后重新写入 `adb_enabled=1`。两台 Redmi G Pro 27U +2026 已完成真实重启验证,恢复通常需要 1–2 分钟。 + +前提与风险: + +- 第一次安装前仍需手动打开 ADB,并接受电脑的 RSA 授权。 +- 必须用 ADB 一次性授予 `WRITE_SECURE_SETTINGS`;它不会显示普通权限弹窗。 +- 5555 会长期暴露在显示器所在局域网,只能用于可信家庭网络,禁止路由器端口转发。 +- 其他固件未必拥有相同白名单,不能直接假设兼容。 + +安装已构建的 MiTV APK: + +```powershell +.\companion\adb-keeper\install-mitv.ps1 ` + -Serial 192.168.1.101:5555,192.168.1.102:5555 ` + -ApkPath .\companion\adb-keeper\out\ADB-Keeper-MiTV-v0.1.4.apk +``` + +## 8. 手工修改配置 配置文件: @@ -139,7 +165,7 @@ adb -s 192.168.1.101:5555 shell settings put global stay_on_while_plugged_in 0 退出通知区域程序后编辑,保存,再重新启动 EXE。IP 变化时只需要改 `devices[].serial`。若显示器升级后按键失效,重新探测 `inputDevice` 和 `keyToken`。 -## 8. 回退与卸载 +## 9. 回退与卸载 只退出程序:右击通知区域图标 → 退出。由于 profile 默认不在退出时 disable 包,小爱应恢复原生行为。 @@ -162,3 +188,9 @@ adb -s 192.168.1.101:5555 shell pm enable --user 0 com.xiaomi.voicecontrol ``` 卸载不会关闭显示器上的 ADB。若不再调试,请到显示器设置中手动关闭。 + +移除显示器端 ADB Keeper: + +```powershell +adb -s 192.168.1.101:5555 uninstall com.example.autoreboot +``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index a3a7d20..a0dfbcb 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -13,6 +13,7 @@ | `unauthorized` | 显示器尚未确认 RSA | 查看显示器弹窗;必要时撤销调试授权后重连 | | `offline` / 反复断开 | IP 变化、端口变化、休眠或网络隔离 | 核对 IP;做 DHCP 保留;检查 AP 隔离 | | 开关显示已开,但 5555 拒绝连接 | 固件待机后停止了 `adbd` | 关→开 ADB;确认功耗后启用 `keepAndroidAwakeWhilePowered` | +| 完整重启后 5555 先出现再关闭 | HyperOS 启动阶段复位 ADB | Redmi G Pro 27U 2026 可选装 [ADB Keeper](../companion/adb-keeper/README.md);其他型号先验证白名单 | | 按遥控器没有 `microphone DOWN` | `inputDevice` / `keyToken` 错误 | 重新运行 `diagnose-device.ps1 -Mode keys` | | 有 DOWN / UP,没有最终文本 | logcat tag 或正则错误;云 ASR 不可用 | 用 `-Mode logs` 重新取样;先验证原生助手能识别 | | 有 `ignored unarmed final ASR` | 最终日志超过等待窗口,或按键事件没有被捕获 | 先修按键配置;必要时调大 timeout |