minitap-mobile-use 3.3.0__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.
Files changed (115) hide show
  1. minitap/mobile_use/__init__.py +0 -0
  2. minitap/mobile_use/agents/contextor/contextor.md +55 -0
  3. minitap/mobile_use/agents/contextor/contextor.py +175 -0
  4. minitap/mobile_use/agents/contextor/types.py +36 -0
  5. minitap/mobile_use/agents/cortex/cortex.md +135 -0
  6. minitap/mobile_use/agents/cortex/cortex.py +152 -0
  7. minitap/mobile_use/agents/cortex/types.py +15 -0
  8. minitap/mobile_use/agents/executor/executor.md +42 -0
  9. minitap/mobile_use/agents/executor/executor.py +87 -0
  10. minitap/mobile_use/agents/executor/tool_node.py +152 -0
  11. minitap/mobile_use/agents/hopper/hopper.md +15 -0
  12. minitap/mobile_use/agents/hopper/hopper.py +44 -0
  13. minitap/mobile_use/agents/orchestrator/human.md +12 -0
  14. minitap/mobile_use/agents/orchestrator/orchestrator.md +21 -0
  15. minitap/mobile_use/agents/orchestrator/orchestrator.py +134 -0
  16. minitap/mobile_use/agents/orchestrator/types.py +11 -0
  17. minitap/mobile_use/agents/outputter/human.md +25 -0
  18. minitap/mobile_use/agents/outputter/outputter.py +85 -0
  19. minitap/mobile_use/agents/outputter/test_outputter.py +167 -0
  20. minitap/mobile_use/agents/planner/human.md +14 -0
  21. minitap/mobile_use/agents/planner/planner.md +126 -0
  22. minitap/mobile_use/agents/planner/planner.py +101 -0
  23. minitap/mobile_use/agents/planner/types.py +51 -0
  24. minitap/mobile_use/agents/planner/utils.py +70 -0
  25. minitap/mobile_use/agents/summarizer/summarizer.py +35 -0
  26. minitap/mobile_use/agents/video_analyzer/__init__.py +5 -0
  27. minitap/mobile_use/agents/video_analyzer/human.md +5 -0
  28. minitap/mobile_use/agents/video_analyzer/video_analyzer.md +37 -0
  29. minitap/mobile_use/agents/video_analyzer/video_analyzer.py +111 -0
  30. minitap/mobile_use/clients/browserstack_client.py +477 -0
  31. minitap/mobile_use/clients/idb_client.py +429 -0
  32. minitap/mobile_use/clients/ios_client.py +332 -0
  33. minitap/mobile_use/clients/ios_client_config.py +141 -0
  34. minitap/mobile_use/clients/ui_automator_client.py +330 -0
  35. minitap/mobile_use/clients/wda_client.py +526 -0
  36. minitap/mobile_use/clients/wda_lifecycle.py +367 -0
  37. minitap/mobile_use/config.py +413 -0
  38. minitap/mobile_use/constants.py +3 -0
  39. minitap/mobile_use/context.py +106 -0
  40. minitap/mobile_use/controllers/__init__.py +0 -0
  41. minitap/mobile_use/controllers/android_controller.py +524 -0
  42. minitap/mobile_use/controllers/controller_factory.py +46 -0
  43. minitap/mobile_use/controllers/device_controller.py +182 -0
  44. minitap/mobile_use/controllers/ios_controller.py +436 -0
  45. minitap/mobile_use/controllers/platform_specific_commands_controller.py +199 -0
  46. minitap/mobile_use/controllers/types.py +106 -0
  47. minitap/mobile_use/controllers/unified_controller.py +193 -0
  48. minitap/mobile_use/graph/graph.py +160 -0
  49. minitap/mobile_use/graph/state.py +115 -0
  50. minitap/mobile_use/main.py +309 -0
  51. minitap/mobile_use/sdk/__init__.py +12 -0
  52. minitap/mobile_use/sdk/agent.py +1294 -0
  53. minitap/mobile_use/sdk/builders/__init__.py +10 -0
  54. minitap/mobile_use/sdk/builders/agent_config_builder.py +307 -0
  55. minitap/mobile_use/sdk/builders/index.py +15 -0
  56. minitap/mobile_use/sdk/builders/task_request_builder.py +236 -0
  57. minitap/mobile_use/sdk/constants.py +1 -0
  58. minitap/mobile_use/sdk/examples/README.md +83 -0
  59. minitap/mobile_use/sdk/examples/__init__.py +1 -0
  60. minitap/mobile_use/sdk/examples/app_lock_messaging.py +54 -0
  61. minitap/mobile_use/sdk/examples/platform_manual_task_example.py +67 -0
  62. minitap/mobile_use/sdk/examples/platform_minimal_example.py +48 -0
  63. minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
  64. minitap/mobile_use/sdk/examples/smart_notification_assistant.py +225 -0
  65. minitap/mobile_use/sdk/examples/video_transcription_example.py +117 -0
  66. minitap/mobile_use/sdk/services/cloud_mobile.py +656 -0
  67. minitap/mobile_use/sdk/services/platform.py +434 -0
  68. minitap/mobile_use/sdk/types/__init__.py +51 -0
  69. minitap/mobile_use/sdk/types/agent.py +84 -0
  70. minitap/mobile_use/sdk/types/exceptions.py +138 -0
  71. minitap/mobile_use/sdk/types/platform.py +183 -0
  72. minitap/mobile_use/sdk/types/task.py +269 -0
  73. minitap/mobile_use/sdk/utils.py +29 -0
  74. minitap/mobile_use/services/accessibility.py +100 -0
  75. minitap/mobile_use/services/llm.py +247 -0
  76. minitap/mobile_use/services/telemetry.py +421 -0
  77. minitap/mobile_use/tools/index.py +67 -0
  78. minitap/mobile_use/tools/mobile/back.py +52 -0
  79. minitap/mobile_use/tools/mobile/erase_one_char.py +56 -0
  80. minitap/mobile_use/tools/mobile/focus_and_clear_text.py +317 -0
  81. minitap/mobile_use/tools/mobile/focus_and_input_text.py +153 -0
  82. minitap/mobile_use/tools/mobile/launch_app.py +86 -0
  83. minitap/mobile_use/tools/mobile/long_press_on.py +169 -0
  84. minitap/mobile_use/tools/mobile/open_link.py +62 -0
  85. minitap/mobile_use/tools/mobile/press_key.py +83 -0
  86. minitap/mobile_use/tools/mobile/stop_app.py +62 -0
  87. minitap/mobile_use/tools/mobile/swipe.py +156 -0
  88. minitap/mobile_use/tools/mobile/tap.py +154 -0
  89. minitap/mobile_use/tools/mobile/video_recording.py +177 -0
  90. minitap/mobile_use/tools/mobile/wait_for_delay.py +81 -0
  91. minitap/mobile_use/tools/scratchpad.py +147 -0
  92. minitap/mobile_use/tools/test_utils.py +413 -0
  93. minitap/mobile_use/tools/tool_wrapper.py +16 -0
  94. minitap/mobile_use/tools/types.py +35 -0
  95. minitap/mobile_use/tools/utils.py +336 -0
  96. minitap/mobile_use/utils/app_launch_utils.py +173 -0
  97. minitap/mobile_use/utils/cli_helpers.py +37 -0
  98. minitap/mobile_use/utils/cli_selection.py +143 -0
  99. minitap/mobile_use/utils/conversations.py +31 -0
  100. minitap/mobile_use/utils/decorators.py +124 -0
  101. minitap/mobile_use/utils/errors.py +6 -0
  102. minitap/mobile_use/utils/file.py +13 -0
  103. minitap/mobile_use/utils/logger.py +183 -0
  104. minitap/mobile_use/utils/media.py +186 -0
  105. minitap/mobile_use/utils/recorder.py +52 -0
  106. minitap/mobile_use/utils/requests_utils.py +37 -0
  107. minitap/mobile_use/utils/shell_utils.py +20 -0
  108. minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
  109. minitap/mobile_use/utils/time.py +6 -0
  110. minitap/mobile_use/utils/ui_hierarchy.py +132 -0
  111. minitap/mobile_use/utils/video.py +281 -0
  112. minitap_mobile_use-3.3.0.dist-info/METADATA +329 -0
  113. minitap_mobile_use-3.3.0.dist-info/RECORD +115 -0
  114. minitap_mobile_use-3.3.0.dist-info/WHEEL +4 -0
  115. minitap_mobile_use-3.3.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,526 @@
