Add intent wedge scanner, MDM support for Android (#863)

Add Android intent wedge scanner support with auto configuration for supported Zebra and Datalogic devices.
Intent wedge scans can be done without pressing the scan button first.

Adds the ability for an MDM server to push settings on Android.
This commit is contained in:
Reza Fouladian
2026-08-16 13:59:20 +10:00
committed by GitHub
parent 2cc5d37f71
commit ca655f36fe
12 changed files with 1114 additions and 1 deletions
+50
View File
@@ -18,6 +18,7 @@ import "package:inventree/api.dart";
import "package:inventree/l10.dart";
import "package:inventree/barcode/camera_controller.dart";
import "package:inventree/barcode/intent_controller.dart";
import "package:inventree/barcode/wedge_controller.dart";
import "package:inventree/barcode/controller.dart";
import "package:inventree/barcode/handler.dart";
@@ -68,6 +69,53 @@ Future<void> barcodeFailure(String msg, dynamic extra) async {
);
}
void initGlobalIntentListener() {
bool _processing = false;
datawedgeStream.listen((event) async {
if (intentScannerActive) return;
if (_processing) return;
_processing = true;
try {
final int controllerType =
await InvenTreeSettingsManager().getValue(
INV_BARCODE_SCAN_TYPE,
BARCODE_CONTROLLER_CAMERA,
)
as int;
if (controllerType != BARCODE_CONTROLLER_INTENT) return;
if (!InvenTreeAPI().isConnected()) return;
String barcode = "";
if (event is Map) {
final map = Map<String, dynamic>.from(event);
barcode = (map["data"] ?? "").toString();
} else if (event is String) {
barcode = event;
}
if (barcode.isEmpty) return;
if (!OneContext.hasContext) return;
await OneContext().navigator.push(
PageRouteBuilder(
pageBuilder: (context, _, _) => IntentBarcodeController(
BarcodeScanHandler(),
initialBarcode: barcode,
),
opaque: false,
),
);
} finally {
_processing = false;
}
});
}
/*
* Launch a barcode scanner with a particular context and handler.
*
@@ -93,6 +141,8 @@ Future<Object?> scanBarcode(
as int;
switch (barcodeControllerType) {
case BARCODE_CONTROLLER_INTENT:
controller = IntentBarcodeController(handler);
case BARCODE_CONTROLLER_WEDGE:
controller = WedgeBarcodeController(handler);
case BARCODE_CONTROLLER_CAMERA:
+154
View File
@@ -0,0 +1,154 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_tabler_icons/flutter_tabler_icons.dart";
import "package:inventree/app_colors.dart";
import "package:inventree/barcode/controller.dart";
import "package:inventree/barcode/handler.dart";
import "package:inventree/l10.dart";
import "package:inventree/widget/progress.dart";
bool intentScannerActive = false;
final Stream<dynamic> datawedgeStream = const EventChannel(
"inventree/datawedge_scans",
).receiveBroadcastStream();
class IntentBarcodeController extends InvenTreeBarcodeController {
const IntentBarcodeController(
BarcodeHandler handler, {
super.key,
this.initialBarcode,
}) : super(handler);
final String? initialBarcode;
bool get isBackgroundScan => (initialBarcode ?? "").isNotEmpty;
@override
State<StatefulWidget> createState() => _IntentBarcodeControllerState();
}
class _IntentBarcodeControllerState extends InvenTreeBarcodeControllerState {
StreamSubscription<dynamic>? _sub;
bool canScan = true;
bool get scanning => mounted && canScan;
bool get isBackgroundScan =>
(widget as IntentBarcodeController).isBackgroundScan;
@override
void initState() {
super.initState();
intentScannerActive = true;
if (isBackgroundScan) {
WidgetsBinding.instance.addPostFrameCallback((_) {
handleBackgroundScan(
(widget as IntentBarcodeController).initialBarcode ?? "",
);
});
return;
}
_sub = datawedgeStream.listen((event) {
if (!scanning) return;
if (event is Map) {
final map = Map<String, dynamic>.from(event);
final data = (map["data"] ?? "").toString();
if (data.isNotEmpty) {
handleBarcodeData(data);
}
} else if (event is String && event.isNotEmpty) {
handleBarcodeData(event);
}
});
}
Future<void> handleBackgroundScan(String barcode) async {
if (!mounted || barcode.isEmpty) return;
final NavigatorState navigator = Navigator.of(context);
final ModalRoute<dynamic>? route = ModalRoute.of(context);
showLoadingOverlay();
try {
await widget.handler.processBarcode(barcode);
} finally {
hideLoadingOverlay();
if (route != null && route.isActive) {
navigator.removeRoute(route);
}
}
}
@override
void dispose() {
intentScannerActive = false;
_sub?.cancel();
_sub = null;
super.dispose();
}
@override
Future<void> pauseScan() async {
if (!mounted) return;
setState(() {
canScan = false;
});
}
@override
Future<void> resumeScan() async {
if (!mounted) return;
setState(() {
canScan = true;
});
}
@override
Widget build(BuildContext context) {
if (isBackgroundScan) {
return const AbsorbPointer(child: SizedBox.expand());
}
return Scaffold(
appBar: AppBar(title: Text(L10().scanBarcode)),
backgroundColor: Colors.black.withValues(alpha: 0.9),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(flex: 5),
const Icon(TablerIcons.barcode, size: 64),
const Spacer(flex: 5),
SizedBox(
width: 64,
height: 64,
child: CircularProgressIndicator(
color: scanning ? COLOR_ACTION : COLOR_PROGRESS,
),
),
const Spacer(flex: 5),
Padding(
padding: const EdgeInsets.all(20),
child: Text(
widget.handler.getOverlayText(context),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
),
),
);
}
}
+67
View File
@@ -0,0 +1,67 @@
import "dart:io";
import "package:flutter/foundation.dart";
import "package:flutter/services.dart";
import "package:inventree/preferences.dart";
const MethodChannel _channel = MethodChannel("inventree/wedge");
class WedgeDetectionResult {
const WedgeDetectionResult({this.vendor = "none", this.supported = false});
final String vendor;
final bool supported;
}
Future<WedgeDetectionResult> detectWedgeScanner() async {
if (kIsWeb || !Platform.isAndroid) {
return const WedgeDetectionResult();
}
try {
final Map<String, dynamic>? result = await _channel
.invokeMapMethod<String, dynamic>("detectWedge");
if (result == null) return const WedgeDetectionResult();
return WedgeDetectionResult(
vendor: (result["vendor"] ?? "none").toString(),
supported: result["supported"] == true,
);
} catch (error) {
return const WedgeDetectionResult();
}
}
Future<void> applyWedgeDetection(WedgeDetectionResult result) async {
if (result.supported) {
await InvenTreeSettingsManager().setValue(
INV_BARCODE_SCAN_TYPE,
BARCODE_CONTROLLER_INTENT,
);
}
await InvenTreeSettingsManager().setValue(INV_BARCODE_WEDGE_DETECTED, true);
}
Future<void> initWedgeScannerDefault() async {
if (await InvenTreeSettingsManager().getBool(
INV_BARCODE_WEDGE_DETECTED,
false,
)) {
return;
}
final dynamic existing = await InvenTreeSettingsManager().getValue(
INV_BARCODE_SCAN_TYPE,
null,
);
if (existing != null) {
await InvenTreeSettingsManager().setValue(INV_BARCODE_WEDGE_DETECTED, true);
return;
}
await applyWedgeDetection(await detectWedgeScanner());
}