mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(driver): clear-state via APK reinstall when pm clear is blocked
When an APK path is set, Android clear-state resets the app by uninstalling and reinstalling instead of asking the sidecar to pm clear, which hardened OEM builds (ColorOS) deny even to the adb shell user. Falls back to the sidecar clear path when no APK path is provided.
This commit is contained in:
1 parent
300af1305e
commit
015f619693
2 files changed
+111
-2
No files matched your search
@@ -4,6 +4,7 @@ package sidecar
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
@@ -18,6 +19,19 @@ type Client struct {
|
||||
connection *grpc.ClientConn
|
||||
stub driverpb.DriverClient
|
||||
platform string
|
||||
|
||||
// serial, apkPath, and output drive the Android clear-state reinstall: when
|
||||
// apkPath is set, Launch resets state by uninstalling and reinstalling the
|
||||
// APK instead of asking the sidecar to `pm clear`, which hardened OEM builds
|
||||
// deny. Empty apkPath leaves the legacy `pm clear` path in place.
|
||||
serial string
|
||||
apkPath string
|
||||
output io.Writer
|
||||
|
||||
// reinstallApp resets an Android app to first-launch state. A seam so tests
|
||||
// exercise the clear-state branch without a connected device; defaults to
|
||||
// android.ReinstallApp.
|
||||
reinstallApp func(ctx context.Context, serial, bundleID, apkPath string, output io.Writer) error
|
||||
}
|
||||
|
||||
// SetPlatform records the target platform so capability methods (e.g.
|
||||
@@ -25,6 +39,15 @@ type Client struct {
|
||||
// Dial.
|
||||
func (c *Client) SetPlatform(platform string) { c.platform = platform }
|
||||
|
||||
// SetClearStateReinstall makes Android clear-state reset the app by reinstalling
|
||||
// the APK at apkPath (uninstall+install) rather than `pm clear`. serial targets
|
||||
// the device when several are connected; output receives progress lines.
|
||||
func (c *Client) SetClearStateReinstall(serial, apkPath string, output io.Writer) {
|
||||
c.serial = serial
|
||||
c.apkPath = apkPath
|
||||
c.output = output
|
||||
}
|
||||
|
||||
// ForegroundApp reports the foreground package. Only Android is supported (via
|
||||
// adb); other platforms return "" so the runner skips app-scope enforcement.
|
||||
func (c *Client) ForegroundApp(ctx context.Context) (string, error) {
|
||||
@@ -51,7 +74,11 @@ func Dial(address string) (*Client, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial sidecar: %w", err)
|
||||
}
|
||||
return &Client{connection: connection, stub: driverpb.NewDriverClient(connection)}, nil
|
||||
return &Client{
|
||||
connection: connection,
|
||||
stub: driverpb.NewDriverClient(connection),
|
||||
reinstallApp: android.ReinstallApp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Close() error { return c.connection.Close() }
|
||||
@@ -76,9 +103,19 @@ func (c *Client) WaitForHealth(ctx context.Context, pollInterval time.Duration)
|
||||
}
|
||||
|
||||
func (c *Client) Launch(ctx context.Context, bundleID string, clearState bool, env map[string]string) error {
|
||||
sidecarClearState := clearState
|
||||
if clearState && c.platform == "android" && c.apkPath != "" {
|
||||
if c.output != nil {
|
||||
fmt.Fprintf(c.output, "clear-state: reinstalling %s from %s\n", bundleID, c.apkPath)
|
||||
}
|
||||
if err := c.reinstallApp(ctx, c.serial, bundleID, c.apkPath, c.output); err != nil {
|
||||
return fmt.Errorf("clear-state reinstall: %w", err)
|
||||
}
|
||||
sidecarClearState = false
|
||||
}
|
||||
_, err := c.stub.Launch(ctx, &driverpb.LaunchRequest{
|
||||
BundleId: bundleID,
|
||||
ClearState: clearState,
|
||||
ClearState: sidecarClearState,
|
||||
Env: env,
|
||||
})
|
||||
return err
|
||||
|
||||
@@ -2,6 +2,7 @@ package sidecar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -300,6 +301,77 @@ func TestClient_LaunchAndTerminate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_LaunchClearStateReinstallsOnAndroid(t *testing.T) {
|
||||
state := newHarness(t)
|
||||
client, _ := Dial(state.address)
|
||||
defer client.Close()
|
||||
client.SetPlatform("android")
|
||||
client.SetClearStateReinstall("serial123", "/tmp/app.apk", io.Discard)
|
||||
|
||||
var got struct {
|
||||
serial, bundleID, apkPath string
|
||||
}
|
||||
called := 0
|
||||
client.reinstallApp = func(_ context.Context, serial, bundleID, apkPath string, _ io.Writer) error {
|
||||
called++
|
||||
got.serial, got.bundleID, got.apkPath = serial, bundleID, apkPath
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := client.Launch(context.Background(), "app.folio", true, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if called != 1 {
|
||||
t.Fatalf("reinstall called %d times, want 1", called)
|
||||
}
|
||||
if got.serial != "serial123" || got.bundleID != "app.folio" || got.apkPath != "/tmp/app.apk" {
|
||||
t.Errorf("reinstall args wrong: %+v", got)
|
||||
}
|
||||
// The sidecar must not also clear: the host reinstall already reset state.
|
||||
if state.fake.clearState {
|
||||
t.Error("sidecar clearState should be false after host reinstall")
|
||||
}
|
||||
if state.fake.launchedBundleID != "app.folio" {
|
||||
t.Errorf("launched bundle wrong: %q", state.fake.launchedBundleID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_LaunchClearStateReinstallFailureAborts(t *testing.T) {
|
||||
state := newHarness(t)
|
||||
client, _ := Dial(state.address)
|
||||
defer client.Close()
|
||||
client.SetPlatform("android")
|
||||
client.SetClearStateReinstall("", "/tmp/app.apk", io.Discard)
|
||||
client.reinstallApp = func(_ context.Context, _, _, _ string, _ io.Writer) error {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
if err := client.Launch(context.Background(), "app.folio", true, nil); err == nil {
|
||||
t.Fatal("expected launch to fail when reinstall fails")
|
||||
}
|
||||
if state.fake.launchedBundleID == "app.folio" {
|
||||
t.Error("sidecar launch should not be called after a failed reinstall")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_LaunchClearStateWithoutApkPathUsesSidecarClear(t *testing.T) {
|
||||
state := newHarness(t)
|
||||
client, _ := Dial(state.address)
|
||||
defer client.Close()
|
||||
client.SetPlatform("android")
|
||||
client.reinstallApp = func(_ context.Context, _, _, _ string, _ io.Writer) error {
|
||||
t.Fatal("reinstall should not run without an apk path")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := client.Launch(context.Background(), "app.folio", true, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !state.fake.clearState {
|
||||
t.Error("without an apk path the sidecar clearState path must remain")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_TapAndTapSelector(t *testing.T) {
|
||||
state := newHarness(t)
|
||||
client, _ := Dial(state.address)
|
||||
|
||||
Reference in new issue
Block a user