@skrillex1224/android-toolkit 0.1.0 → 0.1.2
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/package.json +1 -1
- package/src/frida-client.js +220 -3
package/package.json
CHANGED
package/src/frida-client.js
CHANGED
|
@@ -4,7 +4,7 @@ import {spawn} from 'node:child_process';
|
|
|
4
4
|
import {Code} from './constants.js';
|
|
5
5
|
import {adbShell} from './device.js';
|
|
6
6
|
import {CrawlerError} from './errors.js';
|
|
7
|
-
import {Logger} from './logger.js';
|
|
7
|
+
import {Logger, sleep} from './logger.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* 启动一个持续运行的 Frida WebView 事件记录器。
|
|
@@ -54,6 +54,22 @@ export async function startWebViewEventRecorder(ctx, config = {}, options = {})
|
|
|
54
54
|
* 收集输出和错误显式化。
|
|
55
55
|
*/
|
|
56
56
|
export async function runScript(ctx, source, options = {}) {
|
|
57
|
+
const attempts = Math.max(1, Number(options.attempts || 3));
|
|
58
|
+
let lastError = null;
|
|
59
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
60
|
+
try {
|
|
61
|
+
return await runScriptOnce(ctx, source, options);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
lastError = error;
|
|
64
|
+
if (attempt >= attempts || error?.code !== Code.FridaUnavailable) throw error;
|
|
65
|
+
Logger.warn(`frida ${options.label || 'script'} 第 ${attempt} 次未返回,准备重试`);
|
|
66
|
+
await sleep(Number(options.retryDelayMs || 800));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
throw lastError;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runScriptOnce(ctx, source, options = {}) {
|
|
57
73
|
assertFridaReady(ctx, ctx.fridaWebViewEventAgentScript, 'frida webview event agent script');
|
|
58
74
|
const marker = options.marker || 'ANDROID_TOOLKIT_SCRIPT_JSON ';
|
|
59
75
|
const scriptPath = writeTempScript(ctx, source, options.label || 'script');
|
|
@@ -74,7 +90,7 @@ export async function runScript(ctx, source, options = {}) {
|
|
|
74
90
|
maxLines: Number(options.maxLines || 1200),
|
|
75
91
|
outputPath: options.outputPath || ''
|
|
76
92
|
});
|
|
77
|
-
await recorder.waitForExit(Number(options.timeoutMs ||
|
|
93
|
+
await recorder.waitForExit(Number(options.timeoutMs || 10000));
|
|
78
94
|
const result = await recorder.stop();
|
|
79
95
|
const scriptEvents = result.events.filter((event) => event?.type !== 'parse_error');
|
|
80
96
|
if (scriptEvents.length === 0) {
|
|
@@ -187,7 +203,7 @@ Java.perform(function () {
|
|
|
187
203
|
var view = views[i];
|
|
188
204
|
if (!view.isShown()) continue;
|
|
189
205
|
var value = '';
|
|
190
|
-
try { value = String(Java.cast(view, TextView).getText()); } catch (_) { continue; }
|
|
206
|
+
try { value = String(Java.cast(view, TextView).getText().toString()); } catch (_) { continue; }
|
|
191
207
|
if (exact ? value === target : value.indexOf(target) >= 0) {
|
|
192
208
|
var rect = rectOf(view);
|
|
193
209
|
if (rect.right <= rect.left || rect.bottom <= rect.top) continue;
|
|
@@ -220,6 +236,205 @@ Java.perform(function () {
|
|
|
220
236
|
return event.matches || [];
|
|
221
237
|
}
|
|
222
238
|
|
|
239
|
+
/**
|
|
240
|
+
* 列出当前 App 内部可见输入框。
|
|
241
|
+
*
|
|
242
|
+
* 微信这类 App 往往不给系统 uiautomator 暴露节点,但 App 进程内仍能看到
|
|
243
|
+
* EditText。业务方可以用 hint/text/bounds 挑选输入框,再交给 Device.click
|
|
244
|
+
* 或 setFocusedInputText 使用。
|
|
245
|
+
*/
|
|
246
|
+
export async function findVisibleInputs(ctx, options = {}) {
|
|
247
|
+
const event = await runScript(ctx, `
|
|
248
|
+
Java.perform(function () {
|
|
249
|
+
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
250
|
+
function rectOf(view) {
|
|
251
|
+
var Rect = Java.use('android.graphics.Rect');
|
|
252
|
+
var rect = Rect.$new();
|
|
253
|
+
try { view.getGlobalVisibleRect(rect); } catch (_) { }
|
|
254
|
+
return { left: rect.left.value, top: rect.top.value, right: rect.right.value, bottom: rect.bottom.value };
|
|
255
|
+
}
|
|
256
|
+
function walk(view, out, depth) {
|
|
257
|
+
if (!view || depth > ${Number(options.maxDepth || 20)} || out.length > ${Number(options.maxNodes || 3000)}) return;
|
|
258
|
+
out.push(view);
|
|
259
|
+
try {
|
|
260
|
+
var ViewGroup = Java.use('android.view.ViewGroup');
|
|
261
|
+
var group = Java.cast(view, ViewGroup);
|
|
262
|
+
for (var i = 0; i < group.getChildCount(); i++) walk(group.getChildAt(i), out, depth + 1);
|
|
263
|
+
} catch (_) { }
|
|
264
|
+
}
|
|
265
|
+
function activeActivities() {
|
|
266
|
+
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
267
|
+
var Activity = Java.use('android.app.Activity');
|
|
268
|
+
var ArrayMap = Java.use('android.util.ArrayMap');
|
|
269
|
+
var thread = ActivityThread.currentActivityThread();
|
|
270
|
+
var field = thread.getClass().getDeclaredField('mActivities');
|
|
271
|
+
field.setAccessible(true);
|
|
272
|
+
var map = Java.cast(field.get(thread), ArrayMap);
|
|
273
|
+
var out = [];
|
|
274
|
+
for (var i = 0; i < map.size(); i++) {
|
|
275
|
+
var record = map.valueAt(i);
|
|
276
|
+
var recordClass = record.getClass();
|
|
277
|
+
var activityField = recordClass.getDeclaredField('activity');
|
|
278
|
+
var pausedField = recordClass.getDeclaredField('paused');
|
|
279
|
+
activityField.setAccessible(true);
|
|
280
|
+
pausedField.setAccessible(true);
|
|
281
|
+
if (!pausedField.getBoolean(record)) out.push(Java.cast(activityField.get(record), Activity));
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
Java.scheduleOnMainThread(function () {
|
|
286
|
+
try {
|
|
287
|
+
var EditText = Java.use('android.widget.EditText');
|
|
288
|
+
var inputs = [];
|
|
289
|
+
var activities = activeActivities();
|
|
290
|
+
for (var a = 0; a < activities.length; a++) {
|
|
291
|
+
var views = [];
|
|
292
|
+
walk(activities[a].getWindow().getDecorView(), views, 0);
|
|
293
|
+
for (var i = 0; i < views.length; i++) {
|
|
294
|
+
var view = views[i];
|
|
295
|
+
if (!view.isShown()) continue;
|
|
296
|
+
try {
|
|
297
|
+
var editText = Java.cast(view, EditText);
|
|
298
|
+
var rect = rectOf(view);
|
|
299
|
+
if (rect.right <= rect.left || rect.bottom <= rect.top) continue;
|
|
300
|
+
inputs.push({
|
|
301
|
+
text: String(editText.getText().toString()),
|
|
302
|
+
hint: String(editText.getHint() ? editText.getHint().toString() : ''),
|
|
303
|
+
className: String(view.getClass().getName()),
|
|
304
|
+
idName: (function () { try { return String(view.getResources().getResourceName(view.getId())); } catch (_) { return ''; } })(),
|
|
305
|
+
focused: Boolean(view.hasFocus()),
|
|
306
|
+
bounds: rect
|
|
307
|
+
});
|
|
308
|
+
} catch (_) { }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
emit({ ok: true, inputs: inputs });
|
|
312
|
+
} catch (error) {
|
|
313
|
+
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
`, {
|
|
318
|
+
label: options.label || 'find-visible-inputs',
|
|
319
|
+
packageName: options.packageName || ctx.packageName,
|
|
320
|
+
timeoutMs: options.timeoutMs || 7000
|
|
321
|
+
});
|
|
322
|
+
if (!event.ok) {
|
|
323
|
+
throw new CrawlerError({
|
|
324
|
+
message: `automation_failed: findVisibleInputs failed ${event.error || ''}`.trim(),
|
|
325
|
+
code: Code.AutomationFailed,
|
|
326
|
+
context: event
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return event.inputs || [];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 给当前获得焦点的 EditText 设置文本,并触发常规输入事件。
|
|
334
|
+
*
|
|
335
|
+
* 这比 adb input text 更适合中文,也比直接 setText 更稳:它先 focus 目标
|
|
336
|
+
* EditText,再用 Editable.replace 更新文本,最后把光标放到末尾。
|
|
337
|
+
*/
|
|
338
|
+
export async function setFocusedInputText(ctx, text, options = {}) {
|
|
339
|
+
const event = await runScript(ctx, `
|
|
340
|
+
Java.perform(function () {
|
|
341
|
+
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
342
|
+
function rectOf(view) {
|
|
343
|
+
var Rect = Java.use('android.graphics.Rect');
|
|
344
|
+
var rect = Rect.$new();
|
|
345
|
+
try { view.getGlobalVisibleRect(rect); } catch (_) { }
|
|
346
|
+
return { left: rect.left.value, top: rect.top.value, right: rect.right.value, bottom: rect.bottom.value };
|
|
347
|
+
}
|
|
348
|
+
function walk(view, out, depth) {
|
|
349
|
+
if (!view || depth > ${Number(options.maxDepth || 20)} || out.length > ${Number(options.maxNodes || 3000)}) return;
|
|
350
|
+
out.push(view);
|
|
351
|
+
try {
|
|
352
|
+
var ViewGroup = Java.use('android.view.ViewGroup');
|
|
353
|
+
var group = Java.cast(view, ViewGroup);
|
|
354
|
+
for (var i = 0; i < group.getChildCount(); i++) walk(group.getChildAt(i), out, depth + 1);
|
|
355
|
+
} catch (_) { }
|
|
356
|
+
}
|
|
357
|
+
function activeActivities() {
|
|
358
|
+
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
359
|
+
var Activity = Java.use('android.app.Activity');
|
|
360
|
+
var ArrayMap = Java.use('android.util.ArrayMap');
|
|
361
|
+
var thread = ActivityThread.currentActivityThread();
|
|
362
|
+
var field = thread.getClass().getDeclaredField('mActivities');
|
|
363
|
+
field.setAccessible(true);
|
|
364
|
+
var map = Java.cast(field.get(thread), ArrayMap);
|
|
365
|
+
var out = [];
|
|
366
|
+
for (var i = 0; i < map.size(); i++) {
|
|
367
|
+
var record = map.valueAt(i);
|
|
368
|
+
var recordClass = record.getClass();
|
|
369
|
+
var activityField = recordClass.getDeclaredField('activity');
|
|
370
|
+
var pausedField = recordClass.getDeclaredField('paused');
|
|
371
|
+
activityField.setAccessible(true);
|
|
372
|
+
pausedField.setAccessible(true);
|
|
373
|
+
if (!pausedField.getBoolean(record)) out.push(Java.cast(activityField.get(record), Activity));
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
Java.scheduleOnMainThread(function () {
|
|
378
|
+
try {
|
|
379
|
+
var EditText = Java.use('android.widget.EditText');
|
|
380
|
+
var targetText = ${JSON.stringify(String(text || ''))};
|
|
381
|
+
var activities = activeActivities();
|
|
382
|
+
var chosen = null;
|
|
383
|
+
var fallback = null;
|
|
384
|
+
for (var a = 0; a < activities.length; a++) {
|
|
385
|
+
var views = [];
|
|
386
|
+
walk(activities[a].getWindow().getDecorView(), views, 0);
|
|
387
|
+
for (var i = 0; i < views.length; i++) {
|
|
388
|
+
var view = views[i];
|
|
389
|
+
if (!view.isShown()) continue;
|
|
390
|
+
try {
|
|
391
|
+
var editText = Java.cast(view, EditText);
|
|
392
|
+
var rect = rectOf(view);
|
|
393
|
+
if (rect.right <= rect.left || rect.bottom <= rect.top) continue;
|
|
394
|
+
var item = { view: editText, bounds: rect, text: String(editText.getText().toString()), hint: String(editText.getHint() ? editText.getHint().toString() : ''), className: String(view.getClass().getName()), focused: Boolean(view.hasFocus()) };
|
|
395
|
+
if (item.focused) chosen = item;
|
|
396
|
+
if (!fallback) fallback = item;
|
|
397
|
+
} catch (_) { }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
chosen = chosen || fallback;
|
|
401
|
+
if (!chosen) {
|
|
402
|
+
emit({ ok: false, error: 'visible EditText not found' });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
var JavaString = Java.use('java.lang.String');
|
|
406
|
+
var BufferType = Java.use('android.widget.TextView$BufferType');
|
|
407
|
+
var SpannableStringBuilder = Java.use('android.text.SpannableStringBuilder');
|
|
408
|
+
chosen.view.requestFocus();
|
|
409
|
+
chosen.view.setText(SpannableStringBuilder.$new(JavaString.$new(targetText)), BufferType.EDITABLE.value);
|
|
410
|
+
chosen.view.setSelection(targetText.length);
|
|
411
|
+
emit({
|
|
412
|
+
ok: true,
|
|
413
|
+
text: targetText,
|
|
414
|
+
hint: String(chosen.view.getHint() ? chosen.view.getHint().toString() : ''),
|
|
415
|
+
className: chosen.className,
|
|
416
|
+
bounds: chosen.bounds
|
|
417
|
+
});
|
|
418
|
+
} catch (error) {
|
|
419
|
+
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
`, {
|
|
424
|
+
label: options.label || 'set-focused-input-text',
|
|
425
|
+
packageName: options.packageName || ctx.packageName,
|
|
426
|
+
timeoutMs: options.timeoutMs || 7000
|
|
427
|
+
});
|
|
428
|
+
if (!event.ok) {
|
|
429
|
+
throw new CrawlerError({
|
|
430
|
+
message: `automation_failed: setFocusedInputText failed ${event.error || ''}`.trim(),
|
|
431
|
+
code: Code.AutomationFailed,
|
|
432
|
+
context: event
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return event;
|
|
436
|
+
}
|
|
437
|
+
|
|
223
438
|
/**
|
|
224
439
|
* 找到目标包名当前可 attach 的 pid。
|
|
225
440
|
*
|
|
@@ -425,5 +640,7 @@ export const Frida = {
|
|
|
425
640
|
runScript,
|
|
426
641
|
startActivityInsideApp,
|
|
427
642
|
findVisibleTextBounds,
|
|
643
|
+
findVisibleInputs,
|
|
644
|
+
setFocusedInputText,
|
|
428
645
|
resolvePid
|
|
429
646
|
};
|