import 'dart:convert'; import 'package:flutter/services.dart'; class RecognitionDiagnostic { final DateTime at; final String appName, stage, result, reason, statusStrength; final String? recognitionKind, amountSource, resultFingerprint; final int nodeCount, amountCandidates; final int? ocrMs; final bool? expectedAmountMatched, resultTransitionObserved; final List ocrPreview; final DateTime? previewExpiresAt; const RecognitionDiagnostic({ required this.at, required this.appName, required this.stage, required this.result, required this.reason, required this.nodeCount, required this.amountCandidates, this.ocrMs, this.statusStrength = 'none', this.recognitionKind, this.amountSource, this.resultFingerprint, this.expectedAmountMatched, this.resultTransitionObserved, this.ocrPreview = const [], this.previewExpiresAt, }); factory RecognitionDiagnostic.fromJson(Map value) { final epoch = (value['at'] as num?)?.toInt() ?? 0; return RecognitionDiagnostic( at: DateTime.fromMillisecondsSinceEpoch(epoch), appName: value['appName']?.toString() ?? '支付应用', stage: value['stage']?.toString() ?? 'event', result: value['result']?.toString() ?? 'unknown', reason: value['reason']?.toString() ?? '', nodeCount: (value['nodeCount'] as num?)?.toInt() ?? 0, amountCandidates: (value['amountCandidates'] as num?)?.toInt() ?? 0, ocrMs: (value['ocrMs'] as num?)?.toInt(), statusStrength: value['statusStrength']?.toString() ?? 'none', recognitionKind: value['recognitionKind']?.toString(), amountSource: value['amountSource']?.toString(), resultFingerprint: value['resultFingerprint']?.toString(), expectedAmountMatched: value['expectedAmountMatched'] as bool?, resultTransitionObserved: value['resultTransitionObserved'] as bool?, ocrPreview: (value['ocrPreview'] as List?) ?.map((item) => item.toString()) .toList(growable: false) ?? const [], previewExpiresAt: (value['previewExpiresAt'] as num?) == null ? null : DateTime.fromMillisecondsSinceEpoch( (value['previewExpiresAt'] as num).toInt(), ), ); } } class RecognitionStatus { final bool accessibilityAuthorized; final bool accessibilityConnected; final bool notificationAuthorized; final bool notificationConnected; final bool postNotificationsGranted; final bool accessibilityEvents; final bool notificationEvents; final bool aiScreenshot; final bool ocrDiagnosticPreview; final DateTime? ocrDiagnosticPreviewExpiresAt; final bool batteryOptimizationIgnored; final String manufacturer; final String? latestStatus; final RecognitionDiagnostic? latestDiagnostic; const RecognitionStatus({ required this.accessibilityAuthorized, required this.accessibilityConnected, required this.notificationAuthorized, required this.notificationConnected, required this.postNotificationsGranted, required this.accessibilityEvents, required this.notificationEvents, required this.aiScreenshot, this.ocrDiagnosticPreview = false, this.ocrDiagnosticPreviewExpiresAt, this.batteryOptimizationIgnored = false, this.manufacturer = '', this.latestStatus, this.latestDiagnostic, }); factory RecognitionStatus.fromMap(Map value) { final rawSettings = value['settings']?.toString(); final rawDiagnostic = value['latestDiagnostic']?.toString(); final settings = rawSettings == null || rawSettings.isEmpty ? const {} : jsonDecode(rawSettings) as Map; return RecognitionStatus( accessibilityAuthorized: value['accessibilityAuthorized'] as bool? ?? false, accessibilityConnected: value['accessibilityConnected'] as bool? ?? false, notificationAuthorized: value['notificationAuthorized'] as bool? ?? false, notificationConnected: value['notificationConnected'] as bool? ?? false, postNotificationsGranted: value['postNotificationsGranted'] as bool? ?? true, accessibilityEvents: settings['accessibilityEvents'] as bool? ?? false, notificationEvents: settings['notificationEvents'] as bool? ?? false, aiScreenshot: settings['aiScreenshot'] as bool? ?? false, ocrDiagnosticPreview: settings['ocrDiagnosticPreview'] as bool? ?? false, ocrDiagnosticPreviewExpiresAt: (settings['ocrDiagnosticPreviewExpiresAt'] as num?) == null ? null : DateTime.fromMillisecondsSinceEpoch( (settings['ocrDiagnosticPreviewExpiresAt'] as num).toInt(), ), batteryOptimizationIgnored: value['batteryOptimizationIgnored'] as bool? ?? false, manufacturer: value['manufacturer']?.toString() ?? '', latestStatus: value['latestStatus']?.toString(), latestDiagnostic: rawDiagnostic == null || rawDiagnostic.isEmpty ? null : RecognitionDiagnostic.fromJson( jsonDecode(rawDiagnostic) as Map, ), ); } } class RecognitionCandidate { final String id, clientRequestId, state, confidence, type, source, appName; final double amount; final String? merchant, orderId, sourceText, note; final String recognitionKind, amountSource; final String? categoryHint, resultFingerprint; final int occurredAtEpochMs; RecognitionCandidate.fromJson(Map value) : id = value['id'] as String, clientRequestId = value['clientRequestId'] as String, state = value['state'] as String, confidence = value['confidence'] as String, type = value['type'] as String, source = value['source'] as String, appName = value['appName'] as String? ?? '支付应用', amount = (value['amount'] as num).toDouble(), merchant = value['merchant'] as String?, orderId = value['orderId'] as String?, sourceText = value['sourceText'] as String?, note = value['note'] as String?, recognitionKind = value['recognitionKind']?.toString() ?? 'payment', categoryHint = value['categoryHint']?.toString(), amountSource = value['amountSource']?.toString() ?? 'result', resultFingerprint = value['resultFingerprint']?.toString(), occurredAtEpochMs = (value['occurredAtEpochMs'] as num).toInt(); bool get canAutoImport => state == 'auto_ready' && confidence == 'auto'; } class SpeechEvent { final String type; final String? text; final double rms; const SpeechEvent({required this.type, this.text, this.rms = 0}); } /// Android native capabilities for screenshots, AI progress and speech. class ScreenshotChannel { static const _channel = MethodChannel('com.miaoji/screenshot'); static void Function(String path)? _screenshotReady; static void Function(String error)? _screenshotError; static void Function(SpeechEvent event)? _speechEvent; static void Function(Map action)? _recognitionAction; static bool _handlerInstalled = false; static Future cleanupStaleScreenshots() async { try { await _channel.invokeMethod('cleanupStaleScreenshots'); } on PlatformException { // Cleanup is best-effort and will run again on the next launch. } on MissingPluginException { // Screenshot storage is Android-only. } } static Future capture() async { try { return await _channel.invokeMethod('captureScreenshot'); } on PlatformException catch (e) { throw Exception(e.message ?? '截屏失败,请重试'); } on MissingPluginException { return null; } } static Future startAiProgress() async { try { await _channel.invokeMethod('startAiProgress'); } on MissingPluginException { // Non-Android platforms do not expose native progress notifications. } } static Future updateAiProgress(int count) async { try { await _channel.invokeMethod('updateAiProgress', {'count': count}); } on MissingPluginException { // Non-Android platforms do not expose native progress notifications. } } static Future finishAiProgress(int count, double total) async { try { await _channel.invokeMethod('finishAiProgress', { 'count': count, 'total': total, }); } on MissingPluginException { // Non-Android platforms do not expose native progress notifications. } } static Future failAiProgress(String message) async { try { await _channel.invokeMethod('failAiProgress', {'message': message}); } on MissingPluginException { // Non-Android platforms do not expose native progress notifications. } } static Future recognizeSpeech() async { try { return await _channel.invokeMethod('recognizeSpeech'); } on PlatformException catch (e) { throw Exception(e.message ?? '语音识别不可用'); } on MissingPluginException { return null; } } static Future startSpeechRecognition( void Function(SpeechEvent event) onEvent, ) async { _speechEvent = onEvent; _installHandler(); try { await _channel.invokeMethod('startSpeechRecognition'); } on PlatformException catch (e) { _speechEvent = null; throw Exception(e.message ?? '语音识别不可用'); } on MissingPluginException { _speechEvent = null; throw Exception('当前设备不支持语音识别'); } } static Future stopSpeechRecognition() async { try { await _channel.invokeMethod('stopSpeechRecognition'); } on MissingPluginException { // No native recognizer to stop. } } static Future cancelSpeechRecognition() async { _speechEvent = null; try { await _channel.invokeMethod('cancelSpeechRecognition'); } on MissingPluginException { // No native recognizer to cancel. } } static Future isAccessibilityEnabled() async { try { return await _channel.invokeMethod('isAccessibilityEnabled') ?? false; } on MissingPluginException { return false; } } static Future openAccessibilitySettings() async { try { await _channel.invokeMethod('openAccessibilitySettings'); } on MissingPluginException { // Accessibility shortcut is Android-only. } } static Future openQuickSettings() async { try { await _channel.invokeMethod('openQuickSettings'); } on MissingPluginException { // Quick settings tiles are Android-only. } } static Future recognitionStatus() async { try { final raw = await _channel.invokeMethod>( 'getRecognitionStatus', ); return RecognitionStatus.fromMap(raw ?? const {}); } on MissingPluginException { return const RecognitionStatus( accessibilityAuthorized: false, accessibilityConnected: false, notificationAuthorized: false, notificationConnected: false, postNotificationsGranted: true, accessibilityEvents: false, notificationEvents: false, aiScreenshot: false, ); } } static Future clearRecognitionDiagnostic() async { try { return await _channel.invokeMethod('clearRecognitionDiagnostic') ?? false; } on MissingPluginException { return false; } } static Future setRecognitionToggle(String key, bool enabled) async { try { return await _channel.invokeMethod('setRecognitionToggle', { 'key': key, 'enabled': enabled, }) ?? false; } on MissingPluginException { return false; } } static Future configureRecognitionContext({ required bool hasAccount, required bool aiAllowed, required String baseUrl, String? token, }) async { try { await _channel.invokeMethod('configureRecognitionContext', { 'hasAccount': hasAccount, 'aiAllowed': aiAllowed, 'baseUrl': baseUrl, 'token': token, }); } on MissingPluginException { // Native background recognition is Android-only. } } static Future> drainRecognitionCandidates() async { try { final values = await _channel.invokeMethod>( 'drainRecognitionCandidates', ) ?? const []; return values .whereType() .map( (value) => RecognitionCandidate.fromJson( jsonDecode(value) as Map, ), ) .toList(); } on MissingPluginException { return const []; } } static Future acknowledgeRecognitionCandidate( String id, String state, { int? transactionId, }) async { try { return await _channel.invokeMethod('ackRecognitionCandidate', { 'id': id, 'state': state, if (transactionId != null) 'transactionId': transactionId, }) ?? false; } on MissingPluginException { return false; } } static Future requestNotificationPermission() async { try { return await _channel.invokeMethod( 'requestNotificationPermission', ) ?? false; } on MissingPluginException { return true; } } static Future openNotificationAccessSettings() async { try { await _channel.invokeMethod('openNotificationAccessSettings'); } on MissingPluginException { // Notification access is Android-only. } } static Future openBatteryOptimizationSettings() async { try { await _channel.invokeMethod('openBatteryOptimizationSettings'); } on MissingPluginException { // Background battery settings are Android-only. } } static Future openBackgroundStartupSettings() async { try { await _channel.invokeMethod('openBackgroundStartupSettings'); } on MissingPluginException { // Vendor background startup settings are Android-only. } } static void onRecognitionAction( void Function(Map action) callback, ) { _recognitionAction = callback; _installHandler(); } static Future onScreenshotReady( void Function(String path) callback, { void Function(String error)? onError, }) async { _screenshotReady = callback; _screenshotError = onError; _installHandler(); } static void _installHandler() { if (_handlerInstalled) return; _handlerInstalled = true; _channel.setMethodCallHandler((call) async { if (call.method == 'onScreenshotReady') { final path = call.arguments as String?; if (path != null) _screenshotReady?.call(path); } else if (call.method == 'onScreenshotError') { _screenshotError?.call(call.arguments?.toString() ?? '截屏失败,请重试'); } else if (call.method == 'onSpeechEvent') { final raw = Map.from(call.arguments as Map); final type = raw['type']?.toString() ?? 'error'; final event = SpeechEvent( type: type, text: raw['text']?.toString(), rms: (raw['rms'] as num?)?.toDouble() ?? 0, ); _speechEvent?.call(event); if (type == 'final' || type == 'error' || type == 'cancelled') { _speechEvent = null; } } else if (call.method == 'onRecognitionAction') { final raw = Map.from(call.arguments as Map); _recognitionAction?.call( raw.map((key, value) => MapEntry(key.toString(), value)), ); } }); } }