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,895 @@
|
|
|
1
|
+
"""Transfers: photos, videos and files between this Mac and an iPhone, both ways, over every connection Mobster has,
|
|
2
|
+
checked by count and checksum, with progress. `mobster phone photos`/`file`, MCP's send_media and pull_media, the Mac
|
|
3
|
+
app (POST /api/transfers) and the content queue all use it.
|
|
4
|
+
|
|
5
|
+
Routes, in the order ``auto`` tries them:
|
|
6
|
+
|
|
7
|
+
| Direction | What | Route (``via``) | How | Checked by |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| to the iPhone | photos, videos | ``cable`` / ``wifi`` | Mobster's runner (photos.send) on a USB or Wi-Fi phone | SHA-256 on the phone, the library's count |
|
|
10
|
+
| | | ``airdrop`` | AirDrop to the phone by its name (handoff.airdrop) | the count, and SHA-256 on the phone where it can read it |
|
|
11
|
+
| | | ``mirroring`` | a drop on the iPhone Mirroring window, Photos in front | the same |
|
|
12
|
+
| | | ``icloud`` | this Mac's Photos library, then iCloud Photos (icloud.py); only when chosen | the same, once it arrives |
|
|
13
|
+
| | files | ``cable`` | an app's Documents folder, or a debug build's own folder (files.py) | size, then SHA-256 read back |
|
|
14
|
+
| to this Mac | photos, videos | ``cable`` | the camera roll's files (photos.CameraRoll, AFC) | size against the phone's listing |
|
|
15
|
+
| | | ``wifi`` | the runner reads the originals (photos.export) | SHA-256 on the phone against this Mac's |
|
|
16
|
+
| | | ``tunnel`` | Apple's own tunnel, no cable (tunnel.py, pymobiledevice3 if installed) | nothing is compared; the SHA-256 of what arrived is kept |
|
|
17
|
+
| | | ``icloud`` | this Mac's Photos library (icloud.py); only when chosen | not checked |
|
|
18
|
+
| | files | ``cable`` | files.py get | size against the phone's listing |
|
|
19
|
+
|
|
20
|
+
Rules every route follows:
|
|
21
|
+
|
|
22
|
+
- **One route per transfer.** ``auto`` moves to the next route only when the one before refused before anything was
|
|
23
|
+
sent (not set up, no cable, a helper missing). Once a route sent anything, it is never tried again on another route:
|
|
24
|
+
an outcome that is unknown stays unknown ("Look in Photos before you send it again"), so nothing arrives twice.
|
|
25
|
+
- **Checked.** Each item ends ``verified`` (SHA-256 matches on both sides), ``counted`` (the library grew by the
|
|
26
|
+
number sent, or a pulled file's size matched the phone's listing, but no checksum was compared), ``sent`` (the route
|
|
27
|
+
said it went; nothing on the phone could be read or compared) or ``failed``. The transfer keeps the library's count
|
|
28
|
+
before and after.
|
|
29
|
+
- **Each transfer names one phone.** Several phones run their transfers at the same time; one phone takes one at a
|
|
30
|
+
time (``Transfers``' per-phone lock).
|
|
31
|
+
""" # noqa: E501
|
|
32
|
+
|
|
33
|
+
import contextlib
|
|
34
|
+
import os
|
|
35
|
+
import threading
|
|
36
|
+
import time
|
|
37
|
+
import uuid
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Callable, Optional
|
|
41
|
+
|
|
42
|
+
from . import PhoneIOError, checked_request
|
|
43
|
+
|
|
44
|
+
DIRECTIONS = ("to_phone", "to_mac")
|
|
45
|
+
WHAT = ("photos", "files")
|
|
46
|
+
VIA = ("auto", "cable", "wifi", "airdrop", "mirroring", "icloud", "tunnel")
|
|
47
|
+
ROUTES = {
|
|
48
|
+
("to_phone", "photos"): ("cable", "wifi", "airdrop", "mirroring", "icloud"),
|
|
49
|
+
("to_phone", "files"): ("cable",),
|
|
50
|
+
("to_mac", "photos"): ("cable", "wifi", "tunnel", "icloud"),
|
|
51
|
+
("to_mac", "files"): ("cable",),
|
|
52
|
+
}
|
|
53
|
+
# Routes ``auto`` never picks: they are slow or reach outside the two devices, so the person chooses them.
|
|
54
|
+
CHOSEN_ONLY = frozenset({"icloud"})
|
|
55
|
+
# Codes that mean a route refused before sending anything: auto may try the next one.
|
|
56
|
+
NOT_SENT = frozenset({"needs_cable", "no_device", "tools", "needs_rebuild", "no_mirroring", "permission",
|
|
57
|
+
"airdrop_not_found", "airdrop_ambiguous", "not_ready", "usage_route"})
|
|
58
|
+
ARRIVAL_WAIT_S = {"airdrop": 60, "mirroring": 60, "icloud": 600}
|
|
59
|
+
POLL_S = 2.0
|
|
60
|
+
MAX_ITEMS = 50
|
|
61
|
+
KEEP = 50
|
|
62
|
+
ALBUM = "Mobster"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Phone:
|
|
67
|
+
"""One iPhone as a transfer reaches it."""
|
|
68
|
+
record: dict
|
|
69
|
+
runner: Optional[Callable[[], Any]] = None
|
|
70
|
+
files: Optional[Callable[..., Any]] = None
|
|
71
|
+
roll: Optional[Callable[..., Any]] = None
|
|
72
|
+
tunnel: Optional[Callable[..., Any]] = None
|
|
73
|
+
handoff: Any = None
|
|
74
|
+
icloud: Any = None
|
|
75
|
+
mirroring: Optional[Callable[..., Any]] = None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def id(self):
|
|
79
|
+
return self.record.get("id") or self.record.get("udid") or self.name
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def name(self):
|
|
83
|
+
return self.record.get("name") or self.record.get("id") or "the iPhone"
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def cabled(self):
|
|
87
|
+
return self.record.get("kind") == "usb" and self.record.get("transport") != "wifi"
|
|
88
|
+
|
|
89
|
+
def phone_files(self, container=False):
|
|
90
|
+
if self.files is not None:
|
|
91
|
+
return self.files(self.record, container=container)
|
|
92
|
+
from .files import PhoneFiles
|
|
93
|
+
return PhoneFiles(self.record, container=container)
|
|
94
|
+
|
|
95
|
+
def camera_roll(self):
|
|
96
|
+
if self.roll is not None:
|
|
97
|
+
return self.roll(self.record)
|
|
98
|
+
from .photos import CameraRoll
|
|
99
|
+
return CameraRoll(self.phone_files())
|
|
100
|
+
|
|
101
|
+
def tunnel_roll(self):
|
|
102
|
+
if self.tunnel is not None:
|
|
103
|
+
return self.tunnel(self.record)
|
|
104
|
+
from .tunnel import TunnelRoll
|
|
105
|
+
return TunnelRoll(self.record)
|
|
106
|
+
|
|
107
|
+
def handoffs(self):
|
|
108
|
+
if self.handoff is not None:
|
|
109
|
+
return self.handoff
|
|
110
|
+
from . import handoff
|
|
111
|
+
return handoff
|
|
112
|
+
|
|
113
|
+
def cloud(self):
|
|
114
|
+
if self.icloud is not None:
|
|
115
|
+
return self.icloud
|
|
116
|
+
from . import icloud
|
|
117
|
+
return icloud
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def phone_from_record(record):
|
|
121
|
+
"""A Phone for a devices.py record outside the Mac app; its runner holds the device's lease while open."""
|
|
122
|
+
from . import wda
|
|
123
|
+
|
|
124
|
+
@contextlib.contextmanager
|
|
125
|
+
def runner():
|
|
126
|
+
with wda(record) as request:
|
|
127
|
+
yield request
|
|
128
|
+
return Phone(record, runner=runner if record.get("wdaUrl") else None)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def phone_from_runtime(runtime, device=None):
|
|
132
|
+
"""A Phone for a device of the running Mobster; its runner is refused while a task runs on that phone."""
|
|
133
|
+
import json
|
|
134
|
+
import urllib.request
|
|
135
|
+
|
|
136
|
+
from ..journal import JournalError, Lease, LeaseHeld
|
|
137
|
+
|
|
138
|
+
fleet_phone = runtime.resolve_device(device)
|
|
139
|
+
record = {"id": fleet_phone.id, "kind": fleet_phone.kind, "udid": fleet_phone.udid,
|
|
140
|
+
"name": fleet_phone.name or fleet_phone.id, "transport": getattr(fleet_phone, "transport", None),
|
|
141
|
+
"wdaUrl": fleet_phone.wda_url}
|
|
142
|
+
|
|
143
|
+
@contextlib.contextmanager
|
|
144
|
+
def runner():
|
|
145
|
+
def check_busy():
|
|
146
|
+
if getattr(runtime, "closing", False):
|
|
147
|
+
raise PhoneIOError("Mobster is closing; the transfer did not start.", "device_busy")
|
|
148
|
+
if runtime.device_busy(fleet_phone.id):
|
|
149
|
+
raise PhoneIOError(f"{record['name']} is busy with a task.", "device_busy", "Try again when it ends.")
|
|
150
|
+
if fleet_phone.wda_url != record["wdaUrl"]:
|
|
151
|
+
raise PhoneIOError(f"{record['name']}'s connection changed before the transfer started.",
|
|
152
|
+
"device_busy", "Try the transfer again on its current connection.")
|
|
153
|
+
|
|
154
|
+
check_busy()
|
|
155
|
+
url = (record["wdaUrl"] or "").rstrip("/")
|
|
156
|
+
if not url:
|
|
157
|
+
raise PhoneIOError(f"{record['name']} isn't set up yet.", "no_device", "Set it up in Mobster.")
|
|
158
|
+
try:
|
|
159
|
+
held = Lease.device(url, wait=0, udid=record["udid"])
|
|
160
|
+
except (LeaseHeld, JournalError) as error:
|
|
161
|
+
raise PhoneIOError(f"{record['name']} is busy or its device lock is unavailable: {error}",
|
|
162
|
+
"device_busy", "Try again when its task ends.") from None
|
|
163
|
+
try:
|
|
164
|
+
check_busy() # Admission may have changed while the cross-process lease was being acquired.
|
|
165
|
+
|
|
166
|
+
def send(method, path, body, timeout):
|
|
167
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
168
|
+
req = urllib.request.Request(f"{url}/session/{fleet_phone.wda_session()}{path}", data=data,
|
|
169
|
+
method=method, headers={"Content-Type": "application/json"})
|
|
170
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
171
|
+
return json.load(response)
|
|
172
|
+
|
|
173
|
+
yield checked_request(send)
|
|
174
|
+
finally:
|
|
175
|
+
held.close()
|
|
176
|
+
return Phone(record, runner=runner if fleet_phone.wda_url else None)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class Item:
|
|
181
|
+
name: str
|
|
182
|
+
bytes: int = 0
|
|
183
|
+
sent: int = 0
|
|
184
|
+
state: str = "queued" # queued | sending | checking | verified | counted | sent | failed | unknown
|
|
185
|
+
kind: Optional[str] = None
|
|
186
|
+
sha256: Optional[str] = None
|
|
187
|
+
path: Optional[str] = None # the Mac file (to the iPhone: the source; to this Mac: where it landed)
|
|
188
|
+
remote: Optional[str] = None # where it is on the phone: a Photos id or a path, when known
|
|
189
|
+
error: Optional[str] = None
|
|
190
|
+
|
|
191
|
+
def public(self):
|
|
192
|
+
return {"name": self.name, "bytes": self.bytes, "sent": self.sent, "state": self.state, "kind": self.kind,
|
|
193
|
+
"sha256": self.sha256, "path": self.path, "remote": self.remote, "error": self.error}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class Transfer:
|
|
198
|
+
"""One transfer: what moves, which way, to or from which phone, the route it took and how each item ended."""
|
|
199
|
+
direction: str
|
|
200
|
+
what: str
|
|
201
|
+
phone: Phone
|
|
202
|
+
via: str = "auto"
|
|
203
|
+
paths: list = field(default_factory=list) # to the iPhone: the Mac files
|
|
204
|
+
folder: Optional[str] = None # to this Mac: where files land
|
|
205
|
+
count: int = 1 # to this Mac, photos: how many of the newest
|
|
206
|
+
since: Optional[float] = None
|
|
207
|
+
names: list = field(default_factory=list) # to this Mac, files: the names in the app's folder
|
|
208
|
+
app: Optional[str] = None # files: the app's bundle ID
|
|
209
|
+
container: bool = False
|
|
210
|
+
album: Optional[str] = ALBUM
|
|
211
|
+
convert_heic: bool = False
|
|
212
|
+
date_now: bool = False # to the iPhone over the runner: first in Recents (the queue)
|
|
213
|
+
queue_item: Optional[str] = None # the content queue's post this transfer is for
|
|
214
|
+
id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
215
|
+
state: str = "queued" # queued | running | done | failed | unknown
|
|
216
|
+
route: Optional[str] = None
|
|
217
|
+
items: list = field(default_factory=list)
|
|
218
|
+
before: Optional[int] = None # the phone's photos and videos, before and after
|
|
219
|
+
after: Optional[int] = None
|
|
220
|
+
error: Optional[dict] = None
|
|
221
|
+
tried: list = field(default_factory=list) # routes that refused before sending: [{"via", "why"}]
|
|
222
|
+
created_at: float = field(default_factory=time.time)
|
|
223
|
+
finished_at: Optional[float] = None
|
|
224
|
+
listeners: list = field(default_factory=list)
|
|
225
|
+
|
|
226
|
+
def public(self):
|
|
227
|
+
done = sum(i.sent for i in self.items)
|
|
228
|
+
total = sum(i.bytes for i in self.items)
|
|
229
|
+
return {"id": self.id, "direction": self.direction, "what": self.what, "device": self.phone.id,
|
|
230
|
+
"deviceName": self.phone.name, "via": self.via, "route": self.route, "state": self.state,
|
|
231
|
+
"items": [i.public() for i in self.items], "bytes": total, "sent": done,
|
|
232
|
+
"before": self.before, "after": self.after, "error": self.error, "tried": list(self.tried),
|
|
233
|
+
"album": self.album if self.direction == "to_phone" and self.what == "photos" else None,
|
|
234
|
+
"app": self.app, "folder": self.folder, "queueItem": self.queue_item,
|
|
235
|
+
"createdAt": round(self.created_at * 1000),
|
|
236
|
+
"finishedAt": round(self.finished_at * 1000) if self.finished_at else None,
|
|
237
|
+
"verified": bool(self.items) and all(i.state == "verified" for i in self.items)}
|
|
238
|
+
|
|
239
|
+
def changed(self):
|
|
240
|
+
for listener in list(self.listeners):
|
|
241
|
+
try:
|
|
242
|
+
listener(self)
|
|
243
|
+
except Exception: # noqa: BLE001 - a listener never stops a transfer
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def validate(transfer):
|
|
248
|
+
if transfer.direction not in DIRECTIONS or transfer.what not in WHAT:
|
|
249
|
+
raise PhoneIOError("A transfer goes to_phone or to_mac, with photos or files.", "usage")
|
|
250
|
+
if transfer.via not in VIA or (transfer.via != "auto" and transfer.via not in
|
|
251
|
+
ROUTES[(transfer.direction, transfer.what)]):
|
|
252
|
+
allowed = ", ".join(ROUTES[(transfer.direction, transfer.what)])
|
|
253
|
+
raise PhoneIOError(f"That route doesn't move {transfer.what} that way. Choose auto or one of: {allowed}.",
|
|
254
|
+
"usage")
|
|
255
|
+
if transfer.direction == "to_phone":
|
|
256
|
+
if not transfer.paths or len(transfer.paths) > MAX_ITEMS:
|
|
257
|
+
raise PhoneIOError(f"Name 1 to {MAX_ITEMS} files to send.", "usage")
|
|
258
|
+
for path in transfer.paths:
|
|
259
|
+
if not Path(path).expanduser().is_file():
|
|
260
|
+
raise PhoneIOError(f"There is no file at {path}.", "usage")
|
|
261
|
+
if transfer.what == "files" and not transfer.app:
|
|
262
|
+
raise PhoneIOError("Name the app whose folder the files go to or come from (its bundle ID).", "usage")
|
|
263
|
+
if transfer.direction == "to_mac" and not transfer.folder:
|
|
264
|
+
raise PhoneIOError("Name the folder on this Mac the files go to.", "usage")
|
|
265
|
+
if transfer.album is not None and (not isinstance(transfer.album, str) or len(transfer.album) > 100):
|
|
266
|
+
raise PhoneIOError("An album's name is at most 100 characters.", "usage")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _candidates(transfer):
|
|
270
|
+
order = ROUTES[(transfer.direction, transfer.what)]
|
|
271
|
+
if transfer.via != "auto":
|
|
272
|
+
return [transfer.via]
|
|
273
|
+
return [route for route in order if route not in CHOSEN_ONLY]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _ready(transfer, route):
|
|
277
|
+
"""None when ``route`` can run now, else why not (a sentence)."""
|
|
278
|
+
phone = transfer.phone
|
|
279
|
+
if phone.record.get("kind") == "simulator" and route != "cable":
|
|
280
|
+
return "a simulator takes files only from this Mac directly"
|
|
281
|
+
if route == "cable":
|
|
282
|
+
if transfer.direction == "to_phone" and transfer.what == "photos":
|
|
283
|
+
if phone.record.get("kind") == "simulator":
|
|
284
|
+
return None
|
|
285
|
+
return None if phone.cabled and phone.runner is not None else (
|
|
286
|
+
"it isn't on a cable" if not phone.cabled else "Mobster's runner isn't set up on it")
|
|
287
|
+
return None if phone.cabled else "it isn't on a cable"
|
|
288
|
+
if route == "wifi":
|
|
289
|
+
if phone.runner is None:
|
|
290
|
+
return "Mobster's runner isn't set up on it"
|
|
291
|
+
return None if not phone.cabled or transfer.via == "wifi" else "it's on the cable (the cable route goes first)"
|
|
292
|
+
if route == "tunnel":
|
|
293
|
+
from .tunnel import tool
|
|
294
|
+
return None if phone.tunnel is not None or tool() else "pymobiledevice3 isn't installed"
|
|
295
|
+
if route == "mirroring":
|
|
296
|
+
if phone.mirroring is not None:
|
|
297
|
+
return None
|
|
298
|
+
if not _mirrors(phone):
|
|
299
|
+
return "this Mac isn't set to mirror it (MOBSTER_MIRRORS names the iPhone it mirrors)"
|
|
300
|
+
return None
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def mirrored_phone():
|
|
305
|
+
"""The iPhone this Mac mirrors, as named in MOBSTER_MIRRORS: a drop on the wrong phone can't be undone."""
|
|
306
|
+
value = os.environ.get("MOBSTER_MIRRORS", "").strip()
|
|
307
|
+
return value or None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _mirrors(phone):
|
|
311
|
+
named = mirrored_phone()
|
|
312
|
+
return named is not None and named in (phone.name, phone.record.get("udid"), phone.record.get("id"))
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def run(transfer, *, clock=time.monotonic, sleep=time.sleep):
|
|
316
|
+
"""Run ``transfer`` to its end, updating it as it goes (``transfer.changed()`` after each step). Never raises for
|
|
317
|
+
a phone's refusal: the transfer ends ``failed`` with ``error``. Returns the transfer."""
|
|
318
|
+
validate(transfer)
|
|
319
|
+
transfer.state = "running"
|
|
320
|
+
transfer.changed()
|
|
321
|
+
last = None
|
|
322
|
+
for route in _candidates(transfer):
|
|
323
|
+
why = _ready(transfer, route)
|
|
324
|
+
if why is not None:
|
|
325
|
+
transfer.tried.append({"via": route, "why": why})
|
|
326
|
+
last = PhoneIOError(f"{transfer.phone.name}: {why}.", "not_ready")
|
|
327
|
+
continue
|
|
328
|
+
transfer.route = route
|
|
329
|
+
transfer.items = []
|
|
330
|
+
try:
|
|
331
|
+
_ROUTERS[(transfer.direction, transfer.what)](transfer, route, clock, sleep)
|
|
332
|
+
except PhoneIOError as error:
|
|
333
|
+
if error.code == "device_busy":
|
|
334
|
+
_end(transfer, error)
|
|
335
|
+
return transfer
|
|
336
|
+
# A NOT_SENT code is raised only before anything went, and a copy to this Mac changes nothing on the
|
|
337
|
+
# phone: unless an item already arrived, try the next route.
|
|
338
|
+
if (error.code in NOT_SENT or transfer.direction == "to_mac") and not any(
|
|
339
|
+
i.sent or i.state in ("verified", "counted", "sent", "unknown") for i in transfer.items):
|
|
340
|
+
transfer.tried.append({"via": route, "why": str(error)})
|
|
341
|
+
last = error
|
|
342
|
+
if transfer.via == "auto":
|
|
343
|
+
continue
|
|
344
|
+
_end(transfer, error)
|
|
345
|
+
return transfer
|
|
346
|
+
transfer.state = "done" if all(i.state in ("verified", "counted", "sent") for i in transfer.items) else "failed"
|
|
347
|
+
if transfer.state == "failed" and any(i.state == "unknown" for i in transfer.items):
|
|
348
|
+
transfer.state = "unknown"
|
|
349
|
+
transfer.finished_at = time.time()
|
|
350
|
+
transfer.changed()
|
|
351
|
+
return transfer
|
|
352
|
+
_end(transfer, last or PhoneIOError("No route reaches that iPhone right now.", "not_ready"))
|
|
353
|
+
return transfer
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _end(transfer, error):
|
|
357
|
+
transfer.state = "unknown" if error.code == "unknown" else "failed"
|
|
358
|
+
transfer.error = error.public()
|
|
359
|
+
for item in transfer.items:
|
|
360
|
+
if item.state in ("queued", "sending", "checking"):
|
|
361
|
+
item.state = "unknown" if error.code == "unknown" and item.state != "queued" else "failed"
|
|
362
|
+
item.error = item.error or str(error)
|
|
363
|
+
transfer.finished_at = time.time()
|
|
364
|
+
transfer.changed()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _items_from_paths(transfer):
|
|
368
|
+
from .photos import digest, sniff
|
|
369
|
+
items = []
|
|
370
|
+
for path in transfer.paths:
|
|
371
|
+
file = Path(path).expanduser()
|
|
372
|
+
kind, _ = sniff(file) if transfer.what == "photos" else (None, None)
|
|
373
|
+
items.append(Item(name=file.name, bytes=file.stat().st_size, kind=kind, sha256=digest(file),
|
|
374
|
+
path=str(file.resolve())))
|
|
375
|
+
transfer.items = items
|
|
376
|
+
transfer.changed()
|
|
377
|
+
return items
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _library_count(request):
|
|
381
|
+
from .photos import count
|
|
382
|
+
try:
|
|
383
|
+
value = count(request)
|
|
384
|
+
except PhoneIOError:
|
|
385
|
+
return None
|
|
386
|
+
if not value or "photos" not in value:
|
|
387
|
+
return None
|
|
388
|
+
return value["photos"] + value["videos"]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _photos_to_phone(transfer, route, clock, sleep):
|
|
392
|
+
items = _items_from_paths(transfer)
|
|
393
|
+
phone = transfer.phone
|
|
394
|
+
if route in ("cable", "wifi"):
|
|
395
|
+
if phone.record.get("kind") == "simulator":
|
|
396
|
+
return _simulator_add(transfer)
|
|
397
|
+
from .photos import send
|
|
398
|
+
with phone.runner() as request:
|
|
399
|
+
transfer.before = _library_count(request)
|
|
400
|
+
transfer.changed()
|
|
401
|
+
for item in items:
|
|
402
|
+
item.state = "sending"
|
|
403
|
+
transfer.changed()
|
|
404
|
+
|
|
405
|
+
def progress(sent, total, item=item):
|
|
406
|
+
item.sent = sent
|
|
407
|
+
transfer.changed()
|
|
408
|
+
try:
|
|
409
|
+
result = send(request, item.path, name=phone.name, album=transfer.album, progress=progress,
|
|
410
|
+
sha256=item.sha256, date_now=transfer.date_now,
|
|
411
|
+
activate=lambda bundle: request("POST", "/wda/apps/activate", {"bundleId": bundle},
|
|
412
|
+
15))
|
|
413
|
+
except PhoneIOError as error:
|
|
414
|
+
item.state = "unknown" if error.code == "unknown" else "failed"
|
|
415
|
+
item.error = str(error)
|
|
416
|
+
if item is items[0] and error.code in NOT_SENT:
|
|
417
|
+
raise
|
|
418
|
+
if error.code in ("unknown",):
|
|
419
|
+
raise
|
|
420
|
+
continue
|
|
421
|
+
item.sent, item.remote = item.bytes, result.get("localIdentifier")
|
|
422
|
+
item.state = "verified" if result.get("verified") else "sent"
|
|
423
|
+
if result.get("albumError"):
|
|
424
|
+
item.error = result["albumError"]
|
|
425
|
+
transfer.changed()
|
|
426
|
+
transfer.after = _library_count(request)
|
|
427
|
+
_settle_by_count(transfer)
|
|
428
|
+
return
|
|
429
|
+
# AirDrop, Mirroring and iCloud hand the files over and can't say whether the phone kept them: count before and
|
|
430
|
+
# after (the runner, else the cable's camera roll), then read each new item's checksum where it can.
|
|
431
|
+
watch = _Watch(transfer)
|
|
432
|
+
transfer.before = watch.count()
|
|
433
|
+
transfer.changed()
|
|
434
|
+
for item in items:
|
|
435
|
+
item.state = "sending"
|
|
436
|
+
transfer.changed()
|
|
437
|
+
paths = [item.path for item in items]
|
|
438
|
+
if route == "airdrop":
|
|
439
|
+
accepting = threading.Event()
|
|
440
|
+
watcher = threading.Thread(target=_accept_airdrop, args=(phone, accepting, sleep), daemon=True)
|
|
441
|
+
watcher.start()
|
|
442
|
+
try:
|
|
443
|
+
phone.handoffs().airdrop(paths, phone.name)
|
|
444
|
+
finally:
|
|
445
|
+
accepting.set()
|
|
446
|
+
watcher.join(timeout=5)
|
|
447
|
+
elif route == "mirroring":
|
|
448
|
+
_mirroring_photos(transfer, paths)
|
|
449
|
+
else:
|
|
450
|
+
phone.cloud().import_media(paths, album=transfer.album or ALBUM)
|
|
451
|
+
for item in items:
|
|
452
|
+
item.sent, item.state = item.bytes, "checking"
|
|
453
|
+
transfer.changed()
|
|
454
|
+
watch.wait_for(len(items), ARRIVAL_WAIT_S[route], clock, sleep)
|
|
455
|
+
watch.check(items)
|
|
456
|
+
transfer.changed()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _accept_airdrop(phone, done, sleep=time.sleep, poll=1.0):
|
|
460
|
+
"""While Mobster's own AirDrop is on its way, tap Accept on an alert whose text says AirDrop, and nothing else.
|
|
461
|
+
Not journaled: a transfer has no run, so the tap is bounded here (the alert's text, its Accept, the send)."""
|
|
462
|
+
if phone.runner is None:
|
|
463
|
+
return
|
|
464
|
+
try:
|
|
465
|
+
with phone.runner() as request:
|
|
466
|
+
while not done.is_set():
|
|
467
|
+
try:
|
|
468
|
+
text = request("GET", "/alert/text", None, 5)
|
|
469
|
+
if isinstance(text, str) and "airdrop" in text.casefold():
|
|
470
|
+
buttons = request("GET", "/wda/alert/buttons", None, 5)
|
|
471
|
+
if isinstance(buttons, list) and "Accept" in buttons:
|
|
472
|
+
request("POST", "/alert/accept", {"name": "Accept"}, 10)
|
|
473
|
+
return
|
|
474
|
+
except PhoneIOError:
|
|
475
|
+
pass
|
|
476
|
+
sleep(poll)
|
|
477
|
+
except PhoneIOError:
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _mirroring_photos(transfer, paths):
|
|
482
|
+
"""Photos in front on the phone (Spotlight, through Mirroring), then the drop in the middle of its window."""
|
|
483
|
+
phone = transfer.phone
|
|
484
|
+
if phone.mirroring is not None:
|
|
485
|
+
phone.mirroring(paths)
|
|
486
|
+
return
|
|
487
|
+
open_on_mirroring("Photos")
|
|
488
|
+
phone.handoffs().mirroring_drop(paths, 0.5, 0.55)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def open_on_mirroring(app_name, helper=None, sleep=time.sleep):
|
|
492
|
+
"""Open ``app_name`` on the mirrored iPhone with Spotlight, as Mirroring's driver opens apps."""
|
|
493
|
+
from ..mirroring import Helper, MirroringError
|
|
494
|
+
own = helper is None
|
|
495
|
+
try:
|
|
496
|
+
helper = helper or Helper()
|
|
497
|
+
except Exception as error: # noqa: BLE001 - no helper, no Xcode tools
|
|
498
|
+
raise PhoneIOError(f"Mobster's Mirroring helper couldn't start ({type(error).__name__}).",
|
|
499
|
+
"no_mirroring") from None
|
|
500
|
+
try:
|
|
501
|
+
state = helper.request("state")
|
|
502
|
+
if state.get("state") != "ready":
|
|
503
|
+
raise PhoneIOError("iPhone Mirroring isn't showing the iPhone.", "no_mirroring",
|
|
504
|
+
"Open iPhone Mirroring, lock the iPhone and keep it near this Mac, then try again.")
|
|
505
|
+
helper.request("key", combo="cmd+3")
|
|
506
|
+
sleep(0.8)
|
|
507
|
+
helper.request("type", text=app_name, mode="keys")
|
|
508
|
+
sleep(1.0)
|
|
509
|
+
helper.request("key", combo="return")
|
|
510
|
+
sleep(1.5)
|
|
511
|
+
except MirroringError as error:
|
|
512
|
+
raise PhoneIOError(f"iPhone Mirroring refused: {error}.", "no_mirroring") from None
|
|
513
|
+
finally:
|
|
514
|
+
if own:
|
|
515
|
+
try:
|
|
516
|
+
helper.request("restore", timeout=3)
|
|
517
|
+
except Exception: # noqa: BLE001
|
|
518
|
+
pass
|
|
519
|
+
helper.close()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _settle_by_count(transfer):
|
|
523
|
+
"""Items sent but not checked by checksum become ``counted`` when the library grew by exactly as many."""
|
|
524
|
+
if transfer.before is None or transfer.after is None:
|
|
525
|
+
return
|
|
526
|
+
sent = [i for i in transfer.items if i.state in ("verified", "sent")]
|
|
527
|
+
grew = transfer.after - transfer.before
|
|
528
|
+
for item in sent:
|
|
529
|
+
if item.state == "sent" and grew == len(sent):
|
|
530
|
+
item.state = "counted"
|
|
531
|
+
if grew != len(sent):
|
|
532
|
+
for item in sent:
|
|
533
|
+
item.error = item.error or (f"The library grew by {grew}, not {len(sent)}: look in Photos on "
|
|
534
|
+
f"{transfer.phone.name}.")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _simulator_add(transfer):
|
|
538
|
+
import subprocess
|
|
539
|
+
|
|
540
|
+
from ..journal import JournalError, Lease, LeaseHeld
|
|
541
|
+
from ..native_helpers import require_devices
|
|
542
|
+
record = transfer.phone.record
|
|
543
|
+
url = record.get("wdaUrl")
|
|
544
|
+
if not url:
|
|
545
|
+
raise PhoneIOError(f"{transfer.phone.name} has no device lock identity.", "no_device")
|
|
546
|
+
udid = record.get("udid") or record.get("id")
|
|
547
|
+
if not isinstance(udid, str) or not udid:
|
|
548
|
+
raise PhoneIOError(f"{transfer.phone.name} has no simulator identity.", "no_device")
|
|
549
|
+
try:
|
|
550
|
+
held = Lease.device(url, wait=0, udid=udid)
|
|
551
|
+
except (LeaseHeld, JournalError) as error:
|
|
552
|
+
raise PhoneIOError(f"{transfer.phone.name} is busy or its device lock is unavailable: {error}",
|
|
553
|
+
"device_busy", "Try again when its task ends.") from None
|
|
554
|
+
try:
|
|
555
|
+
try:
|
|
556
|
+
require_devices()
|
|
557
|
+
except RuntimeError as error:
|
|
558
|
+
raise PhoneIOError(str(error), "tools") from None
|
|
559
|
+
for item in transfer.items:
|
|
560
|
+
item.state = "sending"
|
|
561
|
+
transfer.changed()
|
|
562
|
+
try:
|
|
563
|
+
done = subprocess.run(["xcrun", "simctl", "addmedia", udid, item.path], capture_output=True,
|
|
564
|
+
text=True, timeout=120)
|
|
565
|
+
except (OSError, subprocess.SubprocessError) as error:
|
|
566
|
+
raise PhoneIOError(f"The simulator import may have happened ({type(error).__name__}).",
|
|
567
|
+
"unknown", "Check Photos before sending it again.") from None
|
|
568
|
+
if done.returncode != 0:
|
|
569
|
+
raise PhoneIOError(f"The simulator import may have happened: "
|
|
570
|
+
f"{(done.stderr or done.stdout).strip()[:160]}", "unknown",
|
|
571
|
+
"Check Photos before sending it again.")
|
|
572
|
+
item.state, item.sent = "sent", item.bytes
|
|
573
|
+
transfer.changed()
|
|
574
|
+
finally:
|
|
575
|
+
held.close()
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class _Watch:
|
|
579
|
+
"""How many photos and videos the phone has, and which arrived."""
|
|
580
|
+
|
|
581
|
+
def __init__(self, transfer):
|
|
582
|
+
self.transfer = transfer
|
|
583
|
+
self.names_before = None
|
|
584
|
+
|
|
585
|
+
def _runner_count(self):
|
|
586
|
+
phone = self.transfer.phone
|
|
587
|
+
if phone.runner is None:
|
|
588
|
+
return None
|
|
589
|
+
try:
|
|
590
|
+
with phone.runner() as request:
|
|
591
|
+
return _library_count(request)
|
|
592
|
+
except PhoneIOError:
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
def count(self):
|
|
596
|
+
found = self._runner_count()
|
|
597
|
+
if found is not None:
|
|
598
|
+
self.how = "runner"
|
|
599
|
+
return found
|
|
600
|
+
if self.transfer.phone.cabled:
|
|
601
|
+
try:
|
|
602
|
+
media = self.transfer.phone.camera_roll().media()
|
|
603
|
+
except PhoneIOError:
|
|
604
|
+
media = None
|
|
605
|
+
if media is not None:
|
|
606
|
+
self.how, self.names_before = "cable", dict(media)
|
|
607
|
+
return len(media)
|
|
608
|
+
self.how = None
|
|
609
|
+
return None
|
|
610
|
+
|
|
611
|
+
def wait_for(self, grew, limit, clock, sleep):
|
|
612
|
+
transfer = self.transfer
|
|
613
|
+
if transfer.before is None:
|
|
614
|
+
return
|
|
615
|
+
deadline = clock() + limit
|
|
616
|
+
while True:
|
|
617
|
+
now = self._again()
|
|
618
|
+
transfer.after = now
|
|
619
|
+
transfer.changed()
|
|
620
|
+
if now is not None and now - transfer.before >= grew or clock() >= deadline:
|
|
621
|
+
return
|
|
622
|
+
sleep(POLL_S)
|
|
623
|
+
|
|
624
|
+
def _again(self):
|
|
625
|
+
if self.how == "runner":
|
|
626
|
+
return self._runner_count()
|
|
627
|
+
if self.how == "cable":
|
|
628
|
+
try:
|
|
629
|
+
self.names_after = self.transfer.phone.camera_roll().media()
|
|
630
|
+
except PhoneIOError:
|
|
631
|
+
return None
|
|
632
|
+
return len(self.names_after)
|
|
633
|
+
return None
|
|
634
|
+
|
|
635
|
+
def check(self, items):
|
|
636
|
+
"""Each item ``verified`` when a new file on the phone has its checksum, else ``counted`` or ``sent``."""
|
|
637
|
+
transfer = self.transfer
|
|
638
|
+
grew = None if transfer.before is None or transfer.after is None else transfer.after - transfer.before
|
|
639
|
+
matched = set()
|
|
640
|
+
if self.how == "cable" and getattr(self, "names_after", None) is not None:
|
|
641
|
+
new = {path: size for path, size in self.names_after.items() if path not in (self.names_before or {})}
|
|
642
|
+
roll = transfer.phone.camera_roll()
|
|
643
|
+
import tempfile
|
|
644
|
+
|
|
645
|
+
from .photos import digest
|
|
646
|
+
with tempfile.TemporaryDirectory(prefix="mobster-check-") as scratch:
|
|
647
|
+
for item in items:
|
|
648
|
+
for path, size in new.items():
|
|
649
|
+
if path in matched or size != item.bytes:
|
|
650
|
+
continue
|
|
651
|
+
try:
|
|
652
|
+
local = roll.fetch(path, scratch, size)
|
|
653
|
+
except PhoneIOError:
|
|
654
|
+
continue
|
|
655
|
+
if digest(local) == item.sha256:
|
|
656
|
+
matched.add(path)
|
|
657
|
+
item.state, item.remote = "verified", path
|
|
658
|
+
break
|
|
659
|
+
elif self.how == "runner":
|
|
660
|
+
matched = self._runner_checks(items, grew)
|
|
661
|
+
for item in items:
|
|
662
|
+
if item.state == "verified":
|
|
663
|
+
continue
|
|
664
|
+
if grew is not None and grew >= len(items):
|
|
665
|
+
item.state = "counted"
|
|
666
|
+
elif grew is None:
|
|
667
|
+
item.state = "sent"
|
|
668
|
+
item.error = item.error or f"Mobster couldn't read {transfer.phone.name}'s library to check it."
|
|
669
|
+
else:
|
|
670
|
+
item.state = "unknown"
|
|
671
|
+
item.error = (f"{transfer.phone.name}'s library grew by {grew} of {len(items)}. Look in Photos "
|
|
672
|
+
"before you send it again.")
|
|
673
|
+
|
|
674
|
+
def _runner_checks(self, items, grew):
|
|
675
|
+
from .photos import checksum, recent
|
|
676
|
+
phone = self.transfer.phone
|
|
677
|
+
found = set()
|
|
678
|
+
try:
|
|
679
|
+
with phone.runner() as request:
|
|
680
|
+
newest = recent(request, max(len(items), grew or 0) + 2, name=phone.name)
|
|
681
|
+
for item in items:
|
|
682
|
+
for candidate in newest:
|
|
683
|
+
if candidate["id"] in found:
|
|
684
|
+
continue
|
|
685
|
+
answer = checksum(request, candidate["id"], phone.name)
|
|
686
|
+
if answer and answer.get("sha256") == item.sha256:
|
|
687
|
+
found.add(candidate["id"])
|
|
688
|
+
item.state, item.remote = "verified", candidate["id"]
|
|
689
|
+
break
|
|
690
|
+
except PhoneIOError:
|
|
691
|
+
pass
|
|
692
|
+
return found
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _files_to_phone(transfer, route, clock, sleep):
|
|
696
|
+
import tempfile
|
|
697
|
+
|
|
698
|
+
from .photos import digest
|
|
699
|
+
items = _items_from_paths(transfer)
|
|
700
|
+
phone_files = transfer.phone.phone_files(container=transfer.container)
|
|
701
|
+
for item in items:
|
|
702
|
+
item.state = "sending"
|
|
703
|
+
transfer.changed()
|
|
704
|
+
try:
|
|
705
|
+
put = phone_files.put(transfer.app, item.path)
|
|
706
|
+
except PhoneIOError as error:
|
|
707
|
+
item.state, item.error = "failed", str(error)
|
|
708
|
+
if item is items[0] and error.code in NOT_SENT:
|
|
709
|
+
raise
|
|
710
|
+
continue
|
|
711
|
+
item.sent, item.remote, item.state = item.bytes, put["path"], "checking"
|
|
712
|
+
transfer.changed()
|
|
713
|
+
with tempfile.TemporaryDirectory(prefix="mobster-check-") as scratch:
|
|
714
|
+
try:
|
|
715
|
+
back = phone_files.get(transfer.app, put["name"], Path(scratch))
|
|
716
|
+
item.state = "verified" if digest(back) == item.sha256 else "failed"
|
|
717
|
+
if item.state == "failed":
|
|
718
|
+
item.error = f"{put['name']} on {transfer.phone.name} isn't the same as this Mac's copy."
|
|
719
|
+
except PhoneIOError:
|
|
720
|
+
item.state = "counted" # its size matched (files.put checks it); the read-back didn't run
|
|
721
|
+
transfer.changed()
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _photos_to_mac(transfer, route, clock, sleep):
|
|
725
|
+
folder = Path(transfer.folder).expanduser()
|
|
726
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
727
|
+
before = len([p for p in folder.iterdir() if p.is_file()])
|
|
728
|
+
phone = transfer.phone
|
|
729
|
+
if route == "wifi" or (route == "cable" and phone.record.get("kind") == "simulator"):
|
|
730
|
+
from .photos import export, recent
|
|
731
|
+
with phone.runner() as request:
|
|
732
|
+
chosen = recent(request, transfer.count, transfer.since, name=phone.name)
|
|
733
|
+
transfer.items = [Item(name=c["filename"] or c["id"], kind=c["kind"], remote=c["id"]) for c in chosen]
|
|
734
|
+
transfer.changed()
|
|
735
|
+
for item, picked in zip(transfer.items, chosen):
|
|
736
|
+
item.state = "sending"
|
|
737
|
+
|
|
738
|
+
def progress(sent, total, item=item):
|
|
739
|
+
item.sent, item.bytes = sent, total
|
|
740
|
+
transfer.changed()
|
|
741
|
+
try:
|
|
742
|
+
got = export(request, picked, folder, progress=progress, name=phone.name)
|
|
743
|
+
except PhoneIOError as error:
|
|
744
|
+
item.state, item.error = "failed", str(error)
|
|
745
|
+
if item is transfer.items[0] and error.code in NOT_SENT:
|
|
746
|
+
raise
|
|
747
|
+
continue
|
|
748
|
+
item.path, item.sha256, item.bytes, item.sent = got["path"], got["sha256"], got["bytes"], got["bytes"]
|
|
749
|
+
item.state = "verified"
|
|
750
|
+
transfer.changed()
|
|
751
|
+
else:
|
|
752
|
+
if route == "cable":
|
|
753
|
+
got = phone.camera_roll().pull(folder, count=transfer.count, since=transfer.since,
|
|
754
|
+
convert_heic=transfer.convert_heic)
|
|
755
|
+
elif route == "tunnel":
|
|
756
|
+
got = phone.tunnel_roll().pull(folder, count=transfer.count)
|
|
757
|
+
else:
|
|
758
|
+
exported = phone.cloud().export_recent(folder, count=transfer.count)
|
|
759
|
+
from .photos import digest
|
|
760
|
+
got = [{"path": p, "name": Path(p).name, "bytes": Path(p).stat().st_size, "sha256": digest(p),
|
|
761
|
+
"kind": None} for p in exported["paths"]]
|
|
762
|
+
transfer.items = [Item(name=g["name"], bytes=g["bytes"], sent=g["bytes"], kind=g.get("kind"),
|
|
763
|
+
sha256=g.get("sha256"), path=g["path"], remote=g.get("remote"),
|
|
764
|
+
state=_PULL_STATE[route]) for g in got]
|
|
765
|
+
after = len([p for p in folder.iterdir() if p.is_file()])
|
|
766
|
+
transfer.before, transfer.after = before, after
|
|
767
|
+
if after - before != sum(1 for i in transfer.items if i.path):
|
|
768
|
+
for item in transfer.items:
|
|
769
|
+
item.error = item.error or "The folder's count didn't grow by as many as arrived."
|
|
770
|
+
transfer.changed()
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _files_to_mac(transfer, route, clock, sleep):
|
|
774
|
+
from .photos import digest
|
|
775
|
+
folder = Path(transfer.folder).expanduser()
|
|
776
|
+
phone_files = transfer.phone.phone_files(container=transfer.container)
|
|
777
|
+
listing = {e["name"]: e for e in phone_files.ls(transfer.app)}
|
|
778
|
+
names = transfer.names or []
|
|
779
|
+
if not names:
|
|
780
|
+
raise PhoneIOError("Name the files to copy.", "usage")
|
|
781
|
+
transfer.items = [Item(name=n, bytes=(listing.get(n) or {}).get("bytes") or 0) for n in names]
|
|
782
|
+
transfer.changed()
|
|
783
|
+
for item in transfer.items:
|
|
784
|
+
item.state = "sending"
|
|
785
|
+
transfer.changed()
|
|
786
|
+
try:
|
|
787
|
+
saved = phone_files.get(transfer.app, item.name, folder)
|
|
788
|
+
except PhoneIOError as error:
|
|
789
|
+
item.state, item.error = "failed", str(error)
|
|
790
|
+
continue
|
|
791
|
+
item.path, item.sha256, item.sent = str(saved), digest(saved), saved.stat().st_size
|
|
792
|
+
item.state = "counted" if item.sent == item.bytes else "failed"
|
|
793
|
+
transfer.changed()
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
# What a pull to this Mac proves: the cable compared the file's size with the phone's listing; the tunnel and iCloud
|
|
797
|
+
# compared nothing, so only the wifi route (photos.export) ever reaches ``verified``.
|
|
798
|
+
_PULL_STATE = {"cable": "counted", "tunnel": "sent", "icloud": "sent"}
|
|
799
|
+
|
|
800
|
+
_ROUTERS = {("to_phone", "photos"): _photos_to_phone, ("to_phone", "files"): _files_to_phone,
|
|
801
|
+
("to_mac", "photos"): _photos_to_mac, ("to_mac", "files"): _files_to_mac}
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class Transfers:
|
|
805
|
+
"""The running Mobster's transfers: one at a time per phone, phones in parallel; the newest ``KEEP`` are listed."""
|
|
806
|
+
|
|
807
|
+
def __init__(self, runner=run):
|
|
808
|
+
self._run = runner
|
|
809
|
+
self._lock = threading.Lock()
|
|
810
|
+
self._phone_locks = {}
|
|
811
|
+
self._all = {}
|
|
812
|
+
|
|
813
|
+
def start(self, transfer, wait=False):
|
|
814
|
+
validate(transfer)
|
|
815
|
+
with self._lock:
|
|
816
|
+
self._all[transfer.id] = transfer
|
|
817
|
+
while len(self._all) > KEEP:
|
|
818
|
+
oldest = min(self._all.values(), key=lambda t: t.created_at)
|
|
819
|
+
if oldest.state in ("queued", "running"):
|
|
820
|
+
break
|
|
821
|
+
self._all.pop(oldest.id)
|
|
822
|
+
lock = self._phone_locks.setdefault(transfer.phone.id, threading.Lock())
|
|
823
|
+
|
|
824
|
+
def work():
|
|
825
|
+
with lock:
|
|
826
|
+
self._run(transfer)
|
|
827
|
+
if wait:
|
|
828
|
+
work()
|
|
829
|
+
return transfer
|
|
830
|
+
threading.Thread(target=work, name=f"mobster-transfer-{transfer.id}", daemon=True).start()
|
|
831
|
+
return transfer
|
|
832
|
+
|
|
833
|
+
def get(self, transfer_id):
|
|
834
|
+
with self._lock:
|
|
835
|
+
return self._all.get(transfer_id)
|
|
836
|
+
|
|
837
|
+
def all(self):
|
|
838
|
+
with self._lock:
|
|
839
|
+
return sorted(self._all.values(), key=lambda t: t.created_at, reverse=True)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def transfers_for(runtime):
|
|
843
|
+
"""The runtime's Transfers, made on first use."""
|
|
844
|
+
if getattr(runtime, "transfers", None) is None:
|
|
845
|
+
runtime.transfers = Transfers()
|
|
846
|
+
return runtime.transfers
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def target(runtime=None, *, device=None, profile=None, records=None):
|
|
850
|
+
"""The phone a transfer goes to."""
|
|
851
|
+
if profile is not None:
|
|
852
|
+
from ..accounts.store import AccountNotFound, AmbiguousAccount, resolve, store_for
|
|
853
|
+
try:
|
|
854
|
+
account = resolve(store_for(runtime).all(), profile)
|
|
855
|
+
except (AccountNotFound, AmbiguousAccount) as error:
|
|
856
|
+
raise PhoneIOError(str(error), "usage") from None
|
|
857
|
+
wanted = account.get("device")
|
|
858
|
+
if device is not None and wanted and device != wanted:
|
|
859
|
+
raise PhoneIOError(f"@{account['handle']} is on {account.get('deviceName') or 'another phone'}. Leave "
|
|
860
|
+
"the phone out.", "usage")
|
|
861
|
+
device = wanted or device
|
|
862
|
+
if runtime is not None:
|
|
863
|
+
return phone_from_runtime(runtime, device)
|
|
864
|
+
from . import choose_device
|
|
865
|
+
return phone_from_record(choose_device(device, records))
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def summary(transfer):
|
|
869
|
+
"""One sentence for the CLI and MCP: what moved, where, by which route, and how it was checked."""
|
|
870
|
+
public = transfer.public()
|
|
871
|
+
items = transfer.items
|
|
872
|
+
verified = sum(1 for i in items if i.state == "verified")
|
|
873
|
+
counted = sum(1 for i in items if i.state == "counted")
|
|
874
|
+
sent = sum(1 for i in items if i.state == "sent")
|
|
875
|
+
words = {"cable": "over the cable", "wifi": "over Wi-Fi", "airdrop": "by AirDrop", "mirroring":
|
|
876
|
+
"through iPhone Mirroring", "icloud": "through iCloud Photos", "tunnel": "over Apple's tunnel"}
|
|
877
|
+
where = (f"to {transfer.phone.name}" if transfer.direction == "to_phone" else f"from {transfer.phone.name}")
|
|
878
|
+
if transfer.state == "failed" and not items:
|
|
879
|
+
return str((transfer.error or {}).get("message") or "Nothing moved.")
|
|
880
|
+
parts = [f"Moved {verified + counted + sent} of {len(items)} {where} {words.get(transfer.route or '', '')}".strip()
|
|
881
|
+
+ "."]
|
|
882
|
+
if verified:
|
|
883
|
+
parts.append(f"{verified} checked by SHA-256.")
|
|
884
|
+
if counted:
|
|
885
|
+
parts.append(f"{counted} matched by size." if transfer.direction == "to_mac"
|
|
886
|
+
else f"{counted} counted in the library.")
|
|
887
|
+
if sent:
|
|
888
|
+
parts.append(f"{sent} not checked." if transfer.direction == "to_mac"
|
|
889
|
+
else f"{sent} not checked: Mobster couldn't read the phone.")
|
|
890
|
+
if public["before"] is not None and public["after"] is not None and transfer.direction == "to_phone":
|
|
891
|
+
parts.append(f"Photos and videos on the phone: {public['before']:,} before, {public['after']:,} after.")
|
|
892
|
+
failed = [i for i in items if i.state in ("failed", "unknown")]
|
|
893
|
+
if failed:
|
|
894
|
+
parts.append(f"{len(failed)} didn't: {failed[0].error or failed[0].state}")
|
|
895
|
+
return " ".join(parts)
|