mobster-cli 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/state.py
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"""Bounded, normalized UI snapshots. Coordinates are always fractions of the screen."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
import unicodedata
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def finite(value):
|
|
14
|
+
try:
|
|
15
|
+
return type(value) in (int, float) and math.isfinite(value)
|
|
16
|
+
except OverflowError:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ACTION_OPERATIONS = frozenset({
|
|
21
|
+
"TAP", "TYPE", "SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT",
|
|
22
|
+
"BACK", "HOME", "VOLUME_UP", "VOLUME_DOWN", "LAUNCH_APP", "INCREMENT", "DECREMENT", "SUBMIT", "TYPE_SUBMIT"})
|
|
23
|
+
TEXT_OPERATIONS = frozenset({"TYPE", "TYPE_SUBMIT"})
|
|
24
|
+
ELEMENT_OPERATIONS = ACTION_OPERATIONS - {"HOME", "VOLUME_UP", "VOLUME_DOWN", "LAUNCH_APP"}
|
|
25
|
+
HOST_KEY_OPERATIONS = frozenset({"HOME", "VOLUME_UP", "VOLUME_DOWN"})
|
|
26
|
+
WDA_HOST_KEY_NAMES = {"HOME": "home", "VOLUME_UP": "volumeUp", "VOLUME_DOWN": "volumeDown"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# A 1,300-character note timed out part-typed at a flat 15 s and was typed again (iOSWorld mem-048, 24 Sep).
|
|
30
|
+
TYPING_CHARS_PER_SECOND = 10
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def typing_timeout(text):
|
|
34
|
+
return 15 + len(text or "") / TYPING_CHARS_PER_SECOND
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
SIGNATURE_LIMIT = 200
|
|
38
|
+
SHAPE_LIMIT = 200
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def signature(snapshot):
|
|
42
|
+
pairs = sorted({(element.role, element.label) for element in snapshot.elements if element.label})
|
|
43
|
+
return [list(pair) for pair in pairs[:SIGNATURE_LIMIT]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def signature_hash(signature):
|
|
47
|
+
"""The stable screen digest shared by live guards and recorded replay paths."""
|
|
48
|
+
pairs = [list(pair) for pair in signature]
|
|
49
|
+
return hashlib.sha1(json.dumps(pairs, ensure_ascii=False).encode()).hexdigest()[:16]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def screen_shape(pairs):
|
|
53
|
+
out = {hashlib.sha1(json.dumps(list(pair), ensure_ascii=False).encode()).hexdigest()[:8] for pair in pairs or ()}
|
|
54
|
+
return sorted(out)[:SHAPE_LIMIT]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# perception.snapshot_from adds each pixel-only driver's source here.
|
|
58
|
+
PIXEL_SOURCES = {"mirroring"}
|
|
59
|
+
PIXEL_LOCATORS = ("ocr:", "icon:")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def icon_glyph(locator):
|
|
63
|
+
parts = str(locator or "").split(":")
|
|
64
|
+
return parts[2].replace("-", " ") if len(parts) >= 3 and parts[0] == "icon" else ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def pixel_read(snapshot):
|
|
68
|
+
# Fails closed: one element read from pixels makes the whole screen a pixel read.
|
|
69
|
+
if getattr(snapshot, "source", None) in PIXEL_SOURCES:
|
|
70
|
+
return True
|
|
71
|
+
return any(str(getattr(e, "locator", "") or "").startswith(PIXEL_LOCATORS)
|
|
72
|
+
for e in getattr(snapshot, "elements", ()) or ())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_bundle_id(value):
|
|
76
|
+
# A dot is required, so a bundle never collides with an element id.
|
|
77
|
+
if (not isinstance(value, str) or len(value) > 255 or "." not in value
|
|
78
|
+
or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]*[A-Za-z0-9_]", value)):
|
|
79
|
+
raise ValueError("Invalid app bundle identifier")
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_input_text(value, multiline=False):
|
|
84
|
+
# A text view takes line breaks as text: rejecting them saved every multi-line note empty (iOSWorld mem-041).
|
|
85
|
+
allowed = {"\n"} if multiline else set()
|
|
86
|
+
limit = 4000 if multiline else 1000
|
|
87
|
+
if (not isinstance(value, str) or not value.strip() or len(value) > limit
|
|
88
|
+
or any(unicodedata.category(c) in {"Cc", "Cs", "Zl", "Zp"} and c not in allowed for c in value)):
|
|
89
|
+
raise ValueError("TYPE requires bounded nonempty text without control characters")
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class Element:
|
|
95
|
+
id: str
|
|
96
|
+
label: str
|
|
97
|
+
role: str
|
|
98
|
+
rect: tuple[float, float, float, float]
|
|
99
|
+
editable: bool = False
|
|
100
|
+
locator: str = ""
|
|
101
|
+
value: str = ""
|
|
102
|
+
actions: tuple[str, ...] = ("TAP",)
|
|
103
|
+
# () taps the centre.
|
|
104
|
+
hit: tuple = ()
|
|
105
|
+
# Only rich sources set these, so Jev's rows and fingerprints do not change.
|
|
106
|
+
enabled: bool = True
|
|
107
|
+
selected: bool = False
|
|
108
|
+
# XCUITest reports an empty field's value as its placeholder.
|
|
109
|
+
placeholder: str = ""
|
|
110
|
+
|
|
111
|
+
def __post_init__(self):
|
|
112
|
+
if (not isinstance(self.id, str) or not self.id or len(self.id) > 256
|
|
113
|
+
or not isinstance(self.label, str) or len(self.label) > 500
|
|
114
|
+
or not isinstance(self.role, str) or not self.role or len(self.role) > 500
|
|
115
|
+
or not isinstance(self.value, str) or len(self.value) > 500
|
|
116
|
+
or not isinstance(self.locator, str) or len(self.locator) > 16000
|
|
117
|
+
or type(self.editable) is not bool or type(self.enabled) is not bool
|
|
118
|
+
or type(self.selected) is not bool or not isinstance(self.placeholder, str)
|
|
119
|
+
or len(self.placeholder) > 500):
|
|
120
|
+
raise ValueError("Invalid element identity or metadata")
|
|
121
|
+
try:
|
|
122
|
+
for value in (self.id, self.label, self.role, self.value, self.locator):
|
|
123
|
+
value.encode("utf-8")
|
|
124
|
+
except UnicodeError:
|
|
125
|
+
raise ValueError("Element metadata contains invalid Unicode") from None
|
|
126
|
+
if (not isinstance(self.rect, (list, tuple)) or len(self.rect) != 4
|
|
127
|
+
or not isinstance(self.actions, (list, tuple))
|
|
128
|
+
or any(not isinstance(action, str) or action not in ELEMENT_OPERATIONS for action in self.actions)
|
|
129
|
+
or len(set(self.actions)) != len(self.actions)
|
|
130
|
+
or (bool({"TYPE", "TYPE_SUBMIT", "SUBMIT"} & set(self.actions)) and not self.editable)):
|
|
131
|
+
raise ValueError("Invalid element bounds or capabilities")
|
|
132
|
+
x, y, w, h = self.rect
|
|
133
|
+
if not all(finite(n) and 0 <= n <= 1 for n in self.rect):
|
|
134
|
+
raise ValueError("Element rect must use finite normalized coordinates")
|
|
135
|
+
if w <= 0 or h <= 0 or x + w > 1.00001 or y + h > 1.00001:
|
|
136
|
+
raise ValueError("Element is outside the screen")
|
|
137
|
+
object.__setattr__(self, "rect", tuple(self.rect))
|
|
138
|
+
object.__setattr__(self, "actions", tuple(self.actions))
|
|
139
|
+
if self.hit and (len(self.hit) != 2 or not all(finite(n) and x - 1e-6 <= n <= x + d + 1e-6
|
|
140
|
+
for n, x, d in zip(self.hit, (x, y), (w, h)))):
|
|
141
|
+
raise ValueError("Element tap point must lie inside its frame")
|
|
142
|
+
object.__setattr__(self, "hit", tuple(self.hit))
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def center(self):
|
|
146
|
+
x, y, w, h = self.rect
|
|
147
|
+
return x + w / 2, y + h / 2
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def text(self):
|
|
151
|
+
return "" if self.placeholder and self.value == self.placeholder else self.value
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def tap_point(self):
|
|
155
|
+
return self.hit or self.center
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Calculator's display reads "\u200e579": kept, the answer "579" didn't match the screen (MobsterBench pass 6).
|
|
159
|
+
_INVISIBLE_MARKS = re.compile("[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def invisible_marks(text):
|
|
163
|
+
return _INVISIBLE_MARKS.sub("", text) if text else text
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class Snapshot:
|
|
168
|
+
elements: list[Element]
|
|
169
|
+
text: str
|
|
170
|
+
width: float
|
|
171
|
+
height: float
|
|
172
|
+
source: str
|
|
173
|
+
# Looked up per call so a patched clock stamps snapshots too (CI runners with under 1000 s uptime saw them stale).
|
|
174
|
+
captured_at: float = field(default_factory=lambda: time.monotonic())
|
|
175
|
+
image_hash: str = ""
|
|
176
|
+
revision: str = ""
|
|
177
|
+
bundle_id: str = ""
|
|
178
|
+
# Which of these is in front is not in a fast source read, so the driver asks WDA.
|
|
179
|
+
stacked_panes: tuple = ()
|
|
180
|
+
stacked_layers: tuple = ()
|
|
181
|
+
# ((parent path, ((child position, child path), ...)), ...): SwiftUI keeps every tab of a TabView in the tree.
|
|
182
|
+
stacked_pages: tuple = ()
|
|
183
|
+
# "visible", "parked" (a hardware keyboard: keystrokes still reach the focused field) or "".
|
|
184
|
+
keyboard: str = ""
|
|
185
|
+
read_started: float = 0.0
|
|
186
|
+
# What a presented sheet covers: evidence only, never a target, never in the text or the fingerprints.
|
|
187
|
+
behind: tuple = ()
|
|
188
|
+
# Where password fields sit, as (x, y, w, h) fractions of the screen: never their label or value, never a target.
|
|
189
|
+
# An iOS dialog with one is a password prompt (wda/system_dialogs.py).
|
|
190
|
+
secure_fields: tuple = ()
|
|
191
|
+
|
|
192
|
+
def __post_init__(self):
|
|
193
|
+
if not all(finite(n) and n > 0 for n in (self.width, self.height)):
|
|
194
|
+
raise ValueError("Invalid screen dimensions")
|
|
195
|
+
if (not isinstance(self.elements, (list, tuple)) or any(not isinstance(e, Element) for e in self.elements)):
|
|
196
|
+
raise ValueError("Invalid snapshot element collection")
|
|
197
|
+
identities = [e.id for e in self.elements]
|
|
198
|
+
if len(identities) > 240 or len(set(identities)) != len(identities):
|
|
199
|
+
raise ValueError("Too many elements or duplicate element IDs")
|
|
200
|
+
if (not isinstance(self.text, str) or len(self.text) > 241000
|
|
201
|
+
or not isinstance(self.source, str) or not self.source or len(self.source) > 128
|
|
202
|
+
or any(not isinstance(value, str) or len(value) > 500
|
|
203
|
+
for value in (self.image_hash, self.revision, self.bundle_id))):
|
|
204
|
+
raise ValueError("Invalid snapshot metadata")
|
|
205
|
+
|
|
206
|
+
def public(self):
|
|
207
|
+
# Not dataclasses.asdict: 3-5x slower, and this runs ~17 times per agent step.
|
|
208
|
+
return {"source": self.source, "bundle_id": self.bundle_id, "text": self.text[:12000],
|
|
209
|
+
"elements": [{"id": e.id, "label": e.label, "role": e.role, "rect": list(e.rect),
|
|
210
|
+
"editable": e.editable, "value": e.value, "actions": list(e.actions)}
|
|
211
|
+
for e in self.elements]}
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def fingerprint(self):
|
|
215
|
+
# Identical labels do not authorize acting on a replacement control.
|
|
216
|
+
return self._fingerprint(include_revision=True)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def content_fingerprint(self):
|
|
220
|
+
# Never use this weaker comparison to authorize dispatch on a stale revision.
|
|
221
|
+
return self._fingerprint(include_revision=False)
|
|
222
|
+
|
|
223
|
+
def _fingerprint(self, *, include_revision):
|
|
224
|
+
# Read ~60 times a run at 0.4-0.9 ms each; snapshots are mutable, so the cache is stamped with every input.
|
|
225
|
+
stamp = (self.text, self.width, self.height, self.source, self.bundle_id,
|
|
226
|
+
self.revision if include_revision else None)
|
|
227
|
+
elements = self.elements
|
|
228
|
+
cache = self.__dict__.setdefault("_fingerprints", {})
|
|
229
|
+
hit = cache.get(include_revision)
|
|
230
|
+
if (hit is not None and hit[1] == stamp and len(hit[0]) == len(elements)
|
|
231
|
+
and all(a is b for a, b in zip(hit[0], elements))):
|
|
232
|
+
return hit[2]
|
|
233
|
+
grounding = {**self.public(), "text": self.text, "width": self.width, "height": self.height,
|
|
234
|
+
"locators": [e.locator for e in elements]}
|
|
235
|
+
if include_revision:
|
|
236
|
+
grounding["revision"] = self.revision
|
|
237
|
+
value = hashlib.sha256(json.dumps(grounding, sort_keys=True, allow_nan=False).encode()).hexdigest()
|
|
238
|
+
cache[include_revision] = (tuple(elements), stamp, value)
|
|
239
|
+
return value
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def pane_key(role, label, x, y, w, h):
|
|
243
|
+
return (role, label or "", round(x), round(y), round(w), round(h))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def from_wda(xml: str, hidden_panes=frozenset(), hidden_paths=frozenset(), rich=False) -> Snapshot:
|
|
247
|
+
return from_wda_root(_parse_wda(xml), hidden_panes, hidden_paths, rich=rich)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_wda(xml):
|
|
251
|
+
# "<!" first: upper() of a 1 MB Safari source costs ~1 ms, and it cannot create "<!".
|
|
252
|
+
if not isinstance(xml, str) or len(xml) > 8_000_000 or ("<!" in xml and (
|
|
253
|
+
"<!DOCTYPE" in xml.upper() or "<!ENTITY" in xml.upper())):
|
|
254
|
+
raise ValueError("Unsafe or oversized UI source")
|
|
255
|
+
return ET.fromstring(xml)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
FIELD_HEAD, FIELD_TAIL, FIELD_CUT = 200, 295, " … "
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def truncated_field_value(value):
|
|
262
|
+
return (isinstance(value, str) and len(value) == FIELD_HEAD + len(FIELD_CUT) + FIELD_TAIL
|
|
263
|
+
and value[FIELD_HEAD:FIELD_HEAD + len(FIELD_CUT)] == FIELD_CUT)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _wda_text(a, role):
|
|
267
|
+
label = a.get("label") or ""
|
|
268
|
+
name = a.get("name") or ""
|
|
269
|
+
# A bare identifier is plumbing, not screen text: Safari's root reports "SafariWindow?View=Narrow&UUID=...".
|
|
270
|
+
if not label and not WDA_IDENTIFIER.fullmatch(name):
|
|
271
|
+
label = name
|
|
272
|
+
label, value = invisible_marks(label)[:500], invisible_marks(a.get("value", ""))
|
|
273
|
+
if len(value) > 500 and role in ("TextView", "TextField", "SearchField"):
|
|
274
|
+
# Typed text lands at the end: reading only the head, clouddocs-004 typed one note 45 times (iOSWorld).
|
|
275
|
+
value = value[:FIELD_HEAD] + FIELD_CUT + value[-FIELD_TAIL:]
|
|
276
|
+
return label, value[:500]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _extent(value):
|
|
280
|
+
# Reminders' spacer reports CGFLOAT_MAX, which parses as inf and used to fail every read (27 Sep).
|
|
281
|
+
number = float(value)
|
|
282
|
+
return number if math.isfinite(number) else 0.0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def from_wda_root(root, hidden_panes=frozenset(), hidden_paths=frozenset(), rich=False) -> Snapshot:
|
|
286
|
+
app = next((n for n in root.iter() if n.tag == "XCUIElementTypeApplication"), None)
|
|
287
|
+
if app is None:
|
|
288
|
+
raise ValueError("WDA source has no application frame")
|
|
289
|
+
width, height = float(app.attrib["width"]), float(app.attrib["height"])
|
|
290
|
+
# The foreground app WDA stamps on the Application node; absent or malformed stays "", never guessed.
|
|
291
|
+
bundle = app.attrib.get("bundleId", "")
|
|
292
|
+
try:
|
|
293
|
+
bundle = validate_bundle_id(bundle) if bundle else ""
|
|
294
|
+
except ValueError:
|
|
295
|
+
bundle = ""
|
|
296
|
+
if not all(math.isfinite(n) and n > 0 for n in (width, height)):
|
|
297
|
+
raise ValueError("Invalid WDA application dimensions")
|
|
298
|
+
nodes, visited, panes, layers, pages = [], 0, [], [], []
|
|
299
|
+
parked, secure = [], []
|
|
300
|
+
other_leaves, selected_paths, placeholders = set(), set(), {}
|
|
301
|
+
editable_roles = {"TextField", "SearchField", "TextView"}
|
|
302
|
+
structural = {"Application", "Window", "Other", "ScrollView", "Table", "CollectionView", "WebView"}
|
|
303
|
+
|
|
304
|
+
def walk(node, path, ancestor_visible=True, depth=0, named=None, cell=None):
|
|
305
|
+
nonlocal visited
|
|
306
|
+
visited += 1
|
|
307
|
+
if depth > 64 or visited > WDA_NODE_BUDGET:
|
|
308
|
+
raise ValueError("WDA tree exceeds the traversal budget")
|
|
309
|
+
a = node.attrib
|
|
310
|
+
visible = ancestor_visible and a.get("visible", "true") != "false"
|
|
311
|
+
role = node.tag.removeprefix("XCUIElementType")
|
|
312
|
+
if path in hidden_paths:
|
|
313
|
+
return
|
|
314
|
+
if role != "Window" and len(node) > 1:
|
|
315
|
+
same = {}
|
|
316
|
+
for position, child in enumerate(node):
|
|
317
|
+
c = child.attrib
|
|
318
|
+
cw, ch = _extent(c.get("width", 0)), _extent(c.get("height", 0))
|
|
319
|
+
if (child.tag == "XCUIElementTypeOther" and math.isfinite(cw * ch)
|
|
320
|
+
and cw * ch >= WDA_PAGE_MIN_SCREEN_FRACTION * width * height):
|
|
321
|
+
frame = tuple(round(_extent(c.get(k, 0)) / 2) for k in ("x", "y", "width", "height"))
|
|
322
|
+
same.setdefault(frame, []).append(position)
|
|
323
|
+
group = max(same.values(), key=len, default=[])
|
|
324
|
+
if len(group) >= WDA_PAGE_MIN_COUNT:
|
|
325
|
+
paths = [f"{path}/{child.tag}[{index}]" for index, child in _indexed(node)]
|
|
326
|
+
pages.append((path, tuple((position, paths[position]) for position in group)))
|
|
327
|
+
if role == "Window":
|
|
328
|
+
# A presented sheet is a later screen-sized sibling of the screen it covers (iOSWorld CalTrack, 24 Sep).
|
|
329
|
+
full = [f"{path}/{child.tag}[{index}]" for index, child in _indexed(node)
|
|
330
|
+
if _extent(child.attrib.get("width", 0)) * _extent(child.attrib.get("height", 0))
|
|
331
|
+
>= WDA_LAYER_MIN_SCREEN_FRACTION * width * height]
|
|
332
|
+
if len(full) > 1:
|
|
333
|
+
layers.extend(full[:-1])
|
|
334
|
+
x, y = _extent(a.get("x", 0)), _extent(a.get("y", 0))
|
|
335
|
+
w, h = _extent(a.get("width", 0)), _extent(a.get("height", 0))
|
|
336
|
+
if cell is not None and not (cell[0] - 1 <= x + w / 2 <= cell[0] + cell[2] + 1
|
|
337
|
+
and cell[1] - 1 <= y + h / 2 <= cell[1] + cell[3] + 1):
|
|
338
|
+
# An unrendered cell's content reports a collapsed frame: Mail's phantom texts filled the budget (mem-004).
|
|
339
|
+
x, y, w, h = cell
|
|
340
|
+
if not visible and ancestor_visible and w * h == 0 and role == "Other":
|
|
341
|
+
# iOS 26 glass toolbars wrap their content in a zero-size "not visible" wrapper (iOSWorld mem-004).
|
|
342
|
+
for index, child in _indexed(node):
|
|
343
|
+
walk(child, f"{path}/{child.tag}[{index}]", True, depth + 1, named, cell)
|
|
344
|
+
return
|
|
345
|
+
if role == "SecureTextField" and visible and w * h > 0:
|
|
346
|
+
secure.append((x / width, y / height, w / width, h / height))
|
|
347
|
+
if role == "SecureTextField" or not visible:
|
|
348
|
+
return
|
|
349
|
+
if role in WDA_PANE_ROLES and w * h >= WDA_PANE_MIN_SCREEN_FRACTION * width * height:
|
|
350
|
+
key = pane_key(role, a.get("label"), x, y, w, h)
|
|
351
|
+
if key in hidden_panes:
|
|
352
|
+
return
|
|
353
|
+
panes.append(key)
|
|
354
|
+
left, top = max(0, x), max(0, y)
|
|
355
|
+
right, bottom = min(width, x + w), min(height, y + h)
|
|
356
|
+
onscreen = right > left and bottom > top
|
|
357
|
+
if role == "Keyboard" and not onscreen:
|
|
358
|
+
parked.append(path)
|
|
359
|
+
if onscreen:
|
|
360
|
+
label, value = _wda_text(a, role)
|
|
361
|
+
if named and role == "Button" and a.get("accessible") != "true" and named[1] == (x, y, w, h):
|
|
362
|
+
# TasteRank's "Filters" wraps a button named for its SF symbol ("Drag").
|
|
363
|
+
label = invisible_marks(named[0])[:500]
|
|
364
|
+
g = WDA_RECT_GRID_PT
|
|
365
|
+
s_left, s_top, s_right, s_bottom = (round(left / g) * g, round(top / g) * g,
|
|
366
|
+
round(right / g) * g, round(bottom / g) * g)
|
|
367
|
+
if s_right > s_left and s_bottom > s_top:
|
|
368
|
+
left, top = s_left, s_top
|
|
369
|
+
right, bottom = min(width, s_right), min(height, s_bottom)
|
|
370
|
+
nodes.append((path, role, label, value, (left, top, right, bottom),
|
|
371
|
+
a.get("enabled", "true") != "false"))
|
|
372
|
+
if rich:
|
|
373
|
+
# QuickBite's cart bar is an unlabelled Other with an id: multi-088 spent eight turns on it (iOSWorld).
|
|
374
|
+
if (role == "Other" and len(node) == 0 and label and a.get("accessible", "true") != "false"
|
|
375
|
+
and w >= OTHER_TARGET_MIN_PT and h >= OTHER_TARGET_MIN_PT
|
|
376
|
+
and w * h < OTHER_TARGET_MAX_SCREEN_FRACTION * width * height):
|
|
377
|
+
other_leaves.add(path)
|
|
378
|
+
if "Selected" in (a.get("traits") or "").split(", "):
|
|
379
|
+
selected_paths.add(path)
|
|
380
|
+
if role in editable_roles and a.get("placeholderValue"):
|
|
381
|
+
placeholders[path] = invisible_marks(a.get("placeholderValue"))[:500]
|
|
382
|
+
wrapper = ((a.get("label"), (x, y, w, h)) if role == "Other" and a.get("accessible", "true") == "true"
|
|
383
|
+
and a.get("label") else None)
|
|
384
|
+
counts = {}
|
|
385
|
+
for child in node:
|
|
386
|
+
counts[child.tag] = counts.get(child.tag, 0) + 1
|
|
387
|
+
# Never prune by a parent's frame: Safari's web containers scroll away while their children stay.
|
|
388
|
+
walk(child, f"{path}/{child.tag}[{counts[child.tag]}]", visible, depth + 1, wrapper,
|
|
389
|
+
(x, y, w, h) if role == "Cell" and w * h > 0 else cell)
|
|
390
|
+
|
|
391
|
+
walk(root, f"/{root.tag}")
|
|
392
|
+
covered = wda_occlusion(nodes, width, height)
|
|
393
|
+
keyboard_up = any(node[1] == "Keyboard" and not covered(node[0], *node[4]) for node in nodes)
|
|
394
|
+
keyboard = "visible" if keyboard_up else "parked" if parked else ""
|
|
395
|
+
field_actions = ("TAP", "TYPE", "TYPE_SUBMIT", "SUBMIT") if keyboard else ("TAP", "TYPE", "TYPE_SUBMIT")
|
|
396
|
+
hits = content_hits(nodes)
|
|
397
|
+
bands = chrome_bands(nodes, width, height, covered)
|
|
398
|
+
order = {node[0]: index for index, node in enumerate(nodes)} if bands else {}
|
|
399
|
+
elements, texts = [], []
|
|
400
|
+
for path, role, label, value, (left, top, right, bottom), enabled in nodes:
|
|
401
|
+
if covered(path, left, top, right, bottom):
|
|
402
|
+
continue
|
|
403
|
+
if label or value:
|
|
404
|
+
texts.append(label if value.strip() == label.strip() else (label + " " + value).strip())
|
|
405
|
+
# Unlabelled pickers and buttons stay targets (iOSWorld multi-031 alarm time, multi-068 CloudDocs' "+").
|
|
406
|
+
unlabelled_button = (role == "Button" and right - left >= UNLABELLED_BUTTON_MIN_PT
|
|
407
|
+
and bottom - top >= UNLABELLED_BUTTON_MIN_PT)
|
|
408
|
+
other_leaf = path in other_leaves
|
|
409
|
+
if (enabled or rich) and (label or role in editable_roles or role in ADJUSTABLE_ROLES and value.strip()
|
|
410
|
+
or unlabelled_button) and (role not in structural or other_leaf):
|
|
411
|
+
if len(elements) >= 240:
|
|
412
|
+
continue # past the target budget the rest stays text
|
|
413
|
+
hit = hits.get(path)
|
|
414
|
+
if bands:
|
|
415
|
+
span = visible_span(path, order[path], left, top, right, bottom, bands)
|
|
416
|
+
if span is None:
|
|
417
|
+
continue # drawn over by a bar or the keyboard
|
|
418
|
+
if not span[0] <= (hit[1] if hit else (top + bottom) / 2) <= span[1]:
|
|
419
|
+
hit = ((hit[0] if hit else (left + right) / 2), (span[0] + span[1]) / 2)
|
|
420
|
+
elements.append(Element(str(len(elements)), label, role,
|
|
421
|
+
(left / width, top / height, (right-left) / width, (bottom-top) / height),
|
|
422
|
+
role in editable_roles, path, value,
|
|
423
|
+
field_actions if role in editable_roles else ("TAP",),
|
|
424
|
+
_hit_inside(hit, left, top, right, bottom, width, height), enabled, path in selected_paths,
|
|
425
|
+
placeholders.get(path, "")))
|
|
426
|
+
return Snapshot(elements, "\n".join(dict.fromkeys(texts))[:12000], width, height, "wda",
|
|
427
|
+
bundle_id=bundle, stacked_panes=stacked_panes(panes),
|
|
428
|
+
stacked_layers=tuple(layers), stacked_pages=tuple(pages), keyboard=keyboard,
|
|
429
|
+
secure_fields=tuple(secure))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
# A SwiftUI row with a Spacer answers taps only on its content (iOSWorld mem-048, multi-087: 21 dead taps).
|
|
433
|
+
HIT_ROW_MIN_PT = 120
|
|
434
|
+
CONTENT_ROLES = frozenset({"StaticText", "Image"})
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def content_hits(nodes):
|
|
438
|
+
hits = {}
|
|
439
|
+
for index, (path, role, _, _, (left, top, right, bottom), _) in enumerate(nodes):
|
|
440
|
+
if role not in ("Button", "Cell") or right - left < HIT_ROW_MIN_PT:
|
|
441
|
+
continue
|
|
442
|
+
cx, cy = (left + right) / 2, (top + bottom) / 2
|
|
443
|
+
content, prefix = [], path + "/"
|
|
444
|
+
for other in nodes[index + 1:]: # preorder
|
|
445
|
+
if not other[0].startswith(prefix):
|
|
446
|
+
break
|
|
447
|
+
if other[1] in CONTENT_ROLES:
|
|
448
|
+
content.append(other)
|
|
449
|
+
if not content or any(l <= cx <= r and t <= cy <= b for _, _, _, _, (l, t, r, b), _ in content):
|
|
450
|
+
continue
|
|
451
|
+
texts = [c for c in content if c[1] == "StaticText" and (c[2] or c[3])] or content
|
|
452
|
+
l, t, r, b = texts[0][4]
|
|
453
|
+
hits[path] = ((l + r) / 2, (t + b) / 2)
|
|
454
|
+
return hits
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
BAR_MARGIN_PT, KEYBOARD_BACKDROP_PT = 6, 24
|
|
458
|
+
BAR_MIN_BUTTONS, BAR_MIN_SPAN, BAR_MAX_BUTTON_HEIGHT = 3, .6, .09
|
|
459
|
+
BAR_LOW, BAR_LINE_PT = .55, 14
|
|
460
|
+
BAR_FOOT = .85
|
|
461
|
+
# Safari's bottom bar: 98 of 852 pt.
|
|
462
|
+
BAR_MAX_HEIGHT = .2
|
|
463
|
+
# Bars and the keyboard catch touches past their edges: a 26 pt strip took no taps (live, 25 Sep).
|
|
464
|
+
MIN_VISIBLE_PT = 30
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def chrome_bands(nodes, width, height, covered=lambda *node: False):
|
|
468
|
+
# SwiftUI tab bars are rows of buttons: QuickChat's sat over a result and 16 taps missed (iOSWorld multi-087).
|
|
469
|
+
bands = []
|
|
470
|
+
for index, (path, role, _, _, (left, top, right, bottom), _) in enumerate(nodes):
|
|
471
|
+
if role == "Keyboard" and bottom > top:
|
|
472
|
+
# Down to the screen's edge: its emoji and dictation row sits below the node.
|
|
473
|
+
bands.append(((path,), len(nodes), True, left, max(0, top - KEYBOARD_BACKDROP_PT), right, height))
|
|
474
|
+
elif role == "TabBar" and not covered(path, left, top, right, bottom):
|
|
475
|
+
bands.append(((path,), index, True, left, top - BAR_MARGIN_PT, right, _bar_bottom(bottom, height)))
|
|
476
|
+
rows = {}
|
|
477
|
+
frames = {node[0]: node[4] for node in nodes}
|
|
478
|
+
for index, (path, role, _, _, (left, top, right, bottom), _) in enumerate(nodes):
|
|
479
|
+
if (role == "Button" and 0 < bottom - top <= BAR_MAX_BUTTON_HEIGHT * height and bottom >= BAR_LOW * height
|
|
480
|
+
and not any(marker in path for marker in SCROLLING)
|
|
481
|
+
and not covered(path, left, top, right, bottom)):
|
|
482
|
+
rows.setdefault(_bar_owner(path, frames, width, height), []).append((index, left, top, right, bottom,
|
|
483
|
+
path))
|
|
484
|
+
for buttons in rows.values():
|
|
485
|
+
for line in _lines(buttons):
|
|
486
|
+
if (len({round(m[1]) for m in line}) >= BAR_MIN_BUTTONS
|
|
487
|
+
and max(m[3] for m in line) - min(m[1] for m in line) >= BAR_MIN_SPAN * width):
|
|
488
|
+
# Owned by its buttons only: their parent can hold the screen's content too.
|
|
489
|
+
bands.append((tuple(m[5] for m in line), min(m[0] for m in line), True, min(m[1] for m in line),
|
|
490
|
+
min(m[2] for m in line) - BAR_MARGIN_PT, max(m[3] for m in line),
|
|
491
|
+
_bar_bottom(max(m[4] for m in line), height)))
|
|
492
|
+
return bands
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _bar_owner(path, frames, width, height):
|
|
496
|
+
# Safari's bottom bar puts its buttons in separate containers inside one (Quick mode, 10 Oct: 3 of 3 runs).
|
|
497
|
+
parent = path.rsplit("/", 1)[0]
|
|
498
|
+
ancestor = parent
|
|
499
|
+
while ancestor.count("/") > 1:
|
|
500
|
+
frame = frames.get(ancestor)
|
|
501
|
+
if frame is not None:
|
|
502
|
+
left, top, right, bottom = frame
|
|
503
|
+
if (right - left >= BAR_MIN_SPAN * width and top >= BAR_LOW * height
|
|
504
|
+
and bottom - top <= BAR_MAX_HEIGHT * height):
|
|
505
|
+
return ancestor
|
|
506
|
+
if bottom - top > BAR_MAX_HEIGHT * height:
|
|
507
|
+
break # past the bar: this frame holds the screen
|
|
508
|
+
ancestor = ancestor.rsplit("/", 1)[0]
|
|
509
|
+
return parent
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _bar_bottom(bottom, height):
|
|
513
|
+
# A bar's background runs under the home indicator (multi-079, 25 Sep).
|
|
514
|
+
return height if bottom >= BAR_FOOT * height else bottom + BAR_MARGIN_PT
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _lines(buttons):
|
|
518
|
+
lines = []
|
|
519
|
+
for button in sorted(buttons, key=lambda m: (m[2] + m[4]) / 2):
|
|
520
|
+
centre = (button[2] + button[4]) / 2
|
|
521
|
+
if lines and centre - (lines[-1][0][2] + lines[-1][0][4]) / 2 <= BAR_LINE_PT:
|
|
522
|
+
lines[-1].append(button)
|
|
523
|
+
else:
|
|
524
|
+
lines.append([button])
|
|
525
|
+
return lines
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
SCROLLING = ("XCUIElementTypeScrollView[", "XCUIElementTypeTable[", "XCUIElementTypeCollectionView[")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def visible_span(path, position, left, top, right, bottom, bands):
|
|
532
|
+
spans, scrolls = [(top, bottom)], any(marker in path for marker in SCROLLING)
|
|
533
|
+
for owners, order, over_scroll, b_left, b_top, b_right, b_bottom in bands:
|
|
534
|
+
if order <= position and not (over_scroll and scrolls) or any(
|
|
535
|
+
path == owner or path.startswith(owner + "/") for owner in owners):
|
|
536
|
+
continue
|
|
537
|
+
if min(right, b_right) - max(left, b_left) < .5 * (right - left):
|
|
538
|
+
continue
|
|
539
|
+
spans = [piece for s_top, s_bottom in spans for piece in
|
|
540
|
+
((s_top, min(s_bottom, b_top)), (max(s_top, b_bottom), s_bottom)) if piece[1] - piece[0] > 0]
|
|
541
|
+
best = max(spans, key=lambda span: span[1] - span[0], default=None)
|
|
542
|
+
return best if best and best[1] - best[0] >= min(MIN_VISIBLE_PT, .6 * (bottom - top)) else None
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _hit_inside(point, left, top, right, bottom, width, height):
|
|
546
|
+
if point is None or not (left <= point[0] <= right and top <= point[1] <= bottom):
|
|
547
|
+
return ()
|
|
548
|
+
return point[0] / width, point[1] / height
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _indexed(node):
|
|
552
|
+
counts = {}
|
|
553
|
+
for child in node:
|
|
554
|
+
counts[child.tag] = counts.get(child.tag, 0) + 1
|
|
555
|
+
yield counts[child.tag], child
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def stacked_panes(panes):
|
|
559
|
+
# Notes' search results come first in document order yet are drawn in front (MobsterBench pass 6).
|
|
560
|
+
stacked = []
|
|
561
|
+
for key in panes:
|
|
562
|
+
_, _, x, y, w, h = key
|
|
563
|
+
for other in panes:
|
|
564
|
+
if other is key or other == key:
|
|
565
|
+
continue
|
|
566
|
+
_, _, ox, oy, ow, oh = other
|
|
567
|
+
overlap = max(0, min(x + w, ox + ow) - max(x, ox)) * max(0, min(y + h, oy + oh) - max(y, oy))
|
|
568
|
+
if overlap >= .5 * min(w * h, ow * oh):
|
|
569
|
+
stacked.append(key)
|
|
570
|
+
break
|
|
571
|
+
return tuple(stacked)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# iOS 26 glass controls morph by ~1 pt for a second after a transition, which made every read a new screen.
|
|
575
|
+
WDA_RECT_GRID_PT = 4
|
|
576
|
+
# Pages mid-load exceeded the old 4000.
|
|
577
|
+
WDA_NODE_BUDGET = 12000
|
|
578
|
+
WDA_IDENTIFIER = re.compile(r"\S*[?=&]\S*|\S*(Identifier|Controller|ViewController)")
|
|
579
|
+
# Without WDA's (8x slower) `visible`, rows scrolled under a translucent bar would be tapped through it.
|
|
580
|
+
WDA_OCCLUDING_ROLES = frozenset({"NavigationBar", "TabBar", "Toolbar", "Keyboard"})
|
|
581
|
+
WDA_MODAL_ROLES = frozenset({"Alert", "Sheet"})
|
|
582
|
+
# iOS keeps the previous list in the tree with on-screen rects (Settings' rows under its search results).
|
|
583
|
+
WDA_PANE_ROLES = frozenset({"Table", "CollectionView"})
|
|
584
|
+
WDA_PANE_MIN_SCREEN_FRACTION = .4
|
|
585
|
+
WDA_LAYER_MIN_SCREEN_FRACTION = .9
|
|
586
|
+
# Two same-frame pages are more often an overlay and its host: LockedIn paid a probe on every read (25 Sep).
|
|
587
|
+
WDA_PAGE_MIN_SCREEN_FRACTION = .4
|
|
588
|
+
WDA_PAGE_MIN_COUNT = 3
|
|
589
|
+
ADJUSTABLE_ROLES = frozenset({"PickerWheel"})
|
|
590
|
+
UNLABELLED_BUTTON_MIN_PT = 20
|
|
591
|
+
# A leaf that fills the screen is a backdrop, not a control.
|
|
592
|
+
OTHER_TARGET_MIN_PT = 44
|
|
593
|
+
OTHER_TARGET_MAX_SCREEN_FRACTION = .5
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def wda_occlusion(nodes, width=None, height=None):
|
|
597
|
+
def inside(path, owner):
|
|
598
|
+
return path == owner or path.startswith(owner + "/")
|
|
599
|
+
chrome = [(node[0], node[4]) for node in nodes if node[1] in WDA_OCCLUDING_ROLES]
|
|
600
|
+
# Document order is back-to-front.
|
|
601
|
+
modal = next((node[0] for node in reversed(nodes) if node[1] in WDA_MODAL_ROLES), None)
|
|
602
|
+
order = {node[0]: index for index, node in enumerate(nodes)}
|
|
603
|
+
screen = (width * height) if width and height else None
|
|
604
|
+
panes = [(index, node[0], node[4]) for index, node in enumerate(nodes)
|
|
605
|
+
if node[1] in WDA_PANE_ROLES and screen
|
|
606
|
+
and (node[4][2] - node[4][0]) * (node[4][3] - node[4][1]) >= WDA_PANE_MIN_SCREEN_FRACTION * screen]
|
|
607
|
+
|
|
608
|
+
def covered(path, left, top, right, bottom):
|
|
609
|
+
if modal and not inside(path, modal) and not inside(modal, path):
|
|
610
|
+
return True
|
|
611
|
+
cx, cy = (left + right) / 2, (top + bottom) / 2
|
|
612
|
+
# A sheet presented later sits in front of the old screen's bar (Reminders' New List Done was dropped).
|
|
613
|
+
position = order.get(path, -1)
|
|
614
|
+
if any(l <= cx <= r and t <= cy <= b and not inside(path, owner) and not inside(owner, path)
|
|
615
|
+
and position < order.get(owner, -1)
|
|
616
|
+
for owner, (l, t, r, b) in chrome):
|
|
617
|
+
return True
|
|
618
|
+
if len(panes) < 2 or any(inside(path, owner) for owner, _ in chrome):
|
|
619
|
+
return False
|
|
620
|
+
index = order.get(path, -1)
|
|
621
|
+
return any(pane_index > index and l <= cx <= r and t <= cy <= b
|
|
622
|
+
and not inside(path, pane) and not inside(pane, path)
|
|
623
|
+
for pane_index, pane, (l, t, r, b) in panes)
|
|
624
|
+
return covered
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
STACKED_ARTIFACT = 5
|
|
628
|
+
SYMBOL_NAME = re.compile(r"^[a-z][a-z0-9]*(?:\.[a-z0-9]+)+$") # chevron.forward, not 26.0.1
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def redundant_ids(elements):
|
|
632
|
+
# A Settings row is a Button plus its texts and chevron, one tap: offering all four overflowed the model's input.
|
|
633
|
+
def get(element, name):
|
|
634
|
+
return element.get(name) if isinstance(element, dict) else getattr(element, name, None)
|
|
635
|
+
rows = [e for e in elements if get(e, "role") in ("Button", "Cell") and (get(e, "label") or "").strip()]
|
|
636
|
+
redundant = set()
|
|
637
|
+
# Many texts at one frame are a layout artifact: Settings > Notifications put ~180 rows at one rect.
|
|
638
|
+
stacks = {}
|
|
639
|
+
for element in elements:
|
|
640
|
+
if not get(element, "editable"):
|
|
641
|
+
stacks.setdefault(tuple(round(v, 3) for v in get(element, "rect")[:2])
|
|
642
|
+
+ (round(get(element, "rect")[3], 3),),
|
|
643
|
+
[]).append(get(element, "id"))
|
|
644
|
+
for ids in stacks.values():
|
|
645
|
+
if len(ids) >= STACKED_ARTIFACT:
|
|
646
|
+
redundant.update(ids)
|
|
647
|
+
for element in elements:
|
|
648
|
+
role, label = get(element, "role"), (get(element, "label") or "").strip()
|
|
649
|
+
if get(element, "editable") or role not in ("StaticText", "Image"):
|
|
650
|
+
continue
|
|
651
|
+
if role == "Image" and SYMBOL_NAME.match(label):
|
|
652
|
+
redundant.add(get(element, "id"))
|
|
653
|
+
continue
|
|
654
|
+
x, y, w, h = get(element, "rect")
|
|
655
|
+
cx, cy = x + w / 2, y + h / 2
|
|
656
|
+
for row in rows:
|
|
657
|
+
rx, ry, rw, rh = get(row, "rect")
|
|
658
|
+
if (rx <= cx <= rx + rw and ry <= cy <= ry + rh and label
|
|
659
|
+
and label.casefold() in (get(row, "label") or "").casefold()):
|
|
660
|
+
redundant.add(get(element, "id"))
|
|
661
|
+
break
|
|
662
|
+
return redundant
|