androidctl 0.1.0__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 (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. androidctld/waits/matcher.py +41 -0
@@ -0,0 +1,161 @@
1
+ """Shared device-facing types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ from androidctld.config import DEFAULT_HOST
11
+ from androidctld.protocol import ConnectionMode
12
+ from androidctld.refs.models import NodeHandle
13
+ from androidctld.runtime_policy import DEFAULT_DEVICE_PORT
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ConnectionConfig:
18
+ mode: ConnectionMode
19
+ token: str
20
+ serial: str | None = None
21
+ host: str | None = None
22
+ port: int = DEFAULT_DEVICE_PORT
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ConnectionSpec:
27
+ mode: ConnectionMode
28
+ port: int = DEFAULT_DEVICE_PORT
29
+ serial: str | None = None
30
+ host: str | None = None
31
+
32
+ @classmethod
33
+ def from_config(cls, config: ConnectionConfig) -> ConnectionSpec:
34
+ if config.mode is ConnectionMode.ADB:
35
+ return cls(
36
+ mode=config.mode,
37
+ port=config.port,
38
+ serial=config.serial,
39
+ host=None,
40
+ )
41
+ return cls(
42
+ mode=config.mode,
43
+ port=config.port,
44
+ serial=config.serial,
45
+ host=config.host,
46
+ )
47
+
48
+ def to_connection_config(self, token: str) -> ConnectionConfig:
49
+ return ConnectionConfig(
50
+ mode=self.mode,
51
+ token=token,
52
+ serial=self.serial,
53
+ host=self.host,
54
+ port=self.port,
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class DeviceEndpoint:
60
+ host: str = DEFAULT_HOST
61
+ port: int = DEFAULT_DEVICE_PORT
62
+
63
+ @property
64
+ def base_url(self) -> str:
65
+ return f"http://{self.host}:{self.port}"
66
+
67
+
68
+ @dataclass
69
+ class DeviceCapabilities:
70
+ supports_events_poll: bool
71
+ supports_screenshot: bool
72
+ action_kinds: list[str] = field(default_factory=list)
73
+
74
+ def supports_action(self, action_kind: str) -> bool:
75
+ return action_kind in self.action_kinds
76
+
77
+
78
+ @dataclass
79
+ class MetaInfo:
80
+ service: str
81
+ version: str
82
+ capabilities: DeviceCapabilities
83
+
84
+
85
+ class ActionStatus(str, Enum):
86
+ DONE = "done"
87
+ PARTIAL = "partial"
88
+ TIMEOUT = "timeout"
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class ObservedApp:
93
+ package_name: str | None = None
94
+ activity_name: str | None = None
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class ResolvedHandleTarget:
99
+ handle: NodeHandle
100
+ kind: str = "handle"
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class ResolvedCoordinatesTarget:
105
+ x: float
106
+ y: float
107
+ kind: str = "coordinates"
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class ResolvedNoneTarget:
112
+ kind: str = "none"
113
+
114
+
115
+ ResolvedTarget = ResolvedHandleTarget | ResolvedCoordinatesTarget | ResolvedNoneTarget
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class ActionPerformResult:
120
+ action_id: str
121
+ status: ActionStatus
122
+ duration_ms: int | None = None
123
+ resolved_target: ResolvedTarget | None = None
124
+ observed: ObservedApp | None = None
125
+
126
+
127
+ @dataclass(frozen=True)
128
+ class DeviceEvent:
129
+ seq: int
130
+ type: str
131
+ timestamp: str
132
+ data: dict[str, Any]
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class EventsPollResult:
137
+ events: tuple[DeviceEvent, ...]
138
+ latest_seq: int
139
+ need_resync: bool
140
+ timed_out: bool
141
+
142
+
143
+ @dataclass(frozen=True)
144
+ class ScreenshotCaptureResult:
145
+ content_type: str
146
+ width_px: int
147
+ height_px: int
148
+ body_base64: str
149
+
150
+
151
+ @dataclass
152
+ class RuntimeTransport:
153
+ endpoint: DeviceEndpoint
154
+ close: Callable[[], None]
155
+
156
+
157
+ @dataclass
158
+ class BootstrapResult:
159
+ connection: ConnectionSpec
160
+ transport: RuntimeTransport
161
+ meta: MetaInfo
@@ -0,0 +1,94 @@
1
+ """Error models and helpers for androidctld."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from androidctl_contracts.errors import (
10
+ DaemonError as ContractDaemonError,
11
+ )
12
+ from androidctl_contracts.errors import DaemonErrorCode as ContractDaemonErrorCode
13
+
14
+
15
+ class DaemonErrorCode(str, Enum):
16
+ DAEMON_BAD_REQUEST = "DAEMON_BAD_REQUEST"
17
+ DAEMON_UNAUTHORIZED = "DAEMON_UNAUTHORIZED"
18
+ WORKSPACE_BUSY = "WORKSPACE_BUSY"
19
+ RUNTIME_BUSY = "RUNTIME_BUSY"
20
+ RUNTIME_NOT_CONNECTED = "RUNTIME_NOT_CONNECTED"
21
+ SCREEN_NOT_READY = "SCREEN_NOT_READY"
22
+ COMMAND_NOT_FOUND = "COMMAND_NOT_FOUND"
23
+ COMMAND_CANCELLED = "COMMAND_CANCELLED"
24
+ REF_RESOLUTION_FAILED = "REF_RESOLUTION_FAILED"
25
+ REF_STALE = "REF_STALE"
26
+ TARGET_BLOCKED = "TARGET_BLOCKED"
27
+ TARGET_NOT_ACTIONABLE = "TARGET_NOT_ACTIONABLE"
28
+ WAIT_TIMEOUT = "WAIT_TIMEOUT"
29
+ DEVICE_RPC_FAILED = "DEVICE_RPC_FAILED"
30
+ DEVICE_DISCONNECTED = "DEVICE_DISCONNECTED"
31
+ DEVICE_AGENT_UNAVAILABLE = "DEVICE_AGENT_UNAVAILABLE"
32
+ DEVICE_AGENT_UNAUTHORIZED = "DEVICE_AGENT_UNAUTHORIZED"
33
+ DEVICE_AGENT_VERSION_MISMATCH = "DEVICE_AGENT_VERSION_MISMATCH"
34
+ DEVICE_AGENT_CAPABILITY_MISMATCH = "DEVICE_AGENT_CAPABILITY_MISMATCH"
35
+ ACCESSIBILITY_NOT_READY = "ACCESSIBILITY_NOT_READY"
36
+ OPEN_FAILED = "OPEN_FAILED"
37
+ ACTION_NOT_CONFIRMED = "ACTION_NOT_CONFIRMED"
38
+ TYPE_NOT_CONFIRMED = "TYPE_NOT_CONFIRMED"
39
+ SUBMIT_NOT_CONFIRMED = "SUBMIT_NOT_CONFIRMED"
40
+ DEVICE_RPC_TRANSPORT_RESET = "DEVICE_RPC_TRANSPORT_RESET"
41
+ INTERNAL_COMMAND_FAILURE = "INTERNAL_COMMAND_FAILURE"
42
+ WORKSPACE_UNAVAILABLE = "WORKSPACE_UNAVAILABLE"
43
+ ARTIFACT_ROOT_UNWRITABLE = "ARTIFACT_ROOT_UNWRITABLE"
44
+ ARTIFACT_WRITE_FAILED = "ARTIFACT_WRITE_FAILED"
45
+
46
+
47
+ _WIRE_CODE_BY_VALUE = {code.value: code for code in ContractDaemonErrorCode}
48
+
49
+
50
+ @dataclass
51
+ class DaemonError(Exception):
52
+ code: DaemonErrorCode
53
+ message: str
54
+ retryable: bool = False
55
+ details: dict[str, Any] = field(default_factory=dict)
56
+ http_status: int = 200
57
+
58
+ def __post_init__(self) -> None:
59
+ self.code = DaemonErrorCode(self.code)
60
+
61
+ def to_contract_error(self) -> ContractDaemonError:
62
+ return ContractDaemonError(
63
+ code=_to_contract_code(self.code),
64
+ message=self.message,
65
+ retryable=self.retryable,
66
+ details=dict(self.details),
67
+ )
68
+
69
+
70
+ def bad_request(message: str, details: dict[str, Any] | None = None) -> DaemonError:
71
+ return DaemonError(
72
+ code=DaemonErrorCode.DAEMON_BAD_REQUEST,
73
+ message=message,
74
+ retryable=False,
75
+ details=details or {},
76
+ http_status=400,
77
+ )
78
+
79
+
80
+ def unauthorized(message: str = "missing or invalid daemon token") -> DaemonError:
81
+ return DaemonError(
82
+ code=DaemonErrorCode.DAEMON_UNAUTHORIZED,
83
+ message=message,
84
+ retryable=False,
85
+ details={},
86
+ http_status=401,
87
+ )
88
+
89
+
90
+ def _to_contract_code(code: DaemonErrorCode) -> ContractDaemonErrorCode:
91
+ try:
92
+ return _WIRE_CODE_BY_VALUE[code.value]
93
+ except KeyError as error:
94
+ raise ValueError(f"{code.value} is not legal in DaemonErrorEnvelope") from error
@@ -0,0 +1,22 @@
1
+ """Logging configuration for androidctld."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ LOGGER_NAME = "androidctld"
8
+
9
+
10
+ def configure_logging(level: int = logging.INFO) -> logging.Logger:
11
+ logger = logging.getLogger(LOGGER_NAME)
12
+ if not logger.handlers:
13
+ handler = logging.StreamHandler()
14
+ formatter = logging.Formatter(
15
+ fmt="%(asctime)s %(levelname)s %(name)s %(message)s",
16
+ datefmt="%Y-%m-%dT%H:%M:%S",
17
+ )
18
+ handler.setFormatter(formatter)
19
+ logger.addHandler(handler)
20
+ logger.setLevel(level)
21
+ logger.propagate = False
22
+ return logger
@@ -0,0 +1,98 @@
1
+ """Shared observation-loop timing and cursor bookkeeping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from androidctld.device.types import EventsPollResult
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ObservationPolicy:
12
+ min_grace_ms: int
13
+ snapshot_max_interval_ms: int
14
+ stable_window_ms: int
15
+ max_total_ms: int
16
+ poll_slice_ms: int
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ObservationPollOutcome:
21
+ saw_events: bool
22
+ need_resync: bool
23
+ latest_seq: int
24
+
25
+
26
+ @dataclass
27
+ class ObservationLoop:
28
+ policy: ObservationPolicy
29
+ started_at: float
30
+ after_seq: int = 0
31
+ last_refresh_at: float | None = None
32
+ stable_since: float | None = None
33
+
34
+ @classmethod
35
+ def begin(cls, policy: ObservationPolicy, started_at: float) -> ObservationLoop:
36
+ return cls(policy=policy, started_at=started_at)
37
+
38
+ @property
39
+ def grace_deadline_at(self) -> float:
40
+ return self.started_at + (self.policy.min_grace_ms / 1000.0)
41
+
42
+ @property
43
+ def deadline_at(self) -> float:
44
+ return self.started_at + (self.policy.max_total_ms / 1000.0)
45
+
46
+ def timed_out(self, now: float) -> bool:
47
+ return now >= self.deadline_at
48
+
49
+ def remaining_ms(self, now: float) -> int:
50
+ return max(int((self.deadline_at - now) * 1000), 0)
51
+
52
+ def poll_wait_ms(self, now: float) -> int:
53
+ return min(self.policy.poll_slice_ms, self.remaining_ms(now))
54
+
55
+ def grace_elapsed(self, now: float) -> bool:
56
+ return now >= self.grace_deadline_at
57
+
58
+ def apply_poll_result(self, result: EventsPollResult) -> ObservationPollOutcome:
59
+ saw_events = bool(result.events)
60
+ need_resync = bool(result.need_resync)
61
+ latest_seq = int(result.latest_seq)
62
+ if need_resync:
63
+ self.after_seq = 0
64
+ self.stable_since = None
65
+ self.last_refresh_at = None
66
+ else:
67
+ self.after_seq = latest_seq
68
+ return ObservationPollOutcome(
69
+ saw_events=saw_events,
70
+ need_resync=need_resync,
71
+ latest_seq=latest_seq,
72
+ )
73
+
74
+ def should_refresh(
75
+ self, now: float, *, saw_events: bool, need_resync: bool
76
+ ) -> bool:
77
+ if need_resync:
78
+ return True
79
+ if self.last_refresh_at is None:
80
+ return True
81
+ if saw_events:
82
+ return True
83
+ return (
84
+ now - self.last_refresh_at
85
+ ) * 1000 >= self.policy.snapshot_max_interval_ms
86
+
87
+ def mark_refreshed(self, now: float) -> None:
88
+ self.last_refresh_at = now
89
+
90
+ def observe_stability(self, now: float, *, changed: bool) -> bool:
91
+ if changed:
92
+ self.stable_since = None
93
+ return False
94
+ if self.stable_since is None:
95
+ self.stable_since = now
96
+ if not self.grace_elapsed(now):
97
+ return False
98
+ return (now - self.stable_since) * 1000 >= self.policy.stable_window_ms
@@ -0,0 +1,53 @@
1
+ """Centralized protocol enums for androidctld runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class RuntimeStatus(str, Enum):
9
+ NEW = "new"
10
+ BOOTSTRAPPING = "bootstrapping"
11
+ CONNECTED = "connected"
12
+ READY = "ready"
13
+ BROKEN = "broken"
14
+ CLOSED = "closed"
15
+
16
+
17
+ class ConnectionMode(str, Enum):
18
+ ADB = "adb"
19
+ LAN = "lan"
20
+
21
+
22
+ class CommandKind(str, Enum):
23
+ CONNECT = "connect"
24
+ OBSERVE = "observe"
25
+ LIST_APPS = "listApps"
26
+ OPEN = "open"
27
+ TAP = "tap"
28
+ LONG_TAP = "longTap"
29
+ TYPE = "type"
30
+ FOCUS = "focus"
31
+ SUBMIT = "submit"
32
+ SCROLL = "scroll"
33
+ GLOBAL = "global"
34
+ WAIT = "wait"
35
+ SCREENSHOT = "screenshot"
36
+ CLOSE = "close"
37
+
38
+
39
+ class DeviceRpcMethod(str, Enum):
40
+ META_GET = "meta.get"
41
+ SNAPSHOT_GET = "snapshot.get"
42
+ EVENTS_POLL = "events.poll"
43
+ ACTION_PERFORM = "action.perform"
44
+ SCREENSHOT_CAPTURE = "screenshot.capture"
45
+
46
+
47
+ class DeviceRpcErrorCode(str, Enum):
48
+ STALE_TARGET = "STALE_TARGET"
49
+ TARGET_NOT_ACTIONABLE = "TARGET_NOT_ACTIONABLE"
50
+ ACTION_FAILED = "ACTION_FAILED"
51
+ ACTION_TIMEOUT = "ACTION_TIMEOUT"
52
+ RUNTIME_NOT_READY = "RUNTIME_NOT_READY"
53
+ ACCESSIBILITY_DISABLED = "ACCESSIBILITY_DISABLED"
@@ -0,0 +1 @@
1
+ """Ref helpers for semantic targets."""
@@ -0,0 +1,54 @@
1
+ """Runtime ref registry models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class RefFingerprint:
10
+ role: str
11
+ normalized_label: str
12
+ resource_id: str
13
+ class_name: str
14
+ parent_role: str
15
+ parent_label: str
16
+ sibling_labels: tuple[str, ...]
17
+ relative_bounds: tuple[int, int, int, int]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class NodeHandle:
22
+ snapshot_id: int
23
+ rid: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class SemanticProfile:
28
+ state: tuple[str, ...]
29
+ actions: tuple[str, ...]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class RefRepairSourceSignature:
34
+ ref: str
35
+ fingerprint: RefFingerprint
36
+ state: tuple[str, ...]
37
+ actions: tuple[str, ...]
38
+
39
+
40
+ @dataclass
41
+ class RefBinding:
42
+ ref: str
43
+ handle: NodeHandle
44
+ fingerprint: RefFingerprint
45
+ semantic_profile: SemanticProfile
46
+ reused: bool = False
47
+
48
+
49
+ @dataclass
50
+ class RefRegistry:
51
+ bindings: dict[str, RefBinding] = field(default_factory=dict)
52
+
53
+ def get(self, ref: str) -> RefBinding | None:
54
+ return self.bindings.get(ref)