Skip to content

Commit d56228f

Browse files
authored
feat: device.settings.apply with animations toggle (#287)
1 parent 70f41f0 commit d56228f

7 files changed

Lines changed: 173 additions & 0 deletions

File tree

cli/device.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,37 @@ var deviceShutdownCmd = &cobra.Command{
130130
},
131131
}
132132

133+
var settingsCmd = &cobra.Command{
134+
Use: "settings",
135+
Short: "Device settings commands",
136+
Long: `Commands for applying device-level settings such as animations.`,
137+
}
138+
139+
var settingsAnimations string
140+
141+
var settingsApplyCmd = &cobra.Command{
142+
Use: "apply",
143+
Short: "Apply device settings",
144+
Long: `Apply device-level settings. Example: mobilecli device settings apply --animations=off`,
145+
RunE: func(cmd *cobra.Command, args []string) error {
146+
req := commands.ApplySettingsRequest{
147+
DeviceID: deviceId,
148+
}
149+
150+
if cmd.Flags().Changed("animations") {
151+
req.Animations = &settingsAnimations
152+
}
153+
154+
response := commands.ApplySettingsCommand(req)
155+
printJson(response)
156+
if response.Status == "error" {
157+
return fmt.Errorf("%s", response.Error)
158+
}
159+
160+
return nil
161+
},
162+
}
163+
133164
func init() {
134165
rootCmd.AddCommand(deviceCmd)
135166

@@ -139,16 +170,22 @@ func init() {
139170
deviceCmd.AddCommand(deviceBootCmd)
140171
deviceCmd.AddCommand(deviceShutdownCmd)
141172
deviceCmd.AddCommand(orientationCmd)
173+
deviceCmd.AddCommand(settingsCmd)
142174

143175
// add orientation subcommands
144176
orientationCmd.AddCommand(orientationGetCmd)
145177
orientationCmd.AddCommand(orientationSetCmd)
146178

179+
// add settings subcommands
180+
settingsCmd.AddCommand(settingsApplyCmd)
181+
147182
// device command flags
148183
deviceRebootCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to reboot")
149184
deviceInfoCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get info from")
150185
deviceBootCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to boot")
151186
deviceShutdownCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to shutdown")
152187
orientationGetCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to get orientation from")
153188
orientationSetCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to set orientation on")
189+
settingsApplyCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to apply settings to")
190+
settingsApplyCmd.Flags().StringVar(&settingsAnimations, "animations", "", "Toggle system animations: 'on' or 'off'")
154191
}

commands/settings.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/mobile-next/mobilecli/devices"
7+
"github.com/mobile-next/mobilecli/utils"
8+
)
9+
10+
// ApplySettingsRequest applies device-level settings to a device. Pointer
11+
// fields distinguish "not provided" from a zero value, so only the settings
12+
// explicitly set are touched (PATCH semantics).
13+
type ApplySettingsRequest struct {
14+
DeviceID string `json:"deviceId"`
15+
Animations *string `json:"animations,omitempty"` // "on" or "off"
16+
}
17+
18+
// ApplySettingsCommand applies the provided device settings. Settings that a
19+
// platform cannot honor are skipped with a debug log and never fail the call.
20+
func ApplySettingsCommand(req ApplySettingsRequest) *CommandResponse {
21+
device, err := FindDeviceOrAutoSelect(req.DeviceID)
22+
if err != nil {
23+
return NewErrorResponse(err)
24+
}
25+
26+
if req.Animations != nil {
27+
err = applyAnimations(device, *req.Animations)
28+
if err != nil {
29+
return NewErrorResponse(err)
30+
}
31+
}
32+
33+
return NewSuccessResponse(OK)
34+
}
35+
36+
func applyAnimations(device devices.ControllableDevice, animations string) error {
37+
if animations != "on" && animations != "off" {
38+
return fmt.Errorf("invalid value for animations '%s', must be 'on' or 'off'", animations)
39+
}
40+
41+
configurable, ok := device.(devices.AnimationConfigurable)
42+
if !ok {
43+
utils.Verbose("animations not supported on %s (%s), skipping", device.ID(), device.Platform())
44+
return nil
45+
}
46+
47+
err := configurable.SetAnimationsEnabled(animations == "on")
48+
if err != nil {
49+
return fmt.Errorf("failed to apply animations setting: %v", err)
50+
}
51+
52+
return nil
53+
}

devices/android.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,30 @@ func (d *AndroidDevice) SetOrientation(orientation string) error {
17011701
return nil
17021702
}
17031703

