android-emu-agent 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. android_emu_agent/__init__.py +3 -0
  2. android_emu_agent/actions/__init__.py +1 -0
  3. android_emu_agent/actions/executor.py +288 -0
  4. android_emu_agent/actions/selector.py +122 -0
  5. android_emu_agent/actions/wait.py +193 -0
  6. android_emu_agent/artifacts/__init__.py +1 -0
  7. android_emu_agent/artifacts/manager.py +125 -0
  8. android_emu_agent/artifacts/py.typed +0 -0
  9. android_emu_agent/cli/__init__.py +1 -0
  10. android_emu_agent/cli/commands/__init__.py +1 -0
  11. android_emu_agent/cli/commands/action.py +158 -0
  12. android_emu_agent/cli/commands/app_cmd.py +95 -0
  13. android_emu_agent/cli/commands/artifact.py +81 -0
  14. android_emu_agent/cli/commands/daemon.py +62 -0
  15. android_emu_agent/cli/commands/device.py +122 -0
  16. android_emu_agent/cli/commands/emulator.py +46 -0
  17. android_emu_agent/cli/commands/file.py +139 -0
  18. android_emu_agent/cli/commands/reliability.py +310 -0
  19. android_emu_agent/cli/commands/session.py +65 -0
  20. android_emu_agent/cli/commands/ui.py +112 -0
  21. android_emu_agent/cli/commands/wait.py +132 -0
  22. android_emu_agent/cli/daemon_client.py +185 -0
  23. android_emu_agent/cli/main.py +52 -0
  24. android_emu_agent/cli/utils.py +171 -0
  25. android_emu_agent/daemon/__init__.py +1 -0
  26. android_emu_agent/daemon/core.py +62 -0
  27. android_emu_agent/daemon/health.py +177 -0
  28. android_emu_agent/daemon/models.py +244 -0
  29. android_emu_agent/daemon/server.py +1644 -0
  30. android_emu_agent/db/__init__.py +1 -0
  31. android_emu_agent/db/models.py +229 -0
  32. android_emu_agent/device/__init__.py +1 -0
  33. android_emu_agent/device/manager.py +522 -0
  34. android_emu_agent/device/session.py +129 -0
  35. android_emu_agent/errors.py +232 -0
  36. android_emu_agent/files/__init__.py +1 -0
  37. android_emu_agent/files/manager.py +311 -0
  38. android_emu_agent/py.typed +0 -0
  39. android_emu_agent/reliability/__init__.py +1 -0
  40. android_emu_agent/reliability/manager.py +244 -0
  41. android_emu_agent/ui/__init__.py +1 -0
  42. android_emu_agent/ui/context.py +169 -0
  43. android_emu_agent/ui/ref_resolver.py +149 -0
  44. android_emu_agent/ui/snapshotter.py +236 -0
  45. android_emu_agent/validation.py +59 -0
  46. android_emu_agent-0.1.3.dist-info/METADATA +375 -0
  47. android_emu_agent-0.1.3.dist-info/RECORD +50 -0
  48. android_emu_agent-0.1.3.dist-info/WHEEL +4 -0
  49. android_emu_agent-0.1.3.dist-info/entry_points.txt +2 -0
  50. android_emu_agent-0.1.3.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,125 @@
