Se agrego el sdk para el pda de honeywell
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
configurations.maybeCreate("default")
|
||||
artifacts.add("default", file('honeywell.aar'))
|
||||
Binary file not shown.
@@ -24,3 +24,4 @@ plugins {
|
||||
}
|
||||
|
||||
include(":app")
|
||||
include(":honeywell")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23
-1
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
],
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user