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,732 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
5
|
+
from typing import Literal, TypeAlias, TypedDict, cast
|
|
6
|
+
|
|
7
|
+
from typing_extensions import NotRequired
|
|
8
|
+
|
|
9
|
+
from androidctl.renderers import (
|
|
10
|
+
ProjectionDict,
|
|
11
|
+
ProjectionValue,
|
|
12
|
+
RenderPayload,
|
|
13
|
+
projection_dict,
|
|
14
|
+
)
|
|
15
|
+
from androidctl_contracts.public_screen import (
|
|
16
|
+
BLOCKING_GROUP_NAMES,
|
|
17
|
+
OMITTED_REASON_VALUES,
|
|
18
|
+
PUBLIC_GROUP_NAMES,
|
|
19
|
+
PUBLIC_NODE_ROLE_VALUES,
|
|
20
|
+
TRANSIENT_KIND_VALUES,
|
|
21
|
+
BlockingGroupName,
|
|
22
|
+
OmittedReason,
|
|
23
|
+
PublicGroupName,
|
|
24
|
+
TransientKind,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
XmlScalarAttrs: TypeAlias = dict[str, str]
|
|
28
|
+
XmlGroupName: TypeAlias = PublicGroupName
|
|
29
|
+
XmlAttrRule: TypeAlias = tuple[str, Callable[[object], str | None]]
|
|
30
|
+
XmlGroupItemTag: TypeAlias = str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class XmlSurfaceProjection(TypedDict):
|
|
34
|
+
attrs: XmlScalarAttrs
|
|
35
|
+
focus: XmlScalarAttrs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class XmlNodeGroupItemProjection(TypedDict):
|
|
39
|
+
tag: XmlGroupItemTag
|
|
40
|
+
attrs: XmlScalarAttrs
|
|
41
|
+
children: list[XmlNodeGroupItemProjection]
|
|
42
|
+
text: NotRequired[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
XmlGroupItemProjection: TypeAlias = XmlNodeGroupItemProjection
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class XmlGroupProjection(TypedDict):
|
|
49
|
+
name: XmlGroupName
|
|
50
|
+
items: list[XmlGroupItemProjection]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class XmlOmittedEntryProjection(TypedDict):
|
|
54
|
+
group: XmlGroupName
|
|
55
|
+
reason: OmittedReason
|
|
56
|
+
count: NotRequired[str]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class XmlTransientItemProjection(TypedDict):
|
|
60
|
+
text: str
|
|
61
|
+
kind: NotRequired[TransientKind]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class XmlScreenProjection(TypedDict):
|
|
65
|
+
attrs: XmlScalarAttrs
|
|
66
|
+
app: XmlScalarAttrs
|
|
67
|
+
surface: XmlSurfaceProjection
|
|
68
|
+
groups: list[XmlGroupProjection]
|
|
69
|
+
omitted: list[XmlOmittedEntryProjection]
|
|
70
|
+
visibleWindows: list[XmlScalarAttrs]
|
|
71
|
+
transient: list[XmlTransientItemProjection]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class XmlActionTargetProjection(TypedDict):
|
|
75
|
+
attrs: XmlScalarAttrs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class XmlSemanticProjection(TypedDict):
|
|
79
|
+
kind: Literal["semantic"]
|
|
80
|
+
attrs: XmlScalarAttrs
|
|
81
|
+
message: str | None
|
|
82
|
+
truth: XmlScalarAttrs
|
|
83
|
+
actionTarget: XmlActionTargetProjection | None
|
|
84
|
+
uncertainty: list[str]
|
|
85
|
+
warnings: list[str]
|
|
86
|
+
screen: XmlScreenProjection | None
|
|
87
|
+
artifacts: XmlScalarAttrs
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class XmlRetainedProjection(TypedDict):
|
|
91
|
+
kind: Literal["retained"]
|
|
92
|
+
attrs: XmlScalarAttrs
|
|
93
|
+
message: str | None
|
|
94
|
+
details: XmlScalarAttrs
|
|
95
|
+
artifacts: XmlScalarAttrs
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class XmlListAppsProjection(TypedDict):
|
|
99
|
+
kind: Literal["listApps"]
|
|
100
|
+
attrs: XmlScalarAttrs
|
|
101
|
+
apps: list[XmlScalarAttrs]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
XmlProjection: TypeAlias = (
|
|
105
|
+
XmlSemanticProjection | XmlRetainedProjection | XmlListAppsProjection
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_TRUTH_ATTRS = (
|
|
110
|
+
"executionOutcome",
|
|
111
|
+
"continuityStatus",
|
|
112
|
+
"observationQuality",
|
|
113
|
+
"changed",
|
|
114
|
+
)
|
|
115
|
+
_ACTION_TARGET_ATTRS = (
|
|
116
|
+
"sourceRef",
|
|
117
|
+
"sourceScreenId",
|
|
118
|
+
"subjectRef",
|
|
119
|
+
"dispatchedRef",
|
|
120
|
+
"nextScreenId",
|
|
121
|
+
"nextRef",
|
|
122
|
+
"identityStatus",
|
|
123
|
+
"evidence",
|
|
124
|
+
)
|
|
125
|
+
_APP_ATTRS = (
|
|
126
|
+
"packageName",
|
|
127
|
+
"activityName",
|
|
128
|
+
"requestedPackageName",
|
|
129
|
+
"resolvedPackageName",
|
|
130
|
+
"matchType",
|
|
131
|
+
)
|
|
132
|
+
_FOCUS_ATTRS = ("inputRef",)
|
|
133
|
+
_SEMANTIC_ARTIFACT_ATTRS = ("screenshotPng", "screenXml")
|
|
134
|
+
_RETAINED_ARTIFACT_ATTRS = ("screenshotPng",)
|
|
135
|
+
_LIST_APP_ATTRS = ("packageName", "appLabel")
|
|
136
|
+
_RETAINED_DETAILS_ATTRS = (
|
|
137
|
+
"sourceCode",
|
|
138
|
+
"sourceKind",
|
|
139
|
+
"operation",
|
|
140
|
+
"reason",
|
|
141
|
+
"expectedReleaseVersion",
|
|
142
|
+
"actualReleaseVersion",
|
|
143
|
+
)
|
|
144
|
+
_RETAINED_SOURCE_CODE_RE = re.compile(r"^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$")
|
|
145
|
+
_RETAINED_DETAIL_SLUG_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
|
|
146
|
+
_RETAINED_RELEASE_VERSION_RE = re.compile(
|
|
147
|
+
r"^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$"
|
|
148
|
+
)
|
|
149
|
+
_DEVICE_SERIAL_DETAIL_RE = re.compile(r"^(?:emulator-\d+|[A-Z0-9]{6,})$")
|
|
150
|
+
_FINGERPRINT_DETAIL_RE = re.compile(r"^(?:[0-9a-f]{16,}|[0-9a-f]{2}:){7,}[0-9a-f]{2}$")
|
|
151
|
+
_SAFE_TOKEN_DETAIL_VALUES = frozenset({"wrong-token"})
|
|
152
|
+
_VISIBLE_WINDOW_ATTRS = ("windowRef", "role", "focused", "blocking")
|
|
153
|
+
_GROUP_ORDER: tuple[XmlGroupName, ...] = PUBLIC_GROUP_NAMES
|
|
154
|
+
_BLOCKING_GROUPS: set[BlockingGroupName] = set(BLOCKING_GROUP_NAMES)
|
|
155
|
+
_OMITTED_REASONS: set[OmittedReason] = set(OMITTED_REASON_VALUES)
|
|
156
|
+
_TRANSIENT_KINDS: set[TransientKind] = set(TRANSIENT_KIND_VALUES)
|
|
157
|
+
_NODE_ROLE_TAGS = frozenset(PUBLIC_NODE_ROLE_VALUES)
|
|
158
|
+
_NODE_TEXT_ATTRS = ("label", "within", "value")
|
|
159
|
+
_TEXT_ITEM_ATTRS = ("origin", "windowRef", "ambiguity", "within", "value")
|
|
160
|
+
_NODE_SCALAR_ATTRS = (
|
|
161
|
+
"ref",
|
|
162
|
+
"role",
|
|
163
|
+
"origin",
|
|
164
|
+
"windowRef",
|
|
165
|
+
"ambiguity",
|
|
166
|
+
)
|
|
167
|
+
_NODE_SEQUENCE_ATTRS = (
|
|
168
|
+
"actions",
|
|
169
|
+
"state",
|
|
170
|
+
"scrollDirections",
|
|
171
|
+
"submitRefs",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def project_xml_payload(payload: RenderPayload) -> XmlProjection:
|
|
176
|
+
projected = projection_dict(payload)
|
|
177
|
+
if "envelope" in projected:
|
|
178
|
+
return _project_retained_payload(projected)
|
|
179
|
+
if projected.get("command") == "list-apps":
|
|
180
|
+
return _project_list_apps_payload(projected)
|
|
181
|
+
return _project_semantic_payload(projected)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _project_retained_payload(result: ProjectionDict) -> XmlRetainedProjection:
|
|
185
|
+
attrs = _project_retained_top_level_attrs(result)
|
|
186
|
+
message = result.get("message")
|
|
187
|
+
return {
|
|
188
|
+
"kind": "retained",
|
|
189
|
+
"attrs": attrs,
|
|
190
|
+
"message": message if isinstance(message, str) and message else None,
|
|
191
|
+
"details": _project_retained_details_attrs(_mapping(result.get("details"))),
|
|
192
|
+
"artifacts": _project_non_empty_string_attrs(
|
|
193
|
+
_mapping(result.get("artifacts")),
|
|
194
|
+
_RETAINED_ARTIFACT_ATTRS,
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _project_list_apps_payload(result: ProjectionDict) -> XmlListAppsProjection:
|
|
200
|
+
return {
|
|
201
|
+
"kind": "listApps",
|
|
202
|
+
"attrs": _project_required_attr_rules(
|
|
203
|
+
result,
|
|
204
|
+
(
|
|
205
|
+
("ok", _required_ok_attr),
|
|
206
|
+
("command", _scalar_attr),
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
"apps": _project_list_app_items(result.get("apps")),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _project_semantic_payload(result: ProjectionDict) -> XmlSemanticProjection:
|
|
214
|
+
screen_value = result.get("screen")
|
|
215
|
+
screen = (
|
|
216
|
+
_project_screen(screen_value) if isinstance(screen_value, Mapping) else None
|
|
217
|
+
)
|
|
218
|
+
message = result.get("message")
|
|
219
|
+
return {
|
|
220
|
+
"kind": "semantic",
|
|
221
|
+
"attrs": _project_top_level_attrs(result),
|
|
222
|
+
"message": message if isinstance(message, str) and message else None,
|
|
223
|
+
"truth": _project_attrs(_mapping(result.get("truth")), _TRUTH_ATTRS),
|
|
224
|
+
"actionTarget": _project_action_target(result.get("actionTarget")),
|
|
225
|
+
"uncertainty": _project_string_items(result.get("uncertainty")),
|
|
226
|
+
"warnings": _project_string_items(result.get("warnings")),
|
|
227
|
+
"screen": screen,
|
|
228
|
+
"artifacts": _project_non_empty_string_attrs(
|
|
229
|
+
_mapping(result.get("artifacts")),
|
|
230
|
+
_SEMANTIC_ARTIFACT_ATTRS,
|
|
231
|
+
),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _project_list_app_items(items_value: object) -> list[XmlScalarAttrs]:
|
|
236
|
+
if not isinstance(items_value, list):
|
|
237
|
+
return []
|
|
238
|
+
items: list[XmlScalarAttrs] = []
|
|
239
|
+
for item in items_value:
|
|
240
|
+
if not isinstance(item, Mapping):
|
|
241
|
+
continue
|
|
242
|
+
items.append(_project_attrs(item, _LIST_APP_ATTRS))
|
|
243
|
+
return items
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _project_action_target(value: object) -> XmlActionTargetProjection | None:
|
|
247
|
+
if not isinstance(value, Mapping):
|
|
248
|
+
return None
|
|
249
|
+
attrs = _project_attrs(value, _ACTION_TARGET_ATTRS)
|
|
250
|
+
return {"attrs": attrs} if attrs else None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _project_screen(screen: Mapping[str, ProjectionValue]) -> XmlScreenProjection:
|
|
254
|
+
screen_id = screen.get("screenId")
|
|
255
|
+
attrs = {"screenId": str(screen_id)}
|
|
256
|
+
surface = _mapping(screen.get("surface"))
|
|
257
|
+
surface_attrs = _project_surface_attrs(surface)
|
|
258
|
+
return {
|
|
259
|
+
"attrs": attrs,
|
|
260
|
+
"app": _project_non_empty_string_attrs(_mapping(screen.get("app")), _APP_ATTRS),
|
|
261
|
+
"surface": {
|
|
262
|
+
"attrs": surface_attrs,
|
|
263
|
+
"focus": _project_non_empty_string_attrs(
|
|
264
|
+
_mapping(surface.get("focus")),
|
|
265
|
+
_FOCUS_ATTRS,
|
|
266
|
+
),
|
|
267
|
+
},
|
|
268
|
+
"groups": _project_groups(
|
|
269
|
+
screen.get("groups"),
|
|
270
|
+
blocking_group=_projected_blocking_group(surface_attrs),
|
|
271
|
+
),
|
|
272
|
+
"omitted": _project_omitted_entries(screen.get("omitted")),
|
|
273
|
+
"visibleWindows": _project_visible_windows(screen.get("visibleWindows")),
|
|
274
|
+
"transient": _project_transient_items(screen.get("transient")),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _project_surface_attrs(surface: Mapping[str, ProjectionValue]) -> XmlScalarAttrs:
|
|
279
|
+
return _project_attr_rules(surface, _SURFACE_ATTR_RULES)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _project_groups(
|
|
283
|
+
groups_value: object,
|
|
284
|
+
*,
|
|
285
|
+
blocking_group: BlockingGroupName | None,
|
|
286
|
+
) -> list[XmlGroupProjection]:
|
|
287
|
+
entries_by_name: dict[str, list[XmlGroupItemProjection]] = {
|
|
288
|
+
group_name: [] for group_name in _GROUP_ORDER
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if isinstance(groups_value, list):
|
|
292
|
+
for item in groups_value:
|
|
293
|
+
if not isinstance(item, Mapping):
|
|
294
|
+
continue
|
|
295
|
+
group_name = item.get("name")
|
|
296
|
+
nodes = item.get("nodes")
|
|
297
|
+
if not isinstance(group_name, str) or group_name not in entries_by_name:
|
|
298
|
+
continue
|
|
299
|
+
entries_by_name[group_name] = _project_group_items(nodes)
|
|
300
|
+
group_order = list(_GROUP_ORDER)
|
|
301
|
+
if blocking_group in _BLOCKING_GROUPS:
|
|
302
|
+
group_order.remove(blocking_group)
|
|
303
|
+
group_order.insert(0, blocking_group)
|
|
304
|
+
return [
|
|
305
|
+
{
|
|
306
|
+
"name": group_name,
|
|
307
|
+
"items": entries_by_name[group_name],
|
|
308
|
+
}
|
|
309
|
+
for group_name in group_order
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _project_group_items(items_value: object) -> list[XmlGroupItemProjection]:
|
|
314
|
+
if not isinstance(items_value, list):
|
|
315
|
+
return []
|
|
316
|
+
|
|
317
|
+
items: list[XmlGroupItemProjection] = []
|
|
318
|
+
for item in items_value:
|
|
319
|
+
projected = _project_group_item(item)
|
|
320
|
+
if projected is not None:
|
|
321
|
+
items.append(projected)
|
|
322
|
+
return items
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _project_group_item(item: object) -> XmlGroupItemProjection | None:
|
|
326
|
+
if not isinstance(item, Mapping):
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
kind = _group_item_tag(item)
|
|
330
|
+
if kind == "text":
|
|
331
|
+
text_values = _project_text_values(item.get("text"))
|
|
332
|
+
if not text_values:
|
|
333
|
+
return None
|
|
334
|
+
return {
|
|
335
|
+
"tag": "literal",
|
|
336
|
+
"attrs": _project_attrs(item, _TEXT_ITEM_ATTRS),
|
|
337
|
+
"children": [],
|
|
338
|
+
"text": text_values[0],
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
tag = _node_role_tag(item.get("role"))
|
|
342
|
+
if tag is None:
|
|
343
|
+
return None
|
|
344
|
+
attrs = _project_node_attrs(item)
|
|
345
|
+
attrs.pop("role", None)
|
|
346
|
+
attrs.update(_project_attrs(item, _NODE_TEXT_ATTRS))
|
|
347
|
+
children = _project_group_items(item.get("children"))
|
|
348
|
+
if not attrs and not children:
|
|
349
|
+
return None
|
|
350
|
+
return {
|
|
351
|
+
"tag": tag,
|
|
352
|
+
"attrs": attrs,
|
|
353
|
+
"children": children,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _project_visible_windows(windows_value: object) -> list[XmlScalarAttrs]:
|
|
358
|
+
if not isinstance(windows_value, list):
|
|
359
|
+
return []
|
|
360
|
+
|
|
361
|
+
windows: list[XmlScalarAttrs] = []
|
|
362
|
+
for item in windows_value:
|
|
363
|
+
if not isinstance(item, Mapping):
|
|
364
|
+
continue
|
|
365
|
+
attrs = _project_attrs(item, _VISIBLE_WINDOW_ATTRS)
|
|
366
|
+
if attrs:
|
|
367
|
+
windows.append(attrs)
|
|
368
|
+
return windows
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _project_omitted_entries(items_value: object) -> list[XmlOmittedEntryProjection]:
|
|
372
|
+
if not isinstance(items_value, list):
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
items: list[XmlOmittedEntryProjection] = []
|
|
376
|
+
for item in items_value:
|
|
377
|
+
projected = _project_omitted_entry(item)
|
|
378
|
+
if projected is not None:
|
|
379
|
+
items.append(projected)
|
|
380
|
+
return items
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _project_omitted_entry(item: object) -> XmlOmittedEntryProjection | None:
|
|
384
|
+
if not isinstance(item, Mapping):
|
|
385
|
+
return None
|
|
386
|
+
group = _group_name(item.get("group"))
|
|
387
|
+
reason = _omitted_reason(item.get("reason"))
|
|
388
|
+
if group is None or reason is None:
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
entry: XmlOmittedEntryProjection = {
|
|
392
|
+
"group": group,
|
|
393
|
+
"reason": reason,
|
|
394
|
+
}
|
|
395
|
+
count = _count_attr(item.get("count"))
|
|
396
|
+
if count is not None:
|
|
397
|
+
entry["count"] = count
|
|
398
|
+
return entry
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _project_transient_items(
|
|
402
|
+
items_value: object,
|
|
403
|
+
) -> list[XmlTransientItemProjection]:
|
|
404
|
+
if not isinstance(items_value, list):
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
items: list[XmlTransientItemProjection] = []
|
|
408
|
+
for item in items_value:
|
|
409
|
+
projected = _project_transient_item(item)
|
|
410
|
+
if projected is not None:
|
|
411
|
+
items.append(projected)
|
|
412
|
+
return items
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _project_transient_item(item: object) -> XmlTransientItemProjection | None:
|
|
416
|
+
if not isinstance(item, Mapping):
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
text = item.get("text")
|
|
420
|
+
if not isinstance(text, str) or not text:
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
projected: XmlTransientItemProjection = {"text": text}
|
|
424
|
+
kind = _transient_kind(item.get("kind"))
|
|
425
|
+
if kind is not None:
|
|
426
|
+
projected["kind"] = kind
|
|
427
|
+
return projected
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _project_string_items(items_value: object) -> list[str]:
|
|
431
|
+
if not isinstance(items_value, list):
|
|
432
|
+
return []
|
|
433
|
+
return [str(item) for item in items_value if item is not None]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _project_text_values(value: object) -> list[str]:
|
|
437
|
+
if isinstance(value, str):
|
|
438
|
+
return [value]
|
|
439
|
+
if isinstance(value, list):
|
|
440
|
+
return [str(item) for item in value if item is not None]
|
|
441
|
+
return []
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _project_attrs(
|
|
445
|
+
value: Mapping[str, object],
|
|
446
|
+
keys: Sequence[str],
|
|
447
|
+
) -> XmlScalarAttrs:
|
|
448
|
+
attrs: XmlScalarAttrs = {}
|
|
449
|
+
for key in keys:
|
|
450
|
+
if key not in value:
|
|
451
|
+
continue
|
|
452
|
+
projected = _scalar_attr(value.get(key))
|
|
453
|
+
if projected is not None:
|
|
454
|
+
attrs[key] = projected
|
|
455
|
+
return attrs
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _project_node_attrs(value: Mapping[str, object]) -> XmlScalarAttrs:
|
|
459
|
+
attrs = _project_attrs(value, _NODE_SCALAR_ATTRS)
|
|
460
|
+
attrs.update(_project_non_empty_sequence_attrs(value, _NODE_SEQUENCE_ATTRS))
|
|
461
|
+
return attrs
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _project_non_empty_sequence_attrs(
|
|
465
|
+
value: Mapping[str, object],
|
|
466
|
+
keys: Sequence[str],
|
|
467
|
+
) -> XmlScalarAttrs:
|
|
468
|
+
attrs: XmlScalarAttrs = {}
|
|
469
|
+
for key in keys:
|
|
470
|
+
if key not in value:
|
|
471
|
+
continue
|
|
472
|
+
projected = _non_empty_sequence_attr(value.get(key))
|
|
473
|
+
if projected is not None:
|
|
474
|
+
attrs[key] = projected
|
|
475
|
+
return attrs
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _project_top_level_attrs(result: Mapping[str, object]) -> XmlScalarAttrs:
|
|
479
|
+
attrs = _project_required_attr_rules(result, _TOP_LEVEL_REQUIRED_ATTR_RULES)
|
|
480
|
+
attrs.update(_project_attr_rules(result, _TOP_LEVEL_OPTIONAL_ATTR_RULES))
|
|
481
|
+
return attrs
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _project_retained_top_level_attrs(result: Mapping[str, object]) -> XmlScalarAttrs:
|
|
485
|
+
attrs = _project_required_attr_rules(
|
|
486
|
+
result,
|
|
487
|
+
_RETAINED_TOP_LEVEL_REQUIRED_ATTR_RULES,
|
|
488
|
+
)
|
|
489
|
+
attrs.update(_project_attr_rules(result, _RETAINED_TOP_LEVEL_OPTIONAL_ATTR_RULES))
|
|
490
|
+
return attrs
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _project_non_empty_string_attrs(
|
|
494
|
+
value: Mapping[str, object],
|
|
495
|
+
keys: Sequence[str],
|
|
496
|
+
) -> XmlScalarAttrs:
|
|
497
|
+
attrs: XmlScalarAttrs = {}
|
|
498
|
+
for key in keys:
|
|
499
|
+
projected = _non_empty_string_attr(value.get(key))
|
|
500
|
+
if projected is not None:
|
|
501
|
+
attrs[key] = projected
|
|
502
|
+
return attrs
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _project_retained_details_attrs(value: Mapping[str, object]) -> XmlScalarAttrs:
|
|
506
|
+
attrs: XmlScalarAttrs = {}
|
|
507
|
+
for key in _RETAINED_DETAILS_ATTRS:
|
|
508
|
+
projected = _retained_detail_attr(key, value.get(key))
|
|
509
|
+
if projected is not None:
|
|
510
|
+
attrs[key] = projected
|
|
511
|
+
return attrs
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _project_attr_rules(
|
|
515
|
+
value: Mapping[str, object],
|
|
516
|
+
rules: Sequence[XmlAttrRule],
|
|
517
|
+
) -> XmlScalarAttrs:
|
|
518
|
+
attrs: XmlScalarAttrs = {}
|
|
519
|
+
for key, projector in rules:
|
|
520
|
+
if key not in value:
|
|
521
|
+
continue
|
|
522
|
+
projected = projector(value.get(key))
|
|
523
|
+
if projected is not None:
|
|
524
|
+
attrs[key] = projected
|
|
525
|
+
return attrs
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _project_required_attr_rules(
|
|
529
|
+
value: Mapping[str, object],
|
|
530
|
+
rules: Sequence[XmlAttrRule],
|
|
531
|
+
) -> XmlScalarAttrs:
|
|
532
|
+
attrs: XmlScalarAttrs = {}
|
|
533
|
+
for key, projector in rules:
|
|
534
|
+
projected = projector(value[key])
|
|
535
|
+
if projected is not None:
|
|
536
|
+
attrs[key] = projected
|
|
537
|
+
return attrs
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _scalar_attr(value: object) -> str | None:
|
|
541
|
+
if value is None:
|
|
542
|
+
return None
|
|
543
|
+
if isinstance(value, bool):
|
|
544
|
+
return _bool_string(value)
|
|
545
|
+
if isinstance(value, Sequence) and not isinstance(value, str):
|
|
546
|
+
return " ".join(str(item) for item in value)
|
|
547
|
+
if isinstance(value, (str, int, float)):
|
|
548
|
+
return str(value)
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _non_empty_string_attr(value: object) -> str | None:
|
|
553
|
+
if isinstance(value, str) and value:
|
|
554
|
+
return value
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _non_empty_sequence_attr(value: object) -> str | None:
|
|
559
|
+
if isinstance(value, Sequence) and not isinstance(value, str) and value:
|
|
560
|
+
return " ".join(str(item) for item in value)
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _retained_detail_attr(key: str, value: object) -> str | None:
|
|
565
|
+
if not isinstance(value, str):
|
|
566
|
+
return None
|
|
567
|
+
normalized = value.strip()
|
|
568
|
+
if not normalized or normalized != value:
|
|
569
|
+
return None
|
|
570
|
+
if key == "sourceCode":
|
|
571
|
+
return normalized if _safe_retained_source_code(normalized) else None
|
|
572
|
+
if key in {"sourceKind", "operation", "reason"}:
|
|
573
|
+
return normalized if _safe_retained_detail_slug(normalized) else None
|
|
574
|
+
if key in {"expectedReleaseVersion", "actualReleaseVersion"}:
|
|
575
|
+
return normalized if _safe_retained_release_version(normalized) else None
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _safe_retained_source_code(value: str) -> bool:
|
|
580
|
+
if len(value) > 64:
|
|
581
|
+
return False
|
|
582
|
+
if _DEVICE_SERIAL_DETAIL_RE.fullmatch(value):
|
|
583
|
+
return False
|
|
584
|
+
if _FINGERPRINT_DETAIL_RE.fullmatch(value.lower()):
|
|
585
|
+
return False
|
|
586
|
+
return _RETAINED_SOURCE_CODE_RE.fullmatch(value) is not None
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _safe_retained_detail_slug(value: str) -> bool:
|
|
590
|
+
if not _RETAINED_DETAIL_SLUG_RE.fullmatch(value):
|
|
591
|
+
return False
|
|
592
|
+
lower = value.lower()
|
|
593
|
+
if _DEVICE_SERIAL_DETAIL_RE.fullmatch(value):
|
|
594
|
+
return False
|
|
595
|
+
if _FINGERPRINT_DETAIL_RE.fullmatch(lower):
|
|
596
|
+
return False
|
|
597
|
+
if "token" in lower and lower not in _SAFE_TOKEN_DETAIL_VALUES:
|
|
598
|
+
return False
|
|
599
|
+
if any(
|
|
600
|
+
marker in lower
|
|
601
|
+
for marker in (
|
|
602
|
+
"bearer",
|
|
603
|
+
"://",
|
|
604
|
+
"www.",
|
|
605
|
+
".androidctl",
|
|
606
|
+
"artifact-root",
|
|
607
|
+
"artifact_path",
|
|
608
|
+
"artifact-path",
|
|
609
|
+
"raw-rid",
|
|
610
|
+
"raw_rid",
|
|
611
|
+
"rawrid",
|
|
612
|
+
"snapshot",
|
|
613
|
+
"fingerprint",
|
|
614
|
+
)
|
|
615
|
+
):
|
|
616
|
+
return False
|
|
617
|
+
return not lower.startswith(("rid-", "rid_", "snapshot-", "snapshot_"))
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _safe_retained_release_version(value: str) -> bool:
|
|
621
|
+
if len(value) > 32:
|
|
622
|
+
return False
|
|
623
|
+
return _RETAINED_RELEASE_VERSION_RE.fullmatch(value) is not None
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _required_ok_attr(value: object) -> str | None:
|
|
627
|
+
return _scalar_attr(value) or "false"
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _required_stringified_attr(value: object) -> str | None:
|
|
631
|
+
return str(value)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _bool_attr(value: object) -> str | None:
|
|
635
|
+
if isinstance(value, bool):
|
|
636
|
+
return _bool_string(value)
|
|
637
|
+
return None
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _blocking_group_attr(value: object) -> str | None:
|
|
641
|
+
if isinstance(value, str) and value in _BLOCKING_GROUPS:
|
|
642
|
+
return value
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _group_name(value: object) -> XmlGroupName | None:
|
|
647
|
+
if isinstance(value, str) and value in _GROUP_ORDER:
|
|
648
|
+
return cast(XmlGroupName, value)
|
|
649
|
+
return None
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _omitted_reason(value: object) -> OmittedReason | None:
|
|
653
|
+
if isinstance(value, str) and value in _OMITTED_REASONS:
|
|
654
|
+
return cast(OmittedReason, value)
|
|
655
|
+
return None
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def _transient_kind(value: object) -> TransientKind | None:
|
|
659
|
+
if isinstance(value, str) and value in _TRANSIENT_KINDS:
|
|
660
|
+
return cast(TransientKind, value)
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _count_attr(value: object) -> str | None:
|
|
665
|
+
if isinstance(value, bool):
|
|
666
|
+
return None
|
|
667
|
+
if isinstance(value, int) and value >= 0:
|
|
668
|
+
return str(value)
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
_TOP_LEVEL_REQUIRED_ATTR_RULES: tuple[XmlAttrRule, ...] = (
|
|
673
|
+
("ok", _required_ok_attr),
|
|
674
|
+
("command", _required_stringified_attr),
|
|
675
|
+
("category", _required_stringified_attr),
|
|
676
|
+
("payloadMode", _required_stringified_attr),
|
|
677
|
+
)
|
|
678
|
+
_TOP_LEVEL_OPTIONAL_ATTR_RULES: tuple[XmlAttrRule, ...] = (
|
|
679
|
+
("sourceScreenId", _non_empty_string_attr),
|
|
680
|
+
("nextScreenId", _non_empty_string_attr),
|
|
681
|
+
("code", _non_empty_string_attr),
|
|
682
|
+
)
|
|
683
|
+
_RETAINED_TOP_LEVEL_REQUIRED_ATTR_RULES: tuple[XmlAttrRule, ...] = (
|
|
684
|
+
("ok", _required_ok_attr),
|
|
685
|
+
("command", _required_stringified_attr),
|
|
686
|
+
("envelope", _required_stringified_attr),
|
|
687
|
+
)
|
|
688
|
+
_RETAINED_TOP_LEVEL_OPTIONAL_ATTR_RULES: tuple[XmlAttrRule, ...] = (
|
|
689
|
+
("code", _non_empty_string_attr),
|
|
690
|
+
)
|
|
691
|
+
_SURFACE_ATTR_RULES: tuple[XmlAttrRule, ...] = (
|
|
692
|
+
("keyboardVisible", _bool_attr),
|
|
693
|
+
("blockingGroup", _blocking_group_attr),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _bool_string(value: bool) -> str:
|
|
698
|
+
return "true" if value else "false"
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _projected_blocking_group(
|
|
702
|
+
surface_attrs: XmlScalarAttrs,
|
|
703
|
+
) -> BlockingGroupName | None:
|
|
704
|
+
blocking_group = surface_attrs.get("blockingGroup")
|
|
705
|
+
if blocking_group == "dialog":
|
|
706
|
+
return "dialog"
|
|
707
|
+
if blocking_group == "keyboard":
|
|
708
|
+
return "keyboard"
|
|
709
|
+
if blocking_group == "system":
|
|
710
|
+
return "system"
|
|
711
|
+
return None
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _mapping(value: object) -> Mapping[str, ProjectionValue]:
|
|
715
|
+
if not isinstance(value, Mapping):
|
|
716
|
+
return {}
|
|
717
|
+
return {key: item for key, item in value.items() if isinstance(key, str)}
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _group_item_tag(item: Mapping[str, object]) -> XmlGroupItemTag:
|
|
721
|
+
kind = item.get("kind")
|
|
722
|
+
if kind == "container":
|
|
723
|
+
return "container"
|
|
724
|
+
if kind == "text":
|
|
725
|
+
return "text"
|
|
726
|
+
return "node"
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _node_role_tag(role: object) -> str | None:
|
|
730
|
+
if isinstance(role, str) and role in _NODE_ROLE_TAGS:
|
|
731
|
+
return role
|
|
732
|
+
return None
|