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.
- androidctl/__init__.py +5 -0
- androidctl/__main__.py +4 -0
- androidctl/_version.py +1 -0
- androidctl/app.py +73 -0
- androidctl/cli_options.py +27 -0
- androidctl/command_payloads.py +264 -0
- androidctl/command_views.py +157 -0
- androidctl/commands/__init__.py +1 -0
- androidctl/commands/actions.py +236 -0
- androidctl/commands/adb_wireless.py +157 -0
- androidctl/commands/close.py +30 -0
- androidctl/commands/connect.py +69 -0
- androidctl/commands/execute.py +179 -0
- androidctl/commands/list_apps.py +26 -0
- androidctl/commands/observe.py +26 -0
- androidctl/commands/open.py +41 -0
- androidctl/commands/plumbing.py +58 -0
- androidctl/commands/run_pipeline.py +307 -0
- androidctl/commands/screenshot.py +29 -0
- androidctl/commands/setup.py +301 -0
- androidctl/commands/wait.py +60 -0
- androidctl/daemon/__init__.py +1 -0
- androidctl/daemon/client.py +348 -0
- androidctl/daemon/discovery.py +190 -0
- androidctl/daemon/launcher.py +26 -0
- androidctl/daemon/owner.py +349 -0
- androidctl/errors/__init__.py +1 -0
- androidctl/errors/mapping.py +149 -0
- androidctl/errors/models.py +16 -0
- androidctl/exit_codes.py +8 -0
- androidctl/output.py +147 -0
- androidctl/parsing/__init__.py +1 -0
- androidctl/parsing/duration.py +17 -0
- androidctl/parsing/open_target.py +51 -0
- androidctl/parsing/refs.py +12 -0
- androidctl/parsing/screen_id.py +10 -0
- androidctl/parsing/wait.py +70 -0
- androidctl/renderers/__init__.py +110 -0
- androidctl/renderers/_paths.py +109 -0
- androidctl/renderers/xml.py +234 -0
- androidctl/renderers/xml_projection.py +732 -0
- androidctl/resources/__init__.py +1 -0
- androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl/setup/__init__.py +1 -0
- androidctl/setup/accessibility.py +159 -0
- androidctl/setup/adb.py +586 -0
- androidctl/setup/apk_resource.py +29 -0
- androidctl/setup/pairing.py +70 -0
- androidctl/setup/verify.py +175 -0
- androidctl/workspace/__init__.py +3 -0
- androidctl/workspace/resolve.py +27 -0
- androidctl-0.1.0.dist-info/METADATA +217 -0
- androidctl-0.1.0.dist-info/RECORD +187 -0
- androidctl-0.1.0.dist-info/WHEEL +5 -0
- androidctl-0.1.0.dist-info/entry_points.txt +3 -0
- androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- androidctl-0.1.0.dist-info/top_level.txt +3 -0
- androidctl_contracts/__init__.py +55 -0
- androidctl_contracts/_version.py +1 -0
- androidctl_contracts/_wire_helpers.py +31 -0
- androidctl_contracts/base.py +142 -0
- androidctl_contracts/command_catalog.py +414 -0
- androidctl_contracts/command_results.py +630 -0
- androidctl_contracts/daemon_api.py +335 -0
- androidctl_contracts/errors.py +44 -0
- androidctl_contracts/paths.py +5 -0
- androidctl_contracts/public_screen.py +579 -0
- androidctl_contracts/user_state.py +23 -0
- androidctl_contracts/vocabulary.py +82 -0
- androidctld/__init__.py +5 -0
- androidctld/__main__.py +63 -0
- androidctld/_version.py +1 -0
- androidctld/actions/__init__.py +1 -0
- androidctld/actions/action_target.py +142 -0
- androidctld/actions/capabilities.py +539 -0
- androidctld/actions/executor.py +894 -0
- androidctld/actions/focus_confirmation.py +177 -0
- androidctld/actions/focused_input_admissibility.py +120 -0
- androidctld/actions/fresh_current.py +176 -0
- androidctld/actions/postconditions.py +473 -0
- androidctld/actions/repair.py +101 -0
- androidctld/actions/request_builder.py +204 -0
- androidctld/actions/settle.py +146 -0
- androidctld/actions/submit_confirmation.py +211 -0
- androidctld/actions/submit_routing.py +311 -0
- androidctld/actions/type_confirmation.py +257 -0
- androidctld/app_targets.py +71 -0
- androidctld/artifacts/__init__.py +1 -0
- androidctld/artifacts/models.py +26 -0
- androidctld/artifacts/screen_lookup.py +241 -0
- androidctld/artifacts/screen_payloads.py +109 -0
- androidctld/artifacts/writer.py +286 -0
- androidctld/auth/__init__.py +1 -0
- androidctld/auth/active_registry.py +266 -0
- androidctld/auth/secret_files.py +52 -0
- androidctld/auth/token_store.py +59 -0
- androidctld/commands/__init__.py +1 -0
- androidctld/commands/assembly.py +231 -0
- androidctld/commands/command_models.py +254 -0
- androidctld/commands/dispatch.py +99 -0
- androidctld/commands/executor.py +31 -0
- androidctld/commands/from_boundary.py +175 -0
- androidctld/commands/handlers/__init__.py +15 -0
- androidctld/commands/handlers/action.py +439 -0
- androidctld/commands/handlers/connect.py +94 -0
- androidctld/commands/handlers/list_apps.py +215 -0
- androidctld/commands/handlers/observe.py +121 -0
- androidctld/commands/handlers/screenshot.py +105 -0
- androidctld/commands/handlers/wait.py +286 -0
- androidctld/commands/models.py +65 -0
- androidctld/commands/open_targets.py +56 -0
- androidctld/commands/orchestration.py +353 -0
- androidctld/commands/registry.py +116 -0
- androidctld/commands/result_builders.py +40 -0
- androidctld/commands/result_models.py +555 -0
- androidctld/commands/results.py +108 -0
- androidctld/commands/semantic_command_names.py +17 -0
- androidctld/commands/semantic_error_mapping.py +93 -0
- androidctld/commands/semantic_truth.py +135 -0
- androidctld/commands/service.py +67 -0
- androidctld/config.py +75 -0
- androidctld/daemon/__init__.py +1 -0
- androidctld/daemon/active_slot.py +326 -0
- androidctld/daemon/envelope.py +30 -0
- androidctld/daemon/http_host.py +123 -0
- androidctld/daemon/ingress.py +112 -0
- androidctld/daemon/ownership_probe.py +204 -0
- androidctld/daemon/server.py +286 -0
- androidctld/daemon/service.py +99 -0
- androidctld/device/__init__.py +1 -0
- androidctld/device/action_models.py +154 -0
- androidctld/device/action_serialization.py +121 -0
- androidctld/device/adapters.py +220 -0
- androidctld/device/bootstrap.py +153 -0
- androidctld/device/connectors.py +231 -0
- androidctld/device/errors.py +100 -0
- androidctld/device/interfaces.py +58 -0
- androidctld/device/parsing.py +320 -0
- androidctld/device/rpc.py +483 -0
- androidctld/device/schema.py +114 -0
- androidctld/device/types.py +161 -0
- androidctld/errors/__init__.py +94 -0
- androidctld/logging/__init__.py +22 -0
- androidctld/observation.py +98 -0
- androidctld/protocol.py +53 -0
- androidctld/refs/__init__.py +1 -0
- androidctld/refs/models.py +54 -0
- androidctld/refs/repair.py +284 -0
- androidctld/refs/service.py +422 -0
- androidctld/rendering/__init__.py +1 -0
- androidctld/rendering/screen_xml.py +256 -0
- androidctld/runtime/__init__.py +21 -0
- androidctld/runtime/kernel.py +548 -0
- androidctld/runtime/lifecycle.py +19 -0
- androidctld/runtime/models.py +48 -0
- androidctld/runtime/screen_state.py +117 -0
- androidctld/runtime/state_repo.py +70 -0
- androidctld/runtime/store.py +76 -0
- androidctld/runtime_policy.py +127 -0
- androidctld/schema/__init__.py +5 -0
- androidctld/schema/base.py +132 -0
- androidctld/schema/core.py +35 -0
- androidctld/schema/daemon_api.py +108 -0
- androidctld/schema/persistence.py +161 -0
- androidctld/schema/persistence_io.py +41 -0
- androidctld/schema/validation_errors.py +309 -0
- androidctld/semantics/__init__.py +1 -0
- androidctld/semantics/compiler.py +610 -0
- androidctld/semantics/continuity.py +107 -0
- androidctld/semantics/labels.py +252 -0
- androidctld/semantics/models.py +25 -0
- androidctld/semantics/policy.py +23 -0
- androidctld/semantics/public_models.py +123 -0
- androidctld/semantics/registries.py +13 -0
- androidctld/semantics/submit_refs.py +417 -0
- androidctld/semantics/surface.py +254 -0
- androidctld/semantics/targets.py +167 -0
- androidctld/snapshots/__init__.py +1 -0
- androidctld/snapshots/models.py +219 -0
- androidctld/snapshots/refresh.py +273 -0
- androidctld/snapshots/schema.py +74 -0
- androidctld/snapshots/service.py +138 -0
- androidctld/text_equivalence.py +67 -0
- androidctld/waits/__init__.py +1 -0
- androidctld/waits/evaluators.py +216 -0
- androidctld/waits/loop.py +305 -0
- androidctld/waits/matcher.py +41 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Runtime command models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TypeAlias, TypeGuard
|
|
8
|
+
|
|
9
|
+
from androidctld.commands.open_targets import OpenAppTarget, OpenUrlTarget
|
|
10
|
+
from androidctld.device.types import ConnectionConfig
|
|
11
|
+
from androidctld.protocol import CommandKind
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WaitKind(str, Enum):
|
|
15
|
+
TEXT = "text"
|
|
16
|
+
SCREEN_CHANGE = "screen-change"
|
|
17
|
+
GONE = "gone"
|
|
18
|
+
APP = "app"
|
|
19
|
+
IDLE = "idle"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ConnectCommand:
|
|
24
|
+
connection: ConnectionConfig
|
|
25
|
+
kind: CommandKind = field(default=CommandKind.CONNECT, init=False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ObserveCommand:
|
|
30
|
+
kind: CommandKind = field(default=CommandKind.OBSERVE, init=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class ScreenshotCommand:
|
|
35
|
+
kind: CommandKind = field(default=CommandKind.SCREENSHOT, init=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ListAppsCommand:
|
|
40
|
+
kind: CommandKind = field(default=CommandKind.LIST_APPS, init=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _TypedActionCommandMixin:
|
|
44
|
+
kind: CommandKind
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class OpenCommand(_TypedActionCommandMixin):
|
|
49
|
+
target: OpenAppTarget | OpenUrlTarget
|
|
50
|
+
kind: CommandKind = field(default=CommandKind.OPEN, init=False)
|
|
51
|
+
|
|
52
|
+
def __post_init__(self) -> None:
|
|
53
|
+
if not isinstance(self.target, (OpenAppTarget, OpenUrlTarget)):
|
|
54
|
+
raise ValueError("open requires typed target")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class TapCommand(_TypedActionCommandMixin):
|
|
59
|
+
ref: str
|
|
60
|
+
source_screen_id: str
|
|
61
|
+
kind: CommandKind = field(default=CommandKind.TAP, init=False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class LongTapCommand(_TypedActionCommandMixin):
|
|
66
|
+
ref: str
|
|
67
|
+
source_screen_id: str
|
|
68
|
+
kind: CommandKind = field(default=CommandKind.LONG_TAP, init=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class TypeCommand(_TypedActionCommandMixin):
|
|
73
|
+
ref: str
|
|
74
|
+
source_screen_id: str
|
|
75
|
+
text: str
|
|
76
|
+
kind: CommandKind = field(default=CommandKind.TYPE, init=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class FocusCommand(_TypedActionCommandMixin):
|
|
81
|
+
ref: str
|
|
82
|
+
source_screen_id: str
|
|
83
|
+
kind: CommandKind = field(default=CommandKind.FOCUS, init=False)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class SubmitCommand(_TypedActionCommandMixin):
|
|
88
|
+
ref: str
|
|
89
|
+
source_screen_id: str
|
|
90
|
+
kind: CommandKind = field(default=CommandKind.SUBMIT, init=False)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class ScrollCommand(_TypedActionCommandMixin):
|
|
95
|
+
ref: str
|
|
96
|
+
source_screen_id: str
|
|
97
|
+
direction: str
|
|
98
|
+
kind: CommandKind = field(default=CommandKind.SCROLL, init=False)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class GlobalCommand(_TypedActionCommandMixin):
|
|
103
|
+
action: str
|
|
104
|
+
source_screen_id: str | None = None
|
|
105
|
+
kind: CommandKind = field(default=CommandKind.GLOBAL, init=False)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class TextWaitPredicate:
|
|
110
|
+
text: str
|
|
111
|
+
wait_kind: WaitKind = field(default=WaitKind.TEXT, init=False)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class ScreenChangeWaitPredicate:
|
|
116
|
+
source_screen_id: str
|
|
117
|
+
wait_kind: WaitKind = field(default=WaitKind.SCREEN_CHANGE, init=False)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class GoneWaitPredicate:
|
|
122
|
+
source_screen_id: str
|
|
123
|
+
ref: str
|
|
124
|
+
wait_kind: WaitKind = field(default=WaitKind.GONE, init=False)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True, slots=True)
|
|
128
|
+
class AppWaitPredicate:
|
|
129
|
+
package_name: str
|
|
130
|
+
wait_kind: WaitKind = field(default=WaitKind.APP, init=False)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True, slots=True)
|
|
134
|
+
class IdleWaitPredicate:
|
|
135
|
+
wait_kind: WaitKind = field(default=WaitKind.IDLE, init=False)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
WaitPredicate: TypeAlias = (
|
|
139
|
+
TextWaitPredicate
|
|
140
|
+
| ScreenChangeWaitPredicate
|
|
141
|
+
| GoneWaitPredicate
|
|
142
|
+
| AppWaitPredicate
|
|
143
|
+
| IdleWaitPredicate
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
WAIT_PREDICATE_TYPES = (
|
|
147
|
+
TextWaitPredicate,
|
|
148
|
+
ScreenChangeWaitPredicate,
|
|
149
|
+
GoneWaitPredicate,
|
|
150
|
+
AppWaitPredicate,
|
|
151
|
+
IdleWaitPredicate,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class WaitCommand:
|
|
157
|
+
predicate: WaitPredicate
|
|
158
|
+
timeout_ms: int | None = None
|
|
159
|
+
kind: CommandKind = field(default=CommandKind.WAIT, init=False)
|
|
160
|
+
|
|
161
|
+
def __post_init__(self) -> None:
|
|
162
|
+
if not is_wait_predicate(self.predicate):
|
|
163
|
+
raise ValueError("wait requires typed predicate")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def wait_kind(self) -> WaitKind:
|
|
167
|
+
return self.predicate.wait_kind
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
TypedActionCommand: TypeAlias = (
|
|
171
|
+
OpenCommand
|
|
172
|
+
| TapCommand
|
|
173
|
+
| LongTapCommand
|
|
174
|
+
| TypeCommand
|
|
175
|
+
| FocusCommand
|
|
176
|
+
| SubmitCommand
|
|
177
|
+
| ScrollCommand
|
|
178
|
+
| GlobalCommand
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
ActionCommand: TypeAlias = TypedActionCommand
|
|
182
|
+
|
|
183
|
+
REF_BOUND_ACTION_COMMAND_TYPES = (
|
|
184
|
+
TapCommand,
|
|
185
|
+
LongTapCommand,
|
|
186
|
+
TypeCommand,
|
|
187
|
+
FocusCommand,
|
|
188
|
+
SubmitCommand,
|
|
189
|
+
ScrollCommand,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
RefBoundActionCommand: TypeAlias = (
|
|
193
|
+
TapCommand
|
|
194
|
+
| LongTapCommand
|
|
195
|
+
| TypeCommand
|
|
196
|
+
| FocusCommand
|
|
197
|
+
| SubmitCommand
|
|
198
|
+
| ScrollCommand
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
InternalCommand: TypeAlias = (
|
|
202
|
+
ConnectCommand
|
|
203
|
+
| ObserveCommand
|
|
204
|
+
| ListAppsCommand
|
|
205
|
+
| ActionCommand
|
|
206
|
+
| WaitCommand
|
|
207
|
+
| ScreenshotCommand
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def wait_timeout_ms(command: WaitCommand) -> int | None:
|
|
212
|
+
return command.timeout_ms
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def is_wait_predicate(value: object) -> TypeGuard[WaitPredicate]:
|
|
216
|
+
return isinstance(value, WAIT_PREDICATE_TYPES)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def is_ref_bound_action_command(command: object) -> TypeGuard[RefBoundActionCommand]:
|
|
220
|
+
return isinstance(command, REF_BOUND_ACTION_COMMAND_TYPES)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
__all__ = [
|
|
224
|
+
"REF_BOUND_ACTION_COMMAND_TYPES",
|
|
225
|
+
"ActionCommand",
|
|
226
|
+
"AppWaitPredicate",
|
|
227
|
+
"ConnectCommand",
|
|
228
|
+
"FocusCommand",
|
|
229
|
+
"GlobalCommand",
|
|
230
|
+
"GoneWaitPredicate",
|
|
231
|
+
"IdleWaitPredicate",
|
|
232
|
+
"InternalCommand",
|
|
233
|
+
"ListAppsCommand",
|
|
234
|
+
"LongTapCommand",
|
|
235
|
+
"ObserveCommand",
|
|
236
|
+
"OpenAppTarget",
|
|
237
|
+
"OpenCommand",
|
|
238
|
+
"OpenUrlTarget",
|
|
239
|
+
"RefBoundActionCommand",
|
|
240
|
+
"ScreenChangeWaitPredicate",
|
|
241
|
+
"ScreenshotCommand",
|
|
242
|
+
"ScrollCommand",
|
|
243
|
+
"SubmitCommand",
|
|
244
|
+
"TapCommand",
|
|
245
|
+
"TextWaitPredicate",
|
|
246
|
+
"TypeCommand",
|
|
247
|
+
"TypedActionCommand",
|
|
248
|
+
"WaitCommand",
|
|
249
|
+
"WaitKind",
|
|
250
|
+
"WaitPredicate",
|
|
251
|
+
"is_ref_bound_action_command",
|
|
252
|
+
"is_wait_predicate",
|
|
253
|
+
"wait_timeout_ms",
|
|
254
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Per-command semantic boundary dispatch helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from androidctld.commands.command_models import (
|
|
8
|
+
ConnectCommand,
|
|
9
|
+
GlobalCommand,
|
|
10
|
+
InternalCommand,
|
|
11
|
+
ListAppsCommand,
|
|
12
|
+
ObserveCommand,
|
|
13
|
+
OpenCommand,
|
|
14
|
+
ScreenshotCommand,
|
|
15
|
+
WaitCommand,
|
|
16
|
+
is_ref_bound_action_command,
|
|
17
|
+
)
|
|
18
|
+
from androidctld.commands.executor import CommandHandler, CommandResult
|
|
19
|
+
from androidctld.commands.handlers.action import ActionCommandHandler
|
|
20
|
+
from androidctld.commands.handlers.connect import ConnectCommandHandler
|
|
21
|
+
from androidctld.commands.handlers.list_apps import ListAppsCommandHandler
|
|
22
|
+
from androidctld.commands.handlers.observe import ObserveCommandHandler
|
|
23
|
+
from androidctld.commands.handlers.screenshot import ScreenshotCommandHandler
|
|
24
|
+
from androidctld.commands.handlers.wait import WaitCommandHandler
|
|
25
|
+
from androidctld.commands.registry import COMMAND_SPECS
|
|
26
|
+
|
|
27
|
+
__all__ = ["CommandDispatch"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CommandDispatch:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
connect_handler: ConnectCommandHandler,
|
|
35
|
+
observe_handler: ObserveCommandHandler,
|
|
36
|
+
list_apps_handler: ListAppsCommandHandler,
|
|
37
|
+
action_handler: ActionCommandHandler,
|
|
38
|
+
wait_handler: WaitCommandHandler,
|
|
39
|
+
screenshot_handler: ScreenshotCommandHandler,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._connect_handler = connect_handler
|
|
42
|
+
self._observe_handler = observe_handler
|
|
43
|
+
self._list_apps_handler = list_apps_handler
|
|
44
|
+
self._action_handler = action_handler
|
|
45
|
+
self._wait_handler = wait_handler
|
|
46
|
+
self._screenshot_handler = screenshot_handler
|
|
47
|
+
|
|
48
|
+
def build_handlers(self) -> dict[str, CommandHandler]:
|
|
49
|
+
return {
|
|
50
|
+
spec.daemon_kind: cast(
|
|
51
|
+
CommandHandler,
|
|
52
|
+
getattr(self, spec.dispatch_method_name),
|
|
53
|
+
)
|
|
54
|
+
for spec in COMMAND_SPECS.values()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def execute_connect(self, *, command: InternalCommand) -> CommandResult:
|
|
58
|
+
if not isinstance(command, ConnectCommand):
|
|
59
|
+
raise TypeError(f"connect handler received {command.kind!r} command")
|
|
60
|
+
return self._connect_handler.handle(command=command)
|
|
61
|
+
|
|
62
|
+
def execute_observe(self, *, command: InternalCommand) -> CommandResult:
|
|
63
|
+
if not isinstance(command, ObserveCommand):
|
|
64
|
+
raise TypeError(f"observe handler received {command.kind!r} command")
|
|
65
|
+
return self._observe_handler.handle(command=command)
|
|
66
|
+
|
|
67
|
+
def execute_list_apps(self, *, command: InternalCommand) -> CommandResult:
|
|
68
|
+
if not isinstance(command, ListAppsCommand):
|
|
69
|
+
raise TypeError(f"list-apps handler received {command.kind!r} command")
|
|
70
|
+
return self._list_apps_handler.handle(command=command)
|
|
71
|
+
|
|
72
|
+
def execute_open(self, *, command: InternalCommand) -> CommandResult:
|
|
73
|
+
if not isinstance(command, OpenCommand):
|
|
74
|
+
raise TypeError(f"open handler received {command.kind!r} command")
|
|
75
|
+
return self._action_handler.handle_open(command=command)
|
|
76
|
+
|
|
77
|
+
def execute_ref_action(self, *, command: InternalCommand) -> CommandResult:
|
|
78
|
+
if not is_ref_bound_action_command(command):
|
|
79
|
+
raise TypeError(f"ref action handler received {command.kind!r} command")
|
|
80
|
+
return self._action_handler.handle_ref_action(command=command)
|
|
81
|
+
|
|
82
|
+
def execute_global_action(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
command: InternalCommand,
|
|
86
|
+
) -> CommandResult:
|
|
87
|
+
if not isinstance(command, GlobalCommand):
|
|
88
|
+
raise TypeError(f"global action handler received {command.kind!r} command")
|
|
89
|
+
return self._action_handler.handle_global_action(command=command)
|
|
90
|
+
|
|
91
|
+
def execute_wait(self, *, command: InternalCommand) -> CommandResult:
|
|
92
|
+
if isinstance(command, WaitCommand):
|
|
93
|
+
return self._wait_handler.handle_service_wait(command=command)
|
|
94
|
+
raise TypeError(f"wait handler received {command.kind!r} command")
|
|
95
|
+
|
|
96
|
+
def execute_screenshot(self, *, command: InternalCommand) -> CommandResult:
|
|
97
|
+
if not isinstance(command, ScreenshotCommand):
|
|
98
|
+
raise TypeError(f"screenshot handler received {command.kind!r} command")
|
|
99
|
+
return self._screenshot_handler.handle(command=command)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Thin semantic command executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
from androidctld.commands.command_models import InternalCommand
|
|
8
|
+
from androidctld.commands.registry import resolve_command_spec
|
|
9
|
+
|
|
10
|
+
CommandResult = dict[str, object]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CommandHandler(Protocol):
|
|
14
|
+
def __call__(self, *, command: InternalCommand) -> CommandResult: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommandExecutor:
|
|
18
|
+
def __init__(self, *, handlers: dict[str, CommandHandler]) -> None:
|
|
19
|
+
self._handlers = handlers
|
|
20
|
+
|
|
21
|
+
def run(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
command: InternalCommand,
|
|
25
|
+
) -> CommandResult:
|
|
26
|
+
spec = resolve_command_spec(command)
|
|
27
|
+
handler = self._handlers[spec.command_name]
|
|
28
|
+
return handler(command=command)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["CommandExecutor", "CommandHandler", "CommandResult"]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Pure compilers from validated shared wire payloads to executable commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from androidctl_contracts import daemon_api as wire_api
|
|
6
|
+
from androidctld.commands.command_models import (
|
|
7
|
+
AppWaitPredicate,
|
|
8
|
+
ConnectCommand,
|
|
9
|
+
FocusCommand,
|
|
10
|
+
GlobalCommand,
|
|
11
|
+
GoneWaitPredicate,
|
|
12
|
+
IdleWaitPredicate,
|
|
13
|
+
ListAppsCommand,
|
|
14
|
+
LongTapCommand,
|
|
15
|
+
ObserveCommand,
|
|
16
|
+
OpenAppTarget,
|
|
17
|
+
OpenCommand,
|
|
18
|
+
OpenUrlTarget,
|
|
19
|
+
RefBoundActionCommand,
|
|
20
|
+
ScreenChangeWaitPredicate,
|
|
21
|
+
ScreenshotCommand,
|
|
22
|
+
ScrollCommand,
|
|
23
|
+
SubmitCommand,
|
|
24
|
+
TapCommand,
|
|
25
|
+
TextWaitPredicate,
|
|
26
|
+
TypeCommand,
|
|
27
|
+
WaitCommand,
|
|
28
|
+
)
|
|
29
|
+
from androidctld.device.types import ConnectionConfig
|
|
30
|
+
from androidctld.protocol import ConnectionMode
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compile_connect_command(payload: wire_api.ConnectCommandPayload) -> ConnectCommand:
|
|
34
|
+
connection = payload.connection
|
|
35
|
+
mode = ConnectionMode(connection.mode)
|
|
36
|
+
if mode is ConnectionMode.LAN:
|
|
37
|
+
if connection.host is None or connection.port is None:
|
|
38
|
+
raise TypeError("lan connect wire payload requires host and port")
|
|
39
|
+
return ConnectCommand(
|
|
40
|
+
connection=ConnectionConfig(
|
|
41
|
+
mode=mode,
|
|
42
|
+
token=connection.token,
|
|
43
|
+
host=connection.host,
|
|
44
|
+
port=connection.port,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
return ConnectCommand(
|
|
48
|
+
connection=ConnectionConfig(
|
|
49
|
+
mode=mode,
|
|
50
|
+
token=connection.token,
|
|
51
|
+
serial=connection.serial,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compile_observe_command(payload: wire_api.ObserveCommandPayload) -> ObserveCommand:
|
|
57
|
+
del payload
|
|
58
|
+
return ObserveCommand()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def compile_list_apps_command(
|
|
62
|
+
payload: wire_api.ListAppsCommandPayload,
|
|
63
|
+
) -> ListAppsCommand:
|
|
64
|
+
del payload
|
|
65
|
+
return ListAppsCommand()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def compile_open_command(payload: wire_api.OpenCommandPayload) -> OpenCommand:
|
|
69
|
+
target = payload.target
|
|
70
|
+
if isinstance(target, wire_api.OpenAppTargetPayload):
|
|
71
|
+
return OpenCommand(target=OpenAppTarget(package_name=target.value))
|
|
72
|
+
if isinstance(target, wire_api.OpenUrlTargetPayload):
|
|
73
|
+
return OpenCommand(target=OpenUrlTarget(url=target.value))
|
|
74
|
+
raise TypeError(f"unsupported open target payload: {type(target)!r}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compile_ref_action_command(
|
|
78
|
+
payload: (
|
|
79
|
+
wire_api.RefActionCommandPayload
|
|
80
|
+
| wire_api.TypeCommandPayload
|
|
81
|
+
| wire_api.ScrollCommandPayload
|
|
82
|
+
),
|
|
83
|
+
) -> RefBoundActionCommand:
|
|
84
|
+
if isinstance(payload, wire_api.TypeCommandPayload):
|
|
85
|
+
return TypeCommand(
|
|
86
|
+
ref=payload.ref,
|
|
87
|
+
source_screen_id=payload.source_screen_id,
|
|
88
|
+
text=payload.text,
|
|
89
|
+
)
|
|
90
|
+
if isinstance(payload, wire_api.ScrollCommandPayload):
|
|
91
|
+
return ScrollCommand(
|
|
92
|
+
ref=payload.ref,
|
|
93
|
+
source_screen_id=payload.source_screen_id,
|
|
94
|
+
direction=payload.direction,
|
|
95
|
+
)
|
|
96
|
+
if payload.kind == "tap":
|
|
97
|
+
return TapCommand(ref=payload.ref, source_screen_id=payload.source_screen_id)
|
|
98
|
+
if payload.kind == "longTap":
|
|
99
|
+
return LongTapCommand(
|
|
100
|
+
ref=payload.ref,
|
|
101
|
+
source_screen_id=payload.source_screen_id,
|
|
102
|
+
)
|
|
103
|
+
if payload.kind == "focus":
|
|
104
|
+
return FocusCommand(ref=payload.ref, source_screen_id=payload.source_screen_id)
|
|
105
|
+
if payload.kind == "submit":
|
|
106
|
+
return SubmitCommand(
|
|
107
|
+
ref=payload.ref,
|
|
108
|
+
source_screen_id=payload.source_screen_id,
|
|
109
|
+
)
|
|
110
|
+
raise TypeError(f"unsupported ref action payload kind: {payload.kind!r}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def compile_global_action_command(
|
|
114
|
+
payload: wire_api.GlobalActionCommandPayload,
|
|
115
|
+
) -> GlobalCommand:
|
|
116
|
+
return GlobalCommand(
|
|
117
|
+
action=payload.kind,
|
|
118
|
+
source_screen_id=payload.source_screen_id,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def compile_service_wait_command(
|
|
123
|
+
payload: wire_api.WaitCommandPayload,
|
|
124
|
+
) -> WaitCommand:
|
|
125
|
+
predicate = payload.predicate
|
|
126
|
+
if isinstance(predicate, wire_api.TextPresentPredicatePayload):
|
|
127
|
+
return WaitCommand(
|
|
128
|
+
predicate=TextWaitPredicate(text=predicate.text),
|
|
129
|
+
timeout_ms=payload.timeout_ms,
|
|
130
|
+
)
|
|
131
|
+
if isinstance(predicate, wire_api.ScreenChangePredicatePayload):
|
|
132
|
+
return WaitCommand(
|
|
133
|
+
predicate=ScreenChangeWaitPredicate(
|
|
134
|
+
source_screen_id=predicate.source_screen_id,
|
|
135
|
+
),
|
|
136
|
+
timeout_ms=payload.timeout_ms,
|
|
137
|
+
)
|
|
138
|
+
if isinstance(predicate, wire_api.GonePredicatePayload):
|
|
139
|
+
return WaitCommand(
|
|
140
|
+
predicate=GoneWaitPredicate(
|
|
141
|
+
source_screen_id=predicate.source_screen_id,
|
|
142
|
+
ref=predicate.ref,
|
|
143
|
+
),
|
|
144
|
+
timeout_ms=payload.timeout_ms,
|
|
145
|
+
)
|
|
146
|
+
if isinstance(predicate, wire_api.AppPredicatePayload):
|
|
147
|
+
return WaitCommand(
|
|
148
|
+
predicate=AppWaitPredicate(package_name=predicate.package_name),
|
|
149
|
+
timeout_ms=payload.timeout_ms,
|
|
150
|
+
)
|
|
151
|
+
if isinstance(predicate, wire_api.IdlePredicatePayload):
|
|
152
|
+
return WaitCommand(
|
|
153
|
+
predicate=IdleWaitPredicate(),
|
|
154
|
+
timeout_ms=payload.timeout_ms,
|
|
155
|
+
)
|
|
156
|
+
raise TypeError(f"unsupported wait predicate payload: {type(predicate)!r}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def compile_screenshot_command(
|
|
160
|
+
payload: wire_api.ScreenshotCommandPayload,
|
|
161
|
+
) -> ScreenshotCommand:
|
|
162
|
+
del payload
|
|
163
|
+
return ScreenshotCommand()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
__all__ = [
|
|
167
|
+
"compile_connect_command",
|
|
168
|
+
"compile_global_action_command",
|
|
169
|
+
"compile_list_apps_command",
|
|
170
|
+
"compile_observe_command",
|
|
171
|
+
"compile_open_command",
|
|
172
|
+
"compile_ref_action_command",
|
|
173
|
+
"compile_screenshot_command",
|
|
174
|
+
"compile_service_wait_command",
|
|
175
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Command handler modules."""
|
|
2
|
+
|
|
3
|
+
from androidctld.commands.handlers.action import ActionCommandHandler
|
|
4
|
+
from androidctld.commands.handlers.connect import ConnectCommandHandler
|
|
5
|
+
from androidctld.commands.handlers.observe import ObserveCommandHandler
|
|
6
|
+
from androidctld.commands.handlers.screenshot import ScreenshotCommandHandler
|
|
7
|
+
from androidctld.commands.handlers.wait import WaitCommandHandler
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ActionCommandHandler",
|
|
11
|
+
"ConnectCommandHandler",
|
|
12
|
+
"ObserveCommandHandler",
|
|
13
|
+
"ScreenshotCommandHandler",
|
|
14
|
+
"WaitCommandHandler",
|
|
15
|
+
]
|