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.
- minitap/mobile_use/__init__.py +0 -0
- minitap/mobile_use/agents/contextor/contextor.md +55 -0
- minitap/mobile_use/agents/contextor/contextor.py +175 -0
- minitap/mobile_use/agents/contextor/types.py +36 -0
- minitap/mobile_use/agents/cortex/cortex.md +135 -0
- minitap/mobile_use/agents/cortex/cortex.py +152 -0
- minitap/mobile_use/agents/cortex/types.py +15 -0
- minitap/mobile_use/agents/executor/executor.md +42 -0
- minitap/mobile_use/agents/executor/executor.py +87 -0
- minitap/mobile_use/agents/executor/tool_node.py +152 -0
- minitap/mobile_use/agents/hopper/hopper.md +15 -0
- minitap/mobile_use/agents/hopper/hopper.py +44 -0
- minitap/mobile_use/agents/orchestrator/human.md +12 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.md +21 -0
- minitap/mobile_use/agents/orchestrator/orchestrator.py +134 -0
- minitap/mobile_use/agents/orchestrator/types.py +11 -0
- minitap/mobile_use/agents/outputter/human.md +25 -0
- minitap/mobile_use/agents/outputter/outputter.py +85 -0
- minitap/mobile_use/agents/outputter/test_outputter.py +167 -0
- minitap/mobile_use/agents/planner/human.md +14 -0
- minitap/mobile_use/agents/planner/planner.md +126 -0
- minitap/mobile_use/agents/planner/planner.py +101 -0
- minitap/mobile_use/agents/planner/types.py +51 -0
- minitap/mobile_use/agents/planner/utils.py +70 -0
- minitap/mobile_use/agents/summarizer/summarizer.py +35 -0
- minitap/mobile_use/agents/video_analyzer/__init__.py +5 -0
- minitap/mobile_use/agents/video_analyzer/human.md +5 -0
- minitap/mobile_use/agents/video_analyzer/video_analyzer.md +37 -0
- minitap/mobile_use/agents/video_analyzer/video_analyzer.py +111 -0
- minitap/mobile_use/clients/browserstack_client.py +477 -0
- minitap/mobile_use/clients/idb_client.py +429 -0
- minitap/mobile_use/clients/ios_client.py +332 -0
- minitap/mobile_use/clients/ios_client_config.py +141 -0
- minitap/mobile_use/clients/ui_automator_client.py +330 -0
- minitap/mobile_use/clients/wda_client.py +526 -0
- minitap/mobile_use/clients/wda_lifecycle.py +367 -0
- minitap/mobile_use/config.py +413 -0
- minitap/mobile_use/constants.py +3 -0
- minitap/mobile_use/context.py +106 -0
- minitap/mobile_use/controllers/__init__.py +0 -0
- minitap/mobile_use/controllers/android_controller.py +524 -0
- minitap/mobile_use/controllers/controller_factory.py +46 -0
- minitap/mobile_use/controllers/device_controller.py +182 -0
- minitap/mobile_use/controllers/ios_controller.py +436 -0
- minitap/mobile_use/controllers/platform_specific_commands_controller.py +199 -0
- minitap/mobile_use/controllers/types.py +106 -0
- minitap/mobile_use/controllers/unified_controller.py +193 -0
- minitap/mobile_use/graph/graph.py +160 -0
- minitap/mobile_use/graph/state.py +115 -0
- minitap/mobile_use/main.py +309 -0
- minitap/mobile_use/sdk/__init__.py +12 -0
- minitap/mobile_use/sdk/agent.py +1294 -0
- minitap/mobile_use/sdk/builders/__init__.py +10 -0
- minitap/mobile_use/sdk/builders/agent_config_builder.py +307 -0
- minitap/mobile_use/sdk/builders/index.py +15 -0
- minitap/mobile_use/sdk/builders/task_request_builder.py +236 -0
- minitap/mobile_use/sdk/constants.py +1 -0
- minitap/mobile_use/sdk/examples/README.md +83 -0
- minitap/mobile_use/sdk/examples/__init__.py +1 -0
- minitap/mobile_use/sdk/examples/app_lock_messaging.py +54 -0
- minitap/mobile_use/sdk/examples/platform_manual_task_example.py +67 -0
- minitap/mobile_use/sdk/examples/platform_minimal_example.py +48 -0
- minitap/mobile_use/sdk/examples/simple_photo_organizer.py +76 -0
- minitap/mobile_use/sdk/examples/smart_notification_assistant.py +225 -0
- minitap/mobile_use/sdk/examples/video_transcription_example.py +117 -0
- minitap/mobile_use/sdk/services/cloud_mobile.py +656 -0
- minitap/mobile_use/sdk/services/platform.py +434 -0
- minitap/mobile_use/sdk/types/__init__.py +51 -0
- minitap/mobile_use/sdk/types/agent.py +84 -0
- minitap/mobile_use/sdk/types/exceptions.py +138 -0
- minitap/mobile_use/sdk/types/platform.py +183 -0
- minitap/mobile_use/sdk/types/task.py +269 -0
- minitap/mobile_use/sdk/utils.py +29 -0
- minitap/mobile_use/services/accessibility.py +100 -0
- minitap/mobile_use/services/llm.py +247 -0
- minitap/mobile_use/services/telemetry.py +421 -0
- minitap/mobile_use/tools/index.py +67 -0
- minitap/mobile_use/tools/mobile/back.py +52 -0
- minitap/mobile_use/tools/mobile/erase_one_char.py +56 -0
- minitap/mobile_use/tools/mobile/focus_and_clear_text.py +317 -0
- minitap/mobile_use/tools/mobile/focus_and_input_text.py +153 -0
- minitap/mobile_use/tools/mobile/launch_app.py +86 -0
- minitap/mobile_use/tools/mobile/long_press_on.py +169 -0
- minitap/mobile_use/tools/mobile/open_link.py +62 -0
- minitap/mobile_use/tools/mobile/press_key.py +83 -0
- minitap/mobile_use/tools/mobile/stop_app.py +62 -0
- minitap/mobile_use/tools/mobile/swipe.py +156 -0
- minitap/mobile_use/tools/mobile/tap.py +154 -0
- minitap/mobile_use/tools/mobile/video_recording.py +177 -0
- minitap/mobile_use/tools/mobile/wait_for_delay.py +81 -0
- minitap/mobile_use/tools/scratchpad.py +147 -0
- minitap/mobile_use/tools/test_utils.py +413 -0
- minitap/mobile_use/tools/tool_wrapper.py +16 -0
- minitap/mobile_use/tools/types.py +35 -0
- minitap/mobile_use/tools/utils.py +336 -0
- minitap/mobile_use/utils/app_launch_utils.py +173 -0
- minitap/mobile_use/utils/cli_helpers.py +37 -0
- minitap/mobile_use/utils/cli_selection.py +143 -0
- minitap/mobile_use/utils/conversations.py +31 -0
- minitap/mobile_use/utils/decorators.py +124 -0
- minitap/mobile_use/utils/errors.py +6 -0
- minitap/mobile_use/utils/file.py +13 -0
- minitap/mobile_use/utils/logger.py +183 -0
- minitap/mobile_use/utils/media.py +186 -0
- minitap/mobile_use/utils/recorder.py +52 -0
- minitap/mobile_use/utils/requests_utils.py +37 -0
- minitap/mobile_use/utils/shell_utils.py +20 -0
- minitap/mobile_use/utils/test_ui_hierarchy.py +178 -0
- minitap/mobile_use/utils/time.py +6 -0
- minitap/mobile_use/utils/ui_hierarchy.py +132 -0
- minitap/mobile_use/utils/video.py +281 -0
- minitap_mobile_use-3.3.0.dist-info/METADATA +329 -0
- minitap_mobile_use-3.3.0.dist-info/RECORD +115 -0
- minitap_mobile_use-3.3.0.dist-info/WHEEL +4 -0
- minitap_mobile_use-3.3.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UIAutomator2 client for Android device screen data retrieval.
|
|
3
|
+
|
|
4
|
+
Provides an alternative to the Maestro-based screen API with direct device access.
|
|
5
|
+
Handles Maestro blocker detection and removal before connecting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import subprocess
|
|
10
|
+
import xml.etree.ElementTree as ET
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import uiautomator2 as u2
|
|
15
|
+
from PIL.Image import Image
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from minitap.mobile_use.utils.logger import get_logger
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from uiautomator2 import Device
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
MAESTRO_PACKAGE = "dev.mobile.maestro"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UIAutomatorScreenData(BaseModel):
|
|
30
|
+
"""Screen data response from UIAutomator2."""
|
|
31
|
+
|
|
32
|
+
base64: str
|
|
33
|
+
hierarchy_xml: str
|
|
34
|
+
elements: list[dict]
|
|
35
|
+
width: int
|
|
36
|
+
height: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_hierarchy_xml_to_elements(hierarchy_xml: str) -> list[dict]:
|
|
40
|
+
"""
|
|
41
|
+
Parse uiautomator2 XML hierarchy into a flat list of element dictionaries.
|
|
42
|
+
|
|
43
|
+
The output format matches the existing screen API format with attributes like:
|
|
44
|
+
- resource-id
|
|
45
|
+
- text
|
|
46
|
+
- content-desc
|
|
47
|
+
- bounds (as string "[x1,y1][x2,y2]")
|
|
48
|
+
- class
|
|
49
|
+
- package
|
|
50
|
+
- checkable, checked, clickable, enabled, focusable, focused
|
|
51
|
+
- scrollable, long-clickable, password, selected
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
hierarchy_xml: XML string from uiautomator2.dump_hierarchy()
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Flat list of element dictionaries
|
|
58
|
+
"""
|
|
59
|
+
elements: list[dict] = []
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
root = ET.fromstring(hierarchy_xml)
|
|
63
|
+
except ET.ParseError as e:
|
|
64
|
+
logger.error(f"Failed to parse hierarchy XML: {e}")
|
|
65
|
+
return elements
|
|
66
|
+
|
|
67
|
+
def _extract_element(node: ET.Element) -> None:
|
|
68
|
+
"""Recursively extract elements from XML nodes."""
|
|
69
|
+
element: dict = {}
|
|
70
|
+
|
|
71
|
+
for attr_name, attr_value in node.attrib.items():
|
|
72
|
+
if attr_name == "resource-id":
|
|
73
|
+
element["resource-id"] = attr_value
|
|
74
|
+
elif attr_name == "text":
|
|
75
|
+
element["text"] = attr_value
|
|
76
|
+
elif attr_name == "content-desc":
|
|
77
|
+
element["content-desc"] = attr_value
|
|
78
|
+
element["accessibilityText"] = attr_value
|
|
79
|
+
elif attr_name == "bounds":
|
|
80
|
+
element["bounds"] = attr_value
|
|
81
|
+
elif attr_name == "class":
|
|
82
|
+
element["class"] = attr_value
|
|
83
|
+
elif attr_name == "package":
|
|
84
|
+
element["package"] = attr_value
|
|
85
|
+
elif attr_name in (
|
|
86
|
+
"checkable",
|
|
87
|
+
"checked",
|
|
88
|
+
"clickable",
|
|
89
|
+
"enabled",
|
|
90
|
+
"focusable",
|
|
91
|
+
"focused",
|
|
92
|
+
"scrollable",
|
|
93
|
+
"long-clickable",
|
|
94
|
+
"password",
|
|
95
|
+
"selected",
|
|
96
|
+
):
|
|
97
|
+
element[attr_name] = attr_value
|
|
98
|
+
else:
|
|
99
|
+
element[attr_name] = attr_value
|
|
100
|
+
|
|
101
|
+
if element:
|
|
102
|
+
elements.append(element)
|
|
103
|
+
|
|
104
|
+
for child in node:
|
|
105
|
+
_extract_element(child)
|
|
106
|
+
|
|
107
|
+
_extract_element(root)
|
|
108
|
+
|
|
109
|
+
return elements
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _pil_to_base64(img: Image, format: str = "PNG") -> str:
|
|
113
|
+
"""Convert PIL Image to base64 string."""
|
|
114
|
+
buffer = BytesIO()
|
|
115
|
+
img.save(buffer, format=format)
|
|
116
|
+
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_package_installed(device_id: str, pkg: str) -> bool:
|
|
120
|
+
"""Check if a package is installed on the device."""
|
|
121
|
+
try:
|
|
122
|
+
result = subprocess.run(
|
|
123
|
+
["adb", "-s", device_id, "shell", "pm", "list", "packages"],
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
timeout=10,
|
|
127
|
+
)
|
|
128
|
+
if result.returncode != 0:
|
|
129
|
+
logger.warning(f"Failed to list packages: {result.stderr}")
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
lines = result.stdout.splitlines()
|
|
133
|
+
target = f"package:{pkg}"
|
|
134
|
+
return target in lines
|
|
135
|
+
except subprocess.TimeoutExpired:
|
|
136
|
+
logger.warning("Timeout checking installed packages")
|
|
137
|
+
return False
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(f"Error checking installed packages: {e}")
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _uninstall_package(device_id: str, pkg: str) -> bool:
|
|
144
|
+
"""Uninstall a package from the device for the current user."""
|
|
145
|
+
try:
|
|
146
|
+
result = subprocess.run(
|
|
147
|
+
["adb", "-s", device_id, "shell", "pm", "uninstall", "--user", "0", pkg],
|
|
148
|
+
capture_output=True,
|
|
149
|
+
text=True,
|
|
150
|
+
timeout=30,
|
|
151
|
+
)
|
|
152
|
+
if result.returncode == 0:
|
|
153
|
+
logger.info(f"Successfully uninstalled {pkg}")
|
|
154
|
+
return True
|
|
155
|
+
else:
|
|
156
|
+
logger.warning(f"Failed to uninstall {pkg}: {result.stderr}")
|
|
157
|
+
return False
|
|
158
|
+
except subprocess.TimeoutExpired:
|
|
159
|
+
logger.warning(f"Timeout uninstalling {pkg}")
|
|
160
|
+
return False
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.warning(f"Error uninstalling {pkg}: {e}")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _ensure_maestro_not_installed(device_id: str) -> None:
|
|
167
|
+
"""
|
|
168
|
+
Check if Maestro is installed and uninstall it.
|
|
169
|
+
|
|
170
|
+
Maestro conflicts with uiautomator2 - if Maestro's package is installed,
|
|
171
|
+
u2.connect() will fail. This function ensures Maestro is removed before
|
|
172
|
+
attempting to connect.
|
|
173
|
+
"""
|
|
174
|
+
if _is_package_installed(device_id, MAESTRO_PACKAGE):
|
|
175
|
+
logger.warning(
|
|
176
|
+
f"Maestro ({MAESTRO_PACKAGE}) detected - uninstalling to enable UIAutomator2..."
|
|
177
|
+
)
|
|
178
|
+
_uninstall_package(device_id, MAESTRO_PACKAGE)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class UIAutomatorClient:
|
|
182
|
+
"""
|
|
183
|
+
UIAutomator2 client for Android screen data retrieval.
|
|
184
|
+
|
|
185
|
+
This client uses uiautomator2 library for direct device communication,
|
|
186
|
+
providing faster hierarchy and screenshot retrieval compared to Maestro.
|
|
187
|
+
|
|
188
|
+
Important: Maestro must not be installed on the device as it conflicts
|
|
189
|
+
with uiautomator2. This client automatically handles Maestro removal.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(self, device_id: str):
|
|
193
|
+
"""
|
|
194
|
+
Initialize the UIAutomator client.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
device_id: The Android device serial number (e.g., "emulator-5554")
|
|
198
|
+
"""
|
|
199
|
+
self._device_id = device_id
|
|
200
|
+
self._device: Device | None = None
|
|
201
|
+
|
|
202
|
+
def _ensure_connected(self) -> "Device":
|
|
203
|
+
"""
|
|
204
|
+
Ensure connection to the device, handling Maestro blocker.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Connected uiautomator2 Device instance
|
|
208
|
+
"""
|
|
209
|
+
if self._device is not None:
|
|
210
|
+
try:
|
|
211
|
+
# Quick check if connection is still alive
|
|
212
|
+
self._device.info
|
|
213
|
+
return self._device
|
|
214
|
+
except Exception:
|
|
215
|
+
logger.warning("UIAutomator2 connection lost, reconnecting...")
|
|
216
|
+
self._device = None
|
|
217
|
+
|
|
218
|
+
# Ensure Maestro is not blocking us
|
|
219
|
+
_ensure_maestro_not_installed(self._device_id)
|
|
220
|
+
|
|
221
|
+
# Connect to device
|
|
222
|
+
logger.info(f"Connecting UIAutomator2 to device: {self._device_id}")
|
|
223
|
+
self._device = u2.connect(self._device_id)
|
|
224
|
+
logger.info("UIAutomator2 connected successfully")
|
|
225
|
+
return self._device
|
|
226
|
+
|
|
227
|
+
def press_key(self, key: str):
|
|
228
|
+
"""
|
|
229
|
+
Press a key on the device.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
key: Key to press (e.g., "home", "back", "enter"...)
|
|
233
|
+
"""
|
|
234
|
+
device = self._ensure_connected()
|
|
235
|
+
return device.press(key=key)
|
|
236
|
+
|
|
237
|
+
def send_text(self, text: str) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Send text input to the device using FastInputIME.
|
|
240
|
+
|
|
241
|
+
This method supports special characters (e.g., 'ö') that ADB shell
|
|
242
|
+
input text cannot handle. The original IME is restored after input.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
text: The text to input
|
|
246
|
+
"""
|
|
247
|
+
device = self._ensure_connected()
|
|
248
|
+
device.set_fastinput_ime(True)
|
|
249
|
+
try:
|
|
250
|
+
device.send_keys(text)
|
|
251
|
+
finally:
|
|
252
|
+
device.set_fastinput_ime(False)
|
|
253
|
+
|
|
254
|
+
def get_hierarchy(self) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Get the UI hierarchy XML from the device.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Compressed UI hierarchy XML string
|
|
260
|
+
"""
|
|
261
|
+
device = self._ensure_connected()
|
|
262
|
+
return device.dump_hierarchy(compressed=True)
|
|
263
|
+
|
|
264
|
+
def get_screenshot(self) -> Image | None:
|
|
265
|
+
"""
|
|
266
|
+
Capture a screenshot from the device.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
PIL Image or None if capture failed
|
|
270
|
+
"""
|
|
271
|
+
device = self._ensure_connected()
|
|
272
|
+
return device.screenshot()
|
|
273
|
+
|
|
274
|
+
def get_screenshot_base64(self) -> str | None:
|
|
275
|
+
"""
|
|
276
|
+
Capture a screenshot and return as base64 string.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Base64 encoded PNG screenshot or None if capture failed
|
|
280
|
+
"""
|
|
281
|
+
screenshot = self.get_screenshot()
|
|
282
|
+
if screenshot is None:
|
|
283
|
+
return None
|
|
284
|
+
return _pil_to_base64(screenshot)
|
|
285
|
+
|
|
286
|
+
def get_screen_data(self) -> UIAutomatorScreenData:
|
|
287
|
+
"""
|
|
288
|
+
Get complete screen data including screenshot and hierarchy.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
UIAutomatorScreenData with screenshot, hierarchy, elements, and dimensions
|
|
292
|
+
"""
|
|
293
|
+
device = self._ensure_connected()
|
|
294
|
+
|
|
295
|
+
# Get screenshot
|
|
296
|
+
screenshot = device.screenshot()
|
|
297
|
+
if screenshot is None:
|
|
298
|
+
raise RuntimeError("Failed to capture screenshot via UIAutomator2")
|
|
299
|
+
|
|
300
|
+
# Get hierarchy
|
|
301
|
+
hierarchy_xml = device.dump_hierarchy(compressed=True)
|
|
302
|
+
|
|
303
|
+
# Parse XML to flat elements list
|
|
304
|
+
elements = _parse_hierarchy_xml_to_elements(hierarchy_xml)
|
|
305
|
+
|
|
306
|
+
return UIAutomatorScreenData(
|
|
307
|
+
base64=_pil_to_base64(screenshot),
|
|
308
|
+
hierarchy_xml=hierarchy_xml,
|
|
309
|
+
elements=elements,
|
|
310
|
+
width=screenshot.width,
|
|
311
|
+
height=screenshot.height,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def disconnect(self) -> None:
|
|
315
|
+
"""Disconnect from the device."""
|
|
316
|
+
self._device = None
|
|
317
|
+
logger.info("UIAutomator2 client disconnected")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_client(device_id: str) -> UIAutomatorClient:
|
|
321
|
+
"""
|
|
322
|
+
Factory function to create a UIAutomatorClient.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
device_id: The Android device serial number
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
UIAutomatorClient instance
|
|
329
|
+
"""
|
|
330
|
+
return UIAutomatorClient(device_id=device_id)
|