Code readingHow-to

Event-driven barcode scanning on Android without the keyboard wedge

Set up event-driven barcode scanning on Android: register a broadcast receiver for the scan engine's decode intent and retire the keyboard wedge.

LumaSpec Vision9 min read Share on LinkedIn

On this page 11 sections
  1. 01Why the wedge limits an Android app
  2. 02Find the scan service and its documented event
  3. 03What you need
  4. 04Register a broadcast receiver for the decode intent
  5. 05One intent against eighteen keystrokes
  6. 06Keep scans arriving in the background
  7. 07Parse the payload once, at the boundary
  8. 08When the device maker documents nothing
  9. 09Field checks before rollout
  10. 10Where to go next
  11. 11Questions

An Android mobile computer with an integrated scan engine usually leaves the factory in keyboard wedge mode: pull the scan trigger and a background service types the decoded string into whatever field holds focus. That works until focus moves, until you need the symbology alongside the string, or until one scan must update three fields and post to a host. This guide is for developers wiring scans into their own Android app on a rugged handheld. You will find the device maker's scan service, switch it to intent output, register a broadcast receiver that receives each decode as one event, and prove the path end to end with adb before the first live scan.

Why the wedge limits an Android app

A wedge turns a decode into characters delivered to the focused input. The string arrives, but the symbology, the timestamp and the fact that a scanner fired do not. Delivery rides on input focus: move it, or send the app to the background, and the next scan lands somewhere else or nowhere.

The failure is not Android-specific. It is the same mechanism behind Barcode scanner keyboard wedge into an HMI: why scans drop over RDP, and the same wedge pattern that links a barcode reader to HMI software on SCADA screens. An event model removes focus from the equation: one message, addressed to your code, with the symbology and a timestamp attached.

If your only scanner is the Android camera itself, Android barcode scanning without a scanner app: your own preview loop covers grabbing frames and decoding them in-process. The engine path here is different: a scan service already does the decoding, and your job is to receive its result as an event instead of letting it be typed.

Find the scan service and its documented event

Once the engine is built into the mobile computer, the device maker owns the integration. Their preloaded scan app answers the scan key, and that app, or a companion utility, is where output modes live. Look for a delivery setting that offers an intent, broadcast or application mode beside the keystroke mode: several Android makers ship exactly that pairing.

Request the integration note at the same time. It should give you the intent action string, the extra key names and types for the decoded string and the symbology, and any SDK that binds to the service directly. Engine-maker demo apps rarely help on another maker's hardware, because their engine libraries are absent there — which is why the demo-app route usually ends in missing-library errors.

What you need

  • The mobile computer, its integrated engine and the preloaded scan utility, so you can switch output modes yourself.
  • The maker's integration note: the action string, the data and symbology extra keys, and their types.
  • An Android Studio project in Kotlin that you control.
  • Test labels: a clean code, a damaged one, and one symbology your note names.
  • adb on your workstation, for the synthetic-intent test in step 6.

Register a broadcast receiver for the decode intent

  1. Switch the output mode. In the maker's scan utility, select intent or broadcast output and disable keystroke output. Keep one delivery path per workflow: with both enabled, a single scan types into the field and fires the broadcast, and every count runs double.

  2. Copy the identifiers exactly. Take the action string and the extra key names from the note, character for character. Extra keys match case-sensitively, and a mismatched name returns null, which quietly empties the event.

  3. Declare the constants and the receiver. BroadcastReceiver, Intent, IntentFilter and getStringExtra are Android platform API names; the action and extra strings below are illustrative, and ScanEvent and scanHub stand for the types your own code defines.

private const val ACTION_DECODE = "com.example.scan.ACTION_DECODE"
private const val EXTRA_DATA = "decode_data"
private const val EXTRA_SYMBLOGY = "decode_type"

data class ScanEvent(
    val data: String,
    val symbology: String,
    val atMillis: Long
)

