autoxkit 2.0.0__tar.gz → 2.1.0__tar.gz

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 (35) hide show
  1. {autoxkit-2.0.0 → autoxkit-2.1.0}/PKG-INFO +1 -1
  2. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/android/adb.py +10 -0
  3. autoxkit-2.1.0/autoxkit/android/control.py +246 -0
  4. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/android/streams.py +55 -1
  5. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/hook/hook_listener.py +3 -2
  6. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit.egg-info/PKG-INFO +1 -1
  7. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit.egg-info/SOURCES.txt +1 -0
  8. {autoxkit-2.0.0 → autoxkit-2.1.0}/pyproject.toml +1 -1
  9. {autoxkit-2.0.0 → autoxkit-2.1.0}/LICENSE.txt +0 -0
  10. {autoxkit-2.0.0 → autoxkit-2.1.0}/README.md +0 -0
  11. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/__init__.py +0 -0
  12. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/android/__init__.py +0 -0
  13. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/android/binary.py +0 -0
  14. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/android/client.py +0 -0
  15. /autoxkit-2.0.0/autoxkit/android/control.py → /autoxkit-2.1.0/autoxkit/android/control_backup.py +0 -0
  16. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/android/models.py +0 -0
  17. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/constants.py +0 -0
  18. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/hook/__init__.py +0 -0
  19. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/hook/event.py +0 -0
  20. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/hook/hotkey_listener.py +0 -0
  21. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/match/__init__.py +0 -0
  22. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/match/match.py +0 -0
  23. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/mousekey/__init__.py +0 -0
  24. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/mousekey/input.py +0 -0
  25. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/mousekey/keyboard.py +0 -0
  26. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/mousekey/mouse.py +0 -0
  27. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/utils.py +0 -0
  28. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/window/__init__.py +0 -0
  29. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/window/window.py +0 -0
  30. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/window/window_action.py +0 -0
  31. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit/window/window_match.py +0 -0
  32. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit.egg-info/dependency_links.txt +0 -0
  33. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit.egg-info/requires.txt +0 -0
  34. {autoxkit-2.0.0 → autoxkit-2.1.0}/autoxkit.egg-info/top_level.txt +0 -0
  35. {autoxkit-2.0.0 → autoxkit-2.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autoxkit
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Python library for Windows automation and Android device screen casting and control
5
5
  Author-email: YorickFin <1582456060@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -3,10 +3,17 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
+ import os
6
7
  import re
8
+ import subprocess
7
9
  from dataclasses import dataclass
8
10
  from pathlib import Path
9
11
 
12
+ if os.name == "nt":
13
+ _CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW
14
+ else:
15
+ _CREATE_NO_WINDOW = 0
16
+
10
17
 
11
18
  class AdbError(RuntimeError):
12
19
  pass
@@ -66,6 +73,7 @@ class AdbServerLauncher:
66
73
  "devices", "-l",
67
74
  stdout=asyncio.subprocess.PIPE,
68
75
  stderr=asyncio.subprocess.PIPE,
76
+ creationflags=_CREATE_NO_WINDOW,
69
77
  )
70
78
  stdout, _ = await proc.communicate()
71
79
  out = stdout.decode(errors="replace")
@@ -149,6 +157,7 @@ class AdbServerLauncher:
149
157
  *args,
150
158
  stdout=asyncio.subprocess.PIPE,
151
159
  stderr=asyncio.subprocess.PIPE,
160
+ creationflags=_CREATE_NO_WINDOW,
152
161
  )
153
162
 
154
163
  async def _adb(self, *args: str, check: bool = True) -> asyncio.subprocess.Process:
@@ -173,6 +182,7 @@ class AdbServerLauncher:
173
182
  *args,
174
183
  stdout=asyncio.subprocess.PIPE,
175
184
  stderr=asyncio.subprocess.PIPE,
185
+ creationflags=_CREATE_NO_WINDOW,
176
186
  )
177
187
  stdout, stderr = await proc.communicate()
