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,252 @@
|
|
|
1
|
+
"""Semantic compiler label and state-description rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from androidctld.semantics.policy import LABEL_MAX_LENGTH
|
|
10
|
+
from androidctld.snapshots.models import RawNode
|
|
11
|
+
from androidctld.text_equivalence import (
|
|
12
|
+
canonical_text_key,
|
|
13
|
+
normalized_text_surface,
|
|
14
|
+
semantic_state_description_remainder,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_WHITESPACE_RE = re.compile(r"\s+")
|
|
18
|
+
_STATE_DESCRIPTION_SPLIT_RE = re.compile(r"[,;/]+")
|
|
19
|
+
GENERIC_SEMANTIC_ROLES = {"container", "text", "image", "list-item"}
|
|
20
|
+
LABEL_QUALITY_RESOURCE_ID = 1
|
|
21
|
+
LABEL_QUALITY_STATE_DESCRIPTION = 2
|
|
22
|
+
LABEL_QUALITY_NEARBY = 3
|
|
23
|
+
LABEL_QUALITY_HINT = 4
|
|
24
|
+
LABEL_QUALITY_CONTENT_DESC = 5
|
|
25
|
+
LABEL_QUALITY_TEXT = 6
|
|
26
|
+
_STATE_DESCRIPTION_STATE_MAP = {
|
|
27
|
+
"on": "checked",
|
|
28
|
+
"off": "unchecked",
|
|
29
|
+
"checked": "checked",
|
|
30
|
+
"unchecked": "unchecked",
|
|
31
|
+
"not checked": "unchecked",
|
|
32
|
+
"selected": "selected",
|
|
33
|
+
"disabled": "disabled",
|
|
34
|
+
"focused": "focused",
|
|
35
|
+
"expanded": "expanded",
|
|
36
|
+
"collapsed": "collapsed",
|
|
37
|
+
"password": "password",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class LabelInfo:
|
|
43
|
+
label: str
|
|
44
|
+
quality: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parent_node_for(
|
|
48
|
+
raw_node: RawNode, raw_nodes_by_rid: dict[str, RawNode]
|
|
49
|
+
) -> RawNode | None:
|
|
50
|
+
if raw_node.parent_rid is None:
|
|
51
|
+
return None
|
|
52
|
+
return raw_nodes_by_rid.get(raw_node.parent_rid)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def infer_role(raw_node: RawNode | None) -> str:
|
|
56
|
+
if raw_node is None:
|
|
57
|
+
return "container"
|
|
58
|
+
class_name = raw_node.class_name
|
|
59
|
+
if "Dialog" in class_name:
|
|
60
|
+
return "dialog"
|
|
61
|
+
if "Tab" in class_name:
|
|
62
|
+
return "tab"
|
|
63
|
+
if "Keyboard" in class_name or "Key" in class_name:
|
|
64
|
+
return "keyboard-key"
|
|
65
|
+
if raw_node.editable or "EditText" in class_name:
|
|
66
|
+
return "input"
|
|
67
|
+
if "Switch" in class_name:
|
|
68
|
+
return "switch"
|
|
69
|
+
if "CheckBox" in class_name:
|
|
70
|
+
return "checkbox"
|
|
71
|
+
if "RadioButton" in class_name:
|
|
72
|
+
return "radio"
|
|
73
|
+
if "Button" in class_name:
|
|
74
|
+
return "button"
|
|
75
|
+
if "Image" in class_name:
|
|
76
|
+
return "image"
|
|
77
|
+
if raw_node.clickable:
|
|
78
|
+
return "list-item"
|
|
79
|
+
if raw_node.text or raw_node.content_desc:
|
|
80
|
+
return "text"
|
|
81
|
+
return "container"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def synthesize_label_info(
|
|
85
|
+
raw_node: RawNode | None, raw_nodes_by_rid: dict[str, RawNode]
|
|
86
|
+
) -> LabelInfo:
|
|
87
|
+
if raw_node is None:
|
|
88
|
+
return LabelInfo(label="", quality=0)
|
|
89
|
+
for quality, value in (
|
|
90
|
+
(LABEL_QUALITY_TEXT, raw_node.text),
|
|
91
|
+
(LABEL_QUALITY_CONTENT_DESC, raw_node.content_desc),
|
|
92
|
+
(LABEL_QUALITY_HINT, raw_node.hint_text),
|
|
93
|
+
):
|
|
94
|
+
label = normalize_text(value)
|
|
95
|
+
if label:
|
|
96
|
+
return LabelInfo(label=label[:LABEL_MAX_LENGTH], quality=quality)
|
|
97
|
+
near_label = near_label_for_node(raw_node, raw_nodes_by_rid)
|
|
98
|
+
if near_label:
|
|
99
|
+
return LabelInfo(
|
|
100
|
+
label=near_label[:LABEL_MAX_LENGTH],
|
|
101
|
+
quality=LABEL_QUALITY_NEARBY,
|
|
102
|
+
)
|
|
103
|
+
state_description_label = label_from_state_description(raw_node.state_description)
|
|
104
|
+
if state_description_label:
|
|
105
|
+
return LabelInfo(
|
|
106
|
+
label=state_description_label[:LABEL_MAX_LENGTH],
|
|
107
|
+
quality=LABEL_QUALITY_STATE_DESCRIPTION,
|
|
108
|
+
)
|
|
109
|
+
resource_id = normalize_text(raw_node.resource_id)
|
|
110
|
+
if resource_id:
|
|
111
|
+
return LabelInfo(
|
|
112
|
+
label=resource_id.split("/")[-1][:LABEL_MAX_LENGTH],
|
|
113
|
+
quality=LABEL_QUALITY_RESOURCE_ID,
|
|
114
|
+
)
|
|
115
|
+
return LabelInfo(label="", quality=0)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def fallback_label(raw_node: RawNode | None) -> str:
|
|
119
|
+
if raw_node is None:
|
|
120
|
+
return "item"
|
|
121
|
+
return raw_node.class_name.split(".")[-1] or "item"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def extract_state(raw_node: RawNode) -> list[str]:
|
|
125
|
+
state = []
|
|
126
|
+
if raw_node.checked:
|
|
127
|
+
state.append("checked")
|
|
128
|
+
elif raw_node.checkable:
|
|
129
|
+
state.append("unchecked")
|
|
130
|
+
if raw_node.selected:
|
|
131
|
+
state.append("selected")
|
|
132
|
+
if not raw_node.enabled:
|
|
133
|
+
state.append("disabled")
|
|
134
|
+
if raw_node.focused:
|
|
135
|
+
state.append("focused")
|
|
136
|
+
if raw_node.password:
|
|
137
|
+
state.append("password")
|
|
138
|
+
for token in states_from_state_description(raw_node.state_description):
|
|
139
|
+
if token not in state:
|
|
140
|
+
state.append(token)
|
|
141
|
+
return state
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def semantic_role(
|
|
145
|
+
raw_node: RawNode,
|
|
146
|
+
anchor_node: RawNode,
|
|
147
|
+
anchor_actions: list[str],
|
|
148
|
+
) -> str:
|
|
149
|
+
source_role = infer_role(raw_node)
|
|
150
|
+
anchor_role = infer_role(anchor_node)
|
|
151
|
+
if not anchor_actions or source_role not in GENERIC_SEMANTIC_ROLES:
|
|
152
|
+
role = source_role
|
|
153
|
+
elif anchor_role not in GENERIC_SEMANTIC_ROLES:
|
|
154
|
+
role = anchor_role
|
|
155
|
+
elif "type" in anchor_actions:
|
|
156
|
+
role = "input"
|
|
157
|
+
elif "scroll" in anchor_actions:
|
|
158
|
+
role = "container"
|
|
159
|
+
else:
|
|
160
|
+
role = anchor_role
|
|
161
|
+
if role == "input" and not anchor_node.editable:
|
|
162
|
+
if anchor_role != "input":
|
|
163
|
+
return anchor_role
|
|
164
|
+
if "scroll" in anchor_actions:
|
|
165
|
+
return "container"
|
|
166
|
+
if "tap" in anchor_actions or "longTap" in anchor_actions:
|
|
167
|
+
return "list-item"
|
|
168
|
+
return "text"
|
|
169
|
+
return role
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def near_label_for_node(raw_node: RawNode, raw_nodes_by_rid: dict[str, RawNode]) -> str:
|
|
173
|
+
parent_node = parent_node_for(raw_node, raw_nodes_by_rid)
|
|
174
|
+
if parent_node is None:
|
|
175
|
+
return ""
|
|
176
|
+
for child_rid in parent_node.child_rids:
|
|
177
|
+
if child_rid == raw_node.rid:
|
|
178
|
+
continue
|
|
179
|
+
sibling = raw_nodes_by_rid.get(child_rid)
|
|
180
|
+
if sibling is None:
|
|
181
|
+
continue
|
|
182
|
+
label = normalize_text(sibling.text or sibling.content_desc)
|
|
183
|
+
if label:
|
|
184
|
+
return label
|
|
185
|
+
return normalize_text(parent_node.text or parent_node.content_desc)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def sibling_labels_for_node(
|
|
189
|
+
raw_node: RawNode,
|
|
190
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
191
|
+
label_infos: dict[str, LabelInfo],
|
|
192
|
+
) -> list[str]:
|
|
193
|
+
parent_node = parent_node_for(raw_node, raw_nodes_by_rid)
|
|
194
|
+
if parent_node is None:
|
|
195
|
+
return []
|
|
196
|
+
labels = []
|
|
197
|
+
for child_rid in parent_node.child_rids:
|
|
198
|
+
if child_rid == raw_node.rid:
|
|
199
|
+
continue
|
|
200
|
+
sibling = raw_nodes_by_rid.get(child_rid)
|
|
201
|
+
if sibling is None:
|
|
202
|
+
continue
|
|
203
|
+
label = label_infos[sibling.rid].label or fallback_label(sibling)
|
|
204
|
+
if label:
|
|
205
|
+
labels.append(label)
|
|
206
|
+
return labels
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def relative_bounds_for_node(
|
|
210
|
+
raw_node: RawNode, parent_node: RawNode | None
|
|
211
|
+
) -> tuple[int, int, int, int]:
|
|
212
|
+
if parent_node is None:
|
|
213
|
+
return raw_node.bounds
|
|
214
|
+
return (
|
|
215
|
+
raw_node.bounds[0] - parent_node.bounds[0],
|
|
216
|
+
raw_node.bounds[1] - parent_node.bounds[1],
|
|
217
|
+
raw_node.bounds[2] - parent_node.bounds[0],
|
|
218
|
+
raw_node.bounds[3] - parent_node.bounds[1],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def normalize_text(value: Any) -> str:
|
|
223
|
+
if value is None:
|
|
224
|
+
return ""
|
|
225
|
+
return _WHITESPACE_RE.sub(" ", str(value)).strip()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def states_from_state_description(value: Any) -> list[str]:
|
|
229
|
+
states: list[str] = []
|
|
230
|
+
for part in state_description_parts(value):
|
|
231
|
+
token = _STATE_DESCRIPTION_STATE_MAP.get(canonical_text_key(part))
|
|
232
|
+
if token and token not in states:
|
|
233
|
+
states.append(token)
|
|
234
|
+
return states
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def label_from_state_description(value: Any) -> str:
|
|
238
|
+
return semantic_state_description_remainder(value)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def state_description_parts(value: Any) -> list[str]:
|
|
242
|
+
normalized = normalized_text_surface(value)
|
|
243
|
+
if not normalized:
|
|
244
|
+
return []
|
|
245
|
+
parts = [
|
|
246
|
+
part.strip()
|
|
247
|
+
for part in _STATE_DESCRIPTION_SPLIT_RE.split(normalized)
|
|
248
|
+
if part.strip()
|
|
249
|
+
]
|
|
250
|
+
if parts:
|
|
251
|
+
return parts
|
|
252
|
+
return [normalized]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Typed semantic compiler support models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class SemanticMeta:
|
|
10
|
+
resource_id: str | None
|
|
11
|
+
class_name: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RelationScopeNode:
|
|
16
|
+
rid: str
|
|
17
|
+
window_id: str
|
|
18
|
+
parent_rid: str | None
|
|
19
|
+
bounds: tuple[int, int, int, int]
|
|
20
|
+
resource_id: str
|
|
21
|
+
class_name: str
|
|
22
|
+
text: str
|
|
23
|
+
content_desc: str
|
|
24
|
+
pane_title: str
|
|
25
|
+
is_window_root: bool
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Semantic screen compilation policy constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
LABEL_MAX_LENGTH: Final[int] = 48
|
|
8
|
+
|
|
9
|
+
ROLE_BASE_SCORES: Final[dict[str, int]] = {
|
|
10
|
+
"button": 50,
|
|
11
|
+
"input": 45,
|
|
12
|
+
"switch": 40,
|
|
13
|
+
"checkbox": 35,
|
|
14
|
+
"radio": 35,
|
|
15
|
+
"tab": 30,
|
|
16
|
+
"list-item": 25,
|
|
17
|
+
"keyboard-key": 20,
|
|
18
|
+
"text": 5,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
TARGETABLE_SCORE_BONUS: Final[int] = 100
|
|
22
|
+
FOCUSED_SCORE_BONUS: Final[int] = 10
|
|
23
|
+
ENABLED_SCORE_BONUS: Final[int] = 5
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Daemon helpers for shared public screen contract models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
|
|
7
|
+
from androidctl_contracts.public_screen import (
|
|
8
|
+
BLOCKING_GROUP_NAMES,
|
|
9
|
+
PUBLIC_GROUP_NAMES,
|
|
10
|
+
PUBLIC_NODE_ACTION_VALUES,
|
|
11
|
+
PUBLIC_NODE_AMBIGUITY_VALUES,
|
|
12
|
+
PUBLIC_NODE_ORIGIN_VALUES,
|
|
13
|
+
PUBLIC_NODE_ROLE_VALUES,
|
|
14
|
+
PUBLIC_NODE_STATE_VALUES,
|
|
15
|
+
SCROLL_DIRECTION_VALUES,
|
|
16
|
+
AppMatchType,
|
|
17
|
+
BlockingGroupName,
|
|
18
|
+
OmittedEntry,
|
|
19
|
+
OmittedReason,
|
|
20
|
+
PublicApp,
|
|
21
|
+
PublicFocus,
|
|
22
|
+
PublicGroup,
|
|
23
|
+
PublicGroupName,
|
|
24
|
+
PublicItemKind,
|
|
25
|
+
PublicNode,
|
|
26
|
+
PublicNodeAction,
|
|
27
|
+
PublicNodeRole,
|
|
28
|
+
PublicNodeState,
|
|
29
|
+
PublicScreen,
|
|
30
|
+
PublicSemanticMeta,
|
|
31
|
+
PublicSurface,
|
|
32
|
+
ScrollDirection,
|
|
33
|
+
TransientItem,
|
|
34
|
+
TransientKind,
|
|
35
|
+
VisibleWindow,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
GROUP_NAMES = PUBLIC_GROUP_NAMES
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_public_groups(
|
|
42
|
+
*,
|
|
43
|
+
order: tuple[PublicGroupName, ...] = PUBLIC_GROUP_NAMES,
|
|
44
|
+
targets: tuple[PublicNode, ...] = (),
|
|
45
|
+
keyboard: tuple[PublicNode, ...] = (),
|
|
46
|
+
system: tuple[PublicNode, ...] = (),
|
|
47
|
+
context: tuple[PublicNode, ...] = (),
|
|
48
|
+
dialog: tuple[PublicNode, ...] = (),
|
|
49
|
+
) -> tuple[PublicGroup, ...]:
|
|
50
|
+
nodes_by_group: dict[PublicGroupName, tuple[PublicNode, ...]] = {
|
|
51
|
+
"targets": targets,
|
|
52
|
+
"keyboard": keyboard,
|
|
53
|
+
"system": system,
|
|
54
|
+
"context": context,
|
|
55
|
+
"dialog": dialog,
|
|
56
|
+
}
|
|
57
|
+
if set(order) != set(PUBLIC_GROUP_NAMES) or len(order) != len(PUBLIC_GROUP_NAMES):
|
|
58
|
+
raise ValueError("groups order must contain each public group exactly once")
|
|
59
|
+
return tuple(
|
|
60
|
+
PublicGroup(name=group_name, nodes=nodes_by_group[group_name])
|
|
61
|
+
for group_name in order
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def public_groups_by_name(
|
|
66
|
+
screen: PublicScreen,
|
|
67
|
+
) -> dict[PublicGroupName, tuple[PublicNode, ...]]:
|
|
68
|
+
return {group.name: group.nodes for group in screen.groups}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def public_group_nodes(
|
|
72
|
+
screen: PublicScreen,
|
|
73
|
+
group_name: PublicGroupName,
|
|
74
|
+
) -> tuple[PublicNode, ...]:
|
|
75
|
+
return public_groups_by_name(screen).get(group_name, ())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def iter_public_nodes(nodes: Iterable[PublicNode]) -> Iterator[PublicNode]:
|
|
79
|
+
for node in nodes:
|
|
80
|
+
yield node
|
|
81
|
+
yield from iter_public_nodes(node.children)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def dump_public_screen(screen: PublicScreen) -> dict[str, object]:
|
|
85
|
+
return screen.model_dump(by_alias=True, mode="json")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"BLOCKING_GROUP_NAMES",
|
|
90
|
+
"GROUP_NAMES",
|
|
91
|
+
"PUBLIC_GROUP_NAMES",
|
|
92
|
+
"PUBLIC_NODE_ACTION_VALUES",
|
|
93
|
+
"PUBLIC_NODE_AMBIGUITY_VALUES",
|
|
94
|
+
"PUBLIC_NODE_ORIGIN_VALUES",
|
|
95
|
+
"PUBLIC_NODE_ROLE_VALUES",
|
|
96
|
+
"PUBLIC_NODE_STATE_VALUES",
|
|
97
|
+
"SCROLL_DIRECTION_VALUES",
|
|
98
|
+
"AppMatchType",
|
|
99
|
+
"BlockingGroupName",
|
|
100
|
+
"OmittedEntry",
|
|
101
|
+
"OmittedReason",
|
|
102
|
+
"PublicApp",
|
|
103
|
+
"PublicFocus",
|
|
104
|
+
"PublicGroup",
|
|
105
|
+
"PublicGroupName",
|
|
106
|
+
"PublicItemKind",
|
|
107
|
+
"PublicNode",
|
|
108
|
+
"PublicNodeAction",
|
|
109
|
+
"PublicNodeRole",
|
|
110
|
+
"PublicNodeState",
|
|
111
|
+
"PublicScreen",
|
|
112
|
+
"PublicSemanticMeta",
|
|
113
|
+
"PublicSurface",
|
|
114
|
+
"ScrollDirection",
|
|
115
|
+
"TransientItem",
|
|
116
|
+
"TransientKind",
|
|
117
|
+
"VisibleWindow",
|
|
118
|
+
"build_public_groups",
|
|
119
|
+
"dump_public_screen",
|
|
120
|
+
"iter_public_nodes",
|
|
121
|
+
"public_group_nodes",
|
|
122
|
+
"public_groups_by_name",
|
|
123
|
+
]
|