1704+
// SetAnimationsEnabled toggles the three global animation scales. Setting them
1705+
// to 0 disables animations for stable screenshots; 1 restores the defaults.
1706+
func (d *AndroidDevice) SetAnimationsEnabled(enabled bool) error {
1707+
scale := "0"
1708+
if enabled {
1709+
scale = "1"
1710+
}
1711+
1712+
scaleSettings := []string{
1713+
"window_animation_scale",
1714+
"transition_animation_scale",
1715+
"animator_duration_scale",
1716+
}
1717+
1718+
for _, setting := range scaleSettings {
1719+
_, err := d.runAdbCommand("shell", "settings", "put", "global", setting, scale)
1720+
if err != nil {
1721+
return fmt.Errorf("failed to set %s: %v", setting, err)
1722+
}
1723+
}
1724+
1725+
return nil
1726+
}
1727+
17041728
func (d *AndroidDevice) getCrashLog() (string, error) {
17051729
output, err := d.runAdbCommand("logcat", "-b", "crash", "-d", "-v", "year")
17061730
if err != nil {

devices/animations_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package devices
2+
3+
import "testing"
4+
5+
// The animations setting is fire-and-forget and platform-specific: Android
6+
// applies it via adb, while iOS/simulator are expected to no-op. Callers rely
7+
// on the AnimationConfigurable type assertion to decide which path to take, so
8+
// this test pins down which device types satisfy the capability.
9+
func TestAnimationConfigurableImplementers(t *testing.T) {
10+
var android any = (*AndroidDevice)(nil)
11+
if _, ok := android.(AnimationConfigurable); !ok {
12+
t.Error("AndroidDevice should implement AnimationConfigurable")
13+
}
14+
15+
var ios any = IOSDevice{}
16+
if _, ok := ios.(AnimationConfigurable); ok {
17+
t.Error("IOSDevice should not implement AnimationConfigurable (no-op expected)")
18+
}
19+
20+
var simulator any = SimulatorDevice{}
21+
if _, ok := simulator.(AnimationConfigurable); ok {
22+
t.Error("SimulatorDevice should not implement AnimationConfigurable (no-op expected)")
23+
}
24+
}

devices/common.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ type ControllableDevice interface {
155155
GetAppContainerPath(bundleID string) (string, error)
156156
}
157157

158+
// AnimationConfigurable is implemented by devices that can toggle system
159+
// animations. Devices that don't implement it are treated as a no-op by callers.
160+
type AnimationConfigurable interface {
161+
SetAnimationsEnabled(enabled bool) error
162+
}
163+
158164
// WebViewable is implemented by devices that support webview inspection and control.
159165
type WebViewable interface {
160166
ListWebViews() ([]WebViewInfo, error)

server/dispatch.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func GetMethodRegistry() map[string]HandlerFunc {
2929
"device.boot": handleDeviceBoot,
3030
"device.shutdown": handleDeviceShutdown,
3131
"device.reboot": handleDeviceReboot,
32+
"device.settings.apply": handleSettingsApply,
3233
"device.dump.ui": handleDumpUI,
3334
"device.apps.launch": handleAppsLaunch,
3435
"device.apps.terminate": handleAppsTerminate,

server/server.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,11 @@ type IoOrientationSetParams struct {
616616
Orientation string `json:"orientation"`
617617
}
618618

619+
type DeviceSettingsApplyParams struct {
620+
DeviceID string `json:"deviceId"`
621+
Animations *string `json:"animations,omitempty"` // "on" or "off"
622+
}
623+
619624
type DeviceBootParams struct {
620625
DeviceID string `json:"deviceId"`
621626
}
@@ -815,6 +820,29 @@ func handleIoOrientationSet(params json.RawMessage) (any, error) {
815820
return okResponse, nil
816821
}
817822

823+
func handleSettingsApply(params json.RawMessage) (any, error) {
824+
if len(params) == 0 {
825+
return nil, fmt.Errorf("'params' is required with fields: deviceId")
826+
}
827+
828+
var settingsParams DeviceSettingsApplyParams
829+
if err := json.Unmarshal(params, &settingsParams); err != nil {
830+
return nil, fmt.Errorf("invalid parameters: %w. Expected fields: deviceId, animations", err)
831+
}
832+
833+
req := commands.ApplySettingsRequest{
834+
DeviceID: settingsParams.DeviceID,
835+
Animations: settingsParams.Animations,
836+
}
837+
838+
response := commands.ApplySettingsCommand(req)
839+
if response.Status == "error" {
840+
return nil, fmt.Errorf("%s", response.Error)
841+
}
842+
843+
return okResponse, nil
844+
}
845+
818846
func handleDeviceBoot(params json.RawMessage) (any, error) {
819847
if len(params) == 0 {
820848
return nil, fmt.Errorf("'params' is required with fields: deviceId")

0 commit comments

Comments
 (0)