178
188
  out = stdout.decode(errors="replace").strip()
@@ -0,0 +1,246 @@
1
+ """Control and device-message serializers for scrcpy-server v4.0."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .binary import (
6
+ ProtocolError,
7
+ float_to_i16fp,
8
+ float_to_u16fp,
9
+ i16be,
10
+ i32be,
11
+ read_u16be,
12
+ read_u32be,
13
+ read_u64be,
14
+ string8,
15
+ string32,
16
+ u16be,
17
+ u32be,
18
+ u64be,
19
+ )
20
+ from .models import ControlMessage, ControlMessageType, DeviceMessage, DeviceMessageType, Position
21
+
22
+ MESSAGE_MAX_SIZE = 1 << 18
23
+ INJECT_TEXT_MAX_LENGTH = 300
24
+ CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14
25
+
26
+ ACTION_DOWN = 0
27
+ ACTION_UP = 1
28
+ ACTION_MOVE = 2
29
+
30
+ POINTER_ID_MOUSE = -1 & 0xFFFFFFFFFFFFFFFF
31
+ POINTER_ID_GENERIC_FINGER = -2 & 0xFFFFFFFFFFFFFFFF
32
+ MAX_TOUCH_POINTERS = 10
33
+
34
+
35
+ class PointerManager:
36
+ """Manages a pool of unique pointer IDs for multi-touch.
37
+
38
+ Allocates pointer IDs from 0 upward, up to `MAX_TOUCH_POINTERS`
39
+ concurrent touches. IDs are reused when released.
40
+ """
41
+
42
+ def __init__(self) -> None:
43
+ self._next_id = 0
44
+ self._freed: list[int] = []
45
+ self._active: set[int] = set()
46
+
47
+ def allocate(self) -> int | None:
48
+ """Allocate a unique pointer ID.
49
+
50
+ Returns an integer pointer ID, or `None` if the maximum number
51
+ of concurrent touches (`MAX_TOUCH_POINTERS`) has been reached.
52
+ """
53
+ if len(self._active) >= MAX_TOUCH_POINTERS:
54
+ return None
55
+ if self._freed:
56
+ pid = self._freed.pop()
57
+ else:
58
+ pid = self._next_id
59
+ self._next_id += 1
60
+ self._active.add(pid)
61
+ return pid
62
+
63
+ def release(self, pointer_id: int) -> None:
64
+ """Release a previously allocated pointer ID back to the pool."""
65
+ if pointer_id in self._active:
66
+ self._active.discard(pointer_id)
67
+ self._freed.append(pointer_id)
68
+
69
+ def reset(self) -> None:
70
+ """Release all active pointer IDs."""
71
+ self._active.clear()
72
+ self._freed.clear()
73
+ self._next_id = 0
74
+
75
+ def active_count(self) -> int:
76
+ """Return the number of currently active pointer IDs."""
77
+ return len(self._active)
78
+
79
+ def active_ids(self) -> set[int]:
80
+ """Return the set of currently active pointer IDs."""
81
+ return set(self._active)
82
+
83
+
84
+
85
+
86
+ def _position(position: Position) -> bytes:
87
+ return (
88
+ i32be(position.x)
89
+ + i32be(position.y)
90
+ + u16be(position.screen_width)
91
+ + u16be(position.screen_height)
92
+ )
93
+
94
+
95
+ def inject_keycode(action: int, keycode: int, repeat: int = 0, metastate: int = 0) -> ControlMessage:
96
+ payload = (
97
+ bytes([ControlMessageType.INJECT_KEYCODE, action])
98
+ + i32be(keycode)
99
+ + u32be(repeat)
100
+ + u32be(metastate)
101
+ )
102
+ return ControlMessage(ControlMessageType.INJECT_KEYCODE, payload)
103
+
104
+
105
+ def inject_text(text: str) -> ControlMessage:
106
+ payload = bytes([ControlMessageType.INJECT_TEXT]) + string32(text, INJECT_TEXT_MAX_LENGTH)
107
+ return ControlMessage(ControlMessageType.INJECT_TEXT, payload)
108
+
109
+
110
+ def inject_touch_event(
111
+ action: int,
112
+ position: Position,
113
+ pointer_id: int = POINTER_ID_GENERIC_FINGER,
114
+ pressure: float = 1.0,
115
+ action_button: int = 0,
116
+ buttons: int = 0,
117
+ ) -> ControlMessage:
118
+ payload = (
119
+ bytes([ControlMessageType.INJECT_TOUCH_EVENT, action])
120
+ + u64be(pointer_id)
121
+ + _position(position)
122
+ + u16be(float_to_u16fp(pressure))
123
+ + u32be(action_button)
124
+ + u32be(buttons)
125
+ )
126
+ return ControlMessage(ControlMessageType.INJECT_TOUCH_EVENT, payload)
127
+
128
+
129
+ def inject_scroll_event(position: Position, hscroll: float = 0.0, vscroll: float = 0.0, buttons: int = 0) -> ControlMessage:
130
+ h = float_to_i16fp(hscroll / 16)
131
+ v = float_to_i16fp(vscroll / 16)
132
+ payload = (
133
+ bytes([ControlMessageType.INJECT_SCROLL_EVENT])
134
+ + _position(position)
135
+ + i16be(h)
136
+ + i16be(v)
137
+ + u32be(buttons)
138
+ )
139
+ return ControlMessage(ControlMessageType.INJECT_SCROLL_EVENT, payload)
140
+
141
+
142
+ def back_or_screen_on(action: int) -> ControlMessage:
143
+ return ControlMessage(ControlMessageType.BACK_OR_SCREEN_ON, bytes([ControlMessageType.BACK_OR_SCREEN_ON, action]))
144
+
145
+
146
+ def get_clipboard(copy_key: int = 0) -> ControlMessage:
147
+ return ControlMessage(ControlMessageType.GET_CLIPBOARD, bytes([ControlMessageType.GET_CLIPBOARD, copy_key]))
148
+
149
+
150
+ def set_clipboard(sequence: int, text: str, paste: bool = False) -> ControlMessage:
151
+ payload = (
152
+ bytes([ControlMessageType.SET_CLIPBOARD])
153
+ + u64be(sequence)
154
+ + bytes([1 if paste else 0])
155
+ + string32(text, CLIPBOARD_TEXT_MAX_LENGTH)
156
+ )
157
+ return ControlMessage(ControlMessageType.SET_CLIPBOARD, payload)
158
+
159
+
160
+ def set_display_power(on: bool) -> ControlMessage:
161
+ return ControlMessage(ControlMessageType.SET_DISPLAY_POWER, bytes([ControlMessageType.SET_DISPLAY_POWER, int(on)]))
162
+
163
+
164
+ def empty(message_type: ControlMessageType) -> ControlMessage:
165
+ if message_type not in {
166
+ ControlMessageType.EXPAND_NOTIFICATION_PANEL,
167
+ ControlMessageType.EXPAND_SETTINGS_PANEL,
168
+ ControlMessageType.COLLAPSE_PANELS,
169
+ ControlMessageType.ROTATE_DEVICE,
170
+ ControlMessageType.OPEN_HARD_KEYBOARD_SETTINGS,
171
+ ControlMessageType.RESET_VIDEO,
172
+ ControlMessageType.CAMERA_ZOOM_IN,
173
+ ControlMessageType.CAMERA_ZOOM_OUT,
174
+ }:
175
+ raise ValueError(f"{message_type.name} is not an empty control message")
176
+ return ControlMessage(message_type, bytes([message_type]))
177
+
178
+
179
+ def camera_set_torch(on: bool) -> ControlMessage:
180
+ return ControlMessage(ControlMessageType.CAMERA_SET_TORCH, bytes([ControlMessageType.CAMERA_SET_TORCH, int(on)]))
181
+
182
+
183
+ def resize_display(width: int, height: int) -> ControlMessage:
184
+ payload = bytes([ControlMessageType.RESIZE_DISPLAY]) + u16be(width) + u16be(height)
185
+ return ControlMessage(ControlMessageType.RESIZE_DISPLAY, payload)
186
+
187
+
188
+ def start_app(name: str) -> ControlMessage:
189
+ payload = bytes([ControlMessageType.START_APP]) + string8(name, 255)
190
+ return ControlMessage(ControlMessageType.START_APP, payload)
191
+
192
+
193
+ def uhid_create(uhid_id: int, vendor_id: int, product_id: int, name: str, report_desc: bytes) -> ControlMessage:
194
+ payload = (
195
+ bytes([ControlMessageType.UHID_CREATE])
196
+ + u16be(uhid_id)
197
+ + u16be(vendor_id)
198
+ + u16be(product_id)
199
+ + string8(name, 127)
200
+ + u16be(len(report_desc))
201
+ + report_desc
202
+ )
203
+ return ControlMessage(ControlMessageType.UHID_CREATE, payload)
204
+
205
+
206
+ def uhid_input(uhid_id: int, data: bytes) -> ControlMessage:
207
+ payload = bytes([ControlMessageType.UHID_INPUT]) + u16be(uhid_id) + u16be(len(data)) + data
208
+ return ControlMessage(ControlMessageType.UHID_INPUT, payload)
209
+
210
+
211
+ def uhid_destroy(uhid_id: int) -> ControlMessage:
212
+ return ControlMessage(ControlMessageType.UHID_DESTROY, bytes([ControlMessageType.UHID_DESTROY]) + u16be(uhid_id))
213
+
214
+
215
+ def deserialize_device_message(buffer: bytes) -> tuple[DeviceMessage | None, int]:
216
+ if not buffer:
217
+ return None, 0
218
+ try:
219
+ message_type = DeviceMessageType(buffer[0])
220
+ except ValueError as exc:
221
+ raise ProtocolError(f"unknown device message type: {buffer[0]}") from exc
222
+
223
+ if message_type is DeviceMessageType.CLIPBOARD:
224
+ if len(buffer) < 5:
225
+ return None, 0
226
+ size = read_u32be(buffer, 1)
227
+ total = 5 + size
228
+ if len(buffer) < total:
229
+ return None, 0
230
+ text = buffer[5:total].decode("utf-8")
231
+ return DeviceMessage(message_type, text=text), total
232
+
233
+ if message_type is DeviceMessageType.ACK_CLIPBOARD:
234
+ if len(buffer) < 9:
235
+ return None, 0
236
+ return DeviceMessage(message_type, sequence=read_u64be(buffer, 1)), 9
237
+
238
+ if len(buffer) < 5:
239
+ return None, 0
240
+ uhid_id = read_u16be(buffer, 1)
241
+ size = read_u16be(buffer, 3)
242
+ total = 5 + size
243
+ if len(buffer) < total:
244
+ return None, 0
245
+ return DeviceMessage(message_type, uhid_id=uhid_id, data=buffer[5:total]), total
246
+
@@ -1,4 +1,4 @@
1
- """Async stream readers and writers for scrcpy-server v4.0."""
1
+ """Async stream readers and writers for scrcpy-server v4.0."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -190,6 +190,7 @@ class ControlStream:
190
190
  self.reader = reader
