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,610 @@
|
|
|
1
|
+
"""Semantic compiler for screen-first flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from androidctld.semantics.labels import (
|
|
9
|
+
GENERIC_SEMANTIC_ROLES,
|
|
10
|
+
LABEL_QUALITY_CONTENT_DESC,
|
|
11
|
+
LABEL_QUALITY_HINT,
|
|
12
|
+
LABEL_QUALITY_NEARBY,
|
|
13
|
+
LABEL_QUALITY_RESOURCE_ID,
|
|
14
|
+
LABEL_QUALITY_STATE_DESCRIPTION,
|
|
15
|
+
LABEL_QUALITY_TEXT,
|
|
16
|
+
LabelInfo,
|
|
17
|
+
extract_state,
|
|
18
|
+
fallback_label,
|
|
19
|
+
infer_role,
|
|
20
|
+
label_from_state_description,
|
|
21
|
+
near_label_for_node,
|
|
22
|
+
normalize_text,
|
|
23
|
+
parent_node_for,
|
|
24
|
+
relative_bounds_for_node,
|
|
25
|
+
semantic_role,
|
|
26
|
+
sibling_labels_for_node,
|
|
27
|
+
state_description_parts,
|
|
28
|
+
states_from_state_description,
|
|
29
|
+
synthesize_label_info,
|
|
30
|
+
)
|
|
31
|
+
from androidctld.semantics.models import RelationScopeNode, SemanticMeta
|
|
32
|
+
from androidctld.semantics.public_models import (
|
|
33
|
+
BlockingGroupName,
|
|
34
|
+
PublicApp,
|
|
35
|
+
PublicFocus,
|
|
36
|
+
PublicGroupName,
|
|
37
|
+
PublicNode,
|
|
38
|
+
PublicScreen,
|
|
39
|
+
PublicSurface,
|
|
40
|
+
build_public_groups,
|
|
41
|
+
)
|
|
42
|
+
from androidctld.semantics.registries import GROUP_ORDER
|
|
43
|
+
from androidctld.semantics.submit_refs import assign_submit_ref_relations
|
|
44
|
+
from androidctld.semantics.surface import (
|
|
45
|
+
apply_blocking_policy,
|
|
46
|
+
build_action_surface_fingerprint,
|
|
47
|
+
context_sort_key,
|
|
48
|
+
infer_group,
|
|
49
|
+
node_to_public_node,
|
|
50
|
+
passive_node_dedup_key,
|
|
51
|
+
resolve_blocking_group,
|
|
52
|
+
score_node,
|
|
53
|
+
scroll_directions_for_raw_node,
|
|
54
|
+
semantic_node_fingerprint,
|
|
55
|
+
semantic_relation_identity,
|
|
56
|
+
semantic_relation_key,
|
|
57
|
+
stable_screen_id,
|
|
58
|
+
target_sort_key,
|
|
59
|
+
)
|
|
60
|
+
from androidctld.semantics.targets import (
|
|
61
|
+
CLICK_ACTIONS,
|
|
62
|
+
LONG_CLICK_ACTIONS,
|
|
63
|
+
SCROLL_ACTIONS,
|
|
64
|
+
TYPE_ACTIONS,
|
|
65
|
+
actionable_anchor_for,
|
|
66
|
+
ancestor_distance,
|
|
67
|
+
can_promote_to_actionable_ancestor,
|
|
68
|
+
normalize_action_name,
|
|
69
|
+
public_primary_actions_for,
|
|
70
|
+
secondary_public_actions_for,
|
|
71
|
+
select_target_sources,
|
|
72
|
+
semantic_actions_for,
|
|
73
|
+
target_source_sort_key,
|
|
74
|
+
)
|
|
75
|
+
from androidctld.snapshots.models import RawNode, RawSnapshot
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class SemanticNode:
|
|
80
|
+
raw_rid: str
|
|
81
|
+
role: str
|
|
82
|
+
label: str
|
|
83
|
+
state: list[str]
|
|
84
|
+
actions: list[str]
|
|
85
|
+
bounds: tuple[int, int, int, int]
|
|
86
|
+
meta: SemanticMeta
|
|
87
|
+
targetable: bool
|
|
88
|
+
score: int
|
|
89
|
+
group: str
|
|
90
|
+
parent_role: str
|
|
91
|
+
parent_label: str
|
|
92
|
+
sibling_labels: list[str]
|
|
93
|
+
relative_bounds: tuple[int, int, int, int]
|
|
94
|
+
scroll_directions: list[str] = field(default_factory=list)
|
|
95
|
+
text_children: list[str] = field(default_factory=list)
|
|
96
|
+
submit_target_keys: list[tuple[str, str]] = field(default_factory=list)
|
|
97
|
+
submit_relation_tokens: list[str] = field(default_factory=list)
|
|
98
|
+
label_quality: int = 0
|
|
99
|
+
ref: str = ""
|
|
100
|
+
relation_anchor_rid: str = ""
|
|
101
|
+
relation_window_id: str = ""
|
|
102
|
+
relation_window_root_rid: str | None = None
|
|
103
|
+
relation_parent_rid: str | None = None
|
|
104
|
+
relation_ancestor_scopes: tuple[RelationScopeNode, ...] = field(
|
|
105
|
+
default_factory=tuple
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def resource_id(self) -> str:
|
|
110
|
+
return self.meta.resource_id or ""
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def class_name(self) -> str:
|
|
114
|
+
return self.meta.class_name
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class CompiledScreen:
|
|
119
|
+
screen_id: str
|
|
120
|
+
sequence: int
|
|
121
|
+
source_snapshot_id: int
|
|
122
|
+
captured_at: str
|
|
123
|
+
package_name: str | None
|
|
124
|
+
activity_name: str | None
|
|
125
|
+
keyboard_visible: bool
|
|
126
|
+
action_surface_fingerprint: str = ""
|
|
127
|
+
blocking_group: BlockingGroupName | None = None
|
|
128
|
+
targets: list[SemanticNode] = field(default_factory=list)
|
|
129
|
+
context: list[SemanticNode] = field(default_factory=list)
|
|
130
|
+
dialog: list[SemanticNode] = field(default_factory=list)
|
|
131
|
+
keyboard: list[SemanticNode] = field(default_factory=list)
|
|
132
|
+
system: list[SemanticNode] = field(default_factory=list)
|
|
133
|
+
|
|
134
|
+
def focused_input_node(self) -> SemanticNode | None:
|
|
135
|
+
for group_name in cast(
|
|
136
|
+
tuple[PublicGroupName, ...],
|
|
137
|
+
("targets", "dialog", "keyboard", "system", "context"),
|
|
138
|
+
):
|
|
139
|
+
nodes = cast(list[SemanticNode], getattr(self, group_name))
|
|
140
|
+
for node in nodes:
|
|
141
|
+
if node.role != "input":
|
|
142
|
+
continue
|
|
143
|
+
if "focused" not in node.state:
|
|
144
|
+
continue
|
|
145
|
+
return node
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def ref_candidates(self) -> list[SemanticNode]:
|
|
149
|
+
candidate_groups: tuple[PublicGroupName, ...] = (
|
|
150
|
+
(self.blocking_group,)
|
|
151
|
+
if self.blocking_group is not None
|
|
152
|
+
else cast(
|
|
153
|
+
tuple[PublicGroupName, ...],
|
|
154
|
+
("targets", "dialog", "keyboard", "system"),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
candidates: list[SemanticNode] = []
|
|
158
|
+
seen_candidates: set[tuple[str, str]] = set()
|
|
159
|
+
for group_name in candidate_groups:
|
|
160
|
+
nodes = cast(list[SemanticNode], getattr(self, group_name))
|
|
161
|
+
for node in nodes:
|
|
162
|
+
candidate_key = (node.group, node.raw_rid)
|
|
163
|
+
if not node.actions or candidate_key in seen_candidates:
|
|
164
|
+
continue
|
|
165
|
+
candidates.append(node)
|
|
166
|
+
seen_candidates.add(candidate_key)
|
|
167
|
+
focused_input = self.focused_input_node()
|
|
168
|
+
if focused_input is not None:
|
|
169
|
+
candidate_key = (focused_input.group, focused_input.raw_rid)
|
|
170
|
+
if candidate_key not in seen_candidates:
|
|
171
|
+
candidates.append(focused_input)
|
|
172
|
+
return candidates
|
|
173
|
+
|
|
174
|
+
def group_order(self) -> tuple[PublicGroupName, ...]:
|
|
175
|
+
if self.blocking_group is None:
|
|
176
|
+
return cast(tuple[PublicGroupName, ...], GROUP_ORDER)
|
|
177
|
+
return (
|
|
178
|
+
self.blocking_group,
|
|
179
|
+
*(
|
|
180
|
+
cast(PublicGroupName, name)
|
|
181
|
+
for name in GROUP_ORDER
|
|
182
|
+
if name != self.blocking_group
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def focused_input_ref(self) -> str | None:
|
|
187
|
+
focused_input = self.focused_input_node()
|
|
188
|
+
if focused_input is None or not focused_input.ref:
|
|
189
|
+
return None
|
|
190
|
+
return focused_input.ref
|
|
191
|
+
|
|
192
|
+
def to_public_screen(self) -> PublicScreen:
|
|
193
|
+
submit_refs_by_node_id = self._submit_refs_by_node_id()
|
|
194
|
+
return PublicScreen(
|
|
195
|
+
screen_id=self.screen_id,
|
|
196
|
+
app=PublicApp(
|
|
197
|
+
package_name=self.package_name,
|
|
198
|
+
activity_name=self.activity_name,
|
|
199
|
+
),
|
|
200
|
+
surface=PublicSurface(
|
|
201
|
+
keyboard_visible=self.keyboard_visible,
|
|
202
|
+
blocking_group=self.blocking_group,
|
|
203
|
+
focus=PublicFocus(input_ref=self.focused_input_ref()),
|
|
204
|
+
),
|
|
205
|
+
groups=build_public_groups(
|
|
206
|
+
order=self.group_order(),
|
|
207
|
+
targets=self._public_nodes_for(self.targets, submit_refs_by_node_id),
|
|
208
|
+
keyboard=self._public_nodes_for(self.keyboard, submit_refs_by_node_id),
|
|
209
|
+
system=self._public_nodes_for(self.system, submit_refs_by_node_id),
|
|
210
|
+
context=self._public_nodes_for(self.context, submit_refs_by_node_id),
|
|
211
|
+
dialog=self._public_nodes_for(self.dialog, submit_refs_by_node_id),
|
|
212
|
+
),
|
|
213
|
+
omitted=(),
|
|
214
|
+
visible_windows=(),
|
|
215
|
+
transient=(),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _public_nodes_for(
|
|
219
|
+
self,
|
|
220
|
+
nodes: list[SemanticNode],
|
|
221
|
+
submit_refs_by_node_id: dict[int, tuple[str, ...]],
|
|
222
|
+
) -> tuple[PublicNode, ...]:
|
|
223
|
+
return tuple(
|
|
224
|
+
node_to_public_node(
|
|
225
|
+
node,
|
|
226
|
+
submit_refs=submit_refs_by_node_id.get(id(node), ()),
|
|
227
|
+
)
|
|
228
|
+
for node in nodes
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _submit_refs_by_node_id(self) -> dict[int, tuple[str, ...]]:
|
|
232
|
+
nodes = [
|
|
233
|
+
node
|
|
234
|
+
for group_name in self.group_order()
|
|
235
|
+
for node in cast(list[SemanticNode], getattr(self, group_name))
|
|
236
|
+
]
|
|
237
|
+
nodes_by_key: dict[tuple[str, str], list[SemanticNode]] = {}
|
|
238
|
+
for node in nodes:
|
|
239
|
+
for node_key in _submit_relation_lookup_keys(node.group, node):
|
|
240
|
+
nodes_by_key.setdefault(node_key, []).append(node)
|
|
241
|
+
|
|
242
|
+
resolved: dict[int, tuple[str, ...]] = {}
|
|
243
|
+
for node in nodes:
|
|
244
|
+
if node.role != "input" or not node.ref or not node.submit_target_keys:
|
|
245
|
+
continue
|
|
246
|
+
target_refs: list[str] = []
|
|
247
|
+
complete = True
|
|
248
|
+
for target_key in node.submit_target_keys:
|
|
249
|
+
matches = nodes_by_key.get(target_key, [])
|
|
250
|
+
if len(matches) != 1 or not matches[0].ref:
|
|
251
|
+
complete = False
|
|
252
|
+
break
|
|
253
|
+
target_refs.append(matches[0].ref)
|
|
254
|
+
if complete:
|
|
255
|
+
resolved[id(node)] = tuple(target_refs)
|
|
256
|
+
return resolved
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class SemanticCompiler:
|
|
260
|
+
def compile(self, sequence: int, snapshot: RawSnapshot) -> CompiledScreen:
|
|
261
|
+
ime_window_id = snapshot.ime.window_id if snapshot.ime.visible else None
|
|
262
|
+
raw_nodes_by_rid = {node.rid: node for node in snapshot.nodes}
|
|
263
|
+
root_rid_by_window = {
|
|
264
|
+
window.window_id: window.root_rid for window in snapshot.windows
|
|
265
|
+
}
|
|
266
|
+
label_infos = {
|
|
267
|
+
node.rid: synthesize_label_info(node, raw_nodes_by_rid)
|
|
268
|
+
for node in snapshot.nodes
|
|
269
|
+
}
|
|
270
|
+
actionability = {
|
|
271
|
+
node.rid: semantic_actions_for(node) for node in snapshot.nodes
|
|
272
|
+
}
|
|
273
|
+
target_sources = select_target_sources(
|
|
274
|
+
snapshot.nodes,
|
|
275
|
+
raw_nodes_by_rid,
|
|
276
|
+
label_infos,
|
|
277
|
+
actionability,
|
|
278
|
+
)
|
|
279
|
+
promoted_source_rids = {
|
|
280
|
+
source_rid
|
|
281
|
+
for anchor_rid, source_rid in target_sources.items()
|
|
282
|
+
if source_rid != anchor_rid
|
|
283
|
+
}
|
|
284
|
+
semantic_nodes = []
|
|
285
|
+
seen_passive_nodes: set[tuple[str, str, str, str, str]] = set()
|
|
286
|
+
for raw_node in snapshot.nodes:
|
|
287
|
+
if raw_node.rid in target_sources or raw_node.rid in promoted_source_rids:
|
|
288
|
+
continue
|
|
289
|
+
semantic_node = compile_node(
|
|
290
|
+
raw_node,
|
|
291
|
+
raw_nodes_by_rid,
|
|
292
|
+
label_infos,
|
|
293
|
+
actionability,
|
|
294
|
+
ime_window_id=ime_window_id,
|
|
295
|
+
root_rid_by_window=root_rid_by_window,
|
|
296
|
+
)
|
|
297
|
+
if semantic_node is not None:
|
|
298
|
+
duplicate_key = passive_node_dedup_key(semantic_node)
|
|
299
|
+
if duplicate_key is not None:
|
|
300
|
+
if duplicate_key in seen_passive_nodes:
|
|
301
|
+
continue
|
|
302
|
+
seen_passive_nodes.add(duplicate_key)
|
|
303
|
+
semantic_nodes.append(semantic_node)
|
|
304
|
+
for anchor_rid, source_rid in target_sources.items():
|
|
305
|
+
semantic_node = compile_node(
|
|
306
|
+
raw_nodes_by_rid[source_rid],
|
|
307
|
+
raw_nodes_by_rid,
|
|
308
|
+
label_infos,
|
|
309
|
+
actionability,
|
|
310
|
+
ime_window_id=ime_window_id,
|
|
311
|
+
root_rid_by_window=root_rid_by_window,
|
|
312
|
+
action_node=raw_nodes_by_rid[anchor_rid],
|
|
313
|
+
)
|
|
314
|
+
if semantic_node is not None:
|
|
315
|
+
semantic_nodes.append(semantic_node)
|
|
316
|
+
|
|
317
|
+
grouped_nodes = {
|
|
318
|
+
"targets": sorted(
|
|
319
|
+
[node for node in semantic_nodes if node.group == "targets"],
|
|
320
|
+
key=target_sort_key,
|
|
321
|
+
),
|
|
322
|
+
"keyboard": sorted(
|
|
323
|
+
[node for node in semantic_nodes if node.group == "keyboard"],
|
|
324
|
+
key=target_sort_key,
|
|
325
|
+
),
|
|
326
|
+
"system": sorted(
|
|
327
|
+
[node for node in semantic_nodes if node.group == "system"],
|
|
328
|
+
key=target_sort_key,
|
|
329
|
+
),
|
|
330
|
+
"context": sorted(
|
|
331
|
+
[node for node in semantic_nodes if node.group == "context"],
|
|
332
|
+
key=context_sort_key,
|
|
333
|
+
),
|
|
334
|
+
"dialog": sorted(
|
|
335
|
+
[node for node in semantic_nodes if node.group == "dialog"],
|
|
336
|
+
key=target_sort_key,
|
|
337
|
+
),
|
|
338
|
+
}
|
|
339
|
+
blocking_group = resolve_blocking_group(
|
|
340
|
+
snapshot=snapshot,
|
|
341
|
+
grouped_nodes=grouped_nodes,
|
|
342
|
+
)
|
|
343
|
+
apply_blocking_policy(grouped_nodes, blocking_group)
|
|
344
|
+
assign_submit_ref_relations(
|
|
345
|
+
grouped_nodes=grouped_nodes,
|
|
346
|
+
blocking_group=blocking_group,
|
|
347
|
+
)
|
|
348
|
+
action_surface_fingerprint = build_action_surface_fingerprint(
|
|
349
|
+
snapshot=snapshot,
|
|
350
|
+
grouped_nodes=grouped_nodes,
|
|
351
|
+
blocking_group=blocking_group,
|
|
352
|
+
)
|
|
353
|
+
return CompiledScreen(
|
|
354
|
+
screen_id=stable_screen_id(action_surface_fingerprint),
|
|
355
|
+
sequence=sequence,
|
|
356
|
+
source_snapshot_id=snapshot.snapshot_id,
|
|
357
|
+
captured_at=snapshot.captured_at,
|
|
358
|
+
package_name=snapshot.package_name,
|
|
359
|
+
activity_name=snapshot.activity_name,
|
|
360
|
+
keyboard_visible=snapshot.ime.visible,
|
|
361
|
+
action_surface_fingerprint=action_surface_fingerprint,
|
|
362
|
+
blocking_group=blocking_group,
|
|
363
|
+
targets=grouped_nodes["targets"],
|
|
364
|
+
context=grouped_nodes["context"],
|
|
365
|
+
dialog=grouped_nodes["dialog"],
|
|
366
|
+
keyboard=grouped_nodes["keyboard"],
|
|
367
|
+
system=grouped_nodes["system"],
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
__all__ = [
|
|
372
|
+
"CLICK_ACTIONS",
|
|
373
|
+
"GENERIC_SEMANTIC_ROLES",
|
|
374
|
+
"LABEL_QUALITY_CONTENT_DESC",
|
|
375
|
+
"LABEL_QUALITY_HINT",
|
|
376
|
+
"LABEL_QUALITY_NEARBY",
|
|
377
|
+
"LABEL_QUALITY_RESOURCE_ID",
|
|
378
|
+
"LABEL_QUALITY_STATE_DESCRIPTION",
|
|
379
|
+
"LABEL_QUALITY_TEXT",
|
|
380
|
+
"LONG_CLICK_ACTIONS",
|
|
381
|
+
"SCROLL_ACTIONS",
|
|
382
|
+
"TYPE_ACTIONS",
|
|
383
|
+
"CompiledScreen",
|
|
384
|
+
"LabelInfo",
|
|
385
|
+
"SemanticCompiler",
|
|
386
|
+
"SemanticNode",
|
|
387
|
+
"actionable_anchor_for",
|
|
388
|
+
"ancestor_distance",
|
|
389
|
+
"apply_blocking_policy",
|
|
390
|
+
"build_action_surface_fingerprint",
|
|
391
|
+
"can_promote_to_actionable_ancestor",
|
|
392
|
+
"compile_node",
|
|
393
|
+
"context_sort_key",
|
|
394
|
+
"extract_state",
|
|
395
|
+
"fallback_label",
|
|
396
|
+
"infer_group",
|
|
397
|
+
"infer_role",
|
|
398
|
+
"label_from_state_description",
|
|
399
|
+
"near_label_for_node",
|
|
400
|
+
"node_to_public_node",
|
|
401
|
+
"normalize_action_name",
|
|
402
|
+
"normalize_text",
|
|
403
|
+
"parent_node_for",
|
|
404
|
+
"passive_node_dedup_key",
|
|
405
|
+
"relative_bounds_for_node",
|
|
406
|
+
"resolve_blocking_group",
|
|
407
|
+
"score_node",
|
|
408
|
+
"secondary_public_actions_for",
|
|
409
|
+
"select_target_sources",
|
|
410
|
+
"semantic_actions_for",
|
|
411
|
+
"semantic_node_fingerprint",
|
|
412
|
+
"semantic_role",
|
|
413
|
+
"should_filter_node",
|
|
414
|
+
"sibling_labels_for_node",
|
|
415
|
+
"stable_screen_id",
|
|
416
|
+
"state_description_parts",
|
|
417
|
+
"states_from_state_description",
|
|
418
|
+
"synthesize_label_info",
|
|
419
|
+
"target_sort_key",
|
|
420
|
+
"target_source_sort_key",
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def compile_node(
|
|
425
|
+
raw_node: RawNode,
|
|
426
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
427
|
+
label_infos: dict[str, LabelInfo],
|
|
428
|
+
actionability: dict[str, list[str]],
|
|
429
|
+
*,
|
|
430
|
+
ime_window_id: str | None,
|
|
431
|
+
root_rid_by_window: dict[str, str],
|
|
432
|
+
action_node: RawNode | None = None,
|
|
433
|
+
) -> SemanticNode | None:
|
|
434
|
+
if not raw_node.visible_to_user:
|
|
435
|
+
return None
|
|
436
|
+
anchor_node = action_node or raw_node
|
|
437
|
+
primary_actions = actionability[anchor_node.rid]
|
|
438
|
+
role = semantic_role(raw_node, anchor_node, primary_actions)
|
|
439
|
+
public_primary_actions = public_primary_actions_for(
|
|
440
|
+
anchor_node=anchor_node,
|
|
441
|
+
role=role,
|
|
442
|
+
primary_actions=primary_actions,
|
|
443
|
+
)
|
|
444
|
+
secondary_actions = secondary_public_actions_for(
|
|
445
|
+
anchor_node=anchor_node,
|
|
446
|
+
role=role,
|
|
447
|
+
primary_actions=public_primary_actions,
|
|
448
|
+
)
|
|
449
|
+
public_actions = [*public_primary_actions, *secondary_actions]
|
|
450
|
+
public_role = role
|
|
451
|
+
scroll_directions: list[str] = []
|
|
452
|
+
text_children: list[str] = []
|
|
453
|
+
if "scroll" in public_actions and role == "container":
|
|
454
|
+
public_role = "scroll-container"
|
|
455
|
+
scroll_directions = list(scroll_directions_for_raw_node(anchor_node))
|
|
456
|
+
text_children = list(
|
|
457
|
+
scroll_container_text_children(
|
|
458
|
+
raw_node=anchor_node,
|
|
459
|
+
raw_nodes_by_rid=raw_nodes_by_rid,
|
|
460
|
+
label_infos=label_infos,
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
label_info = label_infos[raw_node.rid]
|
|
464
|
+
label = label_info.label
|
|
465
|
+
targetable = bool(public_actions)
|
|
466
|
+
if should_filter_node(raw_node, public_role, label, targetable):
|
|
467
|
+
return None
|
|
468
|
+
parent_node = parent_node_for(raw_node, raw_nodes_by_rid)
|
|
469
|
+
parent_role = infer_role(parent_node) if parent_node is not None else ""
|
|
470
|
+
parent_label = (
|
|
471
|
+
label_infos[parent_node.rid].label or fallback_label(parent_node)
|
|
472
|
+
if parent_node is not None
|
|
473
|
+
else ""
|
|
474
|
+
)
|
|
475
|
+
return SemanticNode(
|
|
476
|
+
raw_rid=anchor_node.rid,
|
|
477
|
+
role=public_role,
|
|
478
|
+
label=label or fallback_label(raw_node),
|
|
479
|
+
state=extract_state(anchor_node),
|
|
480
|
+
actions=public_actions,
|
|
481
|
+
bounds=anchor_node.bounds,
|
|
482
|
+
meta=SemanticMeta(
|
|
483
|
+
resource_id=raw_node.resource_id,
|
|
484
|
+
class_name=raw_node.class_name,
|
|
485
|
+
),
|
|
486
|
+
targetable=targetable,
|
|
487
|
+
score=score_node(anchor_node, public_role, targetable),
|
|
488
|
+
group=(
|
|
489
|
+
"dialog"
|
|
490
|
+
if parent_role == "dialog"
|
|
491
|
+
else infer_group(
|
|
492
|
+
anchor_node,
|
|
493
|
+
public_role,
|
|
494
|
+
targetable,
|
|
495
|
+
ime_window_id=ime_window_id,
|
|
496
|
+
)
|
|
497
|
+
),
|
|
498
|
+
parent_role=parent_role,
|
|
499
|
+
parent_label=parent_label,
|
|
500
|
+
sibling_labels=sibling_labels_for_node(raw_node, raw_nodes_by_rid, label_infos),
|
|
501
|
+
relative_bounds=relative_bounds_for_node(raw_node, parent_node),
|
|
502
|
+
scroll_directions=scroll_directions,
|
|
503
|
+
text_children=text_children,
|
|
504
|
+
label_quality=label_info.quality,
|
|
505
|
+
relation_anchor_rid=anchor_node.rid,
|
|
506
|
+
relation_window_id=anchor_node.window_id,
|
|
507
|
+
relation_window_root_rid=root_rid_by_window.get(anchor_node.window_id),
|
|
508
|
+
relation_parent_rid=anchor_node.parent_rid,
|
|
509
|
+
relation_ancestor_scopes=_relation_ancestor_scopes(
|
|
510
|
+
anchor_node=anchor_node,
|
|
511
|
+
raw_nodes_by_rid=raw_nodes_by_rid,
|
|
512
|
+
root_rid_by_window=root_rid_by_window,
|
|
513
|
+
),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _submit_relation_lookup_keys(
|
|
518
|
+
group_name: str,
|
|
519
|
+
node: SemanticNode,
|
|
520
|
+
) -> tuple[tuple[str, str], ...]:
|
|
521
|
+
keys = [semantic_relation_key(group_name, node)]
|
|
522
|
+
if node.relation_anchor_rid:
|
|
523
|
+
keys.append(
|
|
524
|
+
(
|
|
525
|
+
group_name,
|
|
526
|
+
"|".join(
|
|
527
|
+
(
|
|
528
|
+
semantic_relation_identity(group_name, node),
|
|
529
|
+
f"anchor:{node.relation_anchor_rid}",
|
|
530
|
+
)
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
return tuple(keys)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _relation_ancestor_scopes(
|
|
538
|
+
*,
|
|
539
|
+
anchor_node: RawNode,
|
|
540
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
541
|
+
root_rid_by_window: dict[str, str],
|
|
542
|
+
) -> tuple[RelationScopeNode, ...]:
|
|
543
|
+
scopes: list[RelationScopeNode] = []
|
|
544
|
+
seen_rids: set[str] = set()
|
|
545
|
+
parent_rid = anchor_node.parent_rid
|
|
546
|
+
while parent_rid is not None and parent_rid not in seen_rids:
|
|
547
|
+
seen_rids.add(parent_rid)
|
|
548
|
+
parent_node = raw_nodes_by_rid.get(parent_rid)
|
|
549
|
+
if parent_node is None:
|
|
550
|
+
break
|
|
551
|
+
scopes.append(_relation_scope_node(parent_node, root_rid_by_window))
|
|
552
|
+
parent_rid = parent_node.parent_rid
|
|
553
|
+
return tuple(scopes)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def _relation_scope_node(
|
|
557
|
+
node: RawNode,
|
|
558
|
+
root_rid_by_window: dict[str, str],
|
|
559
|
+
) -> RelationScopeNode:
|
|
560
|
+
return RelationScopeNode(
|
|
561
|
+
rid=node.rid,
|
|
562
|
+
window_id=node.window_id,
|
|
563
|
+
parent_rid=node.parent_rid,
|
|
564
|
+
bounds=node.bounds,
|
|
565
|
+
resource_id=node.resource_id or "",
|
|
566
|
+
class_name=node.class_name,
|
|
567
|
+
text=node.text or "",
|
|
568
|
+
content_desc=node.content_desc or "",
|
|
569
|
+
pane_title=node.pane_title or "",
|
|
570
|
+
is_window_root=node.rid == root_rid_by_window.get(node.window_id),
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def scroll_container_text_children(
|
|
575
|
+
*,
|
|
576
|
+
raw_node: RawNode,
|
|
577
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
578
|
+
label_infos: dict[str, LabelInfo],
|
|
579
|
+
) -> tuple[str, ...]:
|
|
580
|
+
texts: list[str] = []
|
|
581
|
+
for child_rid in raw_node.child_rids:
|
|
582
|
+
child = raw_nodes_by_rid.get(child_rid)
|
|
583
|
+
if child is None or not child.visible_to_user:
|
|
584
|
+
continue
|
|
585
|
+
text = label_infos[child.rid].label or fallback_label(child)
|
|
586
|
+
normalized = normalize_text(text)
|
|
587
|
+
if not normalized or normalized in texts:
|
|
588
|
+
continue
|
|
589
|
+
texts.append(normalized)
|
|
590
|
+
if texts:
|
|
591
|
+
return tuple(texts)
|
|
592
|
+
|
|
593
|
+
fallback_text = label_infos[raw_node.rid].label or fallback_label(raw_node)
|
|
594
|
+
normalized_fallback = normalize_text(fallback_text)
|
|
595
|
+
if not normalized_fallback:
|
|
596
|
+
return ()
|
|
597
|
+
return (normalized_fallback,)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def should_filter_node(
|
|
601
|
+
raw_node: RawNode,
|
|
602
|
+
role: str,
|
|
603
|
+
label: str,
|
|
604
|
+
targetable: bool,
|
|
605
|
+
) -> bool:
|
|
606
|
+
if not targetable and not raw_node.important_for_accessibility:
|
|
607
|
+
return True
|
|
608
|
+
if role == "container" and not label and not targetable:
|
|
609
|
+
return True
|
|
610
|
+
return not label and not targetable and role in {"image", "text"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Semantic screen continuity decisions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from androidctld.refs.service import best_candidate_for_binding, binding_for_candidate
|
|
8
|
+
from androidctld.semantics.compiler import CompiledScreen
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ContinuityDecision:
|
|
13
|
+
next_screen_id: str
|
|
14
|
+
continuity_status: str
|
|
15
|
+
changed: bool | None
|
|
16
|
+
code: str | None = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def bootstrap(cls, candidate_screen: CompiledScreen) -> ContinuityDecision:
|
|
20
|
+
return cls(
|
|
21
|
+
next_screen_id=candidate_screen.screen_id,
|
|
22
|
+
continuity_status="none",
|
|
23
|
+
changed=None,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def stable(
|
|
28
|
+
cls,
|
|
29
|
+
*,
|
|
30
|
+
next_screen_id: str,
|
|
31
|
+
changed: bool,
|
|
32
|
+
) -> ContinuityDecision:
|
|
33
|
+
return cls(
|
|
34
|
+
next_screen_id=next_screen_id,
|
|
35
|
+
continuity_status="stable",
|
|
36
|
+
changed=changed,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def stale(
|
|
41
|
+
cls,
|
|
42
|
+
*,
|
|
43
|
+
next_screen_id: str,
|
|
44
|
+
changed: bool,
|
|
45
|
+
code: str,
|
|
46
|
+
) -> ContinuityDecision:
|
|
47
|
+
return cls(
|
|
48
|
+
next_screen_id=next_screen_id,
|
|
49
|
+
continuity_status="stale",
|
|
50
|
+
changed=changed,
|
|
51
|
+
code=code,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def evaluate_continuity(
|
|
56
|
+
*,
|
|
57
|
+
source_screen: CompiledScreen | None,
|
|
58
|
+
candidate_screen: CompiledScreen,
|
|
59
|
+
) -> ContinuityDecision:
|
|
60
|
+
if source_screen is None:
|
|
61
|
+
return ContinuityDecision.bootstrap(candidate_screen)
|
|
62
|
+
if (
|
|
63
|
+
candidate_screen.action_surface_fingerprint
|
|
64
|
+
== source_screen.action_surface_fingerprint
|
|
65
|
+
):
|
|
66
|
+
return ContinuityDecision.stable(
|
|
67
|
+
next_screen_id=source_screen.screen_id,
|
|
68
|
+
changed=False,
|
|
69
|
+
)
|
|
70
|
+
if _strict_repair_succeeds(
|
|
71
|
+
source_screen=source_screen,
|
|
72
|
+
candidate_screen=candidate_screen,
|
|
73
|
+
):
|
|
74
|
+
return ContinuityDecision.stable(
|
|
75
|
+
next_screen_id=candidate_screen.screen_id,
|
|
76
|
+
changed=True,
|
|
77
|
+
)
|
|
78
|
+
return ContinuityDecision.stale(
|
|
79
|
+
next_screen_id=candidate_screen.screen_id,
|
|
80
|
+
changed=True,
|
|
81
|
+
code="REF_STALE",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _strict_repair_succeeds(
|
|
86
|
+
*,
|
|
87
|
+
source_screen: CompiledScreen,
|
|
88
|
+
candidate_screen: CompiledScreen,
|
|
89
|
+
) -> bool:
|
|
90
|
+
source_candidates = [
|
|
91
|
+
candidate for candidate in source_screen.ref_candidates() if candidate.ref
|
|
92
|
+
]
|
|
93
|
+
if not source_candidates:
|
|
94
|
+
return False
|
|
95
|
+
remaining_candidates = list(candidate_screen.ref_candidates())
|
|
96
|
+
for source_candidate in source_candidates:
|
|
97
|
+
binding = binding_for_candidate(
|
|
98
|
+
ref=source_candidate.ref,
|
|
99
|
+
candidate=source_candidate,
|
|
100
|
+
snapshot_id=0,
|
|
101
|
+
reused=True,
|
|
102
|
+
)
|
|
103
|
+
match = best_candidate_for_binding(binding, remaining_candidates)
|
|
104
|
+
if match is None:
|
|
105
|
+
return False
|
|
106
|
+
remaining_candidates.remove(match[0])
|
|
107
|
+
return True
|