diff --git a/commands/animation.go b/commands/animation.go new file mode 100644 index 0000000..d2a07fa --- /dev/null +++ b/commands/animation.go @@ -0,0 +1,83 @@ +package commands + +import ( + "fmt" + + "github.com/mobile-next/mobilecli/devices" +) + +// AnimationScalesGetRequest represents the request for getting animation scales +type AnimationScalesGetRequest struct { + DeviceID string `json:"deviceId"` +} + +// AnimationScalesSetRequest represents the request for setting animation scales +type AnimationScalesSetRequest struct { + DeviceID string `json:"deviceId"` + Window float64 `json:"window"` + Transition float64 `json:"transition"` + Animator float64 `json:"animator"` +} + +// AnimationScalesResponse holds the three Android global animation scale values +type AnimationScalesResponse struct { + Window float64 `json:"window"` + Transition float64 `json:"transition"` + Animator float64 `json:"animator"` +} + +// AnimationScalesGetCommand gets the current animation scale values from the device +func AnimationScalesGetCommand(req AnimationScalesGetRequest) *CommandResponse { + device, err := FindDeviceOrAutoSelect(req.DeviceID) + if err != nil { + return NewErrorResponse(err) + } + + err = device.StartAgent(devices.StartAgentConfig{ + Hook: GetShutdownHook(), + }) + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to start agent on device %s: %v", device.ID(), err)) + } + + scales, err := device.GetAnimationScales() + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to get animation scales: %v", err)) + } + + return NewSuccessResponse(AnimationScalesResponse{ + Window: scales.Window, + Transition: scales.Transition, + Animator: scales.Animator, + }) +} + +// AnimationScalesSetCommand sets the animation scale values on the device +func AnimationScalesSetCommand(req AnimationScalesSetRequest) *CommandResponse { + device, err := FindDeviceOrAutoSelect(req.DeviceID) + if err != nil { + return NewErrorResponse(err) + } + + err = device.StartAgent(devices.StartAgentConfig{ + Hook: GetShutdownHook(), + }) + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to start agent on device %s: %v", device.ID(), err)) + } + + err = device.SetAnimationScales(devices.AnimationScales{ + Window: req.Window, + Transition: req.Transition, + Animator: req.Animator, + }) + if err != nil { + return NewErrorResponse(fmt.Errorf("failed to set animation scales: %v", err)) + } + + return NewSuccessResponse(AnimationScalesResponse{ + Window: req.Window, + Transition: req.Transition, + Animator: req.Animator, + }) +} diff --git a/devices/android.go b/devices/android.go index 59b1653..aae4373 100644 --- a/devices/android.go +++ b/devices/android.go @@ -1376,6 +1376,53 @@ func (d *AndroidDevice) SetOrientation(orientation string) error { return nil } +// GetAnimationScales reads the current values of all three global animation +// scales. Callers should save the result and pass it to SetAnimationScales to +// restore the original values rather than assuming they were 1. +func (d *AndroidDevice) GetAnimationScales() (AnimationScales, error) { + keys := []string{"window_animation_scale", "transition_animation_scale", "animator_duration_scale"} + values := make([]float64, len(keys)) + + for i, key := range keys { + out, err := d.runAdbCommand("shell", "settings", "get", "global", key) + if err != nil { + return AnimationScales{}, fmt.Errorf("failed to get %s: %v", key, err) + } + val, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err != nil { + return AnimationScales{}, fmt.Errorf("failed to parse %s value %q: %v", key, strings.TrimSpace(string(out)), err) + } + values[i] = val + } + + return AnimationScales{ + Window: values[0], + Transition: values[1], + Animator: values[2], + }, nil +} + +// SetAnimationScales writes the three global animation scales. +// Pass the result of GetAnimationScales to restore original values. +func (d *AndroidDevice) SetAnimationScales(scales AnimationScales) error { + entries := []struct { + key string + value float64 + }{ + {"window_animation_scale", scales.Window}, + {"transition_animation_scale", scales.Transition}, + {"animator_duration_scale", scales.Animator}, + } + + for _, e := range entries { + _, err := d.runAdbCommand("shell", "settings", "put", "global", e.key, strconv.FormatFloat(e.value, 'f', -1, 64)) + if err != nil { + return fmt.Errorf("failed to set %s: %v", e.key, err) + } + } + return nil +} + func (d *AndroidDevice) getCrashLog() (string, error) { output, err := d.runAdbCommand("logcat", "-b", "crash", "-d", "-v", "year") if err != nil { diff --git a/devices/common.go b/devices/common.go index 75f0565..1a5a31e 100644 --- a/devices/common.go +++ b/devices/common.go @@ -126,10 +126,20 @@ type ControllableDevice interface { DumpSourceRaw() (any, error) GetOrientation() (string, error) SetOrientation(orientation string) error + GetAnimationScales() (AnimationScales, error) + SetAnimationScales(scales AnimationScales) error ListCrashReports() ([]CrashReport, error) GetCrashReport(id string) ([]byte, error) } +// AnimationScales holds the three Android global animation scale values. +// A value of 0 disables the animation; 1 is the system default. +type AnimationScales struct { + Window float64 `json:"window"` + Transition float64 `json:"transition"` + Animator float64 `json:"animator"` +} + // GetAllControllableDevices aggregates all known devices with options func GetAllControllableDevices(includeOffline bool) ([]ControllableDevice, error) { diff --git a/devices/ios.go b/devices/ios.go index d6deda1..b87bd7e 100644 --- a/devices/ios.go +++ b/devices/ios.go @@ -1142,6 +1142,16 @@ func (d IOSDevice) SetOrientation(orientation string) error { return d.wdaClient.SetOrientation(orientation) } +// GetAnimationScales is not supported on iOS devices. +func (d IOSDevice) GetAnimationScales() (AnimationScales, error) { + return AnimationScales{}, fmt.Errorf("animation scales are not supported on iOS") +} + +// SetAnimationScales is not supported on iOS devices. +func (d IOSDevice) SetAnimationScales(_ AnimationScales) error { + return fmt.Errorf("animation scales are not supported on iOS") +} + // DeviceKitInfo contains information about the started DeviceKit session type DeviceKitInfo struct { HTTPPort int `json:"httpPort"` diff --git a/devices/remote.go b/devices/remote.go index aff581d..b8fecd5 100644 --- a/devices/remote.go +++ b/devices/remote.go @@ -181,6 +181,24 @@ func (r *RemoteDevice) SetOrientation(orientation string) error { return r.fireRPC("device.io.orientation.set", params{"orientation": orientation}) } +// GetAnimationScales retrieves the current animation scale values from the remote device. +func (r *RemoteDevice) GetAnimationScales() (AnimationScales, error) { + resp, err := rpcCall[AnimationScales](r, "device.io.animation-scales.get", params{}) + if err != nil { + return AnimationScales{}, err + } + return resp, nil +} + +// SetAnimationScales sets the animation scale values on the remote device. +func (r *RemoteDevice) SetAnimationScales(scales AnimationScales) error { + return r.fireRPC("device.io.animation-scales.set", params{ + "window": scales.Window, + "transition": scales.Transition, + "animator": scales.Animator, + }) +} + func (r *RemoteDevice) Info() (*FullDeviceInfo, error) { return rpcCall[*FullDeviceInfo](r, "device.info", params{}) } diff --git a/devices/simulator.go b/devices/simulator.go index 433566e..729ba24 100644 --- a/devices/simulator.go +++ b/devices/simulator.go @@ -857,6 +857,16 @@ func (s SimulatorDevice) SetOrientation(orientation string) error { return s.wdaClient.SetOrientation(orientation) } +// GetAnimationScales is not supported on iOS simulators. +func (s SimulatorDevice) GetAnimationScales() (AnimationScales, error) { + return AnimationScales{}, fmt.Errorf("animation scales are not supported on iOS simulator") +} + +// SetAnimationScales is not supported on iOS simulators. +func (s SimulatorDevice) SetAnimationScales(_ AnimationScales) error { + return fmt.Errorf("animation scales are not supported on iOS simulator") +} + var diagnosticReportsDir = filepath.Join(os.Getenv("HOME"), "Library", "Logs", "DiagnosticReports") func (s SimulatorDevice) ListCrashReports() ([]CrashReport, error) { diff --git a/server/dispatch.go b/server/dispatch.go index 8404435..2bbbc11 100644 --- a/server/dispatch.go +++ b/server/dispatch.go @@ -23,8 +23,10 @@ func GetMethodRegistry() map[string]HandlerFunc { "device.io.gesture": handleIoGesture, "device.url": handleURL, "device.info": handleDeviceInfo, - "device.io.orientation.get": handleIoOrientationGet, - "device.io.orientation.set": handleIoOrientationSet, + "device.io.orientation.get": handleIoOrientationGet, + "device.io.orientation.set": handleIoOrientationSet, + "device.io.animation-scales.get": handleIoAnimationScalesGet, + "device.io.animation-scales.set": handleIoAnimationScalesSet, "device.boot": handleDeviceBoot, "device.shutdown": handleDeviceShutdown, "device.reboot": handleDeviceReboot, diff --git a/server/server.go b/server/server.go index 84a0a89..89dcbca 100644 --- a/server/server.go +++ b/server/server.go @@ -586,6 +586,17 @@ type IoOrientationSetParams struct { Orientation string `json:"orientation"` } +type IoAnimationScalesGetParams struct { + DeviceID string `json:"deviceId"` +} + +type IoAnimationScalesSetParams struct { + DeviceID string `json:"deviceId"` + Window float64 `json:"window"` + Transition float64 `json:"transition"` + Animator float64 `json:"animator"` +} + type DeviceBootParams struct { DeviceID string `json:"deviceId"` } @@ -784,6 +795,51 @@ func handleIoOrientationSet(params json.RawMessage) (any, error) { return okResponse, nil } +// handleIoAnimationScalesGet handles the device.io.animation-scales.get RPC method. +func handleIoAnimationScalesGet(params json.RawMessage) (any, error) { + if len(params) == 0 { + return nil, fmt.Errorf("'params' is required with fields: deviceId") + } + + var p IoAnimationScalesGetParams + if err := json.Unmarshal(params, &p); err != nil { + return nil, fmt.Errorf("invalid parameters: %w. Expected fields: deviceId", err) + } + + response := commands.AnimationScalesGetCommand(commands.AnimationScalesGetRequest{ + DeviceID: p.DeviceID, + }) + if response.Status == "error" { + return nil, fmt.Errorf("%s", response.Error) + } + + return response.Data, nil +} + +// handleIoAnimationScalesSet handles the device.io.animation-scales.set RPC method. +func handleIoAnimationScalesSet(params json.RawMessage) (any, error) { + if len(params) == 0 { + return nil, fmt.Errorf("'params' is required with fields: deviceId, window, transition, animator") + } + + var p IoAnimationScalesSetParams + if err := json.Unmarshal(params, &p); err != nil { + return nil, fmt.Errorf("invalid parameters: %w. Expected fields: deviceId, window, transition, animator", err) + } + + response := commands.AnimationScalesSetCommand(commands.AnimationScalesSetRequest{ + DeviceID: p.DeviceID, + Window: p.Window, + Transition: p.Transition, + Animator: p.Animator, + }) + if response.Status == "error" { + return nil, fmt.Errorf("%s", response.Error) + } + + return okResponse, nil +} + func handleDeviceBoot(params json.RawMessage) (any, error) { if len(params) == 0 { return nil, fmt.Errorf("'params' is required with fields: deviceId")