191
191
  self.writer = writer
192
192
  self._device_buffer = bytearray()
193
+ self.pointer_manager: control.PointerManager | None = None
193
194
 
194
195
  async def send(self, message: control.ControlMessage) -> None:
195
196
  self.writer.write(message.payload)
@@ -213,6 +214,57 @@ class ControlStream:
213
214
  position = control.Position(x, y, screen_width, screen_height)
214
215
  await self.send(control.inject_touch_event(action, position, pointer_id, pressure, action_button, buttons))
215
216
 
217
+ async def send_touch_managed(
218
+ self,
219
+ action: int,
220
+ x: int,
221
+ y: int,
222
+ screen_width: int,
223
+ screen_height: int,
224
+ pointer_id: int | None = None,
225
+ pressure: float = 1.0,
226
+ action_button: int = 0,
227
+ buttons: int = 0,
228
+ ) -> int | None:
229
+ """Send a touch event with automatic pointer ID management.
230
+
231
+ On ``ACTION_DOWN``: if *pointer_id* is ``None``, a new ID is
232
+ allocated from ``self.pointer_manager``. The final pointer_id
233
+ is returned (or ``None`` if allocation failed).
234
+
235
+ On ``ACTION_UP``: the given *pointer_id* is released back to
236
+ the pool after sending.
237
+
238
+ When ``self.pointer_manager`` is ``None``, falls back to the
239
+ default ``POINTER_ID_GENERIC_FINGER``.
240
+ """
241
+ pm = self.pointer_manager
242
+
243
+ if action == control.ACTION_DOWN:
244
+ if pm is not None and pointer_id is None:
245
+ pid = pm.allocate()
246
+ if pid is None:
247
+ return None
248
+ elif pointer_id is not None:
249
+ pid = pointer_id
250
+ else:
251
+ pid = control.POINTER_ID_GENERIC_FINGER
252
+ await self.send_touch(action, x, y, screen_width, screen_height, pointer_id=pid, pressure=pressure, action_button=action_button, buttons=buttons)
253
+ return pid
254
+
255
+ elif action == control.ACTION_UP:
256
+ pid = pointer_id if pointer_id is not None else control.POINTER_ID_GENERIC_FINGER
257
+ await self.send_touch(action, x, y, screen_width, screen_height, pointer_id=pid, pressure=pressure, action_button=action_button, buttons=buttons)
258
+ if pm is not None and pointer_id is not None:
259
+ pm.release(pid)
260
+ return pid
261
+
262
+ # ACTION_MOVE: just forward, no lifecycle management
263
+ pid = pointer_id if pointer_id is not None else control.POINTER_ID_GENERIC_FINGER
264
+ await self.send_touch(action, x, y, screen_width, screen_height, pointer_id=pid, pressure=pressure, action_button=action_button, buttons=buttons)
265
+ return None
266
+
267
+
216
268
  async def set_clipboard(self, text: str, sequence: int = 1, paste: bool = False) -> None:
