@skrillex1224/android-toolkit 1.0.2 → 1.0.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 +7 -8
- package/browser.d.ts +12 -0
- package/dist/browser.js +21 -4
- package/dist/browser.js.map +3 -3
- package/dist/index.cjs +361 -359
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +360 -358
- package/dist/index.js.map +4 -4
- package/index.d.ts +11 -7
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from "node:fs";
|
|
|
11
11
|
var constants_exports = {};
|
|
12
12
|
__export(constants_exports, {
|
|
13
13
|
ActorInfo: () => ActorInfo,
|
|
14
|
+
AppiumSettings: () => AppiumSettings,
|
|
14
15
|
Code: () => Code,
|
|
15
16
|
Status: () => Status,
|
|
16
17
|
UnicodeIme: () => UnicodeIme
|
|
@@ -27,7 +28,7 @@ var Code = Object.freeze({
|
|
|
27
28
|
ContentUnavailable: 30010003,
|
|
28
29
|
SourceExtractionFailed: 30010004,
|
|
29
30
|
AutomationFailed: 30010005,
|
|
30
|
-
|
|
31
|
+
DataAccessUnavailable: 30010008,
|
|
31
32
|
AppNotInstalled: 30010009
|
|
32
33
|
});
|
|
33
34
|
var Status = Object.freeze({
|
|
@@ -37,7 +38,12 @@ var Status = Object.freeze({
|
|
|
37
38
|
var UnicodeIme = Object.freeze({
|
|
38
39
|
packageName: "io.appium.settings",
|
|
39
40
|
component: "io.appium.settings/.UnicodeIME",
|
|
40
|
-
|
|
41
|
+
inputAction: "ADB_INPUT_TEXT"
|
|
42
|
+
});
|
|
43
|
+
var AppiumSettings = Object.freeze({
|
|
44
|
+
packageName: "io.appium.settings",
|
|
45
|
+
clipboardReceiver: "io.appium.settings/.receivers.ClipboardReceiver",
|
|
46
|
+
clipboardGetAction: "io.appium.settings.clipboard.get"
|
|
41
47
|
});
|
|
42
48
|
var normalizeShare = (share) => {
|
|
43
49
|
const source = share && typeof share === "object" ? share : {};
|
|
@@ -82,7 +88,6 @@ function createAndroidContext(input = {}, defaults = {}) {
|
|
|
82
88
|
serial: firstNonEmpty(defaults.serial, device.serial),
|
|
83
89
|
packageName: firstNonEmpty(defaults.packageName, device.packageName),
|
|
84
90
|
adbPath: firstNonEmpty(defaults.adbPath, process.env.ANDROID_TOOLKIT_ADB_PATH),
|
|
85
|
-
fridaPath: firstNonEmpty(defaults.fridaPath, process.env.ANDROID_TOOLKIT_FRIDA_PATH),
|
|
86
91
|
appVersion: firstNonEmpty(defaults.appVersion, device.appVersion),
|
|
87
92
|
slotKey: firstNonEmpty(defaults.slotKey, device.slotKey),
|
|
88
93
|
instanceId: firstNonEmpty(defaults.instanceId, device.instanceId),
|
|
@@ -268,6 +273,8 @@ var Device = {
|
|
|
268
273
|
screenshotPng,
|
|
269
274
|
screenshotBase64,
|
|
270
275
|
dumpUiXml,
|
|
276
|
+
readClipboard,
|
|
277
|
+
clearClipboard,
|
|
271
278
|
wakeAndUnlock,
|
|
272
279
|
hideKeyboard,
|
|
273
280
|
isKeyboardVisible,
|
|
@@ -399,10 +406,12 @@ async function pressEnter(ctx) {
|
|
|
399
406
|
}
|
|
400
407
|
async function typeText(ctx, text2) {
|
|
401
408
|
await ensureUnicodeIme(ctx);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
409
|
+
const value = String(text2 ?? "");
|
|
410
|
+
if (!value) return;
|
|
411
|
+
await adbShell(ctx, ["input", "text", shellSingleQuote(encodeUnicodeImeText(value))], {
|
|
412
|
+
timeoutMs: Math.max(15e3, value.length * 300)
|
|
405
413
|
});
|
|
414
|
+
await sleep(1200);
|
|
406
415
|
}
|
|
407
416
|
async function ensureUnicodeIme(ctx) {
|
|
408
417
|
const out = await adbShell(ctx, ["ime", "list", "-s"], { timeoutMs: 15e3 });
|
|
@@ -428,14 +437,71 @@ async function screenshotPng(ctx) {
|
|
|
428
437
|
async function screenshotBase64(ctx) {
|
|
429
438
|
return `data:image/png;base64,${(await screenshotPng(ctx)).toString("base64")}`;
|
|
430
439
|
}
|
|
440
|
+
async function readClipboard(ctx) {
|
|
441
|
+
const out = await adbShell(ctx, [
|
|
442
|
+
"am",
|
|
443
|
+
"broadcast",
|
|
444
|
+
"-n",
|
|
445
|
+
AppiumSettings.clipboardReceiver,
|
|
446
|
+
"-a",
|
|
447
|
+
AppiumSettings.clipboardGetAction
|
|
448
|
+
], {
|
|
449
|
+
timeoutMs: 8e3,
|
|
450
|
+
maxBuffer: 1024 * 1024
|
|
451
|
+
}).catch((error) => {
|
|
452
|
+
throw new CrawlerError({
|
|
453
|
+
message: `source_extraction_failed: Appium Settings clipboard receiver unavailable ${error?.message || String(error)}`,
|
|
454
|
+
code: Code.SourceExtractionFailed,
|
|
455
|
+
context: { receiver: AppiumSettings.clipboardReceiver }
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
const text2 = normalizeAppiumClipboardBroadcast(out);
|
|
459
|
+
if (!text2) {
|
|
460
|
+
throw new CrawlerError({
|
|
461
|
+
message: "source_extraction_failed: ADB clipboard is empty",
|
|
462
|
+
code: Code.SourceExtractionFailed,
|
|
463
|
+
context: { receiver: AppiumSettings.clipboardReceiver, output: String(out || "").slice(0, 1e3) }
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
return text2;
|
|
467
|
+
}
|
|
468
|
+
async function clearClipboard(ctx) {
|
|
469
|
+
Logger.info("Device.clearClipboard skipped", {
|
|
470
|
+
reason: "Android shell clipboard set is not portable; captureLink validates by link change."
|
|
471
|
+
});
|
|
472
|
+
}
|
|
431
473
|
async function dumpUiXml(ctx) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
474
|
+
let lastError = null;
|
|
475
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
476
|
+
const dumpPath = `/data/local/tmp/android-toolkit-window-${Date.now()}-${Math.random().toString(16).slice(2)}.xml`;
|
|
477
|
+
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
478
|
+
});
|
|
479
|
+
try {
|
|
480
|
+
await adbShell(ctx, ["uiautomator", "dump", dumpPath], {
|
|
481
|
+
timeoutMs: 3e4,
|
|
482
|
+
maxBuffer: 4 * 1024 * 1024
|
|
483
|
+
});
|
|
484
|
+
const raw = await adbShell(ctx, ["cat", dumpPath], {
|
|
485
|
+
timeoutMs: 3e4,
|
|
486
|
+
maxBuffer: 16 * 1024 * 1024
|
|
487
|
+
});
|
|
488
|
+
const start = String(raw || "").indexOf("<?xml");
|
|
489
|
+
if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
|
|
490
|
+
return String(raw || "").slice(start).trim();
|
|
491
|
+
} catch (error) {
|
|
492
|
+
lastError = error;
|
|
493
|
+
Logger.warn("uiautomator dump retry", { attempt, message: error?.message || String(error) });
|
|
494
|
+
await sleep(350 * attempt);
|
|
495
|
+
} finally {
|
|
496
|
+
await adbShell(ctx, ["rm", "-f", dumpPath], { timeoutMs: 1e4 }).catch(() => {
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
throw CrawlerError.isCrawlerError(lastError) ? lastError : new CrawlerError({
|
|
501
|
+
message: `automation_failed: uiautomator dump failed ${lastError?.message || String(lastError || "")}`.trim(),
|
|
502
|
+
code: Code.AutomationFailed,
|
|
503
|
+
context: { lastError: lastError?.message || String(lastError || "") }
|
|
435
504
|
});
|
|
436
|
-
const start = String(raw || "").indexOf("<?xml");
|
|
437
|
-
if (start < 0) throw new CrawlerError({ message: `automation_failed: uiautomator dump missing xml: ${raw}`, code: Code.AutomationFailed });
|
|
438
|
-
return String(raw || "").slice(start).trim();
|
|
439
505
|
}
|
|
440
506
|
async function wakeAndUnlock(ctx) {
|
|
441
507
|
await pressKey(ctx, "KEYCODE_WAKEUP").catch(() => {
|
|
@@ -473,11 +539,51 @@ function requireValue(value, name) {
|
|
|
473
539
|
}
|
|
474
540
|
function isAdbUnavailableError(error) {
|
|
475
541
|
const message = String(error?.message || error || "");
|
|
476
|
-
return /ENOENT|spawn .*adb|
|
|
542
|
+
return /ENOENT|spawn .*adb|device .* not found|no devices|offline/i.test(message);
|
|
477
543
|
}
|
|
478
544
|
function compactArgs(args) {
|
|
479
545
|
return args.map(String).join(" ").slice(0, 500);
|
|
480
546
|
}
|
|
547
|
+
function encodeUnicodeImeText(text2) {
|
|
548
|
+
let out = "";
|
|
549
|
+
let buffer = "";
|
|
550
|
+
const flush = () => {
|
|
551
|
+
if (!buffer) return;
|
|
552
|
+
const bytes = Buffer.alloc(buffer.length * 2);
|
|
553
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
554
|
+
bytes.writeUInt16BE(buffer.charCodeAt(index), index * 2);
|
|
555
|
+
}
|
|
556
|
+
out += `&${bytes.toString("base64").replace(/\//g, ",").replace(/=+$/g, "")}-`;
|
|
557
|
+
buffer = "";
|
|
558
|
+
};
|
|
559
|
+
for (const char of String(text2 || "")) {
|
|
560
|
+
const code = char.charCodeAt(0);
|
|
561
|
+
if (code >= 32 && code <= 126) {
|
|
562
|
+
flush();
|
|
563
|
+
out += char === "&" ? "&-" : char;
|
|
564
|
+
} else {
|
|
565
|
+
buffer += char;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
flush();
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
function shellSingleQuote(value) {
|
|
572
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
573
|
+
}
|
|
574
|
+
function normalizeAppiumClipboardBroadcast(value) {
|
|
575
|
+
const out = String(value || "");
|
|
576
|
+
if (!/Broadcast completed:\s*result=-1\b/.test(out)) return "";
|
|
577
|
+
const match = /data="([^"]*)"/.exec(out) || /data=([^\s]+)/.exec(out);
|
|
578
|
+
const base64 = match ? String(match[1] || "").trim() : "";
|
|
579
|
+
if (!base64) return "";
|
|
580
|
+
try {
|
|
581
|
+
return Buffer.from(base64, "base64").toString("utf8").trim();
|
|
582
|
+
} catch {
|
|
583
|
+
return "";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
var $DeviceInternalsForTest = Object.freeze({});
|
|
481
587
|
|
|
482
588
|
// src/apify-kit.js
|
|
483
589
|
var instance = null;
|
|
@@ -670,16 +776,19 @@ async function waitFor(ctx, selector, options = {}) {
|
|
|
670
776
|
const intervalMs = Number(options.intervalMs || options.pollIntervalMs || 500);
|
|
671
777
|
const deadline = Date.now() + timeoutMs;
|
|
672
778
|
let lastError = null;
|
|
779
|
+
let sawCleanPoll = false;
|
|
673
780
|
while (Date.now() < deadline) {
|
|
674
781
|
try {
|
|
675
782
|
const node = await find(ctx, selector, { ...options, optional: true });
|
|
783
|
+
sawCleanPoll = true;
|
|
784
|
+
lastError = null;
|
|
676
785
|
if (node) return node;
|
|
677
786
|
} catch (error) {
|
|
678
787
|
lastError = error;
|
|
679
788
|
}
|
|
680
789
|
await sleep(intervalMs);
|
|
681
790
|
}
|
|
682
|
-
if (lastError) throw lastError;
|
|
791
|
+
if (lastError && !sawCleanPoll) throw lastError;
|
|
683
792
|
throw new CrawlerError({
|
|
684
793
|
message: `automation_failed: \u7B49\u5F85 View \u8D85\u65F6 ${selectorLabel(selector)}`,
|
|
685
794
|
code: Code.AutomationFailed,
|
|
@@ -866,9 +975,27 @@ async function click(ctx, selectorOrPoint, options = {}) {
|
|
|
866
975
|
return { target, actual, point };
|
|
867
976
|
}
|
|
868
977
|
async function fill(ctx, selector, text2, options = {}) {
|
|
869
|
-
|
|
870
|
-
await
|
|
978
|
+
const value = String(text2 ?? "");
|
|
979
|
+
const clicked = await click(ctx, selector, {
|
|
980
|
+
...options,
|
|
981
|
+
settleMs: Number(options.focusSettleMs || 350)
|
|
982
|
+
});
|
|
983
|
+
try {
|
|
984
|
+
await Device.typeText(ctx, value);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
throw new CrawlerError({
|
|
987
|
+
message: `automation_failed: View \u5199\u5165\u6587\u672C\u5931\u8D25 ${JSON.stringify(selector)}`,
|
|
988
|
+
code: Code.AutomationFailed,
|
|
989
|
+
context: { selector, error: error?.message || String(error) }
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
Logger.info("DeviceInput.fill", {
|
|
993
|
+
selector,
|
|
994
|
+
target: simplifyNode(clicked.actual || clicked.target),
|
|
995
|
+
chars: value.length
|
|
996
|
+
});
|
|
871
997
|
await sleep(Number(options.settleMs || 350));
|
|
998
|
+
return { target: clicked.actual || clicked.target, chars: value.length };
|
|
872
999
|
}
|
|
873
1000
|
async function press(ctx, key) {
|
|
874
1001
|
await Device.pressKey(ctx, key);
|
|
@@ -956,160 +1083,18 @@ function simplifyNode(node) {
|
|
|
956
1083
|
};
|
|
957
1084
|
}
|
|
958
1085
|
|
|
959
|
-
// src/
|
|
960
|
-
import {
|
|
961
|
-
import {
|
|
1086
|
+
// src/device-sqlite.js
|
|
1087
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1088
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
962
1089
|
import { tmpdir } from "node:os";
|
|
963
1090
|
import path from "node:path";
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const pid = await resolvePid(ctx, options.packageName || ctx.packageName);
|
|
969
|
-
const scriptPath = writeTempScript(source, label);
|
|
970
|
-
const args = [
|
|
971
|
-
"-q",
|
|
972
|
-
"-t",
|
|
973
|
-
String(Number(options.fridaTimeoutSeconds || 8)),
|
|
974
|
-
"-D",
|
|
975
|
-
ctx.serial,
|
|
976
|
-
"-p",
|
|
977
|
-
pid,
|
|
978
|
-
"-l",
|
|
979
|
-
scriptPath
|
|
980
|
-
];
|
|
981
|
-
Logger.info("frida script start", { label, pid, timeoutMs: Number(options.timeoutMs || 12e3) });
|
|
982
|
-
try {
|
|
983
|
-
return await runFridaProcess(ctx.fridaPath, args, {
|
|
984
|
-
marker,
|
|
985
|
-
timeoutMs: Number(options.timeoutMs || 12e3),
|
|
986
|
-
maxLines: Number(options.maxLines || 1500)
|
|
987
|
-
});
|
|
988
|
-
} finally {
|
|
989
|
-
rmSync(path.dirname(scriptPath), { recursive: true, force: true });
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
async function resolveFridaPid(ctx, packageName = ctx.packageName) {
|
|
993
|
-
return resolvePid(ctx, packageName);
|
|
994
|
-
}
|
|
995
|
-
async function assertFridaReady(ctx) {
|
|
996
|
-
if (!ctx?.fridaPath) {
|
|
997
|
-
throw new CrawlerError({
|
|
998
|
-
message: "frida_unavailable: ANDROID_TOOLKIT_FRIDA_PATH is required",
|
|
999
|
-
code: Code.FridaUnavailable
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
if (!ctx?.serial) {
|
|
1003
|
-
throw new CrawlerError({
|
|
1004
|
-
message: "frida_unavailable: device serial is required",
|
|
1005
|
-
code: Code.FridaUnavailable
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
async function resolvePid(ctx, packageName = ctx.packageName) {
|
|
1010
|
-
const target = String(packageName || "").trim();
|
|
1011
|
-
if (!target) {
|
|
1012
|
-
throw new CrawlerError({
|
|
1013
|
-
message: "frida_unavailable: packageName is required",
|
|
1014
|
-
code: Code.FridaUnavailable
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
const out = await Device.adbShell(ctx, ["pidof", target], { timeoutMs: 8e3 }).catch(() => "");
|
|
1018
|
-
const pid = String(out || "").trim().split(/\s+/).find(Boolean);
|
|
1019
|
-
if (!pid) {
|
|
1020
|
-
throw new CrawlerError({
|
|
1021
|
-
message: `frida_unavailable: target app pid not found ${target}`,
|
|
1022
|
-
code: Code.FridaUnavailable,
|
|
1023
|
-
context: { packageName: target }
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
return pid;
|
|
1027
|
-
}
|
|
1028
|
-
function runFridaProcess(fridaPath, args, options) {
|
|
1029
|
-
return new Promise((resolve, reject) => {
|
|
1030
|
-
const events = [];
|
|
1031
|
-
const lines = [];
|
|
1032
|
-
let buffer = "";
|
|
1033
|
-
let finished = false;
|
|
1034
|
-
const child = spawn(fridaPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1035
|
-
const timer = setTimeout(() => {
|
|
1036
|
-
if (finished) return;
|
|
1037
|
-
finished = true;
|
|
1038
|
-
child.kill("SIGTERM");
|
|
1039
|
-
reject(new CrawlerError({
|
|
1040
|
-
message: `frida_unavailable: frida script timeout ${options.timeoutMs}ms`,
|
|
1041
|
-
code: Code.FridaUnavailable,
|
|
1042
|
-
context: { lines: lines.slice(-40) }
|
|
1043
|
-
}));
|
|
1044
|
-
}, options.timeoutMs);
|
|
1045
|
-
const consumeLine = (line) => {
|
|
1046
|
-
const text2 = String(line || "").trim();
|
|
1047
|
-
if (!text2) return;
|
|
1048
|
-
lines.push(text2);
|
|
1049
|
-
if (lines.length > options.maxLines) lines.shift();
|
|
1050
|
-
const markerIndex = text2.indexOf(options.marker);
|
|
1051
|
-
if (markerIndex < 0) return;
|
|
1052
|
-
const jsonText = text2.slice(markerIndex + options.marker.length).trim();
|
|
1053
|
-
try {
|
|
1054
|
-
events.push(JSON.parse(jsonText));
|
|
1055
|
-
} catch (error) {
|
|
1056
|
-
events.push({ type: "parse_error", error: error?.message || String(error), line: text2 });
|
|
1057
|
-
}
|
|
1058
|
-
};
|
|
1059
|
-
const onChunk = (chunk) => {
|
|
1060
|
-
buffer += chunk.toString("utf8");
|
|
1061
|
-
let index = buffer.indexOf("\n");
|
|
1062
|
-
while (index >= 0) {
|
|
1063
|
-
consumeLine(buffer.slice(0, index));
|
|
1064
|
-
buffer = buffer.slice(index + 1);
|
|
1065
|
-
index = buffer.indexOf("\n");
|
|
1066
|
-
}
|
|
1067
|
-
};
|
|
1068
|
-
child.stdout.on("data", onChunk);
|
|
1069
|
-
child.stderr.on("data", onChunk);
|
|
1070
|
-
child.on("error", (error) => {
|
|
1071
|
-
if (finished) return;
|
|
1072
|
-
finished = true;
|
|
1073
|
-
clearTimeout(timer);
|
|
1074
|
-
reject(new CrawlerError({
|
|
1075
|
-
message: `frida_unavailable: ${error?.message || String(error)}`,
|
|
1076
|
-
code: Code.FridaUnavailable
|
|
1077
|
-
}));
|
|
1078
|
-
});
|
|
1079
|
-
child.on("close", () => {
|
|
1080
|
-
if (finished) return;
|
|
1081
|
-
finished = true;
|
|
1082
|
-
clearTimeout(timer);
|
|
1083
|
-
if (buffer.trim()) consumeLine(buffer);
|
|
1084
|
-
const parsed = events.filter((event) => event?.type !== "parse_error");
|
|
1085
|
-
if (parsed.length === 0) {
|
|
1086
|
-
reject(new CrawlerError({
|
|
1087
|
-
message: "frida_unavailable: script emitted no event",
|
|
1088
|
-
code: Code.FridaUnavailable,
|
|
1089
|
-
context: { lines: lines.slice(-40), events }
|
|
1090
|
-
}));
|
|
1091
|
-
return;
|
|
1092
|
-
}
|
|
1093
|
-
resolve(parsed.at(-1));
|
|
1094
|
-
});
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
function writeTempScript(source, label) {
|
|
1098
|
-
const dir = mkdtempSync(path.join(tmpdir(), `android-toolkit-${safeName(label)}-`));
|
|
1099
|
-
const scriptPath = path.join(dir, "script.js");
|
|
1100
|
-
writeFileSync(scriptPath, String(source || ""), "utf8");
|
|
1101
|
-
return scriptPath;
|
|
1102
|
-
}
|
|
1103
|
-
function safeName(value) {
|
|
1104
|
-
return String(value || "script").replace(/[^a-z0-9_-]+/gi, "-").slice(0, 80);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
// src/frida-client.js
|
|
1108
|
-
var Frida = {
|
|
1109
|
-
querySQLite,
|
|
1091
|
+
import { promisify as promisify2 } from "node:util";
|
|
1092
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1093
|
+
var DeviceSQLite = {
|
|
1094
|
+
query,
|
|
1110
1095
|
health
|
|
1111
1096
|
};
|
|
1112
|
-
async function
|
|
1097
|
+
async function query(ctx, options = {}) {
|
|
1113
1098
|
if (!options.sql) {
|
|
1114
1099
|
throw new CrawlerError({
|
|
1115
1100
|
message: "invalid_request: sql is required",
|
|
@@ -1124,115 +1109,183 @@ async function querySQLite(ctx, options = {}) {
|
|
|
1124
1109
|
dbNameExcludes: normalizeStringArray(options.dbNameExcludes),
|
|
1125
1110
|
sql: String(options.sql || ""),
|
|
1126
1111
|
args: normalizeStringArray(options.args),
|
|
1127
|
-
maxRows: Math.max(1, Number(options.maxRows || 200))
|
|
1112
|
+
maxRows: Math.max(1, Number(options.maxRows || 200)),
|
|
1113
|
+
label: String(options.label || "query-sqlite")
|
|
1128
1114
|
};
|
|
1129
|
-
|
|
1130
|
-
label: options.label || "query-sqlite",
|
|
1131
|
-
packageName: options.packageName || ctx.packageName,
|
|
1132
|
-
timeoutMs: options.timeoutMs || 3e4,
|
|
1133
|
-
fridaTimeoutSeconds: options.fridaTimeoutSeconds || 25,
|
|
1134
|
-
maxLines: options.maxLines || 4e3
|
|
1135
|
-
});
|
|
1136
|
-
if (!event.ok) {
|
|
1115
|
+
if (!config.dbPath && !config.dbDir) {
|
|
1137
1116
|
throw new CrawlerError({
|
|
1138
|
-
message:
|
|
1139
|
-
code: Code.
|
|
1140
|
-
|
|
1117
|
+
message: "invalid_request: dbPath or dbDir is required",
|
|
1118
|
+
code: Code.InvalidRequest
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
const startedAt = Date.now();
|
|
1122
|
+
const dir = await mkdtemp(path.join(tmpdir(), "android-toolkit-sqlite-"));
|
|
1123
|
+
try {
|
|
1124
|
+
const dbPaths = await resolveDeviceDbPaths(ctx, config);
|
|
1125
|
+
if (dbPaths.length === 0) {
|
|
1126
|
+
throw new CrawlerError({
|
|
1127
|
+
message: "data_access_unavailable: sqlite database not found",
|
|
1128
|
+
code: Code.DataAccessUnavailable,
|
|
1129
|
+
context: { dbDir: config.dbDir, dbPath: config.dbPath, dbNamePrefix: config.dbNamePrefix }
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
const databases = [];
|
|
1133
|
+
for (const dbPath of dbPaths) {
|
|
1134
|
+
const localDbPath = await pullDatabaseSnapshot(ctx, dbPath, dir);
|
|
1135
|
+
const result = await queryLocalSQLite(localDbPath, config);
|
|
1136
|
+
databases.push({
|
|
1137
|
+
dbPath,
|
|
1138
|
+
columns: result.columns,
|
|
1139
|
+
rows: result.rows,
|
|
1140
|
+
rowCount: result.rows.length,
|
|
1141
|
+
truncated: result.truncated
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
Logger.info("DeviceSQLite.query", {
|
|
1145
|
+
label: config.label,
|
|
1146
|
+
databaseCount: databases.length,
|
|
1147
|
+
rowCount: databases.reduce((total, item) => total + item.rowCount, 0),
|
|
1148
|
+
duration: Logger.duration(startedAt)
|
|
1149
|
+
});
|
|
1150
|
+
return { ok: true, databases, rows: databases.length === 1 ? databases[0].rows : [] };
|
|
1151
|
+
} finally {
|
|
1152
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1141
1153
|
});
|
|
1142
1154
|
}
|
|
1143
|
-
return event;
|
|
1144
1155
|
}
|
|
1145
|
-
async function health(ctx) {
|
|
1146
|
-
|
|
1156
|
+
async function health(ctx, options = {}) {
|
|
1157
|
+
const packageName = String(options.packageName || ctx?.packageName || "").trim();
|
|
1158
|
+
if (!packageName) {
|
|
1147
1159
|
throw new CrawlerError({
|
|
1148
|
-
message: "
|
|
1149
|
-
code: Code.
|
|
1160
|
+
message: "invalid_request: packageName is required",
|
|
1161
|
+
code: Code.InvalidRequest
|
|
1150
1162
|
});
|
|
1151
1163
|
}
|
|
1152
|
-
|
|
1164
|
+
await adbSuShell(ctx, `test -d ${shellQuote(`/data/data/${packageName}`)}`, {
|
|
1165
|
+
timeoutMs: Number(options.timeoutMs || 1e4)
|
|
1166
|
+
});
|
|
1167
|
+
return { ok: true, packageName };
|
|
1168
|
+
}
|
|
1169
|
+
async function resolveDeviceDbPaths(ctx, config) {
|
|
1170
|
+
if (config.dbPath) return [config.dbPath];
|
|
1171
|
+
const command = `cd ${shellQuote(config.dbDir)} || exit 1; for file in *; do [ -f "$file" ] || continue; case "$file" in *-wal|*-shm) continue ;; esac; printf "%s\\n" "$file"; done`;
|
|
1172
|
+
const out = await adbSuShell(ctx, command, {
|
|
1173
|
+
timeoutMs: 15e3,
|
|
1174
|
+
maxBuffer: 2 * 1024 * 1024
|
|
1175
|
+
}).catch((error) => {
|
|
1153
1176
|
throw new CrawlerError({
|
|
1154
|
-
message:
|
|
1155
|
-
code: Code.
|
|
1177
|
+
message: `data_access_unavailable: sqlite dbDir unavailable ${error?.message || String(error)}`,
|
|
1178
|
+
code: Code.DataAccessUnavailable,
|
|
1179
|
+
context: { dbDir: config.dbDir }
|
|
1156
1180
|
});
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
function matchesName(name) {
|
|
1177
|
-
if (config.dbNamePrefix && name.indexOf(config.dbNamePrefix) !== 0) return false;
|
|
1178
|
-
if (config.dbNameIncludes && name.indexOf(config.dbNameIncludes) < 0) return false;
|
|
1179
|
-
for (var i = 0; i < config.dbNameExcludes.length; i++) {
|
|
1180
|
-
if (String(name).indexOf(config.dbNameExcludes[i]) >= 0) return false;
|
|
1181
|
-
}
|
|
1182
|
-
return true;
|
|
1183
|
-
}
|
|
1184
|
-
function dbPaths() {
|
|
1185
|
-
if (config.dbPath) return [config.dbPath];
|
|
1186
|
-
var File = Java.use('java.io.File');
|
|
1187
|
-
var dir = File.$new(config.dbDir);
|
|
1188
|
-
var files = dir.listFiles();
|
|
1189
|
-
var out = [];
|
|
1190
|
-
if (!files) return out;
|
|
1191
|
-
for (var i = 0; i < files.length; i++) {
|
|
1192
|
-
var name = String(files[i].getName());
|
|
1193
|
-
if (matchesName(name)) out.push(String(files[i].getAbsolutePath()));
|
|
1194
|
-
}
|
|
1195
|
-
return out;
|
|
1196
|
-
}
|
|
1197
|
-
function queryOne(dbPath) {
|
|
1198
|
-
var SQLiteDatabase = Java.use('android.database.sqlite.SQLiteDatabase');
|
|
1199
|
-
var db = SQLiteDatabase.openDatabase(JavaString.$new(dbPath), null, 1);
|
|
1200
|
-
try {
|
|
1201
|
-
var cursor = db.rawQuery(JavaString.$new(config.sql), stringArray(config.args));
|
|
1202
|
-
var columns = [];
|
|
1203
|
-
var columnCount = cursor.getColumnCount();
|
|
1204
|
-
for (var c = 0; c < columnCount; c++) columns.push(String(cursor.getColumnName(c)));
|
|
1205
|
-
var rows = [];
|
|
1206
|
-
var truncated = false;
|
|
1207
|
-
while (cursor.moveToNext()) {
|
|
1208
|
-
if (rows.length >= config.maxRows) { truncated = true; break; }
|
|
1209
|
-
var row = {};
|
|
1210
|
-
for (var i = 0; i < columns.length; i++) row[columns[i]] = safeString(cursor, i);
|
|
1211
|
-
rows.push(row);
|
|
1212
|
-
}
|
|
1213
|
-
cursor.close();
|
|
1214
|
-
return { dbPath: dbPath, columns: columns, rows: rows, rowCount: rows.length, truncated: truncated };
|
|
1215
|
-
} finally {
|
|
1216
|
-
db.close();
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1181
|
+
});
|
|
1182
|
+
return String(out || "").split("\n").map((line) => line.trim()).filter(Boolean).filter((name) => matchesName(name, config)).map((name) => `${config.dbDir.replace(/\/+$/, "")}/${name}`).sort();
|
|
1183
|
+
}
|
|
1184
|
+
async function pullDatabaseSnapshot(ctx, dbPath, localDir) {
|
|
1185
|
+
const serial = requireNonEmpty2(ctx?.serial, "serial");
|
|
1186
|
+
const adbPath = requireNonEmpty2(ctx?.adbPath, "adbPath");
|
|
1187
|
+
const baseName = path.basename(dbPath);
|
|
1188
|
+
const remoteDir = `/data/local/tmp/android-toolkit-sqlite-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1189
|
+
const remoteDb = `${remoteDir}/${baseName}`;
|
|
1190
|
+
const localDb = path.join(localDir, baseName);
|
|
1191
|
+
const copyCommand = [
|
|
1192
|
+
`rm -rf ${shellQuote(remoteDir)}`,
|
|
1193
|
+
`mkdir -p ${shellQuote(remoteDir)}`,
|
|
1194
|
+
`cp ${shellQuote(dbPath)} ${shellQuote(remoteDb)}`,
|
|
1195
|
+
`[ -f ${shellQuote(`${dbPath}-wal`)} ] && cp ${shellQuote(`${dbPath}-wal`)} ${shellQuote(`${remoteDb}-wal`)} || true`,
|
|
1196
|
+
`[ -f ${shellQuote(`${dbPath}-shm`)} ] && cp ${shellQuote(`${dbPath}-shm`)} ${shellQuote(`${remoteDb}-shm`)} || true`,
|
|
1197
|
+
`chmod 644 ${shellQuote(remoteDir)}/*`
|
|
1198
|
+
].join(" && ");
|
|
1219
1199
|
try {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1200
|
+
await adbSuShell(ctx, copyCommand, { timeoutMs: 3e4, maxBuffer: 4 * 1024 * 1024 });
|
|
1201
|
+
await adbPull(adbPath, serial, remoteDb, localDb).catch((error) => {
|
|
1202
|
+
throw new CrawlerError({
|
|
1203
|
+
message: `data_access_unavailable: sqlite snapshot pull failed ${error?.message || String(error)}`,
|
|
1204
|
+
code: Code.DataAccessUnavailable,
|
|
1205
|
+
context: { dbPath }
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
await adbPullOptional(adbPath, serial, `${remoteDb}-wal`, `${localDb}-wal`);
|
|
1209
|
+
await adbPullOptional(adbPath, serial, `${remoteDb}-shm`, `${localDb}-shm`);
|
|
1210
|
+
return localDb;
|
|
1211
|
+
} finally {
|
|
1212
|
+
await adbSuShell(ctx, `rm -rf ${shellQuote(remoteDir)}`, { timeoutMs: 1e4 }).catch(() => {
|
|
1213
|
+
});
|
|
1227
1214
|
}
|
|
1228
|
-
}
|
|
1215
|
+
}
|
|
1216
|
+
async function adbSuShell(ctx, command, options = {}) {
|
|
1217
|
+
return Device.adbShell(ctx, [`su -c ${shellQuote(command)}`], options);
|
|
1218
|
+
}
|
|
1219
|
+
async function queryLocalSQLite(dbPath, config) {
|
|
1220
|
+
const payloadPath = `${dbPath}.query.json`;
|
|
1221
|
+
const outputPath = `${dbPath}.rows.json`;
|
|
1222
|
+
await writeFile(payloadPath, JSON.stringify({
|
|
1223
|
+
dbPath,
|
|
1224
|
+
sql: config.sql,
|
|
1225
|
+
args: config.args,
|
|
1226
|
+
maxRows: config.maxRows,
|
|
1227
|
+
outputPath
|
|
1228
|
+
}), "utf8");
|
|
1229
|
+
const code = `
|
|
1230
|
+
const fs = require('node:fs');
|
|
1231
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
1232
|
+
const payload = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
1233
|
+
const db = new DatabaseSync(payload.dbPath, { open: true, readOnly: true });
|
|
1234
|
+
try {
|
|
1235
|
+
const stmt = db.prepare(payload.sql);
|
|
1236
|
+
const columns = stmt.columns().map((column) => String(column.name || ''));
|
|
1237
|
+
const rows = [];
|
|
1238
|
+
let truncated = false;
|
|
1239
|
+
for (const row of stmt.iterate(...payload.args)) {
|
|
1240
|
+
if (rows.length >= payload.maxRows) { truncated = true; break; }
|
|
1241
|
+
const out = {};
|
|
1242
|
+
for (const column of columns) out[column] = row[column] == null ? '' : String(row[column]);
|
|
1243
|
+
rows.push(out);
|
|
1244
|
+
}
|
|
1245
|
+
fs.writeFileSync(payload.outputPath, JSON.stringify({ columns, rows, truncated }), 'utf8');
|
|
1246
|
+
} finally {
|
|
1247
|
+
db.close();
|
|
1248
|
+
}
|
|
1229
1249
|
`;
|
|
1250
|
+
await execFileAsync2(process.execPath, ["-e", code, payloadPath], {
|
|
1251
|
+
timeout: 3e4,
|
|
1252
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1253
|
+
encoding: "utf8"
|
|
1254
|
+
});
|
|
1255
|
+
return JSON.parse(await import("node:fs/promises").then((fs3) => fs3.readFile(outputPath, "utf8")));
|
|
1256
|
+
}
|
|
1257
|
+
async function adbPull(adbPath, serial, remotePath, localPath) {
|
|
1258
|
+
await execFileAsync2(adbPath, ["-s", serial, "pull", remotePath, localPath], {
|
|
1259
|
+
timeout: 3e4,
|
|
1260
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1261
|
+
encoding: "utf8"
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
async function adbPullOptional(adbPath, serial, remotePath, localPath) {
|
|
1265
|
+
await adbPull(adbPath, serial, remotePath, localPath).catch(() => {
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
function matchesName(name, config) {
|
|
1269
|
+
if (config.dbNamePrefix && !name.startsWith(config.dbNamePrefix)) return false;
|
|
1270
|
+
if (config.dbNameIncludes && !name.includes(config.dbNameIncludes)) return false;
|
|
1271
|
+
for (const item of config.dbNameExcludes || []) {
|
|
1272
|
+
if (item && name.includes(item)) return false;
|
|
1273
|
+
}
|
|
1274
|
+
return true;
|
|
1230
1275
|
}
|
|
1231
1276
|
function normalizeStringArray(value) {
|
|
1232
1277
|
if (Array.isArray(value)) return value.map((item) => String(item ?? ""));
|
|
1233
1278
|
if (value == null) return [];
|
|
1234
1279
|
return [String(value)];
|
|
1235
1280
|
}
|
|
1281
|
+
function requireNonEmpty2(value, name) {
|
|
1282
|
+
const clean = String(value ?? "").trim();
|
|
1283
|
+
if (!clean) throw new CrawlerError({ message: `invalid_request: ${name} is required`, code: Code.InvalidRequest });
|
|
1284
|
+
return clean;
|
|
1285
|
+
}
|
|
1286
|
+
function shellQuote(value) {
|
|
1287
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1288
|
+
}
|
|
1236
1289
|
|
|
1237
1290
|
// src/launch.js
|
|
1238
1291
|
import fs2 from "node:fs";
|
|
@@ -1526,12 +1579,8 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1526
1579
|
let smallest = null;
|
|
1527
1580
|
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
1528
1581
|
const candidate = await encodeJpeg(sourceImage, compression, scale, quality);
|
|
1529
|
-
if (!smallest || candidate.bytes < smallest.bytes)
|
|
1530
|
-
|
|
1531
|
-
}
|
|
1532
|
-
if (candidate.bytes <= compression.maxBytes) {
|
|
1533
|
-
return { ...candidate, withinLimit: true };
|
|
1534
|
-
}
|
|
1582
|
+
if (!smallest || candidate.bytes < smallest.bytes) smallest = candidate;
|
|
1583
|
+
if (candidate.bytes <= compression.maxBytes) return { ...candidate, withinLimit: true };
|
|
1535
1584
|
if (quality > minQuality) {
|
|
1536
1585
|
quality = Math.max(minQuality, Math.floor(quality * 0.75));
|
|
1537
1586
|
continue;
|
|
@@ -1541,9 +1590,7 @@ var compressImageBuffer = async (buffer, compression) => {
|
|
|
1541
1590
|
compression.minScale,
|
|
1542
1591
|
Math.min(scale * 0.85, scale * ratio * 0.94)
|
|
1543
1592
|
);
|
|
1544
|
-
if (nextScale >= scale * 0.99 || scale <= compression.minScale)
|
|
1545
|
-
break;
|
|
1546
|
-
}
|
|
1593
|
+
if (nextScale >= scale * 0.99 || scale <= compression.minScale) break;
|
|
1547
1594
|
scale = nextScale;
|
|
1548
1595
|
}
|
|
1549
1596
|
const finalCandidate = await encodeJpeg(sourceImage, compression, compression.minScale, minQuality);
|
|
@@ -1559,9 +1606,7 @@ var compressImageBufferToBase64 = async (buffer, compression) => {
|
|
|
1559
1606
|
Logger.warn("captureScreen \u538B\u7F29\u5931\u8D25\uFF0C\u8FD4\u56DE\u539F\u56FE", { message: error?.message || String(error) });
|
|
1560
1607
|
return null;
|
|
1561
1608
|
});
|
|
1562
|
-
if (!result?.buffer)
|
|
1563
|
-
return buffer.toString("base64");
|
|
1564
|
-
}
|
|
1609
|
+
if (!result?.buffer) return buffer.toString("base64");
|
|
1565
1610
|
if (result.withinLimit) {
|
|
1566
1611
|
Logger.info("captureScreen \u5DF2\u538B\u7F29", {
|
|
1567
1612
|
originalBytes,
|
|
@@ -1649,21 +1694,24 @@ async function captureLink(ctx, options = {}) {
|
|
|
1649
1694
|
const deadline = Date.now() + timeoutMs;
|
|
1650
1695
|
const prefix = String(share.prefix || "").trim();
|
|
1651
1696
|
Logger.start("Share.captureLink", { actor: actorInfo.key, prefix, timeoutMs });
|
|
1652
|
-
await
|
|
1653
|
-
|
|
1654
|
-
});
|
|
1697
|
+
const beforeEvent = await readClipboard2(ctx).catch(() => null);
|
|
1698
|
+
const beforeLink = beforeEvent ? selectLink(beforeEvent, prefix) : "";
|
|
1699
|
+
if (beforeLink) Logger.info("Share.captureLink baseline", { beforeLink });
|
|
1655
1700
|
if (typeof options.performActions === "function") {
|
|
1656
1701
|
await options.performActions();
|
|
1657
1702
|
}
|
|
1658
1703
|
let lastEvent = null;
|
|
1659
1704
|
while (Date.now() < deadline) {
|
|
1660
|
-
const event = await
|
|
1705
|
+
const event = await readClipboard2(ctx).catch((error) => ({ ok: false, error: error?.message || String(error) }));
|
|
1661
1706
|
lastEvent = event;
|
|
1662
1707
|
const link = selectLink(event, prefix);
|
|
1663
|
-
if (link) {
|
|
1708
|
+
if (link && link !== beforeLink) {
|
|
1664
1709
|
Logger.success("Share.captureLink", { link });
|
|
1665
1710
|
return { link, source: event.source || "clipboard", payloadSnapshot: event.payloadSnapshot || "" };
|
|
1666
1711
|
}
|
|
1712
|
+
if (link && link === beforeLink) {
|
|
1713
|
+
lastEvent = { ...event, rejected: "same_as_baseline", beforeLink };
|
|
1714
|
+
}
|
|
1667
1715
|
await sleep(pollIntervalMs);
|
|
1668
1716
|
}
|
|
1669
1717
|
throw new CrawlerError({
|
|
@@ -1672,77 +1720,20 @@ async function captureLink(ctx, options = {}) {
|
|
|
1672
1720
|
context: { prefix, lastEvent }
|
|
1673
1721
|
});
|
|
1674
1722
|
}
|
|
1675
|
-
async function
|
|
1676
|
-
await
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
function clipboardScript(mode) {
|
|
1691
|
-
return `
|
|
1692
|
-
Java.perform(function () {
|
|
1693
|
-
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
1694
|
-
var mode = ${JSON.stringify(mode)};
|
|
1695
|
-
var JavaObject = Java.use('java.lang.Object');
|
|
1696
|
-
function text(value) {
|
|
1697
|
-
if (value === null || value === undefined) return '';
|
|
1698
|
-
try { return Java.cast(value, JavaObject).toString() + ''; } catch (_) {}
|
|
1699
|
-
try { return value.toString.overload().call(value) + ''; } catch (_) {}
|
|
1700
|
-
try { return value.toString() + ''; } catch (_) {}
|
|
1701
|
-
return '';
|
|
1702
|
-
}
|
|
1703
|
-
function addCandidate(out, source, value) {
|
|
1704
|
-
var s = text(value);
|
|
1705
|
-
if (!s) return;
|
|
1706
|
-
var matches = s.match(/https?:\\/\\/[^\\s"'<>\uFF0C\u3002]+/g) || [];
|
|
1707
|
-
for (var i = 0; i < matches.length; i++) {
|
|
1708
|
-
var link = matches[i].replace(/[)\\].,\uFF0C\u3002\uFF1B;!?\uFF01\uFF1F]+$/g, '');
|
|
1709
|
-
out.push({ source: source, link: link, payload: s.slice(0, 1000) });
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
try {
|
|
1713
|
-
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
1714
|
-
var ClipData = Java.use('android.content.ClipData');
|
|
1715
|
-
var app = ActivityThread.currentApplication();
|
|
1716
|
-
var manager = app ? app.getSystemService('clipboard') : null;
|
|
1717
|
-
if (!manager) { emit({ ok: false, error: 'clipboard manager unavailable' }); return; }
|
|
1718
|
-
if (mode === 'clear') {
|
|
1719
|
-
manager.setPrimaryClip(ClipData.newPlainText('android-toolkit', ''));
|
|
1720
|
-
emit({ ok: true, source: 'clipboard.clear' });
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
var candidates = [];
|
|
1724
|
-
var clip = manager.getPrimaryClip();
|
|
1725
|
-
var count = clip ? clip.getItemCount() : 0;
|
|
1726
|
-
for (var i = 0; i < count; i++) {
|
|
1727
|
-
var item = clip.getItemAt(i);
|
|
1728
|
-
try { addCandidate(candidates, 'clipboard.text', item.getText()); } catch (_) {}
|
|
1729
|
-
try { addCandidate(candidates, 'clipboard.html', item.getHtmlText()); } catch (_) {}
|
|
1730
|
-
try { addCandidate(candidates, 'clipboard.uri', item.getUri()); } catch (_) {}
|
|
1731
|
-
try { addCandidate(candidates, 'clipboard.intent', item.getIntent()); } catch (_) {}
|
|
1732
|
-
try { addCandidate(candidates, 'clipboard.coerceToText', item.coerceToText(app)); } catch (_) {}
|
|
1733
|
-
}
|
|
1734
|
-
emit({
|
|
1735
|
-
ok: candidates.length > 0,
|
|
1736
|
-
link: candidates.length > 0 ? candidates[0].link : '',
|
|
1737
|
-
source: candidates.length > 0 ? candidates[0].source : 'clipboard',
|
|
1738
|
-
candidates: candidates,
|
|
1739
|
-
payloadSnapshot: candidates.length > 0 ? String(candidates[0].payload || '').slice(0, 500) : ''
|
|
1740
|
-
});
|
|
1741
|
-
} catch (error) {
|
|
1742
|
-
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
1743
|
-
}
|
|
1744
|
-
});
|
|
1745
|
-
`;
|
|
1723
|
+
async function readClipboard2(ctx) {
|
|
1724
|
+
const payload = await Device.readClipboard(ctx);
|
|
1725
|
+
const candidates = extractLinks(payload).map((link) => ({
|
|
1726
|
+
source: "adb.clipboard",
|
|
1727
|
+
link,
|
|
1728
|
+
payload: payload.slice(0, 1e3)
|
|
1729
|
+
}));
|
|
1730
|
+
return {
|
|
1731
|
+
ok: candidates.length > 0,
|
|
1732
|
+
link: candidates[0]?.link || "",
|
|
1733
|
+
source: "adb.clipboard",
|
|
1734
|
+
candidates,
|
|
1735
|
+
payloadSnapshot: payload.slice(0, 500)
|
|
1736
|
+
};
|
|
1746
1737
|
}
|
|
1747
1738
|
async function composeSprite(buffers, options = {}) {
|
|
1748
1739
|
if (!buffers.length) {
|
|
@@ -1787,6 +1778,11 @@ function selectLink(event, prefix) {
|
|
|
1787
1778
|
}
|
|
1788
1779
|
return candidates.find((link) => !prefix || String(link).startsWith(prefix)) || "";
|
|
1789
1780
|
}
|
|
1781
|
+
function extractLinks(value) {
|
|
1782
|
+
const text2 = String(value || "");
|
|
1783
|
+
const matches = text2.match(/https?:\/\/[^\s"'<>,。]+/g) || [];
|
|
1784
|
+
return matches.map((link) => link.replace(/[)\].,,。;;!?!?]+$/g, ""));
|
|
1785
|
+
}
|
|
1790
1786
|
|
|
1791
1787
|
// src/launch.js
|
|
1792
1788
|
var DEFAULT_INPUT_PATH = "/apify_storage/input.json";
|
|
@@ -1815,7 +1811,7 @@ async function run(handler, options = {}) {
|
|
|
1815
1811
|
Device,
|
|
1816
1812
|
DeviceInput,
|
|
1817
1813
|
DeviceView,
|
|
1818
|
-
|
|
1814
|
+
DeviceSQLite,
|
|
1819
1815
|
Share,
|
|
1820
1816
|
Mutation,
|
|
1821
1817
|
Logger,
|
|
@@ -1853,20 +1849,26 @@ function pathOption(value, fallback) {
|
|
|
1853
1849
|
}
|
|
1854
1850
|
|
|
1855
1851
|
// entrys/node.js
|
|
1856
|
-
var
|
|
1857
|
-
|
|
1858
|
-
ApifyKit,
|
|
1859
|
-
DeviceInput,
|
|
1860
|
-
DeviceView,
|
|
1861
|
-
Device,
|
|
1862
|
-
Mutation,
|
|
1863
|
-
Share,
|
|
1864
|
-
Frida,
|
|
1865
|
-
Constants: constants_exports,
|
|
1866
|
-
Errors: errors_exports,
|
|
1867
|
-
Logger,
|
|
1868
|
-
Context
|
|
1852
|
+
var ToolkitMode = Object.freeze({
|
|
1853
|
+
default: "default"
|
|
1869
1854
|
});
|
|
1855
|
+
var useAndroidToolKit = () => {
|
|
1856
|
+
return {
|
|
1857
|
+
Launch,
|
|
1858
|
+
ApifyKit,
|
|
1859
|
+
DeviceInput,
|
|
1860
|
+
DeviceView,
|
|
1861
|
+
DeviceSQLite,
|
|
1862
|
+
Device,
|
|
1863
|
+
Mutation,
|
|
1864
|
+
Share,
|
|
1865
|
+
Constants: constants_exports,
|
|
1866
|
+
Errors: errors_exports,
|
|
1867
|
+
Logger,
|
|
1868
|
+
Context
|
|
1869
|
+
};
|
|
1870
|
+
};
|
|
1871
|
+
useAndroidToolKit.Mode = ToolkitMode;
|
|
1870
1872
|
export {
|
|
1871
1873
|
useAndroidToolKit
|
|
1872
1874
|
};
|