1
+ """Artifact manager - Screenshots, logs, and debug bundles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import zipfile
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ import structlog
12
+
13
+ if TYPE_CHECKING:
14
+ import uiautomator2 as u2
15
+
16
+ logger = structlog.get_logger()
17
+
18
+
19
+ class ArtifactManager:
20
+ """Manages artifact capture and storage."""
21
+
22
+ def __init__(self, output_dir: Path | None = None) -> None:
23
+ default_dir = Path.home() / ".android-agent" / "artifacts"
24
+ self.output_dir = output_dir or default_dir
25
+ self.output_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ async def screenshot(
28
+ self,
29
+ device: u2.Device,
30
+ session_id: str,
31
+ filename: str | None = None,
32
+ ) -> Path:
33
+ """Capture a screenshot from device."""
34
+ if filename is None:
35
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
36
+ filename = f"{session_id}_{timestamp}.png"
37
+
38
+ output_path = self.output_dir / filename
39
+
40
+ # uiautomator2 returns PIL Image
41
+ image = await asyncio.to_thread(device.screenshot)
42
+ if image is None:
43
+ raise RuntimeError("Failed to capture screenshot from device")
44
+ await asyncio.to_thread(image.save, str(output_path))
45
+
46
+ logger.info("screenshot_captured", path=str(output_path))
47
+ return output_path
48
+
49
+ async def pull_logs(
50
+ self,
51
+ device: u2.Device,
52
+ session_id: str,
53
+ since: str | None = None,
54
+ filename: str | None = None,
55
+ ) -> Path:
56
+ """Pull logcat logs from device."""
57
+ if filename is None:
58
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
59
+ filename = f"{session_id}_{timestamp}_logcat.txt"
60
+
61
+ output_path = self.output_dir / filename
62
+
63
+ def _pull() -> str:
64
+ cmd = "logcat -d"
65
+ if since:
66
+ cmd += f" -t '{since}'"
67
+ result = device.shell(cmd)
68
+ output = getattr(result, "output", None)
69
+ return output if isinstance(output, str) else str(result)
70
+
71
+ logs = await asyncio.to_thread(_pull)
72
+ output_path.write_text(logs)
73
+
74
+ logger.info("logs_pulled", path=str(output_path), size=len(logs))
75
+ return output_path
76
+
77
+ async def save_snapshot(
78
+ self,
79
+ snapshot_json: str,
80
+ session_id: str,
81
+ generation: int,
82
+ ) -> Path:
83
+ """Save a snapshot to disk."""
84
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
85
+ filename = f"{session_id}_gen{generation}_{timestamp}.json"
86
+ output_path = self.output_dir / filename
87
+
88
+ output_path.write_text(snapshot_json)
89
+ logger.info("snapshot_saved", path=str(output_path))
90
+ return output_path
91
+
92
+ async def create_debug_bundle(
93
+ self,
94
+ device: u2.Device,
95
+ session_id: str,
96
+ snapshot_json: str | None = None,
97
+ ) -> Path:
98
+ """Create a debug bundle with screenshot, logs, and snapshot."""
99
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
100
+ bundle_name = f"{session_id}_{timestamp}_debug.zip"
101
+ bundle_path = self.output_dir / bundle_name
102
+
103
+ # Collect artifacts
104
+ screenshot_path = await self.screenshot(device, session_id)
105
+ logs_path = await self.pull_logs(device, session_id)
106
+
107
+ # Create zip bundle
108
+ def _create_zip() -> None:
109
+ with zipfile.ZipFile(bundle_path, "w", zipfile.ZIP_DEFLATED) as zf:
110
+ zf.write(screenshot_path, screenshot_path.name)
111
+ zf.write(logs_path, logs_path.name)
112
+ if snapshot_json:
113
+ zf.writestr("snapshot.json", snapshot_json)
114
+ # Add metadata
115
+ metadata = f"session_id: {session_id}\ntimestamp: {timestamp}\n"
116
+ zf.writestr("metadata.txt", metadata)
117
+
118
+ await asyncio.to_thread(_create_zip)
119
+
120
+ # Cleanup temp files
121
+ screenshot_path.unlink(missing_ok=True)
122
+ logs_path.unlink(missing_ok=True)
123
+
124
+ logger.info("debug_bundle_created", path=str(bundle_path))
125
+ return bundle_path
File without changes
@@ -0,0 +1 @@
1
+ """CLI - Thin client that communicates with the daemon."""
@@ -0,0 +1 @@
1
+ """CLI command groups."""
@@ -0,0 +1,158 @@
1
+ """Action execution CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from android_emu_agent.cli.daemon_client import DaemonClient
8
+ from android_emu_agent.cli.utils import handle_response
9
+
10
+ app = typer.Typer(help="Action execution commands")
11
+
12
+
13
+ @app.command("tap")
14
+ def action_tap(
15
+ session_id: str = typer.Argument(..., help="Session ID"),
16
+ ref: str = typer.Argument(..., help="Element ref (@a1)"),
17
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
18
+ ) -> None:
19
+ """Tap an element."""
20
+ client = DaemonClient()
21
+ resp = client.request("POST", "/actions/tap", json_body={"session_id": session_id, "ref": ref})
22
+ client.close()
23
+ handle_response(resp, json_output=json_output)
24
+
25
+
26
+ @app.command("long-tap")
27
+ def action_long_tap(
28
+ session_id: str = typer.Argument(..., help="Session ID"),
29
+ ref: str = typer.Argument(..., help="Element ref (@a1)"),
30
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
31
+ ) -> None:
32
+ """Long tap an element."""
33
+ client = DaemonClient()
34
+ resp = client.request(
35
+ "POST", "/actions/long_tap", json_body={"session_id": session_id, "ref": ref}
36
+ )
37
+ client.close()
38
+ handle_response(resp, json_output=json_output)
39
+
40
+
41
+ @app.command("set-text")
42
+ def action_set_text(
43
+ session_id: str = typer.Argument(..., help="Session ID"),
44
+ ref: str = typer.Argument(..., help="Element ref (@a1)"),
45
+ text: str = typer.Argument(..., help="Text to set"),
46
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
47
+ ) -> None:
48
+ """Set text on an element."""
49
+ client = DaemonClient()
50
+ resp = client.request(
51
+ "POST",
52
+ "/actions/set_text",
53
+ json_body={"session_id": session_id, "ref": ref, "text": text},
54
+ )
55
+ client.close()
56
+ handle_response(resp, json_output=json_output)
57
+
58
+
59
+ @app.command("clear")
60
+ def action_clear(
61
+ session_id: str = typer.Argument(..., help="Session ID"),
62
+ ref: str = typer.Argument(..., help="Element ref (@a1)"),
63
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
64
+ ) -> None:
65
+ """Clear text."""
66
+ client = DaemonClient()
67
+ resp = client.request(
68
+ "POST", "/actions/clear", json_body={"session_id": session_id, "ref": ref}
69
+ )
70
+ client.close()
71
+ handle_response(resp, json_output=json_output)
72
+
73
+
74
+ @app.command("back")
75
+ def action_back(
76
+ session_id: str = typer.Argument(..., help="Session ID"),
77
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
78
+ ) -> None:
79
+ """Press back."""
80
+ client = DaemonClient()
81
+ resp = client.request("POST", "/actions/back", json_body={"session_id": session_id})
82
+ client.close()
83
+ handle_response(resp, json_output=json_output)
84
+
85
+
86
+ @app.command("home")
87
+ def action_home(
88
+ session_id: str = typer.Argument(..., help="Session ID"),
89
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
90
+ ) -> None:
91
+ """Press home."""
92
+ client = DaemonClient()
93
+ resp = client.request("POST", "/actions/home", json_body={"session_id": session_id})
94
+ client.close()
95
+ handle_response(resp, json_output=json_output)
96
+
97
+
98
+ @app.command("recents")
99
+ def action_recents(
100
+ session_id: str = typer.Argument(..., help="Session ID"),
101
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
102
+ ) -> None:
103
+ """Press recents."""
104
+ client = DaemonClient()
105
+ resp = client.request("POST", "/actions/recents", json_body={"session_id": session_id})
106
+ client.close()
107
+ handle_response(resp, json_output=json_output)
108
+
109
+
110
+ @app.command("swipe")
111
+ def action_swipe(
112
+ direction: str = typer.Argument(..., help="Direction: up, down, left, right"),
113
+ session: str = typer.Option(..., "--session", "-s", help="Session ID"),
114
+ container: str | None = typer.Option(None, "--in", help="Container @ref or selector"),
115
+ distance: float = typer.Option(0.8, "--distance", "-d", help="Swipe distance (0.0-1.0)"),
116
+ duration: int = typer.Option(300, "--duration", help="Swipe duration in ms"),
117
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
118
+ ) -> None:
119
+ """Perform swipe gesture."""
120
+ client = DaemonClient()
121
+ resp = client.request(
122
+ "POST",
123
+ "/actions/swipe",
124
+ json_body={
125
+ "session_id": session,
126
+ "direction": direction,
127
+ "container": container,
128
+ "distance": distance,
129
+ "duration_ms": duration,
130
+ },
131
+ )
132
+ client.close()
133
+ handle_response(resp, json_output=json_output)
134
+
135
+
136
+ @app.command("scroll")
137
+ def action_scroll(
138
+ direction: str = typer.Argument(..., help="Direction: up, down, left, right"),
139
+ session: str = typer.Option(..., "--session", "-s", help="Session ID"),
140
+ container: str | None = typer.Option(None, "--in", help="Container @ref or selector"),
141
+ distance: float = typer.Option(0.8, "--distance", "-d", help="Scroll distance (0.0-1.0)"),
142
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
143
+ ) -> None:
144
+ """Scroll in a direction (alias for swipe)."""
145
+ client = DaemonClient()
146
+ resp = client.request(
147
+ "POST",
148
+ "/actions/swipe",
149
+ json_body={
150
+ "session_id": session,
151
+ "direction": direction,
152
+ "container": container,
153
+ "distance": distance,
154
+ "duration_ms": 300,
155
+ },
156
+ )
157
+ client.close()
158
+ handle_response(resp, json_output=json_output)
@@ -0,0 +1,95 @@
1
+ """App management CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from android_emu_agent.cli.daemon_client import DaemonClient
8
+ from android_emu_agent.cli.utils import handle_output_response, handle_response, require_target
9
+
10
+ app = typer.Typer(help="App management commands")
11
+
12
+
13
+ @app.command("reset")
14
+ def app_reset(
15
+ session_id: str = typer.Argument(..., help="Session ID"),
16
+ package: str = typer.Argument(..., help="Package name"),
17
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
18
+ ) -> None:
19
+ """Clear app data for a package."""
20
+ client = DaemonClient()
21
+ resp = client.request(
22
+ "POST",
23
+ "/app/reset",
24
+ json_body={"session_id": session_id, "package": package},
25
+ )
26
+ client.close()
27
+ handle_response(resp, json_output=json_output)
28
+
29
+
30
+ @app.command("launch")
31
+ def app_launch(
32
+ session_id: str = typer.Argument(..., help="Session ID"),
33
+ package: str = typer.Argument(..., help="Package name"),
34
+ activity: str | None = typer.Option(None, "--activity", "-a", help="Activity name"),
35
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
36
+ ) -> None:
37
+ """Launch an app."""
38
+ client = DaemonClient()
39
+ resp = client.request(
40
+ "POST",
41
+ "/app/launch",
42
+ json_body={"session_id": session_id, "package": package, "activity": activity},
43
+ )
44
+ client.close()
45
+ handle_response(resp, json_output=json_output)
46
+
47
+
48
+ @app.command("force-stop")
49
+ def app_force_stop(
50
+ session_id: str = typer.Argument(..., help="Session ID"),
51
+ package: str = typer.Argument(..., help="Package name"),
52
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
53
+ ) -> None:
54
+ """Force stop an app."""
55
+ client = DaemonClient()
56
+ resp = client.request(
57
+ "POST",
58
+ "/app/force_stop",
59
+ json_body={"session_id": session_id, "package": package},
60
+ )
61
+ client.close()
62
+ handle_response(resp, json_output=json_output)
63
+
64
+
65
+ @app.command("deeplink")
66
+ def app_deeplink(
67
+ session_id: str = typer.Argument(..., help="Session ID"),
68
+ uri: str = typer.Argument(..., help="URI to open"),
69
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
70
+ ) -> None:
71
+ """Open a deeplink URI."""
72
+ client = DaemonClient()
73
+ resp = client.request(
74
+ "POST",
75
+ "/app/deeplink",
76
+ json_body={"session_id": session_id, "uri": uri},
77
+ )
78
+ client.close()
79
+ handle_response(resp, json_output=json_output)
80
+
81
+
82
+ @app.command("list")
83
+ def app_list(
84
+ device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
85
+ session_id: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
86
+ scope: str = typer.Option("all", "--scope", help="all|system|third-party"),
87
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
88
+ ) -> None:
89
+ """List installed packages."""
90
+ payload = require_target(device, session_id)
91
+ payload.update({"scope": scope})
92
+ client = DaemonClient()
93
+ resp = client.request("POST", "/app/list", json_body=payload)
94
+ client.close()
95
+ handle_output_response(resp, json_output=json_output)
@@ -0,0 +1,81 @@
1
+ """Artifact and debugging CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from android_emu_agent.cli.daemon_client import DaemonClient
8
+ from android_emu_agent.cli.utils import (
9
+ handle_response,
10
+ handle_response_with_pull,
11
+ require_target,
12
+ resolve_session_id,
13
+ )
14
+
15
+ app = typer.Typer(help="Artifact and debugging commands")
16
+
17
+
18
+ @app.command("save-snapshot")
19
+ def artifact_save_snapshot(
20
+ session_id: str = typer.Argument(..., help="Session ID"),
21
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
22
+ ) -> None:
23
+ """Save the last snapshot to disk."""
24
+ client = DaemonClient()
25
+ resp = client.request("POST", "/artifacts/save_snapshot", json_body={"session_id": session_id})
26
+ client.close()
27
+ handle_response(resp, json_output=json_output)
28
+
29
+
30
+ @app.command("screenshot")
31
+ def artifact_screenshot(
32
+ session_id: str | None = typer.Argument(None, help="Session ID"),
33
+ device: str | None = typer.Option(None, "--device", "-d", help="Device serial"),
34
+ session: str | None = typer.Option(None, "--session", "-s", help="Session ID"),
35
+ pull: bool = typer.Option(False, "--pull", help="Copy screenshot to local path"),
36
+ output: str | None = typer.Option(
37
+ None, "--output", "-o", help="Output path (file or directory)"
38
+ ),
39
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
40
+ ) -> None:
41
+ """Capture a screenshot artifact."""
42
+ try:
43
+ resolved_session = resolve_session_id(session_id, session)
44
+ except typer.BadParameter as exc:
45
+ typer.echo(f"Error: {exc}")
46
+ raise typer.Exit(code=1) from None
47
+
48
+ payload = require_target(device, resolved_session)
49
+ client = DaemonClient()
50
+ resp = client.request("POST", "/ui/screenshot", json_body=payload)
51
+ client.close()
52
+ handle_response_with_pull(resp, json_output=json_output, pull=pull, output=output)
53
+
54
+
55
+ @app.command("logs")
56
+ def artifact_logs(
57
+ session_id: str = typer.Argument(..., help="Session ID"),
58
+ since: str | None = typer.Option(None, "--since", help="Logcat since timestamp"),
59
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
60
+ ) -> None:
61
+ """Pull logcat logs."""
62
+ client = DaemonClient()
63
+ resp = client.request(
64
+ "POST",
65
+ "/artifacts/logs",
66
+ json_body={"session_id": session_id, "since": since},
67
+ )
68
+ client.close()
69
+ handle_response(resp, json_output=json_output)
70
+
71
+
72
+ @app.command("bundle")
73
+ def artifact_bundle(
74
+ session_id: str = typer.Argument(..., help="Session ID"),
75
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
76
+ ) -> None:
77
+ """Create a debug bundle."""
78
+ client = DaemonClient()
79
+ resp = client.request("POST", "/artifacts/debug_bundle", json_body={"session_id": session_id})
80
+ client.close()
81
+ handle_response(resp, json_output=json_output)
@@ -0,0 +1,62 @@
1
+ """Daemon lifecycle CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import typer
8
+
9
+ from android_emu_agent.cli.daemon_client import DaemonClient, DaemonController, format_json
10
+
11
+ app = typer.Typer(help="Daemon lifecycle commands")
12
+
13
+
14
+ @app.command("start")
15
+ def daemon_start() -> None:
16
+ """Start the daemon process."""
17
+ controller = DaemonController()
18
+ status = controller.status()
19
+ if status["pid_running"]:
20
+ typer.echo(f"Daemon already running (pid {status['pid']})")
21
+ return
22
+ if controller.health():
23
+ typer.echo("Daemon already running (pid unknown)")
24
+ return
25
+ pid = controller.start()
26
+ if pid == -1:
27
+ typer.echo("Daemon already running (pid unknown)")
28
+ return
29
+ typer.echo(f"Daemon started (pid {pid})")
30
+
31
+
32
+ @app.command("stop")
33
+ def daemon_stop() -> None:
34
+ """Stop the daemon process."""
35
+ controller = DaemonController()
36
+ stopped = controller.stop()
37
+ if stopped:
38
+ typer.echo("Daemon stopped")
39
+ else:
40
+ typer.echo("Daemon not running")
41
+
42
+
43
+ @app.command("status")
44
+ def daemon_status(json_output: bool = typer.Option(False, "--json", help="Output JSON")) -> None:
45
+ """Show daemon status."""
46
+ controller = DaemonController()
47
+ status = controller.status()
48
+
49
+ health: dict[str, Any] | None = None
50
+ try:
51
+ client = DaemonClient(auto_start=False)
52
+ resp = client.request("GET", "/health")
53
+ health = resp.json()
54
+ client.close()
55
+ except Exception:
56
+ health = None
57
+
58
+ status["health"] = health
59
+ if json_output:
60
+ typer.echo(format_json(status))
61
+ else:
62
+ typer.echo(format_json(status))
@@ -0,0 +1,122 @@
1
+ """Device management CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from android_emu_agent.cli.daemon_client import DaemonClient, format_json
8
+ from android_emu_agent.cli.utils import handle_response
9
+
10
+ app = typer.Typer(help="Device management commands")
11
+ device_set_app = typer.Typer(help="Determinism controls")
12
+ app.add_typer(device_set_app, name="set")
13
+
14
+
15
+ @app.command("list")
16
+ def device_list(json_output: bool = typer.Option(False, "--json", help="Output JSON")) -> None:
17
+ """List connected devices."""
18
+ client = DaemonClient()
19
+ resp = client.request("GET", "/devices")
20
+ client.close()
21
+
22
+ data = resp.json()
23
+ if json_output:
24
+ typer.echo(format_json(data))
25
+ return
26
+
27
+ for device in data.get("devices", []):
28
+ typer.echo(
29
+ f"{device['serial']} model={device['model']} sdk={device['sdk_version']} "
30
+ f"root={device['is_rooted']} emulator={device['is_emulator']}"
31
+ )
32
+
33
+
34
+ @device_set_app.command("animations")
35
+ def device_set_animations(
36
+ state: str = typer.Argument(..., help="on|off"),
37
+ device: str = typer.Option(..., "--device", "-d", help="Device serial"),
38
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
39
+ ) -> None:
40
+ """Enable or disable system animations."""
41
+ client = DaemonClient()
42
+ resp = client.request(
43
+ "POST", "/devices/animations", json_body={"serial": device, "state": state}
44
+ )
45
+ client.close()
46
+ handle_response(resp, json_output=json_output)
47
+
48
+
49
+ @device_set_app.command("stay_awake")
50
+ def device_set_stay_awake(
51
+ state: str = typer.Argument(..., help="on|off"),
52
+ device: str = typer.Option(..., "--device", "-d", help="Device serial"),
53
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
54
+ ) -> None:
55
+ """Enable or disable stay-awake."""
56
+ client = DaemonClient()
57
+ resp = client.request(
58
+ "POST", "/devices/stay_awake", json_body={"serial": device, "state": state}
59
+ )
60
+ client.close()
61
+ handle_response(resp, json_output=json_output)
62
+
63
+
64
+ @device_set_app.command("rotation")
65
+ def device_set_rotation(
66
+ orientation: str = typer.Argument(
67
+ ..., help="portrait|landscape|reverse-portrait|reverse-landscape|auto"
68
+ ),
69
+ device: str = typer.Option(..., "--device", "-d", help="Device serial"),
70
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
71
+ ) -> None:
72
+ """Set screen rotation."""
73
+ client = DaemonClient()
74
+ resp = client.request(
75
+ "POST", "/devices/rotation", json_body={"serial": device, "orientation": orientation}
76
+ )
77
+ client.close()
78
+ handle_response(resp, json_output=json_output)
79
+
80
+
81
+ @device_set_app.command("wifi")
82
+ def device_set_wifi(
83
+ state: str = typer.Argument(..., help="on|off"),
84
+ device: str = typer.Option(..., "--device", "-d", help="Device serial"),
85
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
86
+ ) -> None:
87
+ """Enable or disable WiFi."""
88
+ client = DaemonClient()
89
+ enabled = state.lower() == "on"
90
+ resp = client.request("POST", "/devices/wifi", json_body={"serial": device, "enabled": enabled})
91
+ client.close()
92
+ handle_response(resp, json_output=json_output)
93
+
94
+
95
+ @device_set_app.command("mobile")
96
+ def device_set_mobile(
97
+ state: str = typer.Argument(..., help="on|off"),
98
+ device: str = typer.Option(..., "--device", "-d", help="Device serial"),
99
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
100
+ ) -> None:
101
+ """Enable or disable mobile data."""
102
+ client = DaemonClient()
103
+ enabled = state.lower() == "on"
104
+ resp = client.request(
105
+ "POST", "/devices/mobile", json_body={"serial": device, "enabled": enabled}
106
+ )
107
+ client.close()
108
+ handle_response(resp, json_output=json_output)
109
+
110
+
111
+ @device_set_app.command("doze")
112
+ def device_set_doze(
113
+ state: str = typer.Argument(..., help="on|off"),
114
+ device: str = typer.Option(..., "--device", "-d", help="Device serial"),
115
+ json_output: bool = typer.Option(False, "--json", help="Output JSON"),
116
+ ) -> None:
117
+ """Force device into or out of doze mode."""
118
+ client = DaemonClient()
119
+ enabled = state.lower() == "on"
120
+ resp = client.request("POST", "/devices/doze", json_body={"serial": device, "enabled": enabled})
121
+ client.close()
122
+ handle_response(resp, json_output=json_output)