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.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
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
|
+
"""System Readiness & Diagnostic Orchestration Engine."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from artemis.core.diagnostics.adb_server_connection import adb_server_connection
|
|
24
|
+
from artemis.core.diagnostics.probes.adb_probe import AdbDeviceProbe
|
|
25
|
+
from artemis.core.diagnostics.probes.base import BaseProbe
|
|
26
|
+
from artemis.core.diagnostics.probes.credentials_probe import (
|
|
27
|
+
LLMCredentialsProbe,
|
|
28
|
+
VisionOCRProbe,
|
|
29
|
+
)
|
|
30
|
+
from artemis.core.diagnostics.probes.runtime_probe import (
|
|
31
|
+
PythonRuntimeProbe,
|
|
32
|
+
SystemConfigProbe,
|
|
33
|
+
)
|
|
34
|
+
from artemis.core.diagnostics.probes.toolchain_probe import ToolchainProbe
|
|
35
|
+
from artemis.core.diagnostics.schema import (
|
|
36
|
+
DeviceInfo,
|
|
37
|
+
ProbeAction,
|
|
38
|
+
ProbeCategory,
|
|
39
|
+
ProbeResult,
|
|
40
|
+
ProbeStatus,
|
|
41
|
+
SystemReadinessReport,
|
|
42
|
+
)
|
|
43
|
+
from artemis.toolchain import toolchain
|
|
44
|
+
from artemis.platform import platform
|
|
45
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
46
|
+
|
|
47
|
+
logger = get_logger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReadinessEngine:
|
|
51
|
+
"""Central orchestration engine executing modular readiness probes."""
|
|
52
|
+
|
|
53
|
+
_REPORT_CACHE_TTL_SECONDS = 2.0
|
|
54
|
+
|
|
55
|
+
#: Deadline for one probe inside a report. The ADB probe shells out to
|
|
56
|
+
#: ``adb`` without a deadline of its own, and a wedged ADB server would
|
|
57
|
+
#: otherwise hang every surface that runs the report (``artemis doctor``,
|
|
58
|
+
#: the console wizard, ``mobile_diagnose``). A probe that overruns is
|
|
59
|
+
#: reported as a FAIL with the recovery steps instead.
|
|
60
|
+
PROBE_TIMEOUT_SECONDS = 30.0
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
self._probes: dict[str, BaseProbe] = {}
|
|
64
|
+
self._python_probe = PythonRuntimeProbe()
|
|
65
|
+
self._config_probe = SystemConfigProbe()
|
|
66
|
+
self._toolchain_probe = ToolchainProbe()
|
|
67
|
+
self._credentials_probe = LLMCredentialsProbe()
|
|
68
|
+
self._ocr_probe = VisionOCRProbe()
|
|
69
|
+
self._adb_probe = AdbDeviceProbe()
|
|
70
|
+
self._report_cache: SystemReadinessReport | None = None
|
|
71
|
+
self._report_cache_time = 0.0
|
|
72
|
+
self._report_cache_generation = -1
|
|
73
|
+
self._cache_generation = 0
|
|
74
|
+
self._report_lock = asyncio.Lock()
|
|
75
|
+
|
|
76
|
+
# Register default core probes in logical lifecycle order
|
|
77
|
+
self.register_probe(self._python_probe)
|
|
78
|
+
self.register_probe(self._config_probe)
|
|
79
|
+
self.register_probe(self._toolchain_probe)
|
|
80
|
+
self.register_probe(self._credentials_probe)
|
|
81
|
+
self.register_probe(self._ocr_probe)
|
|
82
|
+
self.register_probe(self._adb_probe)
|
|
83
|
+
|
|
84
|
+
def register_probe(self, probe: BaseProbe) -> None:
|
|
85
|
+
"""Register a new diagnostic probe."""
|
|
86
|
+
self._probes[probe.probe_id] = probe
|
|
87
|
+
|
|
88
|
+
def unregister_probe(self, probe_id: str) -> None:
|
|
89
|
+
"""Remove a diagnostic probe."""
|
|
90
|
+
self._probes.pop(probe_id, None)
|
|
91
|
+
|
|
92
|
+
def set_probe_target_serial(self, serial: str | None) -> None:
|
|
93
|
+
"""Set the diagnostics probes' preferred device serial.
|
|
94
|
+
|
|
95
|
+
This is a probe/report preference only (which device the readiness
|
|
96
|
+
report highlights and verifies first). Task routing never reads it
|
|
97
|
+
back: execution targets come from explicit request serials or the
|
|
98
|
+
device pool. Pass None to clear the preference, e.g. after switching
|
|
99
|
+
ADB server endpoints.
|
|
100
|
+
"""
|
|
101
|
+
self.invalidate_cache()
|
|
102
|
+
if hasattr(self._adb_probe, "set_target_serial"):
|
|
103
|
+
self._adb_probe.set_target_serial(serial)
|
|
104
|
+
|
|
105
|
+
async def run_probe(self, probe_id: str) -> ProbeResult | None:
|
|
106
|
+
"""Execute a single specific probe by ID."""
|
|
107
|
+
probe = self._probes.get(probe_id)
|
|
108
|
+
if not probe:
|
|
109
|
+
return None
|
|
110
|
+
return await probe.probe()
|
|
111
|
+
|
|
112
|
+
async def run_device_submission_probe(self, target_serial: str | None = None) -> ProbeResult:
|
|
113
|
+
"""Run the bounded device gate used by task submission."""
|
|
114
|
+
return await self._adb_probe.probe_submission_readiness(target_serial=target_serial)
|
|
115
|
+
|
|
116
|
+
def invalidate_cache(self) -> None:
|
|
117
|
+
"""Invalidate the UI readiness snapshot after an explicit configuration change."""
|
|
118
|
+
self._cache_generation += 1
|
|
119
|
+
self._report_cache = None
|
|
120
|
+
self._report_cache_time = 0.0
|
|
121
|
+
self._report_cache_generation = -1
|
|
122
|
+
|
|
123
|
+
def _cached_report(self, max_age_seconds: float) -> SystemReadinessReport | None:
|
|
124
|
+
if self._report_cache is None:
|
|
125
|
+
return None
|
|
126
|
+
if self._report_cache_generation != self._cache_generation:
|
|
127
|
+
return None
|
|
128
|
+
if time.monotonic() - self._report_cache_time > max_age_seconds:
|
|
129
|
+
return None
|
|
130
|
+
return self._report_cache.model_copy(deep=True)
|
|
131
|
+
|
|
132
|
+
async def run_all(
|
|
133
|
+
self,
|
|
134
|
+
categories: list[ProbeCategory] | None = None,
|
|
135
|
+
*,
|
|
136
|
+
force_refresh: bool = False,
|
|
137
|
+
) -> SystemReadinessReport:
|
|
138
|
+
"""Return a coalesced readiness snapshot.
|
|
139
|
+
|
|
140
|
+
The dashboard polls frequently, so allowing every HTTP request to launch a
|
|
141
|
+
complete toolchain and ADB scan creates a request storm. Full reports are
|
|
142
|
+
cached briefly and all concurrent refreshes share one execution. The task
|
|
143
|
+
submission gate remains independent and always performs its own bounded
|
|
144
|
+
device check.
|
|
145
|
+
"""
|
|
146
|
+
cacheable = categories is None
|
|
147
|
+
request_started = time.monotonic()
|
|
148
|
+
if cacheable and not force_refresh:
|
|
149
|
+
cached = self._cached_report(self._REPORT_CACHE_TTL_SECONDS)
|
|
150
|
+
if cached is not None:
|
|
151
|
+
return cached
|
|
152
|
+
|
|
153
|
+
async with self._report_lock:
|
|
154
|
+
# A refresh that completed while this caller waited satisfies even a
|
|
155
|
+
# forced request that began before it, coalescing concurrent clicks.
|
|
156
|
+
if cacheable and self._report_cache_time >= request_started:
|
|
157
|
+
cached = self._cached_report(float("inf"))
|
|
158
|
+
if cached is not None:
|
|
159
|
+
return cached
|
|
160
|
+
if cacheable and not force_refresh:
|
|
161
|
+
cached = self._cached_report(self._REPORT_CACHE_TTL_SECONDS)
|
|
162
|
+
if cached is not None:
|
|
163
|
+
return cached
|
|
164
|
+
|
|
165
|
+
if force_refresh:
|
|
166
|
+
toolchain.clear_cache()
|
|
167
|
+
self._adb_probe.invalidate_enrichment_cache()
|
|
168
|
+
|
|
169
|
+
build_generation = self._cache_generation
|
|
170
|
+
report = await self._build_report(categories)
|
|
171
|
+
# If a device/configuration change happened during the scan, return
|
|
172
|
+
# this result only to its original caller and never publish it as the
|
|
173
|
+
# shared snapshot for later requests.
|
|
174
|
+
if cacheable and build_generation == self._cache_generation:
|
|
175
|
+
self._report_cache = report.model_copy(deep=True)
|
|
176
|
+
self._report_cache_time = time.monotonic()
|
|
177
|
+
self._report_cache_generation = build_generation
|
|
178
|
+
return report
|
|
179
|
+
|
|
180
|
+
async def _build_report(
|
|
181
|
+
self, categories: list[ProbeCategory] | None = None
|
|
182
|
+
) -> SystemReadinessReport:
|
|
183
|
+
"""Execute the underlying probes and compile an uncached report."""
|
|
184
|
+
target_probes = [
|
|
185
|
+
probe
|
|
186
|
+
for probe in self._probes.values()
|
|
187
|
+
if categories is None or probe.category in categories
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
# Concurrently execute probes. A probe that raises must not take the
|
|
191
|
+
# whole report down with it: it becomes a structured FAIL so the
|
|
192
|
+
# remaining probes still reach the user and the verdict stays honest.
|
|
193
|
+
outcomes = await asyncio.gather(
|
|
194
|
+
*[
|
|
195
|
+
asyncio.wait_for(probe.probe(), timeout=self.PROBE_TIMEOUT_SECONDS)
|
|
196
|
+
for probe in target_probes
|
|
197
|
+
],
|
|
198
|
+
return_exceptions=True,
|
|
199
|
+
)
|
|
200
|
+
results: list[ProbeResult] = [
|
|
201
|
+
outcome
|
|
202
|
+
if isinstance(outcome, ProbeResult)
|
|
203
|
+
else self._crashed_probe_result(probe, outcome)
|
|
204
|
+
for probe, outcome in zip(target_probes, outcomes, strict=True)
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
blockers = [r for r in results if r.is_blocker]
|
|
208
|
+
passed_blockers = [r for r in blockers if r.status == ProbeStatus.PASS]
|
|
209
|
+
overall_ready = len(blockers) > 0 and len(blockers) == len(passed_blockers)
|
|
210
|
+
|
|
211
|
+
# Extract active device info from ADB probe metadata if available
|
|
212
|
+
active_device: DeviceInfo | None = None
|
|
213
|
+
adb_result = next((r for r in results if r.id == "android_adb"), None)
|
|
214
|
+
if adb_result and adb_result.metadata.get("active_device"):
|
|
215
|
+
try:
|
|
216
|
+
active_device = DeviceInfo(**adb_result.metadata["active_device"])
|
|
217
|
+
except (TypeError, ValueError) as exc:
|
|
218
|
+
# Includes pydantic ValidationError (a ValueError subclass).
|
|
219
|
+
logger.warning(
|
|
220
|
+
f"Malformed active_device metadata from ADB probe; readiness"
|
|
221
|
+
f" report will omit the active device: {exc}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return SystemReadinessReport(
|
|
225
|
+
overall_ready=overall_ready,
|
|
226
|
+
blocker_count=len(blockers),
|
|
227
|
+
passed_blocker_count=len(passed_blockers),
|
|
228
|
+
probes=results,
|
|
229
|
+
active_device=active_device,
|
|
230
|
+
os_type=platform.os_type.value,
|
|
231
|
+
timestamp=time.time(),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _crashed_probe_result(probe: BaseProbe, exc: object) -> ProbeResult:
|
|
236
|
+
"""Turn an exception escaping ``probe.probe()`` into a FAIL result.
|
|
237
|
+
|
|
238
|
+
Cancellation and other non-``Exception`` errors are re-raised: they are
|
|
239
|
+
not a diagnosis of the host, they are the event loop shutting us down.
|
|
240
|
+
"""
|
|
241
|
+
if isinstance(exc, BaseException) and not isinstance(exc, Exception):
|
|
242
|
+
raise exc
|
|
243
|
+
if isinstance(exc, TimeoutError):
|
|
244
|
+
logger.warning(
|
|
245
|
+
f"[ReadinessEngine] Probe '{probe.probe_id}' did not finish within"
|
|
246
|
+
f" {ReadinessEngine.PROBE_TIMEOUT_SECONDS:.0f}s; reporting it as FAIL."
|
|
247
|
+
)
|
|
248
|
+
return ProbeResult(
|
|
249
|
+
id=probe.probe_id,
|
|
250
|
+
category=probe.category,
|
|
251
|
+
title=probe.probe_id.replace("_", " ").title(),
|
|
252
|
+
status=ProbeStatus.FAIL,
|
|
253
|
+
is_blocker=probe.is_blocker,
|
|
254
|
+
summary="Probe timed out",
|
|
255
|
+
description=(
|
|
256
|
+
f"The '{probe.probe_id}' check did not finish within"
|
|
257
|
+
f" {ReadinessEngine.PROBE_TIMEOUT_SECONDS:.0f}s. A hung ADB server is the"
|
|
258
|
+
" usual cause: restart it and run the diagnosis again."
|
|
259
|
+
),
|
|
260
|
+
actions=[
|
|
261
|
+
ProbeAction(
|
|
262
|
+
action_type="command",
|
|
263
|
+
label="Restart ADB",
|
|
264
|
+
payload="adb kill-server && adb start-server",
|
|
265
|
+
),
|
|
266
|
+
],
|
|
267
|
+
metadata={
|
|
268
|
+
"exception_type": "TimeoutError",
|
|
269
|
+
"timeout_seconds": ReadinessEngine.PROBE_TIMEOUT_SECONDS,
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"[ReadinessEngine] Probe '{probe.probe_id}' crashed; reporting it as FAIL: "
|
|
274
|
+
f"{type(exc).__name__}: {exc}"
|
|
275
|
+
)
|
|
276
|
+
return ProbeResult(
|
|
277
|
+
id=probe.probe_id,
|
|
278
|
+
category=probe.category,
|
|
279
|
+
title=probe.probe_id.replace("_", " ").title(),
|
|
280
|
+
status=ProbeStatus.FAIL,
|
|
281
|
+
is_blocker=probe.is_blocker,
|
|
282
|
+
summary="Probe crashed",
|
|
283
|
+
description=(
|
|
284
|
+
f"The '{probe.probe_id}' check raised {type(exc).__name__}: {exc}. "
|
|
285
|
+
"This is a diagnostics bug or a host permission problem, not a device fault."
|
|
286
|
+
),
|
|
287
|
+
metadata={"exception_type": type(exc).__name__, "exception": str(exc)},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def heal_adb_keys(self, force: bool = False) -> dict[str, Any]:
|
|
291
|
+
"""Auto-heal corrupted or 0-byte ADB authentication keys."""
|
|
292
|
+
from artemis.core.diagnostics.adb_keys import heal_adb_keys
|
|
293
|
+
|
|
294
|
+
logger.info("[ReadinessEngine] Auto-healing ADB authentication keys...")
|
|
295
|
+
return await asyncio.to_thread(heal_adb_keys, None, force)
|
|
296
|
+
|
|
297
|
+
async def restart_adb_server(self) -> dict[str, Any]:
|
|
298
|
+
"""Execute adb kill-server && adb start-server to recover connectivity, auto-healing corrupted keys if needed."""
|
|
299
|
+
from artemis.core.diagnostics.adb_keys import heal_adb_keys, inspect_adb_keys
|
|
300
|
+
|
|
301
|
+
endpoint = adb_server_connection.current_endpoint()
|
|
302
|
+
if not endpoint.is_local_default:
|
|
303
|
+
return {
|
|
304
|
+
"success": True,
|
|
305
|
+
"skipped": True,
|
|
306
|
+
"message": (
|
|
307
|
+
"Remote ADB is active. Artemis refreshed device discovery without stopping "
|
|
308
|
+
"the active ADB server endpoint."
|
|
309
|
+
),
|
|
310
|
+
"endpoint": endpoint.to_dict(),
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
adb_path = toolchain.resolve("adb") or "adb"
|
|
314
|
+
|
|
315
|
+
def _restart_sync():
|
|
316
|
+
# If keys are corrupted, heal them first
|
|
317
|
+
key_status = inspect_adb_keys()
|
|
318
|
+
if key_status.is_corrupted:
|
|
319
|
+
logger.warning(
|
|
320
|
+
f"[ReadinessEngine] Corrupted ADB keys detected ({key_status.error_reason}). Auto-healing..."
|
|
321
|
+
)
|
|
322
|
+
return heal_adb_keys(adb_path=adb_path)
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
clean_env = os.environ.copy()
|
|
326
|
+
clean_env.pop("ADB_SERVER_SOCKET", None)
|
|
327
|
+
subprocess.run(
|
|
328
|
+
[adb_path, "kill-server"],
|
|
329
|
+
stdout=subprocess.DEVNULL,
|
|
330
|
+
stderr=subprocess.DEVNULL,
|
|
331
|
+
timeout=5,
|
|
332
|
+
env=clean_env,
|
|
333
|
+
)
|
|
334
|
+
res = subprocess.run(
|
|
335
|
+
[adb_path, "start-server"],
|
|
336
|
+
stdout=subprocess.DEVNULL,
|
|
337
|
+
stderr=subprocess.DEVNULL,
|
|
338
|
+
timeout=5,
|
|
339
|
+
env=clean_env,
|
|
340
|
+
)
|
|
341
|
+
success = res.returncode == 0
|
|
342
|
+
return {
|
|
343
|
+
"success": success,
|
|
344
|
+
"message": "ADB server restarted successfully"
|
|
345
|
+
if success
|
|
346
|
+
else "Failed to restart ADB",
|
|
347
|
+
"output": "",
|
|
348
|
+
}
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
return {
|
|
351
|
+
"success": False,
|
|
352
|
+
"message": f"Error executing ADB restart: {exc}",
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
logger.info("[ReadinessEngine] Restarting ADB server...")
|
|
356
|
+
return await asyncio.to_thread(_restart_sync)
|
|
357
|
+
|
|
358
|
+
async def launch_emulator(self, avd_name: str) -> dict[str, Any]:
|
|
359
|
+
"""Launch an Android emulator by AVD name in the background and track its lifecycle."""
|
|
360
|
+
if not adb_server_connection.current_endpoint().is_local_default:
|
|
361
|
+
return {
|
|
362
|
+
"avd_name": avd_name,
|
|
363
|
+
"status": "failed",
|
|
364
|
+
"pid": None,
|
|
365
|
+
"serial": None,
|
|
366
|
+
"error": "Switch to local ADB before launching a local emulator.",
|
|
367
|
+
"stage_message": "Local ADB is not active",
|
|
368
|
+
"progress_percent": 0,
|
|
369
|
+
"started_at": None,
|
|
370
|
+
"elapsed_seconds": 0,
|
|
371
|
+
"logs": [],
|
|
372
|
+
"can_retry": True,
|
|
373
|
+
}
|
|
374
|
+
from artemis.core.diagnostics.emulator_manager import emulator_manager
|
|
375
|
+
|
|
376
|
+
state = await emulator_manager.launch(avd_name)
|
|
377
|
+
return state.model_dump()
|
|
378
|
+
|
|
379
|
+
def get_emulator_status(self) -> dict[str, Any]:
|
|
380
|
+
"""Query current status of background emulator launch."""
|
|
381
|
+
from artemis.core.diagnostics.emulator_manager import emulator_manager
|
|
382
|
+
|
|
383
|
+
return emulator_manager.get_status().model_dump()
|
|
384
|
+
|
|
385
|
+
async def stop_emulator(self) -> dict[str, Any]:
|
|
386
|
+
"""Stop current running emulator."""
|
|
387
|
+
from artemis.core.diagnostics.emulator_manager import emulator_manager
|
|
388
|
+
|
|
389
|
+
return await emulator_manager.stop()
|
|
390
|
+
|
|
391
|
+
def dismiss_emulator(self) -> dict[str, Any]:
|
|
392
|
+
"""Dismiss current emulator launch state."""
|
|
393
|
+
from artemis.core.diagnostics.emulator_manager import emulator_manager
|
|
394
|
+
|
|
395
|
+
return emulator_manager.dismiss()
|
|
396
|
+
|
|
397
|
+
async def connect_wireless_adb(self, host: str, port: int = 5555) -> dict[str, Any]:
|
|
398
|
+
"""Connect to an Android device over Wi-Fi via adb connect."""
|
|
399
|
+
if not adb_server_connection.current_endpoint().is_local_default:
|
|
400
|
+
return {
|
|
401
|
+
"success": False,
|
|
402
|
+
"message": "Switch to local ADB before connecting a Wireless ADB device.",
|
|
403
|
+
}
|
|
404
|
+
clean_host = host.strip()
|
|
405
|
+
if not clean_host:
|
|
406
|
+
return {"success": False, "message": "Host IP address cannot be empty"}
|
|
407
|
+
target = f"{clean_host}:{port}"
|
|
408
|
+
adb_path = toolchain.resolve("adb") or "adb"
|
|
409
|
+
|
|
410
|
+
def _connect_sync():
|
|
411
|
+
try:
|
|
412
|
+
res = subprocess.run(
|
|
413
|
+
[adb_path, "connect", target],
|
|
414
|
+
capture_output=True,
|
|
415
|
+
text=True,
|
|
416
|
+
timeout=8,
|
|
417
|
+
)
|
|
418
|
+
output = (res.stdout + "\n" + res.stderr).strip()
|
|
419
|
+
success = "connected to" in output.lower() and "failed" not in output.lower()
|
|
420
|
+
return {
|
|
421
|
+
"success": success,
|
|
422
|
+
"target": target,
|
|
423
|
+
"output": output,
|
|
424
|
+
"message": f"Connected to {target}" if success else output,
|
|
425
|
+
}
|
|
426
|
+
except Exception as exc:
|
|
427
|
+
return {
|
|
428
|
+
"success": False,
|
|
429
|
+
"target": target,
|
|
430
|
+
"message": f"Connection error: {exc}",
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
logger.info(f"[ReadinessEngine] Connecting to wireless ADB target '{target}'...")
|
|
434
|
+
return await asyncio.to_thread(_connect_sync)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# Global singleton instance
|
|
438
|
+
readiness_engine = ReadinessEngine()
|
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
"""Real-device parity check between the Accessibility Helper and UIAutomator2.
|
|
16
|
+
|
|
17
|
+
The unit tests prove the two backends produce the same element *shapes*; only
|
|
18
|
+
a real screen can prove they describe the same *content*. ``compare_backends``
|
|
19
|
+
dumps the current screen with both, then checks what the agent depends on:
|
|
20
|
+
|
|
21
|
+
* every labelled UIAutomator element (text or content-desc) has a helper
|
|
22
|
+
element with the same label at the same place (bounds IoU >= 0.5), and vice
|
|
23
|
+
versa, so neither backend invents or loses what is on screen;
|
|
24
|
+
* the helper emits no negative and no off-screen bounds (its bounds are
|
|
25
|
+
clipped to the display, like UIAutomator's);
|
|
26
|
+
* dump latency of both, for the record.
|
|
27
|
+
|
|
28
|
+
``artemis helper parity`` is the CLI front end.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from statistics import median
|
|
34
|
+
import time
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from third_party.mobile_use.clients.ui_automator_client import parse_hierarchy_xml_to_elements
|
|
38
|
+
|
|
39
|
+
MIN_RECALL = 0.9
|
|
40
|
+
MIN_PRECISION = 0.8
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _label(element: dict[str, Any]) -> str:
|
|
44
|
+
text = str(element.get("text") or "").strip()
|
|
45
|
+
if text:
|
|
46
|
+
return text
|
|
47
|
+
return str(element.get("content-desc") or "").strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _iou(a: dict[str, int], b: dict[str, int]) -> float:
|
|
51
|
+
left = max(a["left"], b["left"])
|
|
52
|
+
top = max(a["top"], b["top"])
|
|
53
|
+
right = min(a["right"], b["right"])
|
|
54
|
+
bottom = min(a["bottom"], b["bottom"])
|
|
55
|
+
if right <= left or bottom <= top:
|
|
56
|
+
return 0.0
|
|
57
|
+
inter = (right - left) * (bottom - top)
|
|
58
|
+
area_a = max(1, (a["right"] - a["left"]) * (a["bottom"] - a["top"]))
|
|
59
|
+
area_b = max(1, (b["right"] - b["left"]) * (b["bottom"] - b["top"]))
|
|
60
|
+
return inter / (area_a + area_b - inter)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def analyze_xml(xml: str, width: int, height: int) -> dict[str, Any]:
|
|
64
|
+
"""Counts and labelled elements of one dump, plus the bounds problems the helper must not have."""
|
|
65
|
+
elements = parse_hierarchy_xml_to_elements(xml)
|
|
66
|
+
labelled: list[tuple[str, dict[str, int]]] = []
|
|
67
|
+
negative = offscreen = 0
|
|
68
|
+
package = None
|
|
69
|
+
for element in elements:
|
|
70
|
+
bounds = element.get("parsed_bounds")
|
|
71
|
+
if package is None and element.get("package"):
|
|
72
|
+
package = element["package"]
|
|
73
|
+
if not isinstance(bounds, dict):
|
|
74
|
+
continue
|
|
75
|
+
if min(bounds.values()) < 0:
|
|
76
|
+
negative += 1
|
|
77
|
+
if (
|
|
78
|
+
bounds["left"] >= width
|
|
79
|
+
or bounds["top"] >= height
|
|
80
|
+
or bounds["right"] > width
|
|
81
|
+
or bounds["bottom"] > height
|
|
82
|
+
):
|
|
83
|
+
offscreen += 1
|
|
84
|
+
label = _label(element)
|
|
85
|
+
if label:
|
|
86
|
+
labelled.append((label, bounds))
|
|
87
|
+
return {
|
|
88
|
+
"nodes": len(elements),
|
|
89
|
+
"labelled": len(labelled),
|
|
90
|
+
"labels": labelled,
|
|
91
|
+
"negative_bounds": negative,
|
|
92
|
+
"offscreen_bounds": offscreen,
|
|
93
|
+
"package": package,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def match_labels(
|
|
98
|
+
source: list[tuple[str, dict[str, int]]], target: list[tuple[str, dict[str, int]]]
|
|
99
|
+
) -> tuple[int, list[str]]:
|
|
100
|
+
"""How many labelled ``source`` elements have a same-label, same-place element in ``target``."""
|
|
101
|
+
used = [False] * len(target)
|
|
102
|
+
found = 0
|
|
103
|
+
missing: list[str] = []
|
|
104
|
+
for label, bounds in source:
|
|
105
|
+
hit = False
|
|
106
|
+
for i, (other_label, other_bounds) in enumerate(target):
|
|
107
|
+
if used[i] or other_label != label:
|
|
108
|
+
continue
|
|
109
|
+
if _iou(bounds, other_bounds) >= 0.5:
|
|
110
|
+
used[i] = True
|
|
111
|
+
hit = True
|
|
112
|
+
break
|
|
113
|
+
if hit:
|
|
114
|
+
found += 1
|
|
115
|
+
else:
|
|
116
|
+
missing.append(f"{label!r} at {bounds}")
|
|
117
|
+
return found, missing
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compare_dumps(helper_xml: str, uiautomator_xml: str, width: int, height: int) -> dict[str, Any]:
|
|
121
|
+
"""Pure comparison of two dumps of the same screen."""
|
|
122
|
+
helper = analyze_xml(helper_xml, width, height)
|
|
123
|
+
u2 = analyze_xml(uiautomator_xml, width, height)
|
|
124
|
+
u2_found, missing_in_helper = match_labels(u2["labels"], helper["labels"])
|
|
125
|
+
helper_found, extra_in_helper = match_labels(helper["labels"], u2["labels"])
|
|
126
|
+
recall = u2_found / u2["labelled"] if u2["labelled"] else 1.0
|
|
127
|
+
precision = helper_found / helper["labelled"] if helper["labelled"] else 1.0
|
|
128
|
+
problems: list[str] = []
|
|
129
|
+
if helper["negative_bounds"]:
|
|
130
|
+
problems.append(
|
|
131
|
+
f"helper emitted {helper['negative_bounds']} element(s) with negative bounds"
|
|
132
|
+
)
|
|
133
|
+
if helper["offscreen_bounds"]:
|
|
134
|
+
problems.append(
|
|
135
|
+
f"helper emitted {helper['offscreen_bounds']} element(s) outside the screen"
|
|
136
|
+
)
|
|
137
|
+
if recall < MIN_RECALL:
|
|
138
|
+
problems.append(
|
|
139
|
+
f"only {recall:.0%} of UIAutomator's labelled elements appear in the helper dump "
|
|
140
|
+
f"(need {MIN_RECALL:.0%})"
|
|
141
|
+
)
|
|
142
|
+
if precision < MIN_PRECISION:
|
|
143
|
+
problems.append(
|
|
144
|
+
f"only {precision:.0%} of the helper's labelled elements appear in the UIAutomator dump "
|
|
145
|
+
f"(need {MIN_PRECISION:.0%}); the helper is showing things that are not on screen"
|
|
146
|
+
)
|
|
147
|
+
for side in (helper, u2):
|
|
148
|
+
side.pop("labels", None)
|
|
149
|
+
return {
|
|
150
|
+
"ok": not problems,
|
|
151
|
+
"problems": problems,
|
|
152
|
+
"helper": helper,
|
|
153
|
+
"uiautomator": u2,
|
|
154
|
+
"match": {
|
|
155
|
+
"uiautomator_labelled": u2["labelled"],
|
|
156
|
+
"uiautomator_in_helper": u2_found,
|
|
157
|
+
"recall": recall,
|
|
158
|
+
"helper_labelled": helper["labelled"],
|
|
159
|
+
"helper_in_uiautomator": helper_found,
|
|
160
|
+
"precision": precision,
|
|
161
|
+
"missing_in_helper": missing_in_helper,
|
|
162
|
+
"extra_in_helper": extra_in_helper,
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def compare_backends(serial: str, rounds: int = 3) -> dict[str, Any]:
|
|
168
|
+
"""Dump the current screen of ``serial`` with both backends and compare (device required)."""
|
|
169
|
+
from artemis.clients.accessibility_client import AccessibilityClient
|
|
170
|
+
from third_party.mobile_use.clients.ui_automator_client import UIAutomatorClient
|
|
171
|
+
|
|
172
|
+
helper = AccessibilityClient(serial)
|
|
173
|
+
u2 = UIAutomatorClient(serial)
|
|
174
|
+
|
|
175
|
+
def timed(dump) -> tuple[str, float]:
|
|
176
|
+
times: list[float] = []
|
|
177
|
+
xml = ""
|
|
178
|
+
for _ in range(rounds):
|
|
179
|
+
started = time.perf_counter()
|
|
180
|
+
xml = dump()
|
|
181
|
+
times.append((time.perf_counter() - started) * 1000.0)
|
|
182
|
+
return xml, median(times)
|
|
183
|
+
|
|
184
|
+
# The two cannot run at once: UIAutomator2's UiAutomation connection unbinds
|
|
185
|
+
# the helper. Dump with the helper first, then with UIAutomator2, then stop
|
|
186
|
+
# its server so the helper is back for whoever comes next.
|
|
187
|
+
helper_version = None
|
|
188
|
+
try:
|
|
189
|
+
helper.connect()
|
|
190
|
+
helper_version = helper.session.version_name if helper.session else None
|
|
191
|
+
info = helper.get_hierarchy_json()
|
|
192
|
+
width, height = int(info.get("width") or 0), int(info.get("height") or 0)
|
|
193
|
+
if width <= 0 or height <= 0:
|
|
194
|
+
screen = helper.get_screen_data()
|
|
195
|
+
width, height = screen.width, screen.height
|
|
196
|
+
helper_xml, helper_ms = timed(helper.get_hierarchy)
|
|
197
|
+
finally:
|
|
198
|
+
helper.disconnect()
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
u2.connect()
|
|
202
|
+
u2_xml, u2_ms = timed(u2.get_hierarchy)
|
|
203
|
+
finally:
|
|
204
|
+
u2.disconnect(stop_server=True)
|
|
205
|
+
|
|
206
|
+
report = compare_dumps(helper_xml, u2_xml, width, height)
|
|
207
|
+
report["serial"] = serial
|
|
208
|
+
report["screen"] = {"width": width, "height": height}
|
|
209
|
+
report["helper"]["median_ms"] = helper_ms
|
|
210
|
+
report["helper"]["version"] = helper_version
|
|
211
|
+
report["uiautomator"]["median_ms"] = u2_ms
|
|
212
|
+
report["xml"] = {"helper": helper_xml, "uiautomator": u2_xml}
|
|
213
|
+
return report
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = ["analyze_xml", "compare_backends", "compare_dumps", "match_labels"]
|