package com.beancount.mobile.screenshot import android.content.Context import android.database.ContentObserver import android.net.Uri import android.os.Handler import android.os.Looper import android.provider.MediaStore import android.util.Log /** * 截图监听 ContentObserver(plan.md「3.13 截图自动记账通道」)。 * * 参考 BeeCount 的 ScreenshotObserver.kt: * - 监听 MediaStore.Images.Media.EXTERNAL_CONTENT_URI 变化 * - 关键词匹配(screenshot/截屏/截图/screen_shot) * - 30 秒时间窗(过滤旧截图) * - processedPaths 去重(最多 200 条) * - 过滤小米 .pending- 临时文件 * - 500ms 防抖 * * 由 Config Plugin 注册,触发后通过 DeviceEventEmitter 推送到 JS 层 ScreenshotChannel。 * * ⚠️ 与 src/domain/constants.ts 同步:TIME_WINDOW_MS、MAX_PROCESSED、SCREENSHOT_KEYWORDS * 修改时需两边同时更新。 */ class ScreenshotObserver( private val context: Context, private val handler: Handler = Handler(Looper.getMainLooper()), private val onScreenshot: (uri: Uri) -> Unit, ) : ContentObserver(handler) { companion object { private const val TAG = "ScreenshotObserver" private const val TIME_WINDOW_MS = 30_000L private const val MAX_PROCESSED = 200 private val SCREENSHOT_KEYWORDS = listOf("screenshot", "截屏", "截图", "screen_shot", "Screenshot") } private val resolver = context.contentResolver private val processedPaths = LinkedHashSet() @Volatile private var lastCheckTime = System.currentTimeMillis() /** 注册监听。 */ fun register() { resolver.registerContentObserver( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, this, ) Log.i(TAG, "截图监听已注册") } /** 注销监听。 */ fun unregister() { resolver.unregisterContentObserver(this) Log.i(TAG, "截图监听已注销") } override fun onChange(selfChange: Boolean, uri: Uri?) { super.onChange(selfChange, uri) uri ?: return handler.post { processNewScreenshot(uri) } } private fun processNewScreenshot(uri: Uri) { try { // 查询截图信息 val projection = arrayOf( MediaStore.Images.Media.DATA, MediaStore.Images.Media.DATE_ADDED, MediaStore.Images.Media.DISPLAY_NAME, ) resolver.query(uri, projection, null, null, null)?.use { cursor -> if (!cursor.moveToFirst()) return val path = cursor.getString(0) ?: "" val dateAdded = cursor.getLong(1) val displayName = cursor.getString(2) ?: "" // 1. 时间窗过滤(30 秒内) val now = System.currentTimeMillis() / 1000 if (now - dateAdded > TIME_WINDOW_MS / 1000) { Log.d(TAG, "忽略旧截图(超过30秒): $displayName") return } // 2. 关键词匹配(必须是截图) if (!SCREENSHOT_KEYWORDS.any { kw -> displayName.contains(kw, true) || path.contains(kw, true) }) { return } // 3. 过滤小米 .pending- 临时文件 if (path.endsWith(".pending-") || path.contains(".pending-")) { Log.d(TAG, "过滤小米 pending 临时文件: $displayName") return } // 4. 去重 if (processedPaths.contains(path)) return processedPaths.add(path) if (processedPaths.size > MAX_PROCESSED) { val it = processedPaths.iterator() if (it.hasNext()) { it.next() it.remove() } } Log.i(TAG, "检测到新截图: $displayName") onScreenshot(uri) } } catch (e: Exception) { Log.e(TAG, "处理截图异常: ${e.message}", e) } } }