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,439 @@
1
+ """Semantic action command handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from androidctl_contracts.vocabulary import SemanticResultCode
6
+ from androidctld.actions.executor import ActionExecutionFailure, ActionExecutor
7
+ from androidctld.commands.command_models import (
8
+ GlobalCommand,
9
+ OpenCommand,
10
+ RefBoundActionCommand,
11
+ )
12
+ from androidctld.commands.orchestration import current_command_record
13
+ from androidctld.commands.result_models import (
14
+ SemanticResultAssemblyInput,
15
+ build_semantic_failure_result,
16
+ build_semantic_success_result,
17
+ )
18
+ from androidctld.commands.semantic_command_names import (
19
+ semantic_result_command_for_daemon_kind,
20
+ )
21
+ from androidctld.commands.semantic_error_mapping import (
22
+ SemanticFailure,
23
+ map_daemon_error_to_semantic_failure,
24
+ )
25
+ from androidctld.commands.semantic_truth import (
26
+ capture_runtime_source_basis,
27
+ resolve_global_action_source_basis,
28
+ resolve_open_changed,
29
+ resolve_runtime_continuity,
30
+ )
31
+ from androidctld.errors import DaemonError, DaemonErrorCode
32
+ from androidctld.runtime import RuntimeKernel
33
+ from androidctld.runtime.models import WorkspaceRuntime
34
+ from androidctld.runtime.screen_state import (
35
+ current_artifacts,
36
+ current_public_screen,
37
+ get_authoritative_current_basis,
38
+ )
39
+ from androidctld.semantics.compiler import CompiledScreen
40
+
41
+ _AVAILABILITY_CODES = {
42
+ DaemonErrorCode.RUNTIME_NOT_CONNECTED,
43
+ DaemonErrorCode.SCREEN_NOT_READY,
44
+ DaemonErrorCode.DEVICE_DISCONNECTED,
45
+ DaemonErrorCode.DEVICE_AGENT_UNAVAILABLE,
46
+ DaemonErrorCode.DEVICE_AGENT_UNAUTHORIZED,
47
+ DaemonErrorCode.DEVICE_RPC_FAILED,
48
+ DaemonErrorCode.DEVICE_RPC_TRANSPORT_RESET,
49
+ }
50
+ _CONFIRMATION_FAILURE_CODES = {
51
+ SemanticResultCode.ACTION_NOT_CONFIRMED,
52
+ SemanticResultCode.TYPE_NOT_CONFIRMED,
53
+ SemanticResultCode.SUBMIT_NOT_CONFIRMED,
54
+ }
55
+ _POST_ACTION_OBSERVATION_LOST_MESSAGE = (
56
+ "Action may have been dispatched, but no current screen truth is available."
57
+ )
58
+
59
+
60
+ def _command_result_payload(
61
+ *,
62
+ command_name: str,
63
+ category: str,
64
+ source_screen_id: str | None,
65
+ source_compiled_screen: CompiledScreen | None,
66
+ runtime: WorkspaceRuntime,
67
+ assembly_input: SemanticResultAssemblyInput | None = None,
68
+ require_compiled_source_for_changed: bool = False,
69
+ continuity_status_override: str | None = None,
70
+ changed_override: bool | None = None,
71
+ ) -> dict[str, object]:
72
+ next_screen = current_public_screen(runtime)
73
+ artifacts = current_artifacts(runtime)
74
+ if continuity_status_override is None and changed_override is None:
75
+ continuity_status, changed = _resolve_continuity(
76
+ category=category,
77
+ source_screen_id=source_screen_id,
78
+ source_compiled_screen=source_compiled_screen,
79
+ runtime=runtime,
80
+ require_compiled_source_for_changed=require_compiled_source_for_changed,
81
+ )
82
+ else:
83
+ continuity_status = (
84
+ continuity_status_override
85
+ if continuity_status_override is not None
86
+ else "none"
87
+ )
88
+ changed = changed_override
89
+ return build_semantic_success_result(
90
+ command=command_name,
91
+ category=category,
92
+ source_screen_id=source_screen_id,
93
+ next_screen=next_screen,
94
+ app_payload=(None if assembly_input is None else assembly_input.app_payload),
95
+ artifacts=artifacts,
96
+ continuity_status=continuity_status,
97
+ execution_outcome=(
98
+ "dispatched"
99
+ if assembly_input is None
100
+ else getattr(assembly_input, "execution_outcome", "dispatched")
101
+ ),
102
+ changed=changed,
103
+ action_target=(
104
+ None
105
+ if assembly_input is None
106
+ else getattr(assembly_input, "action_target", None)
107
+ ),
108
+ warnings=([] if assembly_input is None else list(assembly_input.warnings)),
109
+ ).model_dump(by_alias=True, mode="json")
110
+
111
+
112
+ def _command_error_payload(
113
+ *,
114
+ command_name: str,
115
+ category: str,
116
+ source_screen_id: str | None,
117
+ source_compiled_screen: CompiledScreen | None,
118
+ runtime: WorkspaceRuntime,
119
+ error: DaemonError,
120
+ require_compiled_source_for_changed: bool = False,
121
+ continuity_status_override: str | None = None,
122
+ changed_override: bool | None = None,
123
+ dispatch_attempted: bool = False,
124
+ truth_lost_after_dispatch: bool = False,
125
+ ) -> dict[str, object]:
126
+ mapped = map_daemon_error_to_semantic_failure(
127
+ command_name=command_name,
128
+ error=error,
129
+ truth_lost_after_dispatch=truth_lost_after_dispatch,
130
+ )
131
+ if mapped is None:
132
+ raise error
133
+ if mapped.code in _CONFIRMATION_FAILURE_CODES:
134
+ if truth_lost_after_dispatch:
135
+ basis = None
136
+ else:
137
+ basis = get_authoritative_current_basis(runtime)
138
+ if basis is None:
139
+ next_screen = None
140
+ artifacts = None
141
+ mapped = SemanticFailure(
142
+ code=SemanticResultCode.POST_ACTION_OBSERVATION_LOST,
143
+ message=_POST_ACTION_OBSERVATION_LOST_MESSAGE,
144
+ )
145
+ else:
146
+ next_screen = basis.public_screen
147
+ artifacts = basis.artifacts
148
+ else:
149
+ next_screen = (
150
+ None if truth_lost_after_dispatch else current_public_screen(runtime)
151
+ )
152
+ artifacts = current_artifacts(runtime)
153
+ if continuity_status_override is None and changed_override is None:
154
+ continuity_status, changed = _resolve_continuity(
155
+ category=category,
156
+ source_screen_id=source_screen_id,
157
+ source_compiled_screen=source_compiled_screen,
158
+ runtime=runtime,
159
+ require_compiled_source_for_changed=require_compiled_source_for_changed,
160
+ )
161
+ else:
162
+ continuity_status = (
163
+ continuity_status_override
164
+ if continuity_status_override is not None
165
+ else "none"
166
+ )
167
+ changed = changed_override
168
+ if (
169
+ mapped.continuity_status_override is not None
170
+ and next_screen is not None
171
+ and source_screen_id is not None
172
+ ):
173
+ continuity_status = mapped.continuity_status_override
174
+ return build_semantic_failure_result(
175
+ command=command_name,
176
+ category=category,
177
+ code=mapped.code,
178
+ message=mapped.message,
179
+ execution_outcome=_map_failure_execution_outcome(
180
+ error,
181
+ dispatch_attempted=dispatch_attempted,
182
+ truth_lost_after_dispatch=truth_lost_after_dispatch,
183
+ ),
184
+ source_screen_id=source_screen_id,
185
+ current_screen=next_screen,
186
+ artifacts=artifacts,
187
+ continuity_status=continuity_status,
188
+ observation_quality="authoritative" if next_screen is not None else "none",
189
+ changed=changed,
190
+ ).model_dump(by_alias=True, mode="json")
191
+
192
+
193
+ def _map_failure_execution_outcome(
194
+ error: DaemonError,
195
+ *,
196
+ dispatch_attempted: bool = False,
197
+ truth_lost_after_dispatch: bool = False,
198
+ ) -> str:
199
+ if truth_lost_after_dispatch:
200
+ return "dispatched"
201
+ if dispatch_attempted and error.code == DaemonErrorCode.DEVICE_AGENT_UNAUTHORIZED:
202
+ return "dispatched"
203
+ if (
204
+ error.code
205
+ in {
206
+ DaemonErrorCode.DAEMON_BAD_REQUEST,
207
+ DaemonErrorCode.RUNTIME_BUSY,
208
+ DaemonErrorCode.REF_RESOLUTION_FAILED,
209
+ DaemonErrorCode.REF_STALE,
210
+ DaemonErrorCode.TARGET_BLOCKED,
211
+ DaemonErrorCode.DEVICE_AGENT_CAPABILITY_MISMATCH,
212
+ DaemonErrorCode.ACCESSIBILITY_NOT_READY,
213
+ }
214
+ | _AVAILABILITY_CODES
215
+ ):
216
+ return "notAttempted"
217
+ if error.code == DaemonErrorCode.TARGET_NOT_ACTIONABLE:
218
+ return "dispatched" if dispatch_attempted else "notAttempted"
219
+ if error.code in {
220
+ DaemonErrorCode.OPEN_FAILED,
221
+ DaemonErrorCode.ACTION_NOT_CONFIRMED,
222
+ DaemonErrorCode.TYPE_NOT_CONFIRMED,
223
+ DaemonErrorCode.SUBMIT_NOT_CONFIRMED,
224
+ }:
225
+ return "dispatched"
226
+ return "unknown"
227
+
228
+
229
+ def _resolve_continuity(
230
+ *,
231
+ category: str,
232
+ source_screen_id: str | None,
233
+ source_compiled_screen: CompiledScreen | None,
234
+ runtime: WorkspaceRuntime,
235
+ require_compiled_source_for_changed: bool = False,
236
+ ) -> tuple[str, bool | None]:
237
+ if category == "open" or source_screen_id is None:
238
+ return "none", None
239
+ continuity = resolve_runtime_continuity(
240
+ runtime=runtime,
241
+ source_screen_id=source_screen_id,
242
+ source_compiled_screen=source_compiled_screen,
243
+ )
244
+ changed = (
245
+ None
246
+ if require_compiled_source_for_changed and source_compiled_screen is None
247
+ else continuity.changed
248
+ )
249
+ return continuity.continuity_status, changed
250
+
251
+
252
+ class ActionCommandHandler:
253
+ def __init__(
254
+ self,
255
+ *,
256
+ runtime_kernel: RuntimeKernel,
257
+ action_executor: ActionExecutor,
258
+ ) -> None:
259
+ self._runtime_kernel = runtime_kernel
260
+ self._action_executor = action_executor
261
+
262
+ def handle_ref_action(
263
+ self,
264
+ *,
265
+ command: RefBoundActionCommand,
266
+ ) -> dict[str, object]:
267
+ runtime = self._runtime_kernel.ensure_runtime()
268
+ self._runtime_kernel.normalize_stale_ready_runtime(runtime)
269
+ lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
270
+ source_compiled_screen = capture_runtime_source_basis(
271
+ runtime=runtime
272
+ ).source_compiled_screen
273
+ try:
274
+ result = self._action_executor.execute(
275
+ runtime,
276
+ current_command_record(
277
+ kind=command.kind,
278
+ result_command=semantic_result_command_for_daemon_kind(
279
+ command.kind
280
+ ),
281
+ ),
282
+ command,
283
+ lifecycle_lease,
284
+ )
285
+ except ActionExecutionFailure as failure:
286
+ return _command_error_payload(
287
+ command_name=semantic_result_command_for_daemon_kind(command.kind),
288
+ category="transition",
289
+ source_screen_id=command.source_screen_id,
290
+ source_compiled_screen=source_compiled_screen,
291
+ runtime=runtime,
292
+ error=failure.normalized_error,
293
+ dispatch_attempted=failure.dispatch_attempted,
294
+ truth_lost_after_dispatch=failure.truth_lost_after_dispatch,
295
+ )
296
+ except DaemonError as error:
297
+ return _command_error_payload(
298
+ command_name=semantic_result_command_for_daemon_kind(command.kind),
299
+ category="transition",
300
+ source_screen_id=command.source_screen_id,
301
+ source_compiled_screen=source_compiled_screen,
302
+ runtime=runtime,
303
+ error=error,
304
+ )
305
+ return _command_result_payload(
306
+ command_name=semantic_result_command_for_daemon_kind(command.kind),
307
+ category="transition",
308
+ source_screen_id=command.source_screen_id,
309
+ source_compiled_screen=source_compiled_screen,
310
+ runtime=runtime,
311
+ assembly_input=result,
312
+ )
313
+
314
+ def handle_global_action(
315
+ self,
316
+ *,
317
+ command: GlobalCommand,
318
+ ) -> dict[str, object]:
319
+ runtime = self._runtime_kernel.ensure_runtime()
320
+ lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
321
+ source_basis = resolve_global_action_source_basis(
322
+ runtime=runtime,
323
+ source_screen_id=command.source_screen_id,
324
+ )
325
+ try:
326
+ result = self._action_executor.execute(
327
+ runtime,
328
+ current_command_record(
329
+ kind=command.kind,
330
+ result_command=command.action,
331
+ ),
332
+ command,
333
+ lifecycle_lease,
334
+ )
335
+ except ActionExecutionFailure as failure:
336
+ return _command_error_payload(
337
+ command_name=command.action,
338
+ category="transition",
339
+ source_screen_id=source_basis.source_screen_id,
340
+ source_compiled_screen=source_basis.source_compiled_screen,
341
+ runtime=runtime,
342
+ error=failure.normalized_error,
343
+ require_compiled_source_for_changed=True,
344
+ dispatch_attempted=failure.dispatch_attempted,
345
+ truth_lost_after_dispatch=failure.truth_lost_after_dispatch,
346
+ )
347
+ except DaemonError as error:
348
+ return _command_error_payload(
349
+ command_name=command.action,
350
+ category="transition",
351
+ source_screen_id=source_basis.source_screen_id,
352
+ source_compiled_screen=source_basis.source_compiled_screen,
353
+ runtime=runtime,
354
+ error=error,
355
+ require_compiled_source_for_changed=True,
356
+ )
357
+ return _command_result_payload(
358
+ command_name=command.action,
359
+ category="transition",
360
+ source_screen_id=source_basis.source_screen_id,
361
+ source_compiled_screen=source_basis.source_compiled_screen,
362
+ runtime=runtime,
363
+ assembly_input=result,
364
+ require_compiled_source_for_changed=True,
365
+ )
366
+
367
+ def handle_open(
368
+ self,
369
+ *,
370
+ command: OpenCommand,
371
+ source_screen_id: str | None = None,
372
+ ) -> dict[str, object]:
373
+ runtime = self._runtime_kernel.ensure_runtime()
374
+ lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
375
+ source_basis = capture_runtime_source_basis(runtime=runtime)
376
+ source_compiled_screen = source_basis.source_compiled_screen
377
+ if source_screen_id is None:
378
+ if source_basis.source_screen_id is not None:
379
+ source_screen_id = source_basis.source_screen_id
380
+ else:
381
+ current_screen = current_public_screen(runtime)
382
+ source_screen_id = (
383
+ None if current_screen is None else current_screen.screen_id
384
+ )
385
+ source_compiled_screen = None
386
+ elif source_screen_id != source_basis.source_screen_id:
387
+ source_compiled_screen = None
388
+ try:
389
+ result = self._action_executor.execute(
390
+ runtime,
391
+ current_command_record(
392
+ kind=command.kind,
393
+ result_command="open",
394
+ ),
395
+ command,
396
+ lifecycle_lease,
397
+ )
398
+ except ActionExecutionFailure as failure:
399
+ return _command_error_payload(
400
+ command_name="open",
401
+ category="open",
402
+ source_screen_id=source_screen_id,
403
+ source_compiled_screen=source_compiled_screen,
404
+ runtime=runtime,
405
+ error=failure.normalized_error,
406
+ continuity_status_override="none",
407
+ changed_override=None,
408
+ dispatch_attempted=failure.dispatch_attempted,
409
+ truth_lost_after_dispatch=failure.truth_lost_after_dispatch,
410
+ )
411
+ except DaemonError as error:
412
+ return _command_error_payload(
413
+ command_name="open",
414
+ category="open",
415
+ source_screen_id=source_screen_id,
416
+ source_compiled_screen=source_compiled_screen,
417
+ runtime=runtime,
418
+ error=error,
419
+ continuity_status_override="none",
420
+ changed_override=None,
421
+ )
422
+ changed = resolve_open_changed(
423
+ runtime=runtime,
424
+ source_screen_id=source_screen_id,
425
+ source_compiled_screen=source_compiled_screen,
426
+ )
427
+ return _command_result_payload(
428
+ command_name="open",
429
+ category="open",
430
+ source_screen_id=source_screen_id,
431
+ source_compiled_screen=source_compiled_screen,
432
+ runtime=runtime,
433
+ assembly_input=result,
434
+ continuity_status_override="none",
435
+ changed_override=changed,
436
+ )
437
+
438
+
439
+ __all__ = ["ActionCommandHandler"]
@@ -0,0 +1,94 @@
1
+ """Explicit semantic connect handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from androidctld.commands.command_models import ConnectCommand
6
+ from androidctld.commands.result_models import (
7
+ build_projected_retained_failure_result_for_error,
8
+ build_retained_success_result,
9
+ )
10
+ from androidctld.device.bootstrap import DeviceBootstrapper
11
+ from androidctld.device.types import RuntimeTransport
12
+ from androidctld.errors import DaemonError
13
+ from androidctld.protocol import CommandKind
14
+ from androidctld.runtime import RuntimeKernel
15
+ from androidctld.snapshots.refresh import ScreenRefreshService
16
+ from androidctld.snapshots.service import SnapshotService
17
+
18
+
19
+ class ConnectCommandHandler:
20
+ def __init__(
21
+ self,
22
+ *,
23
+ runtime_kernel: RuntimeKernel,
24
+ bootstrapper: DeviceBootstrapper,
25
+ snapshot_service: SnapshotService,
26
+ screen_refresh: ScreenRefreshService,
27
+ ) -> None:
28
+ self._runtime_kernel = runtime_kernel
29
+ self._bootstrapper = bootstrapper
30
+ self._snapshot_service = snapshot_service
31
+ self._screen_refresh = screen_refresh
32
+
33
+ def handle(
34
+ self,
35
+ *,
36
+ command: ConnectCommand,
37
+ ) -> dict[str, object]:
38
+ runtime = self._runtime_kernel.ensure_runtime()
39
+ lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
40
+ connection_config = command.connection
41
+
42
+ handle = None
43
+ connect_started = False
44
+ try:
45
+ handle = self._bootstrapper.establish_transport(connection_config)
46
+ if not self._runtime_kernel.begin_connect(
47
+ runtime,
48
+ lifecycle_lease,
49
+ transport=RuntimeTransport(
50
+ endpoint=handle.endpoint,
51
+ close=handle.close,
52
+ ),
53
+ ):
54
+ handle.close()
55
+ raise RuntimeError("runtime lifecycle changed during connect")
56
+ connect_started = True
57
+ bootstrap_result = self._bootstrapper.bootstrap_runtime(
58
+ handle,
59
+ connection_config,
60
+ )
61
+ if not self._runtime_kernel.activate_connect(
62
+ runtime,
63
+ lifecycle_lease,
64
+ bootstrap_result=bootstrap_result,
65
+ device_token=connection_config.token,
66
+ ):
67
+ raise RuntimeError("runtime lifecycle changed during connect")
68
+ snapshot = self._snapshot_service.fetch(
69
+ runtime,
70
+ force_refresh=True,
71
+ lifecycle_lease=lifecycle_lease,
72
+ )
73
+ snapshot, public_screen, artifacts = self._screen_refresh.refresh(
74
+ runtime,
75
+ snapshot,
76
+ lifecycle_lease=lifecycle_lease,
77
+ command_kind=CommandKind.CONNECT,
78
+ )
79
+ del public_screen, artifacts
80
+ return build_retained_success_result(command="connect").model_dump(
81
+ by_alias=True,
82
+ mode="json",
83
+ )
84
+ except Exception as error:
85
+ if connect_started:
86
+ self._runtime_kernel.fail_connect(runtime, lifecycle_lease)
87
+ elif handle is not None:
88
+ handle.close()
89
+ if isinstance(error, DaemonError):
90
+ return build_projected_retained_failure_result_for_error(
91
+ command="connect",
92
+ error=error,
93
+ ).model_dump(by_alias=True, mode="json")
94
+ raise