fix: harden OriginOS recognition tracking

This commit is contained in:
2026-08-31 11:29:04 +08:00
parent 9ccbdfead4
commit 477f83c5b1
17 changed files with 980 additions and 108 deletions
@@ -27,12 +27,15 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
Timer? _diagnosticRefreshTimer;
Timer? _previewExpiryTimer;
bool _diagnosticRefreshInFlight = false;
bool _connectionCheckInFlight = false;
bool _keepAliveStarting = false;
bool _recentsProtectionNoticeChecked = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_check(waitForConnection: true);
_check();
_diagnosticRefreshTimer = Timer.periodic(
const Duration(seconds: 2),
(_) => _refreshRunningDiagnostic(),
@@ -60,18 +63,16 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
void _refreshRunningDiagnostic() {
if (!mounted ||
_diagnosticRefreshInFlight ||
_status?.latestDiagnostic?.result != 'started') {
!{'started', 'waiting'}.contains(_status?.latestDiagnostic?.result)) {
return;
}
_diagnosticRefreshInFlight = true;
_check().whenComplete(() => _diagnosticRefreshInFlight = false);
}
Future<void> _check({bool waitForConnection = false}) async {
Future<void> _check({bool monitorConnection = true}) async {
try {
var status = waitForConnection
? await ScreenshotChannel.waitForAccessibilityConnection()
: await ScreenshotChannel.recognitionStatus();
var status = await ScreenshotChannel.recognitionStatus();
final invalid = <String>[
if (status.accessibilityEvents &&
(!status.accessibilityAuthorized ||
@@ -99,6 +100,14 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
_error = null;
});
_schedulePreviewExpiry(status);
unawaited(_showRecentsProtectionNotice(status));
if (monitorConnection &&
status.accessibilityAuthorized &&
!status.accessibilityConnected &&
!status.taskCleanerRecoveryNeeded &&
(status.accessibilityEvents || status.aiScreenshot)) {
unawaited(_monitorAccessibilityConnection());
}
} catch (_) {
if (!mounted) return;
setState(() {
@@ -108,6 +117,49 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
}
}
Future<void> _showRecentsProtectionNotice(RecognitionStatus status) async {
if (_recentsProtectionNoticeChecked || !status.recentsProtectionActive) {
return;
}
_recentsProtectionNoticeChecked = true;
final prefs = await SharedPreferences.getInstance();
const key = 'originos_recents_protection_notice_v1';
if (prefs.getBool(key) == true || !mounted) return;
await prefs.setBool(key, true);
if (!mounted) return;
_showMessage('记之已从最近任务隐藏,可从桌面图标重新打开;关闭全部识别后恢复。');
}
Future<void> _monitorAccessibilityConnection({
bool ensureKeepAlive = false,
}) async {
if (_connectionCheckInFlight) return;
_connectionCheckInFlight = true;
if (mounted) setState(() {});
try {
if (ensureKeepAlive) {
_keepAliveStarting = true;
if (mounted) setState(() {});
await ScreenshotChannel.ensureRecognitionKeepAlive();
}
await ScreenshotChannel.waitForAccessibilityConnection();
final status = await ScreenshotChannel.recognitionStatus();
if (!mounted) return;
setState(() {
_status = status;
_error = null;
});
_schedulePreviewExpiry(status);
} catch (_) {
if (!mounted) return;
setState(() => _error = '连接状态复核失败,请重试');
} finally {
_connectionCheckInFlight = false;
_keepAliveStarting = false;
if (mounted) setState(() {});
}
}
void _schedulePreviewExpiry(RecognitionStatus status) {
_previewExpiryTimer?.cancel();
final expiresAt = status.ocrDiagnosticPreviewExpiresAt;
@@ -182,7 +234,7 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
Future<void> _resumeAuthorization() async {
if (_authorizing) return;
await _check(waitForConnection: true);
await _check();
final key = _pendingAuthorizationKey;
final status = _status;
if (key == null || status == null) return;
@@ -433,12 +485,31 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
'仅在微信和支付宝疑似支付流程中读取可见文字,并按需在内存中进行本地截图 OCR;不保存图片、完整控件树,也不监听按键。',
authorized: status!.accessibilityAuthorized,
connected: status.accessibilityConnected,
statusLabel: status.accessibilityConnectionLabel,
statusLabel:
_connectionCheckInFlight && !status.accessibilityConnected
? '正在连接'
: status.accessibilityConnectionLabel,
recoveryMessage: status.accessibilityNeedsRecovery
? status.taskCleanerRecoveryNeeded
? 'OriginOS 清理了识别进程,系统仍保留授权,但无障碍服务已被标记故障。'
: '系统仍显示已授权,但 OriginOS 未重新绑定服务。请前往系统设置,将记之无障碍服务关闭后重新开启。'
: (status.accessibilityEvents || status.aiScreenshot) &&
status.keepAliveNeedsRecovery
? _keepAliveStarting
? '正在启动后台保护…'
: status.keepAliveError ?? '后台保护未运行,划掉应用后识别可能中断。'
: null,
onOpenSettings: ScreenshotChannel.openAccessibilitySettings,
onRetry:
status.accessibilityAuthorized &&
!status.accessibilityConnected
? () => _check(waitForConnection: true)
settingsLabel: '前往无障碍设置',
onRetry: status.accessibilityNeedsRecovery
? () => _monitorAccessibilityConnection()
: null,
onStartKeepAlive:
(status.accessibilityEvents || status.aiScreenshot) &&
status.keepAliveNeedsRecovery &&
!_keepAliveStarting
? () =>
_monitorAccessibilityConnection(ensureKeepAlive: true)
: null,
child: Column(
children: [
@@ -546,7 +617,12 @@ class _ScreenshotSettingsPageState extends State<ScreenshotSettingsPage>
status.notificationEvents ||
status.aiScreenshot) ...[
const SizedBox(height: 10),
_BackgroundKeepAliveCard(status: status),
_BackgroundKeepAliveCard(
status: status,
starting: _keepAliveStarting,
onStart: () =>
_monitorAccessibilityConnection(ensureKeepAlive: true),
),
],
const SizedBox(height: 10),
_RecognitionCard(
@@ -844,8 +920,14 @@ class _EvidencePill extends StatelessWidget {
class _BackgroundKeepAliveCard extends StatelessWidget {
final RecognitionStatus status;
final bool starting;
final VoidCallback onStart;
const _BackgroundKeepAliveCard({required this.status});
const _BackgroundKeepAliveCard({
required this.status,
required this.starting,
required this.onStart,
});
@override
Widget build(BuildContext context) {
@@ -883,9 +965,18 @@ class _BackgroundKeepAliveCard extends StatelessWidget {
),
),
Text(
status.batteryOptimizationIgnored ? '后台限制较少' : '需要设置',
status.recentsProtectionActive
? '任务清理防护中'
: status.keepAliveRunning
? '保护运行中'
: status.keepAliveExpected
? '保护未运行'
: status.batteryOptimizationIgnored
? '后台限制较少'
: '需要设置',
style: TextStyle(
color: status.batteryOptimizationIgnored
color:
status.keepAliveRunning || status.recentsProtectionActive
? AppTheme.primaryDeep
: AppTheme.orange,
fontSize: 10,
@@ -896,15 +987,29 @@ class _BackgroundKeepAliveCard extends StatelessWidget {
),
const SizedBox(height: 10),
Text(
'无障碍和通知监听由 Android 系统持续绑定。请允许记之后台活动,'
'${isVivo ? '并在 OriginOS 中开启自启动,' : ''}'
'否则系统清理进程后可能暂时收不到支付事件。',
status.recentsProtectionActive
? 'OriginOS 最近任务保护已开启。记之不会显示在最近任务中,请从桌面图标重新打开;关闭全部识别后会自动恢复任务卡。'
: '无障碍和通知监听由 Android 系统持续绑定。请允许记之后台活动,'
'${isVivo ? '并在 OriginOS 中开启自启动,' : ''}'
'否则系统清理进程后可能暂时收不到支付事件。',
style: TextStyle(
color: context.jz.text2,
fontSize: 11.5,
height: 1.55,
),
),
if (status.keepAliveNeedsRecovery) ...[
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: JzActionButton(
label: starting ? '正在启动…' : '启动后台保护',
secondary: true,
icon: const Icon(Icons.shield_outlined, size: 18),
onPressed: starting ? null : onStart,
),
),
],
const SizedBox(height: 12),
Row(
children: [
@@ -941,6 +1046,9 @@ class _RecognitionCard extends StatelessWidget {
final Widget child;
final VoidCallback? onOpenSettings;
final VoidCallback? onRetry;
final VoidCallback? onStartKeepAlive;
final String? recoveryMessage;
final String settingsLabel;
const _RecognitionCard({
required this.icon,
@@ -952,12 +1060,15 @@ class _RecognitionCard extends StatelessWidget {
this.statusLabel,
this.onOpenSettings,
this.onRetry,
this.onStartKeepAlive,
this.recoveryMessage,
this.settingsLabel = '前往系统设置',
});
@override
Widget build(BuildContext context) {
final palette = context.jz;
final color = connected ? AppTheme.primary : AppTheme.ai;
final color = connected ? AppTheme.primary : AppTheme.orange;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -976,7 +1087,7 @@ class _RecognitionCard extends StatelessWidget {
decoration: BoxDecoration(
color: connected
? palette.primaryBackground
: palette.aiBackground,
: palette.warningBackground,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 21),
@@ -996,7 +1107,7 @@ class _RecognitionCard extends StatelessWidget {
decoration: BoxDecoration(
color: connected
? palette.primaryBackground
: palette.aiBackground,
: palette.warningBackground,
borderRadius: BorderRadius.circular(20),
),
child: Text(
@@ -1020,9 +1131,35 @@ class _RecognitionCard extends StatelessWidget {
description,
style: TextStyle(color: palette.text2, fontSize: 12, height: 1.55),
),
if (recoveryMessage != null) ...[
const SizedBox(height: 9),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.info_outline_rounded,
color: AppTheme.orange,
size: 18,
),
const SizedBox(width: 7),
Expanded(
child: Text(
recoveryMessage!,
style: TextStyle(
color: palette.text2,
fontSize: 11.5,
height: 1.5,
),
),
),
],
),
],
const SizedBox(height: 6),
child,
if (onOpenSettings != null || onRetry != null) ...[
if (onOpenSettings != null ||
onRetry != null ||
onStartKeepAlive != null) ...[
const SizedBox(height: 6),
Wrap(
spacing: 6,
@@ -1034,11 +1171,17 @@ class _RecognitionCard extends StatelessWidget {
icon: const Icon(Icons.refresh_rounded, size: 18),
label: const Text('重新检测'),
),
if (onStartKeepAlive != null)
TextButton.icon(
onPressed: onStartKeepAlive,
icon: const Icon(Icons.shield_outlined, size: 18),
label: const Text('启动后台保护'),
),
if (onOpenSettings != null)
TextButton.icon(
onPressed: onOpenSettings,
icon: const Icon(Icons.settings_outlined, size: 18),
label: const Text('前往系统设置'),
label: Text(settingsLabel),
),
],
),