Skip to content

feat(android): 安卓端增加摇一摇自动记账功能#321

Open
wait-more wants to merge 1 commit into
TNT-Likely:mainfrom
wait-more:feature/shake-billing
Open

feat(android): 安卓端增加摇一摇自动记账功能#321
wait-more wants to merge 1 commit into
TNT-Likely:mainfrom
wait-more:feature/shake-billing

Conversation

@wait-more

Copy link
Copy Markdown
Contributor

变更类型

  • 新功能
  • Bug 修复
  • 文档更新
  • 代码重构
  • 性能优化
  • 其他

变更说明

当前安卓端缺少一种更无感或更自动化的记账方式。现有方案需手动截屏(电源键+音量键)触发相册监听,操作路径长且截图残留系统相册难以清理。某些品牌ROM甚至需要截图后马上切入app,app才能识别到新截图再触发流程,极大影响用户体验。Android 缺少 iOS Shortcuts + Back Tap 这类系统级快捷记账能力,因此借助 AccessibilityService + 加速度传感器自建一条不依赖相册的独立通路。

本功能优势

  • 任意界面触发:摇一摇即可自动截图记账,不受 App 页面层级限制,任何界面均可触发
  • 无需前台运行:功能开启后,无需 App 在前台也可正常触发,不打断当前操作
  • 不留痕迹:截图存 App 私有目录,记账完毕自动删除,不占用相册空间,无需存储权限
  • 双路径覆盖:API 31+ 走系统截图 API → 视觉 AI;API 30- 降级为节点文本提取 → 文字 AI
  • 抗误触:ShakeDetector v2 基于加速度 jerk + 峰值节律分析,日常携带不误触
  • 稳定保活:五层保活体系(前台服务 + AlarmManager + JobScheduler + WakeLock + 独立 Engine)保障后台稳定运行,开机自启无需每次手动开启

实现内容

  • Android 原生层新增 BillingAccessibilityService(无障碍服务)、ShakeDetector v2(加速度检测)、ScreenshotController(三级降级截屏)、AccessibilityBridge(Engine 共享单例)、KeepAliveService / KeepAliveJobService / BootReceiver(五层保活)
  • Flutter 层 ScreenshotMonitorService 增加无障碍监听与顺序处理队列;AutoBillingService 重构至 smart_billing/ 目录,统一摇一摇/截屏/iOS 三个入口;新增摇一摇设置页与保活状态管理;启动时自动恢复截图、摇一摇、保活状态;全局异常写入崩溃日志
  • 删除旧 automation/ 目录,迁移至 smart_billing/

说明①:AI 智能识别记账(截图 → OCR → 语义解析 → 自动填单)依赖已有 AutoBillingService 及 AI Kit 管道实现,非本次 PR 新增。本期聚焦打通 Android 侧「摇一摇 → 截屏 → 投递」链路与服务稳定性,识别侧仅做适配重构,无算法改动。

说明②:「自动检测支付页」未在本期实现。原因:支付页检测需持续监听前台页面变化,且支付宝/微信等页面在不同版本间布局波动频繁,包名白名单 + 关键词匹配误触率高、维护成本大,且不同APP需做不同适配。本次实现的摇一摇自动记账功能可在任意界面触发,后续AI识别流程会先检查是否有账单,再识别具体的账单。

相关 Issue

无对应 issue。

测试情况

  • 已在 Android 上测试
  • 已在 iOS 上测试
  • 添加了单元测试
  • 添加了集成测试

截图(如适用)

Uploading Screenshot_20260609_155600_com_tntlikely_beecount_dev_MainActivity.jpg…

检查清单

  • 代码遵循项目规范
  • 已运行 dart format 格式化代码
  • 已运行 flutter analyze 无警告
  • 已更新相关文档(设计文档更新至 v2.0)
  • 提交信息符合规范

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../providers.dart';

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不要移动文件