private val decodeReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action != ACTION_DECODE) return
        val data: String = intent.getStringExtra(EXTRA_DATA) ?: return
        scanHub.publish(
            ScanEvent(
                data = data,
                symbology = intent.getStringExtra(EXTRA_SYMBLOGY).orEmpty(),
                atMillis = System.currentTimeMillis()
            )
        )
    }
}
  1. Register at runtime, unregister on schedule. Pair the two calls so the receiver neither leaks nor outlives its UI:
private val decodeFilter = IntentFilter(ACTION_DECODE)

override fun onStart() {
    super.onStart()
    registerReceiver(decodeReceiver, decodeFilter)
}

override fun onStop() {
    unregisterReceiver(decodeReceiver)
    super.onStop()
}

Recent Android releases also ask whether a runtime receiver accepts broadcasts from other apps; the scan service is another app sending an implicit broadcast, so set the export flag as the maker's note directs.

  1. Parse once, dispatch everywhere. Build the ScanEvent at the receiver boundary and let the rest of the app consume events only. A null data extra means a key name is wrong, not that the scan failed: log the intent's extras and compare them against the note. Guard the first scan as well, since a handler that compares against an uninitialised previous value drops it — Barcode scanner first scan ignored: fix the null previousValue is that bug, named and fixed.

  2. Prove the path with a synthetic intent. From your workstation:

adb shell am broadcast -a com.example.scan.ACTION_DECODE --es decode_data "LOT-4471-2025-014"

Your log should print the same event object a trigger pull produces. Then scan live: a clean label, then the damaged one, and confirm the symbology extra carries the value the note promises.

One intent against eighteen keystrokes

Count the win on a real payload. LOT-4471-2025-014 is 17 characters long. In wedge mode the service injects 17 key events plus Enter — 18 input events that whatever holds focus must survive in order. In event mode the same decode is one broadcast intent whose data extra carries a 17-byte string, one ASCII byte per character, delivered to your receiver with focus out of the picture.

wedge input events = characters + 1 = 17 + 1 = 18 intent payload = characters × 1 byte = 17 × 1 = 17 bytes, one delivery

Your handler logs one line and dispatches from it:

ScanEvent(data=LOT-4471-2025-014, symbology=Code128, chars=17, bytes=17, atMillis=1767225867412)

String, symbology and timestamp travelled together in one object, and the lot field, the host post and the archive all read the same ScanEvent.

Keep scans arriving in the background

An activity-scoped receiver stops with the activity. If scans must be captured with no screen on — freezer-aisle checks, pallet moves between floors — move the registration into a foreground service that holds the receiver and queues events for the UI. The manifest route is closed: from Android 8.0 (API level 26), a manifest-declared receiver no longer receives most implicit broadcasts, and a scanner intent is implicit.

Queue, then consume, and keep the handler one-way: if it writes back into the list that raised the event you get re-entrant updates, the problem Barcode scan event handling without recursive updates untangles. Declare the service and its foreground type in the manifest, and run one scanning service, not one per screen.

Parse the payload once, at the boundary

The receiver hands you raw extras; build one ScanEvent and pass it inward. Split lot, part and quantity at that boundary, the way Split barcode string data into fields: parse lot and part numbers shows, and stamp every event with symbology and time. That metadata is what makes decodes auditable: a read without its symbology and timestamp is hard to score later, which is the case Barcode read confidence: how to judge whether a decode is trustworthy makes. If the maker's extras omit symbology, derive it from your label set — Code 128 on logistics labels, DataMatrix on small parts — and record that the value is assumed.

When the device maker documents nothing

The integration note is the gate. If the maker will not supply the action string, the extras or an SDK, the event path on that device stays closed, and receiver code cannot open it. Ask for the note in writing; industrial handheld makers hand it to app developers routinely. If it never arrives, move where the decode is born instead:

  • A standalone handheld code reader connects to the host program over its documented interface, and the event handling moves to that host.
  • A fixed reader at the bench does the same job for a station that never moves.
