This commit is contained in:
+15
-6
@@ -75,10 +75,13 @@ def tool_handlers(
|
|||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
manager=device_manager,
|
manager=device_manager,
|
||||||
),
|
),
|
||||||
"get_ui_tree": lambda device_id=None: call_with_semantic_errors(
|
"get_ui_tree": lambda device_id=None, include_app_info=False: (
|
||||||
get_ui_tree,
|
call_with_semantic_errors(
|
||||||
device_id,
|
get_ui_tree,
|
||||||
manager=device_manager,
|
device_id,
|
||||||
|
manager=device_manager,
|
||||||
|
include_app_info=include_app_info,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"describe_screen": lambda device_id=None: call_with_semantic_errors(
|
"describe_screen": lambda device_id=None: call_with_semantic_errors(
|
||||||
lambda: describe_screen(device_id, manager=device_manager).to_dict()
|
lambda: describe_screen(device_id, manager=device_manager).to_dict()
|
||||||
@@ -150,8 +153,14 @@ def create_mcp_server(
|
|||||||
return handlers["find_icon"](name=name, device_id=device_id)
|
return handlers["find_icon"](name=name, device_id=device_id)
|
||||||
|
|
||||||
@server.tool(name="get_ui_tree")
|
@server.tool(name="get_ui_tree")
|
||||||
def _get_ui_tree(device_id: str | None = None) -> Any:
|
def _get_ui_tree(
|
||||||
return handlers["get_ui_tree"](device_id=device_id)
|
device_id: str | None = None,
|
||||||
|
include_app_info: bool = False,
|
||||||
|
) -> Any:
|
||||||
|
return handlers["get_ui_tree"](
|
||||||
|
device_id=device_id,
|
||||||
|
include_app_info=include_app_info,
|
||||||
|
)
|
||||||
|
|
||||||
@server.tool(name="describe_screen")
|
@server.tool(name="describe_screen")
|
||||||
def _describe_screen(device_id: str | None = None) -> dict[str, Any]:
|
def _describe_screen(device_id: str | None = None) -> dict[str, Any]:
|
||||||
|
|||||||
+43
-1
@@ -75,6 +75,37 @@ _STATE_FIELDS = ("enabled", "clickable", "selected", "checked", "focused")
|
|||||||
_COLOR_FIELDS = ("foreground_color", "background_color")
|
_COLOR_FIELDS = ("foreground_color", "background_color")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ActiveApp:
|
||||||
|
"""Native identifier for the application currently in the foreground."""
|
||||||
|
|
||||||
|
platform: str
|
||||||
|
bundle_id: str | None = None
|
||||||
|
package: str | None = None
|
||||||
|
activity: str | None = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, str]:
|
||||||
|
data = {"platform": self.platform}
|
||||||
|
for field_name in ("bundle_id", "package", "activity"):
|
||||||
|
value = getattr(self, field_name)
|
||||||
|
if value is not None:
|
||||||
|
data[field_name] = value
|
||||||
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: dict[str, Any]) -> "ActiveApp":
|
||||||
|
def text(key: str) -> str | None:
|
||||||
|
value = data.get(key)
|
||||||
|
return value if isinstance(value, str) and value else None
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
platform=text("platform") or "unknown",
|
||||||
|
bundle_id=text("bundle_id"),
|
||||||
|
package=text("package"),
|
||||||
|
activity=text("activity"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SceneElement:
|
class SceneElement:
|
||||||
id: str
|
id: str
|
||||||
@@ -143,12 +174,19 @@ class Scene:
|
|||||||
# Keep raw OCR observations for local execution evidence without duplicating
|
# Keep raw OCR observations for local execution evidence without duplicating
|
||||||
# them in the normalized, LLM-facing scene payload.
|
# them in the normalized, LLM-facing scene payload.
|
||||||
ocr_elements: list[SceneElement] = field(default_factory=list)
|
ocr_elements: list[SceneElement] = field(default_factory=list)
|
||||||
|
# The foreground app is supplied by the Driver, separately from the
|
||||||
|
# accessibility tree, and is absent for drivers that cannot query it.
|
||||||
|
# Kept last to preserve Scene's existing positional constructor arguments.
|
||||||
|
active_app: ActiveApp | None = None
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
data: dict[str, Any] = {
|
||||||
"screen": {"width": self.width, "height": self.height},
|
"screen": {"width": self.width, "height": self.height},
|
||||||
"elements": [element.to_dict() for element in self.elements],
|
"elements": [element.to_dict() for element in self.elements],
|
||||||
}
|
}
|
||||||
|
if self.active_app is not None:
|
||||||
|
data["app"] = self.active_app.to_dict()
|
||||||
|
return data
|
||||||
|
|
||||||
def ocr_results_to_dict(self) -> list[dict[str, Any]]:
|
def ocr_results_to_dict(self) -> list[dict[str, Any]]:
|
||||||
return [element.to_dict() for element in self.ocr_elements]
|
return [element.to_dict() for element in self.ocr_elements]
|
||||||
@@ -156,12 +194,16 @@ class Scene:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: dict[str, Any]) -> "Scene":
|
def from_dict(cls, data: dict[str, Any]) -> "Scene":
|
||||||
screen = data.get("screen") or {}
|
screen = data.get("screen") or {}
|
||||||
|
raw_app = data.get("app")
|
||||||
return cls(
|
return cls(
|
||||||
width=int(screen.get("width") or data.get("width") or 0),
|
width=int(screen.get("width") or data.get("width") or 0),
|
||||||
height=int(screen.get("height") or data.get("height") or 0),
|
height=int(screen.get("height") or data.get("height") or 0),
|
||||||
elements=[
|
elements=[
|
||||||
SceneElement.from_dict(element) for element in data.get("elements", [])
|
SceneElement.from_dict(element) for element in data.get("elements", [])
|
||||||
],
|
],
|
||||||
|
active_app=ActiveApp.from_dict(raw_app)
|
||||||
|
if isinstance(raw_app, dict)
|
||||||
|
else None,
|
||||||
ocr_elements=[
|
ocr_elements=[
|
||||||
SceneElement.from_dict(element)
|
SceneElement.from_dict(element)
|
||||||
for element in data.get("ocr_elements", [])
|
for element in data.get("ocr_elements", [])
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.errors import DeviceOfflineError, DriverError
|
from core.errors import DeviceOfflineError, DriverError
|
||||||
|
from core.models import ActiveApp
|
||||||
from driver.base import Driver
|
from driver.base import Driver
|
||||||
|
|
||||||
# Android KeyEvent.KEYCODE_HOME. Kept as a literal rather than importing the
|
# Android KeyEvent.KEYCODE_HOME. Kept as a literal rather than importing the
|
||||||
@@ -170,6 +171,19 @@ class AndroidDriver(Driver):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise DriverError("ui tree retrieval failed") from exc
|
raise DriverError("ui tree retrieval failed") from exc
|
||||||
|
|
||||||
|
def active_app(self) -> ActiveApp | None:
|
||||||
|
client = self._require_client()
|
||||||
|
try:
|
||||||
|
package = client.current_package
|
||||||
|
activity = client.current_activity
|
||||||
|
except Exception as exc:
|
||||||
|
raise DriverError("active app retrieval failed") from exc
|
||||||
|
package = package if isinstance(package, str) and package else None
|
||||||
|
activity = activity if isinstance(activity, str) and activity else None
|
||||||
|
if package is None and activity is None:
|
||||||
|
return None
|
||||||
|
return ActiveApp(platform="android", package=package, activity=activity)
|
||||||
|
|
||||||
def home(self) -> None:
|
def home(self) -> None:
|
||||||
client = self._require_client()
|
client = self._require_client()
|
||||||
try:
|
try:
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from core.models import ActiveApp
|
||||||
|
|
||||||
|
|
||||||
class Driver(ABC):
|
class Driver(ABC):
|
||||||
"""Driver-independent device capability interface.
|
"""Driver-independent device capability interface.
|
||||||
@@ -71,6 +73,10 @@ class Driver(ABC):
|
|||||||
def tree(self) -> Any:
|
def tree(self) -> Any:
|
||||||
"""Return the raw UI tree from the device driver."""
|
"""Return the raw UI tree from the device driver."""
|
||||||
|
|
||||||
|
def active_app(self) -> ActiveApp | None:
|
||||||
|
"""Return foreground application metadata when the driver supports it."""
|
||||||
|
return None
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def home(self) -> None:
|
def home(self) -> None:
|
||||||
"""Press the device home button."""
|
"""Press the device home button."""
|
||||||
@@ -82,4 +88,3 @@ class Driver(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def unlock(self) -> None:
|
def unlock(self) -> None:
|
||||||
"""Unlock the device."""
|
"""Unlock the device."""
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.errors import DeviceOfflineError, DriverError
|
from core.errors import DeviceOfflineError, DriverError
|
||||||
|
from core.models import ActiveApp
|
||||||
from driver.base import Driver
|
from driver.base import Driver
|
||||||
|
|
||||||
|
|
||||||
@@ -161,6 +162,19 @@ class WDADriver(Driver):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise DriverError("ui tree retrieval failed") from exc
|
raise DriverError("ui tree retrieval failed") from exc
|
||||||
|
|
||||||
|
def active_app(self) -> ActiveApp | None:
|
||||||
|
client = self._require_client()
|
||||||
|
try:
|
||||||
|
response = client.execute_script("mobile: activeAppInfo")
|
||||||
|
except Exception as exc:
|
||||||
|
raise DriverError("active app retrieval failed") from exc
|
||||||
|
if not isinstance(response, dict):
|
||||||
|
return None
|
||||||
|
bundle_id = response.get("bundleId")
|
||||||
|
if not isinstance(bundle_id, str) or not bundle_id:
|
||||||
|
return None
|
||||||
|
return ActiveApp(platform="ios", bundle_id=bundle_id)
|
||||||
|
|
||||||
def home(self) -> None:
|
def home(self) -> None:
|
||||||
client = self._require_client()
|
client = self._require_client()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ list of UI elements with id, type, text, and pixel bounds), and — when
|
|||||||
available — a screenshot of the same screen and a short history of recent
|
available — a screenshot of the same screen and a short history of recent
|
||||||
actions and their outcomes.
|
actions and their outcomes.
|
||||||
|
|
||||||
|
The Scene may include an `app` object with the live foreground application's
|
||||||
|
native identifier. On iOS, use `bundle_id`; on Android, use `package` and
|
||||||
|
`activity`. This metadata may be omitted when the current driver cannot query
|
||||||
|
it.
|
||||||
|
|
||||||
Some elements also carry accessibility state fields when the platform
|
Some elements also carry accessibility state fields when the platform
|
||||||
reports them: `enabled`, `clickable`, `selected`, `checked`, `focused`.
|
reports them: `enabled`, `clickable`, `selected`, `checked`, `focused`.
|
||||||
A field is omitted entirely when the platform does not report it for that
|
A field is omitted entirely when the platform does not report it for that
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from core.models import ActiveApp
|
||||||
from driver.base import Driver
|
from driver.base import Driver
|
||||||
|
|
||||||
PNG_10X20 = (
|
PNG_10X20 = (
|
||||||
@@ -27,12 +28,14 @@ class FakeDriver(Driver):
|
|||||||
fail_connect: bool = False,
|
fail_connect: bool = False,
|
||||||
tree: Any = TREE_XML,
|
tree: Any = TREE_XML,
|
||||||
screenshot: bytes = PNG_10X20,
|
screenshot: bytes = PNG_10X20,
|
||||||
|
active_app: ActiveApp | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.fail_connect = fail_connect
|
self.fail_connect = fail_connect
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.calls: list[tuple[str, tuple[Any, ...]]] = []
|
self.calls: list[tuple[str, tuple[Any, ...]]] = []
|
||||||
self._tree = tree
|
self._tree = tree
|
||||||
self._screenshot = screenshot
|
self._screenshot = screenshot
|
||||||
|
self._active_app = active_app
|
||||||
|
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
self.calls.append(("connect", ()))
|
self.calls.append(("connect", ()))
|
||||||
@@ -85,6 +88,10 @@ class FakeDriver(Driver):
|
|||||||
self.calls.append(("tree", ()))
|
self.calls.append(("tree", ()))
|
||||||
return self._tree
|
return self._tree
|
||||||
|
|
||||||
|
def active_app(self) -> ActiveApp | None:
|
||||||
|
self.calls.append(("active_app", ()))
|
||||||
|
return self._active_app
|
||||||
|
|
||||||
def home(self) -> None:
|
def home(self) -> None:
|
||||||
self.calls.append(("home", ()))
|
self.calls.append(("home", ()))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from core.models import ActiveApp
|
||||||
|
from driver.android_driver import AndroidDriver
|
||||||
|
from driver.wda_driver import WDADriver
|
||||||
|
from tools.ui_tree import get_ui_tree
|
||||||
|
from device.manager import DeviceManager
|
||||||
|
from tests.fakes import FakeDriver
|
||||||
|
|
||||||
|
|
||||||
|
def test_android_driver_reads_current_package_and_activity() -> None:
|
||||||
|
driver = AndroidDriver()
|
||||||
|
driver._client = MagicMock()
|
||||||
|
driver._client.current_package = "com.example.mobile"
|
||||||
|
driver._client.current_activity = ".MainActivity"
|
||||||
|
|
||||||
|
assert driver.active_app() == ActiveApp(
|
||||||
|
platform="android",
|
||||||
|
package="com.example.mobile",
|
||||||
|
activity=".MainActivity",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wda_driver_reads_current_bundle_id() -> None:
|
||||||
|
driver = WDADriver()
|
||||||
|
driver._client = MagicMock()
|
||||||
|
driver._client.execute_script.return_value = {
|
||||||
|
"bundleId": "com.example.mobile",
|
||||||
|
"pid": 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert driver.active_app() == ActiveApp(
|
||||||
|
platform="ios",
|
||||||
|
bundle_id="com.example.mobile",
|
||||||
|
)
|
||||||
|
driver._client.execute_script.assert_called_once_with("mobile: activeAppInfo")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_tree_keeps_list_response_without_active_app_option() -> None:
|
||||||
|
manager = DeviceManager()
|
||||||
|
manager.register_device(
|
||||||
|
"phone-1",
|
||||||
|
lambda: FakeDriver(
|
||||||
|
active_app=ActiveApp(platform="ios", bundle_id="com.example.mobile")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
manager.connect("phone-1")
|
||||||
|
|
||||||
|
response = get_ui_tree("phone-1", manager=manager)
|
||||||
|
|
||||||
|
assert isinstance(response, list)
|
||||||
|
assert any(node["text"] == "Search" for node in response)
|
||||||
@@ -3,11 +3,12 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from core.errors import DriverError
|
from core.errors import DriverError
|
||||||
from core.models import Bounds
|
from core.models import ActiveApp, Bounds, Scene
|
||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from driver.base import Driver
|
from driver.base import Driver
|
||||||
from perception.ocr import OCRBox
|
from perception.ocr import OCRBox
|
||||||
from tools.describe_screen import describe_screen
|
from tools.describe_screen import describe_screen
|
||||||
|
from tests.fakes import FakeDriver
|
||||||
|
|
||||||
PNG_10X20 = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0a\x00\x00\x00\x14"
|
PNG_10X20 = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0a\x00\x00\x00\x14"
|
||||||
|
|
||||||
@@ -91,3 +92,28 @@ def test_describe_screen_degrades_to_ocr_only_when_ui_tree_unavailable() -> None
|
|||||||
|
|
||||||
assert [element.text for element in scene.elements] == ["Search"]
|
assert [element.text for element in scene.elements] == ["Search"]
|
||||||
assert all(element.source == "ocr" for element in scene.elements)
|
assert all(element.source == "ocr" for element in scene.elements)
|
||||||
|
|
||||||
|
|
||||||
|
def test_describe_screen_includes_live_active_app_metadata() -> None:
|
||||||
|
active_app = ActiveApp(
|
||||||
|
platform="android",
|
||||||
|
package="com.example.mobile",
|
||||||
|
activity=".MainActivity",
|
||||||
|
)
|
||||||
|
manager = DeviceManager()
|
||||||
|
manager.register_device("phone-1", lambda: FakeDriver(active_app=active_app))
|
||||||
|
manager.connect("phone-1")
|
||||||
|
|
||||||
|
scene = describe_screen(
|
||||||
|
"phone-1",
|
||||||
|
manager=manager,
|
||||||
|
ocr_engine=FakeOCREngine([]), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert scene.active_app == active_app
|
||||||
|
assert scene.to_dict()["app"] == {
|
||||||
|
"platform": "android",
|
||||||
|
"package": "com.example.mobile",
|
||||||
|
"activity": ".MainActivity",
|
||||||
|
}
|
||||||
|
assert Scene.from_dict(scene.to_dict()).active_app == active_app
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from api.mcp import tool_handlers
|
from api.mcp import tool_handlers
|
||||||
|
from core.models import ActiveApp
|
||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from tests.fakes import FakeDriver
|
from tests.fakes import FakeDriver
|
||||||
|
|
||||||
@@ -36,3 +37,30 @@ def test_mcp_handlers_are_semantic_and_hide_driver_terms() -> None:
|
|||||||
assert "Appium" not in serialized
|
assert "Appium" not in serialized
|
||||||
assert "XCUI" not in serialized
|
assert "XCUI" not in serialized
|
||||||
assert all(response is not None for response in responses)
|
assert all(response is not None for response in responses)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_ui_tree_can_include_active_app_info() -> None:
|
||||||
|
manager = DeviceManager()
|
||||||
|
manager.register_device(
|
||||||
|
"android-1",
|
||||||
|
lambda: FakeDriver(
|
||||||
|
active_app=ActiveApp(
|
||||||
|
platform="android",
|
||||||
|
package="com.example.mobile",
|
||||||
|
activity=".MainActivity",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
manager.connect("android-1", max_retries=1)
|
||||||
|
|
||||||
|
response = tool_handlers(manager=manager)["get_ui_tree"](
|
||||||
|
device_id="android-1",
|
||||||
|
include_app_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response["app"] == {
|
||||||
|
"platform": "android",
|
||||||
|
"package": "com.example.mobile",
|
||||||
|
"activity": ".MainActivity",
|
||||||
|
}
|
||||||
|
assert isinstance(response["nodes"], list)
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.models import ActiveApp
|
||||||
from device.manager import DEFAULT_MANAGER, DeviceManager
|
from device.manager import DEFAULT_MANAGER, DeviceManager
|
||||||
from driver.base import Driver
|
from driver.base import Driver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_driver(
|
def get_driver(
|
||||||
device_id: str | None = None,
|
device_id: str | None = None,
|
||||||
@@ -10,3 +15,15 @@ def get_driver(
|
|||||||
manager: DeviceManager | None = None,
|
manager: DeviceManager | None = None,
|
||||||
) -> Driver:
|
) -> Driver:
|
||||||
return (manager or DEFAULT_MANAGER).active_driver(device_id)
|
return (manager or DEFAULT_MANAGER).active_driver(device_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_app_info(driver: Driver) -> ActiveApp | None:
|
||||||
|
"""Best-effort foreground application lookup that cannot block observation."""
|
||||||
|
try:
|
||||||
|
return driver.active_app()
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"active app retrieval failed; continuing without app metadata",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from core.models import Scene
|
|||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from perception.ocr import PaddleOCREngine
|
from perception.ocr import PaddleOCREngine
|
||||||
from perception.provider import DefaultPerceptionProvider, PerceptionProvider
|
from perception.provider import DefaultPerceptionProvider, PerceptionProvider
|
||||||
from tools._device import get_driver
|
from tools._device import get_active_app_info, get_driver
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,4 +28,6 @@ def describe_screen(
|
|||||||
)
|
)
|
||||||
raw_tree = None
|
raw_tree = None
|
||||||
provider = perception_provider or DefaultPerceptionProvider(ocr_engine=ocr_engine)
|
provider = perception_provider or DefaultPerceptionProvider(ocr_engine=ocr_engine)
|
||||||
return provider.build_scene(screenshot, raw_tree)
|
scene = provider.build_scene(screenshot, raw_tree)
|
||||||
|
scene.active_app = get_active_app_info(driver)
|
||||||
|
return scene
|
||||||
|
|||||||
+12
-6
@@ -4,7 +4,7 @@ from typing import Any
|
|||||||
|
|
||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from perception.ui_parser import parse_ui_tree
|
from perception.ui_parser import parse_ui_tree
|
||||||
from tools._device import get_driver
|
from tools._device import get_active_app_info, get_driver
|
||||||
|
|
||||||
|
|
||||||
def get_raw_ui_tree(
|
def get_raw_ui_tree(
|
||||||
@@ -19,8 +19,14 @@ def get_ui_tree(
|
|||||||
device_id: str | None = None,
|
device_id: str | None = None,
|
||||||
*,
|
*,
|
||||||
manager: DeviceManager | None = None,
|
manager: DeviceManager | None = None,
|
||||||
) -> list[dict[str, object]]:
|
include_app_info: bool = False,
|
||||||
return [
|
) -> list[dict[str, object]] | dict[str, object]:
|
||||||
element.to_dict()
|
driver = get_driver(device_id, manager=manager)
|
||||||
for element in parse_ui_tree(get_raw_ui_tree(device_id, manager=manager))
|
nodes = [element.to_dict() for element in parse_ui_tree(driver.tree())]
|
||||||
]
|
if not include_app_info:
|
||||||
|
return nodes
|
||||||
|
active_app = get_active_app_info(driver)
|
||||||
|
return {
|
||||||
|
"app": active_app.to_dict() if active_app is not None else None,
|
||||||
|
"nodes": nodes,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user