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,204 @@
|
|
|
1
|
+
"""Helpers for building device action requests and resolving ref targets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from androidctld.actions.submit_routing import SubmitRouteOutcome
|
|
6
|
+
from androidctld.commands.command_models import (
|
|
7
|
+
ActionCommand,
|
|
8
|
+
FocusCommand,
|
|
9
|
+
GlobalCommand,
|
|
10
|
+
LongTapCommand,
|
|
11
|
+
OpenCommand,
|
|
12
|
+
RefBoundActionCommand,
|
|
13
|
+
ScrollCommand,
|
|
14
|
+
SubmitCommand,
|
|
15
|
+
TapCommand,
|
|
16
|
+
TypeCommand,
|
|
17
|
+
is_ref_bound_action_command,
|
|
18
|
+
)
|
|
19
|
+
from androidctld.commands.open_targets import (
|
|
20
|
+
OpenAppTarget,
|
|
21
|
+
OpenUrlTarget,
|
|
22
|
+
validate_open_target,
|
|
23
|
+
)
|
|
24
|
+
from androidctld.device.action_models import (
|
|
25
|
+
BuiltDeviceActionRequest,
|
|
26
|
+
GlobalActionRequest,
|
|
27
|
+
HandleTarget,
|
|
28
|
+
LaunchAppActionRequest,
|
|
29
|
+
LongTapActionRequest,
|
|
30
|
+
NodeActionRequest,
|
|
31
|
+
NoneTarget,
|
|
32
|
+
OpenUrlActionRequest,
|
|
33
|
+
ScrollActionRequest,
|
|
34
|
+
TapActionRequest,
|
|
35
|
+
TypeActionRequest,
|
|
36
|
+
)
|
|
37
|
+
from androidctld.errors import DaemonError, DaemonErrorCode, bad_request
|
|
38
|
+
from androidctld.protocol import CommandKind
|
|
39
|
+
from androidctld.refs.models import NodeHandle
|
|
40
|
+
from androidctld.refs.repair import (
|
|
41
|
+
ref_repair_error,
|
|
42
|
+
resolve_ref_decision,
|
|
43
|
+
)
|
|
44
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
45
|
+
from androidctld.runtime.screen_state import (
|
|
46
|
+
current_artifacts,
|
|
47
|
+
current_public_screen,
|
|
48
|
+
)
|
|
49
|
+
from androidctld.runtime_policy import action_timeout_ms
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_action_request(
|
|
53
|
+
session: WorkspaceRuntime, command: ActionCommand
|
|
54
|
+
) -> BuiltDeviceActionRequest:
|
|
55
|
+
if isinstance(command, OpenCommand):
|
|
56
|
+
return build_open_action_request(command)
|
|
57
|
+
if isinstance(command, GlobalCommand):
|
|
58
|
+
return BuiltDeviceActionRequest(
|
|
59
|
+
payload=GlobalActionRequest(
|
|
60
|
+
target=NoneTarget(),
|
|
61
|
+
action=command.action,
|
|
62
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
if is_ref_bound_action_command(command):
|
|
66
|
+
handle = resolve_ref_target(session, command.ref, command.source_screen_id)
|
|
67
|
+
return build_action_request_for_binding(handle, command)
|
|
68
|
+
raise bad_request("unsupported action kind", {"kind": command.kind.value})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_action_request_for_binding(
|
|
72
|
+
handle: NodeHandle, command: RefBoundActionCommand
|
|
73
|
+
) -> BuiltDeviceActionRequest:
|
|
74
|
+
if isinstance(command, TapCommand):
|
|
75
|
+
return BuiltDeviceActionRequest(
|
|
76
|
+
payload=TapActionRequest(
|
|
77
|
+
target=HandleTarget(handle),
|
|
78
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
79
|
+
),
|
|
80
|
+
request_handle=handle,
|
|
81
|
+
)
|
|
82
|
+
if isinstance(command, LongTapCommand):
|
|
83
|
+
return BuiltDeviceActionRequest(
|
|
84
|
+
payload=LongTapActionRequest(
|
|
85
|
+
target=HandleTarget(handle),
|
|
86
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
87
|
+
),
|
|
88
|
+
request_handle=handle,
|
|
89
|
+
)
|
|
90
|
+
if isinstance(command, TypeCommand):
|
|
91
|
+
return BuiltDeviceActionRequest(
|
|
92
|
+
payload=TypeActionRequest(
|
|
93
|
+
target=HandleTarget(handle),
|
|
94
|
+
text=command.text,
|
|
95
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
96
|
+
),
|
|
97
|
+
request_handle=handle,
|
|
98
|
+
)
|
|
99
|
+
if isinstance(command, FocusCommand):
|
|
100
|
+
return BuiltDeviceActionRequest(
|
|
101
|
+
payload=NodeActionRequest(
|
|
102
|
+
target=HandleTarget(handle),
|
|
103
|
+
action=command.kind.value,
|
|
104
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
105
|
+
),
|
|
106
|
+
request_handle=handle,
|
|
107
|
+
)
|
|
108
|
+
if isinstance(command, SubmitCommand):
|
|
109
|
+
return BuiltDeviceActionRequest(
|
|
110
|
+
payload=NodeActionRequest(
|
|
111
|
+
target=HandleTarget(handle),
|
|
112
|
+
action=command.kind.value,
|
|
113
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
114
|
+
),
|
|
115
|
+
request_handle=handle,
|
|
116
|
+
)
|
|
117
|
+
if isinstance(command, ScrollCommand):
|
|
118
|
+
return BuiltDeviceActionRequest(
|
|
119
|
+
payload=ScrollActionRequest(
|
|
120
|
+
target=HandleTarget(handle),
|
|
121
|
+
direction=command.direction,
|
|
122
|
+
timeout_ms=action_timeout_ms(command.kind),
|
|
123
|
+
),
|
|
124
|
+
request_handle=handle,
|
|
125
|
+
)
|
|
126
|
+
raise bad_request("unsupported action kind", {"kind": command.kind.value})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_submit_action_request_for_route(
|
|
130
|
+
route: SubmitRouteOutcome,
|
|
131
|
+
) -> BuiltDeviceActionRequest:
|
|
132
|
+
if route.route == "direct":
|
|
133
|
+
return BuiltDeviceActionRequest(
|
|
134
|
+
payload=NodeActionRequest(
|
|
135
|
+
target=HandleTarget(route.subject_handle),
|
|
136
|
+
action=CommandKind.SUBMIT.value,
|
|
137
|
+
timeout_ms=action_timeout_ms(CommandKind.SUBMIT),
|
|
138
|
+
),
|
|
139
|
+
request_handle=route.subject_handle,
|
|
140
|
+
dispatched_handle=route.dispatched_handle,
|
|
141
|
+
submit_route=route.route,
|
|
142
|
+
)
|
|
143
|
+
return BuiltDeviceActionRequest(
|
|
144
|
+
payload=TapActionRequest(
|
|
145
|
+
target=HandleTarget(route.dispatched_handle),
|
|
146
|
+
timeout_ms=action_timeout_ms(CommandKind.SUBMIT),
|
|
147
|
+
),
|
|
148
|
+
request_handle=route.subject_handle,
|
|
149
|
+
dispatched_handle=route.dispatched_handle,
|
|
150
|
+
submit_route=route.route,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_open_action_request(command: ActionCommand) -> BuiltDeviceActionRequest:
|
|
155
|
+
if not isinstance(command, OpenCommand):
|
|
156
|
+
raise bad_request("open action request requires an open command")
|
|
157
|
+
target = validate_open_target(command.target)
|
|
158
|
+
if isinstance(target, OpenAppTarget):
|
|
159
|
+
return BuiltDeviceActionRequest(
|
|
160
|
+
payload=LaunchAppActionRequest(
|
|
161
|
+
target=NoneTarget(),
|
|
162
|
+
package_name=target.package_name,
|
|
163
|
+
timeout_ms=action_timeout_ms(CommandKind.OPEN),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
if isinstance(target, OpenUrlTarget):
|
|
167
|
+
return BuiltDeviceActionRequest(
|
|
168
|
+
payload=OpenUrlActionRequest(
|
|
169
|
+
target=NoneTarget(),
|
|
170
|
+
url=target.url,
|
|
171
|
+
timeout_ms=action_timeout_ms(CommandKind.OPEN),
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
raise bad_request("open requires target.kind app|url and target.value")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def resolve_ref_target(
|
|
178
|
+
session: WorkspaceRuntime,
|
|
179
|
+
ref: str,
|
|
180
|
+
source_screen_id: str,
|
|
181
|
+
) -> NodeHandle:
|
|
182
|
+
ensure_screen_ready(session)
|
|
183
|
+
decision = resolve_ref_decision(session, ref, source_screen_id)
|
|
184
|
+
if not decision.is_resolved:
|
|
185
|
+
raise ref_repair_error(
|
|
186
|
+
decision,
|
|
187
|
+
public_screen=current_public_screen(session),
|
|
188
|
+
artifacts=current_artifacts(session),
|
|
189
|
+
)
|
|
190
|
+
binding = decision.binding
|
|
191
|
+
if binding is None:
|
|
192
|
+
raise RuntimeError("resolved ref decision is missing its binding")
|
|
193
|
+
return binding.handle
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def ensure_screen_ready(session: WorkspaceRuntime) -> None:
|
|
197
|
+
if session.screen_state is None or session.latest_snapshot is None:
|
|
198
|
+
raise DaemonError(
|
|
199
|
+
code=DaemonErrorCode.SCREEN_NOT_READY,
|
|
200
|
+
message="screen is not ready yet",
|
|
201
|
+
retryable=False,
|
|
202
|
+
details={"workspaceRoot": session.workspace_root.as_posix()},
|
|
203
|
+
http_status=200,
|
|
204
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Action settle loop for post-mutation stabilization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from androidctld.device.interfaces import EventPollingClient
|
|
9
|
+
from androidctld.errors import DaemonError
|
|
10
|
+
from androidctld.observation import (
|
|
11
|
+
ObservationLoop,
|
|
12
|
+
ObservationPolicy,
|
|
13
|
+
ObservationPollOutcome,
|
|
14
|
+
)
|
|
15
|
+
from androidctld.protocol import CommandKind
|
|
16
|
+
from androidctld.runtime import RuntimeLifecycleLease
|
|
17
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
18
|
+
from androidctld.runtime_policy import (
|
|
19
|
+
DEVICE_RPC_REQUEST_ID_SETTLE,
|
|
20
|
+
SETTLE_POLL_SLICE_MS,
|
|
21
|
+
TRANSIENT_INVALID_SNAPSHOT_MAX_RETRIES,
|
|
22
|
+
settle_max_total_ms,
|
|
23
|
+
settle_min_grace_ms,
|
|
24
|
+
settle_snapshot_max_interval_ms,
|
|
25
|
+
settle_stable_window_ms,
|
|
26
|
+
)
|
|
27
|
+
from androidctld.semantics.compiler import SemanticCompiler
|
|
28
|
+
from androidctld.snapshots.models import RawSnapshot
|
|
29
|
+
from androidctld.snapshots.refresh import settle_screen_signature
|
|
30
|
+
from androidctld.snapshots.service import (
|
|
31
|
+
SnapshotService,
|
|
32
|
+
fetch_with_transient_invalid_snapshot_retry,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
SleepFn = Callable[[float], None]
|
|
36
|
+
TimeFn = Callable[[], float]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class SettledSnapshot:
|
|
41
|
+
snapshot: RawSnapshot
|
|
42
|
+
timed_out: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ActionSettler:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
snapshot_service: SnapshotService,
|
|
49
|
+
semantic_compiler: SemanticCompiler,
|
|
50
|
+
sleep_fn: SleepFn,
|
|
51
|
+
time_fn: TimeFn,
|
|
52
|
+
) -> None:
|
|
53
|
+
self._snapshot_service = snapshot_service
|
|
54
|
+
self._semantic_compiler = semantic_compiler
|
|
55
|
+
self._sleep_fn = sleep_fn
|
|
56
|
+
self._time_fn = time_fn
|
|
57
|
+
|
|
58
|
+
def settle(
|
|
59
|
+
self,
|
|
60
|
+
session: WorkspaceRuntime,
|
|
61
|
+
client: EventPollingClient,
|
|
62
|
+
kind: CommandKind,
|
|
63
|
+
baseline_signature: tuple[object, ...],
|
|
64
|
+
*,
|
|
65
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
66
|
+
) -> SettledSnapshot:
|
|
67
|
+
observation = ObservationLoop.begin(
|
|
68
|
+
ObservationPolicy(
|
|
69
|
+
min_grace_ms=settle_min_grace_ms(kind),
|
|
70
|
+
snapshot_max_interval_ms=settle_snapshot_max_interval_ms(kind),
|
|
71
|
+
stable_window_ms=settle_stable_window_ms(kind),
|
|
72
|
+
max_total_ms=settle_max_total_ms(kind),
|
|
73
|
+
poll_slice_ms=SETTLE_POLL_SLICE_MS,
|
|
74
|
+
),
|
|
75
|
+
started_at=self._time_fn(),
|
|
76
|
+
)
|
|
77
|
+
latest_snapshot: RawSnapshot | None = None
|
|
78
|
+
latest_signature = baseline_signature
|
|
79
|
+
while True:
|
|
80
|
+
now = self._time_fn()
|
|
81
|
+
poll_wait_ms = observation.poll_wait_ms(now)
|
|
82
|
+
poll_outcome = ObservationPollOutcome(
|
|
83
|
+
saw_events=False,
|
|
84
|
+
need_resync=False,
|
|
85
|
+
latest_seq=observation.after_seq,
|
|
86
|
+
)
|
|
87
|
+
if poll_wait_ms > 0:
|
|
88
|
+
polled_at = self._time_fn()
|
|
89
|
+
try:
|
|
90
|
+
poll_result = client.events_poll(
|
|
91
|
+
after_seq=observation.after_seq,
|
|
92
|
+
wait_ms=poll_wait_ms,
|
|
93
|
+
limit=1,
|
|
94
|
+
request_id=DEVICE_RPC_REQUEST_ID_SETTLE,
|
|
95
|
+
)
|
|
96
|
+
poll_outcome = observation.apply_poll_result(poll_result)
|
|
97
|
+
except DaemonError:
|
|
98
|
+
self._sleep_fn(poll_wait_ms / 1000.0)
|
|
99
|
+
else:
|
|
100
|
+
if not poll_outcome.saw_events and self._time_fn() <= polled_at:
|
|
101
|
+
self._sleep_fn(poll_wait_ms / 1000.0)
|
|
102
|
+
now = self._time_fn()
|
|
103
|
+
if not observation.should_refresh(
|
|
104
|
+
now,
|
|
105
|
+
saw_events=poll_outcome.saw_events,
|
|
106
|
+
need_resync=poll_outcome.need_resync,
|
|
107
|
+
):
|
|
108
|
+
if observation.timed_out(now) and latest_snapshot is not None:
|
|
109
|
+
return SettledSnapshot(
|
|
110
|
+
snapshot=latest_snapshot,
|
|
111
|
+
timed_out=True,
|
|
112
|
+
)
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
snapshot = fetch_with_transient_invalid_snapshot_retry(
|
|
116
|
+
self._snapshot_service,
|
|
117
|
+
session=session,
|
|
118
|
+
force_refresh=True,
|
|
119
|
+
lifecycle_lease=lifecycle_lease,
|
|
120
|
+
deadline_at=observation.deadline_at,
|
|
121
|
+
max_retries=TRANSIENT_INVALID_SNAPSHOT_MAX_RETRIES,
|
|
122
|
+
sleep_fn=self._sleep_fn,
|
|
123
|
+
time_fn=self._time_fn,
|
|
124
|
+
)
|
|
125
|
+
observation.mark_refreshed(self._time_fn())
|
|
126
|
+
compiled_screen = self._semantic_compiler.compile(
|
|
127
|
+
session.screen_sequence + 1,
|
|
128
|
+
snapshot,
|
|
129
|
+
)
|
|
130
|
+
signature = settle_screen_signature(compiled_screen, snapshot)
|
|
131
|
+
latest_snapshot = snapshot
|
|
132
|
+
is_stable = observation.observe_stability(
|
|
133
|
+
self._time_fn(),
|
|
134
|
+
changed=signature != latest_signature,
|
|
135
|
+
)
|
|
136
|
+
latest_signature = signature
|
|
137
|
+
if is_stable:
|
|
138
|
+
return SettledSnapshot(
|
|
139
|
+
snapshot=snapshot,
|
|
140
|
+
timed_out=False,
|
|
141
|
+
)
|
|
142
|
+
if observation.timed_out(self._time_fn()):
|
|
143
|
+
return SettledSnapshot(
|
|
144
|
+
snapshot=snapshot,
|
|
145
|
+
timed_out=True,
|
|
146
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Shared submit confirmation helpers for standalone submit commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from androidctld.actions.type_confirmation import (
|
|
10
|
+
TypeConfirmationContext,
|
|
11
|
+
fingerprint_rematch_confirmation_node,
|
|
12
|
+
reused_ref_confirmation_node,
|
|
13
|
+
snapshot_node_for_handle,
|
|
14
|
+
)
|
|
15
|
+
from androidctld.commands.command_models import SubmitCommand
|
|
16
|
+
from androidctld.device.types import ActionPerformResult, ActionStatus
|
|
17
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
18
|
+
from androidctld.refs.models import NodeHandle, RefBinding
|
|
19
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
20
|
+
from androidctld.semantics.public_models import (
|
|
21
|
+
PublicNode,
|
|
22
|
+
PublicScreen,
|
|
23
|
+
public_group_nodes,
|
|
24
|
+
)
|
|
25
|
+
from androidctld.snapshots.models import RawNode, RawSnapshot
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SubmitConfirmationContext:
|
|
30
|
+
ref: str | None
|
|
31
|
+
request_handle: NodeHandle | None
|
|
32
|
+
binding: RefBinding | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class SubmitConfirmationOutcome:
|
|
37
|
+
status: Literal["sameTarget", "targetGone", "publicChange", "unconfirmed"]
|
|
38
|
+
node: RawNode | None
|
|
39
|
+
target_handle: NodeHandle
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_submit_confirmation_context(
|
|
43
|
+
session: WorkspaceRuntime,
|
|
44
|
+
command: SubmitCommand,
|
|
45
|
+
request_handle: NodeHandle | None,
|
|
46
|
+
) -> SubmitConfirmationContext:
|
|
47
|
+
binding = session.ref_registry.get(command.ref)
|
|
48
|
+
return SubmitConfirmationContext(
|
|
49
|
+
ref=command.ref,
|
|
50
|
+
request_handle=request_handle,
|
|
51
|
+
binding=None if binding is None else deepcopy(binding),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def submit_confirmation_node(
|
|
56
|
+
*,
|
|
57
|
+
session: WorkspaceRuntime | None,
|
|
58
|
+
snapshot: RawSnapshot,
|
|
59
|
+
context: SubmitConfirmationContext | None,
|
|
60
|
+
command_target_handle: NodeHandle,
|
|
61
|
+
) -> RawNode | None:
|
|
62
|
+
direct_node = snapshot_node_for_handle(snapshot, command_target_handle)
|
|
63
|
+
if direct_node is not None:
|
|
64
|
+
return direct_node
|
|
65
|
+
if session is None or context is None:
|
|
66
|
+
return None
|
|
67
|
+
if (
|
|
68
|
+
context.request_handle is not None
|
|
69
|
+
and command_target_handle != context.request_handle
|
|
70
|
+
):
|
|
71
|
+
return None
|
|
72
|
+
type_context = TypeConfirmationContext(
|
|
73
|
+
ref=context.ref,
|
|
74
|
+
request_handle=context.request_handle,
|
|
75
|
+
binding=context.binding,
|
|
76
|
+
)
|
|
77
|
+
for node in (
|
|
78
|
+
reused_ref_confirmation_node(session, snapshot, type_context),
|
|
79
|
+
fingerprint_rematch_confirmation_node(session, snapshot, type_context),
|
|
80
|
+
):
|
|
81
|
+
if node is not None:
|
|
82
|
+
return node
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def submit_public_change_is_attributable(
|
|
87
|
+
previous_screen: PublicScreen | None,
|
|
88
|
+
public_screen: PublicScreen,
|
|
89
|
+
) -> bool:
|
|
90
|
+
if previous_screen is None:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def structural_groups(
|
|
94
|
+
screen: PublicScreen,
|
|
95
|
+
) -> tuple[tuple[tuple[object, ...], ...], ...]:
|
|
96
|
+
return (
|
|
97
|
+
_structural_node_signatures(public_group_nodes(screen, "targets")),
|
|
98
|
+
_structural_node_signatures(public_group_nodes(screen, "context")),
|
|
99
|
+
_structural_node_signatures(public_group_nodes(screen, "dialog")),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return structural_groups(previous_screen) != structural_groups(public_screen)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _structural_node_signatures(
|
|
106
|
+
nodes: tuple[PublicNode, ...],
|
|
107
|
+
) -> tuple[tuple[object, ...], ...]:
|
|
108
|
+
return tuple(_structural_node_signature(node) for node in nodes)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _structural_node_signature(node: PublicNode) -> tuple[object, ...]:
|
|
112
|
+
return (
|
|
113
|
+
node.kind,
|
|
114
|
+
node.role,
|
|
115
|
+
node.label,
|
|
116
|
+
node.text,
|
|
117
|
+
tuple(_structural_node_signature(child) for child in node.children),
|
|
118
|
+
tuple(node.scroll_directions),
|
|
119
|
+
(
|
|
120
|
+
None
|
|
121
|
+
if node.meta is None
|
|
122
|
+
else (
|
|
123
|
+
node.meta.resource_id,
|
|
124
|
+
node.meta.class_name,
|
|
125
|
+
)
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_submit_confirmation(
|
|
131
|
+
*,
|
|
132
|
+
session: WorkspaceRuntime | None = None,
|
|
133
|
+
route_kind: Literal["direct", "attributed"],
|
|
134
|
+
action_result: ActionPerformResult,
|
|
135
|
+
previous_snapshot: RawSnapshot | None,
|
|
136
|
+
snapshot: RawSnapshot,
|
|
137
|
+
previous_screen: PublicScreen | None,
|
|
138
|
+
public_screen: PublicScreen,
|
|
139
|
+
context: SubmitConfirmationContext | None = None,
|
|
140
|
+
command_target_handle: NodeHandle | None,
|
|
141
|
+
) -> SubmitConfirmationOutcome:
|
|
142
|
+
if command_target_handle is None:
|
|
143
|
+
raise DaemonError(
|
|
144
|
+
code=DaemonErrorCode.SUBMIT_NOT_CONFIRMED,
|
|
145
|
+
message="submit effect could not be confirmed",
|
|
146
|
+
retryable=True,
|
|
147
|
+
details={"reason": "missing_command_target_identity"},
|
|
148
|
+
http_status=200,
|
|
149
|
+
)
|
|
150
|
+
if action_result.status is not ActionStatus.DONE:
|
|
151
|
+
raise DaemonError(
|
|
152
|
+
code=DaemonErrorCode.SUBMIT_NOT_CONFIRMED,
|
|
153
|
+
message="submit effect could not be confirmed",
|
|
154
|
+
retryable=True,
|
|
155
|
+
details={
|
|
156
|
+
"reason": "device_submit_not_accepted",
|
|
157
|
+
"status": action_result.status.value,
|
|
158
|
+
},
|
|
159
|
+
http_status=200,
|
|
160
|
+
)
|
|
161
|
+
if previous_snapshot is not None and (
|
|
162
|
+
snapshot.package_name != previous_snapshot.package_name
|
|
163
|
+
or snapshot.activity_name != previous_snapshot.activity_name
|
|
164
|
+
):
|
|
165
|
+
return SubmitConfirmationOutcome(
|
|
166
|
+
status="unconfirmed",
|
|
167
|
+
node=None,
|
|
168
|
+
target_handle=command_target_handle,
|
|
169
|
+
)
|
|
170
|
+
confirmation_node = submit_confirmation_node(
|
|
171
|
+
session=session,
|
|
172
|
+
snapshot=snapshot,
|
|
173
|
+
context=context,
|
|
174
|
+
command_target_handle=command_target_handle,
|
|
175
|
+
)
|
|
176
|
+
if confirmation_node is None:
|
|
177
|
+
return SubmitConfirmationOutcome(
|
|
178
|
+
status="targetGone",
|
|
179
|
+
node=None,
|
|
180
|
+
target_handle=command_target_handle,
|
|
181
|
+
)
|
|
182
|
+
if submit_public_change_is_attributable(
|
|
183
|
+
previous_screen,
|
|
184
|
+
public_screen,
|
|
185
|
+
):
|
|
186
|
+
return SubmitConfirmationOutcome(
|
|
187
|
+
status="publicChange",
|
|
188
|
+
node=confirmation_node,
|
|
189
|
+
target_handle=command_target_handle,
|
|
190
|
+
)
|
|
191
|
+
if not confirmation_node.focused or not confirmation_node.editable:
|
|
192
|
+
if route_kind == "attributed":
|
|
193
|
+
raise DaemonError(
|
|
194
|
+
code=DaemonErrorCode.SUBMIT_NOT_CONFIRMED,
|
|
195
|
+
message="submit effect could not be confirmed",
|
|
196
|
+
retryable=True,
|
|
197
|
+
details={"reason": "attributed_submit_blur_only"},
|
|
198
|
+
http_status=200,
|
|
199
|
+
)
|
|
200
|
+
return SubmitConfirmationOutcome(
|
|
201
|
+
status="sameTarget",
|
|
202
|
+
node=confirmation_node,
|
|
203
|
+
target_handle=command_target_handle,
|
|
204
|
+
)
|
|
205
|
+
raise DaemonError(
|
|
206
|
+
code=DaemonErrorCode.SUBMIT_NOT_CONFIRMED,
|
|
207
|
+
message="submit effect could not be confirmed",
|
|
208
|
+
retryable=True,
|
|
209
|
+
details={"reason": "target_still_focused_editable"},
|
|
210
|
+
http_status=200,
|
|
211
|
+
)
|