From 4de253e0fa267f1354dc533ae6eb3efff4c6356a Mon Sep 17 00:00:00 2001 From: Vasya Aksyonov Date: Fri, 6 Mar 2026 10:23:14 +0000 Subject: [PATCH] feat: shake gesture for emulators --- .gitignore | 1 + cli/io.go | 21 +++++++++++++++++++++ commands/input.go | 29 +++++++++++++++++++++++++++++ devices/android.go | 22 ++++++++++++++++++++++ devices/common.go | 1 + devices/ios.go | 4 ++++ devices/remote.go | 4 ++++ devices/simulator.go | 5 +++++ server/dispatch.go | 1 + server/server.go | 22 ++++++++++++++++++++++ 10 files changed, 110 insertions(+) diff --git a/.gitignore b/.gitignore index 521b68b..2de2e53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ mobilecli screenshot.png .DS_Store +.idea **/node_modules **/coverage*.out **/coverage*.html diff --git a/cli/io.go b/cli/io.go index 9e96d58..75ead69 100644 --- a/cli/io.go +++ b/cli/io.go @@ -134,6 +134,25 @@ var ioTextCmd = &cobra.Command{ }, } +var ioShakeCmd = &cobra.Command{ + Use: "shake", + Short: "Simulate a shake gesture on a device", + Long: `Sends a shake gesture event to the specified device. Supported on iOS simulators and Android emulators only.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + req := commands.ShakeRequest{ + DeviceID: deviceId, + } + + response := commands.ShakeCommand(req) + printJson(response) + if response.Status == "error" { + return fmt.Errorf("%s", response.Error) + } + return nil + }, +} + var ioSwipeCmd = &cobra.Command{ Use: "swipe [x1,y1,x2,y2]", Short: "Swipe on a device screen from one point to another", @@ -184,6 +203,7 @@ func init() { ioCmd.AddCommand(ioLongPressCmd) ioCmd.AddCommand(ioButtonCmd) ioCmd.AddCommand(ioTextCmd) + ioCmd.AddCommand(ioShakeCmd) ioCmd.AddCommand(ioSwipeCmd) // io command flags @@ -192,5 +212,6 @@ func init() { ioLongPressCmd.Flags().IntVar(&longPressDuration, "duration", 500, "duration of the long press in milliseconds") ioButtonCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to press button on") ioTextCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to send keys to") + ioShakeCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to shake") ioSwipeCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to swipe on") } diff --git a/commands/input.go b/commands/input.go index 5ddb227..282d38a 100644 --- a/commands/input.go +++ b/commands/input.go @@ -41,6 +41,11 @@ type GestureRequest struct { Actions []any `json:"actions"` } +// ShakeRequest represents the parameters for a shake gesture command +type ShakeRequest struct { + DeviceID string `json:"deviceId"` +} + // SwipeRequest represents the parameters for a swipe command type SwipeRequest struct { DeviceID string `json:"deviceId"` @@ -205,6 +210,30 @@ func GestureCommand(req GestureRequest) *CommandResponse { }) } +// ShakeCommand performs a shake gesture on the specified device +func ShakeCommand(req ShakeRequest) *CommandResponse { + targetDevice, err := FindDeviceOrAutoSelect(req.DeviceID) + if err != nil { + return NewErrorResponse(fmt.Errorf("error finding device: %v", err)) + } + + err = targetDevice.StartAgent(devices.StartAgentConfig{ + Hook: GetShutdownHook(), + }) + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to start agent on device %s: %v", targetDevice.ID(), err)) + } + + err = targetDevice.Shake() + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to shake device %s: %v", targetDevice.ID(), err)) + } + + return NewSuccessResponse(map[string]any{ + "message": fmt.Sprintf("Performed shake gesture on device %s", targetDevice.ID()), + }) +} + // SwipeCommand performs a swipe operation on the specified device func SwipeCommand(req SwipeRequest) *CommandResponse { targetDevice, err := FindDeviceOrAutoSelect(req.DeviceID) diff --git a/devices/android.go b/devices/android.go index 1721ddf..a9312f7 100644 --- a/devices/android.go +++ b/devices/android.go @@ -357,6 +357,28 @@ func (d *AndroidDevice) Swipe(x1, y1, x2, y2 int) error { return nil } +// Shake simulates a shake gesture on Android emulators by injecting accelerometer data. +// Only supported on emulators; real devices will return an error. +func (d *AndroidDevice) Shake() error { + if d.DeviceType() != "emulator" { + return fmt.Errorf("shake gesture is only supported on Android emulators, not real devices") + } + + _, err := d.runAdbCommand("emu", "sensor", "set", "acceleration", "100:100:100") + if err != nil { + return fmt.Errorf("failed to set accelerometer data: %w", err) + } + + time.Sleep(500 * time.Millisecond) + + _, err = d.runAdbCommand("emu", "sensor", "set", "acceleration", "0:0:0") + if err != nil { + return fmt.Errorf("failed to reset accelerometer data: %w", err) + } + + return nil +} + // Gesture performs a sequence of touch actions on the Android device func (d *AndroidDevice) Gesture(actions []wda.TapAction) error { diff --git a/devices/common.go b/devices/common.go index 805c9eb..f6517d3 100644 --- a/devices/common.go +++ b/devices/common.go @@ -55,6 +55,7 @@ type ControllableDevice interface { Tap(x, y int) error LongPress(x, y, duration int) error Swipe(x1, y1, x2, y2 int) error + Shake() error Gesture(actions []wda.TapAction) error StartAgent(config StartAgentConfig) error SendKeys(text string) error diff --git a/devices/ios.go b/devices/ios.go index ce0860e..27fce8a 100644 --- a/devices/ios.go +++ b/devices/ios.go @@ -657,6 +657,10 @@ func (d *IOSDevice) PressButton(key string) error { return d.wdaClient.PressButton(key) } +func (d *IOSDevice) Shake() error { + return fmt.Errorf("shake gesture is not supported on real iOS devices") +} + func deviceWithRsdProvider(device goios.DeviceEntry, udid string, address string, rsdPort int) (goios.DeviceEntry, error) { rsdService, err := goios.NewWithAddrPortDevice(address, rsdPort, device) if err != nil { diff --git a/devices/remote.go b/devices/remote.go index 2fb70e7..3525e04 100644 --- a/devices/remote.go +++ b/devices/remote.go @@ -125,6 +125,10 @@ func (r *RemoteDevice) Swipe(x1, y1, x2, y2 int) error { return r.fireRPC("device.io.swipe", params{"x1": x1, "y1": y1, "x2": x2, "y2": y2}) } +func (r *RemoteDevice) Shake() error { + return r.fireRPC("device.io.shake", params{}) +} + func (r *RemoteDevice) Gesture(actions []wda.TapAction) error { return r.fireRPC("device.io.gesture", params{"actions": actions}) } diff --git a/devices/simulator.go b/devices/simulator.go index 427056b..1f996db 100644 --- a/devices/simulator.go +++ b/devices/simulator.go @@ -654,6 +654,11 @@ func (s SimulatorDevice) Swipe(x1, y1, x2, y2 int) error { return s.wdaClient.Swipe(x1, y1, x2, y2) } +func (s SimulatorDevice) Shake() error { + _, err := runSimctl("notify_post", s.ID(), "com.apple.UIKit.SimulatorShake") + return err +} + func (s SimulatorDevice) Gesture(actions []wda.TapAction) error { return s.wdaClient.Gesture(actions) } diff --git a/server/dispatch.go b/server/dispatch.go index b21d631..66a216c 100644 --- a/server/dispatch.go +++ b/server/dispatch.go @@ -20,6 +20,7 @@ func GetMethodRegistry() map[string]HandlerFunc { "device.io.text": handleIoText, "device.io.button": handleIoButton, "device.io.swipe": handleIoSwipe, + "device.io.shake": handleIoShake, "device.io.gesture": handleIoGesture, "device.url": handleURL, "device.info": handleDeviceInfo, diff --git a/server/server.go b/server/server.go index cd24844..ac7dfe6 100644 --- a/server/server.go +++ b/server/server.go @@ -663,6 +663,28 @@ func handleIoButton(params json.RawMessage) (any, error) { return okResponse, nil } +func handleIoShake(params json.RawMessage) (any, error) { + var shakeParams struct { + DeviceID string `json:"deviceId"` + } + if len(params) > 0 { + if err := json.Unmarshal(params, &shakeParams); err != nil { + return nil, fmt.Errorf("invalid parameters: %w", err) + } + } + + req := commands.ShakeRequest{ + DeviceID: shakeParams.DeviceID, + } + + response := commands.ShakeCommand(req) + if response.Status == "error" { + return nil, fmt.Errorf("%s", response.Error) + } + + return okResponse, nil +} + func handleIoGesture(params json.RawMessage) (any, error) { if len(params) == 0 { return nil, fmt.Errorf("'params' is required with fields: deviceId, actions")