android-midscene-automation 0.2.4 → 1.1.1

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 (525) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +159 -29
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,887 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Android ADB & Device Connectivity Probe with AVD Detection."""
16
+
17
+ import asyncio
18
+ import os
19
+ from pathlib import Path
20
+ import re
21
+ import shutil
22
+ import subprocess
23
+ import time
24
+ from typing import Any
25
+
26
+ from artemis.core.diagnostics.probes.base import BaseProbe
27
+ from artemis.core.diagnostics.schema import (
28
+ DeviceInfo,
29
+ ProbeAction,
30
+ ProbeCategory,
31
+ ProbeResult,
32
+ ProbeStatus,
33
+ )
34
+ from artemis.platform import OSType, platform
35
+ from artemis.toolchain import toolchain
36
+ from third_party.mobile_use.utils.logger import get_logger
37
+
38
+ logger = get_logger(__name__)
39
+
40
+
41
+ class AdbDeviceProbe(BaseProbe):
42
+ """Deep inspection probe for Android Debug Bridge, connected mobile devices, and local AVD emulators."""
43
+
44
+ _ENRICHMENT_CACHE_TTL_SECONDS = 60.0
45
+ _LAST_LOCK_STATE_TTL_SECONDS = 15.0
46
+
47
+ def __init__(self, target_serial: str | None = None):
48
+ self._target_serial = target_serial
49
+ self._device_enrichment_cache: dict[str, tuple[float, dict[str, Any]]] = {}
50
+ self._last_lock_states: dict[str, tuple[float, bool]] = {}
51
+ self._lock_state_sources: dict[str, str] = {}
52
+
53
+ @property
54
+ def probe_id(self) -> str:
55
+ return "android_adb"
56
+
57
+ @property
58
+ def category(self) -> ProbeCategory:
59
+ return ProbeCategory.DEVICE
60
+
61
+ @property
62
+ def is_blocker(self) -> bool:
63
+ return True
64
+
65
+ def set_target_serial(self, serial: str | None) -> None:
66
+ """Set or update preferred active device serial."""
67
+ self._target_serial = serial
68
+
69
+ def invalidate_enrichment_cache(self) -> None:
70
+ """Refresh static device metadata on an explicit full re-check."""
71
+ self._device_enrichment_cache.clear()
72
+
73
+ def _locate_adb(self) -> str | None:
74
+ """Find the adb binary path from ToolchainResolver."""
75
+ return toolchain.resolve("adb")
76
+
77
+ def _locate_emulator(self) -> str | None:
78
+ """Find the emulator binary path from PATH or standard SDK environments."""
79
+ emu_path = shutil.which("emulator")
80
+ if emu_path:
81
+ return emu_path
82
+
83
+ sdk_candidates = [
84
+ os.environ.get("ANDROID_HOME"),
85
+ os.environ.get("ANDROID_SDK_ROOT"),
86
+ str(Path.home() / "Android" / "Sdk"),
87
+ str(Path.home() / "Android" / "sdk"),
88
+ str(Path.home() / "Library" / "Android" / "sdk"),
89
+ os.getenv("LOCALAPPDATA", "") + "/Android/Sdk" if os.getenv("LOCALAPPDATA") else None,
90
+ "/usr/lib/android-sdk",
91
+ "/opt/android-sdk",
92
+ ]
93
+ for base in sdk_candidates:
94
+ if base:
95
+ cand = (
96
+ Path(base)
97
+ / "emulator"
98
+ / ("emulator.exe" if platform.os_type == OSType.WINDOWS else "emulator")
99
+ )
100
+ if cand.is_file():
101
+ return str(cand)
102
+ return None
103
+
104
+ def _list_installed_avds(self, emulator_path: str | None = None) -> list[str]:
105
+ """Discover all locally created Android Virtual Devices (AVDs)."""
106
+ avd_names: list[str] = []
107
+
108
+ # Method 1: Scan ~/.android/avd/*.ini
109
+ avd_dir = Path.home() / ".android" / "avd"
110
+ if avd_dir.is_dir():
111
+ for ini_file in avd_dir.glob("*.ini"):
112
+ name = ini_file.stem
113
+ if name and name not in avd_names:
114
+ avd_names.append(name)
115
+
116
+ return avd_names
117
+
118
+ async def _get_adb_version(self, adb_path: str) -> str:
119
+ """Query ADB version string."""
120
+ try:
121
+ proc = await asyncio.create_subprocess_exec(
122
+ adb_path,
123
+ "version",
124
+ stdout=subprocess.PIPE,
125
+ stderr=subprocess.PIPE,
126
+ )
127
+ stdout, _ = await proc.communicate()
128
+ if stdout:
129
+ first_line = stdout.decode(errors="replace").strip().splitlines()[0]
130
+ return first_line
131
+ except Exception as e:
132
+ logger.debug(f"Failed to read adb version: {e}")
133
+ return "Installed"
134
+
135
+ @staticmethod
136
+ def _parse_device_lock_state(policy_output: str, trust_output: str) -> bool | None:
137
+ """Parse Android Keyguard state from dumpsys output.
138
+
139
+ ``KeyguardServiceDelegate.showing`` also catches swipe-only lock screens,
140
+ while Trust's current-user ``deviceLocked`` covers secure locks and
141
+ screen-off transitions. Any positive signal wins so an occluded lock
142
+ screen cannot accidentally be treated as ready.
143
+ """
144
+ policy_showing: bool | None = None
145
+ policy_lines = policy_output.splitlines()
146
+ for index, raw_line in enumerate(policy_lines):
147
+ if raw_line.strip() != "KeyguardServiceDelegate":
148
+ continue
149
+ for delegate_line in policy_lines[index + 1 : index + 16]:
150
+ match = re.fullmatch(
151
+ r"showing\s*=\s*(true|false|1|0)",
152
+ delegate_line.strip(),
153
+ flags=re.IGNORECASE,
154
+ )
155
+ if match:
156
+ policy_showing = match.group(1).lower() in {"true", "1"}
157
+ break
158
+ break
159
+
160
+ # Older Android releases expose differently named policy fields.
161
+ legacy_matches = re.findall(
162
+ r"\b(?:mShowingLockscreen|mKeyguardShowing|keyguardShowing|"
163
+ r"isKeyguardLocked|showingAndNotOccluded)\s*=\s*(true|false|1|0)",
164
+ policy_output,
165
+ flags=re.IGNORECASE,
166
+ )
167
+ legacy_states = [value.lower() in {"true", "1"} for value in legacy_matches]
168
+
169
+ trust_match = re.search(
170
+ r"^.*\(current\).*?\bdeviceLocked\s*=\s*(true|false|1|0)\b",
171
+ trust_output,
172
+ flags=re.IGNORECASE | re.MULTILINE,
173
+ )
174
+ if trust_match is None:
175
+ trust_match = re.search(
176
+ r"\bdeviceLocked\s*=\s*(true|false|1|0)\b",
177
+ trust_output,
178
+ flags=re.IGNORECASE,
179
+ )
180
+ trust_locked = trust_match.group(1).lower() in {"true", "1"} if trust_match else None
181
+
182
+ # Prefer the modern, current-user signals. Legacy fields can coexist in
183
+ # dumpsys output as stale or display-specific diagnostics and must not
184
+ # override an explicit modern unlocked result.
185
+ modern_states = [state for state in [policy_showing, trust_locked] if state is not None]
186
+ if any(modern_states):
187
+ return True
188
+ if modern_states:
189
+ return False
190
+ if any(legacy_states):
191
+ return True
192
+ if legacy_states:
193
+ return False
194
+ return None
195
+
196
+ async def _get_device_lock_state(
197
+ self,
198
+ adb_path: str,
199
+ serial: str,
200
+ timeout_seconds: float = 2.0,
201
+ ) -> bool | None:
202
+ """Query Keyguard state without changing or waking the target device."""
203
+
204
+ async def run_dumpsys(*service_args: str) -> str:
205
+ proc = await asyncio.create_subprocess_exec(
206
+ adb_path,
207
+ "-s",
208
+ serial,
209
+ "shell",
210
+ "dumpsys",
211
+ *service_args,
212
+ stdout=subprocess.PIPE,
213
+ stderr=subprocess.PIPE,
214
+ )
215
+ try:
216
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_seconds)
217
+ except TimeoutError:
218
+ proc.kill()
219
+ await proc.communicate()
220
+ return ""
221
+ if proc.returncode != 0:
222
+ return ""
223
+ return stdout.decode(errors="replace")
224
+
225
+ try:
226
+ policy_output, trust_output = await asyncio.gather(
227
+ run_dumpsys("window", "policy"),
228
+ run_dumpsys("trust"),
229
+ )
230
+ return self._parse_device_lock_state(policy_output, trust_output)
231
+ except Exception as exc:
232
+ logger.debug(f"Failed to query lock state for {serial}: {exc}")
233
+ return None
234
+
235
+ async def _get_confirmed_device_lock_state(
236
+ self,
237
+ adb_path: str,
238
+ serial: str,
239
+ timeout_seconds: float = 2.0,
240
+ ) -> bool | None:
241
+ """Confirm a positive lock result so one transient sample cannot flap the UI."""
242
+ first = await self._get_device_lock_state(adb_path, serial, timeout_seconds=timeout_seconds)
243
+ if first is not True:
244
+ return first
245
+
246
+ second = await self._get_device_lock_state(
247
+ adb_path, serial, timeout_seconds=timeout_seconds
248
+ )
249
+ if second is True:
250
+ return True
251
+ if second is False:
252
+ return False
253
+ return None
254
+
255
+ async def _get_dashboard_lock_state(
256
+ self,
257
+ adb_path: str,
258
+ serial: str,
259
+ timeout_seconds: float = 2.0,
260
+ ) -> bool | None:
261
+ """Use a recent confirmed state when one dashboard sample times out.
262
+
263
+ This smoothing is intentionally dashboard-only. Task submission uses the
264
+ fresh confirmed probe and fails closed when the state is unknown.
265
+ """
266
+ observed = await self._get_confirmed_device_lock_state(
267
+ adb_path, serial, timeout_seconds=timeout_seconds
268
+ )
269
+ now = time.monotonic()
270
+ if observed is not None:
271
+ self._last_lock_states[serial] = (now, observed)
272
+ self._lock_state_sources[serial] = "observed"
273
+ return observed
274
+
275
+ previous = self._last_lock_states.get(serial)
276
+ if previous and now - previous[0] <= self._LAST_LOCK_STATE_TTL_SECONDS:
277
+ self._lock_state_sources[serial] = "recent_confirmed"
278
+ return previous[1]
279
+
280
+ self._lock_state_sources[serial] = "unknown"
281
+ return None
282
+
283
+ @staticmethod
284
+ async def _run_adb_shell(
285
+ adb_path: str,
286
+ serial: str,
287
+ *args: str,
288
+ timeout_seconds: float = 2.5,
289
+ ) -> str:
290
+ """Run a bounded ADB shell command and always reap timed-out children."""
291
+ proc = await asyncio.create_subprocess_exec(
292
+ adb_path,
293
+ "-s",
294
+ serial,
295
+ "shell",
296
+ *args,
297
+ stdout=subprocess.PIPE,
298
+ stderr=subprocess.PIPE,
299
+ )
300
+ try:
301
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_seconds)
302
+ except TimeoutError:
303
+ proc.kill()
304
+ await proc.communicate()
305
+ return ""
306
+ if proc.returncode != 0:
307
+ return ""
308
+ return stdout.decode(errors="replace").strip()
309
+
310
+ async def _get_device_states(
311
+ self,
312
+ adb_path: str,
313
+ timeout_seconds: float = 1.0,
314
+ ) -> list[tuple[str, str]] | None:
315
+ """Return only ADB serial/state pairs for the submission fast path.
316
+
317
+ ``None`` means the command could not be completed within the bounded
318
+ submission window. An empty list means ADB responded with no devices.
319
+ """
320
+ proc = None
321
+ try:
322
+ proc = await asyncio.create_subprocess_exec(
323
+ adb_path,
324
+ "devices",
325
+ "-l",
326
+ stdout=subprocess.PIPE,
327
+ stderr=subprocess.PIPE,
328
+ )
329
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout_seconds)
330
+ if proc.returncode != 0:
331
+ return None
332
+
333
+ states: list[tuple[str, str]] = []
334
+ lines = stdout.decode(errors="replace").splitlines()
335
+ for raw_line in lines[1:]:
336
+ parts = raw_line.strip().split()
337
+ if len(parts) >= 2:
338
+ states.append((parts[0], parts[1]))
339
+ return states
340
+ except TimeoutError:
341
+ if proc is not None:
342
+ # The process may have exited between the timeout and the
343
+ # kill (frequent under concurrent submissions on Windows).
344
+ try:
345
+ proc.kill()
346
+ await proc.communicate()
347
+ except (ProcessLookupError, OSError):
348
+ pass
349
+ return None
350
+ except Exception as exc:
351
+ logger.debug(f"Failed submission-time ADB device check: {exc}")
352
+ return None
353
+
354
+ async def probe_submission_readiness(self, target_serial: str | None = None) -> ProbeResult:
355
+ """Run the minimal fail-safe device check required before enqueueing.
356
+
357
+ The full diagnostics probe enriches device metadata, scans packages,
358
+ and discovers emulators. None of that is needed to reject a locked
359
+ device at submission time, so this path only checks ADB connectivity
360
+ and Keyguard state with strict time bounds.
361
+ """
362
+ adb_path = toolchain.resolve("adb")
363
+ if not adb_path:
364
+ return ProbeResult(
365
+ id=self.probe_id,
366
+ category=self.category,
367
+ title="Device / Emulator Connected",
368
+ status=ProbeStatus.FAIL,
369
+ is_blocker=self.is_blocker,
370
+ summary="ADB Not Found",
371
+ description="Android Debug Bridge (adb) is not installed or not in PATH.",
372
+ metadata={"installed": False, "submission_probe": True},
373
+ )
374
+
375
+ device_states = await self._get_device_states(adb_path)
376
+ if device_states is None:
377
+ return ProbeResult(
378
+ id=self.probe_id,
379
+ category=self.category,
380
+ title="Device / Emulator Connected",
381
+ status=ProbeStatus.WARN,
382
+ is_blocker=self.is_blocker,
383
+ summary="Lock State Unknown",
384
+ description=(
385
+ "ADB did not respond quickly enough to verify the Android lock-screen state."
386
+ ),
387
+ metadata={"installed": True, "submission_probe": True},
388
+ )
389
+
390
+ ready_serials = [serial for serial, state in device_states if state == "device"]
391
+ if not ready_serials:
392
+ states = {state for _, state in device_states}
393
+ summary = "Device Unauthorized" if "unauthorized" in states else "No Device Found"
394
+ return ProbeResult(
395
+ id=self.probe_id,
396
+ category=self.category,
397
+ title="Device / Emulator Connected",
398
+ status=ProbeStatus.WARN,
399
+ is_blocker=self.is_blocker,
400
+ summary=summary,
401
+ description="No authorized Android device is currently ready.",
402
+ metadata={
403
+ "installed": True,
404
+ "submission_probe": True,
405
+ "devices": [
406
+ {"serial": serial, "state": state} for serial, state in device_states
407
+ ],
408
+ },
409
+ )
410
+
411
+ preferred = (
412
+ target_serial
413
+ if target_serial and target_serial in ready_serials
414
+ else self._target_serial
415
+ if self._target_serial in ready_serials
416
+ else ready_serials[0]
417
+ )
418
+
419
+ serial = preferred
420
+ is_locked = await self._get_confirmed_device_lock_state(
421
+ adb_path,
422
+ serial,
423
+ timeout_seconds=1.0,
424
+ )
425
+
426
+ # If preferred device is locked or unknown, check remaining ready devices for an unlocked one
427
+ if is_locked is not False and len(ready_serials) > 1:
428
+ for candidate in ready_serials:
429
+ if candidate == preferred:
430
+ continue
431
+ candidate_locked = await self._get_confirmed_device_lock_state(
432
+ adb_path,
433
+ candidate,
434
+ timeout_seconds=1.0,
435
+ )
436
+ if candidate_locked is False:
437
+ logger.info(
438
+ f"Device {preferred} is not ready/unlocked ({is_locked}); "
439
+ f"switching to unlocked ready device {candidate}"
440
+ )
441
+ serial = candidate
442
+ is_locked = False
443
+ break
444
+ elif candidate_locked is None and is_locked is True:
445
+ serial = candidate
446
+ is_locked = None
447
+ summary = (
448
+ "Device Locked"
449
+ if is_locked is True
450
+ else "Connected"
451
+ if is_locked is False
452
+ else "Lock State Unknown"
453
+ )
454
+ status = ProbeStatus.PASS if is_locked is False else ProbeStatus.WARN
455
+ description = (
456
+ f"ADB connected to {serial}. Ready for task submission."
457
+ if is_locked is False
458
+ else f"Android device {serial} is locked. Unlock it before running a task."
459
+ if is_locked is True
460
+ else f"Android lock-screen state could not be verified for {serial}."
461
+ )
462
+ return ProbeResult(
463
+ id=self.probe_id,
464
+ category=self.category,
465
+ title="Device / Emulator Connected",
466
+ status=status,
467
+ is_blocker=self.is_blocker,
468
+ summary=summary,
469
+ description=description,
470
+ metadata={
471
+ "installed": True,
472
+ "submission_probe": True,
473
+ "active_device": {"serial": serial, "is_locked": is_locked},
474
+ },
475
+ )
476
+
477
+ async def _parse_adb_devices(self, adb_path: str) -> list[DeviceInfo]:
478
+ """Execute `adb devices -l` and extract structured device metadata."""
479
+ devices: list[DeviceInfo] = []
480
+ try:
481
+ proc = await asyncio.create_subprocess_exec(
482
+ adb_path,
483
+ "devices",
484
+ "-l",
485
+ stdout=subprocess.PIPE,
486
+ stderr=subprocess.PIPE,
487
+ )
488
+ stdout, _ = await proc.communicate()
489
+ if proc.returncode != 0 or not stdout:
490
+ return devices
491
+
492
+ lines = [
493
+ line.strip()
494
+ for line in stdout.decode(errors="replace").splitlines()
495
+ if line.strip()
496
+ ]
497
+ for line in lines[1:]: # Skip "List of devices attached"
498
+ parts = line.split()
499
+ if len(parts) < 2:
500
+ continue
501
+
502
+ serial = parts[0]
503
+ state = parts[1]
504
+ extra_tokens = parts[2:]
505
+
506
+ model = None
507
+ product = None
508
+ for token in extra_tokens:
509
+ if token.startswith("model:"):
510
+ model = token.split(":", 1)[1].replace("_", " ")
511
+ elif token.startswith("product:"):
512
+ product = token.split(":", 1)[1]
513
+
514
+ is_emulator = (
515
+ serial.startswith("emulator-") or "127.0.0.1" in serial or "localhost" in serial
516
+ )
517
+ if is_emulator and not model:
518
+ model = "Android Emulator"
519
+
520
+ device_info = DeviceInfo(
521
+ serial=serial,
522
+ state=state,
523
+ model=model,
524
+ product=product,
525
+ is_emulator=is_emulator,
526
+ )
527
+ devices.append(device_info)
528
+
529
+ except Exception as e:
530
+ logger.error(f"Error querying adb devices: {e}")
531
+
532
+ # Lock state stays live, while static device properties and the installed
533
+ # package inventory are cached to keep frequent dashboard probes cheap.
534
+ for dev in devices:
535
+ if dev.state == "device":
536
+ try:
537
+ lock_state_task = asyncio.create_task(
538
+ self._get_dashboard_lock_state(adb_path, dev.serial)
539
+ )
540
+ cached = self._device_enrichment_cache.get(dev.serial)
541
+ now = time.monotonic()
542
+ if cached and now - cached[0] <= self._ENRICHMENT_CACHE_TTL_SECONDS:
543
+ enrichment = cached[1]
544
+ else:
545
+ prop_task = self._run_adb_shell(
546
+ adb_path,
547
+ dev.serial,
548
+ "getprop",
549
+ "ro.build.version.release",
550
+ )
551
+ size_task = self._run_adb_shell(adb_path, dev.serial, "wm", "size")
552
+ packages_task = self._run_adb_shell(
553
+ adb_path,
554
+ dev.serial,
555
+ "pm",
556
+ "list",
557
+ "packages",
558
+ )
559
+ android_version, size_str, packages_output = await asyncio.gather(
560
+ prop_task, size_task, packages_task
561
+ )
562
+ enrichment = {
563
+ "android_version": android_version or None,
564
+ "screen_resolution": (
565
+ size_str.split("Physical size:")[-1].strip()
566
+ if "Physical size:" in size_str
567
+ else None
568
+ ),
569
+ "installed_packages": [
570
+ line.split("package:", 1)[1].strip()
571
+ for line in packages_output.splitlines()
572
+ if line.startswith("package:")
573
+ and line.split("package:", 1)[1].strip()
574
+ ],
575
+ }
576
+ self._device_enrichment_cache[dev.serial] = (now, enrichment)
577
+
578
+ dev.android_version = enrichment["android_version"]
579
+ dev.screen_resolution = enrichment["screen_resolution"]
580
+ dev.installed_packages = list(enrichment["installed_packages"])
581
+ dev.is_locked = await lock_state_task
582
+ except Exception as exc:
583
+ logger.debug(f"Failed to enrich device {dev.serial}: {exc}")
584
+
585
+ return devices
586
+
587
+ async def probe(self) -> ProbeResult:
588
+ """Run complete ADB, device, and local AVD connectivity probe."""
589
+ adb_path = self._locate_adb()
590
+ emulator_path = self._locate_emulator()
591
+ installed_avds = self._list_installed_avds(emulator_path)
592
+
593
+ # 1. ADB binary check
594
+ if not adb_path:
595
+ actions = []
596
+ if platform.os_type == OSType.WINDOWS:
597
+ actions.append(
598
+ ProbeAction(
599
+ action_type="command",
600
+ label="Install via WinGet",
601
+ payload="winget install Google.PlatformTools",
602
+ )
603
+ )
604
+ actions.append(
605
+ ProbeAction(
606
+ action_type="command",
607
+ label="Run PowerShell Setup",
608
+ payload="powershell -ExecutionPolicy Bypass -File scripts/install_deps.ps1",
609
+ )
610
+ )
611
+ elif platform.os_type == OSType.MACOS:
612
+ actions.append(
613
+ ProbeAction(
614
+ action_type="command",
615
+ label="Install via Homebrew",
616
+ payload="brew install android-platform-tools",
617
+ )
618
+ )
619
+ else:
620
+ actions.append(
621
+ ProbeAction(
622
+ action_type="command",
623
+ label="Linux Quick Install",
624
+ payload="sudo apt-get install -y adb",
625
+ )
626
+ )
627
+ actions.append(
628
+ ProbeAction(
629
+ action_type="command",
630
+ label="One-Click Install All",
631
+ payload="bash scripts/install_deps.sh",
632
+ )
633
+ )
634
+
635
+ actions.append(
636
+ ProbeAction(
637
+ action_type="link",
638
+ label="Download Android Platform Tools",
639
+ payload="https://developer.android.com/tools/releases/platform-tools",
640
+ )
641
+ )
642
+
643
+ return ProbeResult(
644
+ id=self.probe_id,
645
+ category=self.category,
646
+ title="Device / Emulator Connected",
647
+ status=ProbeStatus.FAIL,
648
+ is_blocker=self.is_blocker,
649
+ summary="ADB Not Found",
650
+ description="Android Debug Bridge (adb) is not installed or not in PATH.",
651
+ metadata={"installed": False},
652
+ actions=actions,
653
+ )
654
+
655
+ adb_ver = await self._get_adb_version(adb_path)
656
+ devices = await self._parse_adb_devices(adb_path)
657
+
658
+ # Inspect ADB authentication key health
659
+ from artemis.core.diagnostics.adb_keys import inspect_adb_keys
660
+
661
+ key_status = inspect_adb_keys()
662
+
663
+ # Normalize emulator command display
664
+ is_emu_in_path = shutil.which("emulator") is not None
665
+ emu_display_cmd = "emulator" if is_emu_in_path else (emulator_path or "emulator")
666
+
667
+ metadata: dict[str, Any] = {
668
+ "installed": True,
669
+ "adb_path": adb_path,
670
+ "adb_version": adb_ver,
671
+ "adb_keys": key_status.to_dict(),
672
+ "emulator_path": emulator_path,
673
+ "is_emulator_in_path": is_emu_in_path,
674
+ "installed_avds": installed_avds,
675
+ "device_count": len(devices),
676
+ "devices": [d.model_dump() for d in devices],
677
+ }
678
+
679
+ # 2. Case: No devices found
680
+ if not devices:
681
+ actions: list[ProbeAction] = []
682
+
683
+ if key_status.is_corrupted:
684
+ actions.append(
685
+ ProbeAction(
686
+ action_type="command",
687
+ label="Auto-Heal ADB Keys",
688
+ payload="artemis doctor --fix",
689
+ )
690
+ )
691
+
692
+ # If user has installed AVDs, generate exact launch commands for each
693
+ if installed_avds:
694
+ for avd in installed_avds:
695
+ actions.append(
696
+ ProbeAction(
697
+ action_type="command",
698
+ label=f"Launch {avd}",
699
+ payload=f"{emu_display_cmd} -avd {avd}",
700
+ )
701
+ )
702
+ else:
703
+ actions.append(
704
+ ProbeAction(
705
+ action_type="command",
706
+ label="Start Default Emulator",
707
+ payload=f"{emu_display_cmd} -avd Pixel_8_API_34",
708
+ )
709
+ )
710
+
711
+ actions.append(
712
+ ProbeAction(
713
+ action_type="hint",
714
+ label="Connect via USB",
715
+ payload="Connect your Android phone via USB cable and enable Developer Options -> USB Debugging.",
716
+ )
717
+ )
718
+
719
+ desc = "ADB is ready, but no active Android device or emulator was detected."
720
+ if key_status.is_corrupted:
721
+ desc += f" Warning: Corrupted ADB RSA key detected ({key_status.error_reason})."
722
+ if installed_avds:
723
+ desc += f" Found {len(installed_avds)} installed emulator(s): {', '.join(installed_avds)}."
724
+
725
+ return ProbeResult(
726
+ id=self.probe_id,
727
+ category=self.category,
728
+ title="Device / Emulator Connected",
729
+ status=ProbeStatus.WARN,
730
+ is_blocker=self.is_blocker,
731
+ summary="No Device Found",
732
+ description=desc,
733
+ metadata=metadata,
734
+ actions=actions,
735
+ )
736
+
737
+ # 3. Check for authorized and ready devices
738
+ ready_devices = [d for d in devices if d.state == "device"]
739
+ unauthorized_devices = [d for d in devices if d.state == "unauthorized"]
740
+
741
+ if not ready_devices:
742
+ if unauthorized_devices:
743
+ unauth_serial = unauthorized_devices[0].serial
744
+ if key_status.is_corrupted:
745
+ return ProbeResult(
746
+ id=self.probe_id,
747
+ category=self.category,
748
+ title="Device / Emulator Connected",
749
+ status=ProbeStatus.WARN,
750
+ is_blocker=self.is_blocker,
751
+ summary="ADB Key Corrupted",
752
+ description=(
753
+ f"Device detected ({unauth_serial}), but ADB authentication keys are corrupted "
754
+ f"({key_status.error_reason}). This prevents the device from displaying the USB Debugging prompt."
755
+ ),
756
+ metadata=metadata,
757
+ actions=[
758
+ ProbeAction(
759
+ action_type="command",
760
+ label="Auto-Heal ADB Keys",
761
+ payload="artemis doctor --fix",
762
+ ),
763
+ ProbeAction(
764
+ action_type="command",
765
+ label="Restart ADB Server",
766
+ payload=f"{adb_path} kill-server && {adb_path} start-server",
767
+ ),
768
+ ],
769
+ )
770
+
771
+ return ProbeResult(
772
+ id=self.probe_id,
773
+ category=self.category,
774
+ title="Device / Emulator Connected",
775
+ status=ProbeStatus.WARN,
776
+ is_blocker=self.is_blocker,
777
+ summary="Device Unauthorized",
778
+ description=f"Device detected ({unauth_serial}), but USB debugging is not yet authorized.",
779
+ metadata=metadata,
780
+ actions=[
781
+ ProbeAction(
782
+ action_type="hint",
783
+ label="Authorize USB Debugging",
784
+ payload="Please unlock your Android phone and tap 'Allow' on the USB Debugging permission prompt.",
785
+ ),
786
+ ProbeAction(
787
+ action_type="command",
788
+ label="Restart ADB Server",
789
+ payload=f"{adb_path} kill-server && {adb_path} start-server",
790
+ ),
791
+ ],
792
+ )
793
+ else:
794
+ first_serial = devices[0].serial
795
+ first_state = devices[0].state
796
+ return ProbeResult(
797
+ id=self.probe_id,
798
+ category=self.category,
799
+ title="Device / Emulator Connected",
800
+ status=ProbeStatus.WARN,
801
+ is_blocker=self.is_blocker,
802
+ summary="Device Booting",
803
+ description=f"Device detected ({first_serial}, state: {first_state}). It is currently booting up or offline. Please wait a few seconds...",
804
+ metadata=metadata,
805
+ actions=[
806
+ ProbeAction(
807
+ action_type="hint",
808
+ label="Device Booting",
809
+ payload=f"Device {first_serial} is initializing. It will be ready automatically in a few seconds.",
810
+ )
811
+ ],
812
+ )
813
+
814
+ # 4. Ready device available
815
+ preferred_dev = None
816
+ if self._target_serial:
817
+ preferred_dev = next(
818
+ (d for d in ready_devices if d.serial == self._target_serial), None
819
+ )
820
+
821
+ if preferred_dev and preferred_dev.is_locked is False:
822
+ active_dev = preferred_dev
823
+ else:
824
+ # Prefer a confirmed unlocked ready device
825
+ unlocked_dev = next((d for d in ready_devices if d.is_locked is False), None)
826
+ if unlocked_dev:
827
+ active_dev = unlocked_dev
828
+ else:
829
+ # If no confirmed unlocked device, prefer undetermined lock state over confirmed locked
830
+ unknown_dev = next((d for d in ready_devices if d.is_locked is None), None)
831
+ active_dev = preferred_dev or unknown_dev or ready_devices[0]
832
+
833
+ display_name = active_dev.model or active_dev.serial
834
+ if active_dev.screen_resolution:
835
+ display_name = f"{display_name} ({active_dev.screen_resolution})"
836
+
837
+ metadata["active_device"] = active_dev.model_dump()
838
+ metadata["lock_state_source"] = self._lock_state_sources.get(active_dev.serial, "unknown")
839
+
840
+ if active_dev.is_locked is not False:
841
+ is_locked = active_dev.is_locked is True
842
+ summary = "Device Locked" if is_locked else "Lock State Unknown"
843
+ description = (
844
+ f"ADB connected to {display_name}, but the Android lock screen is active. "
845
+ "Unlock the device and enter the home screen before running a task."
846
+ if is_locked
847
+ else f"ADB connected to {display_name}, but Android lock-screen state could not "
848
+ "be verified. Keep the device unlocked on the home screen and check again."
849
+ )
850
+ return ProbeResult(
851
+ id=self.probe_id,
852
+ category=self.category,
853
+ title="Device / Emulator Connected",
854
+ status=ProbeStatus.WARN,
855
+ is_blocker=self.is_blocker,
856
+ summary=summary,
857
+ description=description,
858
+ metadata=metadata,
859
+ actions=[
860
+ ProbeAction(
861
+ action_type="hint",
862
+ label="Unlock Device" if is_locked else "Verify Device Screen",
863
+ payload=(
864
+ "Unlock the Android device and leave it on the home screen. "
865
+ "Readiness is checked automatically every few seconds."
866
+ ),
867
+ )
868
+ ],
869
+ )
870
+
871
+ return ProbeResult(
872
+ id=self.probe_id,
873
+ category=self.category,
874
+ title="Device / Emulator Connected",
875
+ status=ProbeStatus.PASS,
876
+ is_blocker=self.is_blocker,
877
+ summary="Connected",
878
+ description=f"ADB connected to {display_name}. Ready for perception and touch automation.",
879
+ metadata=metadata,
880
+ actions=[
881
+ ProbeAction(
882
+ action_type="hint",
883
+ label="Device Ready",
884
+ payload=f"Serial: {active_dev.serial} | Android OS: {active_dev.android_version or 'Unknown'}",
885
+ )
886
+ ],
887
+ )