From ca655f36fe551b1e84e5963a195db52133c61f4a Mon Sep 17 00:00:00 2001
From: Reza Fouladian <9205977+rezafouladian@users.noreply.github.com>
Date: Sat, 15 Aug 2026 20:59:20 -0700
Subject: [PATCH] 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.
---
android/app/build.gradle | 1 +
android/app/src/main/AndroidManifest.xml | 11 +
.../inventree/inventree_app/MainActivity.java | 166 ++++++++
.../inventree_app/WedgeConfigurator.java | 225 +++++++++++
lib/barcode/barcode.dart | 50 +++
lib/barcode/intent_controller.dart | 154 +++++++
lib/barcode/wedge_detect.dart | 67 ++++
lib/l10n/app_en.arb | 6 +
lib/main.dart | 42 +-
lib/managed_config.dart | 379 ++++++++++++++++++
lib/preferences.dart | 4 +
lib/settings/barcode_settings.dart | 10 +
12 files changed, 1114 insertions(+), 1 deletion(-)
create mode 100644 android/app/src/main/java/inventree/inventree_app/WedgeConfigurator.java
create mode 100644 lib/barcode/intent_controller.dart
create mode 100644 lib/barcode/wedge_detect.dart
create mode 100644 lib/managed_config.dart
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