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
androidctl/__init__.py
ADDED
androidctl/__main__.py
ADDED
androidctl/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
androidctl/app.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import typer
|
|
5
|
+
from typer.core import TyperGroup
|
|
6
|
+
|
|
7
|
+
from androidctl import __version__
|
|
8
|
+
from androidctl.cli_options import CliOptions
|
|
9
|
+
from androidctl.command_views import help_order_for_public_command
|
|
10
|
+
from androidctl.commands.actions import register as register_action_commands
|
|
11
|
+
from androidctl.commands.adb_wireless import register as register_adb_wireless_commands
|
|
12
|
+
from androidctl.commands.close import register as register_close_command
|
|
13
|
+
from androidctl.commands.connect import register as register_connect_command
|
|
14
|
+
from androidctl.commands.list_apps import register as register_list_apps_command
|
|
15
|
+
from androidctl.commands.observe import register as register_observe_command
|
|
16
|
+
from androidctl.commands.open import register as register_open_command
|
|
17
|
+
from androidctl.commands.screenshot import register as register_screenshot_command
|
|
18
|
+
from androidctl.commands.setup import register as register_setup_command
|
|
19
|
+
from androidctl.commands.wait import register as register_wait_command
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OrderedHelpGroup(TyperGroup):
|
|
23
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
24
|
+
names = list(super().list_commands(ctx))
|
|
25
|
+
return sorted(names, key=help_order_for_public_command)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="androidctl",
|
|
30
|
+
cls=OrderedHelpGroup,
|
|
31
|
+
help=(
|
|
32
|
+
"Agent loop: observe/list-apps/open -> act -> wait. Retained support routes: "
|
|
33
|
+
"connect, screenshot, close."
|
|
34
|
+
),
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
pretty_exceptions_enable=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _show_version(value: bool) -> None:
|
|
41
|
+
if not value:
|
|
42
|
+
return
|
|
43
|
+
typer.echo(__version__)
|
|
44
|
+
raise typer.Exit()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.callback()
|
|
48
|
+
def main(
|
|
49
|
+
ctx: typer.Context,
|
|
50
|
+
version: bool = typer.Option(
|
|
51
|
+
False,
|
|
52
|
+
"--version",
|
|
53
|
+
callback=_show_version,
|
|
54
|
+
is_eager=True,
|
|
55
|
+
help="Show the androidctl release version and exit.",
|
|
56
|
+
),
|
|
57
|
+
workspace_root: Path | None = typer.Option(None, "--workspace-root"),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Public happy path for driving the daemon-backed device runtime."""
|
|
60
|
+
del version
|
|
61
|
+
ctx.obj = CliOptions(workspace_root=workspace_root)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
register_connect_command(app)
|
|
65
|
+
register_observe_command(app)
|
|
66
|
+
register_list_apps_command(app)
|
|
67
|
+
register_open_command(app)
|
|
68
|
+
register_action_commands(app)
|
|
69
|
+
register_wait_command(app)
|
|
70
|
+
register_screenshot_command(app)
|
|
71
|
+
register_close_command(app)
|
|
72
|
+
register_setup_command(app)
|
|
73
|
+
register_adb_wireless_commands(app)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class CliOptions:
|
|
11
|
+
workspace_root: Path | None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_cli_options(ctx: typer.Context) -> CliOptions:
|
|
15
|
+
payload = ctx.obj
|
|
16
|
+
if isinstance(payload, CliOptions):
|
|
17
|
+
return payload
|
|
18
|
+
return CliOptions(workspace_root=None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def command_cli_options(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
*,
|
|
24
|
+
workspace_root: Path | None = None,
|
|
25
|
+
) -> CliOptions:
|
|
26
|
+
root_options = read_cli_options(ctx)
|
|
27
|
+
return CliOptions(workspace_root=workspace_root or root_options.workspace_root)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Final, Literal, TypeAlias
|
|
5
|
+
|
|
6
|
+
from androidctl_contracts.daemon_api import (
|
|
7
|
+
DaemonCommandPayload,
|
|
8
|
+
GlobalActionCommandPayload,
|
|
9
|
+
GonePredicatePayload,
|
|
10
|
+
LiveScreenBoundCommandPayload,
|
|
11
|
+
RefActionCommandPayload,
|
|
12
|
+
ScreenChangePredicatePayload,
|
|
13
|
+
ScreenRelativeWaitPredicatePayload,
|
|
14
|
+
ScrollCommandPayload,
|
|
15
|
+
TypeCommandPayload,
|
|
16
|
+
WaitCommandPayload,
|
|
17
|
+
WaitPredicatePayload,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ScrollDirection = Literal["up", "down", "left", "right", "backward"]
|
|
21
|
+
RefActionKind = Literal["tap", "longTap", "focus", "submit"]
|
|
22
|
+
GlobalActionKind = Literal["back", "home", "recents", "notifications"]
|
|
23
|
+
LateBoundActionKind = Literal["tap", "longTap", "focus", "submit", "type", "scroll"]
|
|
24
|
+
SCROLL_DIRECTIONS: Final = frozenset({"up", "down", "left", "right", "backward"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_optional_cli_string(value: str | None) -> str | None:
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
normalized = value.strip()
|
|
31
|
+
return normalized or None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _normalize_source_screen_id(value: str | None) -> str | None:
|
|
35
|
+
if value is None:
|
|
36
|
+
return None
|
|
37
|
+
normalized = value.strip()
|
|
38
|
+
if not normalized:
|
|
39
|
+
raise ValueError("source_screen_id must be non-empty when provided")
|
|
40
|
+
return normalized
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _required_source_screen_id(value: str) -> str:
|
|
44
|
+
normalized = _normalize_optional_cli_string(value)
|
|
45
|
+
if normalized is None:
|
|
46
|
+
raise RuntimeError("prepared command is missing source_screen_id")
|
|
47
|
+
return normalized
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _validate_timeout_ms(value: int | None) -> int | None:
|
|
51
|
+
if isinstance(value, bool):
|
|
52
|
+
raise TypeError("timeout_ms must be an int, not bool")
|
|
53
|
+
if value is not None and not isinstance(value, int):
|
|
54
|
+
raise TypeError("timeout_ms must be an int")
|
|
55
|
+
if value is not None and value < 0:
|
|
56
|
+
raise ValueError("timeout_ms must be >= 0")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class LateBoundActionCommand:
|
|
62
|
+
kind: LateBoundActionKind
|
|
63
|
+
ref: str
|
|
64
|
+
text: str | None = None
|
|
65
|
+
direction: ScrollDirection | None = None
|
|
66
|
+
|
|
67
|
+
def __post_init__(self) -> None:
|
|
68
|
+
if _normalize_optional_cli_string(self.ref) is None:
|
|
69
|
+
raise ValueError("late-bound action command requires ref")
|
|
70
|
+
if self.kind == "type":
|
|
71
|
+
if self.text is None:
|
|
72
|
+
raise ValueError("late-bound type command requires text")
|
|
73
|
+
if self.direction is not None:
|
|
74
|
+
raise ValueError("late-bound type command does not accept direction")
|
|
75
|
+
return
|
|
76
|
+
if self.kind == "scroll":
|
|
77
|
+
if self.direction is None:
|
|
78
|
+
raise ValueError("late-bound scroll command requires direction")
|
|
79
|
+
if self.direction not in SCROLL_DIRECTIONS:
|
|
80
|
+
raise ValueError("late-bound scroll command requires a valid direction")
|
|
81
|
+
if self.text is not None:
|
|
82
|
+
raise ValueError("late-bound scroll command does not accept text")
|
|
83
|
+
return
|
|
84
|
+
if self.text is not None or self.direction is not None:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"late-bound ref action commands only accept kind and ref fields"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def bind(self, source_screen_id: str) -> LiveScreenBoundCommandPayload:
|
|
90
|
+
resolved_source_screen_id = _required_source_screen_id(source_screen_id)
|
|
91
|
+
if self.kind == "type":
|
|
92
|
+
if self.text is None:
|
|
93
|
+
raise RuntimeError("prepared type command is missing text")
|
|
94
|
+
return TypeCommandPayload(
|
|
95
|
+
kind="type",
|
|
96
|
+
ref=self.ref,
|
|
97
|
+
text=self.text,
|
|
98
|
+
source_screen_id=resolved_source_screen_id,
|
|
99
|
+
)
|
|
100
|
+
if self.kind == "scroll":
|
|
101
|
+
if self.direction is None:
|
|
102
|
+
raise RuntimeError("prepared scroll command is missing direction")
|
|
103
|
+
return ScrollCommandPayload(
|
|
104
|
+
kind="scroll",
|
|
105
|
+
ref=self.ref,
|
|
106
|
+
direction=self.direction,
|
|
107
|
+
source_screen_id=resolved_source_screen_id,
|
|
108
|
+
)
|
|
109
|
+
return RefActionCommandPayload(
|
|
110
|
+
kind=self.kind,
|
|
111
|
+
ref=self.ref,
|
|
112
|
+
source_screen_id=resolved_source_screen_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class LateBoundScreenRelativePredicate:
|
|
118
|
+
kind: Literal["screen-change", "gone"]
|
|
119
|
+
ref: str | None = None
|
|
120
|
+
|
|
121
|
+
def __post_init__(self) -> None:
|
|
122
|
+
normalized_ref = _normalize_optional_cli_string(self.ref)
|
|
123
|
+
if self.kind == "gone" and normalized_ref is None:
|
|
124
|
+
raise ValueError("late-bound gone predicate requires a non-empty ref")
|
|
125
|
+
if self.kind == "screen-change" and self.ref is not None:
|
|
126
|
+
raise ValueError("screen-change predicate does not accept ref")
|
|
127
|
+
|
|
128
|
+
def bind(self, source_screen_id: str) -> ScreenRelativeWaitPredicatePayload:
|
|
129
|
+
resolved_source_screen_id = _required_source_screen_id(source_screen_id)
|
|
130
|
+
if self.kind == "screen-change":
|
|
131
|
+
return ScreenChangePredicatePayload(
|
|
132
|
+
kind="screen-change",
|
|
133
|
+
source_screen_id=resolved_source_screen_id,
|
|
134
|
+
)
|
|
135
|
+
if self.ref is None:
|
|
136
|
+
raise RuntimeError("prepared gone predicate is missing ref")
|
|
137
|
+
return GonePredicatePayload(
|
|
138
|
+
kind="gone",
|
|
139
|
+
ref=self.ref,
|
|
140
|
+
source_screen_id=resolved_source_screen_id,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
CliWaitPredicatePayload: TypeAlias = (
|
|
145
|
+
WaitPredicatePayload | LateBoundScreenRelativePredicate
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True)
|
|
150
|
+
class LateBoundWaitCommand:
|
|
151
|
+
predicate: LateBoundScreenRelativePredicate
|
|
152
|
+
timeout_ms: int | None = None
|
|
153
|
+
|
|
154
|
+
def __post_init__(self) -> None:
|
|
155
|
+
_validate_timeout_ms(self.timeout_ms)
|
|
156
|
+
|
|
157
|
+
def bind(self, source_screen_id: str) -> WaitCommandPayload:
|
|
158
|
+
return WaitCommandPayload(
|
|
159
|
+
kind="wait",
|
|
160
|
+
predicate=self.predicate.bind(source_screen_id),
|
|
161
|
+
timeout_ms=self.timeout_ms,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class LateBoundGlobalActionCommand:
|
|
167
|
+
kind: GlobalActionKind
|
|
168
|
+
|
|
169
|
+
def bind(self, source_screen_id: str | None) -> GlobalActionCommandPayload:
|
|
170
|
+
normalized_source_screen_id = (
|
|
171
|
+
None if source_screen_id is None else source_screen_id.strip() or None
|
|
172
|
+
)
|
|
173
|
+
return GlobalActionCommandPayload(
|
|
174
|
+
kind=self.kind,
|
|
175
|
+
source_screen_id=normalized_source_screen_id,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
LateBoundCommand: TypeAlias = (
|
|
180
|
+
LateBoundActionCommand | LateBoundGlobalActionCommand | LateBoundWaitCommand
|
|
181
|
+
)
|
|
182
|
+
CliCommandPayload: TypeAlias = DaemonCommandPayload | LateBoundCommand
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_ref_action_command(
|
|
186
|
+
*,
|
|
187
|
+
kind: RefActionKind,
|
|
188
|
+
ref: str,
|
|
189
|
+
source_screen_id: str | None,
|
|
190
|
+
) -> CliCommandPayload:
|
|
191
|
+
normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
|
|
192
|
+
if normalized_source_screen_id is None:
|
|
193
|
+
return LateBoundActionCommand(kind=kind, ref=ref)
|
|
194
|
+
return RefActionCommandPayload(
|
|
195
|
+
kind=kind,
|
|
196
|
+
ref=ref,
|
|
197
|
+
source_screen_id=normalized_source_screen_id,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def build_type_command(
|
|
202
|
+
*,
|
|
203
|
+
ref: str,
|
|
204
|
+
text: str,
|
|
205
|
+
source_screen_id: str | None,
|
|
206
|
+
) -> CliCommandPayload:
|
|
207
|
+
normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
|
|
208
|
+
if normalized_source_screen_id is None:
|
|
209
|
+
return LateBoundActionCommand(kind="type", ref=ref, text=text)
|
|
210
|
+
return TypeCommandPayload(
|
|
211
|
+
kind="type",
|
|
212
|
+
ref=ref,
|
|
213
|
+
text=text,
|
|
214
|
+
source_screen_id=normalized_source_screen_id,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def build_scroll_command(
|
|
219
|
+
*,
|
|
220
|
+
ref: str,
|
|
221
|
+
direction: ScrollDirection,
|
|
222
|
+
source_screen_id: str | None,
|
|
223
|
+
) -> CliCommandPayload:
|
|
224
|
+
normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
|
|
225
|
+
if normalized_source_screen_id is None:
|
|
226
|
+
return LateBoundActionCommand(kind="scroll", ref=ref, direction=direction)
|
|
227
|
+
return ScrollCommandPayload(
|
|
228
|
+
kind="scroll",
|
|
229
|
+
ref=ref,
|
|
230
|
+
direction=direction,
|
|
231
|
+
source_screen_id=normalized_source_screen_id,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def build_wait_command(
|
|
236
|
+
*,
|
|
237
|
+
predicate: CliWaitPredicatePayload,
|
|
238
|
+
timeout_ms: int | None,
|
|
239
|
+
) -> CliCommandPayload:
|
|
240
|
+
validated_timeout_ms = _validate_timeout_ms(timeout_ms)
|
|
241
|
+
if isinstance(predicate, LateBoundScreenRelativePredicate):
|
|
242
|
+
return LateBoundWaitCommand(
|
|
243
|
+
predicate=predicate,
|
|
244
|
+
timeout_ms=validated_timeout_ms,
|
|
245
|
+
)
|
|
246
|
+
return WaitCommandPayload(
|
|
247
|
+
kind="wait",
|
|
248
|
+
predicate=predicate,
|
|
249
|
+
timeout_ms=validated_timeout_ms,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def build_global_action_command(
|
|
254
|
+
*,
|
|
255
|
+
kind: GlobalActionKind,
|
|
256
|
+
source_screen_id: str | None,
|
|
257
|
+
) -> CliCommandPayload:
|
|
258
|
+
normalized_source_screen_id = _normalize_source_screen_id(source_screen_id)
|
|
259
|
+
if normalized_source_screen_id is None:
|
|
260
|
+
return LateBoundGlobalActionCommand(kind=kind)
|
|
261
|
+
return GlobalActionCommandPayload(
|
|
262
|
+
kind=kind,
|
|
263
|
+
source_screen_id=normalized_source_screen_id,
|
|
264
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from androidctl_contracts.command_catalog import (
|
|
6
|
+
PUBLIC_COMMAND_NAMES,
|
|
7
|
+
entry_for_public_command,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class CommandView:
|
|
13
|
+
public_name: str
|
|
14
|
+
help_order: int
|
|
15
|
+
pre_dispatch_execution_outcome: str | None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _command_view(
|
|
19
|
+
public_name: str,
|
|
20
|
+
*,
|
|
21
|
+
help_order: int,
|
|
22
|
+
pre_dispatch_execution_outcome: str | None,
|
|
23
|
+
) -> CommandView:
|
|
24
|
+
if entry_for_public_command(public_name) is None:
|
|
25
|
+
raise RuntimeError(f"missing shared command catalog entry for {public_name!r}")
|
|
26
|
+
return CommandView(
|
|
27
|
+
public_name=public_name,
|
|
28
|
+
help_order=help_order,
|
|
29
|
+
pre_dispatch_execution_outcome=pre_dispatch_execution_outcome,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_COMMAND_VIEWS = (
|
|
34
|
+
_command_view(
|
|
35
|
+
public_name="observe",
|
|
36
|
+
help_order=0,
|
|
37
|
+
pre_dispatch_execution_outcome="notApplicable",
|
|
38
|
+
),
|
|
39
|
+
_command_view(
|
|
40
|
+
public_name="list-apps",
|
|
41
|
+
help_order=1,
|
|
42
|
+
pre_dispatch_execution_outcome="notApplicable",
|
|
43
|
+
),
|
|
44
|
+
_command_view(
|
|
45
|
+
public_name="open",
|
|
46
|
+
help_order=2,
|
|
47
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
48
|
+
),
|
|
49
|
+
_command_view(
|
|
50
|
+
public_name="tap",
|
|
51
|
+
help_order=3,
|
|
52
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
53
|
+
),
|
|
54
|
+
_command_view(
|
|
55
|
+
public_name="long-tap",
|
|
56
|
+
help_order=4,
|
|
57
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
58
|
+
),
|
|
59
|
+
_command_view(
|
|
60
|
+
public_name="focus",
|
|
61
|
+
help_order=5,
|
|
62
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
63
|
+
),
|
|
64
|
+
_command_view(
|
|
65
|
+
public_name="type",
|
|
66
|
+
help_order=6,
|
|
67
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
68
|
+
),
|
|
69
|
+
_command_view(
|
|
70
|
+
public_name="submit",
|
|
71
|
+
help_order=7,
|
|
72
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
73
|
+
),
|
|
74
|
+
_command_view(
|
|
75
|
+
public_name="scroll",
|
|
76
|
+
help_order=8,
|
|
77
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
78
|
+
),
|
|
79
|
+
_command_view(
|
|
80
|
+
public_name="back",
|
|
81
|
+
help_order=9,
|
|
82
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
83
|
+
),
|
|
84
|
+
_command_view(
|
|
85
|
+
public_name="home",
|
|
86
|
+
help_order=10,
|
|
87
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
88
|
+
),
|
|
89
|
+
_command_view(
|
|
90
|
+
public_name="recents",
|
|
91
|
+
help_order=11,
|
|
92
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
93
|
+
),
|
|
94
|
+
_command_view(
|
|
95
|
+
public_name="notifications",
|
|
96
|
+
help_order=12,
|
|
97
|
+
pre_dispatch_execution_outcome="notAttempted",
|
|
98
|
+
),
|
|
99
|
+
_command_view(
|
|
100
|
+
public_name="wait",
|
|
101
|
+
help_order=13,
|
|
102
|
+
pre_dispatch_execution_outcome="notApplicable",
|
|
103
|
+
),
|
|
104
|
+
_command_view(
|
|
105
|
+
public_name="connect",
|
|
106
|
+
help_order=14,
|
|
107
|
+
pre_dispatch_execution_outcome="notApplicable",
|
|
108
|
+
),
|
|
109
|
+
_command_view(
|
|
110
|
+
public_name="screenshot",
|
|
111
|
+
help_order=15,
|
|
112
|
+
pre_dispatch_execution_outcome="notApplicable",
|
|
113
|
+
),
|
|
114
|
+
_command_view(
|
|
115
|
+
public_name="close",
|
|
116
|
+
help_order=16,
|
|
117
|
+
pre_dispatch_execution_outcome="notApplicable",
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
_command_view_names = [view.public_name for view in _COMMAND_VIEWS]
|
|
122
|
+
_help_orders = [view.help_order for view in _COMMAND_VIEWS]
|
|
123
|
+
if len(_command_view_names) != len(set(_command_view_names)):
|
|
124
|
+
raise RuntimeError("duplicate public command in CLI command views")
|
|
125
|
+
if len(_help_orders) != len(set(_help_orders)):
|
|
126
|
+
raise RuntimeError("duplicate help order in CLI command views")
|
|
127
|
+
if set(_command_view_names) != PUBLIC_COMMAND_NAMES:
|
|
128
|
+
missing = sorted(PUBLIC_COMMAND_NAMES - set(_command_view_names))
|
|
129
|
+
extra = sorted(set(_command_view_names) - PUBLIC_COMMAND_NAMES)
|
|
130
|
+
raise RuntimeError(
|
|
131
|
+
"CLI command views drifted from shared public catalog: "
|
|
132
|
+
f"missing={missing}, extra={extra}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
_COMMAND_VIEW_BY_PUBLIC_NAME = {view.public_name: view for view in _COMMAND_VIEWS}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def command_view_for_public_command(public_name: str) -> CommandView | None:
|
|
139
|
+
return _COMMAND_VIEW_BY_PUBLIC_NAME.get(public_name)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def help_order_for_public_command(public_name: str) -> int:
|
|
143
|
+
view = command_view_for_public_command(public_name)
|
|
144
|
+
if view is None:
|
|
145
|
+
return len(_COMMAND_VIEWS)
|
|
146
|
+
return view.help_order
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def pre_dispatch_execution_outcome_for_public_command(
|
|
150
|
+
public_name: str | None,
|
|
151
|
+
) -> str | None:
|
|
152
|
+
if public_name is None:
|
|
153
|
+
return None
|
|
154
|
+
view = command_view_for_public_command(public_name)
|
|
155
|
+
if view is None:
|
|
156
|
+
return None
|
|
157
|
+
return view.pre_dispatch_execution_outcome
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command handlers."""
|