Handheld code readersHikrobot MV-IDH2000P/13NR/05RN/U Industial Wired Handheld Code ReaderMV-IDH2000P/13NR/05RN/U
Interface
USB
Connection
Wired
$179.00View part
Handheld code readersHikrobot MV-IDH5000P/13SR/04RN/U General-purpose wired industrial handheld code readerMV-IDH5000P/13SR/04RN/U
Interface
USB
Connection
Wired
$719.00View part
Handheld code readersHikrobot MV-IDH9000B/13DP/04RP/LS Ultra-rugged Wireless Handheld Code Reader PackageMV-IDH9000B/13DP/04RP/LS
Connection
Wireless
$2,979.00 · quotedView and quote

The first two are wired USB handhelds for a single workstation; the third is a wireless package for scan points away from the host. For a fixed position, the LS-CRD-0047 covers a bench: 1.2 MP frames over USB 2.0 at a 60-120 mm reading distance, with the decode delivered over the cable as data.

Fixed code readersCompact fixed industrial barcode reader, 1.2 MP, USB 2.0, 60-120 mm reading distance (LS-CRD-0047)LS-CRD-0047
Resolution
Up to 3 MP
Shutter
Global
Interface
USB
$559.00View part

Planning the link from those readers into a PLC or host is its own task: the input options sit in Barcode scanner input methods to a PLC: serial, USB and Ethernet, and the network side in TCP client or listener: match your network barcode reader to your host.

Field checks before rollout

  • One delivery path. Scan five codes in a row; the app must log five events, not ten.
  • Focus independence. Put a different app in the foreground, pull the trigger, and confirm your queue grows while no text lands anywhere.
  • Rough labels. Include creased and low-contrast codes; if read rates sag, the fixes live in the image pipeline — Improve barcode decode rate on damaged low contrast codes lists them.
  • Cold start. Reboot the device and scan once: the service and your receiver must deliver from the first scan after boot, not the second.
  • Clock agreement. Compare one logged atMillis against the device clock your archive uses, so event order survives a shift change.

Where to go next

The order of play: get the integration note, switch the output mode, register the receiver, test with adb, then scan live. Equipping a crew rather than one device starts at the hardware: handheld code readers for mobile work, fixed code readers for stations, or the code reader finder to filter both. More decode-side guides sit on the code-reading topic page, and integration questions go through the contact page.

Questions

What should I ask the device maker for before writing code?

Ask for the scan service's integration note: the exact intent action string, the names and data types of the extras that carry the decoded string and the symbology, how to switch the output mode in the preloaded scan utility, and whether an SDK or binder interface exists for two-way control such as triggering and illumination. With those four items the receiver code takes minutes; without them the event path is closed.

Why does one scan appear twice in my app?

Both output paths are active. The scan service typed the string into the focused field and also sent the broadcast, so the field edit and your receiver each register the decode. Open the maker's scan utility and disable keystroke output, leaving intent mode as the only delivery path, then repeat the scan. If duplicates persist, check for a second receiver registered in a fragment that was not destroyed.

Can I bind to the scan service and get callbacks instead of broadcasts?

Some device makers expose an SDK with a service you bind to; the decode then arrives as an in-process callback carrying the same fields, which gives you ordered delivery and two-way calls such as remote triggering. It ties your build to that maker's classes, so keep the broadcast receiver as a fallback and isolate the SDK behind one interface in your code.

How do I test the receiver without pulling the scan trigger?

Send the intent yourself from a computer over adb: adb shell am broadcast with the documented action and the data extra set to a known string. Your app should log the same event object a real scan produces. A synthetic intent proves your receiver, filter and parsing, but not the engine's own extras, so finish with live scans of valid and damaged labels.

Does the same event model work for a USB or Bluetooth handheld scanner?

Usually not out of the box. Most handhelds deliver HID keystrokes to the host unless the vendor offers an SDK or app mode, and cordless sets typically pair with a base station that speaks serial or USB to a host. The event-driven pattern then lives in the host application, not in Android, so plan the link and the parsing there.