@skrillex1224/android-toolkit 0.1.0 → 0.1.1
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 +203 -2
package/package.json
CHANGED
package/src/frida-client.js
CHANGED
|
@@ -74,7 +74,7 @@ export async function runScript(ctx, source, options = {}) {
|
|
|
74
74
|
maxLines: Number(options.maxLines || 1200),
|
|
75
75
|
outputPath: options.outputPath || ''
|
|
76
76
|
});
|
|
77
|
-
await recorder.waitForExit(Number(options.timeoutMs ||
|
|
77
|
+
await recorder.waitForExit(Number(options.timeoutMs || 10000));
|
|
78
78
|
const result = await recorder.stop();
|
|
79
79
|
const scriptEvents = result.events.filter((event) => event?.type !== 'parse_error');
|
|
80
80
|
if (scriptEvents.length === 0) {
|
|
@@ -187,7 +187,7 @@ Java.perform(function () {
|
|
|
187
187
|
var view = views[i];
|
|
188
188
|
if (!view.isShown()) continue;
|
|
189
189
|
var value = '';
|
|
190
|
-
try { value = String(Java.cast(view, TextView).getText()); } catch (_) { continue; }
|
|
190
|
+
try { value = String(Java.cast(view, TextView).getText().toString()); } catch (_) { continue; }
|
|
191
191
|
if (exact ? value === target : value.indexOf(target) >= 0) {
|
|
192
192
|
var rect = rectOf(view);
|
|
193
193
|
if (rect.right <= rect.left || rect.bottom <= rect.top) continue;
|
|
@@ -220,6 +220,205 @@ Java.perform(function () {
|
|
|
220
220
|
return event.matches || [];
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* 列出当前 App 内部可见输入框。
|
|
225
|
+
*
|
|
226
|
+
* 微信这类 App 往往不给系统 uiautomator 暴露节点,但 App 进程内仍能看到
|
|
227
|
+
* EditText。业务方可以用 hint/text/bounds 挑选输入框,再交给 Device.click
|
|
228
|
+
* 或 setFocusedInputText 使用。
|
|
229
|
+
*/
|
|
230
|
+
export async function findVisibleInputs(ctx, options = {}) {
|
|
231
|
+
const event = await runScript(ctx, `
|
|
232
|
+
Java.perform(function () {
|
|
233
|
+
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
234
|
+
function rectOf(view) {
|
|
235
|
+
var Rect = Java.use('android.graphics.Rect');
|
|
236
|
+
var rect = Rect.$new();
|
|
237
|
+
try { view.getGlobalVisibleRect(rect); } catch (_) { }
|
|
238
|
+
return { left: rect.left.value, top: rect.top.value, right: rect.right.value, bottom: rect.bottom.value };
|
|
239
|
+
}
|
|
240
|
+
function walk(view, out, depth) {
|
|
241
|
+
if (!view || depth > ${Number(options.maxDepth || 20)} || out.length > ${Number(options.maxNodes || 3000)}) return;
|
|
242
|
+
out.push(view);
|
|
243
|
+
try {
|
|
244
|
+
var ViewGroup = Java.use('android.view.ViewGroup');
|
|
245
|
+
var group = Java.cast(view, ViewGroup);
|
|
246
|
+
for (var i = 0; i < group.getChildCount(); i++) walk(group.getChildAt(i), out, depth + 1);
|
|
247
|
+
} catch (_) { }
|
|
248
|
+
}
|
|
249
|
+
function activeActivities() {
|
|
250
|
+
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
251
|
+
var Activity = Java.use('android.app.Activity');
|
|
252
|
+
var ArrayMap = Java.use('android.util.ArrayMap');
|
|
253
|
+
var thread = ActivityThread.currentActivityThread();
|
|
254
|
+
var field = thread.getClass().getDeclaredField('mActivities');
|
|
255
|
+
field.setAccessible(true);
|
|
256
|
+
var map = Java.cast(field.get(thread), ArrayMap);
|
|
257
|
+
var out = [];
|
|
258
|
+
for (var i = 0; i < map.size(); i++) {
|
|
259
|
+
var record = map.valueAt(i);
|
|
260
|
+
var recordClass = record.getClass();
|
|
261
|
+
var activityField = recordClass.getDeclaredField('activity');
|
|
262
|
+
var pausedField = recordClass.getDeclaredField('paused');
|
|
263
|
+
activityField.setAccessible(true);
|
|
264
|
+
pausedField.setAccessible(true);
|
|
265
|
+
if (!pausedField.getBoolean(record)) out.push(Java.cast(activityField.get(record), Activity));
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
Java.scheduleOnMainThread(function () {
|
|
270
|
+
try {
|
|
271
|
+
var EditText = Java.use('android.widget.EditText');
|
|
272
|
+
var inputs = [];
|
|
273
|
+
var activities = activeActivities();
|
|
274
|
+
for (var a = 0; a < activities.length; a++) {
|
|
275
|
+
var views = [];
|
|
276
|
+
walk(activities[a].getWindow().getDecorView(), views, 0);
|
|
277
|
+
for (var i = 0; i < views.length; i++) {
|
|
278
|
+
var view = views[i];
|
|
279
|
+
if (!view.isShown()) continue;
|
|
280
|
+
try {
|
|
281
|
+
var editText = Java.cast(view, EditText);
|
|
282
|
+
var rect = rectOf(view);
|
|
283
|
+
if (rect.right <= rect.left || rect.bottom <= rect.top) continue;
|
|
284
|
+
inputs.push({
|
|
285
|
+
text: String(editText.getText().toString()),
|
|
286
|
+
hint: String(editText.getHint() ? editText.getHint().toString() : ''),
|
|
287
|
+
className: String(view.getClass().getName()),
|
|
288
|
+
idName: (function () { try { return String(view.getResources().getResourceName(view.getId())); } catch (_) { return ''; } })(),
|
|
289
|
+
focused: Boolean(view.hasFocus()),
|
|
290
|
+
bounds: rect
|
|
291
|
+
});
|
|
292
|
+
} catch (_) { }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
emit({ ok: true, inputs: inputs });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
`, {
|
|
302
|
+
label: options.label || 'find-visible-inputs',
|
|
303
|
+
packageName: options.packageName || ctx.packageName,
|
|
304
|
+
timeoutMs: options.timeoutMs || 7000
|
|
305
|
+
});
|
|
306
|
+
if (!event.ok) {
|
|
307
|
+
throw new CrawlerError({
|
|
308
|
+
message: `automation_failed: findVisibleInputs failed ${event.error || ''}`.trim(),
|
|
309
|
+
code: Code.AutomationFailed,
|
|
310
|
+
context: event
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return event.inputs || [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 给当前获得焦点的 EditText 设置文本,并触发常规输入事件。
|
|
318
|
+
*
|
|
319
|
+
* 这比 adb input text 更适合中文,也比直接 setText 更稳:它先 focus 目标
|
|
320
|
+
* EditText,再用 Editable.replace 更新文本,最后把光标放到末尾。
|
|
321
|
+
*/
|
|
322
|
+
export async function setFocusedInputText(ctx, text, options = {}) {
|
|
323
|
+
const event = await runScript(ctx, `
|
|
324
|
+
Java.perform(function () {
|
|
325
|
+
function emit(payload) { console.log('ANDROID_TOOLKIT_SCRIPT_JSON ' + JSON.stringify(payload)); }
|
|
326
|
+
function rectOf(view) {
|
|
327
|
+
var Rect = Java.use('android.graphics.Rect');
|
|
328
|
+
var rect = Rect.$new();
|
|
329
|
+
try { view.getGlobalVisibleRect(rect); } catch (_) { }
|
|
330
|
+
return { left: rect.left.value, top: rect.top.value, right: rect.right.value, bottom: rect.bottom.value };
|
|
331
|
+
}
|
|
332
|
+
function walk(view, out, depth) {
|
|
333
|
+
if (!view || depth > ${Number(options.maxDepth || 20)} || out.length > ${Number(options.maxNodes || 3000)}) return;
|
|
334
|
+
out.push(view);
|
|
335
|
+
try {
|
|
336
|
+
var ViewGroup = Java.use('android.view.ViewGroup');
|
|
337
|
+
var group = Java.cast(view, ViewGroup);
|
|
338
|
+
for (var i = 0; i < group.getChildCount(); i++) walk(group.getChildAt(i), out, depth + 1);
|
|
339
|
+
} catch (_) { }
|
|
340
|
+
}
|
|
341
|
+
function activeActivities() {
|
|
342
|
+
var ActivityThread = Java.use('android.app.ActivityThread');
|
|
343
|
+
var Activity = Java.use('android.app.Activity');
|
|
344
|
+
var ArrayMap = Java.use('android.util.ArrayMap');
|
|
345
|
+
var thread = ActivityThread.currentActivityThread();
|
|
346
|
+
var field = thread.getClass().getDeclaredField('mActivities');
|
|
347
|
+
field.setAccessible(true);
|
|
348
|
+
var map = Java.cast(field.get(thread), ArrayMap);
|
|
349
|
+
var out = [];
|
|
350
|
+
for (var i = 0; i < map.size(); i++) {
|
|
351
|
+
var record = map.valueAt(i);
|
|
352
|
+
var recordClass = record.getClass();
|
|
353
|
+
var activityField = recordClass.getDeclaredField('activity');
|
|
354
|
+
var pausedField = recordClass.getDeclaredField('paused');
|
|
355
|
+
activityField.setAccessible(true);
|
|
356
|
+
pausedField.setAccessible(true);
|
|
357
|
+
if (!pausedField.getBoolean(record)) out.push(Java.cast(activityField.get(record), Activity));
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
Java.scheduleOnMainThread(function () {
|
|
362
|
+
try {
|
|
363
|
+
var EditText = Java.use('android.widget.EditText');
|
|
364
|
+
var targetText = ${JSON.stringify(String(text || ''))};
|
|
365
|
+
var activities = activeActivities();
|
|
366
|
+
var chosen = null;
|
|
367
|
+
var fallback = null;
|
|
368
|
+
for (var a = 0; a < activities.length; a++) {
|
|
369
|
+
var views = [];
|
|
370
|
+
walk(activities[a].getWindow().getDecorView(), views, 0);
|
|
371
|
+
for (var i = 0; i < views.length; i++) {
|
|
372
|
+
var view = views[i];
|
|
373
|
+
if (!view.isShown()) continue;
|
|
374
|
+
try {
|
|
375
|
+
var editText = Java.cast(view, EditText);
|
|
376
|
+
var rect = rectOf(view);
|
|
377
|
+
if (rect.right <= rect.left || rect.bottom <= rect.top) continue;
|
|
378
|
+
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()) };
|
|
379
|
+
if (item.focused) chosen = item;
|
|
380
|
+
if (!fallback) fallback = item;
|
|
381
|
+
} catch (_) { }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
chosen = chosen || fallback;
|
|
385
|
+
if (!chosen) {
|
|
386
|
+
emit({ ok: false, error: 'visible EditText not found' });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
var JavaString = Java.use('java.lang.String');
|
|
390
|
+
var BufferType = Java.use('android.widget.TextView$BufferType');
|
|
391
|
+
var SpannableStringBuilder = Java.use('android.text.SpannableStringBuilder');
|
|
392
|
+
chosen.view.requestFocus();
|
|
393
|
+
chosen.view.setText(SpannableStringBuilder.$new(JavaString.$new(targetText)), BufferType.EDITABLE.value);
|
|
394
|
+
chosen.view.setSelection(targetText.length);
|
|
395
|
+
emit({
|
|
396
|
+
ok: true,
|
|
397
|
+
text: targetText,
|
|
398
|
+
hint: String(chosen.view.getHint() ? chosen.view.getHint().toString() : ''),
|
|
399
|
+
className: chosen.className,
|
|
400
|
+
bounds: chosen.bounds
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
emit({ ok: false, error: String(error), stack: String(error.stack || '') });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
`, {
|
|
408
|
+
label: options.label || 'set-focused-input-text',
|
|
409
|
+
packageName: options.packageName || ctx.packageName,
|
|
410
|
+
timeoutMs: options.timeoutMs || 7000
|
|
411
|
+
});
|
|
412
|
+
if (!event.ok) {
|
|
413
|
+
throw new CrawlerError({
|
|
414
|
+
message: `automation_failed: setFocusedInputText failed ${event.error || ''}`.trim(),
|
|
415
|
+
code: Code.AutomationFailed,
|
|
416
|
+
context: event
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return event;
|
|
420
|
+
}
|
|
421
|
+
|
|
223
422
|
/**
|
|
224
423
|
* 找到目标包名当前可 attach 的 pid。
|
|
225
424
|
*
|
|
@@ -425,5 +624,7 @@ export const Frida = {
|
|
|
425
624
|
runScript,
|
|
426
625
|
startActivityInsideApp,
|
|
427
626
|
findVisibleTextBounds,
|
|
627
|
+
findVisibleInputs,
|
|
628
|
+
setFocusedInputText,
|
|
428
629
|
resolvePid
|
|
429
630
|
};
|