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,283 @@
|
|
|
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
|
+
"""Cross-process ownership for Android's shell screen-bright wake lock."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from datetime import UTC, datetime
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
import re
|
|
26
|
+
import subprocess
|
|
27
|
+
import time
|
|
28
|
+
import uuid
|
|
29
|
+
|
|
30
|
+
from artemis.config.paths import get_temp_dir
|
|
31
|
+
from artemis.runtime.adb_endpoint import adb_command
|
|
32
|
+
from artemis.runtime.process_probe import pid_is_alive
|
|
33
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
_WAKE_LOCK_TYPE = "SCREEN_BRIGHT_WAKE_LOCK"
|
|
38
|
+
_WAKE_LOCK_RE = re.compile(
|
|
39
|
+
rf"{_WAKE_LOCK_TYPE}:.*\bheld=(true|false)\b.*\brefCount=(\d+)\b",
|
|
40
|
+
re.IGNORECASE,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ScreenAwakeLease:
|
|
45
|
+
"""Share exactly one Android shell wake-lock reference across clients.
|
|
46
|
+
|
|
47
|
+
``cmd power set-wakelock`` exposes one ref-counted shell wake lock for a
|
|
48
|
+
display/type pair. Calling it independently from every process leaks
|
|
49
|
+
references whenever a process crashes. PID-aware lease files make the
|
|
50
|
+
Android reference a process-shared resource instead: the first live client
|
|
51
|
+
normalizes stale references and acquires one, later clients only register,
|
|
52
|
+
and the last client drains the shared reference on disconnect.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_MUTEX_TIMEOUT_SECONDS = 10.0
|
|
56
|
+
_MUTEX_POLL_SECONDS = 0.05
|
|
57
|
+
_MALFORMED_GRACE_SECONDS = 5.0
|
|
58
|
+
_MAX_REFERENCE_DRAIN = 256
|
|
59
|
+
|
|
60
|
+
def __init__(self, device_id: str):
|
|
61
|
+
self.device_id = device_id
|
|
62
|
+
self.token = uuid.uuid4().hex
|
|
63
|
+
device_hash = hashlib.sha256(device_id.encode("utf-8")).hexdigest()[:16]
|
|
64
|
+
root = get_temp_dir("awake-leases")
|
|
65
|
+
self.lease_dir = root / f"{device_hash}.leases"
|
|
66
|
+
self.mutex_path = root / f"{device_hash}.mutex"
|
|
67
|
+
self.lease_path = self.lease_dir / f"{self.token}.lease"
|
|
68
|
+
self._acquired = False
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _process_created_at(pid: int | None = None) -> float:
|
|
72
|
+
try:
|
|
73
|
+
import psutil
|
|
74
|
+
|
|
75
|
+
return float(psutil.Process(pid or os.getpid()).create_time())
|
|
76
|
+
except Exception:
|
|
77
|
+
return 0.0
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _owner_is_alive(payload: dict) -> bool:
|
|
81
|
+
try:
|
|
82
|
+
pid = int(payload["pid"])
|
|
83
|
+
created_at = float(payload.get("process_created_at", 0.0) or 0.0)
|
|
84
|
+
except (KeyError, TypeError, ValueError):
|
|
85
|
+
return False
|
|
86
|
+
return pid_is_alive(pid, created_at if created_at > 0 else None)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _read_payload(path: Path) -> dict | None:
|
|
90
|
+
try:
|
|
91
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
92
|
+
return value if isinstance(value, dict) else None
|
|
93
|
+
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def _path_is_stale(cls, path: Path) -> bool:
|
|
98
|
+
payload = cls._read_payload(path)
|
|
99
|
+
if payload is not None:
|
|
100
|
+
return not cls._owner_is_alive(payload)
|
|
101
|
+
try:
|
|
102
|
+
return time.time() - path.stat().st_mtime >= cls._MALFORMED_GRACE_SECONDS
|
|
103
|
+
except OSError:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def _payload(self) -> dict:
|
|
107
|
+
return {
|
|
108
|
+
"pid": os.getpid(),
|
|
109
|
+
"process_created_at": self._process_created_at(),
|
|
110
|
+
"token": self.token,
|
|
111
|
+
"device_id": self.device_id,
|
|
112
|
+
"created_at": datetime.now(UTC).isoformat(),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@contextmanager
|
|
116
|
+
def _mutex(self):
|
|
117
|
+
self.mutex_path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
started_at = time.monotonic()
|
|
119
|
+
acquired = False
|
|
120
|
+
while not acquired:
|
|
121
|
+
try:
|
|
122
|
+
fd = os.open(self.mutex_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
123
|
+
except FileExistsError:
|
|
124
|
+
if self._path_is_stale(self.mutex_path):
|
|
125
|
+
try:
|
|
126
|
+
self.mutex_path.unlink(missing_ok=True)
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
continue
|
|
130
|
+
if time.monotonic() - started_at >= self._MUTEX_TIMEOUT_SECONDS:
|
|
131
|
+
raise TimeoutError("Timed out acquiring the Artemis awake-lease mutex.")
|
|
132
|
+
time.sleep(self._MUTEX_POLL_SECONDS)
|
|
133
|
+
else:
|
|
134
|
+
try:
|
|
135
|
+
os.write(fd, json.dumps(self._payload()).encode("utf-8"))
|
|
136
|
+
finally:
|
|
137
|
+
os.close(fd)
|
|
138
|
+
acquired = True
|
|
139
|
+
try:
|
|
140
|
+
yield
|
|
141
|
+
finally:
|
|
142
|
+
payload = self._read_payload(self.mutex_path)
|
|
143
|
+
if payload is not None and payload.get("token") == self.token:
|
|
144
|
+
try:
|
|
145
|
+
self.mutex_path.unlink(missing_ok=True)
|
|
146
|
+
except OSError:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def _run(self, args: list[str], description: str) -> subprocess.CompletedProcess[str] | None:
|
|
150
|
+
try:
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
adb_command(["-s", self.device_id, *args]),
|
|
153
|
+
stdin=subprocess.DEVNULL,
|
|
154
|
+
capture_output=True,
|
|
155
|
+
text=True,
|
|
156
|
+
timeout=10,
|
|
157
|
+
check=False,
|
|
158
|
+
)
|
|
159
|
+
if result.returncode != 0:
|
|
160
|
+
detail = (result.stderr or result.stdout).strip()
|
|
161
|
+
logger.warning(
|
|
162
|
+
f"Could not {description} on {self.device_id}: {detail or 'ADB command failed'}"
|
|
163
|
+
)
|
|
164
|
+
return result
|
|
165
|
+
except Exception as exc:
|
|
166
|
+
logger.warning(f"Could not {description} on {self.device_id}: {exc}")
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def _wake_lock_state(self) -> tuple[bool, int] | None:
|
|
170
|
+
result = self._run(
|
|
171
|
+
["shell", "cmd", "power", "set-wakelock", "list"],
|
|
172
|
+
"inspect the shared screen-bright wake lock",
|
|
173
|
+
)
|
|
174
|
+
if result is None or result.returncode != 0:
|
|
175
|
+
return None
|
|
176
|
+
match = _WAKE_LOCK_RE.search(result.stdout)
|
|
177
|
+
if match is None:
|
|
178
|
+
return (False, 0)
|
|
179
|
+
return (match.group(1).lower() == "true", int(match.group(2)))
|
|
180
|
+
|
|
181
|
+
def _set_wake_lock(self, action: str) -> bool:
|
|
182
|
+
result = self._run(
|
|
183
|
+
[
|
|
184
|
+
"shell",
|
|
185
|
+
"cmd",
|
|
186
|
+
"power",
|
|
187
|
+
"set-wakelock",
|
|
188
|
+
action,
|
|
189
|
+
"-d",
|
|
190
|
+
"0",
|
|
191
|
+
_WAKE_LOCK_TYPE,
|
|
192
|
+
],
|
|
193
|
+
f"{action} the shared screen-bright wake lock",
|
|
194
|
+
)
|
|
195
|
+
return result is not None and result.returncode == 0
|
|
196
|
+
|
|
197
|
+
def _drain_references(self) -> bool:
|
|
198
|
+
"""Release legacy or crashed-process references until none remain."""
|
|
199
|
+
for _ in range(self._MAX_REFERENCE_DRAIN):
|
|
200
|
+
state = self._wake_lock_state()
|
|
201
|
+
if state is None:
|
|
202
|
+
return False
|
|
203
|
+
held, count = state
|
|
204
|
+
if not held or count <= 0:
|
|
205
|
+
return True
|
|
206
|
+
if not self._set_wake_lock("release"):
|
|
207
|
+
return False
|
|
208
|
+
logger.warning(
|
|
209
|
+
f"Refused to drain more than {self._MAX_REFERENCE_DRAIN} shared wake-lock "
|
|
210
|
+
f"references on {self.device_id}"
|
|
211
|
+
)
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def _live_leases(self) -> list[Path]:
|
|
215
|
+
self.lease_dir.mkdir(parents=True, exist_ok=True)
|
|
216
|
+
live: list[Path] = []
|
|
217
|
+
for path in self.lease_dir.glob("*.lease"):
|
|
218
|
+
if self._path_is_stale(path):
|
|
219
|
+
try:
|
|
220
|
+
path.unlink(missing_ok=True)
|
|
221
|
+
except OSError:
|
|
222
|
+
pass
|
|
223
|
+
else:
|
|
224
|
+
live.append(path)
|
|
225
|
+
return live
|
|
226
|
+
|
|
227
|
+
def acquire(self) -> bool:
|
|
228
|
+
if self._acquired:
|
|
229
|
+
return True
|
|
230
|
+
try:
|
|
231
|
+
with self._mutex():
|
|
232
|
+
live = self._live_leases()
|
|
233
|
+
state = self._wake_lock_state()
|
|
234
|
+
if state is None:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if not live:
|
|
238
|
+
# No registered owner means every existing reference is
|
|
239
|
+
# legacy or belongs to a crashed process.
|
|
240
|
+
if not self._drain_references():
|
|
241
|
+
return False
|
|
242
|
+
state = (False, 0)
|
|
243
|
+
|
|
244
|
+
held, count = state
|
|
245
|
+
if not held or count <= 0:
|
|
246
|
+
if not self._set_wake_lock("acquire"):
|
|
247
|
+
return False
|
|
248
|
+
verified = self._wake_lock_state()
|
|
249
|
+
if verified is None or not verified[0] or verified[1] <= 0:
|
|
250
|
+
return False
|
|
251
|
+
|
|
252
|
+
self.lease_path.write_text(
|
|
253
|
+
json.dumps(self._payload(), ensure_ascii=False), encoding="utf-8"
|
|
254
|
+
)
|
|
255
|
+
self._acquired = True
|
|
256
|
+
return True
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
logger.warning(f"Could not acquire shared awake lease on {self.device_id}: {exc}")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def release(self) -> None:
|
|
262
|
+
if not self._acquired:
|
|
263
|
+
return
|
|
264
|
+
try:
|
|
265
|
+
with self._mutex():
|
|
266
|
+
self.lease_path.unlink(missing_ok=True)
|
|
267
|
+
if not self._live_leases():
|
|
268
|
+
self._drain_references()
|
|
269
|
+
except Exception as exc:
|
|
270
|
+
logger.warning(f"Could not release shared awake lease on {self.device_id}: {exc}")
|
|
271
|
+
finally:
|
|
272
|
+
self._acquired = False
|
|
273
|
+
|
|
274
|
+
def cleanup_unowned_references(self) -> bool:
|
|
275
|
+
"""Drain legacy shell wake locks only when no live old host owns them."""
|
|
276
|
+
try:
|
|
277
|
+
with self._mutex():
|
|
278
|
+
if self._live_leases():
|
|
279
|
+
return False
|
|
280
|
+
return self._drain_references()
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
logger.warning(f"Could not clean unowned screen wake locks on {self.device_id}: {exc}")
|
|
283
|
+
return False
|
|
@@ -0,0 +1,343 @@
|
|
|
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
|
+
"""USB-scoped screen-awake policy shared by every local Artemis entrypoint."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import threading
|
|
23
|
+
from typing import Iterable
|
|
24
|
+
|
|
25
|
+
from adbutils import AdbClient
|
|
26
|
+
|
|
27
|
+
from artemis.config import settings
|
|
28
|
+
from artemis.runtime.adb_endpoint import adb_command
|
|
29
|
+
from artemis.runtime.awake_lease import ScreenAwakeLease
|
|
30
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
AWAKE_STRATEGY_USB = "usb_stay_on"
|
|
35
|
+
AWAKE_STRATEGY_HEARTBEAT = "host_heartbeat"
|
|
36
|
+
AWAKE_HEARTBEAT_INTERVAL_SECONDS = 5.0
|
|
37
|
+
DEVICE_DISCOVERY_INTERVAL_SECONDS = 2.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _awake_enabled() -> bool:
|
|
41
|
+
enabled = os.environ.get("ARTEMIS_KEEP_DEVICE_AWAKE", "true").strip().lower()
|
|
42
|
+
return enabled not in {"0", "false", "no", "off"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_awake_adb_command(
|
|
46
|
+
device_id: str, args: list[str], description: str
|
|
47
|
+
) -> subprocess.CompletedProcess[str] | None:
|
|
48
|
+
"""Run one non-fatal ADB command used by the USB awake policy."""
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
adb_command(["-s", device_id, *args]),
|
|
52
|
+
stdin=subprocess.DEVNULL,
|
|
53
|
+
capture_output=True,
|
|
54
|
+
text=True,
|
|
55
|
+
timeout=10,
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode != 0:
|
|
59
|
+
detail = (result.stderr or result.stdout).strip()
|
|
60
|
+
logger.warning(
|
|
61
|
+
f"Could not {description} on {device_id}: {detail or 'ADB command failed'}"
|
|
62
|
+
)
|
|
63
|
+
return result
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
logger.warning(f"Could not {description} on {device_id}: {exc}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def sanitize_device_state(device_id: str) -> None:
|
|
70
|
+
"""Sanitize Android device state by cleaning up orphan screenrecord or modal dialogs."""
|
|
71
|
+
if not device_id:
|
|
72
|
+
return
|
|
73
|
+
# 1. Kill any dangling screenrecord processes left from interrupted tasks
|
|
74
|
+
_run_awake_adb_command(
|
|
75
|
+
device_id,
|
|
76
|
+
["shell", "pkill", "-f", "screenrecord"],
|
|
77
|
+
"kill orphan screenrecord processes",
|
|
78
|
+
)
|
|
79
|
+
# 2. Close system modal dialogs (ANR / crash popups)
|
|
80
|
+
_run_awake_adb_command(
|
|
81
|
+
device_id,
|
|
82
|
+
["shell", "am", "broadcast", "-a", "android.intent.action.CLOSE_SYSTEM_DIALOGS"],
|
|
83
|
+
"dismiss system crash/ANR dialogs",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _configure_usb_stay_awake(device_id: str) -> str | None:
|
|
88
|
+
"""Enable USB stay-awake, falling back to an effective host heartbeat.
|
|
89
|
+
|
|
90
|
+
The policy is intentionally scoped to USB power rather than an Artemis
|
|
91
|
+
process or task. Android applies the normal screen timeout as soon as USB
|
|
92
|
+
power is removed, even though no ADB command can be sent after unplugging.
|
|
93
|
+
AC, wireless, and dock charging retain the user's normal behavior.
|
|
94
|
+
"""
|
|
95
|
+
if not _awake_enabled() or os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# Migration cleanup: previous Artemis versions used a persistent shell
|
|
99
|
+
# wake lock. Drain it only after every live legacy owner has exited.
|
|
100
|
+
ScreenAwakeLease(device_id).cleanup_unowned_references()
|
|
101
|
+
|
|
102
|
+
commands = (
|
|
103
|
+
(
|
|
104
|
+
"enable USB-scoped stay-awake policy",
|
|
105
|
+
["shell", "svc", "power", "stayon", "usb"],
|
|
106
|
+
),
|
|
107
|
+
("wake display", ["shell", "input", "keyevent", "KEYCODE_WAKEUP"]),
|
|
108
|
+
("dismiss non-secure keyguard", ["shell", "wm", "dismiss-keyguard"]),
|
|
109
|
+
)
|
|
110
|
+
for description, args in commands:
|
|
111
|
+
_run_awake_adb_command(device_id, args, description)
|
|
112
|
+
|
|
113
|
+
configured = _run_awake_adb_command(
|
|
114
|
+
device_id,
|
|
115
|
+
["shell", "settings", "get", "global", "stay_on_while_plugged_in"],
|
|
116
|
+
"verify USB-scoped stay-awake policy",
|
|
117
|
+
)
|
|
118
|
+
power_state = _run_awake_adb_command(
|
|
119
|
+
device_id,
|
|
120
|
+
["shell", "dumpsys", "power"],
|
|
121
|
+
"verify active USB stay-awake state",
|
|
122
|
+
)
|
|
123
|
+
try:
|
|
124
|
+
value = int(configured.stdout.strip()) if configured is not None else -1
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
value = -1
|
|
127
|
+
stay_on_active = (
|
|
128
|
+
configured is not None
|
|
129
|
+
and configured.returncode == 0
|
|
130
|
+
and value == 2
|
|
131
|
+
and power_state is not None
|
|
132
|
+
and power_state.returncode == 0
|
|
133
|
+
and re.search(r"\bmStayOn=true\b", power_state.stdout) is not None
|
|
134
|
+
)
|
|
135
|
+
if stay_on_active:
|
|
136
|
+
logger.info(f"USB-scoped stay-awake policy verified active on {device_id}")
|
|
137
|
+
return AWAKE_STRATEGY_USB
|
|
138
|
+
|
|
139
|
+
logger.warning(
|
|
140
|
+
f"USB stay-awake is not active on {device_id} (setting={value}); "
|
|
141
|
+
f"using a {AWAKE_HEARTBEAT_INTERVAL_SECONDS:g}-second host heartbeat"
|
|
142
|
+
)
|
|
143
|
+
primed = _run_awake_adb_command(
|
|
144
|
+
device_id,
|
|
145
|
+
["shell", "input", "keyevent", "KEYCODE_UNKNOWN"],
|
|
146
|
+
"prime the host stay-awake heartbeat",
|
|
147
|
+
)
|
|
148
|
+
if primed is None or primed.returncode != 0:
|
|
149
|
+
return None
|
|
150
|
+
return AWAKE_STRATEGY_HEARTBEAT
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _discover_connected_device_ids() -> list[str]:
|
|
154
|
+
"""Return the explicitly targeted device, or the connected devices Artemis manages."""
|
|
155
|
+
if not _awake_enabled() or os.environ.get("ARTEMIS_CLOUD_MODE") == "1":
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
target = os.environ.get("ARTEMIS_DEVICE_ID") or os.environ.get("ADB_DEVICE_SERIAL")
|
|
159
|
+
host = os.environ.get("ADB_HOST") or settings.ADB_HOST or "127.0.0.1"
|
|
160
|
+
port_text = os.environ.get("ADB_PORT")
|
|
161
|
+
try:
|
|
162
|
+
port = int(port_text) if port_text else int(settings.ADB_PORT or 5037)
|
|
163
|
+
except (TypeError, ValueError):
|
|
164
|
+
port = 5037
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
devices = AdbClient(host=host, port=port).device_list()
|
|
168
|
+
serials = [device.serial for device in devices if device.serial]
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
logger.debug(f"Could not discover a device for the USB awake policy: {exc}")
|
|
171
|
+
return []
|
|
172
|
+
|
|
173
|
+
if target:
|
|
174
|
+
return [target] if target in serials else []
|
|
175
|
+
# Keep awake only the devices Artemis actually manages: serials claimed in
|
|
176
|
+
# the device pool by an active execution lock or a live queue reservation.
|
|
177
|
+
# Other devices sharing the same ADB server belong to unrelated tools or
|
|
178
|
+
# users, and their power policy must not be touched.
|
|
179
|
+
try:
|
|
180
|
+
from artemis.runtime.device_pool import device_pool
|
|
181
|
+
|
|
182
|
+
claimed = device_pool.get_claimed_serials()
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
logger.debug(f"Could not query claimed devices for the awake policy: {exc}")
|
|
185
|
+
return []
|
|
186
|
+
return [serial for serial in serials if serial in claimed]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class ScreenAwakeService:
|
|
190
|
+
"""Apply USB stay-awake or a connection-scoped host heartbeat."""
|
|
191
|
+
|
|
192
|
+
def __init__(self):
|
|
193
|
+
self._lock = threading.RLock()
|
|
194
|
+
self._strategies: dict[str, str] = {}
|
|
195
|
+
self._heartbeat_stops: dict[str, threading.Event] = {}
|
|
196
|
+
self._heartbeat_threads: dict[str, threading.Thread] = {}
|
|
197
|
+
self._monitor_stop: threading.Event | None = None
|
|
198
|
+
self._monitor_thread: threading.Thread | None = None
|
|
199
|
+
self._shutdown_requested = False
|
|
200
|
+
|
|
201
|
+
def start(self, device_ids: Iterable[str] | None = None) -> dict[str, str | None]:
|
|
202
|
+
if self._shutdown_requested:
|
|
203
|
+
return {}
|
|
204
|
+
selected = list(device_ids) if device_ids is not None else _discover_connected_device_ids()
|
|
205
|
+
results = {}
|
|
206
|
+
for device_id in dict.fromkeys(selected):
|
|
207
|
+
if self._shutdown_requested:
|
|
208
|
+
break
|
|
209
|
+
if device_id:
|
|
210
|
+
results[device_id] = self.ensure_device(device_id)
|
|
211
|
+
if (
|
|
212
|
+
_awake_enabled()
|
|
213
|
+
and os.environ.get("ARTEMIS_CLOUD_MODE") != "1"
|
|
214
|
+
and not self._shutdown_requested
|
|
215
|
+
):
|
|
216
|
+
self._start_device_monitor()
|
|
217
|
+
return results
|
|
218
|
+
|
|
219
|
+
def ensure_device(self, device_id: str) -> str | None:
|
|
220
|
+
"""Configure one device once per process; Android owns unplug behavior."""
|
|
221
|
+
if (
|
|
222
|
+
not device_id
|
|
223
|
+
or not _awake_enabled()
|
|
224
|
+
or os.environ.get("ARTEMIS_CLOUD_MODE") == "1"
|
|
225
|
+
or self._shutdown_requested
|
|
226
|
+
):
|
|
227
|
+
return None
|
|
228
|
+
with self._lock:
|
|
229
|
+
existing = self._strategies.get(device_id)
|
|
230
|
+
if existing is not None:
|
|
231
|
+
return existing
|
|
232
|
+
strategy = _configure_usb_stay_awake(device_id)
|
|
233
|
+
if strategy is not None:
|
|
234
|
+
self._strategies[device_id] = strategy
|
|
235
|
+
if strategy == AWAKE_STRATEGY_HEARTBEAT:
|
|
236
|
+
self._start_heartbeat(device_id)
|
|
237
|
+
return strategy
|
|
238
|
+
|
|
239
|
+
def _start_heartbeat(self, device_id: str) -> None:
|
|
240
|
+
if device_id in self._heartbeat_threads:
|
|
241
|
+
return
|
|
242
|
+
stop_event = threading.Event()
|
|
243
|
+
|
|
244
|
+
def heartbeat() -> None:
|
|
245
|
+
while not stop_event.wait(AWAKE_HEARTBEAT_INTERVAL_SECONDS):
|
|
246
|
+
_run_awake_adb_command(
|
|
247
|
+
device_id,
|
|
248
|
+
["shell", "input", "keyevent", "KEYCODE_UNKNOWN"],
|
|
249
|
+
"send the host stay-awake heartbeat",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
thread = threading.Thread(
|
|
253
|
+
target=heartbeat,
|
|
254
|
+
name=f"artemis-awake-heartbeat-{device_id}",
|
|
255
|
+
daemon=True,
|
|
256
|
+
)
|
|
257
|
+
self._heartbeat_stops[device_id] = stop_event
|
|
258
|
+
self._heartbeat_threads[device_id] = thread
|
|
259
|
+
thread.start()
|
|
260
|
+
|
|
261
|
+
def _start_device_monitor(self) -> None:
|
|
262
|
+
with self._lock:
|
|
263
|
+
if self._monitor_thread is not None and self._monitor_thread.is_alive():
|
|
264
|
+
return
|
|
265
|
+
stop_event = threading.Event()
|
|
266
|
+
|
|
267
|
+
def monitor() -> None:
|
|
268
|
+
while not stop_event.wait(DEVICE_DISCOVERY_INTERVAL_SECONDS):
|
|
269
|
+
connected = set(_discover_connected_device_ids())
|
|
270
|
+
if stop_event.is_set():
|
|
271
|
+
break
|
|
272
|
+
self._reconcile_connected_devices(connected)
|
|
273
|
+
|
|
274
|
+
thread = threading.Thread(
|
|
275
|
+
target=monitor,
|
|
276
|
+
name="artemis-awake-device-monitor",
|
|
277
|
+
daemon=True,
|
|
278
|
+
)
|
|
279
|
+
self._monitor_stop = stop_event
|
|
280
|
+
self._monitor_thread = thread
|
|
281
|
+
thread.start()
|
|
282
|
+
|
|
283
|
+
def _reconcile_connected_devices(self, connected: set[str]) -> None:
|
|
284
|
+
"""Stop unplugged heartbeats and enroll newly attached devices."""
|
|
285
|
+
with self._lock:
|
|
286
|
+
missing = set(self._strategies) - connected
|
|
287
|
+
for device_id in missing:
|
|
288
|
+
self._stop_device(device_id)
|
|
289
|
+
for device_id in connected:
|
|
290
|
+
self.ensure_device(device_id)
|
|
291
|
+
|
|
292
|
+
def _stop_device(self, device_id: str) -> None:
|
|
293
|
+
with self._lock:
|
|
294
|
+
stop_event = self._heartbeat_stops.pop(device_id, None)
|
|
295
|
+
thread = self._heartbeat_threads.pop(device_id, None)
|
|
296
|
+
self._strategies.pop(device_id, None)
|
|
297
|
+
if stop_event is not None:
|
|
298
|
+
stop_event.set()
|
|
299
|
+
if thread is not None and thread is not threading.current_thread():
|
|
300
|
+
thread.join(timeout=2.0)
|
|
301
|
+
|
|
302
|
+
def shutdown(self) -> None:
|
|
303
|
+
"""Stop host work; unplug behavior remains owned by Android's USB policy."""
|
|
304
|
+
self._shutdown_requested = True
|
|
305
|
+
with self._lock:
|
|
306
|
+
monitor_stop = self._monitor_stop
|
|
307
|
+
monitor_thread = self._monitor_thread
|
|
308
|
+
heartbeat_stops = list(self._heartbeat_stops.values())
|
|
309
|
+
heartbeat_threads = list(self._heartbeat_threads.values())
|
|
310
|
+
if monitor_stop is not None:
|
|
311
|
+
monitor_stop.set()
|
|
312
|
+
for stop_event in heartbeat_stops:
|
|
313
|
+
stop_event.set()
|
|
314
|
+
self._monitor_stop = None
|
|
315
|
+
self._monitor_thread = None
|
|
316
|
+
self._heartbeat_stops.clear()
|
|
317
|
+
self._heartbeat_threads.clear()
|
|
318
|
+
self._strategies.clear()
|
|
319
|
+
|
|
320
|
+
if monitor_thread is not None:
|
|
321
|
+
monitor_thread.join(timeout=2.0)
|
|
322
|
+
for thread in heartbeat_threads:
|
|
323
|
+
thread.join(timeout=2.0)
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def device_ids(self) -> tuple[str, ...]:
|
|
327
|
+
with self._lock:
|
|
328
|
+
return tuple(self._strategies)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
screen_awake_service = ScreenAwakeService()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def start_awake_service(device_ids: Iterable[str] | None = None) -> dict[str, str | None]:
|
|
335
|
+
return screen_awake_service.start(device_ids)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def ensure_device_awake(device_id: str) -> str | None:
|
|
339
|
+
return screen_awake_service.ensure_device(device_id)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def shutdown_awake_service() -> None:
|
|
343
|
+
screen_awake_service.shutdown()
|