kotonebot 0.5.0__py3-none-any.whl → 0.6.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 (103) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -312
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +1002 -1002
  6. kotonebot/backend/context/task_action.py +183 -183
  7. kotonebot/backend/core.py +86 -129
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/image.py +36 -5
  15. kotonebot/backend/loop.py +222 -208
  16. kotonebot/backend/ocr.py +535 -535
  17. kotonebot/backend/preprocessor.py +103 -103
  18. kotonebot/client/__init__.py +9 -9
  19. kotonebot/client/device.py +369 -529
  20. kotonebot/client/fast_screenshot.py +377 -377
  21. kotonebot/client/host/__init__.py +43 -43
  22. kotonebot/client/host/adb_common.py +101 -107
  23. kotonebot/client/host/custom.py +118 -118
  24. kotonebot/client/host/leidian_host.py +196 -196
  25. kotonebot/client/host/mumu12_host.py +353 -353
  26. kotonebot/client/host/protocol.py +214 -214
  27. kotonebot/client/host/windows_common.py +58 -58
  28. kotonebot/client/implements/__init__.py +65 -70
  29. kotonebot/client/implements/adb.py +89 -89
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -188
  34. kotonebot/client/implements/uiautomator2.py +85 -85
  35. kotonebot/client/implements/windows.py +176 -176
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/client/scaler.py +467 -0
  39. kotonebot/config/base_config.py +96 -96
  40. kotonebot/config/config.py +61 -0
  41. kotonebot/config/manager.py +36 -36
  42. kotonebot/core/__init__.py +13 -0
  43. kotonebot/core/entities/base.py +182 -0
  44. kotonebot/core/entities/compound.py +75 -0
  45. kotonebot/core/entities/ocr.py +117 -0
  46. kotonebot/core/entities/template_match.py +198 -0
  47. kotonebot/devtools/__init__.py +42 -0
  48. kotonebot/devtools/cli/__init__.py +6 -0
  49. kotonebot/devtools/cli/main.py +53 -0
  50. kotonebot/{tools → devtools}/mirror.py +354 -354
  51. kotonebot/devtools/project/project.py +41 -0
  52. kotonebot/devtools/project/scanner.py +202 -0
  53. kotonebot/devtools/project/schema.py +99 -0
  54. kotonebot/devtools/resgen/__init__.py +42 -0
  55. kotonebot/devtools/resgen/codegen.py +331 -0
  56. kotonebot/devtools/resgen/core.py +94 -0
  57. kotonebot/devtools/resgen/parsers.py +360 -0
  58. kotonebot/devtools/resgen/utils.py +158 -0
  59. kotonebot/devtools/resgen/validation.py +115 -0
  60. kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
  61. kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
  62. kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
  63. kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
  64. kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
  65. kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
  66. kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
  67. kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
  68. kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
  69. kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
  70. kotonebot/devtools/web/dist/index.html +25 -0
  71. kotonebot/devtools/web/server/__init__.py +0 -0
  72. kotonebot/devtools/web/server/rest_api.py +217 -0
  73. kotonebot/devtools/web/server/server.py +85 -0
  74. kotonebot/errors.py +76 -76
  75. kotonebot/interop/win/__init__.py +11 -9
  76. kotonebot/interop/win/_mouse.py +310 -310
  77. kotonebot/interop/win/message_box.py +313 -313
  78. kotonebot/interop/win/reg.py +37 -37
  79. kotonebot/interop/win/shake_mouse.py +224 -0
  80. kotonebot/interop/win/shortcut.py +43 -43
  81. kotonebot/interop/win/task_dialog.py +513 -513
  82. kotonebot/logging/__init__.py +2 -2
  83. kotonebot/logging/log.py +17 -17
  84. kotonebot/primitives/__init__.py +19 -17
  85. kotonebot/primitives/geometry.py +1067 -862
  86. kotonebot/primitives/visual.py +143 -63
  87. kotonebot/ui/file_host/sensio.py +36 -36
  88. kotonebot/ui/file_host/tmp_send.py +54 -54
  89. kotonebot/ui/pushkit/__init__.py +3 -3
  90. kotonebot/ui/pushkit/image_host.py +88 -88
  91. kotonebot/ui/pushkit/protocol.py +13 -13
  92. kotonebot/ui/pushkit/wxpusher.py +54 -54
  93. kotonebot/ui/user.py +148 -148
  94. kotonebot/util.py +436 -436
  95. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +84 -82
  96. kotonebot-0.6.0.dist-info/RECORD +105 -0
  97. kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
  98. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +673 -673
  99. kotonebot/client/implements/adb_raw.py +0 -163
  100. kotonebot-0.5.0.dist-info/RECORD +0 -71
  101. /kotonebot/{tools → devtools/project}/__init__.py +0 -0
  102. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
  103. {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,189 +1,189 @@
1
- # ruff: noqa: E402
2
- from kotonebot.util import require_windows
3
- require_windows('"RemoteWindowsImpl" implementation')
4
-
5
- import io
6
- import base64
7
- import logging
8
- import xmlrpc.client
9
- import xmlrpc.server
10
- from typing import Literal, cast, Any, Tuple
11
- from functools import cached_property
12
- from threading import Thread
13
- from dataclasses import dataclass
14
-
15
- import cv2
16
- import numpy as np
17
- from cv2.typing import MatLike
18
-
19
- from kotonebot import logging
20
- from ..device import Device, WindowsDevice
21
- from ..protocol import Touchable, Screenshotable
22
- from ..registration import ImplConfig
23
- from .windows import WindowsImpl, WindowsImplConfig
24
-
25
- logger = logging.getLogger(__name__)
26
-
27
- # 定义配置模型
28
- @dataclass
29
- class RemoteWindowsImplConfig(ImplConfig):
30
- windows_impl_config: WindowsImplConfig
31
- host: str = "localhost"
32
- port: int = 8000
33
-
34
- def _encode_image(image: MatLike) -> str:
35
- """Encode an image as a base64 string."""
36
- success, buffer = cv2.imencode('.png', image)
37
- if not success:
38
- raise RuntimeError("Failed to encode image")
39
- return base64.b64encode(buffer.tobytes()).decode('ascii')
40
-
41
- def _decode_image(encoded_image: str) -> MatLike:
42
- """Decode a base64 string to an image."""
43
- buffer = base64.b64decode(encoded_image)
44
- image = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR)
45
- if image is None:
46
- raise RuntimeError("Failed to decode image")
47
- return image
48
-
49
- class RemoteWindowsServer:
50
- """
51
- XML-RPC server that exposes a WindowsImpl instance.
52
-
53
- This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
54
- """
55
-
56
- def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
57
- """Initialize the server with the given host and port."""
58
- self.host = host
59
- self.port = port
60
- self.server = None
61
- self.device = WindowsDevice()
62
- self.impl = WindowsImpl(
63
- WindowsDevice(),
64
- ahk_exe_path=windows_impl_config.ahk_exe_path,
65
- window_title=windows_impl_config.window_title
66
- )
67
- self.device._screenshot = self.impl
68
- self.device._touch = self.impl
69
-
70
- def start(self):
71
- """Start the XML-RPC server."""
72
- self.server = xmlrpc.server.SimpleXMLRPCServer(
73
- (self.host, self.port),
74
- logRequests=True,
75
- allow_none=True
76
- )
77
- self.server.register_instance(self)
78
- logger.info(f"Starting RemoteWindowsServer on {self.host}:{self.port}")
79
- self.server.serve_forever()
80
-
81
- def start_in_thread(self):
82
- """Start the XML-RPC server in a separate thread."""
83
- thread = Thread(target=self.start, daemon=True)
84
- thread.start()
85
- return thread
86
-
87
- # Screenshotable methods
88
-
89
- def screenshot(self) -> str:
90
- """Take a screenshot and return it as a base64-encoded string."""
91
- try:
92
- image = self.impl.screenshot()
93
- return _encode_image(image)
94
- except Exception as e:
95
- logger.error(f"Error taking screenshot: {e}")
96
- raise
97
-
98
- def get_screen_size(self) -> tuple[int, int]:
99
- """Get the screen size."""
100
- return self.impl.screen_size
101
-
102
- def detect_orientation(self) -> str | None:
103
- """Detect the screen orientation."""
104
- return self.impl.detect_orientation()
105
-
106
- # Touchable methods
107
-
108
- def click(self, x: int, y: int) -> bool:
109
- """Click at the given coordinates."""
110
- try:
111
- self.impl.click(x, y)
112
- return True
113
- except Exception as e:
114
- logger.error(f"Error clicking at ({x}, {y}): {e}")
115
- return False
116
-
117
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> bool:
118
- """Swipe from (x1, y1) to (x2, y2)."""
119
- try:
120
- self.impl.swipe(x1, y1, x2, y2, duration)
121
- return True
122
- except Exception as e:
123
- logger.error(f"Error swiping from ({x1}, {y1}) to ({x2}, {y2}): {e}")
124
- return False
125
-
126
- # Other methods
127
-
128
- def get_scale_ratio(self) -> float:
129
- """Get the scale ratio."""
130
- return self.impl.scale_ratio
131
-
132
- def ping(self) -> bool:
133
- """Check if the server is alive."""
134
- return True
135
-
136
-
137
- class RemoteWindowsImpl(Touchable, Screenshotable):
138
- """
139
- Client implementation that connects to a remote Windows machine via XML-RPC.
140
-
141
- This class implements the same interfaces as WindowsImpl but forwards all
142
- method calls to a remote server.
143
- """
144
-
145
- def __init__(self, device: Device, host="localhost", port=8000):
146
- """Initialize the client with the given device, host, and port."""
147
- self.device = device
148
- self.host = host
149
- self.port = port
150
- self.proxy = xmlrpc.client.ServerProxy(
151
- f"http://{host}:{port}/",
152
- allow_none=True
153
- )
154
- # Test connection
155
- try:
156
- if not self.proxy.ping():
157
- raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}")
158
- logger.info(f"Connected to RemoteWindowsServer at {host}:{port}")
159
- except Exception as e:
160
- raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}: {e}")
161
-
162
- @cached_property
163
- def scale_ratio(self) -> float:
164
- """Get the scale ratio from the remote server."""
165
- return cast(float, self.proxy.get_scale_ratio())
166
-
167
- @property
168
- def screen_size(self) -> tuple[int, int]:
169
- """Get the screen size from the remote server."""
170
- return cast(Tuple[int, int], self.proxy.get_screen_size())
171
-
172
- def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
173
- """Detect the screen orientation from the remote server."""
174
- return cast(None | Literal['portrait'] | Literal['landscape'], self.proxy.detect_orientation())
175
-
176
- def screenshot(self) -> MatLike:
177
- """Take a screenshot from the remote server."""
178
- encoded_image = cast(str, self.proxy.screenshot())
179
- return _decode_image(encoded_image)
180
-
181
- def click(self, x: int, y: int) -> None:
182
- """Click at the given coordinates on the remote server."""
183
- if not self.proxy.click(x, y):
184
- raise RuntimeError(f"Failed to click at ({x}, {y})")
185
-
186
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
187
- """Swipe from (x1, y1) to (x2, y2) on the remote server."""
188
- if not self.proxy.swipe(x1, y1, x2, y2, duration):
1
+ # ruff: noqa: E402
2
+ from kotonebot.util import require_windows
3
+ require_windows('"RemoteWindowsImpl" implementation')
4
+
5
+ import io
6
+ import base64
7
+ import logging
8
+ import xmlrpc.client
9
+ import xmlrpc.server
10
+ from typing import Literal, cast, Any, Tuple
11
+ from functools import cached_property
12
+ from threading import Thread
13
+ from dataclasses import dataclass
14
+
15
+ import cv2
16
+ import numpy as np
17
+ from cv2.typing import MatLike
18
+
19
+ from kotonebot import logging
20
+ from ..device import Device, WindowsDevice
21
+ from ..protocol import Touchable, Screenshotable
22
+ from ..registration import ImplConfig
23
+ from .windows import WindowsImpl, WindowsImplConfig
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # 定义配置模型
28
+ @dataclass
29
+ class RemoteWindowsImplConfig(ImplConfig):
30
+ windows_impl_config: WindowsImplConfig
31
+ host: str = "localhost"
32
+ port: int = 8000
33
+
34
+ def _encode_image(image: MatLike) -> str:
35
+ """Encode an image as a base64 string."""
36
+ success, buffer = cv2.imencode('.png', image)
37
+ if not success:
38
+ raise RuntimeError("Failed to encode image")
39
+ return base64.b64encode(buffer.tobytes()).decode('ascii')
40
+
41
+ def _decode_image(encoded_image: str) -> MatLike:
42
+ """Decode a base64 string to an image."""
43
+ buffer = base64.b64decode(encoded_image)
44
+ image = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR)
45
+ if image is None:
46
+ raise RuntimeError("Failed to decode image")
47
+ return image
48
+
49
+ class RemoteWindowsServer:
50
+ """
51
+ XML-RPC server that exposes a WindowsImpl instance.
52
+
53
+ This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
54
+ """
55
+
56
+ def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
57
+ """Initialize the server with the given host and port."""
58
+ self.host = host
59
+ self.port = port
60
+ self.server = None
61
+ self.device = WindowsDevice()
62
+ self.impl = WindowsImpl(
63
+ WindowsDevice(),
64
+ ahk_exe_path=windows_impl_config.ahk_exe_path,
65
+ window_title=windows_impl_config.window_title
66
+ )
67
+ self.device._screenshot = self.impl
68
+ self.device._touch = self.impl
69
+
70
+ def start(self):
71
+ """Start the XML-RPC server."""
72
+ self.server = xmlrpc.server.SimpleXMLRPCServer(
73
+ (self.host, self.port),
74
+ logRequests=True,
75
+ allow_none=True
76
+ )
77
+ self.server.register_instance(self)
78
+ logger.info(f"Starting RemoteWindowsServer on {self.host}:{self.port}")
79
+ self.server.serve_forever()
80
+
81
+ def start_in_thread(self):
82
+ """Start the XML-RPC server in a separate thread."""
83
+ thread = Thread(target=self.start, daemon=True)
84
+ thread.start()
85
+ return thread
86
+
87
+ # Screenshotable methods
88
+
89
+ def screenshot(self) -> str:
90
+ """Take a screenshot and return it as a base64-encoded string."""
91
+ try:
92
+ image = self.impl.screenshot()
93
+ return _encode_image(image)
94
+ except Exception as e:
95
+ logger.error(f"Error taking screenshot: {e}")
96
+ raise
97
+
98
+ def get_screen_size(self) -> tuple[int, int]:
99
+ """Get the screen size."""
100
+ return self.impl.screen_size
101
+
102
+ def detect_orientation(self) -> str | None:
103
+ """Detect the screen orientation."""
104
+ return self.impl.detect_orientation()
105
+
106
+ # Touchable methods
107
+
108
+ def click(self, x: int, y: int) -> bool:
109
+ """Click at the given coordinates."""
110
+ try:
111
+ self.impl.click(x, y)
112
+ return True
113
+ except Exception as e:
114
+ logger.error(f"Error clicking at ({x}, {y}): {e}")
115
+ return False
116
+
117
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> bool:
118
+ """Swipe from (x1, y1) to (x2, y2)."""
119
+ try:
120
+ self.impl.swipe(x1, y1, x2, y2, duration)
121
+ return True
122
+ except Exception as e:
123
+ logger.error(f"Error swiping from ({x1}, {y1}) to ({x2}, {y2}): {e}")
124
+ return False
125
+
126
+ # Other methods
127
+
128
+ def get_scale_ratio(self) -> float:
129
+ """Get the scale ratio."""
130
+ return self.impl.scale_ratio
131
+
132
+ def ping(self) -> bool:
133
+ """Check if the server is alive."""
134
+ return True
135
+
136
+
137
+ class RemoteWindowsImpl(Touchable, Screenshotable):
138
+ """
139
+ Client implementation that connects to a remote Windows machine via XML-RPC.
140
+
141
+ This class implements the same interfaces as WindowsImpl but forwards all
142
+ method calls to a remote server.
143
+ """
144
+
145
+ def __init__(self, device: Device, host="localhost", port=8000):
146
+ """Initialize the client with the given device, host, and port."""
147
+ self.device = device
148
+ self.host = host
149
+ self.port = port
150
+ self.proxy = xmlrpc.client.ServerProxy(
151
+ f"http://{host}:{port}/",
152
+ allow_none=True
153
+ )
154
+ # Test connection
155
+ try:
156
+ if not self.proxy.ping():
157
+ raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}")
158
+ logger.info(f"Connected to RemoteWindowsServer at {host}:{port}")
159
+ except Exception as e:
160
+ raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}: {e}")
161
+
162
+ @cached_property
163
+ def scale_ratio(self) -> float:
164
+ """Get the scale ratio from the remote server."""
165
+ return cast(float, self.proxy.get_scale_ratio())
166
+
167
+ @property
168
+ def screen_size(self) -> tuple[int, int]:
169
+ """Get the screen size from the remote server."""
170
+ return cast(Tuple[int, int], self.proxy.get_screen_size())
171
+
172
+ def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
173
+ """Detect the screen orientation from the remote server."""
174
+ return cast(None | Literal['portrait'] | Literal['landscape'], self.proxy.detect_orientation())
175
+
176
+ def screenshot(self) -> MatLike:
177
+ """Take a screenshot from the remote server."""
178
+ encoded_image = cast(str, self.proxy.screenshot())
179
+ return _decode_image(encoded_image)
180
+
181
+ def click(self, x: int, y: int) -> None:
182
+ """Click at the given coordinates on the remote server."""
183
+ if not self.proxy.click(x, y):
184
+ raise RuntimeError(f"Failed to click at ({x}, {y})")
185
+
186
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
187
+ """Swipe from (x1, y1) to (x2, y2) on the remote server."""
188
+ if not self.proxy.swipe(x1, y1, x2, y2, duration):
189
189
  raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
@@ -1,86 +1,86 @@
1
- import time
2
- from typing import Literal
3
-
4
- import numpy as np
5
- try:
6
- import uiautomator2 as u2
7
- from adbutils._device import AdbDevice as AdbUtilsDevice
8
- except ImportError as _e:
9
- from kotonebot.errors import MissingDependencyError
10
- raise MissingDependencyError(_e, 'android')
11
- from cv2.typing import MatLike
12
-
13
- from kotonebot import logging
14
- from ..device import Device
15
- from ..protocol import Screenshotable, Commandable, Touchable
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- SCREENSHOT_INTERVAL = 0.2
20
-
21
- class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
22
- def __init__(self, adb_connection: AdbUtilsDevice):
23
- self.u2_client = u2.Device(adb_connection.serial)
24
- self.__last_screenshot_time = 0
25
-
26
- def screenshot(self) -> MatLike:
27
- """
28
- 截图
29
- """
30
- from kotonebot import sleep
31
- delta = time.time() - self.__last_screenshot_time
32
- if delta < SCREENSHOT_INTERVAL:
33
- time.sleep(SCREENSHOT_INTERVAL - delta)
34
- start_time = time.time()
35
- image = self.u2_client.screenshot(format='opencv')
36
- logger.verbose(f'uiautomator2 screenshot: {time.time() - start_time}s')
37
- self.__last_screenshot_time = time.time()
38
- assert isinstance(image, np.ndarray)
39
- return image
40
-
41
- @property
42
- def screen_size(self) -> tuple[int, int]:
43
- info = self.u2_client.info
44
- sizes = info['displayWidth'], info['displayHeight']
45
- return sizes
46
-
47
- def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
48
- """
49
- 检测设备方向
50
- """
51
- orientation = self.u2_client.info['displayRotation']
52
- if orientation == 1:
53
- return 'portrait'
54
- elif orientation == 0:
55
- return 'landscape'
56
- else:
57
- return None
58
-
59
- def launch_app(self, package_name: str) -> None:
60
- """
61
- 启动应用
62
- """
63
- self.u2_client.app_start(package_name)
64
-
65
- def current_package(self) -> str | None:
66
- """
67
- 获取当前应用包名
68
- """
69
- try:
70
- result = self.u2_client.app_current()
71
- logger.verbose(f'uiautomator2 current_package: {result}')
72
- return result['package']
73
- except:
74
- return None
75
-
76
- def click(self, x: int, y: int) -> None:
77
- """
78
- 点击屏幕
79
- """
80
- self.u2_client.click(x, y)
81
-
82
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None) -> None:
83
- """
84
- 滑动屏幕
85
- """
1
+ import time
2
+ from typing import Literal
3
+
4
+ import numpy as np
5
+ try:
6
+ import uiautomator2 as u2
7
+ from adbutils._device import AdbDevice as AdbUtilsDevice
8
+ except ImportError as _e:
9
+ from kotonebot.errors import MissingDependencyError
10
+ raise MissingDependencyError(_e, 'android')
11
+ from cv2.typing import MatLike
12
+
13
+ from kotonebot import logging
14
+ from ..device import Device
15
+ from ..protocol import Screenshotable, Commandable, Touchable
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ SCREENSHOT_INTERVAL = 0.2
20
+
21
+ class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
22
+ def __init__(self, adb_connection: AdbUtilsDevice):
23
+ self.u2_client = u2.Device(adb_connection.serial)
24
+ self.__last_screenshot_time = 0
25
+
26
+ def screenshot(self) -> MatLike:
27
+ """
28
+ 截图
29
+ """
30
+ from kotonebot import sleep
31
+ delta = time.time() - self.__last_screenshot_time
32
+ if delta < SCREENSHOT_INTERVAL:
33
+ time.sleep(SCREENSHOT_INTERVAL - delta)
34
+ start_time = time.time()
35
+ image = self.u2_client.screenshot(format='opencv')
36
+ logger.verbose(f'uiautomator2 screenshot: {time.time() - start_time}s')
37
+ self.__last_screenshot_time = time.time()
38
+ assert isinstance(image, np.ndarray)
39
+ return image
40
+
41
+ @property
42
+ def screen_size(self) -> tuple[int, int]:
43
+ info = self.u2_client.info
44
+ sizes = info['displayWidth'], info['displayHeight']
45
+ return sizes
46
+
47
+ def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
48
+ """
49
+ 检测设备方向
50
+ """
51
+ orientation = self.u2_client.info['displayRotation']
52
+ if orientation == 1:
53
+ return 'portrait'
54
+ elif orientation == 0:
55
+ return 'landscape'
56
+ else:
57
+ return None
58
+
59
+ def launch_app(self, package_name: str) -> None:
60
+ """
61
+ 启动应用
62
+ """
63
+ self.u2_client.app_start(package_name)
64
+
65
+ def current_package(self) -> str | None:
66
+ """
67
+ 获取当前应用包名
68
+ """
69
+ try:
70
+ result = self.u2_client.app_current()
71
+ logger.verbose(f'uiautomator2 current_package: {result}')
72
+ return result['package']
73
+ except:
74
+ return None
75
+
76
+ def click(self, x: int, y: int) -> None:
77
+ """
78
+ 点击屏幕
79
+ """
80
+ self.u2_client.click(x, y)
81
+
82
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None) -> None:
83
+ """
84
+ 滑动屏幕
85
+ """
86
86
  self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)