diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5b47490c..6d16b3ae 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -94,4 +94,5 @@ dependencies {
androidTestImplementation 'com.android.support:multidex:2.0.1'
implementation "androidx.core:core:1.9.0"
implementation 'androidx.appcompat:appcompat:1.6.0'
+ compileOnly 'com.datalogic:datalogic-android-sdk:1.50'
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e21a83bf..cba340f0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -15,11 +15,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/inventree/inventree_app/MainActivity.java b/android/app/src/main/java/inventree/inventree_app/MainActivity.java
index 6d88807f..232e24f2 100644
--- a/android/app/src/main/java/inventree/inventree_app/MainActivity.java
+++ b/android/app/src/main/java/inventree/inventree_app/MainActivity.java
@@ -1,6 +1,172 @@
package inventree.inventree_app;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.RestrictionsManager;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
import io.flutter.embedding.android.FlutterActivity;
+import io.flutter.embedding.engine.FlutterEngine;
+import io.flutter.plugin.common.EventChannel;
+import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterActivity {
+
+ static final String ACTION_CUSTOM_SCAN = "inventree.inventree_app.SCAN";
+
+ // Datalogic default wedge settings
+ static final String ACTION_DATALOGIC_DECODE = "com.datalogic.decodewedge.decode_action";
+ static final String CATEGORY_DATALOGIC_DECODE = "com.datalogic.decodewedge.decode_category";
+
+ private static final String EVENT_CHANNEL = "inventree/datawedge_scans";
+ private static final String MANAGED_CONFIG_CHANNEL = "inventree/managed_config";
+ private static final String WEDGE_CHANNEL = "inventree/wedge";
+
+ private EventChannel.EventSink eventSink;
+
+ private final BroadcastReceiver scanReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) return;
+
+ String action = intent.getAction();
+ if (action == null) return;
+
+ // Check if it matches one of the strings
+ if (!ACTION_CUSTOM_SCAN.equals(action) && !ACTION_DATALOGIC_DECODE.equals(action)) {
+ return;
+ }
+
+ String data = intent.getStringExtra("com.symbol.datawedge.data_string");
+ if (data == null || data.isEmpty()) {
+ data = intent.getStringExtra("com.datalogic.decode.intentwedge.barcode_string");
+ }
+ if (data == null || data.isEmpty()) return;
+ if (eventSink == null) return;
+
+ Map payload = new HashMap<>();
+ payload.put("data", data);
+ payload.put("action", action);
+ eventSink.success(payload);
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Auto-configure the datawedge for Zebra/Datalogic
+ WedgeConfigurator.configure(this);
+ }
+
+ @Override
+ public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
+ super.configureFlutterEngine(flutterEngine);
+
+ new EventChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), EVENT_CHANNEL)
+ .setStreamHandler(new EventChannel.StreamHandler() {
+ @Override
+ public void onListen(Object arguments, EventChannel.EventSink events) {
+ eventSink = events;
+ }
+
+ @Override
+ public void onCancel(Object arguments) {
+ eventSink = null;
+ }
+ });
+
+ new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), MANAGED_CONFIG_CHANNEL)
+ .setMethodCallHandler((call, result) -> {
+ if ("getManagedConfiguration".equals(call.method)) {
+ Map config = new HashMap<>();
+
+ RestrictionsManager rm =
+ (RestrictionsManager) getSystemService(Context.RESTRICTIONS_SERVICE);
+
+ if (rm != null) {
+ Bundle restrictions = rm.getApplicationRestrictions();
+
+ if (restrictions != null) {
+ for (String key : restrictions.keySet()) {
+ @SuppressWarnings("deprecation")
+ Object value = restrictions.get(key);
+
+ if (value instanceof Boolean
+ || value instanceof Integer
+ || value instanceof Long
+ || value instanceof Double
+ || value instanceof String) {
+ config.put(key, value);
+ } else if (value instanceof String[]) {
+ config.put(key, java.util.Arrays.asList((String[]) value));
+ } else if (value != null) {
+ config.put(key, value.toString());
+ }
+ }
+ }
+ }
+
+ result.success(config);
+ } else {
+ result.notImplemented();
+ }
+ });
+
+ new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), WEDGE_CHANNEL)
+ .setMethodCallHandler((call, result) -> {
+ if ("detectWedge".equals(call.method)) {
+ String vendor = WedgeConfigurator.detectVendor(this);
+
+ Map info = new HashMap<>();
+ info.put("vendor", vendor);
+ info.put("supported", !WedgeConfigurator.VENDOR_NONE.equals(vendor));
+
+ result.success(info);
+ } else {
+ result.notImplemented();
+ }
+ });
+ }
+
+ @Override
+ public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) {
+ eventSink = null;
+ super.cleanUpFlutterEngine(flutterEngine);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_CUSTOM_SCAN);
+ filter.addAction(ACTION_DATALOGIC_DECODE);
+ filter.addCategory(CATEGORY_DATALOGIC_DECODE);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ registerReceiver(scanReceiver, filter, Context.RECEIVER_EXPORTED);
+ } else {
+ registerReceiver(scanReceiver, filter);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ try {
+ unregisterReceiver(scanReceiver);
+ } catch (IllegalArgumentException ignored) {
+ // Receiver was never registered
+ }
+ super.onPause();
+ }
+
}
diff --git a/android/app/src/main/java/inventree/inventree_app/WedgeConfigurator.java b/android/app/src/main/java/inventree/inventree_app/WedgeConfigurator.java
new file mode 100644
index 00000000..2af774fe
--- /dev/null
+++ b/android/app/src/main/java/inventree/inventree_app/WedgeConfigurator.java
@@ -0,0 +1,225 @@
+package inventree.inventree_app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import com.datalogic.decode.BarcodeManager;
+import com.datalogic.decode.configuration.IntentDeliveryMode;
+import com.datalogic.decode.configuration.ScannerProperties;
+import com.datalogic.device.ErrorManager;
+import com.datalogic.device.configuration.ConfigException;
+
+public class WedgeConfigurator {
+
+ // Zebra specific settings
+ private static final String DATAWEDGE_PROFILE = "InvenTree";
+ public static final String ZEBRA_BROADCAST_INTENT = "2";
+ private static final String DATAWEDGE_API_ACTION = "com.symbol.datawedge.api.ACTION";
+ private static final String DATAWEDGE_SET_CONFIG = "com.symbol.datawedge.api.SET_CONFIG";
+
+ // Datalogic retry settings
+ private static final int DATALOGIC_MAX_ATTEMPTS = 5;
+ private static final long DATALOGIC_RETRY_DELAY_MS = 2000;
+
+ // Vendor identifiers
+ public static final String VENDOR_ZEBRA = "zebra";
+ public static final String VENDOR_DATALOGIC = "datalogic";
+ public static final String VENDOR_NONE = "none";
+
+ private static final Map VENDOR_PACKAGES = buildVendorPackages();
+
+ // Spawn a new thread for Datalogic calls
+ private static final ExecutorService DATALOGIC_EXECUTOR = Executors.newSingleThreadExecutor();
+
+ private static Map buildVendorPackages() {
+ Map map = new LinkedHashMap<>();
+
+ map.put(VENDOR_ZEBRA, new String[] {"com.symbol.datawedge"});
+
+ map.put(
+ VENDOR_DATALOGIC,
+ new String[] {
+ "com.datalogic.decode",
+ "com.datalogic.decodewedge",
+ "com.datalogic.systemsettings"
+ });
+
+ return map;
+ }
+
+ public static String detectVendor(Context context) {
+ try {
+ PackageManager pm = context.getPackageManager();
+
+ if (pm == null) {
+ return VENDOR_NONE;
+ }
+
+ for (Map.Entry entry : VENDOR_PACKAGES.entrySet()) {
+ for (String packageName : entry.getValue()) {
+ if (isPackageInstalled(pm, packageName)) {
+ return entry.getKey();
+ }
+ }
+ }
+ } catch (Throwable e) {
+ // PackageManager lookup failed
+ }
+
+ return detectVendorByManufacturer();
+ }
+
+ private static String detectVendorByManufacturer() {
+ String manufacturer = Build.MANUFACTURER == null ? "" : Build.MANUFACTURER.toLowerCase();
+
+ if (manufacturer.contains("zebra") || manufacturer.contains("symbol")) {
+ return VENDOR_ZEBRA;
+ }
+
+ if (manufacturer.contains("datalogic")) {
+ return VENDOR_DATALOGIC;
+ }
+
+ return VENDOR_NONE;
+ }
+
+ private static boolean isPackageInstalled(PackageManager pm, String packageName) {
+ try {
+ pm.getPackageInfo(packageName, 0);
+ return true;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ } catch (Throwable e) {
+ return false;
+ }
+ }
+
+ public static void configure(Context context) {
+ try {
+ String vendor = detectVendor(context);
+
+ if (VENDOR_ZEBRA.equals(vendor)) {
+ configureZebra(context);
+ } else if (VENDOR_DATALOGIC.equals(vendor)) {
+ scheduleDatalogicConfig(1);
+ }
+ } catch (Throwable e) {
+ // Vendor detection was unsuccessful
+ }
+ }
+
+ /**
+ * Schedule the Datalogic wedge configuration.
+ * @param attempt Current attempt number
+ */
+ private static void scheduleDatalogicConfig(int attempt) {
+ new Handler(Looper.getMainLooper())
+ .postDelayed(
+ () -> DATALOGIC_EXECUTOR.execute(() -> {
+ try {
+ configureDatalogic();
+ } catch (Throwable e) {
+ if (attempt < DATALOGIC_MAX_ATTEMPTS) {
+ scheduleDatalogicConfig(attempt + 1);
+ }
+ }
+ }),
+ DATALOGIC_RETRY_DELAY_MS);
+ }
+
+ /**
+ * Configure Zebra datawedge by creating a custom profile for the app.
+ * @param context MainActivity instance passed in as context
+ */
+ private static void configureZebra(Context context) {
+ Bundle profileConfig = new Bundle();
+ profileConfig.putString("PROFILE_NAME", DATAWEDGE_PROFILE);
+ profileConfig.putString("PROFILE_ENABLED", "true");
+ profileConfig.putString("CONFIG_MODE", "CREATE_IF_NOT_EXIST");
+
+ Bundle appConfig = new Bundle();
+ appConfig.putString("PACKAGE_NAME", context.getPackageName());
+ appConfig.putStringArray("ACTIVITY_LIST", new String[] {"*"});
+ profileConfig.putParcelableArray("APP_LIST", new Bundle[] {appConfig});
+
+ ArrayList plugins = new ArrayList<>();
+
+ Bundle barcodePlugin = new Bundle();
+ barcodePlugin.putString("PLUGIN_NAME", "BARCODE");
+ barcodePlugin.putString("RESET_CONFIG", "true");
+
+ Bundle barcodeProps = new Bundle();
+ barcodeProps.putString("scanner_input_enabled", "true");
+ barcodeProps.putString("scanner_selection", "auto");
+ barcodePlugin.putBundle("PARAM_LIST", barcodeProps);
+ plugins.add(barcodePlugin);
+
+ Bundle intentPlugin = new Bundle();
+ intentPlugin.putString("PLUGIN_NAME", "INTENT");
+ intentPlugin.putString("RESET_CONFIG", "true");
+
+ Bundle intentProps = new Bundle();
+ intentProps.putString("intent_output_enabled", "true");
+ intentProps.putString("intent_action", MainActivity.ACTION_CUSTOM_SCAN);
+ intentProps.putString("intent_delivery", ZEBRA_BROADCAST_INTENT);
+ intentPlugin.putBundle("PARAM_LIST", intentProps);
+ plugins.add(intentPlugin);
+
+ Bundle keystrokePlugin = new Bundle();
+ keystrokePlugin.putString("PLUGIN_NAME", "KEYSTROKE");
+ keystrokePlugin.putString("RESET_CONFIG", "true");
+
+ Bundle keystrokeProps = new Bundle();
+ keystrokeProps.putString("keystroke_output_enabled", "false");
+ keystrokePlugin.putBundle("PARAM_LIST", keystrokeProps);
+ plugins.add(keystrokePlugin);
+
+ profileConfig.putParcelableArrayList("PLUGIN_CONFIG", plugins);
+
+ Intent intent = new Intent();
+ intent.setAction(DATAWEDGE_API_ACTION);
+ intent.putExtra(DATAWEDGE_SET_CONFIG, profileConfig);
+ context.sendBroadcast(intent);
+ }
+
+ /**
+ * Attempt to apply Datalogic wedge configuration.
+ */
+ private static void configureDatalogic() {
+ ErrorManager.enableExceptions(false);
+
+ BarcodeManager manager = new BarcodeManager();
+
+ ScannerProperties configuration = ScannerProperties.edit(manager);
+
+ if (configuration == null) {
+ throw new IllegalStateException();
+ }
+
+ // Keyboard wedge only on focus of a text field
+ configuration.keyboardWedge.onlyOnFocus.set(true);
+ configuration.intentWedge.enable.set(true);
+ // Default action, category
+ configuration.intentWedge.action.set(MainActivity.ACTION_DATALOGIC_DECODE);
+ configuration.intentWedge.category.set(MainActivity.CATEGORY_DATALOGIC_DECODE);
+ // Enable broadcast
+ configuration.intentWedge.deliveryMode.set(IntentDeliveryMode.BROADCAST);
+
+ int result = configuration.store(manager, true);
+
+ if (result != ConfigException.SUCCESS) {
+ throw new IllegalStateException();
+ }
+ }
+}
diff --git a/lib/barcode/barcode.dart b/lib/barcode/barcode.dart
index d4a45fbc..1ae45478 100644
--- a/lib/barcode/barcode.dart
+++ b/lib/barcode/barcode.dart
@@ -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 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.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