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.
Files changed (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. androidctld/waits/matcher.py +41 -0
@@ -0,0 +1,177 @@
1
+ """Focus confirmation helpers for post-action validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass
7
+
8
+ from androidctld.actions.type_confirmation import (
9
+ TypeConfirmationContext,
10
+ fingerprint_rematch_confirmation_node,
11
+ resolved_target_handle,
12
+ reused_ref_confirmation_node,
13
+ snapshot_node_for_handle,
14
+ )
15
+ from androidctld.commands.command_models import FocusCommand
16
+ from androidctld.device.types import ResolvedTarget
17
+ from androidctld.errors import DaemonError, DaemonErrorCode
18
+ from androidctld.refs.models import NodeHandle, RefBinding
19
+ from androidctld.runtime.models import WorkspaceRuntime
20
+ from androidctld.snapshots.models import RawNode, RawSnapshot
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class FocusConfirmationCandidate:
25
+ strategy: str
26
+ node: RawNode
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class FocusConfirmationOutcome:
31
+ strategy: str
32
+ node: RawNode
33
+ target_handle: NodeHandle
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class FocusConfirmationContext:
38
+ request_handle: NodeHandle | None
39
+ binding: RefBinding | None
40
+ resolved_target: ResolvedTarget | None
41
+
42
+
43
+ def build_focus_confirmation_context(
44
+ session: WorkspaceRuntime,
45
+ command: FocusCommand,
46
+ request_handle: NodeHandle | None,
47
+ ) -> FocusConfirmationContext:
48
+ binding = session.ref_registry.get(command.ref)
49
+ return FocusConfirmationContext(
50
+ request_handle=request_handle,
51
+ binding=None if binding is None else deepcopy(binding),
52
+ resolved_target=None,
53
+ )
54
+
55
+
56
+ def validate_focus_confirmation(
57
+ *,
58
+ session: WorkspaceRuntime,
59
+ previous_snapshot: RawSnapshot | None,
60
+ snapshot: RawSnapshot,
61
+ context: FocusConfirmationContext,
62
+ ) -> FocusConfirmationOutcome:
63
+ previous_candidates = focus_confirmation_candidates(
64
+ session=session,
65
+ snapshot=previous_snapshot,
66
+ context=context,
67
+ )
68
+ candidates = focus_confirmation_candidates(
69
+ session=session,
70
+ snapshot=snapshot,
71
+ context=context,
72
+ )
73
+ candidate = first_valid_focus_candidate(candidates)
74
+ if candidate is None and not candidates:
75
+ raise DaemonError(
76
+ code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
77
+ message="focus did not expose a resolvable target handle",
78
+ retryable=True,
79
+ details={},
80
+ http_status=200,
81
+ )
82
+ previous_candidate = previous_candidate_for_strategy(
83
+ previous_candidates,
84
+ strategy=None if candidate is None else candidate.strategy,
85
+ )
86
+ if previous_candidate is not None and previous_candidate.node.focused:
87
+ raise DaemonError(
88
+ code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
89
+ message="focus target was already focused before refresh",
90
+ retryable=True,
91
+ details={"reason": "already_focused"},
92
+ http_status=200,
93
+ )
94
+ if candidate is None:
95
+ raise DaemonError(
96
+ code=DaemonErrorCode.TARGET_NOT_ACTIONABLE,
97
+ message="focus did not land on the requested input target",
98
+ retryable=True,
99
+ details={},
100
+ http_status=200,
101
+ )
102
+ return FocusConfirmationOutcome(
103
+ strategy=candidate.strategy,
104
+ node=candidate.node,
105
+ target_handle=NodeHandle(
106
+ snapshot_id=snapshot.snapshot_id,
107
+ rid=candidate.node.rid,
108
+ ),
109
+ )
110
+
111
+
112
+ def focus_confirmation_candidates(
113
+ *,
114
+ session: WorkspaceRuntime,
115
+ snapshot: RawSnapshot | None,
116
+ context: FocusConfirmationContext,
117
+ ) -> list[FocusConfirmationCandidate]:
118
+ if snapshot is None:
119
+ return []
120
+ type_context = TypeConfirmationContext(
121
+ ref=None if context.binding is None else context.binding.ref,
122
+ request_handle=context.request_handle,
123
+ binding=context.binding,
124
+ )
125
+ candidates: list[FocusConfirmationCandidate] = []
126
+ seen_rids: set[str] = set()
127
+
128
+ def add_candidate(strategy: str, node: RawNode | None) -> None:
129
+ if node is None or node.rid in seen_rids:
130
+ return
131
+ seen_rids.add(node.rid)
132
+ candidates.append(FocusConfirmationCandidate(strategy=strategy, node=node))
133
+
134
+ add_candidate(
135
+ "resolvedTarget",
136
+ snapshot_node_for_handle(
137
+ snapshot,
138
+ (
139
+ None
140
+ if context.resolved_target is None
141
+ else resolved_target_handle(context.resolved_target)
142
+ ),
143
+ ),
144
+ )
145
+ add_candidate(
146
+ "requestTarget", snapshot_node_for_handle(snapshot, context.request_handle)
147
+ )
148
+ add_candidate(
149
+ "reusedRef", reused_ref_confirmation_node(session, snapshot, type_context)
150
+ )
151
+ add_candidate(
152
+ "fingerprintRematch",
153
+ fingerprint_rematch_confirmation_node(session, snapshot, type_context),
154
+ )
155
+ return candidates
156
+
157
+
158
+ def first_valid_focus_candidate(
159
+ candidates: list[FocusConfirmationCandidate],
160
+ ) -> FocusConfirmationCandidate | None:
161
+ for candidate in candidates:
162
+ if candidate.node.focused and candidate.node.editable:
163
+ return candidate
164
+ return None
165
+
166
+
167
+ def previous_candidate_for_strategy(
168
+ candidates: list[FocusConfirmationCandidate],
169
+ *,
170
+ strategy: str | None,
171
+ ) -> FocusConfirmationCandidate | None:
172
+ if strategy is None:
173
+ return None
174
+ for candidate in candidates:
175
+ if candidate.strategy == strategy:
176
+ return candidate
177
+ return None
@@ -0,0 +1,120 @@
1
+ """Pure focused-input admission predicates for action validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from androidctld.semantics.compiler import CompiledScreen, SemanticNode
6
+ from androidctld.semantics.public_models import PublicNode, PublicScreen
7
+
8
+
9
+ def public_focused_input_ref(screen: PublicScreen) -> str | None:
10
+ return screen.surface.focus.input_ref
11
+
12
+
13
+ def semantic_focused_input_ref(screen: CompiledScreen) -> str | None:
14
+ return screen.focused_input_ref()
15
+
16
+
17
+ def public_node_is_focused_input(
18
+ screen: PublicScreen,
19
+ node: PublicNode,
20
+ ) -> bool:
21
+ return (
22
+ node.role == "input"
23
+ and node.ref is not None
24
+ and public_focused_input_ref(screen) == node.ref
25
+ )
26
+
27
+
28
+ def semantic_node_is_focused_input(
29
+ screen: CompiledScreen,
30
+ node: SemanticNode,
31
+ ) -> bool:
32
+ focused_node = screen.focused_input_node()
33
+ return (
34
+ node.role == "input"
35
+ and focused_node is not None
36
+ and focused_node.raw_rid == node.raw_rid
37
+ )
38
+
39
+
40
+ def submit_subject_is_cross_checked_focused_input(
41
+ public_screen: PublicScreen,
42
+ compiled_screen: CompiledScreen,
43
+ public_node: PublicNode,
44
+ semantic_node: SemanticNode,
45
+ ) -> bool:
46
+ return public_node_is_focused_input(
47
+ public_screen,
48
+ public_node,
49
+ ) and semantic_node_is_focused_input(compiled_screen, semantic_node)
50
+
51
+
52
+ def keyboard_blocker_allows_public_type(
53
+ *,
54
+ blocking_group: str | None,
55
+ action: str,
56
+ screen: PublicScreen,
57
+ node: PublicNode,
58
+ ) -> bool:
59
+ return (
60
+ blocking_group == "keyboard"
61
+ and action == "type"
62
+ and public_node_is_focused_input(screen, node)
63
+ )
64
+
65
+
66
+ def keyboard_blocker_allows_semantic_type(
67
+ *,
68
+ blocking_group: str | None,
69
+ action: str,
70
+ screen: CompiledScreen,
71
+ node: SemanticNode,
72
+ ) -> bool:
73
+ return (
74
+ blocking_group == "keyboard"
75
+ and action == "type"
76
+ and semantic_node_is_focused_input(screen, node)
77
+ )
78
+
79
+
80
+ def keyboard_blocker_allows_submit_subject(
81
+ *,
82
+ blocking_group: str | None,
83
+ public_screen: PublicScreen,
84
+ compiled_screen: CompiledScreen,
85
+ public_node: PublicNode,
86
+ semantic_node: SemanticNode,
87
+ ) -> bool:
88
+ return (
89
+ blocking_group == "keyboard"
90
+ and submit_subject_is_cross_checked_focused_input(
91
+ public_screen,
92
+ compiled_screen,
93
+ public_node,
94
+ semantic_node,
95
+ )
96
+ )
97
+
98
+
99
+ def blocked_by_group_fields(
100
+ *,
101
+ blocking_group: str,
102
+ ref: str | None,
103
+ ) -> dict[str, object]:
104
+ return {
105
+ "reason": f"blocked_by_{blocking_group}",
106
+ "ref": ref,
107
+ "blockingGroup": blocking_group,
108
+ }
109
+
110
+
111
+ def focus_mismatch_fields(
112
+ *,
113
+ ref: str | None,
114
+ focused_input_ref: str | None,
115
+ ) -> dict[str, object]:
116
+ return {
117
+ "reason": "focus_mismatch",
118
+ "ref": ref,
119
+ "focusedInputRef": focused_input_ref,
120
+ }
@@ -0,0 +1,176 @@
1
+ """Fresh-current evidence checks for post-dispatch global actions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from androidctld.errors import DaemonError, DaemonErrorCode
9
+ from androidctld.semantics.compiler import CompiledScreen, SemanticNode
10
+ from androidctld.snapshots.models import RawSnapshot
11
+ from androidctld.text_equivalence import canonical_text_key, searchable_raw_node_texts
12
+
13
+ _SYSTEMUI_PACKAGE_PREFIX = "com.android.systemui"
14
+ _SYSTEM_EVIDENCE_REQUIRED_ACTIONS = frozenset({"recents", "notifications"})
15
+ _APP_SURFACE_GROUPS = ("targets", "context", "dialog", "keyboard")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class GlobalFreshCurrentBaseline:
20
+ action: str
21
+ snapshot_identity: tuple[int, str] | None
22
+ app_signature: tuple[Any, ...] | None
23
+
24
+
25
+ def capture_global_fresh_current_baseline(
26
+ *,
27
+ action: str,
28
+ snapshot: RawSnapshot | None,
29
+ compiled_screen: CompiledScreen | None,
30
+ ) -> GlobalFreshCurrentBaseline:
31
+ return GlobalFreshCurrentBaseline(
32
+ action=action,
33
+ snapshot_identity=None if snapshot is None else _snapshot_identity(snapshot),
34
+ app_signature=(
35
+ None
36
+ if snapshot is None
37
+ else _fresh_current_app_signature(compiled_screen, snapshot)
38
+ ),
39
+ )
40
+
41
+
42
+ def validate_global_fresh_current_evidence(
43
+ baseline: GlobalFreshCurrentBaseline,
44
+ *,
45
+ snapshot: RawSnapshot,
46
+ compiled_screen: CompiledScreen,
47
+ ) -> None:
48
+ if baseline.snapshot_identity is None:
49
+ return
50
+ if baseline.snapshot_identity == _snapshot_identity(snapshot):
51
+ raise _fresh_current_error(
52
+ baseline.action,
53
+ reason="post_action_snapshot_identity_unchanged",
54
+ )
55
+ if baseline.action not in _SYSTEM_EVIDENCE_REQUIRED_ACTIONS:
56
+ return
57
+ candidate_signature = _fresh_current_app_signature(compiled_screen, snapshot)
58
+ if (
59
+ baseline.app_signature is not None
60
+ and candidate_signature != baseline.app_signature
61
+ ):
62
+ return
63
+ if _has_real_systemui_entry(snapshot, compiled_screen):
64
+ return
65
+ raise _fresh_current_error(
66
+ baseline.action,
67
+ reason="post_action_system_evidence_missing",
68
+ )
69
+
70
+
71
+ def _snapshot_identity(snapshot: RawSnapshot) -> tuple[int, str]:
72
+ return snapshot.snapshot_id, snapshot.captured_at
73
+
74
+
75
+ def _fresh_current_app_signature(
76
+ compiled_screen: CompiledScreen | None,
77
+ snapshot: RawSnapshot,
78
+ ) -> tuple[Any, ...]:
79
+ if compiled_screen is None:
80
+ return _raw_app_surface_signature(snapshot)
81
+
82
+ raw_actions_by_rid = {
83
+ node.rid: tuple(node.actions)
84
+ for node in snapshot.nodes
85
+ if not _is_systemui_package(node.package_name)
86
+ }
87
+ return (
88
+ compiled_screen.package_name,
89
+ compiled_screen.activity_name,
90
+ compiled_screen.keyboard_visible,
91
+ tuple(
92
+ _compiled_app_node_signature(
93
+ group_name,
94
+ node,
95
+ raw_actions=raw_actions_by_rid.get(node.raw_rid),
96
+ )
97
+ for group_name in _APP_SURFACE_GROUPS
98
+ for node in getattr(compiled_screen, group_name)
99
+ ),
100
+ )
101
+
102
+
103
+ def _compiled_app_node_signature(
104
+ group_name: str,
105
+ node: SemanticNode,
106
+ *,
107
+ raw_actions: tuple[str, ...] | None,
108
+ ) -> tuple[Any, ...]:
109
+ return (
110
+ group_name,
111
+ node.role,
112
+ canonical_text_key(node.label),
113
+ tuple(canonical_text_key(value) for value in node.state),
114
+ tuple(node.actions) if raw_actions is None else raw_actions,
115
+ None if node.bounds is None else tuple(node.bounds),
116
+ )
117
+
118
+
119
+ def _raw_app_surface_signature(snapshot: RawSnapshot) -> tuple[Any, ...]:
120
+ return (
121
+ snapshot.package_name,
122
+ snapshot.activity_name,
123
+ snapshot.ime.visible,
124
+ tuple(
125
+ (
126
+ node.class_name,
127
+ canonical_text_key(node.resource_id),
128
+ tuple(
129
+ canonical_text_key(value)
130
+ for value in searchable_raw_node_texts(node)
131
+ ),
132
+ (
133
+ node.enabled,
134
+ node.editable,
135
+ node.focusable,
136
+ node.focused,
137
+ node.checkable,
138
+ node.checked,
139
+ node.selected,
140
+ node.scrollable,
141
+ ),
142
+ tuple(node.actions),
143
+ tuple(node.bounds),
144
+ )
145
+ for node in snapshot.nodes
146
+ if node.visible_to_user and not _is_systemui_package(node.package_name)
147
+ ),
148
+ )
149
+
150
+
151
+ def _has_real_systemui_entry(
152
+ snapshot: RawSnapshot,
153
+ compiled_screen: CompiledScreen,
154
+ ) -> bool:
155
+ if _is_systemui_package(compiled_screen.package_name):
156
+ return True
157
+ return _is_systemui_package(snapshot.package_name)
158
+
159
+
160
+ def _is_systemui_package(package_name: str | None) -> bool:
161
+ return isinstance(package_name, str) and package_name.startswith(
162
+ _SYSTEMUI_PACKAGE_PREFIX
163
+ )
164
+
165
+
166
+ def _fresh_current_error(action: str, *, reason: str) -> DaemonError:
167
+ return DaemonError(
168
+ code=DaemonErrorCode.SCREEN_NOT_READY,
169
+ message="No fresh current screen observation is available after global action.",
170
+ retryable=True,
171
+ details={
172
+ "reason": reason,
173
+ "globalAction": action,
174
+ },
175
+ http_status=200,
176
+ )