@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/frida-client.js +203 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skrillex1224/android-toolkit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -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 || 7000));
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
  };