/// 默认模板。强制 JSON 数组 + 完整字段说明 + 多笔示例。
/// 默认模板。先判断是否为账单,再强制 JSON 数组 + 完整字段说明 + 多笔示例。
static const String defaultTemplate =
'''{{INPUT_SOURCE}}提取记账信息,返回JSON数组。

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不相关的不要一起带进来 这种可以走独立pr

@TNT-Likely

TNT-Likely commented Jun 10, 2026

Copy link
Copy Markdown
Owner

@wait-more 思路我认为没问题,但是一直不做无障碍的原因是不符合无障碍这个功能的本意+会被拒审。所以这里要区分环境,google play这种场景肯定是无法过审的。

另外当前pr夹杂了一些不相关的内容,建议分批次提pr,目前的review难度有点大。

@@ -0,0 +1,615 @@
# 摇一摇自动记账方案

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

设计文档也不要带进来了

@wait-more

Copy link
Copy Markdown
Contributor Author

@wait-more 思路我认为没问题,但是一直不做无障碍的原因是不符合无障碍这个功能的本意+会被拒审。所以这里要区分环境,google play这种场景肯定是无法过审的。

另外当前pr夹杂了一些不相关的内容,建议分批次提pr,目前的review难度有点大。

好的,我先拆分和简化一下

@wait-more wait-more force-pushed the feature/shake-billing branch 2 times, most recently from 038b888 to f4fc7d2 Compare June 11, 2026 03:17
- 摇一摇检测:ShakeDetector 加速度传感器,连续峰值算法判定摇动动作
- 无障碍截屏:BillingAccessibilityService 接管截屏,API 31+ 截屏文件 / API 30- 节点文本双路径
- 五层保活:前台服务+通知栏+JobScheduler+广播唤醒+相互唤醒
- 设置页:无障碍引导、保活状态/电池优化/自启动检查、通知渠道横幅检测
- 启动恢复:应用重启后自动恢复摇一摇与保活状态
- 通知优化:processScreenshot 支持 showProgressNotifications 参数控制中间通知;
  结果通知优先走原生通道,降级 Flutter 通知 + SP 兜底;
  超时后等待 AI 延迟响应,通知追加待处理计数
@wait-more wait-more force-pushed the feature/shake-billing branch from f4fc7d2 to 3e6bdb7 Compare June 11, 2026 03:55
@wait-more

Copy link
Copy Markdown
Contributor Author

@TNT-Likely 作者大佬,最新的提交已完成功能拆分和代码精简,当前剩的基本都是和摇一摇自动记账功能相关的代码了,你看下呢

@TNT-Likely

TNT-Likely commented Jun 11, 2026

Copy link
Copy Markdown
Owner

@wait-more

首先认真说一句:这个 PR 的工程投入和完成度都看得出来——ShakeDetector 的峰值节律抗误触、顺序处理队列、原生超时兜底、Engine 死活检测,这些设计都有想法,文档注释也很扎实。正因为如此,更要把结论和理由完整地写清楚。

结论:这个方向我们在 2026-05-30 和 06-04 做过两轮专门评审,最终搁置了,这个 PR 暂时不能合入主干。 决定性原因不在代码,在平台:

  1. Android 16「高级保护模式」/ Android 17 默认策略会在 OS 层吊销非无障碍工具类应用的无障碍授权——记账 app 不在豁免范围。即使用户装好开好,系统也会静默关闭服务,这与审核宽严无关,是平台物理关闭这条路。
  2. Google Play 政策:无障碍 API 的允许用途明文排除 monitoring/automation;isAccessibilityTool=true 对记账 app 属虚假声明,=false 则要过"显著告知 + 声明表 + 演示视频"且大概率被"应使用更窄 API"驳回;2025-10-30 起还新增了"禁止自主执行动作"的禁令。
  3. 这个 PR 还叠加了几个 Play 高危项,任何一项都可能单独触发驳回:USE_FULL_SCREEN_INTENT(政策仅限通话/闹钟类)、通知使用 CATEGORY_ALARM + setBypassDndonTaskRemoved 自拉活 + AlarmManager 二次拉起这类对抗系统清理的设计。

PR 目前没有任何渠道门控,功能会进所有 build 包括 Play 包。如果未来要做这个功能,前提是接受「国内渠道 flavor 独占 + Play 包完全剥离无障碍服务声明」的双 build 方案——这是产品层面的决策,不是代码改一改能绕过的。

——

除平台问题外,几个架构/正确性问题也记录在这里,部分对你后续拆分 PR 有用:

双 FlutterEngine 跑同一个 main()(最关键的技术问题)

  • KeepAliveService.ensureFlutterEngine 创建的后台引擎入口是 main,等于完整跑一遍 app 引导:第二个数据库连接、全套 provider、runApp 和所有启动副作用都是双份;
  • Drift 的流失效是 per-isolate 的:App 在前台开着时摇一摇(注释写明"始终走 backgroundEngine 路径"),交易由后台 isolate 落库,前台 UI 不会刷新,要重启才能看到新账;
  • 惯例做法是注册轻量的 @pragma('vm:entry-point') 独立入口只起记账管线(参考 workmanager 类插件),而不是执行 main
  • 另外 MainActivity 的引擎缓存(shouldDestroyEngineWithHost=false)对所有用户生效,但 engineHasBeenUsed 的处理让每次 Activity 重建都 destroy+重建引擎,缓存实际从未被复用。

正确性问题

  • checkFlutterSharedPreferencesHealth 会在文件 >1MB 时静默删除整个 SharedPreferences——用户主题、皮肤、全部开关无确认清空,这种破坏性兜底不能进主干;
  • "AlarmManager 1 分钟心跳"实际是一次性闹钟:setExactAndAllowWhileIdle 是 one-shot,ACTION_HEARTBEAT 分支没有重排下一次,心跳只响一次;且 allow-while-idle 在 Doze 下被系统限频到约 9 分钟一次,1 分钟的设计本身落不了地;
  • API 31+ 截图主路径疑似从未生效:Bitmap.wrapHardwareBuffer 得到的 HARDWARE bitmap 直接 compress() 在多数版本会抛像素不可访问异常,被 catch 后永远降级到文本提取。常规做法先 copy(ARGB_8888, false)。如果你真机上图片路径跑通了,麻烦说明机型和系统版本;
  • _handleDelayedResponse 里统计刷新和 PostProcessor 包在 if (showNotification) 内,不显示通知的路径下交易落库了但不触发同步;
  • 10 分钟 WakeLock 对前台服务无作用、纯耗电;加速度监听熄屏不注销;ensureProgressChannelHigh 删渠道重建来强提 importance,在原生系统上无效(已删渠道会恢复用户原设置)。

对既有功能的外溢

  • _showFinalNotification 重写后,既有的截图监听和 iOS 快捷指令路径的最终通知也改走 shake_result 渠道并强制震动响铃,改变了老用户行为;原来方法里关于 iOS banner 行为的关键注释被删除;
  • runZonedGuardedrunAppensureInitialized 在 zone 外,debug 下会报 zone mismatch,Flutter 3.10+ 推荐 PlatformDispatcher.instance.onError
  • 项目规范:日志一律走 LoggerService(不用 print),用户可见文案需走 i18n(PR 里有多处硬编码中文)。

流程

  • PR 描述与 diff 不符(描述说迁移到 smart_billing/ 并删除 automation/,实际目录未动);截图链接是未完成的占位;没有单元测试。

——

可以继续的方向

  1. ShakeDetector(峰值节律算法)和顺序处理队列本身没有政策风险,欢迎拆成独立小 PR,将来接到合规的触发源上;
  2. 智谱超时、_handleDelayedResponse 这类与无障碍无关的健壮性改进也欢迎单独提;
  3. 如果你对双 build / 国内渠道独占方案有兴趣,可以先开 issue 讨论方案设计,确认边界后再动代码。

再次感谢投入,这不是对工作量的否定,是平台把这条路焊死了——希望上面的技术记录对后续拆分有帮助。

@TNT-Likely

Copy link
Copy Markdown
Owner

@wait-more 补充一下上面说的「渠道门控」具体怎么做——项目里已经有现成机制(截屏自动记账就是这样在 Play 版被剥离的),照着接即可:

项目现状:同一个 tag,CI 构建两种产物.github/workflows/release.yml

渠道 构建命令 分发 功能
APK 渠道(GitHub Release 直装) flutter build apk --flavor prod不带 GOOGLE_PLAY define) GitHub Release 多 ABI APK 全量
Google Play 渠道 flutter build appbundle --flavor prod --dart-define=GOOGLE_PLAY=true AAB 上传 Play Console 剥离政策受限功能

三层门控,从外到内:

1. Dart 层 — 编译期常量(参考 lib/services/platform/screenshot_monitor_service.dart

const _isGooglePlayBuild = bool.fromEnvironment('GOOGLE_PLAY', defaultValue: false);
  • 功能层:服务入口直接短路(screenshot_monitor_service.dart:51if (_isGooglePlayBuild) return false;
  • UI 层:设置页隐藏入口(smart_billing_page.dart:250if (!(Platform.isAndroid && _isGooglePlayBuild))

因为是 const,tree-shaking 会把 Play 包里的整条死代码路径剥掉,不只是运行时关闭。摇一摇的开关页、ShakeBillingService 入口都应套这一层。

2. 原生 Manifest 层 — 构建时 overlay 剥离(无障碍场景的关键一层)

光 Dart 门控不够:manifest 里只要存在 <service android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> 声明,Play 审核就会触发无障碍声明流程,跟代码跑不跑无关。现有做法是 AAB 构建前由 CI 动态生成 android/app/src/prod/AndroidManifest.xml overlay,用 tools:node="remove" 删掉受限项(见 release.yml 的 "Build App Bundle" 步骤,目前已经在剥 READ_MEDIA_* 等权限)。无障碍版本需要在同一个 overlay 里追加:

<service android:name=".BillingAccessibilityService" tools:node="remove" />
<service android:name=".KeepAliveService" tools:node="remove" />
<service android:name=".KeepAliveJobService" tools:node="remove" />
<receiver android:name=".BootReceiver" tools:node="remove" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" tools:node="remove" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" tools:node="remove" />

Kotlin 类本身可以留在包里(没有 manifest 声明系统就不会绑定它),但权限和四大组件声明必须从 Play 包的 merged manifest 里消失。构建完 CI 会 rm -rf android/app/src/prod 清理现场,不影响 APK 渠道构建。

3. 体验层 — 运行时兜底提示

APK 渠道用户从 Play 重装/迁移过来时设置项会"消失",开关页最好放一行说明(如"此功能仅 GitHub 直装版提供"),避免被当成 bug 反馈。

验证方式:构建 AAB 后用 bundletool build-apks + aapt dump xmltree 确认 merged manifest 里无 BIND_ACCESSIBILITY_SERVICEaccessibility_service_config 引用;APK 渠道包则反向确认功能完整。

——

即使做了以上门控,Android 16/17 的 OS 级吊销问题对 APK 渠道用户依然存在(高级保护模式下系统会自动关闭非辅助工具的无障碍授权),这是功能本身要在开启引导里向用户说明的限制。所以整体路径是:先开 issue 把双 build 方案 + 双引擎架构问题的解法确认下来,再动手改,避免再返工一轮。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants