android2harmony 0.1.4 → 0.1.5
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 +1 -407
- package/agents/self-tester.md +376 -376
- package/dist/index.js +134 -116
- package/dist/index.js.map +3 -3
- package/package.json +36 -32
- package/skills/a2h-resource-convert/SKILL.md +902 -0
- package/skills/a2h-resource-convert/references/code-vector-icon-rules.md +335 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/conversion-rules.md +15 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/dependency-analysis-rules.md +24 -2
- package/skills/a2h-resource-convert/references/lottie-conversion-rules.md +219 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/resource-mapping-rules.md +27 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/xml-drawable-to-svg-rules.md +118 -1
- package/skills/a2h-resource-convert/scripts/a2h_resource_convert.js +2166 -0
- package/skills/a2h-resource-convert/scripts/code_vector_icons.js +607 -0
- package/skills/a2h-resource-convert/scripts/package.json +3 -0
- package/skills/a2h-resource-convert/scripts/svg_fidelity_check.js +632 -0
- package/skills/a2h-ui-transfer/SKILL.md +420 -0
- package/skills/a2h-ui-transfer/references/conversion-procedure.md +572 -0
- package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +1 -1
- package/skills/a2h-ui-transfer/references/mvvm-v2/_/350/243/205/351/245/260/345/231/250/351/200/237/346/237/245.md +133 -0
- package/skills/{hmos-batch-ui-align/scripts/android_parse_fast.ts → a2h-ui-transfer/scripts/android_parse_fast.js} +397 -283
- package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +1624 -0
- package/skills/a2h-ui-transfer/scripts/measure_pack.js +1005 -0
- package/skills/a2h-ui-transfer/scripts/package.json +3 -0
- package/skills/hmos-incremental-ui-align/README.md +251 -251
- package/skills/hmos-incremental-ui-align/SKILL.md +364 -364
- package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +2 -2
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.js +790 -0
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.js +285 -0
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -76
- package/skills/hmos-incremental-ui-align/scripts/page_capture.js +756 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.js +155 -0
- package/skills/hmos-batch-ui-align/SKILL.md +0 -141
- package/skills/hmos-batch-ui-align/references/conversion-procedure.md +0 -217
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +0 -999
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +0 -343
- package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +0 -977
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +0 -188
- package/skills/hmos-resources-convert/SKILL.md +0 -654
- package/skills/hmos-resources-convert/template/AppScope/app.json5 +0 -10
- package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +0 -8
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/build-profile.json5 +0 -42
- package/skills/hmos-resources-convert/template/code-linter.json5 +0 -32
- package/skills/hmos-resources-convert/template/entry/build-profile.json5 +0 -33
- package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +0 -23
- package/skills/hmos-resources-convert/template/entry/oh-package.json5 +0 -10
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +0 -48
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +0 -23
- package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +0 -55
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +0 -3
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +0 -5
- package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +0 -2
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +0 -35
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +0 -16
- package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +0 -33
- package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +0 -23
- package/skills/hmos-resources-convert/template/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/oh-package-lock.json5 +0 -28
- package/skills/hmos-resources-convert/template/oh-package.json5 +0 -10
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Once/350/243/205/351/245/260/345/231/250/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Param/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/220/221V2/350/277/201/347/247/273/344/270/216/346/267/267/347/224/250/346/214/207/345/257/274.md" +0 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
const SCROLL_DURATION_MS = 300;
|
|
7
|
+
const HDC_SWIPE_SPEED = 600;
|
|
8
|
+
const IDLE_POLL_INTERVAL = 0.2;
|
|
9
|
+
const IDLE_TIMEOUT = 5;
|
|
10
|
+
const HMOS_SKIP_TYPES = /* @__PURE__ */ new Set(["WindowScene", "__Common__", "EffectComponent", "metaballNode"]);
|
|
11
|
+
const HMOS_SKIP_BUNDLES = /* @__PURE__ */ new Set([
|
|
12
|
+
"com.huawei.systemui",
|
|
13
|
+
"com.ohos.systemui",
|
|
14
|
+
"com.huawei.android.launcher"
|
|
15
|
+
]);
|
|
16
|
+
const HORIZONTAL_SCROLL_CLASSES = /* @__PURE__ */ new Set([
|
|
17
|
+
"android.widget.HorizontalScrollView",
|
|
18
|
+
"HorizontalScrollView"
|
|
19
|
+
]);
|
|
20
|
+
function sleep(seconds) {
|
|
21
|
+
const end = Date.now() + seconds * 1e3;
|
|
22
|
+
const sab = new SharedArrayBuffer(4);
|
|
23
|
+
const ia = new Int32Array(sab);
|
|
24
|
+
while (Date.now() < end) {
|
|
25
|
+
Atomics.wait(ia, 0, 0, Math.min(50, end - Date.now()));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const XML_ENTITIES = {
|
|
29
|
+
amp: "&",
|
|
30
|
+
lt: "<",
|
|
31
|
+
gt: ">",
|
|
32
|
+
quot: '"',
|
|
33
|
+
apos: "'"
|
|
34
|
+
};
|
|
35
|
+
function decodeEntities(s) {
|
|
36
|
+
return s.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, ent) => {
|
|
37
|
+
if (ent[0] === "#") {
|
|
38
|
+
const code = ent[1] === "x" || ent[1] === "X" ? parseInt(ent.slice(2), 16) : parseInt(ent.slice(1), 10);
|
|
39
|
+
return Number.isNaN(code) ? m : String.fromCodePoint(code);
|
|
40
|
+
}
|
|
41
|
+
return ent in XML_ENTITIES ? XML_ENTITIES[ent] : m;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
class XmlParseError extends Error {
|
|
45
|
+
}
|
|
46
|
+
function parseXml(xml) {
|
|
47
|
+
let i = 0;
|
|
48
|
+
const n = xml.length;
|
|
49
|
+
const stack = [];
|
|
50
|
+
let root = null;
|
|
51
|
+
const readAttrs = (raw) => {
|
|
52
|
+
const attrs = {};
|
|
53
|
+
const re = /([^\s=/>]+)\s*=\s*"([^"]*)"|([^\s=/>]+)\s*=\s*'([^']*)'/g;
|
|
54
|
+
let m;
|
|
55
|
+
while ((m = re.exec(raw)) !== null) {
|
|
56
|
+
if (m[1] !== void 0) attrs[m[1]] = decodeEntities(m[2]);
|
|
57
|
+
else if (m[3] !== void 0) attrs[m[3]] = decodeEntities(m[4]);
|
|
58
|
+
}
|
|
59
|
+
return attrs;
|
|
60
|
+
};
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const lt = xml.indexOf("<", i);
|
|
63
|
+
if (lt < 0) break;
|
|
64
|
+
i = lt;
|
|
65
|
+
if (xml.startsWith("<?", i)) {
|
|
66
|
+
const end = xml.indexOf("?>", i);
|
|
67
|
+
if (end < 0) throw new XmlParseError("unterminated declaration");
|
|
68
|
+
i = end + 2;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (xml.startsWith("<!--", i)) {
|
|
72
|
+
const end = xml.indexOf("-->", i);
|
|
73
|
+
if (end < 0) throw new XmlParseError("unterminated comment");
|
|
74
|
+
i = end + 3;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (xml.startsWith("<!", i)) {
|
|
78
|
+
const end = xml.indexOf(">", i);
|
|
79
|
+
if (end < 0) throw new XmlParseError("unterminated markup declaration");
|
|
80
|
+
i = end + 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
let j = i + 1;
|
|
84
|
+
let quote = null;
|
|
85
|
+
for (; j < n; j++) {
|
|
86
|
+
const c = xml[j];
|
|
87
|
+
if (quote) {
|
|
88
|
+
if (c === quote) quote = null;
|
|
89
|
+
} else if (c === '"' || c === "'") {
|
|
90
|
+
quote = c;
|
|
91
|
+
} else if (c === ">") {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (j >= n) throw new XmlParseError("unterminated tag");
|
|
96
|
+
const inner = xml.slice(i + 1, j);
|
|
97
|
+
i = j + 1;
|
|
98
|
+
if (inner[0] === "/") {
|
|
99
|
+
const closed = stack.pop();
|
|
100
|
+
if (!closed) throw new XmlParseError("unexpected closing tag");
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const selfClosing = inner.endsWith("/");
|
|
104
|
+
const body = selfClosing ? inner.slice(0, -1) : inner;
|
|
105
|
+
const spaceIdx = body.search(/[\s/]/);
|
|
106
|
+
const tag = spaceIdx < 0 ? body : body.slice(0, spaceIdx);
|
|
107
|
+
const attrRaw = spaceIdx < 0 ? "" : body.slice(spaceIdx);
|
|
108
|
+
const node = { tag, attrs: readAttrs(attrRaw), children: [] };
|
|
109
|
+
if (stack.length > 0) {
|
|
110
|
+
stack[stack.length - 1].children.push(node);
|
|
111
|
+
} else if (root === null) {
|
|
112
|
+
root = node;
|
|
113
|
+
}
|
|
114
|
+
if (!selfClosing) stack.push(node);
|
|
115
|
+
}
|
|
116
|
+
if (root === null) throw new XmlParseError("no root element");
|
|
117
|
+
return root;
|
|
118
|
+
}
|
|
119
|
+
function* iterNodes(root, tag) {
|
|
120
|
+
if (root.tag === tag) yield root;
|
|
121
|
+
for (const child of root.children) {
|
|
122
|
+
yield* iterNodes(child, tag);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function* iterAll(root) {
|
|
126
|
+
yield root;
|
|
127
|
+
for (const child of root.children) {
|
|
128
|
+
yield* iterAll(child);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function attr(node, key, dflt = "") {
|
|
132
|
+
return key in node.attrs ? node.attrs[key] : dflt;
|
|
133
|
+
}
|
|
134
|
+
function runCapture(file, args, opts = {}) {
|
|
135
|
+
const timeoutMs = (opts.timeout ?? 15) * 1e3;
|
|
136
|
+
const res = spawnSync(file, args, {
|
|
137
|
+
timeout: timeoutMs,
|
|
138
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
139
|
+
encoding: opts.binary ? "buffer" : void 0,
|
|
140
|
+
windowsHide: true
|
|
141
|
+
});
|
|
142
|
+
if (opts.binary) {
|
|
143
|
+
const buf = res.stdout ?? Buffer.alloc(0);
|
|
144
|
+
return { stdout: "", stdoutBuf: buf, stderr: "", code: res.status };
|
|
145
|
+
}
|
|
146
|
+
const outBuf = res.stdout ?? Buffer.alloc(0);
|
|
147
|
+
const errBuf = res.stderr ?? Buffer.alloc(0);
|
|
148
|
+
return {
|
|
149
|
+
stdout: outBuf.toString("utf-8"),
|
|
150
|
+
stdoutBuf: outBuf,
|
|
151
|
+
stderr: errBuf.toString("utf-8"),
|
|
152
|
+
code: res.status
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function adb(args, serial = null, timeout = 15) {
|
|
156
|
+
const full = [];
|
|
157
|
+
if (serial) full.push("-s", serial);
|
|
158
|
+
full.push(...args);
|
|
159
|
+
const res = spawnSync("adb", full, {
|
|
160
|
+
timeout: timeout * 1e3,
|
|
161
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
162
|
+
windowsHide: true
|
|
163
|
+
});
|
|
164
|
+
if (res.error && res.error.code === "ETIMEDOUT") {
|
|
165
|
+
console.log(` [WARN] adb timeout: ${args.join(" ")}`);
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
const out = res.stdout ?? Buffer.alloc(0);
|
|
169
|
+
return out.toString("utf-8").trim();
|
|
170
|
+
}
|
|
171
|
+
function adbShell(args, serial = null, timeout = 15) {
|
|
172
|
+
return adb(["shell", ...args], serial, timeout);
|
|
173
|
+
}
|
|
174
|
+
function adbWaitIdle(serial = null) {
|
|
175
|
+
const deadline = Date.now() + IDLE_TIMEOUT * 1e3;
|
|
176
|
+
let prevFocus = null;
|
|
177
|
+
let interval = IDLE_POLL_INTERVAL;
|
|
178
|
+
while (Date.now() < deadline) {
|
|
179
|
+
sleep(interval);
|
|
180
|
+
interval = Math.min(interval * 1.5, 0.5);
|
|
181
|
+
const focusOut = adbShell(["dumpsys", "window"], serial);
|
|
182
|
+
let currentFocus = "";
|
|
183
|
+
for (const line of focusOut.split(/\r?\n/)) {
|
|
184
|
+
if (line.includes("mCurrentFocus")) {
|
|
185
|
+
currentFocus = line.trim();
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (currentFocus && currentFocus === prevFocus) return;
|
|
190
|
+
prevFocus = currentFocus;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function hdc(args, timeout = 15) {
|
|
194
|
+
const res = spawnSync("hdc", args, {
|
|
195
|
+
timeout: timeout * 1e3,
|
|
196
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
197
|
+
windowsHide: true
|
|
198
|
+
});
|
|
199
|
+
if (res.error && res.error.code === "ETIMEDOUT") {
|
|
200
|
+
console.log(` [WARN] hdc timeout: ${args.join(" ")}`);
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
const errBuf = res.stderr ?? Buffer.alloc(0);
|
|
204
|
+
const errStr = errBuf.toString("utf-8").trim();
|
|
205
|
+
if (res.status !== 0 && errStr) {
|
|
206
|
+
console.log(` [WARN] hdc stderr: ${errStr}`);
|
|
207
|
+
}
|
|
208
|
+
const out = res.stdout ?? Buffer.alloc(0);
|
|
209
|
+
return out.toString("utf-8").trim();
|
|
210
|
+
}
|
|
211
|
+
function hdcShell(args, timeout = 15) {
|
|
212
|
+
return hdc(["shell", ...args], timeout);
|
|
213
|
+
}
|
|
214
|
+
function hdcWaitIdle() {
|
|
215
|
+
sleep(1);
|
|
216
|
+
}
|
|
217
|
+
function hdcGetDensity() {
|
|
218
|
+
let dpi = null;
|
|
219
|
+
const out = hdcShell(["hidumper", "-s", "3601", "-a", "-a"]);
|
|
220
|
+
const m = out.match(/(?:densityDpi|dpi)\D*(\d{2,4})/i);
|
|
221
|
+
if (m) {
|
|
222
|
+
const val = parseInt(m[1], 10);
|
|
223
|
+
if (val >= 100 && val <= 1e3) dpi = val;
|
|
224
|
+
}
|
|
225
|
+
if (dpi === null) {
|
|
226
|
+
const param = hdcShell(["param", "get", "persist.sys.dpi"]).trim();
|
|
227
|
+
if (/^\d+$/.test(param)) {
|
|
228
|
+
const val = parseInt(param, 10);
|
|
229
|
+
if (val >= 100 && val <= 1e3) dpi = val;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (dpi === null) dpi = 480;
|
|
233
|
+
return { dpi, density_factor: Math.round(dpi / 160 * 1e4) / 1e4 };
|
|
234
|
+
}
|
|
235
|
+
function extractInts(s) {
|
|
236
|
+
const m = s.match(/\d+/g);
|
|
237
|
+
return m ? m.map((x) => parseInt(x, 10)) : [];
|
|
238
|
+
}
|
|
239
|
+
function findScrollableContainers(xmlContent) {
|
|
240
|
+
const containers = [];
|
|
241
|
+
if (!xmlContent) return containers;
|
|
242
|
+
let root;
|
|
243
|
+
try {
|
|
244
|
+
root = parseXml(xmlContent);
|
|
245
|
+
} catch {
|
|
246
|
+
return containers;
|
|
247
|
+
}
|
|
248
|
+
for (const node of iterNodes(root, "node")) {
|
|
249
|
+
if (attr(node, "scrollable") !== "true") continue;
|
|
250
|
+
const cls = attr(node, "class");
|
|
251
|
+
if (HORIZONTAL_SCROLL_CLASSES.has(cls)) continue;
|
|
252
|
+
const nums = extractInts(attr(node, "bounds"));
|
|
253
|
+
if (nums.length !== 4) continue;
|
|
254
|
+
const [x1, y1, x2, y2] = nums;
|
|
255
|
+
if (y2 - y1 < 200) continue;
|
|
256
|
+
containers.push({ bounds: [x1, y1, x2, y2], class: cls, "resource-id": attr(node, "resource-id") });
|
|
257
|
+
}
|
|
258
|
+
containers.sort(
|
|
259
|
+
(a, b) => (b.bounds[2] - b.bounds[0]) * (b.bounds[3] - b.bounds[1]) - (a.bounds[2] - a.bounds[0]) * (a.bounds[3] - a.bounds[1])
|
|
260
|
+
);
|
|
261
|
+
return containers;
|
|
262
|
+
}
|
|
263
|
+
function extractNodeKeys(xmlContent) {
|
|
264
|
+
const keys = /* @__PURE__ */ new Set();
|
|
265
|
+
if (!xmlContent) return keys;
|
|
266
|
+
let root;
|
|
267
|
+
try {
|
|
268
|
+
root = parseXml(xmlContent);
|
|
269
|
+
} catch {
|
|
270
|
+
return keys;
|
|
271
|
+
}
|
|
272
|
+
for (const node of iterNodes(root, "node")) {
|
|
273
|
+
keys.add(
|
|
274
|
+
[
|
|
275
|
+
attr(node, "class"),
|
|
276
|
+
attr(node, "resource-id"),
|
|
277
|
+
attr(node, "text"),
|
|
278
|
+
attr(node, "content-desc"),
|
|
279
|
+
attr(node, "bounds")
|
|
280
|
+
].join("\0")
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return keys;
|
|
284
|
+
}
|
|
285
|
+
function setsEqual(a, b) {
|
|
286
|
+
if (a.size !== b.size) return false;
|
|
287
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
function parseBoundsCenter(bounds) {
|
|
291
|
+
const nums = extractInts(bounds);
|
|
292
|
+
if (nums.length === 4) {
|
|
293
|
+
const [x1, y1, x2, y2] = nums;
|
|
294
|
+
return [Math.trunc((x1 + x2) / 2), Math.trunc((y1 + y2) / 2)];
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function getClickableElements(xmlContent) {
|
|
299
|
+
const elements = [];
|
|
300
|
+
if (!xmlContent) return elements;
|
|
301
|
+
let root;
|
|
302
|
+
try {
|
|
303
|
+
root = parseXml(xmlContent);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
console.log(` [WARN] XML parse error: ${e.message}`);
|
|
306
|
+
return elements;
|
|
307
|
+
}
|
|
308
|
+
const listviewChildSet = /* @__PURE__ */ new Set();
|
|
309
|
+
for (const node of iterNodes(root, "node")) {
|
|
310
|
+
const cls = attr(node, "class");
|
|
311
|
+
if (cls.includes("ListView") || cls.includes("GridView") || cls === "List") {
|
|
312
|
+
for (const child of node.children) listviewChildSet.add(child);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const extractText = (node) => {
|
|
316
|
+
const t = attr(node, "text");
|
|
317
|
+
if (t) return t;
|
|
318
|
+
for (const child of iterNodes(node, "node")) {
|
|
319
|
+
const ct = attr(child, "text");
|
|
320
|
+
if (ct) return ct;
|
|
321
|
+
}
|
|
322
|
+
return "";
|
|
323
|
+
};
|
|
324
|
+
for (const node of iterNodes(root, "node")) {
|
|
325
|
+
if (attr(node, "enabled") !== "true") continue;
|
|
326
|
+
const isClickable = attr(node, "clickable") === "true";
|
|
327
|
+
const isLongClickable = attr(node, "long-clickable") === "true";
|
|
328
|
+
const isListviewItem = listviewChildSet.has(node);
|
|
329
|
+
const isCheckable = attr(node, "checkable") === "true";
|
|
330
|
+
if (!(isClickable || isLongClickable || isListviewItem || isCheckable)) continue;
|
|
331
|
+
const bounds = attr(node, "bounds");
|
|
332
|
+
let text = attr(node, "text");
|
|
333
|
+
if (!text) text = extractText(node);
|
|
334
|
+
const baseInfo = {
|
|
335
|
+
class: attr(node, "class"),
|
|
336
|
+
text,
|
|
337
|
+
"resource-id": attr(node, "resource-id"),
|
|
338
|
+
"content-desc": attr(node, "content-desc"),
|
|
339
|
+
bounds,
|
|
340
|
+
center: parseBoundsCenter(bounds)
|
|
341
|
+
};
|
|
342
|
+
if (isClickable || isListviewItem || isCheckable) {
|
|
343
|
+
elements.push({ ...baseInfo, "long-clickable": false });
|
|
344
|
+
}
|
|
345
|
+
if (isLongClickable) {
|
|
346
|
+
elements.push({ ...baseInfo, "long-clickable": true });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return elements;
|
|
350
|
+
}
|
|
351
|
+
function hmosToXml(node) {
|
|
352
|
+
const attrs = node.attributes ?? {};
|
|
353
|
+
const t = String(attrs["type"] ?? "");
|
|
354
|
+
const bundle = String(attrs["bundleName"] ?? "");
|
|
355
|
+
if (HMOS_SKIP_TYPES.has(t)) return null;
|
|
356
|
+
if (HMOS_SKIP_BUNDLES.has(bundle)) return null;
|
|
357
|
+
const get = (k, d = "") => attrs[k] === void 0 || attrs[k] === null ? d : String(attrs[k]);
|
|
358
|
+
const xmlAttrs = {
|
|
359
|
+
class: t,
|
|
360
|
+
"resource-id": get("key"),
|
|
361
|
+
text: get("text"),
|
|
362
|
+
"content-desc": get("description"),
|
|
363
|
+
clickable: get("clickable", "false"),
|
|
364
|
+
"long-clickable": get("longClickable", "false"),
|
|
365
|
+
scrollable: get("scrollable", "false"),
|
|
366
|
+
enabled: get("enabled", "true"),
|
|
367
|
+
focused: get("focused", "false"),
|
|
368
|
+
bounds: get("bounds"),
|
|
369
|
+
package: bundle
|
|
370
|
+
};
|
|
371
|
+
const el = { tag: "node", attrs: xmlAttrs, children: [] };
|
|
372
|
+
for (const child of node.children ?? []) {
|
|
373
|
+
const childEl = hmosToXml(child);
|
|
374
|
+
if (childEl !== null) el.children.push(childEl);
|
|
375
|
+
}
|
|
376
|
+
return el;
|
|
377
|
+
}
|
|
378
|
+
function escapeXmlAttr(s) {
|
|
379
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
380
|
+
}
|
|
381
|
+
function serializeXml(node) {
|
|
382
|
+
const attrStr = Object.entries(node.attrs).map(([k, v]) => ` ${k}="${escapeXmlAttr(v)}"`).join("");
|
|
383
|
+
if (node.children.length === 0) {
|
|
384
|
+
return `<${node.tag}${attrStr} />`;
|
|
385
|
+
}
|
|
386
|
+
const inner = node.children.map((c) => serializeXml(c)).join("");
|
|
387
|
+
return `<${node.tag}${attrStr}>${inner}</${node.tag}>`;
|
|
388
|
+
}
|
|
389
|
+
function serializeXmlIndented(node, level = 0) {
|
|
390
|
+
const pad = " ".repeat(level);
|
|
391
|
+
const attrStr = Object.entries(node.attrs).map(([k, v]) => ` ${k}="${escapeXmlAttr(v)}"`).join("");
|
|
392
|
+
if (node.children.length === 0) {
|
|
393
|
+
return `${pad}<${node.tag}${attrStr} />`;
|
|
394
|
+
}
|
|
395
|
+
const inner = node.children.map((c) => serializeXmlIndented(c, level + 1)).join("\n");
|
|
396
|
+
return `${pad}<${node.tag}${attrStr}>
|
|
397
|
+
${inner}
|
|
398
|
+
${pad}</${node.tag}>`;
|
|
399
|
+
}
|
|
400
|
+
function adbDumpXmlQuick(serial = null) {
|
|
401
|
+
const remotePath = "/sdcard/ui_dump.xml";
|
|
402
|
+
adbShell(["rm -f /sdcard/ui_dump.xml && uiautomator dump /sdcard/ui_dump.xml"], serial, 20);
|
|
403
|
+
for (let k = 0; k < 6; k++) {
|
|
404
|
+
const check = adbShell(["ls", remotePath], serial);
|
|
405
|
+
if (check.includes(remotePath)) break;
|
|
406
|
+
sleep(0.2);
|
|
407
|
+
}
|
|
408
|
+
return adbShell(["cat", remotePath], serial, 15);
|
|
409
|
+
}
|
|
410
|
+
function adbDumpXml(outputDir, serial = null) {
|
|
411
|
+
const xmlContent = adbDumpXmlQuick(serial);
|
|
412
|
+
if (xmlContent) fs.writeFileSync(path.join(outputDir, "view.xml"), xmlContent, "utf-8");
|
|
413
|
+
return xmlContent;
|
|
414
|
+
}
|
|
415
|
+
function adbScreenshot(outputDir, filename = "screenshot.png", serial = null) {
|
|
416
|
+
const localPath = path.join(outputDir, filename);
|
|
417
|
+
const args = [];
|
|
418
|
+
if (serial) args.push("-s", serial);
|
|
419
|
+
args.push("exec-out", "screencap", "-p");
|
|
420
|
+
const { stdoutBuf, code } = runCapture("adb", args, { timeout: 15, binary: true });
|
|
421
|
+
if (code === null) {
|
|
422
|
+
console.log(" [WARN] screenshot timeout");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (stdoutBuf.length > 0) fs.writeFileSync(localPath, stdoutBuf);
|
|
426
|
+
}
|
|
427
|
+
function adbGetDensity(serial = null) {
|
|
428
|
+
let dpi = null;
|
|
429
|
+
const out = adbShell(["wm", "density"], serial);
|
|
430
|
+
const mOver = out.match(/Override density:\s*(\d+)/);
|
|
431
|
+
const mPhys = out.match(/Physical density:\s*(\d+)/);
|
|
432
|
+
if (mOver) {
|
|
433
|
+
dpi = parseInt(mOver[1], 10);
|
|
434
|
+
} else if (mPhys) {
|
|
435
|
+
dpi = parseInt(mPhys[1], 10);
|
|
436
|
+
}
|
|
437
|
+
if (dpi === null) {
|
|
438
|
+
const prop = adbShell(["getprop", "ro.sf.lcd_density"], serial).trim();
|
|
439
|
+
if (/^\d+$/.test(prop)) dpi = parseInt(prop, 10);
|
|
440
|
+
}
|
|
441
|
+
if (dpi === null) dpi = 480;
|
|
442
|
+
return { dpi, density_factor: Math.round(dpi / 160 * 1e4) / 1e4 };
|
|
443
|
+
}
|
|
444
|
+
function adbGetActivity(serial = null) {
|
|
445
|
+
const info = {};
|
|
446
|
+
const actOut = adbShell(["dumpsys", "activity", "activities"], serial);
|
|
447
|
+
const winOut = adbShell(["dumpsys", "window"], serial);
|
|
448
|
+
for (const line of actOut.split(/\r?\n/)) {
|
|
449
|
+
if (/ResumedActivity|topActivity/i.test(line)) {
|
|
450
|
+
info["resumed_activity"] = line.trim();
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const line of winOut.split(/\r?\n/)) {
|
|
455
|
+
if (line.includes("mCurrentFocus")) {
|
|
456
|
+
info["current_focus"] = line.trim();
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
if (line.includes("mFocusedApp") && !("focused_app" in info)) {
|
|
460
|
+
info["focused_app"] = line.trim();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return info;
|
|
464
|
+
}
|
|
465
|
+
function adbScrollAndCapture(outputDir, containerBounds, maxScrolls, initialXml, serial = null) {
|
|
466
|
+
const [x1, y1, x2, y2] = containerBounds;
|
|
467
|
+
const cx = Math.trunc((x1 + x2) / 2);
|
|
468
|
+
const height = y2 - y1;
|
|
469
|
+
const swipeFromY = y1 + Math.trunc(height * 0.7);
|
|
470
|
+
const swipeToY = y1 + Math.trunc(height * 0.3);
|
|
471
|
+
const xmlList = [];
|
|
472
|
+
let prevKeys = initialXml ? extractNodeKeys(initialXml) : null;
|
|
473
|
+
let scrollCount = 0;
|
|
474
|
+
for (let i = 0; i < maxScrolls; i++) {
|
|
475
|
+
adbShell(
|
|
476
|
+
["input", "swipe", String(cx), String(swipeFromY), String(cx), String(swipeToY), String(SCROLL_DURATION_MS)],
|
|
477
|
+
serial
|
|
478
|
+
);
|
|
479
|
+
adbWaitIdle(serial);
|
|
480
|
+
const newXml = adbDumpXmlQuick(serial);
|
|
481
|
+
const newKeys = extractNodeKeys(newXml);
|
|
482
|
+
if (prevKeys !== null && setsEqual(newKeys, prevKeys)) {
|
|
483
|
+
console.log(` Reached bottom after ${i + 1} scroll(s)`);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
prevKeys = newKeys;
|
|
487
|
+
scrollCount += 1;
|
|
488
|
+
xmlList.push(newXml);
|
|
489
|
+
fs.writeFileSync(path.join(outputDir, `view_scroll_${i + 1}.xml`), newXml, "utf-8");
|
|
490
|
+
adbScreenshot(outputDir, `screenshot_scroll_${i + 1}.png`, serial);
|
|
491
|
+
console.log(` Scroll ${i + 1}: XML + screenshot saved`);
|
|
492
|
+
}
|
|
493
|
+
if (scrollCount > 0) {
|
|
494
|
+
console.log(` Restoring initial position: scrolling back ${scrollCount} time(s)...`);
|
|
495
|
+
for (let i = 0; i < scrollCount; i++) {
|
|
496
|
+
adbShell(
|
|
497
|
+
["input", "swipe", String(cx), String(swipeToY), String(cx), String(swipeFromY), String(SCROLL_DURATION_MS)],
|
|
498
|
+
serial
|
|
499
|
+
);
|
|
500
|
+
adbWaitIdle(serial);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return xmlList;
|
|
504
|
+
}
|
|
505
|
+
function dedupeClickable(allXmlContents) {
|
|
506
|
+
const seen = /* @__PURE__ */ new Set();
|
|
507
|
+
const clickable = [];
|
|
508
|
+
allXmlContents.forEach((xc, idx) => {
|
|
509
|
+
for (const elem of getClickableElements(xc)) {
|
|
510
|
+
const key = [
|
|
511
|
+
elem["resource-id"],
|
|
512
|
+
elem.text,
|
|
513
|
+
elem["content-desc"],
|
|
514
|
+
elem.class,
|
|
515
|
+
elem.bounds,
|
|
516
|
+
String(elem["long-clickable"])
|
|
517
|
+
].join("\0");
|
|
518
|
+
if (!seen.has(key)) {
|
|
519
|
+
seen.add(key);
|
|
520
|
+
elem.scroll_index = idx;
|
|
521
|
+
clickable.push(elem);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return clickable;
|
|
526
|
+
}
|
|
527
|
+
function captureAndroid(outputDir, serial = null, maxScrolls = 10) {
|
|
528
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
529
|
+
console.log("[1/4] Dumping view XML...");
|
|
530
|
+
const xmlContent = adbDumpXml(outputDir, serial);
|
|
531
|
+
if (!xmlContent) {
|
|
532
|
+
console.log("[ERR] Failed to dump XML");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
console.log("[2/4] Taking screenshot...");
|
|
536
|
+
adbScreenshot(outputDir, "screenshot.png", serial);
|
|
537
|
+
console.log("[3/4] Checking scrollable containers...");
|
|
538
|
+
const containers = findScrollableContainers(xmlContent);
|
|
539
|
+
const allXmlContents = [xmlContent];
|
|
540
|
+
if (containers.length > 0) {
|
|
541
|
+
const container = containers[0];
|
|
542
|
+
console.log(` Found scrollable: ${container.class} ${container["resource-id"]}`);
|
|
543
|
+
const scrollXmls = adbScrollAndCapture(outputDir, container.bounds, maxScrolls, xmlContent, serial);
|
|
544
|
+
allXmlContents.push(...scrollXmls);
|
|
545
|
+
console.log(` Scroll capture done: ${scrollXmls.length} scroll(s)`);
|
|
546
|
+
} else {
|
|
547
|
+
console.log(" No scrollable container found");
|
|
548
|
+
}
|
|
549
|
+
console.log("[4/4] Building meta.json...");
|
|
550
|
+
const density = adbGetDensity(serial);
|
|
551
|
+
console.log(` Density: ${density.dpi}dpi (${density.density_factor}x)`);
|
|
552
|
+
const activityInfo = adbGetActivity(serial);
|
|
553
|
+
const clickable = dedupeClickable(allXmlContents);
|
|
554
|
+
const meta = {
|
|
555
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
556
|
+
device: "adb",
|
|
557
|
+
dpi: density.dpi,
|
|
558
|
+
density_factor: density.density_factor,
|
|
559
|
+
activity_info: activityInfo,
|
|
560
|
+
clickable_count: clickable.length,
|
|
561
|
+
clickable_elements: clickable
|
|
562
|
+
};
|
|
563
|
+
fs.writeFileSync(path.join(outputDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
564
|
+
console.log(`
|
|
565
|
+
Done! Files saved to ${outputDir}`);
|
|
566
|
+
}
|
|
567
|
+
function hdcDumpLayout(outputDir) {
|
|
568
|
+
const out = hdcShell(["uitest dumpLayout"]);
|
|
569
|
+
const match = out.match(/saved to:(.+\.json)/);
|
|
570
|
+
if (!match) throw new Error(`Cannot parse dumpLayout output: ${out}`);
|
|
571
|
+
const remoteJson = match[1].trim();
|
|
572
|
+
const localJson = path.join(outputDir, "_tmp_layout.json");
|
|
573
|
+
hdc(["file", "recv", remoteJson, localJson]);
|
|
574
|
+
const data = JSON.parse(fs.readFileSync(localJson, "utf-8"));
|
|
575
|
+
let pageName = "";
|
|
576
|
+
if (data.children && data.children.length > 0) {
|
|
577
|
+
const pp = String(data.children[0].attributes?.["pagePath"] ?? "");
|
|
578
|
+
pageName = pp ? pp.split("/").pop() || "unknown" : "unknown";
|
|
579
|
+
}
|
|
580
|
+
const rootEl = hmosToXml(data);
|
|
581
|
+
const xmlPath = path.join(outputDir, "view.xml");
|
|
582
|
+
if (rootEl) {
|
|
583
|
+
const doc = `<?xml version='1.0' encoding='utf-8'?>
|
|
584
|
+
${serializeXmlIndented(rootEl)}`;
|
|
585
|
+
fs.writeFileSync(xmlPath, doc, "utf-8");
|
|
586
|
+
}
|
|
587
|
+
const xmlStr = rootEl ? serializeXml(rootEl) : "";
|
|
588
|
+
fs.rmSync(localJson, { force: true });
|
|
589
|
+
return { xml: xmlStr, pageName };
|
|
590
|
+
}
|
|
591
|
+
function hdcDumpLayoutQuick(outputDir) {
|
|
592
|
+
const out = hdcShell(["uitest dumpLayout"]);
|
|
593
|
+
const match = out.match(/saved to:(.+\.json)/);
|
|
594
|
+
if (!match) return "";
|
|
595
|
+
const remoteJson = match[1].trim();
|
|
596
|
+
const localJson = path.join(outputDir, "_tmp_layout.json");
|
|
597
|
+
hdc(["file", "recv", remoteJson, localJson]);
|
|
598
|
+
try {
|
|
599
|
+
const data = JSON.parse(fs.readFileSync(localJson, "utf-8"));
|
|
600
|
+
const rootEl = hmosToXml(data);
|
|
601
|
+
return rootEl ? serializeXml(rootEl) : "";
|
|
602
|
+
} finally {
|
|
603
|
+
if (fs.existsSync(localJson)) fs.rmSync(localJson, { force: true });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function hdcScreenshot(outputDir, filename = "screenshot.jpeg") {
|
|
607
|
+
const remoteImg = "/data/local/tmp/screenshot.jpeg";
|
|
608
|
+
hdcShell([`snapshot_display -f ${remoteImg}`]);
|
|
609
|
+
hdc(["file", "recv", remoteImg, path.join(outputDir, filename)]);
|
|
610
|
+
}
|
|
611
|
+
function hdcScrollAndCapture(outputDir, containerBounds, maxScrolls, initialXml) {
|
|
612
|
+
const [x1, y1, x2, y2] = containerBounds;
|
|
613
|
+
const cx = Math.trunc((x1 + x2) / 2);
|
|
614
|
+
const height = y2 - y1;
|
|
615
|
+
const swipeFromY = y1 + Math.trunc(height * 0.7);
|
|
616
|
+
const swipeToY = y1 + Math.trunc(height * 0.3);
|
|
617
|
+
const xmlList = [];
|
|
618
|
+
let prevKeys = initialXml ? extractNodeKeys(initialXml) : null;
|
|
619
|
+
let scrollCount = 0;
|
|
620
|
+
for (let i = 0; i < maxScrolls; i++) {
|
|
621
|
+
hdcShell(["uitest", "uiInput", "swipe", String(cx), String(swipeFromY), String(cx), String(swipeToY), String(HDC_SWIPE_SPEED)]);
|
|
622
|
+
hdcWaitIdle();
|
|
623
|
+
const newXml = hdcDumpLayoutQuick(outputDir);
|
|
624
|
+
const newKeys = extractNodeKeys(newXml);
|
|
625
|
+
if (prevKeys !== null && setsEqual(newKeys, prevKeys)) {
|
|
626
|
+
console.log(` Reached bottom after ${i + 1} scroll(s)`);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
prevKeys = newKeys;
|
|
630
|
+
scrollCount += 1;
|
|
631
|
+
xmlList.push(newXml);
|
|
632
|
+
fs.writeFileSync(path.join(outputDir, `view_scroll_${i + 1}.xml`), newXml, "utf-8");
|
|
633
|
+
hdcScreenshot(outputDir, `screenshot_scroll_${i + 1}.jpeg`);
|
|
634
|
+
console.log(` Scroll ${i + 1}: XML + screenshot saved`);
|
|
635
|
+
}
|
|
636
|
+
if (scrollCount > 0) {
|
|
637
|
+
console.log(` Restoring initial position: scrolling back ${scrollCount} time(s)...`);
|
|
638
|
+
for (let i = 0; i < scrollCount; i++) {
|
|
639
|
+
hdcShell(["uitest", "uiInput", "swipe", String(cx), String(swipeToY), String(cx), String(swipeFromY), String(HDC_SWIPE_SPEED)]);
|
|
640
|
+
hdcWaitIdle();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return xmlList;
|
|
644
|
+
}
|
|
645
|
+
function captureHdc(outputDir, maxScrolls = 10) {
|
|
646
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
647
|
+
console.log("[1/3] Dumping layout (JSON -> XML)...");
|
|
648
|
+
const { xml: xmlContent, pageName } = hdcDumpLayout(outputDir);
|
|
649
|
+
if (!xmlContent) {
|
|
650
|
+
console.log("[ERR] Failed to dump layout");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
console.log(` Page: ${pageName}`);
|
|
654
|
+
console.log("[2/3] Taking screenshot...");
|
|
655
|
+
hdcScreenshot(outputDir);
|
|
656
|
+
console.log("[3/3] Checking scrollable containers...");
|
|
657
|
+
const containers = findScrollableContainers(xmlContent);
|
|
658
|
+
if (containers.length > 0) {
|
|
659
|
+
const container = containers[0];
|
|
660
|
+
console.log(` Found scrollable: ${container.class} ${container["resource-id"]}`);
|
|
661
|
+
const scrollXmls = hdcScrollAndCapture(outputDir, container.bounds, maxScrolls, xmlContent);
|
|
662
|
+
console.log(` Scroll capture done: ${scrollXmls.length} scroll(s)`);
|
|
663
|
+
} else {
|
|
664
|
+
console.log(" No scrollable container found");
|
|
665
|
+
}
|
|
666
|
+
const density = hdcGetDensity();
|
|
667
|
+
console.log(` Density: ${density.dpi}dpi (${density.density_factor}x)`);
|
|
668
|
+
const meta = {
|
|
669
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
670
|
+
device: "hdc",
|
|
671
|
+
dpi: density.dpi,
|
|
672
|
+
density_factor: density.density_factor,
|
|
673
|
+
page_name: pageName
|
|
674
|
+
};
|
|
675
|
+
fs.writeFileSync(path.join(outputDir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
676
|
+
console.log(`
|
|
677
|
+
Done! Files saved to ${outputDir}`);
|
|
678
|
+
}
|
|
679
|
+
function parseArgs(argv) {
|
|
680
|
+
const args = { device: "hdc", outputDir: "", maxScrolls: 10, serial: null };
|
|
681
|
+
for (let i = 0; i < argv.length; i++) {
|
|
682
|
+
const a = argv[i];
|
|
683
|
+
const next = () => {
|
|
684
|
+
if (i + 1 >= argv.length) {
|
|
685
|
+
console.error(`Missing value for ${a}`);
|
|
686
|
+
process.exit(2);
|
|
687
|
+
}
|
|
688
|
+
return argv[++i];
|
|
689
|
+
};
|
|
690
|
+
if (a === "--device") {
|
|
691
|
+
const v = next();
|
|
692
|
+
if (v !== "hdc" && v !== "adb") {
|
|
693
|
+
console.error(`--device must be hdc or adb`);
|
|
694
|
+
process.exit(2);
|
|
695
|
+
}
|
|
696
|
+
args.device = v;
|
|
697
|
+
} else if (a === "--output-dir" || a === "-o") {
|
|
698
|
+
args.outputDir = next();
|
|
699
|
+
} else if (a === "--max-scrolls") {
|
|
700
|
+
args.maxScrolls = parseInt(next(), 10);
|
|
701
|
+
} else if (a === "--serial") {
|
|
702
|
+
args.serial = next();
|
|
703
|
+
} else if (a === "-h" || a === "--help") {
|
|
704
|
+
printHelp();
|
|
705
|
+
process.exit(0);
|
|
706
|
+
} else {
|
|
707
|
+
console.error(`Unknown argument: ${a}`);
|
|
708
|
+
process.exit(2);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (!args.outputDir) {
|
|
712
|
+
console.error("error: --output-dir/-o is required");
|
|
713
|
+
process.exit(2);
|
|
714
|
+
}
|
|
715
|
+
return args;
|
|
716
|
+
}
|
|
717
|
+
function printHelp() {
|
|
718
|
+
console.log(
|
|
719
|
+
[
|
|
720
|
+
"Capture current page: view tree XML + screenshots (Android / HarmonyOS)",
|
|
721
|
+
"",
|
|
722
|
+
"Usage: node page_capture.ts --device <hdc|adb> -o <dir> [--max-scrolls N] [--serial S]",
|
|
723
|
+
"",
|
|
724
|
+
" --device Device type: hdc=HarmonyOS, adb=Android (default: hdc)",
|
|
725
|
+
" --output-dir,-o Output directory for captured files (required)",
|
|
726
|
+
" --max-scrolls Max scroll count per container (default: 10)",
|
|
727
|
+
" --serial ADB device serial (optional, for multi-device)"
|
|
728
|
+
].join("\n")
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
function main() {
|
|
732
|
+
const args = parseArgs(process.argv.slice(2));
|
|
733
|
+
if (args.device === "adb") {
|
|
734
|
+
captureAndroid(args.outputDir, args.serial, args.maxScrolls);
|
|
735
|
+
} else {
|
|
736
|
+
captureHdc(args.outputDir, args.maxScrolls);
|
|
737
|
+
}
|
|
738
|
+
return 0;
|
|
739
|
+
}
|
|
740
|
+
const isEntry = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
741
|
+
if (isEntry) {
|
|
742
|
+
process.exit(main());
|
|
743
|
+
}
|
|
744
|
+
export {
|
|
745
|
+
dedupeClickable,
|
|
746
|
+
extractNodeKeys,
|
|
747
|
+
findScrollableContainers,
|
|
748
|
+
getClickableElements,
|
|
749
|
+
hmosToXml,
|
|
750
|
+
iterAll,
|
|
751
|
+
iterNodes,
|
|
752
|
+
parseBoundsCenter,
|
|
753
|
+
parseXml,
|
|
754
|
+
serializeXml,
|
|
755
|
+
serializeXmlIndented
|
|
756
|
+
};
|