package com.beancount.mobile.accessibility import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.Promise import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.bridge.ReadableArray /** * 无障碍服务 RN 桥接模块(plan.md「3.6 无障碍服务」JS 接线)。 * * BillingAccessibilityService 是 AccessibilityService 子类(非 RN 模块), * 其方法无法直接从 JS 调用。本模块作为中间层,通过 instance 静态引用 * 把 JS 调用委托给服务实例。 * * 由 Config Plugin 的 withMainApplication 注入 add(AccessibilityBridgePackage())。 */ class AccessibilityBridgeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { init { ReactContextHolder.context = reactContext } override fun invalidate() { super.invalidate() if (ReactContextHolder.context === reactContext) { ReactContextHolder.context = null } } override fun getName() = "AccessibilityBridge" /** 无障碍服务是否已连接(用户已在系统设置中启用)。 */ @ReactMethod fun isServiceRunning(promise: Promise) { promise.resolve(BillingAccessibilityService.instance != null) } /** * 记住当前页面:把当前顶部 App 的 pkg|activity 加入白名单, * 之后该页面内容变化时自动截图 → OCR。 */ @ReactMethod fun rememberCurrentPage(promise: Promise) { val service = BillingAccessibilityService.instance if (service == null) { promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") return } val pkg = service.getTopPackage() if (pkg == null) { promise.reject("NO_TOP_PACKAGE", "当前没有检测到前台 App") return } service.rememberCurrentPage() val result = WritableNativeMap() result.putString("package", pkg) result.putString("activity", service.getTopActivity() ?: "") result.putString("signature", "$pkg|${service.getTopActivity() ?: ""}") promise.resolve(result) } /** 手动触发一次 OCR(截取当前屏幕并发送给 JS 层处理)。 */ @ReactMethod fun triggerManualOcr(promise: Promise) { val service = BillingAccessibilityService.instance if (service == null) { promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") return } try { service.triggerManualOcr() promise.resolve(true) } catch (e: Exception) { promise.reject("OCR_TRIGGER_FAIL", e.message) } } /** 获取所有已记住的页面签名列表。 */ @ReactMethod fun getPageSignatures(promise: Promise) { val service = BillingAccessibilityService.instance val sigsSet = if (service != null) { service.getPageSignatures() } else { try { val prefs = reactContext.getSharedPreferences("billing_accessibility_prefs", android.content.Context.MODE_PRIVATE) prefs.getStringSet("page_signatures", emptySet()) ?: emptySet() } catch (e: Exception) { emptySet() } } val arr = WritableNativeArray() for (sig in sigsSet) { val parts = sig.split("|", limit = 2) val map = WritableNativeMap() map.putString("signature", sig) map.putString("package", parts.getOrNull(0) ?: "") map.putString("activity", parts.getOrNull(1) ?: "") arr.pushMap(map) } promise.resolve(arr) } /** 清空所有已记住的页面签名。 */ @ReactMethod fun clearPageSignatures(promise: Promise) { val service = BillingAccessibilityService.instance if (service != null) { service.clearPageSignatures() } else { try { val prefs = reactContext.getSharedPreferences("billing_accessibility_prefs", android.content.Context.MODE_PRIVATE) prefs.edit().putStringSet("page_signatures", emptySet()).apply() } catch (e: Exception) { promise.reject("CLEAR_PREFS_FAIL", e.message) return } } promise.resolve(true) } /** 删除指定页面签名。 */ @ReactMethod fun removePageSignature(signature: String, promise: Promise) { val service = BillingAccessibilityService.instance if (service != null) { service.removePageSignature(signature) } else { try { val prefs = reactContext.getSharedPreferences("billing_accessibility_prefs", android.content.Context.MODE_PRIVATE) val saved = prefs.getStringSet("page_signatures", emptySet()) ?: emptySet() val mutable = HashSet(saved) if (mutable.remove(signature)) { prefs.edit().putStringSet("page_signatures", mutable).apply() } } catch (e: Exception) { promise.reject("REMOVE_PREFS_FAIL", e.message) return } } promise.resolve(true) } /** 获取支付 App 白名单(供 JS 端展示)。 */ @ReactMethod fun getPaymentPackages(promise: Promise) { val arr = WritableNativeArray() for (pkg in BillingAccessibilityService.PAYMENT_PACKAGES) { arr.pushString(pkg) } promise.resolve(arr) } /** 获取当前顶部 App 信息(供 JS 判断用户是否在支付页面)。 */ @ReactMethod fun getTopApp(promise: Promise) { val service = BillingAccessibilityService.instance if (service == null) { promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") return } val map = WritableNativeMap() map.putString("package", service.getTopPackage() ?: "") map.putString("activity", service.getTopActivity() ?: "") promise.resolve(map) } /** 将当前应用拉起至前台,用以在后台识别出账单后,弹窗让用户进行交易确认 */ @ReactMethod fun bringAppToForeground(promise: Promise) { val service = BillingAccessibilityService.instance if (service == null) { promise.reject("SERVICE_NOT_RUNNING", "无障碍服务未启用") return } try { service.bringAppToForeground() promise.resolve(true) } catch (e: Exception) { promise.reject("FAIL", e.message) } } /** 显示账单浮窗(直接在当前其他应用上方渲染,不返回 App 内) */ @ReactMethod fun showFloatingBill( amount: String, merchant: String, time: String, packageName: String, categories: ReadableArray, accounts: ReadableArray, direction: String, draftId: String, promise: Promise ) { val context = reactContext.currentActivity ?: BillingAccessibilityService.instance if (context == null) { promise.reject("NO_CONTEXT", "无法获取当前前台 Activity 或 AccessibilityService 实例") return } val categoryList = mutableListOf>() for (i in 0 until categories.size()) { val map = categories.getMap(i) categoryList.add(mapOf( "id" to (map?.getString("id") ?: ""), "name" to (map?.getString("name") ?: ""), "account" to (map?.getString("account") ?: ""), "type" to (map?.getString("type") ?: "") )) } val accountList = mutableListOf() for (i in 0 until accounts.size()) { accountList.add(accounts.getString(i) ?: "") } android.os.Handler(android.os.Looper.getMainLooper()).post { try { val floatingView = FloatingBillView(context, draftId, amount, merchant, time, packageName, categoryList, accountList, direction) floatingView.show() promise.resolve(true) } catch (e: Exception) { promise.reject("FAIL", e.message) } } } /** 设置悬浮球开关状态。 */ @ReactMethod fun setFloatingBallEnabled(enabled: Boolean, promise: Promise) { BillingAccessibilityService.floatingBallEnabled = enabled val service = BillingAccessibilityService.instance if (service == null) { promise.resolve(true) return } if (!enabled) { service.dismissFloatingHelper() } else { service.refreshFloatingHelper() } promise.resolve(true) } }