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,120 @@
|
|
|
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 IME and keyboard text typing helper."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import base64
|
|
19
|
+
from typing import Any
|
|
20
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _escape_for_adb_text(s: str) -> str:
|
|
26
|
+
"""Escapes special characters for adb shell input text."""
|
|
27
|
+
return (
|
|
28
|
+
s.replace("\\", "\\\\")
|
|
29
|
+
.replace('"', '\\"')
|
|
30
|
+
.replace("'", "\\'")
|
|
31
|
+
.replace("`", "\\`")
|
|
32
|
+
.replace("$", "\\$")
|
|
33
|
+
.replace("&", "\\&")
|
|
34
|
+
.replace("|", "\\|")
|
|
35
|
+
.replace(";", "\\;")
|
|
36
|
+
.replace("<", "\\<")
|
|
37
|
+
.replace(">", "\\>")
|
|
38
|
+
.replace("(", "\\(")
|
|
39
|
+
.replace(")", "\\)")
|
|
40
|
+
.replace("*", "\\*")
|
|
41
|
+
.replace("?", "\\?")
|
|
42
|
+
.replace("~", "\\~")
|
|
43
|
+
.replace(" ", "%s")
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AndroidInputIME:
|
|
48
|
+
"""Handles text input, unicode typing, and key combination injection."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, adb_device: Any, ui_client: Any = None):
|
|
51
|
+
self.device = adb_device
|
|
52
|
+
self.ui_client = ui_client
|
|
53
|
+
|
|
54
|
+
async def type_text(self, text: str, clear_existing: bool = True) -> bool:
|
|
55
|
+
"""Types text using adb input text, UIAutomator, or broadcast IME."""
|
|
56
|
+
try:
|
|
57
|
+
if clear_existing:
|
|
58
|
+
# Safe & robust clearing: Move to End -> Shift+Home selection -> Delete -> Fallback backspaces
|
|
59
|
+
clear_cmd = (
|
|
60
|
+
"input keyevent 123 && "
|
|
61
|
+
"input keyevent --meta 1 122 && "
|
|
62
|
+
"input keyevent 67 && "
|
|
63
|
+
"input keyevent 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67"
|
|
64
|
+
)
|
|
65
|
+
await asyncio.to_thread(self.device.shell, clear_cmd)
|
|
66
|
+
else:
|
|
67
|
+
# Append mode: move cursor to the end of existing text
|
|
68
|
+
await asyncio.to_thread(
|
|
69
|
+
self.device.shell,
|
|
70
|
+
"input keyevent 123", # KEYCODE_MOVE_END
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 1. Fast path for ASCII text: Use ADB input text directly.
|
|
74
|
+
if text.isascii():
|
|
75
|
+
lines = text.split("\n")
|
|
76
|
+
for i, line in enumerate(lines):
|
|
77
|
+
if i > 0:
|
|
78
|
+
# Send Enter key between lines
|
|
79
|
+
await asyncio.to_thread(self.device.shell, "input keyevent 66")
|
|
80
|
+
if line:
|
|
81
|
+
escaped = _escape_for_adb_text(line)
|
|
82
|
+
await asyncio.to_thread(self.device.shell, f"input text {escaped}")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# 2. For non-ASCII / Unicode text: Try UIAutomator client
|
|
86
|
+
if self.ui_client and hasattr(self.ui_client, "send_text"):
|
|
87
|
+
try:
|
|
88
|
+
res = self.ui_client.send_text(text)
|
|
89
|
+
if asyncio.iscoroutine(res):
|
|
90
|
+
await res
|
|
91
|
+
return True
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.warning(f"UIAutomator send_text failed: {e}")
|
|
94
|
+
|
|
95
|
+
# 3. Check if ADBKeyboard is currently active
|
|
96
|
+
try:
|
|
97
|
+
default_ime = await asyncio.to_thread(
|
|
98
|
+
self.device.shell, "settings get secure default_input_method"
|
|
99
|
+
)
|
|
100
|
+
if "adbkeyboard" in str(default_ime).lower():
|
|
101
|
+
b64_text = base64.b64encode(text.encode("utf-8")).decode("utf-8")
|
|
102
|
+
broadcast_cmd = f"am broadcast -a ADB_INPUT_B64 --es msg '{b64_text}'"
|
|
103
|
+
await asyncio.to_thread(self.device.shell, broadcast_cmd)
|
|
104
|
+
return True
|
|
105
|
+
except Exception as e:
|
|
106
|
+
# ADBKeyboard probe/broadcast failed; fall through to ADB input.
|
|
107
|
+
logger.debug(f"ADBKeyboard IME path failed, falling back to ADB input: {e}")
|
|
108
|
+
|
|
109
|
+
# 4. Fallback: ADB input text
|
|
110
|
+
lines = text.split("\n")
|
|
111
|
+
for i, line in enumerate(lines):
|
|
112
|
+
if i > 0:
|
|
113
|
+
await asyncio.to_thread(self.device.shell, "input keyevent 66")
|
|
114
|
+
if line:
|
|
115
|
+
escaped = _escape_for_adb_text(line)
|
|
116
|
+
await asyncio.to_thread(self.device.shell, f"input text {escaped}")
|
|
117
|
+
return True
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.error(f"Failed to type text '{text}': {e}")
|
|
120
|
+
return False
|
|
@@ -0,0 +1,57 @@
|
|
|
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 screen recording and video transcode utilities."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ScreenRecorder:
|
|
26
|
+
"""Manages background screen recording sessions using adb screenrecord."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, adb_device: Any, output_path: Path):
|
|
29
|
+
self.device = adb_device
|
|
30
|
+
self.output_path = output_path
|
|
31
|
+
self.is_recording = False
|
|
32
|
+
self._remote_path = "/sdcard/artemis_record.mp4"
|
|
33
|
+
|
|
34
|
+
async def start(self) -> None:
|
|
35
|
+
"""Starts remote screen recording."""
|
|
36
|
+
self.is_recording = True
|
|
37
|
+
logger.info("Starting screen recording on device...")
|
|
38
|
+
# Start background screenrecord
|
|
39
|
+
asyncio.create_task(
|
|
40
|
+
asyncio.to_thread(self.device.shell, f"screenrecord {self._remote_path}")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def stop(self) -> Path | None:
|
|
44
|
+
"""Stops screen recording and pulls file to host."""
|
|
45
|
+
if not self.is_recording:
|
|
46
|
+
return None
|
|
47
|
+
self.is_recording = False
|
|
48
|
+
logger.info("Stopping screen recording...")
|
|
49
|
+
try:
|
|
50
|
+
await asyncio.to_thread(self.device.shell, "pkill -2 screenrecord")
|
|
51
|
+
await asyncio.sleep(1.0)
|
|
52
|
+
self.output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
await asyncio.to_thread(self.device.sync.pull, self._remote_path, str(self.output_path))
|
|
54
|
+
return self.output_path
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.warning(f"Failed to pull screen recording: {e}")
|
|
57
|
+
return None
|
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
"""Universal Base Device Driver Protocol.
|
|
16
|
+
|
|
17
|
+
Defines the contract for hardware and simulator device interaction,
|
|
18
|
+
abstracting low-level protocols (ADB, UIAutomator2, WebDriver, Cloud RPC)
|
|
19
|
+
behind a clean, unified async interface.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from abc import ABC, abstractmethod
|
|
23
|
+
import asyncio
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
import re
|
|
27
|
+
import time
|
|
28
|
+
from typing import Any, Literal
|
|
29
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
30
|
+
|
|
31
|
+
from third_party.mobile_use.controllers.types import ElementQuery
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SwipeDirection(str, Enum):
|
|
35
|
+
UP = "up"
|
|
36
|
+
DOWN = "down"
|
|
37
|
+
LEFT = "left"
|
|
38
|
+
RIGHT = "right"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class KeyCode(str, Enum):
|
|
42
|
+
HOME = "home"
|
|
43
|
+
BACK = "back"
|
|
44
|
+
ENTER = "enter"
|
|
45
|
+
DELETE = "delete"
|
|
46
|
+
POWER = "power"
|
|
47
|
+
APP_SWITCH = "app_switch"
|
|
48
|
+
VOLUME_UP = "volume_up"
|
|
49
|
+
VOLUME_DOWN = "volume_down"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ScreenData(BaseModel):
|
|
53
|
+
"""Encapsulates raw and parsed UI screen observation data."""
|
|
54
|
+
|
|
55
|
+
screenshot_bytes: bytes = Field(..., description="Raw PNG/JPEG screenshot bytes")
|
|
56
|
+
screenshot_base64: str = Field(..., description="Base64 encoded screenshot string")
|
|
57
|
+
ui_hierarchy_xml: str | None = Field(default=None, description="Raw UI layout XML hierarchy")
|
|
58
|
+
ui_elements: list[dict[str, Any]] = Field(
|
|
59
|
+
default_factory=list, description="Parsed element node list"
|
|
60
|
+
)
|
|
61
|
+
width: int = Field(default=1080, description="Device screen width in pixels")
|
|
62
|
+
height: int = Field(default=2400, description="Device screen height in pixels")
|
|
63
|
+
timestamp: float = Field(default_factory=time.time, description="Capture timestamp")
|
|
64
|
+
platform: str = Field(default="android", description="Platform identifier")
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BaseDeviceDriver(ABC):
|
|
70
|
+
"""Abstract Base Class for mobile device and emulator drivers."""
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def device_id(self) -> str:
|
|
75
|
+
"""Returns the unique hardware or simulator serial/identifier."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def screen_size(self) -> tuple[int, int]:
|
|
81
|
+
"""Returns (width, height) in physical device pixels."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
async def connect(self) -> None:
|
|
86
|
+
"""Establishes connection to the target device."""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def disconnect(self) -> None:
|
|
91
|
+
"""Closes connection and releases allocated resources."""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
async def get_screen_data(self, skip_settling: bool = False) -> ScreenData:
|
|
96
|
+
"""Captures real-time screenshot and UI element hierarchy.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
skip_settling: If True, captures immediately without waiting for screen stabilization.
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def tap(
|
|
105
|
+
self,
|
|
106
|
+
x: int,
|
|
107
|
+
y: int,
|
|
108
|
+
duration_ms: int = 100,
|
|
109
|
+
times: int = 1,
|
|
110
|
+
delay_ms: int = 100,
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""Performs single or multiple tap interactions at absolute pixel coordinates.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
x: Absolute horizontal coordinate in pixels.
|
|
116
|
+
y: Absolute vertical coordinate in pixels.
|
|
117
|
+
duration_ms: Touch hold duration in milliseconds.
|
|
118
|
+
times: Number of consecutive taps (e.g. 2 for double tap, 7 for dev options).
|
|
119
|
+
delay_ms: Delay in milliseconds between consecutive taps.
|
|
120
|
+
"""
|
|
121
|
+
...
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
async def long_press(self, x: int, y: int, duration_ms: int = 1000) -> bool:
|
|
125
|
+
"""Performs a long press at absolute pixel coordinates."""
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def swipe(
|
|
130
|
+
self,
|
|
131
|
+
start_x: int,
|
|
132
|
+
start_y: int,
|
|
133
|
+
end_x: int,
|
|
134
|
+
end_y: int,
|
|
135
|
+
duration_ms: int = 800,
|
|
136
|
+
) -> bool:
|
|
137
|
+
"""Performs a drag/swipe gesture from start to end pixel coordinates."""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
async def swipe_direction(
|
|
142
|
+
self,
|
|
143
|
+
direction: SwipeDirection | Literal["up", "down", "left", "right"],
|
|
144
|
+
duration_ms: int = 800,
|
|
145
|
+
) -> bool:
|
|
146
|
+
"""Performs standard cardinal scrolling gesture across device viewport."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
@abstractmethod
|
|
150
|
+
async def input_text(self, text: str, clear_existing: bool = True) -> bool:
|
|
151
|
+
"""Types text into the currently focused input field."""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
@abstractmethod
|
|
155
|
+
async def press_key(self, key: KeyCode | str | int) -> bool:
|
|
156
|
+
"""Simulates physical or soft key press (e.g. HOME, BACK, ENTER)."""
|
|
157
|
+
...
|
|
158
|
+
|
|
159
|
+
@abstractmethod
|
|
160
|
+
async def launch_app(self, package_name: str) -> bool:
|
|
161
|
+
"""Launches target application package into the foreground."""
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
async def stop_app(self, package_name: str) -> bool:
|
|
166
|
+
"""Force-stops target application package."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
@abstractmethod
|
|
170
|
+
async def get_current_package(self) -> str | None:
|
|
171
|
+
"""Retrieves currently focused foreground application package name."""
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
@abstractmethod
|
|
175
|
+
async def execute_shell(self, command: str, timeout_seconds: float = 15.0) -> str:
|
|
176
|
+
"""Executes a system shell command directly on the device."""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
@abstractmethod
|
|
180
|
+
async def start_video_recording(self, output_dir: Path | None = None) -> None:
|
|
181
|
+
"""Starts dynamic screen video capture in the background."""
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
@abstractmethod
|
|
185
|
+
async def stop_video_recording(self) -> str | None:
|
|
186
|
+
"""Stops background video capture and returns local recording file path."""
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
async def wait_for_delay(self, seconds: float = 1.0) -> bool:
|
|
190
|
+
"""Waits for specified duration in seconds."""
|
|
191
|
+
await asyncio.sleep(seconds)
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
async def tap_normalized(
|
|
195
|
+
self,
|
|
196
|
+
norm_x: int,
|
|
197
|
+
norm_y: int,
|
|
198
|
+
long_press: bool = False,
|
|
199
|
+
duration_ms: int = 1000,
|
|
200
|
+
times: int = 1,
|
|
201
|
+
delay_ms: int = 100,
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""Taps at normalized (0-1000 scale) screen coordinates."""
|
|
204
|
+
width, height = self.screen_size
|
|
205
|
+
abs_x = int(max(0, min(width - 1, norm_x * width / 1000.0)))
|
|
206
|
+
abs_y = int(max(0, min(height - 1, norm_y * height / 1000.0)))
|
|
207
|
+
if long_press:
|
|
208
|
+
return await self.long_press(abs_x, abs_y, duration_ms=duration_ms)
|
|
209
|
+
return await self.tap(abs_x, abs_y, duration_ms=100, times=times, delay_ms=delay_ms)
|
|
210
|
+
|
|
211
|
+
async def swipe_normalized(
|
|
212
|
+
self,
|
|
213
|
+
start_norm: list[int],
|
|
214
|
+
end_norm: list[int],
|
|
215
|
+
duration_ms: int = 800,
|
|
216
|
+
) -> bool:
|
|
217
|
+
"""Swipes between normalized (0-1000 scale) screen coordinates."""
|
|
218
|
+
width, height = self.screen_size
|
|
219
|
+
sx = int(max(0, min(width - 1, start_norm[0] * width / 1000.0)))
|
|
220
|
+
sy = int(max(0, min(height - 1, start_norm[1] * height / 1000.0)))
|
|
221
|
+
ex = int(max(0, min(width - 1, end_norm[0] * width / 1000.0)))
|
|
222
|
+
ey = int(max(0, min(height - 1, end_norm[1] * height / 1000.0)))
|
|
223
|
+
return await self.swipe(sx, sy, ex, ey, duration_ms=duration_ms)
|
|
224
|
+
|
|
225
|
+
async def find_element(
|
|
226
|
+
self, query: ElementQuery, screen_data: ScreenData | None = None
|
|
227
|
+
) -> tuple[dict[str, Any] | None, list[int] | None, str | None]:
|
|
228
|
+
"""Finds matching UI element in screen hierarchy by resource_id or text."""
|
|
229
|
+
resource_id, text, index = query.resource_id, query.text, query.index
|
|
230
|
+
data = screen_data or await self.get_screen_data()
|
|
231
|
+
elements = data.ui_elements or []
|
|
232
|
+
|
|
233
|
+
matched = []
|
|
234
|
+
for elem in elements:
|
|
235
|
+
r_id = elem.get("resource_id") or elem.get("resource-id") or ""
|
|
236
|
+
t_val = elem.get("text") or elem.get("content-desc") or ""
|
|
237
|
+
|
|
238
|
+
if resource_id and resource_id in r_id:
|
|
239
|
+
matched.append(elem)
|
|
240
|
+
elif text and text.lower() in t_val.lower():
|
|
241
|
+
matched.append(elem)
|
|
242
|
+
|
|
243
|
+
if not matched:
|
|
244
|
+
return None, None, f"Element not found ({query.describe()})"
|
|
245
|
+
|
|
246
|
+
if index >= len(matched):
|
|
247
|
+
return None, None, f"Element index {index} out of range (matched {len(matched)})"
|
|
248
|
+
|
|
249
|
+
elem = matched[index]
|
|
250
|
+
bounds = elem.get("bounds")
|
|
251
|
+
parsed_bounds = elem.get("parsed_bounds")
|
|
252
|
+
numeric_bounds: list[int] | None = None
|
|
253
|
+
if isinstance(parsed_bounds, dict):
|
|
254
|
+
keys = ("left", "top", "right", "bottom")
|
|
255
|
+
if all(isinstance(parsed_bounds.get(key), (int, float)) for key in keys):
|
|
256
|
+
numeric_bounds = [int(parsed_bounds[key]) for key in keys]
|
|
257
|
+
if numeric_bounds is None and isinstance(bounds, (list, tuple)) and len(bounds) == 4:
|
|
258
|
+
if all(isinstance(value, (int, float)) for value in bounds):
|
|
259
|
+
numeric_bounds = [int(value) for value in bounds]
|
|
260
|
+
if numeric_bounds is None and isinstance(bounds, str):
|
|
261
|
+
match = re.fullmatch(
|
|
262
|
+
r"\[\s*(-?\d+)\s*,\s*(-?\d+)\s*\]\[\s*(-?\d+)\s*,\s*(-?\d+)\s*\]",
|
|
263
|
+
bounds,
|
|
264
|
+
)
|
|
265
|
+
if match:
|
|
266
|
+
numeric_bounds = [int(value) for value in match.groups()]
|
|
267
|
+
# Bounds belong to the freshly captured hierarchy. Some UIAutomator
|
|
268
|
+
# adapters also attach a precomputed center which can lag one layout
|
|
269
|
+
# transition behind (for example when an expanding drawer shifts a
|
|
270
|
+
# keypad). Always derive the tap point from live bounds when possible;
|
|
271
|
+
# retain center only for adapters that do not expose bounds.
|
|
272
|
+
if numeric_bounds is not None:
|
|
273
|
+
center = [
|
|
274
|
+
(numeric_bounds[0] + numeric_bounds[2]) // 2,
|
|
275
|
+
(numeric_bounds[1] + numeric_bounds[3]) // 2,
|
|
276
|
+
]
|
|
277
|
+
else:
|
|
278
|
+
center = elem.get("center")
|
|
279
|
+
return elem, center, None
|
|
280
|
+
|
|
281
|
+
async def tap_element(
|
|
282
|
+
self, query: ElementQuery, long_press: bool = False, duration_ms: int = 1000
|
|
283
|
+
) -> bool:
|
|
284
|
+
"""Finds and taps a UI element by resource_id or text."""
|
|
285
|
+
_, center, error = await self.find_element(query)
|
|
286
|
+
if error or not center:
|
|
287
|
+
return False
|
|
288
|
+
if long_press:
|
|
289
|
+
return await self.long_press(center[0], center[1], duration_ms=duration_ms)
|
|
290
|
+
return await self.tap(center[0], center[1])
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
"""Remote Cloud Mobile Device Driver."""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Literal
|
|
19
|
+
from artemis.drivers.base import BaseDeviceDriver, KeyCode, ScreenData, SwipeDirection
|
|
20
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CloudDeviceDriver(BaseDeviceDriver):
|
|
26
|
+
"""Driver communicating with remote Android devices via Cloud Gateway RPC."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
device_id: str = "cloud-device",
|
|
31
|
+
gateway_url: str = "http://127.0.0.1:8000",
|
|
32
|
+
session_id: str = "default-cloud-session",
|
|
33
|
+
width: int = 1080,
|
|
34
|
+
height: int = 2400,
|
|
35
|
+
):
|
|
36
|
+
self._device_id = device_id
|
|
37
|
+
self.gateway_url = gateway_url
|
|
38
|
+
self.session_id = session_id
|
|
39
|
+
self._width = width
|
|
40
|
+
self._height = height
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def device_id(self) -> str:
|
|
44
|
+
return self._device_id
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def screen_size(self) -> tuple[int, int]:
|
|
48
|
+
return (self._width, self._height)
|
|
49
|
+
|
|
50
|
+
async def connect(self) -> None:
|
|
51
|
+
logger.info(f"Connecting to Cloud Device {self._device_id} at {self.gateway_url}")
|
|
52
|
+
|
|
53
|
+
async def disconnect(self) -> None:
|
|
54
|
+
logger.info(f"Disconnecting Cloud Device {self._device_id}")
|
|
55
|
+
|
|
56
|
+
async def get_screen_data(self, skip_settling: bool = False) -> ScreenData:
|
|
57
|
+
return ScreenData(
|
|
58
|
+
screenshot_bytes=b"",
|
|
59
|
+
screenshot_base64="",
|
|
60
|
+
width=self._width,
|
|
61
|
+
height=self._height,
|
|
62
|
+
platform="cloud",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def tap(
|
|
66
|
+
self,
|
|
67
|
+
x: int,
|
|
68
|
+
y: int,
|
|
69
|
+
duration_ms: int = 100,
|
|
70
|
+
times: int = 1,
|
|
71
|
+
delay_ms: int = 100,
|
|
72
|
+
) -> bool:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
async def long_press(self, x: int, y: int, duration_ms: int = 1000) -> bool:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
async def swipe(
|
|
79
|
+
self,
|
|
80
|
+
start_x: int,
|
|
81
|
+
start_y: int,
|
|
82
|
+
end_x: int,
|
|
83
|
+
end_y: int,
|
|
84
|
+
duration_ms: int = 400,
|
|
85
|
+
) -> bool:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
async def swipe_direction(
|
|
89
|
+
self,
|
|
90
|
+
direction: SwipeDirection | Literal["up", "down", "left", "right"],
|
|
91
|
+
duration_ms: int = 400,
|
|
92
|
+
) -> bool:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
async def input_text(self, text: str, clear_existing: bool = True) -> bool:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
async def press_key(self, key: KeyCode | str | int) -> bool:
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
async def launch_app(self, package_name: str) -> bool:
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
async def stop_app(self, package_name: str) -> bool:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
async def get_current_package(self) -> str | None:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
async def execute_shell(self, command: str, timeout_seconds: float = 15.0) -> str:
|
|
111
|
+
return f"cloud_output: {command}"
|
|
112
|
+
|
|
113
|
+
async def start_video_recording(self, output_dir: Path | None = None) -> None:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
async def stop_video_recording(self) -> str | None:
|
|
117
|
+
return None
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
"""Device Driver Factory & Registry."""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from adbutils import AdbClient
|
|
21
|
+
|
|
22
|
+
from artemis.config import settings
|
|
23
|
+
from artemis.drivers.android.adb_driver import AndroidAdbDriver
|
|
24
|
+
from artemis.drivers.base import BaseDeviceDriver
|
|
25
|
+
from artemis.drivers.mock.mock_driver import MockDeviceDriver
|
|
26
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from artemis.context import ArtemisContext
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_driver(ctx: "ArtemisContext") -> BaseDeviceDriver:
|
|
35
|
+
"""Instantiates the appropriate BaseDeviceDriver based on the runtime context."""
|
|
36
|
+
# 1. Cloud mode check. Cloud devices are reached through the gateway's
|
|
37
|
+
# RemoteUIAutomatorClient; ARTEMIS_HIERARCHY_BACKEND does not apply there
|
|
38
|
+
# because the Accessibility Helper needs a local adb forward.
|
|
39
|
+
if os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
|
|
40
|
+
if ctx.adb_client is None:
|
|
41
|
+
from cloud_service.virtualization import RemoteAdbClient
|
|
42
|
+
|
|
43
|
+
ctx.adb_client = RemoteAdbClient()
|
|
44
|
+
if ctx.ui_adb_client is None:
|
|
45
|
+
from cloud_service.virtualization import RemoteUIAutomatorClient
|
|
46
|
+
|
|
47
|
+
ctx.ui_adb_client = RemoteUIAutomatorClient(adb_client=ctx.adb_client)
|
|
48
|
+
|
|
49
|
+
# 2. Mock mode check
|
|
50
|
+
if (
|
|
51
|
+
getattr(ctx.device, "mobile_platform", None) == "mock"
|
|
52
|
+
or os.environ.get("ARTEMIS_MOCK_DRIVER") == "1"
|
|
53
|
+
):
|
|
54
|
+
return MockDeviceDriver(
|
|
55
|
+
device_id=ctx.device.device_id if ctx.device else "mock-device",
|
|
56
|
+
width=ctx.device.device_width if ctx.device else 1080,
|
|
57
|
+
height=ctx.device.device_height if ctx.device else 2400,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 3. Default Android ADB driver
|
|
61
|
+
if ctx.adb_client is None:
|
|
62
|
+
ctx.adb_client = AdbClient(
|
|
63
|
+
host=settings.ADB_HOST or "localhost", port=settings.ADB_PORT or 5037
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return AndroidAdbDriver(
|
|
67
|
+
device_id=ctx.device.device_id,
|
|
68
|
+
adb_client=ctx.adb_client,
|
|
69
|
+
ui_adb_client=getattr(ctx, "ui_adb_client", None),
|
|
70
|
+
width=ctx.device.device_width,
|
|
71
|
+
height=ctx.device.device_height,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_driver(ctx: "ArtemisContext") -> BaseDeviceDriver:
|
|
76
|
+
"""Cached accessor for device driver in the current context."""
|
|
77
|
+
if not hasattr(ctx, "_active_driver") or getattr(ctx, "_active_driver") is None:
|
|
78
|
+
driver = create_driver(ctx)
|
|
79
|
+
setattr(ctx, "_active_driver", driver)
|
|
80
|
+
return getattr(ctx, "_active_driver")
|