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,167 @@
|
|
|
1
|
+
"""Semantic compiler target and promotion rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from androidctld.semantics.labels import (
|
|
6
|
+
GENERIC_SEMANTIC_ROLES,
|
|
7
|
+
LabelInfo,
|
|
8
|
+
infer_role,
|
|
9
|
+
normalize_text,
|
|
10
|
+
parent_node_for,
|
|
11
|
+
synthesize_label_info,
|
|
12
|
+
)
|
|
13
|
+
from androidctld.snapshots.models import RawNode
|
|
14
|
+
|
|
15
|
+
CLICK_ACTIONS = {"tap", "click"}
|
|
16
|
+
LONG_CLICK_ACTIONS = {"longTap", "longClick"}
|
|
17
|
+
TYPE_ACTIONS = {"setText"}
|
|
18
|
+
SCROLL_ACTIONS = {
|
|
19
|
+
"scrollForward",
|
|
20
|
+
"scrollBackward",
|
|
21
|
+
"scrollUp",
|
|
22
|
+
"scrollDown",
|
|
23
|
+
"scrollLeft",
|
|
24
|
+
"scrollRight",
|
|
25
|
+
}
|
|
26
|
+
SUBMIT_ACTIONS = {"submit"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def semantic_actions_for(raw_node: RawNode) -> list[str]:
|
|
30
|
+
raw_actions = {normalize_action_name(action) for action in raw_node.actions}
|
|
31
|
+
semantic_actions: list[str] = []
|
|
32
|
+
if raw_actions.intersection(CLICK_ACTIONS):
|
|
33
|
+
semantic_actions.append("tap")
|
|
34
|
+
if raw_actions.intersection(LONG_CLICK_ACTIONS):
|
|
35
|
+
semantic_actions.append("longTap")
|
|
36
|
+
if raw_node.editable and raw_actions.intersection(TYPE_ACTIONS):
|
|
37
|
+
semantic_actions.append("type")
|
|
38
|
+
if raw_node.scrollable and raw_actions.intersection(SCROLL_ACTIONS):
|
|
39
|
+
semantic_actions.append("scroll")
|
|
40
|
+
return semantic_actions
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def public_primary_actions_for(
|
|
44
|
+
*,
|
|
45
|
+
anchor_node: RawNode,
|
|
46
|
+
role: str,
|
|
47
|
+
primary_actions: list[str],
|
|
48
|
+
) -> list[str]:
|
|
49
|
+
if role != "input" or not anchor_node.editable:
|
|
50
|
+
return list(primary_actions)
|
|
51
|
+
if anchor_node.focused:
|
|
52
|
+
return list(primary_actions)
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def secondary_public_actions_for(
|
|
57
|
+
*,
|
|
58
|
+
anchor_node: RawNode,
|
|
59
|
+
role: str,
|
|
60
|
+
primary_actions: list[str],
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
del primary_actions
|
|
63
|
+
anchor_actions = {normalize_action_name(action) for action in anchor_node.actions}
|
|
64
|
+
if role != "input" or not anchor_node.editable:
|
|
65
|
+
return []
|
|
66
|
+
secondary_actions: list[str] = []
|
|
67
|
+
if anchor_node.focused:
|
|
68
|
+
if anchor_actions.intersection(SUBMIT_ACTIONS):
|
|
69
|
+
secondary_actions.append("submit")
|
|
70
|
+
return secondary_actions
|
|
71
|
+
if "focus" not in anchor_actions:
|
|
72
|
+
return secondary_actions
|
|
73
|
+
secondary_actions.append("focus")
|
|
74
|
+
return secondary_actions
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def select_target_sources(
|
|
78
|
+
raw_nodes: tuple[RawNode, ...],
|
|
79
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
80
|
+
label_infos: dict[str, LabelInfo],
|
|
81
|
+
actionability: dict[str, list[str]],
|
|
82
|
+
) -> dict[str, str]:
|
|
83
|
+
target_sources: dict[str, str] = {}
|
|
84
|
+
for raw_node in raw_nodes:
|
|
85
|
+
anchor_node = actionable_anchor_for(raw_node, raw_nodes_by_rid, actionability)
|
|
86
|
+
if anchor_node is None:
|
|
87
|
+
continue
|
|
88
|
+
existing_source = target_sources.get(anchor_node.rid)
|
|
89
|
+
if existing_source is None or target_source_sort_key(
|
|
90
|
+
raw_nodes_by_rid[existing_source],
|
|
91
|
+
anchor_node,
|
|
92
|
+
raw_nodes_by_rid,
|
|
93
|
+
label_infos,
|
|
94
|
+
) < target_source_sort_key(
|
|
95
|
+
raw_node,
|
|
96
|
+
anchor_node,
|
|
97
|
+
raw_nodes_by_rid,
|
|
98
|
+
label_infos,
|
|
99
|
+
):
|
|
100
|
+
target_sources[anchor_node.rid] = raw_node.rid
|
|
101
|
+
return target_sources
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def actionable_anchor_for(
|
|
105
|
+
raw_node: RawNode,
|
|
106
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
107
|
+
actionability: dict[str, list[str]],
|
|
108
|
+
) -> RawNode | None:
|
|
109
|
+
if actionability[raw_node.rid]:
|
|
110
|
+
return raw_node
|
|
111
|
+
current = parent_node_for(raw_node, raw_nodes_by_rid)
|
|
112
|
+
while current is not None:
|
|
113
|
+
anchor_actions = actionability[current.rid]
|
|
114
|
+
if anchor_actions:
|
|
115
|
+
if can_promote_to_actionable_ancestor(raw_node, anchor_actions):
|
|
116
|
+
return current
|
|
117
|
+
return None
|
|
118
|
+
current = parent_node_for(current, raw_nodes_by_rid)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def can_promote_to_actionable_ancestor(
|
|
123
|
+
raw_node: RawNode,
|
|
124
|
+
anchor_actions: list[str],
|
|
125
|
+
) -> bool:
|
|
126
|
+
if "scroll" in anchor_actions:
|
|
127
|
+
return False
|
|
128
|
+
label_quality = synthesize_label_info(raw_node, {raw_node.rid: raw_node}).quality
|
|
129
|
+
role = infer_role(raw_node)
|
|
130
|
+
return label_quality > 0 or role not in GENERIC_SEMANTIC_ROLES
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def target_source_sort_key(
|
|
134
|
+
raw_node: RawNode,
|
|
135
|
+
anchor_node: RawNode,
|
|
136
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
137
|
+
label_infos: dict[str, LabelInfo],
|
|
138
|
+
) -> tuple[int, int, int, int, int, str]:
|
|
139
|
+
label_info = label_infos[raw_node.rid]
|
|
140
|
+
role = infer_role(raw_node)
|
|
141
|
+
actionable_role_bonus = int(role not in GENERIC_SEMANTIC_ROLES)
|
|
142
|
+
return (
|
|
143
|
+
label_info.quality,
|
|
144
|
+
actionable_role_bonus,
|
|
145
|
+
int(raw_node.rid != anchor_node.rid),
|
|
146
|
+
-ancestor_distance(raw_node, anchor_node.rid, raw_nodes_by_rid),
|
|
147
|
+
len(normalize_text(label_info.label)),
|
|
148
|
+
raw_node.rid,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ancestor_distance(
|
|
153
|
+
raw_node: RawNode,
|
|
154
|
+
ancestor_rid: str,
|
|
155
|
+
raw_nodes_by_rid: dict[str, RawNode],
|
|
156
|
+
) -> int:
|
|
157
|
+
distance = 0
|
|
158
|
+
current: RawNode | None = raw_node
|
|
159
|
+
while current is not None and current.rid != ancestor_rid:
|
|
160
|
+
current = parent_node_for(current, raw_nodes_by_rid)
|
|
161
|
+
distance += 1
|
|
162
|
+
return distance
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def normalize_action_name(value: str) -> str:
|
|
166
|
+
normalized = normalize_text(value)
|
|
167
|
+
return normalized[:1].lower() + normalized[1:]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Raw snapshot fetching and caching."""
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Validated raw snapshot domain models and parsing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from androidctld.device.errors import DeviceBootstrapError, device_rpc_failed
|
|
10
|
+
from androidctld.schema.core import SchemaDecodeError
|
|
11
|
+
from androidctld.schema.validation_errors import validation_error_to_schema_decode_error
|
|
12
|
+
from androidctld.snapshots.schema import (
|
|
13
|
+
RawDisplayPayload,
|
|
14
|
+
RawImePayload,
|
|
15
|
+
RawNodePayload,
|
|
16
|
+
RawSnapshotPayload,
|
|
17
|
+
RawWindowPayload,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RawIme:
|
|
23
|
+
visible: bool
|
|
24
|
+
window_id: str | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class RawWindow:
|
|
29
|
+
window_id: str
|
|
30
|
+
type: str
|
|
31
|
+
layer: int
|
|
32
|
+
package_name: str | None
|
|
33
|
+
bounds: tuple[int, int, int, int]
|
|
34
|
+
root_rid: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class RawNode:
|
|
39
|
+
rid: str
|
|
40
|
+
window_id: str
|
|
41
|
+
parent_rid: str | None
|
|
42
|
+
child_rids: tuple[str, ...]
|
|
43
|
+
class_name: str
|
|
44
|
+
resource_id: str | None
|
|
45
|
+
text: str | None
|
|
46
|
+
content_desc: str | None
|
|
47
|
+
hint_text: str | None
|
|
48
|
+
state_description: str | None
|
|
49
|
+
pane_title: str | None
|
|
50
|
+
package_name: str | None
|
|
51
|
+
bounds: tuple[int, int, int, int]
|
|
52
|
+
visible_to_user: bool
|
|
53
|
+
important_for_accessibility: bool
|
|
54
|
+
clickable: bool
|
|
55
|
+
enabled: bool
|
|
56
|
+
editable: bool
|
|
57
|
+
focusable: bool
|
|
58
|
+
focused: bool
|
|
59
|
+
checkable: bool
|
|
60
|
+
checked: bool
|
|
61
|
+
selected: bool
|
|
62
|
+
scrollable: bool
|
|
63
|
+
password: bool
|
|
64
|
+
actions: tuple[str, ...]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class RawSnapshot:
|
|
69
|
+
snapshot_id: int
|
|
70
|
+
captured_at: str
|
|
71
|
+
package_name: str | None
|
|
72
|
+
activity_name: str | None
|
|
73
|
+
ime: RawIme
|
|
74
|
+
windows: tuple[RawWindow, ...]
|
|
75
|
+
nodes: tuple[RawNode, ...]
|
|
76
|
+
display: dict[str, int]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parse_raw_snapshot(payload: object) -> RawSnapshot:
|
|
80
|
+
try:
|
|
81
|
+
boundary = RawSnapshotPayload.model_validate(payload)
|
|
82
|
+
except ValidationError as error:
|
|
83
|
+
raise invalid_snapshot_validation_error(error, field_name="result") from error
|
|
84
|
+
try:
|
|
85
|
+
return adapt_raw_snapshot(boundary, field_name="result")
|
|
86
|
+
except SchemaDecodeError as error:
|
|
87
|
+
raise invalid_snapshot_payload(error.field, error.problem) from error
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def adapt_raw_snapshot(
|
|
91
|
+
payload: RawSnapshotPayload,
|
|
92
|
+
*,
|
|
93
|
+
field_name: str = "result",
|
|
94
|
+
) -> RawSnapshot:
|
|
95
|
+
return RawSnapshot(
|
|
96
|
+
snapshot_id=payload.snapshot_id,
|
|
97
|
+
captured_at=payload.captured_at,
|
|
98
|
+
package_name=payload.package_name,
|
|
99
|
+
activity_name=payload.activity_name,
|
|
100
|
+
ime=adapt_ime(payload.ime, field_name=f"{field_name}.ime"),
|
|
101
|
+
windows=tuple(
|
|
102
|
+
adapt_raw_window(window, field_name=f"{field_name}.windows[{index}]")
|
|
103
|
+
for index, window in enumerate(payload.windows)
|
|
104
|
+
),
|
|
105
|
+
nodes=tuple(
|
|
106
|
+
adapt_raw_node(node, field_name=f"{field_name}.nodes[{index}]")
|
|
107
|
+
for index, node in enumerate(payload.nodes)
|
|
108
|
+
),
|
|
109
|
+
display=adapt_display(payload.display, field_name=f"{field_name}.display"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def adapt_display(
|
|
114
|
+
payload: RawDisplayPayload,
|
|
115
|
+
*,
|
|
116
|
+
field_name: str = "result.display",
|
|
117
|
+
) -> dict[str, int]:
|
|
118
|
+
del field_name
|
|
119
|
+
return {
|
|
120
|
+
"widthPx": payload.width_px,
|
|
121
|
+
"heightPx": payload.height_px,
|
|
122
|
+
"densityDpi": payload.density_dpi,
|
|
123
|
+
"rotation": payload.rotation,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def adapt_ime(
|
|
128
|
+
payload: RawImePayload,
|
|
129
|
+
*,
|
|
130
|
+
field_name: str = "result.ime",
|
|
131
|
+
) -> RawIme:
|
|
132
|
+
del field_name
|
|
133
|
+
return RawIme(
|
|
134
|
+
visible=payload.visible,
|
|
135
|
+
window_id=payload.window_id,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def adapt_raw_window(
|
|
140
|
+
payload: RawWindowPayload,
|
|
141
|
+
*,
|
|
142
|
+
field_name: str,
|
|
143
|
+
) -> RawWindow:
|
|
144
|
+
return RawWindow(
|
|
145
|
+
window_id=payload.window_id,
|
|
146
|
+
type=payload.type,
|
|
147
|
+
layer=payload.layer,
|
|
148
|
+
package_name=payload.package_name,
|
|
149
|
+
bounds=adapt_bounds(payload.bounds, field_name=f"{field_name}.bounds"),
|
|
150
|
+
root_rid=payload.root_rid,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def adapt_raw_node(
|
|
155
|
+
payload: RawNodePayload,
|
|
156
|
+
*,
|
|
157
|
+
field_name: str,
|
|
158
|
+
) -> RawNode:
|
|
159
|
+
return RawNode(
|
|
160
|
+
rid=payload.rid,
|
|
161
|
+
window_id=payload.window_id,
|
|
162
|
+
parent_rid=payload.parent_rid,
|
|
163
|
+
child_rids=tuple(payload.child_rids),
|
|
164
|
+
class_name=payload.class_name,
|
|
165
|
+
resource_id=payload.resource_id,
|
|
166
|
+
text=payload.text,
|
|
167
|
+
content_desc=payload.content_desc,
|
|
168
|
+
hint_text=payload.hint_text,
|
|
169
|
+
state_description=payload.state_description,
|
|
170
|
+
pane_title=payload.pane_title,
|
|
171
|
+
package_name=payload.package_name,
|
|
172
|
+
bounds=adapt_bounds(payload.bounds, field_name=f"{field_name}.bounds"),
|
|
173
|
+
visible_to_user=payload.visible_to_user,
|
|
174
|
+
important_for_accessibility=payload.important_for_accessibility,
|
|
175
|
+
clickable=payload.clickable,
|
|
176
|
+
enabled=payload.enabled,
|
|
177
|
+
editable=payload.editable,
|
|
178
|
+
focusable=payload.focusable,
|
|
179
|
+
focused=payload.focused,
|
|
180
|
+
checkable=payload.checkable,
|
|
181
|
+
checked=payload.checked,
|
|
182
|
+
selected=payload.selected,
|
|
183
|
+
scrollable=payload.scrollable,
|
|
184
|
+
password=payload.password,
|
|
185
|
+
actions=tuple(payload.actions),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def adapt_bounds(
|
|
190
|
+
bounds: list[int],
|
|
191
|
+
*,
|
|
192
|
+
field_name: str,
|
|
193
|
+
) -> tuple[int, int, int, int]:
|
|
194
|
+
if len(bounds) != 4:
|
|
195
|
+
raise SchemaDecodeError(field_name, "must contain exactly 4 integers")
|
|
196
|
+
return (bounds[0], bounds[1], bounds[2], bounds[3])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def invalid_snapshot_validation_error(
|
|
200
|
+
error: ValidationError,
|
|
201
|
+
*,
|
|
202
|
+
field_name: str,
|
|
203
|
+
) -> DeviceBootstrapError:
|
|
204
|
+
schema_error = validation_error_to_schema_decode_error(
|
|
205
|
+
error,
|
|
206
|
+
field_name=field_name,
|
|
207
|
+
)
|
|
208
|
+
return invalid_snapshot_payload(schema_error.field, schema_error.problem)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def invalid_snapshot_payload(field_name: str, problem: str) -> DeviceBootstrapError:
|
|
212
|
+
return device_rpc_failed(
|
|
213
|
+
f"device RPC {field_name} {problem}",
|
|
214
|
+
{
|
|
215
|
+
"field": field_name,
|
|
216
|
+
"reason": "invalid_snapshot",
|
|
217
|
+
},
|
|
218
|
+
retryable=False,
|
|
219
|
+
)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Shared screen refresh transaction and signature helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from androidctld.artifacts.models import ScreenArtifacts
|
|
11
|
+
from androidctld.artifacts.writer import ArtifactWriter
|
|
12
|
+
from androidctld.commands.models import CommandRecord
|
|
13
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
14
|
+
from androidctld.protocol import CommandKind
|
|
15
|
+
from androidctld.refs.models import RefRegistry
|
|
16
|
+
from androidctld.refs.service import RefRegistryBuilder
|
|
17
|
+
from androidctld.runtime import RuntimeKernel, RuntimeLifecycleLease
|
|
18
|
+
from androidctld.runtime.kernel import ScreenRefreshUpdate
|
|
19
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
20
|
+
from androidctld.semantics.compiler import (
|
|
21
|
+
CompiledScreen,
|
|
22
|
+
SemanticCompiler,
|
|
23
|
+
SemanticNode,
|
|
24
|
+
)
|
|
25
|
+
from androidctld.semantics.public_models import PublicScreen
|
|
26
|
+
from androidctld.snapshots.models import RawSnapshot
|
|
27
|
+
from androidctld.text_equivalence import canonical_text_key, searchable_raw_node_texts
|
|
28
|
+
|
|
29
|
+
RefreshScreenResult = tuple[
|
|
30
|
+
RawSnapshot,
|
|
31
|
+
PublicScreen,
|
|
32
|
+
ScreenArtifacts,
|
|
33
|
+
]
|
|
34
|
+
RefreshCandidateValidator = Callable[[RawSnapshot, PublicScreen, CompiledScreen], None]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ScreenRefreshBasis:
|
|
39
|
+
sequence: int
|
|
40
|
+
previous_registry: RefRegistry
|
|
41
|
+
lifecycle_lease: RuntimeLifecycleLease | None
|
|
42
|
+
command_kind: CommandKind | None
|
|
43
|
+
wait_kind: object | None
|
|
44
|
+
record: CommandRecord | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_wait_kind(wait_kind: object | None) -> str | None:
|
|
48
|
+
if wait_kind is None:
|
|
49
|
+
return None
|
|
50
|
+
return str(getattr(wait_kind, "value", wait_kind))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ScreenRefreshService:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
runtime_kernel: RuntimeKernel,
|
|
57
|
+
semantic_compiler: SemanticCompiler | None = None,
|
|
58
|
+
artifact_writer: ArtifactWriter | None = None,
|
|
59
|
+
ref_registry_builder: RefRegistryBuilder | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._runtime_kernel = runtime_kernel
|
|
62
|
+
self._semantic_compiler = semantic_compiler or SemanticCompiler()
|
|
63
|
+
self._artifact_writer = artifact_writer or ArtifactWriter()
|
|
64
|
+
self._ref_registry_builder = ref_registry_builder or RefRegistryBuilder()
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def runtime_kernel(self) -> RuntimeKernel:
|
|
68
|
+
return self._runtime_kernel
|
|
69
|
+
|
|
70
|
+
def refresh(
|
|
71
|
+
self,
|
|
72
|
+
session: WorkspaceRuntime,
|
|
73
|
+
snapshot: RawSnapshot,
|
|
74
|
+
*,
|
|
75
|
+
lifecycle_lease: RuntimeLifecycleLease | None = None,
|
|
76
|
+
command_kind: CommandKind | None = None,
|
|
77
|
+
wait_kind: object | None = None,
|
|
78
|
+
record: CommandRecord | None = None,
|
|
79
|
+
candidate_validator: RefreshCandidateValidator | None = None,
|
|
80
|
+
) -> RefreshScreenResult:
|
|
81
|
+
basis = _capture_refresh_basis(
|
|
82
|
+
session,
|
|
83
|
+
lifecycle_lease=lifecycle_lease,
|
|
84
|
+
command_kind=command_kind,
|
|
85
|
+
wait_kind=wait_kind,
|
|
86
|
+
record=record,
|
|
87
|
+
)
|
|
88
|
+
compiled_screen = self._semantic_compiler.compile(basis.sequence, snapshot)
|
|
89
|
+
reconcile_result = self._ref_registry_builder.finalize_compiled_screen(
|
|
90
|
+
compiled_screen=compiled_screen,
|
|
91
|
+
snapshot_id=snapshot.snapshot_id,
|
|
92
|
+
previous_registry=basis.previous_registry,
|
|
93
|
+
)
|
|
94
|
+
ref_registry = reconcile_result.registry
|
|
95
|
+
finalized_compiled_screen = reconcile_result.compiled_screen
|
|
96
|
+
public_screen = finalized_compiled_screen.to_public_screen()
|
|
97
|
+
staged_artifacts = self._artifact_writer.stage_screen(
|
|
98
|
+
session,
|
|
99
|
+
public_screen,
|
|
100
|
+
sequence=finalized_compiled_screen.sequence,
|
|
101
|
+
source_snapshot_id=finalized_compiled_screen.source_snapshot_id,
|
|
102
|
+
captured_at=finalized_compiled_screen.captured_at,
|
|
103
|
+
ref_registry=ref_registry,
|
|
104
|
+
)
|
|
105
|
+
artifacts = staged_artifacts.artifacts
|
|
106
|
+
|
|
107
|
+
def raise_if_refresh_stale(active_session: WorkspaceRuntime) -> None:
|
|
108
|
+
raise_if_stale(
|
|
109
|
+
active_session,
|
|
110
|
+
basis.lifecycle_lease,
|
|
111
|
+
kind=basis.command_kind,
|
|
112
|
+
wait_kind=basis.wait_kind,
|
|
113
|
+
record=basis.record,
|
|
114
|
+
)
|
|
115
|
+
if candidate_validator is not None:
|
|
116
|
+
candidate_validator(snapshot, public_screen, finalized_compiled_screen)
|
|
117
|
+
|
|
118
|
+
self._runtime_kernel.commit_screen_refresh(
|
|
119
|
+
session,
|
|
120
|
+
update=ScreenRefreshUpdate(
|
|
121
|
+
sequence=basis.sequence,
|
|
122
|
+
snapshot=snapshot,
|
|
123
|
+
public_screen=public_screen,
|
|
124
|
+
compiled_screen=finalized_compiled_screen,
|
|
125
|
+
artifacts=artifacts,
|
|
126
|
+
ref_registry=ref_registry,
|
|
127
|
+
staged_artifacts=staged_artifacts,
|
|
128
|
+
),
|
|
129
|
+
pre_commit=raise_if_refresh_stale,
|
|
130
|
+
)
|
|
131
|
+
return snapshot, public_screen, artifacts
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _capture_refresh_basis(
|
|
135
|
+
session: WorkspaceRuntime,
|
|
136
|
+
*,
|
|
137
|
+
lifecycle_lease: RuntimeLifecycleLease | None,
|
|
138
|
+
command_kind: CommandKind | None,
|
|
139
|
+
wait_kind: object | None,
|
|
140
|
+
record: CommandRecord | None,
|
|
141
|
+
) -> ScreenRefreshBasis:
|
|
142
|
+
with session.lock:
|
|
143
|
+
raise_if_stale(
|
|
144
|
+
session,
|
|
145
|
+
lifecycle_lease,
|
|
146
|
+
kind=command_kind,
|
|
147
|
+
wait_kind=wait_kind,
|
|
148
|
+
record=record,
|
|
149
|
+
)
|
|
150
|
+
return ScreenRefreshBasis(
|
|
151
|
+
sequence=session.screen_sequence + 1,
|
|
152
|
+
previous_registry=deepcopy(session.ref_registry),
|
|
153
|
+
lifecycle_lease=lifecycle_lease,
|
|
154
|
+
command_kind=command_kind,
|
|
155
|
+
wait_kind=deepcopy(wait_kind),
|
|
156
|
+
record=deepcopy(record),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def raise_if_stale(
|
|
161
|
+
session: WorkspaceRuntime,
|
|
162
|
+
lease: RuntimeLifecycleLease | None,
|
|
163
|
+
*,
|
|
164
|
+
kind: CommandKind | None = None,
|
|
165
|
+
wait_kind: object | None = None,
|
|
166
|
+
record: CommandRecord | None = None,
|
|
167
|
+
) -> None:
|
|
168
|
+
if lease is None or lease.is_current(session):
|
|
169
|
+
return
|
|
170
|
+
details = {"workspaceRoot": session.workspace_root.as_posix()}
|
|
171
|
+
if record is not None:
|
|
172
|
+
details["commandId"] = record.command_id
|
|
173
|
+
if kind is not None:
|
|
174
|
+
details["kind"] = kind.value
|
|
175
|
+
normalized_wait_kind = _normalize_wait_kind(wait_kind)
|
|
176
|
+
if kind is CommandKind.WAIT and normalized_wait_kind is not None:
|
|
177
|
+
details["waitKind"] = normalized_wait_kind
|
|
178
|
+
raise DaemonError(
|
|
179
|
+
code=DaemonErrorCode.COMMAND_CANCELLED,
|
|
180
|
+
message=(
|
|
181
|
+
"command was canceled"
|
|
182
|
+
if kind is None
|
|
183
|
+
else (
|
|
184
|
+
f"wait {normalized_wait_kind} was canceled"
|
|
185
|
+
if kind is CommandKind.WAIT and normalized_wait_kind is not None
|
|
186
|
+
else f"{kind.value} was canceled"
|
|
187
|
+
)
|
|
188
|
+
),
|
|
189
|
+
retryable=False,
|
|
190
|
+
details=details,
|
|
191
|
+
http_status=200,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def compiled_screen_nodes(compiled_screen: CompiledScreen) -> tuple[SemanticNode, ...]:
|
|
196
|
+
return (
|
|
197
|
+
*compiled_screen.targets,
|
|
198
|
+
*compiled_screen.context,
|
|
199
|
+
*compiled_screen.dialog,
|
|
200
|
+
*compiled_screen.keyboard,
|
|
201
|
+
*compiled_screen.system,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def compiled_screen_signature(
|
|
206
|
+
compiled_screen: CompiledScreen | None,
|
|
207
|
+
snapshot: RawSnapshot,
|
|
208
|
+
) -> tuple[Any, ...]:
|
|
209
|
+
if compiled_screen is None:
|
|
210
|
+
return (
|
|
211
|
+
snapshot.package_name,
|
|
212
|
+
snapshot.activity_name,
|
|
213
|
+
tuple(
|
|
214
|
+
tuple(
|
|
215
|
+
canonical_text_key(value)
|
|
216
|
+
for value in searchable_raw_node_texts(node)
|
|
217
|
+
)
|
|
218
|
+
for node in snapshot.nodes
|
|
219
|
+
if node.visible_to_user
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
return (
|
|
223
|
+
compiled_screen.package_name,
|
|
224
|
+
compiled_screen.activity_name,
|
|
225
|
+
compiled_screen.keyboard_visible,
|
|
226
|
+
tuple(
|
|
227
|
+
(
|
|
228
|
+
node.group,
|
|
229
|
+
node.role,
|
|
230
|
+
canonical_text_key(node.label),
|
|
231
|
+
tuple(canonical_text_key(value) for value in node.state),
|
|
232
|
+
tuple(node.actions),
|
|
233
|
+
node.ref,
|
|
234
|
+
None if node.bounds is None else tuple(node.bounds),
|
|
235
|
+
)
|
|
236
|
+
for node in compiled_screen_nodes(compiled_screen)
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def settle_screen_signature(
|
|
242
|
+
compiled_screen: CompiledScreen | None,
|
|
243
|
+
snapshot: RawSnapshot,
|
|
244
|
+
) -> tuple[Any, ...]:
|
|
245
|
+
if compiled_screen is None:
|
|
246
|
+
return (
|
|
247
|
+
snapshot.package_name,
|
|
248
|
+
snapshot.activity_name,
|
|
249
|
+
tuple(
|
|
250
|
+
tuple(
|
|
251
|
+
canonical_text_key(value)
|
|
252
|
+
for value in searchable_raw_node_texts(node)
|
|
253
|
+
)
|
|
254
|
+
for node in snapshot.nodes
|
|
255
|
+
if node.visible_to_user
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
return (
|
|
259
|
+
compiled_screen.package_name,
|
|
260
|
+
compiled_screen.activity_name,
|
|
261
|
+
compiled_screen.keyboard_visible,
|
|
262
|
+
tuple(
|
|
263
|
+
(
|
|
264
|
+
node.group,
|
|
265
|
+
node.role,
|
|
266
|
+
canonical_text_key(node.label),
|
|
267
|
+
tuple(canonical_text_key(value) for value in node.state),
|
|
268
|
+
tuple(node.actions),
|
|
269
|
+
None if node.bounds is None else tuple(node.bounds),
|
|
270
|
+
)
|
|
271
|
+
for node in compiled_screen_nodes(compiled_screen)
|
|
272
|
+
),
|
|
273
|
+
)
|