1
+ import asyncio
2
+ import os
3
+ import signal
4
+ import subprocess
5
+ from functools import wraps
6
+ from typing import Any
7
+
8
+ import wda
9
+ from wda.exceptions import WDAError, WDARequestError
10
+
11
+ from minitap.mobile_use.clients.idb_client import IOSAppInfo
12
+ from minitap.mobile_use.clients.ios_client_config import WdaClientConfig
13
+ from minitap.mobile_use.clients.wda_lifecycle import (
14
+ build_and_run_wda,
15
+ check_iproxy_running,
16
+ check_wda_running,
17
+ get_wda_setup_instructions,
18
+ parse_wda_port_from_url,
19
+ start_iproxy,
20
+ wait_for_wda,
21
+ )
22
+ from minitap.mobile_use.utils.logger import get_logger
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ def with_wda_client(func):
28
+ """Decorator to handle WDA client lifecycle and error handling.
29
+
30
+ This decorator ensures that WDA operations are properly wrapped with
31
+ error handling and logging. Unlike IDB which requires building a new
32
+ client connection for each operation, WDA maintains a persistent session.
33
+
34
+ Note: Function must have None or bool in return type for error fallback.
35
+ """
36
+
37
+ @wraps(func)
38
+ async def wrapper(self, *args, **kwargs):
39
+ method_name = func.__name__
40
+ try:
41
+ logger.debug(f"Executing WDA operation: {method_name}...")
42
+ result = await func(self, *args, **kwargs)
43
+ logger.debug(f"{method_name} completed successfully")
44
+ return result
45
+ except WDARequestError as e:
46
+ logger.error(f"WDA request error in {method_name}: {e}")
47
+ return_type = func.__annotations__.get("return")
48
+ if return_type is bool:
49
+ return False
50
+ return None
51
+ except WDAError as e:
52
+ logger.error(f"WDA error in {method_name}: {e}")
53
+ return_type = func.__annotations__.get("return")
54
+ if return_type is bool:
55
+ return False
56
+ return None
57
+ except Exception as e:
58
+ logger.error(f"Failed to {method_name}: {e}")
59
+ import traceback
60
+
61
+ logger.debug(f"Traceback: {traceback.format_exc()}")
62
+
63
+ return_type = func.__annotations__.get("return")
64
+ if return_type is bool:
65
+ return False
66
+ return None
67
+
68
+ return wrapper
69
+
70
+
71
+ class WdaClientWrapper:
72
+ """Wrapper around facebook-wda client for physical iOS device automation.
73
+
74
+ This wrapper provides an interface similar to IdbClientWrapper but uses
75
+ WebDriverAgent (WDA) for physical iOS device automation instead of fb-idb.
76
+
77
+ WDA is used for:
78
+ - Physical iOS devices connected via USB
79
+
80
+ Prerequisites:
81
+ 1. WebDriverAgent must be running on the target device
82
+ 2. Port forwarding must be set up (e.g., iproxy 8100 8100)
83
+
84
+ Example:
85
+ # Basic usage with auto-start iproxy
86
+ wrapper = WdaClientWrapper(
87
+ wda_url="http://localhost:8100",
88
+ udid="00008130-000C04D12011401C",
89
+ auto_start_iproxy=True
90
+ )
91
+ await wrapper.init_client()
92
+ await wrapper.tap(100, 200)
93
+ await wrapper.cleanup()
94
+
95
+ # Using context manager
96
+ async with WdaClientWrapper(wda_url="http://localhost:8100") as wrapper:
97
+ await wrapper.tap(100, 200)
98
+ screenshot = await wrapper.screenshot()
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ udid: str | None = None,
104
+ config: WdaClientConfig | None = None,
105
+ ):
106
+ """Initialize the WDA client wrapper.
107
+
108
+ Args:
109
+ udid: Device UDID (required for auto-starting iproxy/WDA)
110
+ """
111
+ resolved_config = config or WdaClientConfig()
112
+
113
+ self.wda_url = resolved_config.wda_url
114
+ self.timeout = resolved_config.timeout
115
+ self.udid = udid
116
+ self.auto_start_iproxy = resolved_config.auto_start_iproxy
117
+ self.auto_start_wda = resolved_config.auto_start_wda
118
+ self.wda_project_path = resolved_config.wda_project_path
119
+ self.wda_startup_timeout = resolved_config.wda_startup_timeout
120
+ self._port = parse_wda_port_from_url(self.wda_url)
121
+ self._client: wda.Client | None = None
122
+ self._session: wda.Session | None = None
123
+ self._iproxy_process: subprocess.Popen | None = None
124
+ self._wda_process: subprocess.Popen | None = None
125
+ self._owns_iproxy: bool = False
126
+ self._owns_wda: bool = False
127
+
128
+ async def init_client(self) -> bool:
129
+ """Initialize the WDA client connection.
130
+
131
+ This method will:
132
+ 1. Check if iproxy is running, start it if auto_start_iproxy=True
133
+ 2. Check if WDA is responding
134
+ 3. Create a WDA session
135
+
136
+ Returns:
137
+ True if client initialized successfully, False otherwise
138
+ """
139
+ try:
140
+ # Step 1: Check/start iproxy if we have a UDID
141
+ if self.udid and self.auto_start_iproxy:
142
+ if not check_iproxy_running(self._port):
143
+ logger.info(f"iproxy not running on port {self._port}, starting...")
144
+ self._iproxy_process = await start_iproxy(
145
+ local_port=self._port,
146
+ device_port=self._port,
147
+ udid=self.udid,
148
+ )
149
+ if self._iproxy_process:
150
+ self._owns_iproxy = True
151
+ logger.info("iproxy started successfully")
152
+ else:
153
+ logger.warning(
154
+ "Failed to start iproxy automatically. "
155
+ f"Please run: iproxy {self._port} {self._port} -u {self.udid}"
156
+ )
157
+ else:
158
+ logger.debug(f"iproxy already running on port {self._port}")
159
+
160
+ # Step 2: Check if WDA is responding, auto-start if needed
161
+ wda_ready = await check_wda_running(self._port, timeout=5.0)
162
+ if not wda_ready:
163
+ if self.auto_start_wda and self.udid:
164
+ # Try to auto-start WDA
165
+ logger.info("WDA not responding, attempting to build and run...")
166
+ self._wda_process = await build_and_run_wda(
167
+ udid=self.udid,
168
+ project_path=self.wda_project_path,
169
+ timeout=self.wda_startup_timeout,
170
+ )
171
+ if self._wda_process:
172
+ self._owns_wda = True
173
+ # Wait for WDA to become ready
174
+ logger.info("Waiting for WDA to become ready...")
175
+ wda_ready = await wait_for_wda(
176
+ port=self._port,
177
+ timeout=60.0,
178
+ poll_interval=2.0,
179
+ )
180
+
181
+ if not wda_ready:
182
+ # Provide helpful error message
183
+ error_msg = (
184
+ f"WebDriverAgent not responding on port {self._port}.\n\n"
185
+ "Please ensure WDA is running on your device.\n"
186
+ )
187
+ if self.udid:
188
+ error_msg += get_wda_setup_instructions(self.udid)
189
+ else:
190
+ error_msg += (
191
+ "Start WDA using Xcode or xcodebuild, then run:\n"
192
+ f" iproxy {self._port} {self._port}\n"
193
+ )
194
+ logger.error(error_msg)
195
+ await self.cleanup() # Clean up any started processes
196
+ return False
197
+
198
+ # Step 3: Connect to WDA
199
+ logger.info(f"Connecting to WebDriverAgent at {self.wda_url}")
200
+ self._client = await asyncio.to_thread(wda.Client, self.wda_url)
201
+
202
+ # Verify connection by getting status
203
+ status = await asyncio.to_thread(self._client.status)
204
+ logger.debug(f"WDA status: {status}")
205
+
206
+ # Create a session for operations
207
+ self._session = await asyncio.to_thread(self._client.session)
208
+
209
+ logger.info(f"Successfully connected to WebDriverAgent at {self.wda_url}")
210
+ return True
211
+
212
+ except Exception as e:
213
+ logger.error(f"Failed to connect to WebDriverAgent: {e}")
214
+ if self.udid:
215
+ logger.error(get_wda_setup_instructions(self.udid))
216
+ else:
217
+ logger.error(
218
+ "\nMake sure:\n"
219
+ "1. WebDriverAgent is installed using this tutorial: https://appium.github.io/appium-xcuitest-driver/4.25/setup/#installation\n"
220
+ f"2. Port forwarding is active: iproxy {self._port} {self._port}\n"
221
+ f"3. URL is correct: {self.wda_url}"
222
+ )
223
+ self._client = None
224
+ await self.cleanup() # Clean up any started processes
225
+ self._session = None
226
+ return False
227
+
228
+ async def cleanup(self) -> None:
229
+ """Clean up WDA client resources and stop owned processes."""
230
+ if self._session is not None:
231
+ try:
232
+ logger.debug("Closing WDA session")
233
+ await asyncio.to_thread(self._session.close)
234
+ except Exception as e:
235
+ logger.debug(f"Error closing WDA session: {e}")
236
+ finally:
237
+ self._session = None
238
+
239
+ self._client = None
240
+
241
+ # Stop WDA process if we started it
242
+ if self._owns_wda and self._wda_process:
243
+ try:
244
+ pid = self._wda_process.pid
245
+ logger.info(f"Stopping WDA xcodebuild process (PID: {pid})")
246
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
247
+ self._wda_process.wait(timeout=10)
248
+ except Exception as e:
249
+ logger.debug(f"Error stopping WDA: {e}")
250
+ finally:
251
+ self._wda_process = None
252
+ self._owns_wda = False
253
+
254
+ # Stop iproxy if we started it
255
+ if self._owns_iproxy and self._iproxy_process:
256
+ try:
257
+ pid = self._iproxy_process.pid
258
+ logger.info(f"Stopping iproxy process (PID: {pid})")
259
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
260
+ self._iproxy_process.wait(timeout=5)
261
+ except Exception as e:
262
+ logger.debug(f"Error stopping iproxy: {e}")
263
+ finally:
264
+ self._iproxy_process = None
265
+ self._owns_iproxy = False
266
+
267
+ logger.debug("WDA client cleanup completed")
268
+
269
+ async def __aenter__(self):
270
+ """Async context manager entry."""
271
+ if not await self.init_client():
272
+ raise RuntimeError(f"Failed to connect to WebDriverAgent at {self.wda_url}")
273
+ return self
274
+
275
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
276
+ """Async context manager exit."""
277
+ await self.cleanup()
278
+ return False
279
+
280
+ def _ensure_session(self) -> wda.Session:
281
+ """Ensure a valid WDA session exists.
282
+
283
+ Returns:
284
+ The WDA session
285
+
286
+ Raises:
287
+ RuntimeError: If no session is available
288
+ """
289
+ if self._session is None:
290
+ raise RuntimeError(
291
+ "WDA session not initialized. Call init_client() first or use as context manager."
292
+ )
293
+ return self._session
294
+
295
+ @with_wda_client
296
+ async def tap(self, x: int, y: int, duration: float | None = None) -> bool:
297
+ """Tap at the specified coordinates.
298
+
299
+ Args:
300
+ x: X coordinate
301
+ y: Y coordinate
302
+ duration: Optional tap duration in seconds (for long press)
303
+
304
+ Returns:
305
+ True if tap succeeded, False otherwise
306
+ """
307
+ session = self._ensure_session()
308
+ if duration:
309
+ await asyncio.to_thread(session.tap_hold, x, y, duration)
310
+ else:
311
+ await asyncio.to_thread(session.tap, x, y)
312
+ return True
313
+
314
+ @with_wda_client
315
+ async def swipe(
316
+ self,
317
+ x_start: int,
318
+ y_start: int,
319
+ x_end: int,
320
+ y_end: int,
321
+ duration: float | None = None,
322
+ ) -> bool:
323
+ """Swipe from start coordinates to end coordinates.
324
+
325
+ Args:
326
+ x_start: Starting X coordinate
327
+ y_start: Starting Y coordinate
328
+ x_end: Ending X coordinate
329
+ y_end: Ending Y coordinate
330
+ duration: Optional swipe duration in seconds
331
+
332
+ Returns:
333
+ True if swipe succeeded, False otherwise
334
+ """
335
+ session = self._ensure_session()
336
+ await asyncio.to_thread(session.swipe, x_start, y_start, x_end, y_end, duration) # type: ignore
337
+ return True
338
+
339
+ @with_wda_client
340
+ async def screenshot(self, output_path: str | None = None) -> bytes | None:
341
+ """Take a screenshot and return raw image data.
342
+
343
+ Args:
344
+ output_path: Optional path to save the screenshot
345
+
346
+ Returns:
347
+ Raw image data (PNG bytes) or None on failure
348
+ """
349
+ session = self._ensure_session()
350
+ # Use format='raw' to get PNG bytes directly
351
+ screenshot_data = await asyncio.to_thread(
352
+ session.screenshot, png_filename=output_path, format="raw"
353
+ )
354
+ if isinstance(screenshot_data, bytes):
355
+ return screenshot_data
356
+ logger.warning(f"Expected bytes, got: {type(screenshot_data)}")
357
+ return None
358
+
359
+ @with_wda_client
360
+ async def launch(
361
+ self,
362
+ bundle_id: str,
363
+ args: list[str] | None = None,
364
+ env: dict[str, str] | None = None,
365
+ ) -> bool:
366
+ """Launch an application by bundle ID.
367
+
368
+ Args:
369
+ bundle_id: The bundle identifier of the app to launch
370
+ args: Optional list of arguments to pass to the app
371
+ env: Optional environment variables for the app
372
+
373
+ Returns:
374
+ True if launch succeeded, False otherwise
375
+ """
376
+ session = self._ensure_session()
377
+ await asyncio.to_thread(
378
+ session.app_launch,
379
+ bundle_id,
380
+ arguments=args or [],
381
+ environment=env or {},
382
+ )
383
+ return True
384
+
385
+ @with_wda_client
386
+ async def terminate(self, bundle_id: str) -> bool:
387
+ """Terminate an application by bundle ID.
388
+
389
+ Args:
390
+ bundle_id: The bundle identifier of the app to terminate
391
+
392
+ Returns:
393
+ True if termination succeeded, False otherwise
394
+ """
395
+ session = self._ensure_session()
396
+ await asyncio.to_thread(session.app_terminate, bundle_id)
397
+ return True
398
+
399
+ @with_wda_client
400
+ async def text(self, text: str) -> bool:
401
+ """Type text using the keyboard.
402
+
403
+ Args:
404
+ text: The text to type
405
+
406
+ Returns:
407
+ True if text input succeeded, False otherwise
408
+ """
409
+ session = self._ensure_session()
410
+ await asyncio.to_thread(session.send_keys, text)
411
+ return True
412
+
413
+ @with_wda_client
414
+ async def open_url(self, url: str) -> bool:
415
+ """Open a URL on the device.
416
+
417
+ Args:
418
+ url: The URL to open
419
+
420
+ Returns:
421
+ True if URL opened successfully, False otherwise
422
+ """
423
+ session = self._ensure_session()
424
+ await asyncio.to_thread(session.open_url, url)
425
+ return True
426
+
427
+ @with_wda_client
428
+ async def key(self, key_code: int) -> bool:
429
+ """Send a key press.
430
+
431
+ Note: WDA doesn't have direct key code support like IDB.
432
+ For delete (key_code=42), we send a backspace character.
433
+
434
+ Args:
435
+ key_code: HID key code (42 = delete/backspace)
436
+
437
+ Returns:
438
+ True if key press succeeded, False otherwise
439
+ """
440
+ session = self._ensure_session()
441
+ if key_code == 42: # Delete/backspace
442
+ await asyncio.to_thread(session.send_keys, "\b")
443
+ return True
444
+
445
+ @with_wda_client
446
+ async def button(self, button_type: Any) -> bool:
447
+ """Press a hardware button (compatible with IDB's HIDButtonType).
448
+
449
+ Args:
450
+ button_type: Button type (HIDButtonType.HOME, etc.)
451
+
452
+ Returns:
453
+ True if button press succeeded, False otherwise
454
+ """
455
+ client = self._client
456
+ if client is None:
457
+ raise RuntimeError("WDA client not initialized")
458
+ button_name = getattr(button_type, "name", str(button_type)).lower()
459
+ if button_name == "home":
460
+ await asyncio.to_thread(client.home)
461
+ elif button_name in ("volume_up", "volumeup"):
462
+ session = self._ensure_session()
463
+ await asyncio.to_thread(session.press, "volumeUp")
464
+ elif button_name in ("volume_down", "volumedown"):
465
+ session = self._ensure_session()
466
+ await asyncio.to_thread(session.press, "volumeDown")
467
+ return True
468
+
469
+ async def describe_all(self) -> list[dict[str, Any]] | None:
470
+ """Get UI hierarchy as a flat list (compatible with IDB's describe_all).
471
+
472
+ Returns:
473
+ List of UI elements or None on error
474
+ """
475
+ try:
476
+ session = self._ensure_session()
477
+ xml_source = await asyncio.to_thread(session.source, format="xml")
478
+ if xml_source is None:
479
+ return None
480
+ return self._parse_xml_to_elements(xml_source)
481
+ except Exception as e:
482
+ logger.error(f"Failed to describe_all: {e}")
483
+ return None
484
+
485
+ def _parse_xml_to_elements(self, xml_source: str) -> list[dict[str, Any]]:
486
+ """Parse WDA XML source into flat element list matching IDB format."""
487
+ import xml.etree.ElementTree as ET
488
+
489
+ elements = []
490
+ try:
491
+ root = ET.fromstring(xml_source)
492
+ for elem in root.iter():
493
+ if elem.tag == "AppiumAUT":
494
+ continue
495
+ frame = {
496
+ "x": float(elem.get("x", 0)),
497
+ "y": float(elem.get("y", 0)),
498
+ "width": float(elem.get("width", 0)),
499
+ "height": float(elem.get("height", 0)),
500
+ }
501
+ element = {
502
+ "type": elem.get("type", elem.tag),
503
+ "value": elem.get("value", ""),
504
+ "label": elem.get("label", elem.get("name", "")),
505
+ "frame": frame,
506
+ "enabled": elem.get("enabled", "false").lower() == "true",
507
+ "visible": elem.get("visible", "true").lower() == "true",
508
+ }
509
+ elements.append(element)
510
+ except ET.ParseError as e:
511
+ logger.error(f"Failed to parse XML: {e}")
512
+ return elements
513
+
514
+ @with_wda_client
515
+ async def app_current(self) -> IOSAppInfo | None:
516
+ """Get information about the currently active app.
517
+
518
+ Returns:
519
+ Dictionary with pid, name, bundleId or None on error
520
+ """
521
+ session = self._ensure_session()
522
+ result = await asyncio.to_thread(session.app_current)
523
+ return IOSAppInfo(
524
+ name=result.get("name"),
525
+ bundle_id=result.get("bundleId"),
526
+ )