@midscene/android 0.13.1 → 0.13.2-beta-20250401015137.0

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.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ ## Android prerequisites
2
+
3
+ Android is driven by adb, so you need install adb first:
4
+ - [CLI](https://developer.android.com/tools/adb)
5
+
6
+ ```bash
7
+ npm run test:ai -- setting.test.ts
8
+ ```
9
+
10
+ or
11
+
12
+ ```bash
13
+ npm run test:ai -- todo.test.ts
14
+ ```
package/bin/yadb ADDED
Binary file
@@ -1 +1,81 @@
1
- export { AndroidAgent, AndroidPage } from '@midscene/web/android';
1
+ import { AbstractPage } from '@midscene/web';
2
+ export { PageAgent as AndroidAgent } from '@midscene/web';
3
+ import { Size, Point } from '@midscene/core';
4
+ import { ElementInfo } from '@midscene/shared/extractor';
5
+ import { ADB } from 'appium-adb';
6
+
7
+ declare class Page implements AbstractPage {
8
+ private deviceId;
9
+ private screenSize;
10
+ private yadbPushed;
11
+ private deviceRatio;
12
+ private adbInitPromise;
13
+ pageType: string;
14
+ constructor({ deviceId }: {
15
+ deviceId: string;
16
+ });
17
+ private initAdb;
18
+ getAdb(): Promise<ADB>;
19
+ private execYadb;
20
+ getElementsInfo(): Promise<ElementInfo[]>;
21
+ getElementsNodeTree(): Promise<any>;
22
+ size(): Promise<Size>;
23
+ /**
24
+ * Convert logical coordinates to physical coordinates, handling device ratio
25
+ * @param x Logical X coordinate
26
+ * @param y Logical Y coordinate
27
+ * @returns Physical coordinate point
28
+ */
29
+ private adjustCoordinates;
30
+ /**
31
+ * Convert physical coordinates to logical coordinates, handling device ratio
32
+ * @param x Physical X coordinate
33
+ * @param y Physical Y coordinate
34
+ * @returns Logical coordinate point
35
+ */
36
+ private reverseAdjustCoordinates;
37
+ screenshotBase64(): Promise<string>;
38
+ get mouse(): {
39
+ click: (x: number, y: number) => Promise<void>;
40
+ wheel: (deltaX: number, deltaY: number) => Promise<void>;
41
+ move: (x: number, y: number) => Promise<void>;
42
+ drag: (from: {
43
+ x: number;
44
+ y: number;
45
+ }, to: {
46
+ x: number;
47
+ y: number;
48
+ }) => Promise<void>;
49
+ };
50
+ get keyboard(): {
51
+ type: (text: string) => Promise<void>;
52
+ press: (action: {
53
+ key: string;
54
+ command?: string;
55
+ } | {
56
+ key: string;
57
+ command?: string;
58
+ }[]) => Promise<void>;
59
+ };
60
+ clearInput(element: ElementInfo): Promise<void>;
61
+ url(): Promise<string>;
62
+ scrollUntilTop(startingPoint?: Point): Promise<void>;
63
+ scrollUntilBottom(startingPoint?: Point): Promise<void>;
64
+ scrollUntilLeft(startingPoint?: Point): Promise<void>;
65
+ scrollUntilRight(startingPoint?: Point): Promise<void>;
66
+ scrollUp(distance?: number, startingPoint?: Point): Promise<void>;
67
+ scrollDown(distance?: number, startingPoint?: Point): Promise<void>;
68
+ scrollLeft(distance?: number, startingPoint?: Point): Promise<void>;
69
+ scrollRight(distance?: number, startingPoint?: Point): Promise<void>;
70
+ private pushYadb;
71
+ private keyboardType;
72
+ private keyboardPress;
73
+ private keyboardPressAction;
74
+ private mouseClick;
75
+ private mouseMove;
76
+ private mouseDrag;
77
+ private mouseWheel;
78
+ destroy(): Promise<void>;
79
+ }
80
+
81
+ export { Page as AndroidPage };
package/dist/es/index.js CHANGED
@@ -1,6 +1,329 @@
1
1
  // src/index.ts
2
- import { AndroidAgent, AndroidPage } from "@midscene/web/android";
2
+ import { PageAgent } from "@midscene/web";
3
+
4
+ // src/page/index.ts
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { getTmpFile } from "@midscene/core/utils";
8
+ import { resizeImg } from "@midscene/shared/img";
9
+ import { getDebug } from "@midscene/shared/logger";
10
+ import { ADB } from "appium-adb";
11
+ var debugPage = getDebug("android");
12
+ var Page = class {
13
+ constructor({ deviceId }) {
14
+ this.screenSize = null;
15
+ this.yadbPushed = false;
16
+ this.deviceRatio = 1;
17
+ this.pageType = "android";
18
+ this.deviceId = deviceId;
19
+ this.adbInitPromise = this.initAdb();
20
+ }
21
+ async initAdb() {
22
+ debugPage(`Initializing ADB with device ID: ${this.deviceId}`);
23
+ const adb = await ADB.createADB({
24
+ udid: this.deviceId,
25
+ adbExecTimeout: 6e4
26
+ });
27
+ debugPage("ADB initialized successfully");
28
+ return adb;
29
+ }
30
+ async getAdb() {
31
+ return this.adbInitPromise;
32
+ }
33
+ async execYadb(keyboardContent) {
34
+ await this.pushYadb();
35
+ const adb = await this.getAdb();
36
+ await adb.shell(
37
+ `app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`
38
+ );
39
+ }
40
+ async getElementsInfo() {
41
+ return [];
42
+ }
43
+ async getElementsNodeTree() {
44
+ return {
45
+ node: null,
46
+ children: []
47
+ };
48
+ }
49
+ async size() {
50
+ if (this.screenSize) {
51
+ return this.screenSize;
52
+ }
53
+ const adb = await this.getAdb();
54
+ const screenSize = await adb.getScreenSize();
55
+ let width;
56
+ let height;
57
+ if (typeof screenSize === "string") {
58
+ const match = screenSize.match(/(\d+)x(\d+)/);
59
+ if (!match || match.length < 3) {
60
+ throw new Error(`Unable to parse screen size: ${screenSize}`);
61
+ }
62
+ width = Number.parseInt(match[1], 10);
63
+ height = Number.parseInt(match[2], 10);
64
+ } else if (typeof screenSize === "object" && screenSize !== null) {
65
+ const sizeObj = screenSize;
66
+ if ("width" in sizeObj && "height" in sizeObj) {
67
+ width = Number(sizeObj.width);
68
+ height = Number(sizeObj.height);
69
+ } else {
70
+ throw new Error(
71
+ `Invalid screen size object: ${JSON.stringify(screenSize)}`
72
+ );
73
+ }
74
+ } else {
75
+ throw new Error(`Invalid screen size format: ${screenSize}`);
76
+ }
77
+ const densityNum = await adb.getScreenDensity();
78
+ this.deviceRatio = Number(densityNum) / 160;
79
+ const { x: logicalWidth, y: logicalHeight } = this.reverseAdjustCoordinates(
80
+ width,
81
+ height
82
+ );
83
+ this.screenSize = {
84
+ width: logicalWidth,
85
+ height: logicalHeight
86
+ };
87
+ return this.screenSize;
88
+ }
89
+ /**
90
+ * Convert logical coordinates to physical coordinates, handling device ratio
91
+ * @param x Logical X coordinate
92
+ * @param y Logical Y coordinate
93
+ * @returns Physical coordinate point
94
+ */
95
+ adjustCoordinates(x, y) {
96
+ const ratio = this.deviceRatio;
97
+ return {
98
+ x: Math.round(x * ratio),
99
+ y: Math.round(y * ratio)
100
+ };
101
+ }
102
+ /**
103
+ * Convert physical coordinates to logical coordinates, handling device ratio
104
+ * @param x Physical X coordinate
105
+ * @param y Physical Y coordinate
106
+ * @returns Logical coordinate point
107
+ */
108
+ reverseAdjustCoordinates(x, y) {
109
+ const ratio = this.deviceRatio;
110
+ return {
111
+ x: Math.round(x / ratio),
112
+ y: Math.round(y / ratio)
113
+ };
114
+ }
115
+ async screenshotBase64() {
116
+ debugPage("screenshotBase64 begin");
117
+ const { width, height } = await this.size();
118
+ const adb = await this.getAdb();
119
+ let screenshotBuffer;
120
+ try {
121
+ screenshotBuffer = await adb.takeScreenshot(null);
122
+ } catch (error) {
123
+ const screenshotPath = getTmpFile("png");
124
+ await adb.shell("screencap -p /sdcard/screenshot.png");
125
+ await adb.pull("/sdcard/screenshot.png", screenshotPath);
126
+ screenshotBuffer = await fs.promises.readFile(screenshotPath);
127
+ }
128
+ const resizedScreenshotBuffer = await resizeImg(screenshotBuffer, {
129
+ width,
130
+ height
131
+ });
132
+ const result = `data:image/jpeg;base64,${resizedScreenshotBuffer.toString("base64")}`;
133
+ debugPage("screenshotBase64 end");
134
+ return result;
135
+ }
136
+ get mouse() {
137
+ return {
138
+ click: (x, y) => this.mouseClick(x, y),
139
+ wheel: (deltaX, deltaY) => this.mouseWheel(deltaX, deltaY),
140
+ move: (x, y) => this.mouseMove(x, y),
141
+ drag: (from, to) => this.mouseDrag(from, to)
142
+ };
143
+ }
144
+ get keyboard() {
145
+ return {
146
+ type: (text) => this.keyboardType(text),
147
+ press: (action) => this.keyboardPressAction(action)
148
+ };
149
+ }
150
+ async clearInput(element) {
151
+ if (!element) {
152
+ return;
153
+ }
154
+ await this.pushYadb();
155
+ const adb = await this.getAdb();
156
+ await adb.shell(
157
+ 'app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "~CLEAR~"'
158
+ );
159
+ }
160
+ async url() {
161
+ const adb = await this.getAdb();
162
+ const { appPackage, appActivity } = await adb.getFocusedPackageAndActivity();
163
+ return `${appPackage}/${appActivity}`;
164
+ }
165
+ async scrollUntilTop(startingPoint) {
166
+ if (startingPoint) {
167
+ await this.mouse.move(startingPoint.left, startingPoint.top);
168
+ }
169
+ await this.mouseWheel(0, 9999999, 100);
170
+ }
171
+ async scrollUntilBottom(startingPoint) {
172
+ if (startingPoint) {
173
+ await this.mouse.move(startingPoint.left, startingPoint.top);
174
+ }
175
+ await this.mouseWheel(0, -9999999, 100);
176
+ }
177
+ async scrollUntilLeft(startingPoint) {
178
+ if (startingPoint) {
179
+ await this.mouse.move(startingPoint.left, startingPoint.top);
180
+ }
181
+ await this.mouseWheel(9999999, 0, 100);
182
+ }
183
+ async scrollUntilRight(startingPoint) {
184
+ if (startingPoint) {
185
+ await this.mouse.move(startingPoint.left, startingPoint.top);
186
+ }
187
+ await this.mouseWheel(-9999999, 0, 100);
188
+ }
189
+ async scrollUp(distance, startingPoint) {
190
+ const { height } = await this.size();
191
+ const scrollDistance = distance || height * 0.7;
192
+ if (startingPoint) {
193
+ await this.mouse.move(startingPoint.left, startingPoint.top);
194
+ }
195
+ await this.mouseWheel(0, scrollDistance, 1e3);
196
+ }
197
+ async scrollDown(distance, startingPoint) {
198
+ const { height } = await this.size();
199
+ const scrollDistance = distance || height * 0.7;
200
+ if (startingPoint) {
201
+ await this.mouse.move(startingPoint.left, startingPoint.top);
202
+ }
203
+ await this.mouseWheel(0, -scrollDistance, 1e3);
204
+ }
205
+ async scrollLeft(distance, startingPoint) {
206
+ const { width } = await this.size();
207
+ const scrollDistance = distance || width * 0.7;
208
+ if (startingPoint) {
209
+ await this.mouse.move(startingPoint.left, startingPoint.top);
210
+ }
211
+ await this.mouseWheel(scrollDistance, 0, 1e3);
212
+ }
213
+ async scrollRight(distance, startingPoint) {
214
+ const { width } = await this.size();
215
+ const scrollDistance = distance || width * 0.7;
216
+ if (startingPoint) {
217
+ await this.mouse.move(startingPoint.left, startingPoint.top);
218
+ }
219
+ await this.mouseWheel(-scrollDistance, 0, 1e3);
220
+ }
221
+ async pushYadb() {
222
+ if (!this.yadbPushed) {
223
+ const adb = await this.getAdb();
224
+ const yadbBin = path.join(__dirname, "../../bin/yadb");
225
+ await adb.push(yadbBin, "/data/local/tmp");
226
+ this.yadbPushed = true;
227
+ }
228
+ }
229
+ async keyboardType(text) {
230
+ if (!text)
231
+ return;
232
+ const adb = await this.getAdb();
233
+ const isChinese = /[\p{Script=Han}\p{sc=Hani}]/u.test(text);
234
+ if (!isChinese) {
235
+ await adb.inputText(text);
236
+ return;
237
+ }
238
+ await this.execYadb(text);
239
+ }
240
+ async keyboardPress(key) {
241
+ const keyCodeMap = {
242
+ Enter: 66,
243
+ Backspace: 67,
244
+ Tab: 61,
245
+ ArrowUp: 19,
246
+ ArrowDown: 20,
247
+ ArrowLeft: 21,
248
+ ArrowRight: 22,
249
+ Escape: 111,
250
+ Home: 3,
251
+ End: 123
252
+ };
253
+ const adb = await this.getAdb();
254
+ const keyCode = keyCodeMap[key];
255
+ if (keyCode !== void 0) {
256
+ await adb.keyevent(keyCode);
257
+ } else {
258
+ if (key.length === 1) {
259
+ const asciiCode = key.toUpperCase().charCodeAt(0);
260
+ if (asciiCode >= 65 && asciiCode <= 90) {
261
+ await adb.keyevent(asciiCode - 36);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ async keyboardPressAction(action) {
267
+ if (Array.isArray(action)) {
268
+ for (const act of action) {
269
+ await this.keyboardPress(act.key);
270
+ }
271
+ } else {
272
+ await this.keyboardPress(action.key);
273
+ }
274
+ }
275
+ async mouseClick(x, y) {
276
+ await this.mouseMove(x, y);
277
+ const adb = await this.getAdb();
278
+ const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
279
+ await adb.shell(`input tap ${adjustedX} ${adjustedY}`);
280
+ }
281
+ async mouseMove(x, y) {
282
+ return Promise.resolve();
283
+ }
284
+ async mouseDrag(from, to) {
285
+ const adb = await this.getAdb();
286
+ const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
287
+ const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
288
+ await adb.shell(`input swipe ${fromX} ${fromY} ${toX} ${toY} 300`);
289
+ }
290
+ async mouseWheel(deltaX, deltaY, duration = 1e3) {
291
+ const { width, height } = await this.size();
292
+ const n = 4;
293
+ const startX = deltaX < 0 ? (n - 1) * (width / n) : width / n;
294
+ const startY = deltaY < 0 ? (n - 1) * (height / n) : height / n;
295
+ const maxNegativeDeltaX = startX;
296
+ const maxPositiveDeltaX = (n - 1) * (width / n);
297
+ const maxNegativeDeltaY = startY;
298
+ const maxPositiveDeltaY = (n - 1) * (height / n);
299
+ deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
300
+ deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
301
+ const endX = startX + deltaX;
302
+ const endY = startY + deltaY;
303
+ const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(
304
+ startX,
305
+ startY
306
+ );
307
+ const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(
308
+ endX,
309
+ endY
310
+ );
311
+ const adb = await this.getAdb();
312
+ await adb.shell(
313
+ `input swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${duration}`
314
+ );
315
+ }
316
+ async destroy() {
317
+ try {
318
+ const adb = await this.getAdb();
319
+ await adb.shell("rm -f /sdcard/screenshot.png");
320
+ await adb.shell("rm -f /sdcard/window_dump.xml");
321
+ } catch (error) {
322
+ console.error("Error during cleanup:", error);
323
+ }
324
+ }
325
+ };
3
326
  export {
4
- AndroidAgent,
5
- AndroidPage
327
+ PageAgent as AndroidAgent,
328
+ Page as AndroidPage
6
329
  };
@@ -1 +1,81 @@
1
- export { AndroidAgent, AndroidPage } from '@midscene/web/android';
1
+ import { AbstractPage } from '@midscene/web';
2
+ export { PageAgent as AndroidAgent } from '@midscene/web';
3
+ import { Size, Point } from '@midscene/core';
4
+ import { ElementInfo } from '@midscene/shared/extractor';
5
+ import { ADB } from 'appium-adb';
6
+
7
+ declare class Page implements AbstractPage {
8
+ private deviceId;
9
+ private screenSize;
10
+ private yadbPushed;
11
+ private deviceRatio;
12
+ private adbInitPromise;
13
+ pageType: string;
14
+ constructor({ deviceId }: {
15
+ deviceId: string;
16
+ });
17
+ private initAdb;
18
+ getAdb(): Promise<ADB>;
19
+ private execYadb;
20
+ getElementsInfo(): Promise<ElementInfo[]>;
21
+ getElementsNodeTree(): Promise<any>;
22
+ size(): Promise<Size>;
23
+ /**
24
+ * Convert logical coordinates to physical coordinates, handling device ratio
25
+ * @param x Logical X coordinate
26
+ * @param y Logical Y coordinate
27
+ * @returns Physical coordinate point
28
+ */
29
+ private adjustCoordinates;
30
+ /**
31
+ * Convert physical coordinates to logical coordinates, handling device ratio
32
+ * @param x Physical X coordinate
33
+ * @param y Physical Y coordinate
34
+ * @returns Logical coordinate point
35
+ */
36
+ private reverseAdjustCoordinates;
37
+ screenshotBase64(): Promise<string>;
38
+ get mouse(): {
39
+ click: (x: number, y: number) => Promise<void>;
40
+ wheel: (deltaX: number, deltaY: number) => Promise<void>;
41
+ move: (x: number, y: number) => Promise<void>;
42
+ drag: (from: {
43
+ x: number;
44
+ y: number;
45
+ }, to: {
46
+ x: number;
47
+ y: number;
48
+ }) => Promise<void>;
49
+ };
50
+ get keyboard(): {
51
+ type: (text: string) => Promise<void>;
52
+ press: (action: {
53
+ key: string;
54
+ command?: string;
55
+ } | {
56
+ key: string;
57
+ command?: string;
58
+ }[]) => Promise<void>;
59
+ };
60
+ clearInput(element: ElementInfo): Promise<void>;
61
+ url(): Promise<string>;
62
+ scrollUntilTop(startingPoint?: Point): Promise<void>;
63
+ scrollUntilBottom(startingPoint?: Point): Promise<void>;
64
+ scrollUntilLeft(startingPoint?: Point): Promise<void>;
65
+ scrollUntilRight(startingPoint?: Point): Promise<void>;
66
+ scrollUp(distance?: number, startingPoint?: Point): Promise<void>;
67
+ scrollDown(distance?: number, startingPoint?: Point): Promise<void>;
68
+ scrollLeft(distance?: number, startingPoint?: Point): Promise<void>;
69
+ scrollRight(distance?: number, startingPoint?: Point): Promise<void>;
70
+ private pushYadb;
71
+ private keyboardType;
72
+ private keyboardPress;
73
+ private keyboardPressAction;
74
+ private mouseClick;
75
+ private mouseMove;
76
+ private mouseDrag;
77
+ private mouseWheel;
78
+ destroy(): Promise<void>;
79
+ }
80
+
81
+ export { Page as AndroidPage };
package/dist/lib/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,16 +17,347 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var src_exports = {};
22
32
  __export(src_exports, {
23
- AndroidAgent: () => import_android.AndroidAgent,
24
- AndroidPage: () => import_android.AndroidPage
33
+ AndroidAgent: () => import_web.PageAgent,
34
+ AndroidPage: () => Page
25
35
  });
26
36
  module.exports = __toCommonJS(src_exports);
27
- var import_android = require("@midscene/web/android");
37
+ var import_web = require("@midscene/web");
38
+
39
+ // src/page/index.ts
40
+ var import_node_fs = __toESM(require("fs"));
41
+ var import_node_path = __toESM(require("path"));
42
+ var import_utils = require("@midscene/core/utils");
43
+ var import_img = require("@midscene/shared/img");
44
+ var import_logger = require("@midscene/shared/logger");
45
+ var import_appium_adb = require("appium-adb");
46
+ var debugPage = (0, import_logger.getDebug)("android");
47
+ var Page = class {
48
+ constructor({ deviceId }) {
49
+ this.screenSize = null;
50
+ this.yadbPushed = false;
51
+ this.deviceRatio = 1;
52
+ this.pageType = "android";
53
+ this.deviceId = deviceId;
54
+ this.adbInitPromise = this.initAdb();
55
+ }
56
+ async initAdb() {
57
+ debugPage(`Initializing ADB with device ID: ${this.deviceId}`);
58
+ const adb = await import_appium_adb.ADB.createADB({
59
+ udid: this.deviceId,
60
+ adbExecTimeout: 6e4
61
+ });
62
+ debugPage("ADB initialized successfully");
63
+ return adb;
64
+ }
65
+ async getAdb() {
66
+ return this.adbInitPromise;
67
+ }
68
+ async execYadb(keyboardContent) {
69
+ await this.pushYadb();
70
+ const adb = await this.getAdb();
71
+ await adb.shell(
72
+ `app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "${keyboardContent}"`
73
+ );
74
+ }
75
+ async getElementsInfo() {
76
+ return [];
77
+ }
78
+ async getElementsNodeTree() {
79
+ return {
80
+ node: null,
81
+ children: []
82
+ };
83
+ }
84
+ async size() {
85
+ if (this.screenSize) {
86
+ return this.screenSize;
87
+ }
88
+ const adb = await this.getAdb();
89
+ const screenSize = await adb.getScreenSize();
90
+ let width;
91
+ let height;
92
+ if (typeof screenSize === "string") {
93
+ const match = screenSize.match(/(\d+)x(\d+)/);
94
+ if (!match || match.length < 3) {
95
+ throw new Error(`Unable to parse screen size: ${screenSize}`);
96
+ }
97
+ width = Number.parseInt(match[1], 10);
98
+ height = Number.parseInt(match[2], 10);
99
+ } else if (typeof screenSize === "object" && screenSize !== null) {
100
+ const sizeObj = screenSize;
101
+ if ("width" in sizeObj && "height" in sizeObj) {
102
+ width = Number(sizeObj.width);
103
+ height = Number(sizeObj.height);
104
+ } else {
105
+ throw new Error(
106
+ `Invalid screen size object: ${JSON.stringify(screenSize)}`
107
+ );
108
+ }
109
+ } else {
110
+ throw new Error(`Invalid screen size format: ${screenSize}`);
111
+ }
112
+ const densityNum = await adb.getScreenDensity();
113
+ this.deviceRatio = Number(densityNum) / 160;
114
+ const { x: logicalWidth, y: logicalHeight } = this.reverseAdjustCoordinates(
115
+ width,
116
+ height
117
+ );
118
+ this.screenSize = {
119
+ width: logicalWidth,
120
+ height: logicalHeight
121
+ };
122
+ return this.screenSize;
123
+ }
124
+ /**
125
+ * Convert logical coordinates to physical coordinates, handling device ratio
126
+ * @param x Logical X coordinate
127
+ * @param y Logical Y coordinate
128
+ * @returns Physical coordinate point
129
+ */
130
+ adjustCoordinates(x, y) {
131
+ const ratio = this.deviceRatio;
132
+ return {
133
+ x: Math.round(x * ratio),
134
+ y: Math.round(y * ratio)
135
+ };
136
+ }
137
+ /**
138
+ * Convert physical coordinates to logical coordinates, handling device ratio
139
+ * @param x Physical X coordinate
140
+ * @param y Physical Y coordinate
141
+ * @returns Logical coordinate point
142
+ */
143
+ reverseAdjustCoordinates(x, y) {
144
+ const ratio = this.deviceRatio;
145
+ return {
146
+ x: Math.round(x / ratio),
147
+ y: Math.round(y / ratio)
148
+ };
149
+ }
150
+ async screenshotBase64() {
151
+ debugPage("screenshotBase64 begin");
152
+ const { width, height } = await this.size();
153
+ const adb = await this.getAdb();
154
+ let screenshotBuffer;
155
+ try {
156
+ screenshotBuffer = await adb.takeScreenshot(null);
157
+ } catch (error) {
158
+ const screenshotPath = (0, import_utils.getTmpFile)("png");
159
+ await adb.shell("screencap -p /sdcard/screenshot.png");
160
+ await adb.pull("/sdcard/screenshot.png", screenshotPath);
161
+ screenshotBuffer = await import_node_fs.default.promises.readFile(screenshotPath);
162
+ }
163
+ const resizedScreenshotBuffer = await (0, import_img.resizeImg)(screenshotBuffer, {
164
+ width,
165
+ height
166
+ });
167
+ const result = `data:image/jpeg;base64,${resizedScreenshotBuffer.toString("base64")}`;
168
+ debugPage("screenshotBase64 end");
169
+ return result;
170
+ }
171
+ get mouse() {
172
+ return {
173
+ click: (x, y) => this.mouseClick(x, y),
174
+ wheel: (deltaX, deltaY) => this.mouseWheel(deltaX, deltaY),
175
+ move: (x, y) => this.mouseMove(x, y),
176
+ drag: (from, to) => this.mouseDrag(from, to)
177
+ };
178
+ }
179
+ get keyboard() {
180
+ return {
181
+ type: (text) => this.keyboardType(text),
182
+ press: (action) => this.keyboardPressAction(action)
183
+ };
184
+ }
185
+ async clearInput(element) {
186
+ if (!element) {
187
+ return;
188
+ }
189
+ await this.pushYadb();
190
+ const adb = await this.getAdb();
191
+ await adb.shell(
192
+ 'app_process -Djava.class.path=/data/local/tmp/yadb /data/local/tmp com.ysbing.yadb.Main -keyboard "~CLEAR~"'
193
+ );
194
+ }
195
+ async url() {
196
+ const adb = await this.getAdb();
197
+ const { appPackage, appActivity } = await adb.getFocusedPackageAndActivity();
198
+ return `${appPackage}/${appActivity}`;
199
+ }
200
+ async scrollUntilTop(startingPoint) {
201
+ if (startingPoint) {
202
+ await this.mouse.move(startingPoint.left, startingPoint.top);
203
+ }
204
+ await this.mouseWheel(0, 9999999, 100);
205
+ }
206
+ async scrollUntilBottom(startingPoint) {
207
+ if (startingPoint) {
208
+ await this.mouse.move(startingPoint.left, startingPoint.top);
209
+ }
210
+ await this.mouseWheel(0, -9999999, 100);
211
+ }
212
+ async scrollUntilLeft(startingPoint) {
213
+ if (startingPoint) {
214
+ await this.mouse.move(startingPoint.left, startingPoint.top);
215
+ }
216
+ await this.mouseWheel(9999999, 0, 100);
217
+ }
218
+ async scrollUntilRight(startingPoint) {
219
+ if (startingPoint) {
220
+ await this.mouse.move(startingPoint.left, startingPoint.top);
221
+ }
222
+ await this.mouseWheel(-9999999, 0, 100);
223
+ }
224
+ async scrollUp(distance, startingPoint) {
225
+ const { height } = await this.size();
226
+ const scrollDistance = distance || height * 0.7;
227
+ if (startingPoint) {
228
+ await this.mouse.move(startingPoint.left, startingPoint.top);
229
+ }
230
+ await this.mouseWheel(0, scrollDistance, 1e3);
231
+ }
232
+ async scrollDown(distance, startingPoint) {
233
+ const { height } = await this.size();
234
+ const scrollDistance = distance || height * 0.7;
235
+ if (startingPoint) {
236
+ await this.mouse.move(startingPoint.left, startingPoint.top);
237
+ }
238
+ await this.mouseWheel(0, -scrollDistance, 1e3);
239
+ }
240
+ async scrollLeft(distance, startingPoint) {
241
+ const { width } = await this.size();
242
+ const scrollDistance = distance || width * 0.7;
243
+ if (startingPoint) {
244
+ await this.mouse.move(startingPoint.left, startingPoint.top);
245
+ }
246
+ await this.mouseWheel(scrollDistance, 0, 1e3);
247
+ }
248
+ async scrollRight(distance, startingPoint) {
249
+ const { width } = await this.size();
250
+ const scrollDistance = distance || width * 0.7;
251
+ if (startingPoint) {
252
+ await this.mouse.move(startingPoint.left, startingPoint.top);
253
+ }
254
+ await this.mouseWheel(-scrollDistance, 0, 1e3);
255
+ }
256
+ async pushYadb() {
257
+ if (!this.yadbPushed) {
258
+ const adb = await this.getAdb();
259
+ const yadbBin = import_node_path.default.join(__dirname, "../../bin/yadb");
260
+ await adb.push(yadbBin, "/data/local/tmp");
261
+ this.yadbPushed = true;
262
+ }
263
+ }
264
+ async keyboardType(text) {
265
+ if (!text)
266
+ return;
267
+ const adb = await this.getAdb();
268
+ const isChinese = /[\p{Script=Han}\p{sc=Hani}]/u.test(text);
269
+ if (!isChinese) {
270
+ await adb.inputText(text);
271
+ return;
272
+ }
273
+ await this.execYadb(text);
274
+ }
275
+ async keyboardPress(key) {
276
+ const keyCodeMap = {
277
+ Enter: 66,
278
+ Backspace: 67,
279
+ Tab: 61,
280
+ ArrowUp: 19,
281
+ ArrowDown: 20,
282
+ ArrowLeft: 21,
283
+ ArrowRight: 22,
284
+ Escape: 111,
285
+ Home: 3,
286
+ End: 123
287
+ };
288
+ const adb = await this.getAdb();
289
+ const keyCode = keyCodeMap[key];
290
+ if (keyCode !== void 0) {
291
+ await adb.keyevent(keyCode);
292
+ } else {
293
+ if (key.length === 1) {
294
+ const asciiCode = key.toUpperCase().charCodeAt(0);
295
+ if (asciiCode >= 65 && asciiCode <= 90) {
296
+ await adb.keyevent(asciiCode - 36);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ async keyboardPressAction(action) {
302
+ if (Array.isArray(action)) {
303
+ for (const act of action) {
304
+ await this.keyboardPress(act.key);
305
+ }
306
+ } else {
307
+ await this.keyboardPress(action.key);
308
+ }
309
+ }
310
+ async mouseClick(x, y) {
311
+ await this.mouseMove(x, y);
312
+ const adb = await this.getAdb();
313
+ const { x: adjustedX, y: adjustedY } = this.adjustCoordinates(x, y);
314
+ await adb.shell(`input tap ${adjustedX} ${adjustedY}`);
315
+ }
316
+ async mouseMove(x, y) {
317
+ return Promise.resolve();
318
+ }
319
+ async mouseDrag(from, to) {
320
+ const adb = await this.getAdb();
321
+ const { x: fromX, y: fromY } = this.adjustCoordinates(from.x, from.y);
322
+ const { x: toX, y: toY } = this.adjustCoordinates(to.x, to.y);
323
+ await adb.shell(`input swipe ${fromX} ${fromY} ${toX} ${toY} 300`);
324
+ }
325
+ async mouseWheel(deltaX, deltaY, duration = 1e3) {
326
+ const { width, height } = await this.size();
327
+ const n = 4;
328
+ const startX = deltaX < 0 ? (n - 1) * (width / n) : width / n;
329
+ const startY = deltaY < 0 ? (n - 1) * (height / n) : height / n;
330
+ const maxNegativeDeltaX = startX;
331
+ const maxPositiveDeltaX = (n - 1) * (width / n);
332
+ const maxNegativeDeltaY = startY;
333
+ const maxPositiveDeltaY = (n - 1) * (height / n);
334
+ deltaX = Math.max(-maxNegativeDeltaX, Math.min(deltaX, maxPositiveDeltaX));
335
+ deltaY = Math.max(-maxNegativeDeltaY, Math.min(deltaY, maxPositiveDeltaY));
336
+ const endX = startX + deltaX;
337
+ const endY = startY + deltaY;
338
+ const { x: adjustedStartX, y: adjustedStartY } = this.adjustCoordinates(
339
+ startX,
340
+ startY
341
+ );
342
+ const { x: adjustedEndX, y: adjustedEndY } = this.adjustCoordinates(
343
+ endX,
344
+ endY
345
+ );
346
+ const adb = await this.getAdb();
347
+ await adb.shell(
348
+ `input swipe ${adjustedStartX} ${adjustedStartY} ${adjustedEndX} ${adjustedEndY} ${duration}`
349
+ );
350
+ }
351
+ async destroy() {
352
+ try {
353
+ const adb = await this.getAdb();
354
+ await adb.shell("rm -f /sdcard/screenshot.png");
355
+ await adb.shell("rm -f /sdcard/window_dump.xml");
356
+ } catch (error) {
357
+ console.error("Error during cleanup:", error);
358
+ }
359
+ }
360
+ };
28
361
  // Annotate the CommonJS export names for ESM import in node:
29
362
  0 && (module.exports = {
30
363
  AndroidAgent,
@@ -1 +1,81 @@
1
- export { AndroidAgent, AndroidPage } from '@midscene/web/android';
1
+ import { AbstractPage } from '@midscene/web';
2
+ export { PageAgent as AndroidAgent } from '@midscene/web';
3
+ import { Size, Point } from '@midscene/core';
4
+ import { ElementInfo } from '@midscene/shared/extractor';
5
+ import { ADB } from 'appium-adb';
6
+
7
+ declare class Page implements AbstractPage {
8
+ private deviceId;
9
+ private screenSize;
10
+ private yadbPushed;
11
+ private deviceRatio;
12
+ private adbInitPromise;
13
+ pageType: string;
14
+ constructor({ deviceId }: {
15
+ deviceId: string;
16
+ });
17
+ private initAdb;
18
+ getAdb(): Promise<ADB>;
19
+ private execYadb;
20
+ getElementsInfo(): Promise<ElementInfo[]>;
21
+ getElementsNodeTree(): Promise<any>;
22
+ size(): Promise<Size>;
23
+ /**
24
+ * Convert logical coordinates to physical coordinates, handling device ratio
25
+ * @param x Logical X coordinate
26
+ * @param y Logical Y coordinate
27
+ * @returns Physical coordinate point
28
+ */
29
+ private adjustCoordinates;
30
+ /**
31
+ * Convert physical coordinates to logical coordinates, handling device ratio
32
+ * @param x Physical X coordinate
33
+ * @param y Physical Y coordinate
34
+ * @returns Logical coordinate point
35
+ */
36
+ private reverseAdjustCoordinates;
37
+ screenshotBase64(): Promise<string>;
38
+ get mouse(): {
39
+ click: (x: number, y: number) => Promise<void>;
40
+ wheel: (deltaX: number, deltaY: number) => Promise<void>;
41
+ move: (x: number, y: number) => Promise<void>;
42
+ drag: (from: {
43
+ x: number;
44
+ y: number;
45
+ }, to: {
46
+ x: number;
47
+ y: number;
48
+ }) => Promise<void>;
49
+ };
50
+ get keyboard(): {
51
+ type: (text: string) => Promise<void>;
52
+ press: (action: {
53
+ key: string;
54
+ command?: string;
55
+ } | {
56
+ key: string;
57
+ command?: string;
58
+ }[]) => Promise<void>;
59
+ };
60
+ clearInput(element: ElementInfo): Promise<void>;
61
+ url(): Promise<string>;
62
+ scrollUntilTop(startingPoint?: Point): Promise<void>;
63
+ scrollUntilBottom(startingPoint?: Point): Promise<void>;
64
+ scrollUntilLeft(startingPoint?: Point): Promise<void>;
65
+ scrollUntilRight(startingPoint?: Point): Promise<void>;
66
+ scrollUp(distance?: number, startingPoint?: Point): Promise<void>;
67
+ scrollDown(distance?: number, startingPoint?: Point): Promise<void>;
68
+ scrollLeft(distance?: number, startingPoint?: Point): Promise<void>;
69
+ scrollRight(distance?: number, startingPoint?: Point): Promise<void>;
70
+ private pushYadb;
71
+ private keyboardType;
72
+ private keyboardPress;
73
+ private keyboardPressAction;
74
+ private mouseClick;
75
+ private mouseMove;
76
+ private mouseDrag;
77
+ private mouseWheel;
78
+ destroy(): Promise<void>;
79
+ }
80
+
81
+ export { Page as AndroidPage };
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@midscene/android",
3
- "version": "0.13.1",
3
+ "version": "0.13.2-beta-20250401015137.0",
4
+ "description": "Android automation library for Midscene",
4
5
  "main": "./dist/lib/index.js",
5
6
  "types": "./dist/types/index.d.ts",
6
7
  "files": [
7
- "dist"
8
+ "dist",
9
+ "bin",
10
+ "README.md"
8
11
  ],
9
12
  "exports": {
10
13
  ".": {
@@ -13,17 +16,23 @@
13
16
  }
14
17
  },
15
18
  "dependencies": {
16
- "@midscene/web": "0.13.1"
19
+ "appium-adb": "12.12.1",
20
+ "@midscene/web": "0.13.2-beta-20250401015137.0",
21
+ "@midscene/core": "0.13.2-beta-20250401015137.0",
22
+ "@midscene/shared": "0.13.2-beta-20250401015137.0"
17
23
  },
18
24
  "devDependencies": {
19
25
  "@modern-js/module-tools": "2.60.6",
20
26
  "@types/node": "^18.0.0",
21
- "typescript": "^5.8.2"
27
+ "dotenv": "16.4.5",
28
+ "typescript": "^5.8.2",
29
+ "vitest": "3.0.5"
22
30
  },
23
- "author": "",
24
- "license": "ISC",
25
- "description": "",
31
+ "license": "MIT",
26
32
  "scripts": {
27
- "build": "modern build -c ./modern.config.ts"
33
+ "build": "modern build -c ./modern.config.ts",
34
+ "test": "vitest --run",
35
+ "test:u": "vitest --run -u",
36
+ "test:ai": "MIDSCENE_CACHE=true npm run test"
28
37
  }
29
38
  }