217
269
  await self.send(control.set_clipboard(sequence, text, paste))
218
270
 
@@ -277,3 +329,5 @@ class AudioVideoCombinedStream:
277
329
  finally:
278
330
  for task in tasks:
279
331
  task.cancel()
332
+
333
+
@@ -277,8 +277,9 @@ class HookListener:
277
277
  user32.TranslateMessage(byref(msg))
278
278
  user32.DispatchMessageW(byref(msg))
279
279
  except Exception:
280
- # 出现异常直接跳出循环,保证能清理钩子
281
- break
280
+ # 记录异常但不退出循环,确保钩子持续工作
281
+ # 回调中的异常应由上层自行处理
282
+ continue
282
283
 
283
284
  # 离开循环之前确保取消钩子
284
285
  if self.keyboard_hook:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: autoxkit
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Python library for Windows automation and Android device screen casting and control
5
5
  Author-email: YorickFin <1582456060@qq.com>
6
6
  License: GPL-3.0-or-later
@@ -14,6 +14,7 @@ autoxkit/android/adb.py
14
14
  autoxkit/android/binary.py
15
15
  autoxkit/android/client.py
16
16
  autoxkit/android/control.py
17
+ autoxkit/android/control_backup.py
17
18
  autoxkit/android/models.py
18
19
  autoxkit/android/streams.py
19
20
  autoxkit/hook/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "autoxkit"
7
- version = "2.0.0"
7
+ version = "2.1.0"
8
8
  description = "Python library for Windows automation and Android device screen casting and control"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes