Se agrego el sdk para el pda de honeywell

This commit is contained in:
Alejandro Lara
2026-06-19 08:57:58 -06:00
parent 50fe9a9d36
commit d6db216d2d
21 changed files with 1038 additions and 77 deletions
+21 -15
View File
@@ -1,13 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.honeywell.decode.permission.DECODE" />
<application
android:name=".QrScannerApplication"
android:label="qrscanner"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
android:usesCleartextTraffic="true"
tools:replace="android:label">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -17,10 +20,6 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
@@ -29,22 +28,29 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="com.example.qrscanner.action.BARCODE_DATA"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<receiver
android:name=".HoneywellScanReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.example.qrscanner.action.BARCODE_DATA"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
</manifest>
@@ -0,0 +1,160 @@
package com.example.qrscanner
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import io.flutter.plugin.common.EventChannel
object HoneywellScanManager {
const val ACTION_BARCODE_DATA = "com.example.qrscanner.action.BARCODE_DATA"
const val EXTRA_SCAN_DATA = "scan_data"
private const val PREFS = "honeywell_scan_bridge"
private const val PENDING_KEY = "pending_scan"
private const val ACTION_CLAIM_SCANNER = "com.honeywell.aidc.action.ACTION_CLAIM_SCANNER"
private const val ACTION_RELEASE_SCANNER = "com.honeywell.aidc.action.ACTION_RELEASE_SCANNER"
private const val EXTRA_SCANNER = "com.honeywell.aidc.extra.EXTRA_SCANNER"
private const val EXTRA_PROPERTIES = "com.honeywell.aidc.extra.EXTRA_PROPERTIES"
private const val SCANNER_PACKAGE = "com.intermec.datacollectionservice"
@Volatile
private var eventSink: EventChannel.EventSink? = null
fun setEventSink(sink: EventChannel.EventSink?) {
eventSink = sink
}
fun isSupported(context: Context): Boolean {
return try {
context.packageManager.getPackageInfo(SCANNER_PACKAGE, 0)
true
} catch (_: PackageManager.NameNotFoundException) {
false
}
}
fun claimScanner(context: Context) {
if (!isSupported(context)) return
val properties = Bundle().apply {
putBoolean("DPR_DATA_INTENT", true)
putString("DPR_DATA_INTENT_ACTION", ACTION_BARCODE_DATA)
putBoolean("DPR_LAUNCH_BROWSER", false)
putBoolean("DEC_QR_ENABLED", true)
putBoolean("DEC_AZTEC_ENABLED", false)
putBoolean("DEC_CODABAR_ENABLED", false)
putBoolean("DEC_CODE39_ENABLED", false)
putBoolean("DEC_CODE93_ENABLED", false)
putBoolean("DEC_CODE128_ENABLED", false)
putBoolean("DEC_DATAMATRIX_ENABLED", false)
putBoolean("DEC_EAN8_ENABLED", false)
putBoolean("DEC_EAN13_ENABLED", false)
putBoolean("DEC_MAXICODE_ENABLED", false)
putBoolean("DEC_PDF417_ENABLED", false)
putBoolean("DEC_RSS_14_ENABLED", false)
putBoolean("DEC_RSS_EXPANDED_ENABLED", false)
putBoolean("DEC_UPCA_ENABLE", false)
putBoolean("DEC_UPCE0_ENABLED", false)
}
sendScannerBroadcast(
context,
Intent(ACTION_CLAIM_SCANNER)
.setPackage(SCANNER_PACKAGE)
.putExtra(EXTRA_SCANNER, "dcs.scanner.imager")
.putExtra(EXTRA_PROPERTIES, properties),
)
}
fun releaseScanner(context: Context) {
if (!isSupported(context)) return
sendScannerBroadcast(context, Intent(ACTION_RELEASE_SCANNER))
}
fun handleBarcodeIntent(context: Context, intent: Intent?) {
val data = extractScanData(intent) ?: return
deliverScan(context, data)
}
fun deliverScan(context: Context, data: String) {
val trimmed = data.trim()
if (trimmed.isEmpty()) return
storePendingScan(context, trimmed)
eventSink?.success(trimmed)
}
fun launchAppWithScan(context: Context, data: String) {
val trimmed = data.trim()
if (trimmed.isEmpty()) return
storePendingScan(context, trimmed)
val launchIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra(EXTRA_SCAN_DATA, trimmed)
}
context.startActivity(launchIntent)
}
fun consumePendingScan(context: Context): String? {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val stored = prefs.getString(PENDING_KEY, null)?.trim()
if (!stored.isNullOrEmpty()) {
prefs.edit().remove(PENDING_KEY).apply()
return stored
}
return null
}
private fun storePendingScan(context: Context, data: String) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit()
.putString(PENDING_KEY, data)
.apply()
}
private fun extractScanData(intent: Intent?): String? {
if (intent == null) return null
intent.getStringExtra(EXTRA_SCAN_DATA)?.let { return it }
if (intent.action == ACTION_BARCODE_DATA) {
val version = intent.getIntExtra("version", 0)
if (version >= 1) {
return intent.getStringExtra("data")
}
}
return intent.getStringExtra("data")
}
private fun sendScannerBroadcast(context: Context, intent: Intent) {
if (Build.VERSION.SDK_INT < 26) {
context.sendBroadcast(intent)
return
}
val matches = context.packageManager.queryBroadcastReceivers(intent, PackageManager.MATCH_DEFAULT_ONLY)
if (matches.isEmpty()) {
context.sendBroadcast(intent)
return
}
for (resolveInfo in matches) {
val explicit = Intent(intent).apply {
component = ComponentName(
resolveInfo.activityInfo.applicationInfo.packageName,
resolveInfo.activityInfo.name,
)
}
context.sendBroadcast(explicit)
}
}
}
@@ -0,0 +1,19 @@
package com.example.qrscanner
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class HoneywellScanReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != HoneywellScanManager.ACTION_BARCODE_DATA) return
val version = intent.getIntExtra("version", 0)
if (version < 1) return
val data = intent.getStringExtra("data")?.trim().orEmpty()
if (data.isEmpty()) return
HoneywellScanManager.launchAppWithScan(context, data)
}
}
@@ -1,5 +1,59 @@
package com.example.qrscanner
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
companion object {
private const val METHOD_CHANNEL = "com.example.qrscanner/scan_launch"
private const val EVENT_CHANNEL = "com.example.qrscanner/scan_events"
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"isSupported" -> result.success(HoneywellScanManager.isSupported(this))
"claimScanner" -> {
HoneywellScanManager.claimScanner(this)
result.success(true)
}
"releaseScanner" -> {
HoneywellScanManager.releaseScanner(this)
result.success(true)
}
"consumePendingScan" -> {
result.success(HoneywellScanManager.consumePendingScan(this))
}
else -> result.notImplemented()
}
}
EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
HoneywellScanManager.setEventSink(events)
}
override fun onCancel(arguments: Any?) {
HoneywellScanManager.setEventSink(null)
}
})
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
setIntent(intent)
HoneywellScanManager.handleBarcodeIntent(this, intent)
}
override fun onResume() {
super.onResume()
HoneywellScanManager.claimScanner(this)
HoneywellScanManager.handleBarcodeIntent(this, intent)
}
}
@@ -0,0 +1,10 @@
package com.example.qrscanner
import android.app.Application
class QrScannerApplication : Application() {
override fun onCreate() {
super.onCreate()
HoneywellScanManager.claimScanner(this)
}
}
+2
View File
@@ -0,0 +1,2 @@
configurations.maybeCreate("default")
artifacts.add("default", file('honeywell.aar'))
Binary file not shown.
+1
View File
@@ -24,3 +24,4 @@ plugins {
}
include(":app")
include(":honeywell")
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23 -1
View File
@@ -1,20 +1,42 @@
import 'package:flutter/material.dart';
import 'package:qrscanner/screens/home_screen.dart';
import 'package:qrscanner/screens/login_screen.dart';
import 'package:qrscanner/services/scan_navigation_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
ScanNavigationService.instance.initialize();
}
@override
void dispose() {
ScanNavigationService.instance.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'QR Scanner',
debugShowCheckedModeBanner: false,
theme: AppTheme.darkTheme,
navigatorKey: ScanNavigationService.instance.navigatorKey,
navigatorObservers: [homeRouteObserver],
home: const LoginScreen(),
);
}
+68 -6
View File
@@ -3,9 +3,14 @@ import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/screens/login_screen.dart';
import 'package:qrscanner/screens/scanner_screen.dart';
import 'package:qrscanner/services/auth_service.dart';
import 'package:qrscanner/services/hardware_scanner_service.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
class HomeScreen extends StatelessWidget {
final RouteObserver<ModalRoute<void>> homeRouteObserver =
RouteObserver<ModalRoute<void>>();
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required this.session,
@@ -13,7 +18,29 @@ class HomeScreen extends StatelessWidget {
final AuthSession session;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _hardwareAvailable = false;
@override
void initState() {
super.initState();
_checkHardware();
}
Future<void> _checkHardware() async {
final available = await ScanLaunchService.instance.isSupported();
if (mounted) {
setState(() => _hardwareAvailable = available);
}
}
Future<void> _logout(BuildContext context) async {
await HardwareScannerService.instance.ensureStopped();
await ScanLaunchService.instance.releaseScanner();
await AuthService().clearSession();
if (!context.mounted) return;
@@ -30,11 +57,12 @@ class HomeScreen extends StatelessWidget {
);
}
void _openScanner(BuildContext context) {
Navigator.of(context).push(
Future<void> _openScanner(BuildContext context) async {
await Navigator.of(context).push(
PageRouteBuilder<void>(
settings: const RouteSettings(name: '/scanner'),
pageBuilder: (context, animation, secondaryAnimation) =>
ScannerScreen(session: session),
ScannerScreen(session: widget.session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
@@ -57,7 +85,7 @@ class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final expiresHours = (session.expiresIn / 3600).round();
final expiresHours = (widget.session.expiresIn / 3600).round();
return Scaffold(
body: Stack(
@@ -140,7 +168,7 @@ class HomeScreen extends StatelessWidget {
),
const SizedBox(height: 28),
Text(
'Hola, ${session.username}',
'Hola, ${widget.session.username}',
style:
Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
@@ -175,6 +203,40 @@ class HomeScreen extends StatelessWidget {
),
),
),
if (_hardwareAvailable) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
decoration: BoxDecoration(
color: AppTheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.primary.withValues(alpha: 0.25),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.barcode_reader,
size: 16,
color: AppTheme.primary.withValues(alpha: 0.9),
),
const SizedBox(width: 8),
Text(
'Escáner activo · abre la app al escanear',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 12,
),
),
],
),
),
],
const SizedBox(height: 48),
_ScanButton(onPressed: () => _openScanner(context)),
],
+5 -16
View File
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:qrscanner/config/api_config.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/screens/home_screen.dart';
import 'package:qrscanner/services/auth_service.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
import 'package:qrscanner/services/scan_navigation_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
class LoginScreen extends StatefulWidget {
@@ -47,6 +48,7 @@ class _LoginScreenState extends State<LoginScreen>
curve: Curves.easeOutCubic,
));
_animationController.forward();
ScanLaunchService.instance.claimScanner();
_checkStoredSession();
}
@@ -54,7 +56,7 @@ class _LoginScreenState extends State<LoginScreen>
final session = await _authService.loadStoredSession();
if (!mounted || session == null) return;
_goToHome(session);
await ScanNavigationService.instance.navigateToHome(session);
}
@override
@@ -83,7 +85,7 @@ class _LoginScreenState extends State<LoginScreen>
if (!mounted) return;
if (response.success && response.accessToken != null) {
_goToHome(
await ScanNavigationService.instance.navigateToHome(
AuthSession.fromLoginResponse(
response.copyWith(
username: response.username ?? _usernameController.text.trim(),
@@ -109,19 +111,6 @@ class _LoginScreenState extends State<LoginScreen>
}
}
void _goToHome(AuthSession session) {
Navigator.of(context).pushReplacement(
PageRouteBuilder<void>(
pageBuilder: (context, animation, secondaryAnimation) =>
HomeScreen(session: session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
+333 -20
View File
@@ -1,25 +1,33 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/models/pass_code.dart';
import 'package:qrscanner/models/scan_response.dart';
import 'package:qrscanner/services/feedback_service.dart';
import 'package:qrscanner/services/hardware_scanner_service.dart';
import 'package:qrscanner/services/pass_code_service.dart';
import 'package:qrscanner/theme/app_theme.dart';
enum _ScanPhase { scanning, processing, success, warning }
enum _ScannerInput { loading, camera, hardware }
const double _scanWindowSize = 260;
const double _scanWindowRadius = 28;
const Alignment _scanWindowAlignment = Alignment(0, -0.12);
const Duration _resultDisplayDuration = Duration(seconds: 5);
class ScannerScreen extends StatefulWidget {
const ScannerScreen({
super.key,
required this.session,
this.initialCode,
});
final AuthSession session;
final String? initialCode;
@override
State<ScannerScreen> createState() => _ScannerScreenState();
@@ -27,21 +35,21 @@ class ScannerScreen extends StatefulWidget {
class _ScannerScreenState extends State<ScannerScreen>
with TickerProviderStateMixin {
final _scannerController = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
detectionTimeoutMs: 500,
facing: CameraFacing.back,
formats: const [BarcodeFormat.qrCode],
autoZoom: true,
);
MobileScannerController? _cameraController;
final _hardwareScanner = HardwareScannerService.instance;
final _passCodeService = PassCodeService();
final _feedbackService = FeedbackService();
_ScannerInput _inputMode = _ScannerInput.loading;
bool _hardwareAvailable = false;
bool _isInitialSetup = true;
bool _isSwitchingInput = false;
_ScanPhase _phase = _ScanPhase.scanning;
bool _isHandlingDetection = false;
String? _lastScannedCode;
String? _message;
PassCode? _passCode;
Timer? _resultAutoDismissTimer;
late final AnimationController _scanLineController;
late final AnimationController _resultController;
@@ -76,8 +84,158 @@ class _ScannerScreenState extends State<ScannerScreen>
parent: _resultController,
curve: Curves.easeOutBack,
));
_initializeScannerInput();
}
Future<void> _initializeScannerInput() async {
_hardwareAvailable = await HardwareScannerService.isAvailable();
if (!mounted) return;
if (_hardwareAvailable) {
await _activateHardwareMode(showLoading: true);
} else {
await _activateCameraMode(showLoading: true);
}
final pendingCode = widget.initialCode?.trim();
if (!mounted || pendingCode == null || pendingCode.isEmpty) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _processScan(pendingCode);
});
}
MobileScannerController _createCameraController() {
return MobileScannerController(
autoStart: false,
detectionSpeed: DetectionSpeed.normal,
detectionTimeoutMs: 500,
facing: CameraFacing.back,
formats: const [BarcodeFormat.qrCode],
autoZoom: true,
);
}
Future<void> _activateHardwareMode({bool showLoading = false}) async {
if (!mounted) return;
if (showLoading) {
setState(() => _inputMode = _ScannerInput.loading);
}
try {
try {
await _cameraController?.stop();
} catch (_) {}
await _cameraController?.dispose();
_cameraController = null;
_scanLineController.stop();
await Future<void>.delayed(const Duration(milliseconds: 300));
final started = await _hardwareScanner.activate(onScan: _onRawScan);
if (!mounted) return;
if (started) {
setState(() {
_inputMode = _ScannerInput.hardware;
_isInitialSetup = false;
});
return;
}
await _activateCameraMode(
showLoading: showLoading,
fallbackFromHardware: true,
);
} finally {
if (mounted && _inputMode == _ScannerInput.loading) {
setState(() {
_inputMode = _ScannerInput.hardware;
_isInitialSetup = false;
});
}
}
}
Future<void> _activateCameraMode({
bool showLoading = false,
bool fallbackFromHardware = false,
}) async {
if (!mounted) return;
if (showLoading) {
setState(() => _inputMode = _ScannerInput.loading);
}
try {
await _hardwareScanner.pause();
await Future<void>.delayed(const Duration(milliseconds: 300));
try {
await _cameraController?.stop();
} catch (_) {}
await _cameraController?.dispose();
_cameraController = _createCameraController();
if (!_scanLineController.isAnimating) {
_scanLineController.repeat();
}
if (!mounted) return;
setState(() {
_inputMode = _ScannerInput.camera;
_isInitialSetup = false;
});
await Future<void>.delayed(const Duration(milliseconds: 200));
try {
await _cameraController!.start();
} catch (_) {
if (!mounted) return;
if (!fallbackFromHardware) {
await _activateHardwareMode();
}
}
} finally {
if (mounted && _inputMode == _ScannerInput.loading) {
setState(() {
_inputMode = _ScannerInput.camera;
_isInitialSetup = false;
});
}
}
}
Future<void> _toggleScannerInput() async {
if (!_hardwareAvailable ||
_phase != _ScanPhase.scanning ||
_isSwitchingInput ||
_inputMode == _ScannerInput.loading) {
return;
}
setState(() => _isSwitchingInput = true);
try {
if (_inputMode == _ScannerInput.hardware) {
await _activateCameraMode();
} else if (_inputMode == _ScannerInput.camera) {
await _activateHardwareMode();
}
} finally {
if (mounted) {
setState(() => _isSwitchingInput = false);
} else {
_isSwitchingInput = false;
}
}
}
bool get _showsScannerInput =>
_phase == _ScanPhase.scanning ||
_phase == _ScanPhase.processing ||
_phase == _ScanPhase.success ||
_phase == _ScanPhase.warning;
Rect _scanWindowRect(Size size) {
final center = _scanWindowAlignment.alongSize(size);
return Rect.fromCenter(
@@ -87,23 +245,58 @@ class _ScannerScreenState extends State<ScannerScreen>
);
}
void _cancelAutoDismiss() {
_resultAutoDismissTimer?.cancel();
_resultAutoDismissTimer = null;
}
void _scheduleAutoDismiss() {
_cancelAutoDismiss();
_resultAutoDismissTimer = Timer(_resultDisplayDuration, () {
if (!mounted) return;
if (_phase == _ScanPhase.success || _phase == _ScanPhase.warning) {
unawaited(_scanAgain());
}
});
}
@override
void dispose() {
_cancelAutoDismiss();
_scanLineController.dispose();
_resultController.dispose();
_scannerController.dispose();
_cameraController?.dispose();
_hardwareScanner.deactivate();
_feedbackService.dispose();
super.dispose();
}
Future<void> _onDetect(BarcodeCapture capture) async {
if (_phase != _ScanPhase.scanning || _isHandlingDetection) return;
void _onRawScan(String rawValue) {
_processScan(rawValue);
}
Future<void> _onDetect(BarcodeCapture capture) async {
final rawValue = capture.barcodes.firstOrNull?.rawValue?.trim();
if (rawValue == null || rawValue.isEmpty) return;
await _processScan(rawValue);
}
Future<void> _processScan(String rawValue) async {
if (_isHandlingDetection || _phase == _ScanPhase.processing) return;
final code = _extractCode(rawValue);
if (code.isEmpty || code == _lastScannedCode) return;
if (code.isEmpty) return;
final showingResult =
_phase == _ScanPhase.success || _phase == _ScanPhase.warning;
if (showingResult) {
_cancelAutoDismiss();
_resultController.reset();
} else {
if (_phase != _ScanPhase.scanning) return;
if (code == _lastScannedCode) return;
}
_isHandlingDetection = true;
setState(() {
@@ -114,7 +307,9 @@ class _ScannerScreenState extends State<ScannerScreen>
});
_scanLineController.stop();
await _scannerController.stop();
if (_inputMode == _ScannerInput.camera) {
await _cameraController?.stop();
}
await _feedbackService.onCodeDetected();
try {
@@ -166,9 +361,15 @@ class _ScannerScreenState extends State<ScannerScreen>
}
await _resultController.forward();
_scheduleAutoDismiss();
if (_inputMode == _ScannerInput.camera) {
await _cameraController?.start();
}
}
Future<void> _scanAgain() async {
_cancelAutoDismiss();
_resultController.reset();
_isHandlingDetection = false;
setState(() {
@@ -177,8 +378,10 @@ class _ScannerScreenState extends State<ScannerScreen>
_message = null;
_passCode = null;
});
_scanLineController.repeat();
await _scannerController.start();
if (_inputMode == _ScannerInput.camera) {
_scanLineController.repeat();
await _cameraController?.start();
}
}
@override
@@ -203,16 +406,45 @@ class _ScannerScreenState extends State<ScannerScreen>
),
title: const Text('Escanear pase'),
centerTitle: true,
actions: [
if (_hardwareAvailable && _phase == _ScanPhase.scanning)
IconButton(
tooltip: _inputMode == _ScannerInput.hardware
? 'Usar cámara'
: 'Usar escáner Honeywell',
icon: _isSwitchingInput
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Icon(
_inputMode == _ScannerInput.hardware
? Icons.camera_alt_outlined
: Icons.barcode_reader,
),
onPressed: _isSwitchingInput ? null : _toggleScannerInput,
),
],
),
body: Stack(
fit: StackFit.expand,
children: [
if (_phase == _ScanPhase.scanning || _phase == _ScanPhase.processing)
if (_inputMode == _ScannerInput.loading && _isInitialSetup)
const Center(
child: CircularProgressIndicator(color: AppTheme.primary),
),
if (_showsScannerInput &&
_inputMode == _ScannerInput.camera &&
_cameraController != null)
LayoutBuilder(
builder: (context, constraints) {
final scanRect = _scanWindowRect(constraints.biggest);
return MobileScanner(
controller: _scannerController,
controller: _cameraController!,
onDetect: _onDetect,
scanWindow: scanRect,
overlayBuilder: (context, constraints) {
@@ -236,8 +468,8 @@ class _ScannerScreenState extends State<ScannerScreen>
);
},
),
if (_phase != _ScanPhase.scanning && _phase != _ScanPhase.processing)
Container(color: AppTheme.backgroundDark),
if (_showsScannerInput && _inputMode == _ScannerInput.hardware)
_buildHardwareScannerView(),
AnimatedContainer(
duration: const Duration(milliseconds: 450),
curve: Curves.easeOutCubic,
@@ -256,7 +488,7 @@ class _ScannerScreenState extends State<ScannerScreen>
),
),
),
if (_phase == _ScanPhase.scanning)
if (_phase == _ScanPhase.scanning && _inputMode != _ScannerInput.loading)
SafeArea(
child: Align(
alignment: Alignment.bottomCenter,
@@ -452,11 +684,92 @@ class _ScannerScreenState extends State<ScannerScreen>
);
}
Widget _buildHardwareScannerView() {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.backgroundDark,
Color(0xFF0D1B2A),
Color(0xFF12182B),
],
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 36),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
color: AppTheme.primary.withValues(alpha: 0.12),
border: Border.all(
color: AppTheme.primary.withValues(alpha: 0.35),
),
),
child: const Icon(
Icons.barcode_reader,
size: 56,
color: AppTheme.primary,
),
),
const SizedBox(height: 28),
Text(
'Escáner Honeywell',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Presiona el botón lateral del EDA51 para leer el código QR del pase.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.white.withValues(alpha: 0.6),
height: 1.5,
),
),
const SizedBox(height: 20),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.08),
),
),
child: Text(
'Sensor dedicado · Honeywell ScanPal EDA51',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.45),
fontSize: 12,
),
),
),
],
),
),
),
);
}
Widget _buildHint() {
final message = _inputMode == _ScannerInput.hardware
? 'Presiona el botón lateral para escanear'
: 'Apunta la cámara al código QR del pase';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Apunta la cámara al código QR del pase',
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
+71 -18
View File
@@ -1,43 +1,92 @@
import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart';
class FeedbackService {
FeedbackService() : _player = AudioPlayer() {
_player.setReleaseMode(ReleaseMode.stop);
FeedbackService() {
_configurePlayers();
}
final AudioPlayer _player;
final AudioPlayer _scanPlayer = AudioPlayer();
final AudioPlayer _successPlayer = AudioPlayer();
final AudioPlayer _warningPlayer = AudioPlayer();
static final AudioContext _loudContext = AudioContext(
android: AudioContextAndroid(
isSpeakerphoneOn: true,
audioMode: AndroidAudioMode.normal,
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.alarm,
audioFocus: AndroidAudioFocus.gain,
),
iOS: AudioContextIOS(
category: AVAudioSessionCategory.playback,
options: {
AVAudioSessionOptions.mixWithOthers,
AVAudioSessionOptions.duckOthers,
},
),
);
Future<void> _configurePlayers() async {
for (final player in [_scanPlayer, _successPlayer, _warningPlayer]) {
await player.setReleaseMode(ReleaseMode.stop);
await player.setPlayerMode(PlayerMode.lowLatency);
await player.setVolume(1.0);
await player.setAudioContext(_loudContext);
}
}
Future<void> onCodeDetected() async {
await Future.wait([
_play('sounds/scan.wav'),
_vibratePattern(const [0, 40]),
_play(_scanPlayer, 'sounds/scan.wav'),
_vibratePattern(const [0, 80]),
HapticFeedback.mediumImpact(),
]);
}
Future<void> onSuccess() async {
await Future.wait([
_play('sounds/success.wav'),
_vibratePattern(const [0, 90, 70, 140]),
_playSuccessSequence(),
_vibratePattern(const [0, 120, 60, 180]),
HapticFeedback.heavyImpact(),
]);
await HapticFeedback.heavyImpact();
}
Future<void> onWarning() async {
await Future.wait([
_play('sounds/warning.wav'),
_vibratePattern(const [0, 120, 80, 120, 80, 120]),
_playWarningSequence(),
_vibratePattern(const [0, 180, 80, 180, 80, 220]),
HapticFeedback.heavyImpact(),
]);
await HapticFeedback.mediumImpact();
}
Future<void> _play(String asset) async {
Future<void> _playSuccessSequence() async {
await _play(_successPlayer, 'sounds/success.wav');
if (!kIsWeb && Platform.isAndroid) {
await Future<void>.delayed(const Duration(milliseconds: 120));
await _play(_successPlayer, 'sounds/success.wav');
}
}
Future<void> _playWarningSequence() async {
for (var i = 0; i < 2; i++) {
await _play(_warningPlayer, 'sounds/warning.wav');
if (i == 0) {
await Future<void>.delayed(const Duration(milliseconds: 140));
}
}
}
Future<void> _play(AudioPlayer player, String asset) async {
try {
await _player.stop();
await _player.play(AssetSource(asset));
await player.stop();
await player.play(AssetSource(asset));
} catch (_) {
// Ignore audio errors on devices without speaker routing.
await SystemSound.play(SystemSoundType.alert);
}
}
@@ -51,7 +100,7 @@ class FeedbackService {
final intensities = pattern
.asMap()
.entries
.map((entry) => entry.key.isOdd ? 180 : 0)
.map((entry) => entry.key.isOdd ? 255 : 0)
.toList();
await Vibration.vibrate(
pattern: pattern,
@@ -62,11 +111,15 @@ class FeedbackService {
await Vibration.vibrate(pattern: pattern);
} catch (_) {
await HapticFeedback.selectionClick();
await HapticFeedback.heavyImpact();
}
}
Future<void> dispose() async {
await _player.dispose();
await Future.wait([
_scanPlayer.dispose(),
_successPlayer.dispose(),
_warningPlayer.dispose(),
]);
}
}
@@ -0,0 +1,62 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
typedef HardwareScanCallback = void Function(String rawValue);
/// Integración con el escáner físico Honeywell vía Intent API nativa (EDA51).
class HardwareScannerService {
HardwareScannerService._();
static final HardwareScannerService instance = HardwareScannerService._();
final _scanLaunch = ScanLaunchService.instance;
HardwareScanCallback? _onScan;
void Function(Exception error)? _onError;
StreamSubscription<String>? _scanSubscription;
bool _active = false;
bool get isActive => _active;
static Future<bool> isAvailable() => instance._scanLaunch.isSupported();
Future<bool> activate({
required HardwareScanCallback onScan,
void Function(Exception error)? onError,
}) async {
if (kIsWeb || !Platform.isAndroid) return false;
_onScan = onScan;
_onError = onError;
await _scanLaunch.claimScanner();
await _scanSubscription?.cancel();
_scanSubscription = _scanLaunch.scanStream.listen(
(code) => _onScan?.call(code),
onError: (Object error) => _onError?.call(Exception(error.toString())),
);
_active = true;
return _scanLaunch.isSupported();
}
Future<void> pause() async {
_active = false;
}
Future<void> deactivate() async {
await _scanSubscription?.cancel();
_scanSubscription = null;
_onScan = null;
_onError = null;
_active = false;
}
Future<void> ensureStopped() async {
await deactivate();
await _scanLaunch.releaseScanner();
}
}
+62
View File
@@ -0,0 +1,62 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
class ScanLaunchService {
ScanLaunchService._();
static final ScanLaunchService instance = ScanLaunchService._();
static const _methodChannel = MethodChannel('com.example.qrscanner/scan_launch');
static const _eventChannel = EventChannel('com.example.qrscanner/scan_events');
Stream<String>? _scanStream;
bool get isAndroid => !kIsWeb && Platform.isAndroid;
Stream<String> get scanStream {
_scanStream ??= _eventChannel
.receiveBroadcastStream()
.where((event) => event is String && event.trim().isNotEmpty)
.map((event) => (event as String).trim());
return _scanStream!;
}
Future<bool> isSupported() async {
if (!isAndroid) return false;
try {
final supported = await _methodChannel.invokeMethod<bool>('isSupported');
return supported ?? false;
} catch (_) {
return false;
}
}
Future<void> claimScanner() async {
if (!isAndroid) return;
try {
await _methodChannel.invokeMethod<void>('claimScanner');
} catch (_) {}
}
Future<void> releaseScanner() async {
if (!isAndroid) return;
try {
await _methodChannel.invokeMethod<void>('releaseScanner');
} catch (_) {}
}
Future<String?> consumePendingScan() async {
if (!isAndroid) return null;
try {
final code = await _methodChannel.invokeMethod<String>('consumePendingScan');
final trimmed = code?.trim();
if (trimmed == null || trimmed.isEmpty) return null;
return trimmed;
} catch (_) {
return null;
}
}
}
+137
View File
@@ -0,0 +1,137 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:qrscanner/models/auth_session.dart';
import 'package:qrscanner/screens/home_screen.dart';
import 'package:qrscanner/screens/scanner_screen.dart';
import 'package:qrscanner/services/auth_service.dart';
import 'package:qrscanner/services/scan_launch_service.dart';
class ScanNavigationService {
ScanNavigationService._();
static final ScanNavigationService instance = ScanNavigationService._();
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
final AuthService _authService = AuthService();
StreamSubscription<String>? _scanSubscription;
String? _pendingCode;
bool _handlingScan = false;
bool _scannerOpen = false;
String? get pendingCode => _pendingCode;
void setPendingCode(String code) {
final trimmed = code.trim();
if (trimmed.isEmpty) return;
_pendingCode = trimmed;
}
String? takePendingCode() {
final code = _pendingCode;
_pendingCode = null;
return code;
}
Future<void> initialize() async {
await ScanLaunchService.instance.claimScanner();
final pending = await ScanLaunchService.instance.consumePendingScan();
if (pending != null) {
setPendingCode(pending);
}
await _scanSubscription?.cancel();
_scanSubscription = ScanLaunchService.instance.scanStream.listen(
(code) => unawaited(handleScan(code)),
);
}
Future<void> dispose() async {
await _scanSubscription?.cancel();
_scanSubscription = null;
}
Future<void> handleScan(String code, {AuthSession? session}) async {
final trimmed = code.trim();
if (trimmed.isEmpty || _handlingScan) return;
_handlingScan = true;
try {
setPendingCode(trimmed);
final activeSession = session ??
_authService.currentSession ??
await _authService.loadStoredSession();
if (activeSession == null) return;
final navigator = navigatorKey.currentState;
if (navigator == null) return;
takePendingCode();
if (_scannerOpen) return;
_scannerOpen = true;
try {
await navigator.push(
PageRouteBuilder<void>(
settings: const RouteSettings(name: '/scanner'),
pageBuilder: (context, animation, secondaryAnimation) =>
ScannerScreen(
session: activeSession,
initialCode: trimmed,
),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.06),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
)),
child: child,
),
);
},
transitionDuration: const Duration(milliseconds: 380),
),
);
} finally {
_scannerOpen = false;
}
} finally {
_handlingScan = false;
}
}
Future<void> openPendingScanAfterAuth(AuthSession session) async {
final code = takePendingCode();
if (code == null) return;
await handleScan(code, session: session);
}
Future<void> navigateToHome(AuthSession session) async {
final navigator = navigatorKey.currentState;
if (navigator == null) return;
await navigator.pushAndRemoveUntil(
PageRouteBuilder<void>(
pageBuilder: (context, animation, secondaryAnimation) =>
HomeScreen(session: session),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
),
(route) => false,
);
await openPendingScanAfterAuth(session);
}
}
+8
View File
@@ -208,6 +208,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
honeywell_scanner:
dependency: "direct main"
description:
name: honeywell_scanner
sha256: a2bf8600e8e33887e268df20d300d9341aed708179a3be74bc2acc9f8806a7c1
url: "https://pub.dev"
source: hosted
version: "8.0.0"
hooks:
dependency: transitive
description:
+1
View File
@@ -39,6 +39,7 @@ dependencies:
mobile_scanner: ^7.0.1
shared_preferences: ^2.3.3
vibration: ^3.1.3
honeywell_scanner: ^8.0.0
dev_dependencies:
flutter_test: