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,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
|
+
)
|