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,305 @@
|
|
|
1
|
+
"""Shared wait-loop orchestration for canonical wait commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TypeAlias
|
|
8
|
+
|
|
9
|
+
from androidctld.commands.command_models import WaitCommand, wait_timeout_ms
|
|
10
|
+
from androidctld.commands.models import CommandRecord
|
|
11
|
+
from androidctld.device.interfaces import (
|
|
12
|
+
DeviceClientFactory,
|
|
13
|
+
EventPollingClient,
|
|
14
|
+
)
|
|
15
|
+
from androidctld.observation import (
|
|
16
|
+
ObservationLoop,
|
|
17
|
+
ObservationPolicy,
|
|
18
|
+
ObservationPollOutcome,
|
|
19
|
+
)
|
|
20
|
+
from androidctld.protocol import CommandKind
|
|
21
|
+
from androidctld.runtime import RuntimeLifecycleLease
|
|
22
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
23
|
+
from androidctld.runtime.screen_state import (
|
|
24
|
+
current_compiled_screen,
|
|
25
|
+
current_public_screen,
|
|
26
|
+
)
|
|
27
|
+
from androidctld.runtime_policy import (
|
|
28
|
+
DEVICE_RPC_REQUEST_ID_WAIT,
|
|
29
|
+
TRANSIENT_INVALID_SNAPSHOT_MAX_RETRIES,
|
|
30
|
+
WAIT_EVENT_POLL_SLICE_MS,
|
|
31
|
+
WAIT_IDLE_STABLE_WINDOW_MS,
|
|
32
|
+
WAIT_LOOP_SLEEP_SECONDS,
|
|
33
|
+
WAIT_SNAPSHOT_MAX_INTERVAL_MS,
|
|
34
|
+
default_wait_timeout_ms,
|
|
35
|
+
)
|
|
36
|
+
from androidctld.semantics.compiler import CompiledScreen
|
|
37
|
+
from androidctld.semantics.public_models import PublicScreen
|
|
38
|
+
from androidctld.snapshots.models import RawSnapshot
|
|
39
|
+
from androidctld.snapshots.refresh import ScreenRefreshService, raise_if_stale
|
|
40
|
+
from androidctld.snapshots.service import (
|
|
41
|
+
SnapshotService,
|
|
42
|
+
fetch_with_transient_invalid_snapshot_retry,
|
|
43
|
+
)
|
|
44
|
+
from androidctld.waits.evaluators import (
|
|
45
|
+
WaitEvaluationOutcome,
|
|
46
|
+
WaitIdleEvaluationState,
|
|
47
|
+
WaitIdleTracking,
|
|
48
|
+
WaitMatchData,
|
|
49
|
+
WaitMatched,
|
|
50
|
+
WaitReadyContext,
|
|
51
|
+
evaluate_ready_wait_match,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
SleepFn = Callable[[float], None]
|
|
55
|
+
TimeFn = Callable[[], float]
|
|
56
|
+
WaitReadyState: TypeAlias = WaitReadyContext
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class WaitLoopTimedOut:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
WaitLoopOutcome: TypeAlias = WaitMatchData | WaitLoopTimedOut
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _wait_state(
|
|
68
|
+
session: WorkspaceRuntime,
|
|
69
|
+
) -> tuple[
|
|
70
|
+
RawSnapshot | None,
|
|
71
|
+
PublicScreen | None,
|
|
72
|
+
CompiledScreen | None,
|
|
73
|
+
]:
|
|
74
|
+
return (
|
|
75
|
+
session.latest_snapshot,
|
|
76
|
+
current_public_screen(session),
|
|
77
|
+
current_compiled_screen(session),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ready_wait_state(
|
|
82
|
+
snapshot: RawSnapshot | None,
|
|
83
|
+
public_screen: PublicScreen | None,
|
|
84
|
+
compiled_screen: CompiledScreen | None,
|
|
85
|
+
) -> WaitReadyState | None:
|
|
86
|
+
if snapshot is None or public_screen is None:
|
|
87
|
+
return None
|
|
88
|
+
return WaitReadyContext(
|
|
89
|
+
snapshot=snapshot,
|
|
90
|
+
public_screen=public_screen,
|
|
91
|
+
compiled_screen=compiled_screen,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _cached_ready_wait_state(
|
|
96
|
+
session: WorkspaceRuntime,
|
|
97
|
+
*,
|
|
98
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
99
|
+
command: WaitCommand,
|
|
100
|
+
kind: CommandKind,
|
|
101
|
+
record: CommandRecord,
|
|
102
|
+
) -> WaitReadyState | None:
|
|
103
|
+
with session.lock:
|
|
104
|
+
ready_state = _ready_wait_state(*_wait_state(session))
|
|
105
|
+
if ready_state is None:
|
|
106
|
+
return None
|
|
107
|
+
raise_if_stale(
|
|
108
|
+
session,
|
|
109
|
+
lifecycle_lease,
|
|
110
|
+
kind=kind,
|
|
111
|
+
wait_kind=command.wait_kind,
|
|
112
|
+
record=record,
|
|
113
|
+
)
|
|
114
|
+
return ready_state
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class WaitRuntimeLoop:
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
snapshot_service: SnapshotService,
|
|
122
|
+
screen_refresh: ScreenRefreshService,
|
|
123
|
+
device_client_factory: DeviceClientFactory,
|
|
124
|
+
sleep_fn: SleepFn,
|
|
125
|
+
time_fn: TimeFn,
|
|
126
|
+
) -> None:
|
|
127
|
+
self._snapshot_service = snapshot_service
|
|
128
|
+
self._screen_refresh = screen_refresh
|
|
129
|
+
self._device_client_factory = device_client_factory
|
|
130
|
+
self._sleep_fn = sleep_fn
|
|
131
|
+
self._time_fn = time_fn
|
|
132
|
+
|
|
133
|
+
def run(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
session: WorkspaceRuntime,
|
|
137
|
+
record: CommandRecord,
|
|
138
|
+
command: WaitCommand,
|
|
139
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
140
|
+
) -> WaitLoopOutcome:
|
|
141
|
+
kind = command.kind
|
|
142
|
+
timeout_override = wait_timeout_ms(command)
|
|
143
|
+
timeout_ms = (
|
|
144
|
+
timeout_override
|
|
145
|
+
if timeout_override is not None
|
|
146
|
+
else default_wait_timeout_ms(command.wait_kind)
|
|
147
|
+
)
|
|
148
|
+
observation = ObservationLoop.begin(
|
|
149
|
+
ObservationPolicy(
|
|
150
|
+
min_grace_ms=0,
|
|
151
|
+
snapshot_max_interval_ms=WAIT_SNAPSHOT_MAX_INTERVAL_MS,
|
|
152
|
+
stable_window_ms=WAIT_IDLE_STABLE_WINDOW_MS,
|
|
153
|
+
max_total_ms=timeout_ms,
|
|
154
|
+
poll_slice_ms=WAIT_EVENT_POLL_SLICE_MS,
|
|
155
|
+
),
|
|
156
|
+
started_at=self._time_fn(),
|
|
157
|
+
)
|
|
158
|
+
idle_state = WaitIdleEvaluationState()
|
|
159
|
+
|
|
160
|
+
initial_state = _cached_ready_wait_state(
|
|
161
|
+
session,
|
|
162
|
+
lifecycle_lease=lifecycle_lease,
|
|
163
|
+
command=command,
|
|
164
|
+
kind=kind,
|
|
165
|
+
record=record,
|
|
166
|
+
)
|
|
167
|
+
if initial_state is not None:
|
|
168
|
+
outcome = self._evaluate_ready_state(
|
|
169
|
+
command=command,
|
|
170
|
+
ready_state=initial_state,
|
|
171
|
+
idle_state=idle_state,
|
|
172
|
+
now=observation.started_at,
|
|
173
|
+
)
|
|
174
|
+
if isinstance(outcome, WaitMatched):
|
|
175
|
+
return outcome.match
|
|
176
|
+
if isinstance(outcome, WaitIdleTracking):
|
|
177
|
+
idle_state = outcome.idle_state
|
|
178
|
+
|
|
179
|
+
client = self._device_client_factory(
|
|
180
|
+
session,
|
|
181
|
+
lifecycle_lease=lifecycle_lease,
|
|
182
|
+
)
|
|
183
|
+
while True:
|
|
184
|
+
poll_outcome = self._poll(observation, client)
|
|
185
|
+
|
|
186
|
+
current_state = self._current_ready_state(
|
|
187
|
+
session=session,
|
|
188
|
+
observation=observation,
|
|
189
|
+
poll_outcome=poll_outcome,
|
|
190
|
+
lifecycle_lease=lifecycle_lease,
|
|
191
|
+
command=command,
|
|
192
|
+
kind=kind,
|
|
193
|
+
record=record,
|
|
194
|
+
)
|
|
195
|
+
if current_state is None:
|
|
196
|
+
if observation.timed_out(self._time_fn()):
|
|
197
|
+
return WaitLoopTimedOut()
|
|
198
|
+
self._sleep_fn(WAIT_LOOP_SLEEP_SECONDS)
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
outcome = self._evaluate_ready_state(
|
|
202
|
+
command=command,
|
|
203
|
+
ready_state=current_state,
|
|
204
|
+
idle_state=idle_state,
|
|
205
|
+
now=self._time_fn(),
|
|
206
|
+
)
|
|
207
|
+
if isinstance(outcome, WaitMatched):
|
|
208
|
+
return outcome.match
|
|
209
|
+
if isinstance(outcome, WaitIdleTracking):
|
|
210
|
+
idle_state = outcome.idle_state
|
|
211
|
+
if observation.timed_out(self._time_fn()):
|
|
212
|
+
return WaitLoopTimedOut()
|
|
213
|
+
self._sleep_fn(WAIT_LOOP_SLEEP_SECONDS)
|
|
214
|
+
|
|
215
|
+
def _poll(
|
|
216
|
+
self,
|
|
217
|
+
observation: ObservationLoop,
|
|
218
|
+
client: EventPollingClient,
|
|
219
|
+
) -> ObservationPollOutcome:
|
|
220
|
+
poll_wait_ms = observation.poll_wait_ms(self._time_fn())
|
|
221
|
+
poll_outcome = ObservationPollOutcome(
|
|
222
|
+
saw_events=False,
|
|
223
|
+
need_resync=False,
|
|
224
|
+
latest_seq=observation.after_seq,
|
|
225
|
+
)
|
|
226
|
+
if poll_wait_ms > 0:
|
|
227
|
+
polled_at = self._time_fn()
|
|
228
|
+
poll_result = client.events_poll(
|
|
229
|
+
after_seq=observation.after_seq,
|
|
230
|
+
wait_ms=poll_wait_ms,
|
|
231
|
+
limit=1,
|
|
232
|
+
request_id=DEVICE_RPC_REQUEST_ID_WAIT,
|
|
233
|
+
)
|
|
234
|
+
poll_outcome = observation.apply_poll_result(poll_result)
|
|
235
|
+
if not poll_outcome.saw_events and self._time_fn() <= polled_at:
|
|
236
|
+
self._sleep_fn(poll_wait_ms / 1000.0)
|
|
237
|
+
return poll_outcome
|
|
238
|
+
|
|
239
|
+
def _current_ready_state(
|
|
240
|
+
self,
|
|
241
|
+
*,
|
|
242
|
+
session: WorkspaceRuntime,
|
|
243
|
+
observation: ObservationLoop,
|
|
244
|
+
poll_outcome: ObservationPollOutcome,
|
|
245
|
+
lifecycle_lease: RuntimeLifecycleLease,
|
|
246
|
+
command: WaitCommand,
|
|
247
|
+
kind: CommandKind,
|
|
248
|
+
record: CommandRecord,
|
|
249
|
+
) -> WaitReadyState | None:
|
|
250
|
+
if observation.should_refresh(
|
|
251
|
+
self._time_fn(),
|
|
252
|
+
saw_events=poll_outcome.saw_events,
|
|
253
|
+
need_resync=poll_outcome.need_resync,
|
|
254
|
+
):
|
|
255
|
+
snapshot = fetch_with_transient_invalid_snapshot_retry(
|
|
256
|
+
self._snapshot_service,
|
|
257
|
+
session=session,
|
|
258
|
+
force_refresh=True,
|
|
259
|
+
lifecycle_lease=lifecycle_lease,
|
|
260
|
+
deadline_at=observation.deadline_at,
|
|
261
|
+
max_retries=TRANSIENT_INVALID_SNAPSHOT_MAX_RETRIES,
|
|
262
|
+
sleep_fn=self._sleep_fn,
|
|
263
|
+
time_fn=self._time_fn,
|
|
264
|
+
)
|
|
265
|
+
observation.mark_refreshed(self._time_fn())
|
|
266
|
+
self._screen_refresh.refresh(
|
|
267
|
+
session,
|
|
268
|
+
snapshot,
|
|
269
|
+
lifecycle_lease=lifecycle_lease,
|
|
270
|
+
command_kind=kind,
|
|
271
|
+
wait_kind=command.wait_kind,
|
|
272
|
+
record=record,
|
|
273
|
+
)
|
|
274
|
+
return _cached_ready_wait_state(
|
|
275
|
+
session,
|
|
276
|
+
lifecycle_lease=lifecycle_lease,
|
|
277
|
+
command=command,
|
|
278
|
+
kind=kind,
|
|
279
|
+
record=record,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _evaluate_ready_state(
|
|
283
|
+
self,
|
|
284
|
+
*,
|
|
285
|
+
command: WaitCommand,
|
|
286
|
+
ready_state: WaitReadyState,
|
|
287
|
+
idle_state: WaitIdleEvaluationState,
|
|
288
|
+
now: float,
|
|
289
|
+
) -> WaitEvaluationOutcome:
|
|
290
|
+
return evaluate_ready_wait_match(
|
|
291
|
+
command=command,
|
|
292
|
+
ready=ready_state,
|
|
293
|
+
idle_state=idle_state,
|
|
294
|
+
now=now,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
__all__ = [
|
|
299
|
+
"SleepFn",
|
|
300
|
+
"TimeFn",
|
|
301
|
+
"WaitLoopOutcome",
|
|
302
|
+
"WaitLoopTimedOut",
|
|
303
|
+
"WaitReadyState",
|
|
304
|
+
"WaitRuntimeLoop",
|
|
305
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Text matching helpers for wait commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from androidctld.snapshots.models import RawSnapshot
|
|
8
|
+
from androidctld.text_equivalence import canonical_text_key, searchable_raw_node_texts
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def matches_text(
|
|
12
|
+
snapshot: RawSnapshot,
|
|
13
|
+
query: str,
|
|
14
|
+
) -> bool:
|
|
15
|
+
normalized_query = normalize_wait_match_text(query)
|
|
16
|
+
if not normalized_query:
|
|
17
|
+
return False
|
|
18
|
+
seen_candidates: set[str] = set()
|
|
19
|
+
for value in text_candidates(snapshot):
|
|
20
|
+
candidate = normalize_wait_match_text(value)
|
|
21
|
+
if not candidate or candidate in seen_candidates:
|
|
22
|
+
continue
|
|
23
|
+
seen_candidates.add(candidate)
|
|
24
|
+
if normalized_query in candidate:
|
|
25
|
+
return True
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize_wait_match_text(value: Any) -> str:
|
|
30
|
+
return canonical_text_key(value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def text_candidates(
|
|
34
|
+
snapshot: RawSnapshot,
|
|
35
|
+
) -> list[str]:
|
|
36
|
+
candidates: list[str] = []
|
|
37
|
+
for raw_node in snapshot.nodes:
|
|
38
|
+
if not raw_node.visible_to_user:
|
|
39
|
+
continue
|
|
40
|
+
candidates.extend(searchable_raw_node_texts(raw_node))
|
|
41
|
+
return candidates
|