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
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""What the X-ray crawler may tap. It fails closed: only a navigation-shaped control whose words name no act."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .. import harness_api
|
|
7
|
+
from ..packs.core import ACT_LABEL, NEVER
|
|
8
|
+
from ..secret_filter import redact
|
|
9
|
+
|
|
10
|
+
# Words a crawler never taps, on top of the engine's commit words, wherever they sit in a label. Whole words, case
|
|
11
|
+
# folded.
|
|
12
|
+
# Screen text is data: a label is matched against these lists and never adds to them.
|
|
13
|
+
DENY = re.compile(
|
|
14
|
+
r"\b(delete|erase|reset|wipe|remove|sign ?out|log ?out|sign ?off|deactivate|disable account|"
|
|
15
|
+
r"buy|purchase|subscribe|subscription|trial|try (?:it )?free|"
|
|
16
|
+
r"upgrade|go premium|get premium|get pro|unlock|pay|checkout|check out|order|book|donate|tip|"
|
|
17
|
+
r"call|dial|facetime|message|send|post|share|invite|publish|upload|reply|comment|"
|
|
18
|
+
r"allow|don'?t allow|accept|agree|decline|approve|confirm|submit|verify|authenticate|"
|
|
19
|
+
r"sign ?in|log ?in|sign ?up|register|create (?:an )?account|"
|
|
20
|
+
r"block|mute|unfollow|follow|unmatch|cancel subscription|install|download|"
|
|
21
|
+
r"turn (?:on|off)|enable|disable|disconnect|pair|forget|"
|
|
22
|
+
r"emergency|sos|power off|shut ?down|restart|redeem|claim|activate)\b", re.I)
|
|
23
|
+
# Act verbs common on screens as nouns ("Feels like 67°", "Clear"): they count only as a label's first word.
|
|
24
|
+
LEADING_ONLY = ("like", "likes", "liked", "react", "get", "request", "cancel", "clear", "next", "update", "report",
|
|
25
|
+
"done", "continue", "start", "begin", "restore", "apply", "join", "connect", "save", "open in", "go")
|
|
26
|
+
_LEADING_ONLY = re.compile(r"\b(" + "|".join(re.escape(word) for word in LEADING_ONLY) + r")\b", re.I)
|
|
27
|
+
# A label that starts with any of these, or with a word above, reads as a command: never tapped, whatever follows.
|
|
28
|
+
LEADING = re.compile(r"^(?:" + "|".join(re.escape(word) for word in LEADING_ONLY) + r"|add|new|create|edit|compose|"
|
|
29
|
+
r"write|record|scan|play|shuffle|try|rate|leave|end|archive|hide|pin|unpin|mark|empty|free up|"
|
|
30
|
+
r"offload|translate|retry|skip|ok|okay|yes|no|tap|swipe|press|click|accept|send)\b", re.I)
|
|
31
|
+
BUTTON_VERB = re.compile(r"^(edit|add|new|create|open|get|go|try|start|join|sign in|log in|sign up|learn more|"
|
|
32
|
+
r"skip|close|ok|yes|no|retry|refresh|search|filter|sort|select|view|show|hide|play|"
|
|
33
|
+
r"pause|stop|record|scan)\b", re.I)
|
|
34
|
+
SEE_ALL = re.compile(r"^(see all|show all|view all|more|show more|see more|all)\b", re.I)
|
|
35
|
+
# A developer's identifier standing in for a label: where it leads is unknown.
|
|
36
|
+
IDENTIFIER = re.compile(r"^(?=.*[_.]|[a-z0-9]+(?:-[a-z0-9]+){2,}$)[A-Za-z0-9_.\-]+$")
|
|
37
|
+
# Roles never tapped.
|
|
38
|
+
CONTROL_ROLES = frozenset({"Switch", "Toggle", "Slider", "Stepper", "Picker", "PickerWheel", "DatePicker",
|
|
39
|
+
"SegmentedControl", "TextField", "SecureTextField", "SearchField", "TextView", "Key",
|
|
40
|
+
"Keyboard", "Menu", "MenuItem", "Alert", "Sheet", "Image", "Icon", "StatusBar",
|
|
41
|
+
"NavigationBar", "Toolbar", "TabBar", "ProgressIndicator", "ActivityIndicator",
|
|
42
|
+
"PageIndicator", "ScrollBar", "Map", "WebView"})
|
|
43
|
+
TOGGLES = frozenset({"Switch", "Toggle"})
|
|
44
|
+
CHROME_ROLES = frozenset({"Toolbar", "TabBar", "SearchField", "Keyboard"})
|
|
45
|
+
CHEVRON = re.compile(r"^(chevron\.(forward|right)|disclosure|›|>)$", re.I)
|
|
46
|
+
# A row's own info button ("More Info", the circled i): the button opens the details, and the row itself may act
|
|
47
|
+
# (a Wi-Fi network joins, a Bluetooth device connects). Such a row is never tapped.
|
|
48
|
+
INFO = re.compile(r"^(more info|info|info\.circle|i)$", re.I)
|
|
49
|
+
CARD_MIN_H, CARD_MIN_W = .1, .3
|
|
50
|
+
DATA_ROW_MIN_W, DATA_ROW_PARTS = .7, 3
|
|
51
|
+
ICON_WORDS = frozenset({"heart", "star", "bookmark", "plus", "minus", "ellipsis", "more", "menu", "close", "xmark",
|
|
52
|
+
"trash", "camera", "mic", "microphone", "play", "pause", "share", "cart", "bag", "bell",
|
|
53
|
+
"search", "filter", "gift", "favorite", "favourite", "settings", "gear", "info"})
|
|
54
|
+
# A row outside the content area (under the bars) is reached by scrolling, not tapped where it sits.
|
|
55
|
+
CONTENT_TOP, CONTENT_BOTTOM = .12, .88
|
|
56
|
+
TAB_BAR_TOP = .88
|
|
57
|
+
OCR_ROW_LEFT_MAX = .4
|
|
58
|
+
OCR_ROW_MAX_WORDS = 5
|
|
59
|
+
OCR_ALIGN = .03
|
|
60
|
+
OCR_CENTRED = .1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Verdict:
|
|
65
|
+
ok: bool
|
|
66
|
+
reason: str # why: "cell", "chevron row", "tab", "link", "see all", "list row" when ok; the refusal otherwise
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def image_only(snapshot):
|
|
70
|
+
# Unsure is image-only, where stricter rules apply: it fails closed.
|
|
71
|
+
if getattr(snapshot, "source", "") in ("mirroring", "screen", "ocr"):
|
|
72
|
+
return True
|
|
73
|
+
roles = {element.role for element in snapshot.elements}
|
|
74
|
+
return not roles - {"StaticText", "SearchField", "TextField"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def plain(text):
|
|
78
|
+
"""A label on one line, masked here at the source (secret_filter), so no map, event or report holds a secret."""
|
|
79
|
+
return redact(" ".join(str(text or "").replace("\u00a0", " ").split()))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _first_word_only(text):
|
|
83
|
+
"""``text`` with the LEADING_ONLY words after its first word taken out: what the act lists read."""
|
|
84
|
+
first, _, rest = text.partition(" ")
|
|
85
|
+
return " ".join((first + " " + _LEADING_ONLY.sub(" ", rest)).split())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def act_words(label):
|
|
89
|
+
text = plain(label)
|
|
90
|
+
if not text:
|
|
91
|
+
return ""
|
|
92
|
+
lead = LEADING.match(text)
|
|
93
|
+
if lead:
|
|
94
|
+
return f"never tapped: “{lead.group(0).lower()}”"
|
|
95
|
+
read = _first_word_only(text)
|
|
96
|
+
act = harness_api.commit_act("TAP", read, None, None)
|
|
97
|
+
if act:
|
|
98
|
+
return f"names an act ({act})"
|
|
99
|
+
if ACT_LABEL.match(text) or NEVER.search(read):
|
|
100
|
+
return "names an act"
|
|
101
|
+
# An identifier's words count too: "AUTH_PROX_SIGN_IN_CELL" says "sign in".
|
|
102
|
+
spaced = re.sub(r"[_\-.]+", " ", text)
|
|
103
|
+
spaced_read = _first_word_only(spaced)
|
|
104
|
+
found = (DENY.search(read) or DENY.search(spaced_read) or LEADING.match(spaced)
|
|
105
|
+
or (NEVER.search(spaced_read) if spaced != text else None))
|
|
106
|
+
if found:
|
|
107
|
+
return f"never tapped: “{found.group(0).lower()}”"
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _inside(point, rect, pad=0.0):
|
|
112
|
+
x, y = point
|
|
113
|
+
rx, ry, rw, rh = rect
|
|
114
|
+
return rx - pad <= x <= rx + rw + pad and ry - pad <= y <= ry + rh + pad
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _contains(outer, inner):
|
|
118
|
+
return (outer[0] - .005 <= inner[0] and outer[1] - .005 <= inner[1]
|
|
119
|
+
and inner[0] + inner[2] <= outer[0] + outer[2] + .005 and inner[1] + inner[3] <= outer[1] + outer[3] + .005)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def in_tab_bar(element, snapshot):
|
|
123
|
+
if "XCUIElementTypeTabBar" in (element.locator or ""):
|
|
124
|
+
return True
|
|
125
|
+
bars = [e for e in snapshot.elements if e.role == "TabBar"]
|
|
126
|
+
return any(_inside(element.center, bar.rect) for bar in bars)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def holds_toggle(element, snapshot):
|
|
130
|
+
"""A cell or row with a switch inside it: tapping it could flip the switch."""
|
|
131
|
+
for other in snapshot.elements:
|
|
132
|
+
if other is element or other.role not in TOGGLES:
|
|
133
|
+
continue
|
|
134
|
+
if _inside(other.center, element.rect) or (element.locator and other.locator.startswith(element.locator + "/")):
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def has_chevron(element, snapshot):
|
|
140
|
+
"""A row with a disclosure chevron: an image named like one inside it (Settings' rows: chevron.forward)."""
|
|
141
|
+
for other in snapshot.elements:
|
|
142
|
+
if other is element or not CHEVRON.match(plain(other.label)):
|
|
143
|
+
continue
|
|
144
|
+
if (element.locator and other.locator.startswith(element.locator + "/")) or _inside(other.center, element.rect):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def holds_info(element, snapshot):
|
|
150
|
+
"""A row with an info button inside it (Settings' Wi-Fi networks and Bluetooth devices)."""
|
|
151
|
+
for other in snapshot.elements:
|
|
152
|
+
if other is element or other.role not in ("Button", "Image") or not INFO.match(plain(other.label)):
|
|
153
|
+
continue
|
|
154
|
+
if (element.locator and other.locator.startswith(element.locator + "/")) or _inside(other.center, element.rect):
|
|
155
|
+
return True
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
CHECKMARK = re.compile(r"^(selected|checkmark|checked|checkmark\.circle(\.fill)?)$", re.I)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def choice_list(snapshot):
|
|
163
|
+
# A tap on a row of a list of options picks one, which changes a setting. A selected tab or segment is not one.
|
|
164
|
+
for element in snapshot.elements:
|
|
165
|
+
if in_tab_bar(element, snapshot) or "SegmentedControl" in (element.locator or ""):
|
|
166
|
+
continue
|
|
167
|
+
if element.selected and element.role == "Cell":
|
|
168
|
+
return True
|
|
169
|
+
if element.role in ("Image", "Button") and CHECKMARK.match(plain(element.label)):
|
|
170
|
+
return True
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def under_chrome(element, snapshot):
|
|
175
|
+
"""Whether the tap would land on a toolbar, a search bar or the keyboard drawn over the content."""
|
|
176
|
+
point = element.tap_point
|
|
177
|
+
for other in snapshot.elements:
|
|
178
|
+
if other is element or other.role not in CHROME_ROLES:
|
|
179
|
+
continue
|
|
180
|
+
if _inside(point, other.rect):
|
|
181
|
+
return True
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def decide(element, snapshot):
|
|
186
|
+
"""Whether the crawler may tap ``element`` on ``snapshot``, and why."""
|
|
187
|
+
label = plain(element.label)
|
|
188
|
+
words = act_words(label) or act_words(element.value if element.role not in TOGGLES else "")
|
|
189
|
+
if words:
|
|
190
|
+
return Verdict(False, words)
|
|
191
|
+
if element.role in TOGGLES or element.role in CONTROL_ROLES:
|
|
192
|
+
return Verdict(False, "a control, not navigation" if element.role not in TOGGLES else "a switch")
|
|
193
|
+
if element.editable:
|
|
194
|
+
return Verdict(False, "a text field")
|
|
195
|
+
if not element.enabled or "TAP" not in element.actions:
|
|
196
|
+
return Verdict(False, "not tappable")
|
|
197
|
+
if not label:
|
|
198
|
+
return Verdict(False, "no label")
|
|
199
|
+
if holds_toggle(element, snapshot):
|
|
200
|
+
return Verdict(False, "holds a switch")
|
|
201
|
+
if image_only(snapshot):
|
|
202
|
+
return _decide_ocr(element, snapshot, label)
|
|
203
|
+
if IDENTIFIER.match(label):
|
|
204
|
+
return Verdict(False, "no readable label")
|
|
205
|
+
x, y = element.tap_point
|
|
206
|
+
tab = in_tab_bar(element, snapshot)
|
|
207
|
+
if not tab and not CONTENT_TOP <= y <= CONTENT_BOTTOM:
|
|
208
|
+
return Verdict(False, "outside the content area")
|
|
209
|
+
if not tab and under_chrome(element, snapshot):
|
|
210
|
+
return Verdict(False, "under a bar")
|
|
211
|
+
if tab and element.role == "Button":
|
|
212
|
+
return Verdict(True, "tab")
|
|
213
|
+
if choice_list(snapshot) and not has_chevron(element, snapshot) and element.role != "Link":
|
|
214
|
+
return Verdict(False, "an option in a choice list")
|
|
215
|
+
if holds_info(element, snapshot):
|
|
216
|
+
return Verdict(False, "a row with its own info button: the row itself may join or connect")
|
|
217
|
+
if element.role == "Cell":
|
|
218
|
+
return Verdict(True, "cell")
|
|
219
|
+
if element.role == "Link":
|
|
220
|
+
return Verdict(True, "link")
|
|
221
|
+
if element.role == "Button" and has_chevron(element, snapshot):
|
|
222
|
+
return Verdict(True, "chevron row")
|
|
223
|
+
if element.role == "Button" and "XCUIElementTypeCell[" in (element.locator or ""):
|
|
224
|
+
return Verdict(True, "cell")
|
|
225
|
+
if element.role in ("Button", "StaticText") and SEE_ALL.match(label):
|
|
226
|
+
return Verdict(True, "see all")
|
|
227
|
+
if element.role in ("Button", "Other") and card_shaped(element, label):
|
|
228
|
+
return Verdict(True, "card")
|
|
229
|
+
return Verdict(False, "not navigation-shaped")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def card_shaped(element, label):
|
|
233
|
+
"""A card (tall) or a full-width row of data whose label isn't one icon's word: Weather's modules and days."""
|
|
234
|
+
x, y, w, h = element.rect
|
|
235
|
+
if label.casefold() in ICON_WORDS or len(re.findall(r"[A-Za-z]{2,}", label)) == 0:
|
|
236
|
+
return False
|
|
237
|
+
if " " not in label and re.search(r"[a-z][A-Z]", label):
|
|
238
|
+
return False # one token with capitals inside it ("KxLxpHjhdScjmXV"): an id, not words
|
|
239
|
+
if h >= CARD_MIN_H and w >= CARD_MIN_W:
|
|
240
|
+
return True
|
|
241
|
+
return w >= DATA_ROW_MIN_W and len([part for part in label.split(",") if part.strip()]) >= DATA_ROW_PARTS
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _decide_ocr(element, snapshot, label):
|
|
245
|
+
"""Image-only: a list row or a tab label, from where the line sits and what sits beside it."""
|
|
246
|
+
x, y, w, h = element.rect
|
|
247
|
+
cx, cy = element.center
|
|
248
|
+
words = label.split()
|
|
249
|
+
if any(ch.isdigit() for ch in label) and not re.search(r"[A-Za-z]{3}", label):
|
|
250
|
+
return Verdict(False, "a number")
|
|
251
|
+
if re.fullmatch(r"[A-Z0-9 &'’.,-]{2,}", label) and len(label) > 3:
|
|
252
|
+
return Verdict(False, "a heading")
|
|
253
|
+
if BUTTON_VERB.match(label) and len(words) <= 3:
|
|
254
|
+
return Verdict(False, "reads like a button")
|
|
255
|
+
if label.endswith((".", "!", "?", ":")) or len(words) > OCR_ROW_MAX_WORDS:
|
|
256
|
+
return Verdict(False, "a sentence")
|
|
257
|
+
lines = [e for e in snapshot.elements if e is not element and e.label]
|
|
258
|
+
if cy >= TAB_BAR_TOP:
|
|
259
|
+
row = [e for e in lines if abs(e.center[1] - cy) < .015 and len(plain(e.label).split()) <= 2]
|
|
260
|
+
if len(row) >= 2 and len(words) <= 2:
|
|
261
|
+
return Verdict(True, "tab")
|
|
262
|
+
return Verdict(False, "outside the content area")
|
|
263
|
+
if not CONTENT_TOP <= cy <= CONTENT_BOTTOM:
|
|
264
|
+
return Verdict(False, "outside the content area")
|
|
265
|
+
if abs(cx - .5) < OCR_CENTRED and x > .15:
|
|
266
|
+
return Verdict(False, "centred, reads like a button")
|
|
267
|
+
if SEE_ALL.match(label):
|
|
268
|
+
return Verdict(True, "see all")
|
|
269
|
+
if x > OCR_ROW_LEFT_MAX:
|
|
270
|
+
return Verdict(False, "not a list row")
|
|
271
|
+
aligned = [e for e in lines if abs(e.rect[0] - x) < OCR_ALIGN and e.center[1] != cy
|
|
272
|
+
and abs(e.center[1] - cy) < .12 and len(plain(e.label).split()) <= OCR_ROW_MAX_WORDS]
|
|
273
|
+
if not aligned:
|
|
274
|
+
return Verdict(False, "not a list row")
|
|
275
|
+
return Verdict(True, "list row")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
SAFE_DISMISS = re.compile(r"^(not now|cancel|close)$", re.I)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def safe_dismiss(buttons):
|
|
282
|
+
"""The one dialog button the crawler may press to get it out of the way, or None: "Not Now", "Cancel" or "Close"."""
|
|
283
|
+
# Anything else (Allow, Don't Allow, OK, Continue) is the user's to answer.
|
|
284
|
+
for name in buttons or ():
|
|
285
|
+
if SAFE_DISMISS.match(plain(name)):
|
|
286
|
+
return name
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Apps where a tap on a plain row acts in a way words and shapes can't show: App X-ray doesn't start in them.
|
|
291
|
+
ACTING_APPS = {
|
|
292
|
+
"com.apple.mobilephone": ("Phone", "a tap on a row there places a call"),
|
|
293
|
+
"com.apple.facetime": ("FaceTime", "a tap on a row there places a call"),
|
|
294
|
+
"com.apple.shortcuts": ("Shortcuts", "a tap on a tile there runs a shortcut"),
|
|
295
|
+
"com.apple.Home": ("Home", "a tap on a tile there switches an accessory"),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def app_refusal(bundle):
|
|
300
|
+
"""One sentence for an app X-ray won't map (what happened, then what to do), or None for any other app."""
|
|
301
|
+
found = ACTING_APPS.get(bundle if isinstance(bundle, str) else "")
|
|
302
|
+
if found is None:
|
|
303
|
+
return None
|
|
304
|
+
name, why = found
|
|
305
|
+
return f"App X-ray doesn't map {name}: {why}. Pick another app."
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from ..api_errors import APIError
|
|
2
|
+
from ..api_routes import Response, Route
|
|
3
|
+
from ..api_routes import register as add_route
|
|
4
|
+
from . import job, policy
|
|
5
|
+
|
|
6
|
+
HOOK_ORDER = 1 # before memory routines (10): an x-ray run is never a routine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start(request):
|
|
10
|
+
"""POST /api/xray/app: {"appId": bundle, "device"?, "screens"?, "minutes"?, "key"?} -> 201 {"runId"}."""
|
|
11
|
+
# A task like any other: Runtime.create asks the plan gate, the phone's owner and the queue.
|
|
12
|
+
body = request.body if isinstance(request.body, dict) else {}
|
|
13
|
+
if set(body) - {"appId", "device", "screens", "minutes", "key"}:
|
|
14
|
+
raise ValueError("App X-ray takes appId, device, screens, minutes and key")
|
|
15
|
+
bundle = body.get("appId")
|
|
16
|
+
if not isinstance(bundle, str) or not 1 <= len(bundle) <= 160:
|
|
17
|
+
raise ValueError("appId is the app's bundle ID")
|
|
18
|
+
key, device = body.get("key"), body.get("device")
|
|
19
|
+
if key is not None and (not isinstance(key, str) or not 1 <= len(key) <= 100):
|
|
20
|
+
raise ValueError("key is 1 to 100 characters")
|
|
21
|
+
if device is not None and not isinstance(device, str):
|
|
22
|
+
raise ValueError("device must be a device's id, UDID or name")
|
|
23
|
+
refusal = policy.app_refusal(bundle)
|
|
24
|
+
if refusal:
|
|
25
|
+
raise APIError(refusal, 422, "xray_app_refused")
|
|
26
|
+
options = job.clean({name: body[name] for name in ("screens", "minutes") if name in body})
|
|
27
|
+
runtime = request.runtime
|
|
28
|
+
app = job.find_app(runtime, bundle, device)
|
|
29
|
+
if app is None:
|
|
30
|
+
raise ValueError("Choose an app that is installed on the phone")
|
|
31
|
+
origin = request.origin if request.origin in ("app", "tui", "cli") else "api"
|
|
32
|
+
created = runtime.create(app["id"], job.GOAL.format(name=app["name"]), "live", f"xray-{key}" if key else None,
|
|
33
|
+
**({"device": device} if device is not None
|
|
34
|
+
else {}), extras={"xray": options}, origin=origin, queue=True)
|
|
35
|
+
run, _ = created if key else (created, False)
|
|
36
|
+
return Response.json({"runId": run.id}, 201)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def register(api):
|
|
40
|
+
"""``api`` is mobile_agent.harness_api."""
|
|
41
|
+
api.register_run_field("xray", lambda value, runtime: job.clean(value), model=False) # it maps with no model
|
|
42
|
+
api.register_pre_run(job.hook, order=HOOK_ORDER)
|
|
43
|
+
add_route(Route("xray", "POST", "/api/xray/app", start))
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""An X-ray map as one self-contained page (report.html): it makes no requests, and every screen string is escaped."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .. import links
|
|
7
|
+
from ..verify.report import BASE_STYLE, CSP, _e, brand, icon
|
|
8
|
+
|
|
9
|
+
XRAY_STYLE = """
|
|
10
|
+
.tag.tone::before { background: var(--violet); }
|
|
11
|
+
.facts-card { margin-top: 32px; }
|
|
12
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 20px 16px; }
|
|
13
|
+
.tile { min-width: 0; scroll-margin-top: 24px; }
|
|
14
|
+
.tile:target .thumb-x { box-shadow: 0 0 0 2px var(--violet); }
|
|
15
|
+
.thumb-x { display: block; aspect-ratio: 402 / 874; border-radius: 18px; overflow: hidden; background: var(--sunken);
|
|
16
|
+
box-shadow: var(--shadow-thumb); }
|
|
17
|
+
.thumb-x img, .thumb-x svg { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }
|
|
18
|
+
.tile h3 { margin: 10px 0 0; font-size: 14px; line-height: 20px; font-weight: var(--weight-medium);
|
|
19
|
+
overflow-wrap: anywhere; }
|
|
20
|
+
.tile .via { margin: 2px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-3); overflow-wrap: anywhere; }
|
|
21
|
+
.tile .opens { margin: 4px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-2); }
|
|
22
|
+
.tile .pw { color: var(--amber); font-weight: var(--weight-medium); }
|
|
23
|
+
.depth + .depth { margin-top: 32px; }
|
|
24
|
+
.depth h3.level { margin: 0 0 12px; font-size: 13px; line-height: 20px; font-weight: var(--weight-medium);
|
|
25
|
+
color: var(--ink-3); }
|
|
26
|
+
.wf-row { fill: color-mix(in srgb, var(--ink) 6%, transparent); }
|
|
27
|
+
.wf-bar { fill: color-mix(in srgb, var(--ink) 9%, transparent); }
|
|
28
|
+
.wf-text { fill: color-mix(in srgb, var(--ink) 22%, transparent); }
|
|
29
|
+
.wf-control { fill: color-mix(in srgb, var(--violet) 35%, transparent); }
|
|
30
|
+
.paywall { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 20px; padding: 20px; }
|
|
31
|
+
.paywall + .paywall { border-top: 1px solid var(--line); }
|
|
32
|
+
.paywall h3 { margin: 0; font-size: 16px; line-height: 24px; font-weight: var(--weight-title); }
|
|
33
|
+
.paywall .headline { margin: 2px 0 0; color: var(--ink-2); overflow-wrap: anywhere; }
|
|
34
|
+
table.plans { width: 100%; margin-top: 12px; border-collapse: collapse; font-size: 13px; line-height: 20px;
|
|
35
|
+
font-variant-numeric: tabular-nums; }
|
|
36
|
+
table.plans th { text-align: left; font-weight: var(--weight-medium); color: var(--ink-3); padding: 6px 12px 6px 0;
|
|
37
|
+
border-bottom: 1px solid var(--line); }
|
|
38
|
+
table.plans td { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
|
|
39
|
+
table.plans tr:last-child td { border-bottom: 0; }
|
|
40
|
+
.signals { margin: 10px 0 0; font-size: 12px; line-height: 18px; color: var(--ink-3); }
|
|
41
|
+
ul.tree, ul.tree ul { list-style: none; margin: 0; padding: 0; }
|
|
42
|
+
ul.tree { padding: 16px 20px; font-size: 13px; line-height: 22px; }
|
|
43
|
+
ul.tree ul { padding-left: 18px; border-left: 1px solid var(--line); margin-left: 6px; }
|
|
44
|
+
ul.tree a { text-decoration: none; }
|
|
45
|
+
ul.tree a:hover { text-decoration: underline; }
|
|
46
|
+
ul.tree .edge { color: var(--ink-3); }
|
|
47
|
+
ul.tree .again { color: var(--ink-3); }
|
|
48
|
+
table.left { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 20px; }
|
|
49
|
+
table.left td, table.left th { text-align: left; padding: 10px 16px; border-bottom: 1px solid var(--line);
|
|
50
|
+
vertical-align: top; overflow-wrap: anywhere; }
|
|
51
|
+
table.left th { font-weight: var(--weight-medium); color: var(--ink-3); }
|
|
52
|
+
table.left tr:last-child td { border-bottom: 0; }
|
|
53
|
+
table.left td.n { font-variant-numeric: tabular-nums; color: var(--ink-2); white-space: nowrap; }
|
|
54
|
+
.why { margin: 0 0 12px; color: var(--ink-2); max-width: 720px; }
|
|
55
|
+
@media (max-width: 640px) {
|
|
56
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 12px; }
|
|
57
|
+
.paywall { grid-template-columns: 96px minmax(0, 1fr); padding: 16px; gap: 16px; }
|
|
58
|
+
}
|
|
59
|
+
"""
|
|
60
|
+
STYLE = BASE_STYLE + XRAY_STYLE
|
|
61
|
+
APP_NAMES = {"com.apple.Preferences": "Settings", "com.apple.weather": "Weather", "com.apple.mobilesafari": "Safari",
|
|
62
|
+
"com.apple.Maps": "Maps", "com.apple.MobileSMS": "Messages", "com.apple.mobilenotes": "Notes"}
|
|
63
|
+
PERIODS = {"week": "Weekly", "month": "Monthly", "quarter": "Quarterly", "half-year": "Every 6 months",
|
|
64
|
+
"year": "Yearly", "lifetime": "Lifetime", "": "Not stated"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def duration(seconds):
|
|
68
|
+
seconds = float(seconds or 0)
|
|
69
|
+
if seconds < 60:
|
|
70
|
+
return f"{round(seconds)} s"
|
|
71
|
+
minutes, rest = divmod(round(seconds), 60)
|
|
72
|
+
return f"{minutes} min {rest:02d} s"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def app_name(result):
|
|
76
|
+
bundle = (result.get("app") or {}).get("bundle", "")
|
|
77
|
+
if bundle in APP_NAMES:
|
|
78
|
+
return APP_NAMES[bundle]
|
|
79
|
+
screens = result.get("screens") or []
|
|
80
|
+
return (screens[0].get("title") if screens else "") or bundle or "App"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _thumb_src(base, screen):
|
|
84
|
+
name = screen.get("thumb")
|
|
85
|
+
if not name:
|
|
86
|
+
return None
|
|
87
|
+
try:
|
|
88
|
+
data = (Path(base) / name).read_bytes()
|
|
89
|
+
except OSError:
|
|
90
|
+
return None
|
|
91
|
+
return "data:image/jpeg;base64," + base64.b64encode(data).decode()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def wireframe(screen):
|
|
95
|
+
"""The screen's layout as a drawn skeleton: rows, bars, controls and text as bars of their own width."""
|
|
96
|
+
parts = ['<svg viewBox="0 0 100 217" preserveAspectRatio="none" aria-hidden="true">']
|
|
97
|
+
for box in screen.get("boxes") or ():
|
|
98
|
+
x, y, w, h, kind = box[0] * 100, box[1] * 217, box[2] * 100, box[3] * 217, box[4]
|
|
99
|
+
if kind == "row":
|
|
100
|
+
parts.append(f'<rect class="wf-row" x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{max(h - .6, .6):.1f}" '
|
|
101
|
+
'rx="1.5"/>')
|
|
102
|
+
elif kind == "bar":
|
|
103
|
+
parts.append(f'<rect class="wf-bar" x="{x:.1f}" y="{y:.1f}" width="{w:.1f}" height="{h:.1f}"/>')
|
|
104
|
+
else:
|
|
105
|
+
line = min(h, 3.2)
|
|
106
|
+
cls = "wf-control" if kind == "control" else "wf-text"
|
|
107
|
+
parts.append(f'<rect class="{cls}" x="{x:.1f}" y="{y + (h - line) / 2:.1f}" width="{max(w, 2):.1f}" '
|
|
108
|
+
f'height="{line:.1f}" rx="{line / 2:.1f}"/>')
|
|
109
|
+
parts.append("</svg>")
|
|
110
|
+
return "".join(parts)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _thumb(base, screen, alt):
|
|
114
|
+
src = _thumb_src(base, screen)
|
|
115
|
+
inner = (f'<img src="{src}" alt="{_e(alt)}" loading="lazy">' if src else wireframe(screen))
|
|
116
|
+
return f'<span class="thumb-x">{inner}</span>'
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _plans(paywall):
|
|
120
|
+
rows = []
|
|
121
|
+
for plan in paywall.get("plans") or ():
|
|
122
|
+
per_month = (f"{plan.get('currency', '')} {plan['per_month']:.2f}" if plan.get("per_month")
|
|
123
|
+
and plan.get("period") not in ("month", "lifetime") else "")
|
|
124
|
+
rows.append(f"<tr><td>{_e(plan.get('name') or PERIODS.get(plan.get('period', ''), ''))}</td>"
|
|
125
|
+
f"<td>{_e(plan.get('price'))}</td><td>{_e(PERIODS.get(plan.get('period', ''), plan.get('period')))}"
|
|
126
|
+
f"</td><td>{_e(per_month)}</td><td>{_e(plan.get('trial') or paywall.get('trial') or '')}</td></tr>")
|
|
127
|
+
if not rows:
|
|
128
|
+
return '<p class="empty">No prices read on this screen.</p>'
|
|
129
|
+
return ('<table class="plans"><thead><tr><th>Plan</th><th>Price</th><th>Billed</th><th>A month</th>'
|
|
130
|
+
'<th>Trial</th></tr></thead><tbody>' + "".join(rows) + "</tbody></table>")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _crumbs(screen):
|
|
134
|
+
return " › ".join(step.get("label", "") for step in screen.get("path") or ()) or "First screen"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def render(result, base="."):
|
|
138
|
+
screens = result.get("screens") or []
|
|
139
|
+
edges = result.get("edges") or []
|
|
140
|
+
stats = result.get("stats") or {}
|
|
141
|
+
by_id = {s["id"]: s for s in screens}
|
|
142
|
+
name = app_name(result)
|
|
143
|
+
bundle = (result.get("app") or {}).get("bundle", "")
|
|
144
|
+
out_edges = {}
|
|
145
|
+
for edge in edges:
|
|
146
|
+
out_edges.setdefault(edge["from"], []).append(edge)
|
|
147
|
+
connection = "Pixels (iPhone Mirroring)" if result.get("connection") == "pixels" else "Accessibility tree"
|
|
148
|
+
lede = (f"{stats.get('screens', 0)} screens and {stats.get('edges', 0)} links, {stats.get('max_depth', 0)} taps "
|
|
149
|
+
f"deep, mapped in {duration(stats.get('wall_seconds'))} with no model.")
|
|
150
|
+
parts = [
|
|
151
|
+
"<!doctype html>", '<html lang="en"><head><meta charset="utf-8">',
|
|
152
|
+
f'<meta http-equiv="Content-Security-Policy" content="{CSP}">',
|
|
153
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
154
|
+
'<meta name="color-scheme" content="light dark">',
|
|
155
|
+
f"<title>{_e(name)} X-ray</title>", f"<style>{STYLE}</style></head><body><main>",
|
|
156
|
+
brand("App X-ray", bundle),
|
|
157
|
+
'<header class="head"><div class="chips">',
|
|
158
|
+
f'<span class="tag tone">{_e(connection)}</span><span class="tag">{icon("coin")}$0 in model calls</span>',
|
|
159
|
+
f"</div><h1>{_e(name)}</h1>", f'<p class="lede">{_e(lede)}</p>',
|
|
160
|
+
'<ul class="meta">',
|
|
161
|
+
f'<li>{icon("app")}<span class="mono">{_e(bundle)}</span></li>',
|
|
162
|
+
f'<li>{icon("calendar")}{_e((result.get("created_at") or "")[:16].replace("T", " "))}</li>',
|
|
163
|
+
f'<li>{icon("clock")}Stopped: {_e(result.get("stopped"))}</li></ul></header>',
|
|
164
|
+
]
|
|
165
|
+
facts = [("Screens", stats.get("screens")), ("Links", stats.get("edges")), ("Deepest", stats.get("max_depth")),
|
|
166
|
+
("Time", duration(stats.get("wall_seconds"))), ("Screens a minute", stats.get("screens_per_minute")),
|
|
167
|
+
("Taps", stats.get("taps")), ("Left alone", stats.get("refused")), ("Paywalls", stats.get("paywalls")),
|
|
168
|
+
("Ends with nothing to open", stats.get("dead_ends")), ("Model cost", "$0")]
|
|
169
|
+
parts.append('<section class="facts-card"><dl class="facts card">' + "".join(
|
|
170
|
+
f"<div><dt>{_e(k)}</dt><dd>{_e(v)}</dd></div>" for k, v in facts) + "</dl></section>")
|
|
171
|
+
|
|
172
|
+
paywalled = [s for s in screens if s.get("paywall")]
|
|
173
|
+
if paywalled:
|
|
174
|
+
parts.append(f'<section><h2>Paywalls<span class="count">{len(paywalled)}</span></h2><div class="card">')
|
|
175
|
+
for screen in paywalled:
|
|
176
|
+
pw = screen["paywall"]
|
|
177
|
+
parts.append('<div class="paywall"><a '
|
|
178
|
+
f'href="#{_e(screen["id"])}">{_thumb(base, screen, screen.get("name") or screen["title"])}</a>'
|
|
179
|
+
f'<div><h3>{_e(screen.get("name") or screen.get("title") or "Untitled")}</h3>'
|
|
180
|
+
f'<p class="headline">{_e(pw.get("headline") or _crumbs(screen))}</p>{_plans(pw)}'
|
|
181
|
+
f'<p class="signals">Read as a paywall from: {_e(", ".join(pw.get("signals") or ()))}. '
|
|
182
|
+
f'{"Shows Restore Purchases. " if pw.get("restore") else ""}Nothing on it was tapped.</p>'
|
|
183
|
+
"</div></div>")
|
|
184
|
+
parts.append("</div></section>")
|
|
185
|
+
|
|
186
|
+
parts.append(f'<section><h2>Screens<span class="count">{len(screens)}</span></h2>')
|
|
187
|
+
for heading, group in _branches(screens, by_id):
|
|
188
|
+
parts.append(f'<div class="depth"><h3 class="level">{_e(heading)} ({len(group)})</h3><div class="grid">')
|
|
189
|
+
for screen in group:
|
|
190
|
+
parts.append(_tile(base, screen, by_id, out_edges))
|
|
191
|
+
parts.append("</div></div>")
|
|
192
|
+
parts.append("</section>")
|
|
193
|
+
|
|
194
|
+
parts.append('<section><h2>Paths</h2><div class="card"><ul class="tree">'
|
|
195
|
+
+ _outline(screens, out_edges, by_id) + "</ul></div></section>")
|
|
196
|
+
|
|
197
|
+
refused = {}
|
|
198
|
+
for screen in screens:
|
|
199
|
+
for item in screen.get("refused") or ():
|
|
200
|
+
refused.setdefault(item["reason"], []).append(item["label"])
|
|
201
|
+
if refused:
|
|
202
|
+
parts.append(f'<section><h2>Left alone<span class="count">{stats.get("refused", 0)}</span></h2>'
|
|
203
|
+
'<p class="why">The crawler taps only controls shaped like navigation whose words name no act. '
|
|
204
|
+
'These it saw and didn’t tap.</p><div class="card"><table class="left"><thead><tr><th>Why</th>'
|
|
205
|
+
'<th>Count</th><th>Examples</th></tr></thead><tbody>')
|
|
206
|
+
for reason, labels in sorted(refused.items(), key=lambda kv: -len(kv[1])):
|
|
207
|
+
examples = ", ".join(f"“{label}”" for label in list(dict.fromkeys(labels))[:6])
|
|
208
|
+
parts.append(f'<tr><td>{_e(reason)}</td><td class="n">{len(labels)}</td><td>{_e(examples)}</td></tr>')
|
|
209
|
+
parts.append("</tbody></table></div></section>")
|
|
210
|
+
|
|
211
|
+
parts.append(f'<footer class="foot"><span><a href="{links.MADE_WITH_XRAY}">Mobster</a> App X-ray, '
|
|
212
|
+
'experimental.</span>'
|
|
213
|
+
"<span>Screen text is shown as read from the app.</span></footer></main></body></html>")
|
|
214
|
+
return "".join(parts)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _branches(screens, by_id):
|
|
218
|
+
def top(screen):
|
|
219
|
+
seen = set()
|
|
220
|
+
while screen.get("depth", 0) > 1 and screen.get("parent") in by_id and screen["id"] not in seen:
|
|
221
|
+
seen.add(screen["id"])
|
|
222
|
+
screen = by_id[screen["parent"]]
|
|
223
|
+
return screen
|
|
224
|
+
opening = [s for s in screens if s.get("depth", 0) == 0]
|
|
225
|
+
groups = [("Where it opens", opening)] if opening else []
|
|
226
|
+
sections = {}
|
|
227
|
+
for screen in screens:
|
|
228
|
+
if screen.get("depth", 0) == 0:
|
|
229
|
+
continue
|
|
230
|
+
sections.setdefault(top(screen)["id"], []).append(screen)
|
|
231
|
+
for head_id, group in sections.items():
|
|
232
|
+
head = by_id.get(head_id, group[0])
|
|
233
|
+
groups.append((head.get("name") or head.get("title") or "Untitled", group))
|
|
234
|
+
return groups
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _tile(base, screen, by_id, out_edges):
|
|
238
|
+
leads = out_edges.get(screen["id"], [])
|
|
239
|
+
opens = f"Opens {len(leads)}" if leads else "Nothing further"
|
|
240
|
+
parent = by_id.get(screen.get("parent"))
|
|
241
|
+
via = (f'From <a href="#{_e(parent["id"])}">{_e(parent.get("name") or parent.get("title") or parent["id"])}</a>'
|
|
242
|
+
f' by “{_e(screen.get("via"))}”' if parent else "Where the app opens")
|
|
243
|
+
flag = ' <span class="pw">Paywall</span>' if screen.get("paywall") else ""
|
|
244
|
+
return (f'<article class="tile" id="{_e(screen["id"])}">'
|
|
245
|
+
f'{_thumb(base, screen, screen.get("name") or screen.get("title"))}'
|
|
246
|
+
f'<h3>{_e(screen.get("name") or screen.get("title") or "Untitled")}{flag}</h3><p class="via">{via}</p>'
|
|
247
|
+
f'<p class="opens">{_e(opens)}</p></article>')
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _outline(screens, out_edges, by_id):
|
|
251
|
+
"""The crawl as nested lists: each screen under the one it was found from, each link with the control tapped."""
|
|
252
|
+
children = {}
|
|
253
|
+
for screen in screens:
|
|
254
|
+
children.setdefault(screen.get("parent") or "", []).append(screen)
|
|
255
|
+
lines = []
|
|
256
|
+
|
|
257
|
+
def walk(screen, depth):
|
|
258
|
+
lines.append('<li><a '
|
|
259
|
+
f'href="#{_e(screen["id"])}">{_e(screen.get("name") or screen.get("title") or "Untitled")}</a>')
|
|
260
|
+
kids = children.get(screen["id"], [])
|
|
261
|
+
again = [e for e in out_edges.get(screen["id"], [])
|
|
262
|
+
if e.get("to") and by_id.get(e["to"], {}).get("parent") != screen["id"]]
|
|
263
|
+
if kids or again:
|
|
264
|
+
lines.append("<ul>")
|
|
265
|
+
for kid in kids:
|
|
266
|
+
walk(kid, depth + 1)
|
|
267
|
+
for edge in again[:8]:
|
|
268
|
+
target = by_id.get(edge["to"], {})
|
|
269
|
+
lines.append(f'<li class="again">“{_e(edge["label"])}” goes back to <a href="#{_e(edge["to"])}">'
|
|
270
|
+
f'{_e(target.get("name") or target.get("title") or edge["to"])}</a></li>')
|
|
271
|
+
lines.append("</ul>")
|
|
272
|
+
lines.append("</li>")
|
|
273
|
+
|
|
274
|
+
for root in children.get("", []):
|
|
275
|
+
walk(root, 0)
|
|
276
|
+
return "".join(lines)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def write(result, folder, out=None):
|
|
280
|
+
folder = Path(folder)
|
|
281
|
+
path = Path(out) if out else folder / "report.html"
|
|
282
|
+
path.write_text(render(result, folder), encoding="utf-8")
|
|
283
|
+
return path
|