mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(android): harden physical-device runs in device prep
Device prep now disables the AOSP cached-app freezer, phantom-process killer, and Doze (and exempts the driver) so OEM background management stops suspending the driver mid-run. Adds ReinstallApp for clear-state on ROMs that deny pm clear, and teaches focus detection to report the notification shade as systemui so the scope guard can dismiss it.
This commit is contained in:
1 parent
da7557ac0b
commit
300af1305e
2 files changed
+111
-4
No files matched your search
@@ -68,11 +68,11 @@ func PrepareDevice(ctx context.Context, serial string, stdout io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, shellCommand := range [][]string{
|
for _, shellCommand := range append([][]string{
|
||||||
{"svc", "power", "stayon", "true"},
|
{"svc", "power", "stayon", "true"},
|
||||||
{"input", "keyevent", "KEYCODE_WAKEUP"},
|
{"input", "keyevent", "KEYCODE_WAKEUP"},
|
||||||
{"wm", "dismiss-keyguard"},
|
{"wm", "dismiss-keyguard"},
|
||||||
} {
|
}, antiFreezeCommands()...) {
|
||||||
args := []string{}
|
args := []string{}
|
||||||
if serial != "" {
|
if serial != "" {
|
||||||
args = append(args, "-s", serial)
|
args = append(args, "-s", serial)
|
||||||
@@ -85,6 +85,57 @@ func PrepareDevice(ctx context.Context, serial string, stdout io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// driverPackages are the on-device native-driver instrumentation packages that
|
||||||
|
// the platform and OEM background freezers must not suspend mid-run.
|
||||||
|
var driverPackages = []string{"dev.mobile.maestro", "dev.mobile.maestro.test"}
|
||||||
|
|
||||||
|
// antiFreezeCommands turns off the background-process freezers that suspend the
|
||||||
|
// driver between actions. Android 12+ adds a cached-app freezer and a
|
||||||
|
// phantom-process killer; OEM builds (e.g. OnePlus/Oppo ColorOS OSense) add
|
||||||
|
// their own. Left on, they freeze the driver instrumentation while the app is
|
||||||
|
// foreground and the run stalls. set_sync_disabled_for_tests keeps the
|
||||||
|
// device_config writes from being reverted by server-side sync. All best effort:
|
||||||
|
// the caller skips and logs any command an OEM build rejects.
|
||||||
|
func antiFreezeCommands() [][]string {
|
||||||
|
commands := [][]string{
|
||||||
|
{"device_config", "set_sync_disabled_for_tests", "persistent"},
|
||||||
|
{"device_config", "put", "activity_manager_native_boot", "use_freezer", "false"},
|
||||||
|
{"device_config", "put", "activity_manager_native_boot", "freeze_exempt_inst_pkg", strings.Join(driverPackages, ",")},
|
||||||
|
{"settings", "put", "global", "settings_enable_monitor_phantom_procs", "false"},
|
||||||
|
{"device_config", "put", "activity_manager", "max_phantom_processes", "2147483647"},
|
||||||
|
{"dumpsys", "deviceidle", "disable"},
|
||||||
|
}
|
||||||
|
for _, pkg := range driverPackages {
|
||||||
|
commands = append(commands, []string{"dumpsys", "deviceidle", "whitelist", "+" + pkg})
|
||||||
|
}
|
||||||
|
return commands
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReinstallApp resets an app to first-launch state by uninstalling and
|
||||||
|
// reinstalling it. This replaces `pm clear` for clear-state: ColorOS and other
|
||||||
|
// hardened OEM builds deny CLEAR_APP_USER_DATA even to the adb shell user, so a
|
||||||
|
// clear aborts the launch, whereas uninstall+install is always permitted.
|
||||||
|
// The uninstall is best effort so a not-installed app is not an error.
|
||||||
|
func ReinstallApp(ctx context.Context, serial, bundleID, apkPath string, stdout io.Writer) error {
|
||||||
|
adb, err := AdbBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
withSerial := func(args ...string) []string {
|
||||||
|
if serial != "" {
|
||||||
|
return append([]string{"-s", serial}, args...)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
if output, err := exec.CommandContext(ctx, adb, withSerial("uninstall", bundleID)...).CombinedOutput(); err != nil {
|
||||||
|
fmt.Fprintf(stdout, "clear-state: uninstall %s skipped (%v: %s)\n", bundleID, err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
if output, err := exec.CommandContext(ctx, adb, withSerial("install", "-r", apkPath)...).CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("install %s: %w: %s", apkPath, err, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AdbReverse sets up adb reverse forwarding for a local abstract socket.
|
// AdbReverse sets up adb reverse forwarding for a local abstract socket.
|
||||||
func AdbReverse(socket string, port int) error {
|
func AdbReverse(socket string, port int) error {
|
||||||
adb, err := AdbBinary()
|
adb, err := AdbBinary()
|
||||||
@@ -314,7 +365,10 @@ func FocusedWindowPackage(ctx context.Context) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
output, err := exec.CommandContext(ctx, adb, "shell", "dumpsys", "window").Output()
|
// Grep the focus line on-device: the full dumpsys window output is large and
|
||||||
|
// this runs on the per-step scope guard, so transferring it whole would add
|
||||||
|
// latency to every step.
|
||||||
|
output, err := exec.CommandContext(ctx, adb, "shell", "dumpsys window | grep mCurrentFocus").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -340,9 +394,22 @@ func parseForegroundPackage(dumpsys string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// systemUIPackage is the owner reported for system overlays (notification
|
||||||
|
// shade, quick settings) that take window focus without a package/activity
|
||||||
|
// component name. The scope guard treats it as "not the app" and dismisses it.
|
||||||
|
const systemUIPackage = "com.android.systemui"
|
||||||
|
|
||||||
|
// systemOverlayWindowNames are the mCurrentFocus window names for the system
|
||||||
|
// panels a fuzzer gesture can pull over the app (a swipe from the status bar
|
||||||
|
// opens NotificationShade). They own focus while the app stays the resumed
|
||||||
|
// activity, so the resumed-activity signal alone misses them.
|
||||||
|
var systemOverlayWindowNames = []string{"NotificationShade", "ShadePanel", "QuickSettings", "VolumeUiDialog"}
|
||||||
|
|
||||||
// parseFocusedWindowPackage extracts the focused-window package from
|
// parseFocusedWindowPackage extracts the focused-window package from
|
||||||
// `dumpsys window` output by reading the mCurrentFocus component name. A
|
// `dumpsys window` output by reading the mCurrentFocus component name. A
|
||||||
// "mCurrentFocus=null" line (no focused window) yields "".
|
// "mCurrentFocus=null" line (no focused window) yields "". A system overlay
|
||||||
|
// (e.g. the notification shade) yields systemUIPackage so callers can tell it
|
||||||
|
// apart from the app and from "no focus".
|
||||||
func parseFocusedWindowPackage(dumpsys string) string {
|
func parseFocusedWindowPackage(dumpsys string) string {
|
||||||
for line := range strings.SplitSeq(dumpsys, "\n") {
|
for line := range strings.SplitSeq(dumpsys, "\n") {
|
||||||
if !strings.Contains(line, "mCurrentFocus") {
|
if !strings.Contains(line, "mCurrentFocus") {
|
||||||
@@ -351,6 +418,11 @@ func parseFocusedWindowPackage(dumpsys string) string {
|
|||||||
if match := resumedActivityPackage.FindStringSubmatch(line); match != nil {
|
if match := resumedActivityPackage.FindStringSubmatch(line); match != nil {
|
||||||
return match[1]
|
return match[1]
|
||||||
}
|
}
|
||||||
|
for _, overlay := range systemOverlayWindowNames {
|
||||||
|
if strings.Contains(line, overlay) {
|
||||||
|
return systemUIPackage
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package android
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -185,6 +186,11 @@ func TestParseFocusedWindowPackage(t *testing.T) {
|
|||||||
dumpsys: " some unrelated dumpsys window output\n",
|
dumpsys: " some unrelated dumpsys window output\n",
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "notification shade focused",
|
||||||
|
dumpsys: " mCurrentFocus=Window{885e289 u0 NotificationShade}",
|
||||||
|
want: "com.android.systemui",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -194,3 +200,32 @@ func TestParseFocusedWindowPackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAntiFreezeCommands_DisablesFreezersAndExemptsDriver(t *testing.T) {
|
||||||
|
commands := antiFreezeCommands()
|
||||||
|
joined := make([]string, len(commands))
|
||||||
|
for i, c := range commands {
|
||||||
|
joined[i] = strings.Join(c, " ")
|
||||||
|
}
|
||||||
|
all := strings.Join(joined, "\n")
|
||||||
|
|
||||||
|
wantContains := []string{
|
||||||
|
"device_config set_sync_disabled_for_tests persistent",
|
||||||
|
"device_config put activity_manager_native_boot use_freezer false",
|
||||||
|
"settings put global settings_enable_monitor_phantom_procs false",
|
||||||
|
"dumpsys deviceidle disable",
|
||||||
|
}
|
||||||
|
for _, want := range wantContains {
|
||||||
|
if !strings.Contains(all, want) {
|
||||||
|
t.Errorf("anti-freeze commands missing %q\ngot:\n%s", want, all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, pkg := range driverPackages {
|
||||||
|
if !strings.Contains(all, "freeze_exempt_inst_pkg") || !strings.Contains(all, pkg) {
|
||||||
|
t.Errorf("driver package %q not exempted from freezer\ngot:\n%s", pkg, all)
|
||||||
|
}
|
||||||
|
if !strings.Contains(all, "deviceidle whitelist +"+pkg) {
|
||||||
|
t.Errorf("driver package %q not whitelisted from doze\ngot:\n%s", pkg, all)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user