I'm Andrew Hoefling, and I work for FileOnQ as a Lead Software Engineer building mobile technologies for Government, Financial and First Responders using Xamarin. 

 

Xamarin Android Barcode Scanning with Zebra’s EMDK on TC70x


The Zebra TC70x is a mobile touch computer or android device with built-in hardware barcode scanning. This allows developers to create rich barcode scanning capable apps without using the camera. If you have ever built a barcode scanning app that uses a camera there is a lot of switching back and forth between the camera and the page. This really isn’t ideal when the app requires scanning multiple barcodes in sequence.

Zebra provides 2 libraries for handling barcode scanning DataWedge and EMDK there are reasons to use both and Zebra’s official recommendation as of today is to use the DataWedge I am personally a fan of the EMDK library as it gives your app 100% custom control to everything including your configuration pages. With great power comes great responsibility, which holds true with the EMDK library. There are a lot of moving parts to keep straight and it is not as easy as plug and play.

Overview: Barcode Scanning

The goal is to get hardware access to the barcode sanner to execute custom code when the Android App receives data from the hardware. In our example we are going to isolate the scanning code from any Android Activity.

The Specification: EMDK Wrapper

With any good code wrapper it will be best to define our API prior to implementation, this will give us a good foundation on what we are building and how the client will be using it.

public class IBarcodeScannerManager
{
    event EventHandler<Scanner.DataEventArgs> ScanReceived;
    bool IsScannerEnabled { get; }
    void Enable();
    void Disable();
}

Once we have an implementation to the IBarcodeScannerManager we can enable the scanner and listen for events anywhere in our app.

IBarcodeScannerManager manager = new BarcodeScannerManager(this);
manager.Enable();
manager.ScanReceived += (s, e) => 
{
    // Do something with the received data
};

Example App: Scanner Toggle

Our example is going to be a simple app with a toggle button the prints the scanned item to the screen. Below is a screenshot of what it may look like when you are done.

Build the MainActivity

We are now ready to get started building our Android App. Before we start coding our wrapper it is best to have our Android code completed. 

  1. Create a new Blank Android App using Visual Studio
  2. Run the app and make sure everything compiles

Build the Layout xml

Our app is going to have 2 main controls

  • TextView - Which will render the last scanned barcode
  • Button - Toggles the barcode scanner on and off

This isn't a blog about android layouts so you can safely copy and paste this layout into your MainActivity Layout Resource. Ours was locations at: Resources -> layout -> activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:text=""
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:minWidth="25px"
            android:minHeight="25px"
            android:id="@+id/last_scan_received" />
    </FrameLayout>
    <Button
        android:text="Toggle Scanner"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal | bottom"
        android:id="@+id/toggle_scanner_button"/>
    
</FrameLayout>

The MainActivity

At this point our MainActivity won't have many changes to it, here is our current MainActivity code. I omited Xamarin Essentials from this example, if you create a new Xamarin Android project it ships with Xamarin Essentials out of the box so your code may look slightly different than this.

[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity, View.IOnClickListener
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
	
        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.activity_main);
    }
}

Make sure to run your app at this point and verify that everything is rendering correctly.

Build the EMDK Wrapper

Now that the app is built and we have the necessary controls we can start building our Barcode Scanning EMDK Wrapper implementation.  Earlier we created our specification as a C# interface. Go ahead and create that exact file in your project if you have not done so yet.

Add EMDK NuGets

There is both a native android and a C# Binding Xamarin Android library for the EMDK library. Both are provided by Zebra and it is recommended to use the official library. I tried going down a different path when I first implemented this and it was not a good idea, use the official library. 

  • Symbol.XamarinEMDK

Once the official library is installed into your project you are ready to start taking advantage of the hardware barcode scanner.

Create the Barcode Manager

Create a new C# file called BarcodeScannerManager and create a default implementation of the EMDKManager.IEMDKListener

public class BarcodeScannerManager : Java.Lang.Object, EMDKManager.IEMDKListener
{
    public void OnOpened(EMDKManager manager)
    {
    }

    public void OnClosed()
    {
    }
}

Let's breakdown everything going on into 2 sections

  • Java.Lang.Object
  • EMDKManager.IEMDKListener

Java.Lang.Object

The EMDK Library requires an instance of the Android Context which comes from your MainActivity in our case, or any Activity that leverages our EMDK Wrapper. Since the Xamarin EMDK library is just a wrapper around their native android library everything is expected to be a Java.Lang.Object instead of .NET object. 

If your wrapper does not inherit from Java.Lang.Object your library will not work correctly and the EMDK library will throw exceptions.

EMDKManager.IEMDKListener

The EMDKManager.IEMDKListener provides initialization and cleanup methods that allow the system to properly communicate with the native hardware. The wrapper iteself doesn't initialize the EMDKManager, it just creates a callback that passes an instance of the EMDKManager back to our wrapper.

  • OnOpened() - Callback that contains an instance of the EMDKManager which gives the wrapper hardware access
  • OnClosed() - Callback that is invoked by the EMDK Library to clean-up any resources that need to be collected by the Garbage Collector. This method needs to deallocate event handlers and manually dispose of any objects that can't be natively collected by the Garbage Collector. 

Initialize the EMDK Library

It is time to build our initialize implemtnation of the library, which like many Android objects is a constructor that passes an instance of the current Android Context.

Usage

// Invoked from the MainActivity, we can pass the instance of
// the MainActivity which is a child of the android Context.
// If this code is invoked outside of an Android Context it will 
// need to have a reference to the Context to work.
BarcodeScannerManager manager = new BarcodeScannerManager(this);

Implementation

public class BarcodeScannerManager : Java.Lang.Object, EMDKManager.IEMDKListener
{
    private EMDKManager _emdkManager;
    
    public BarcodeScannerManager(Context context)
    {
        InitializeEMDK(context);
    }

    private void InitializeEMDK(Context context)
    {
        // To retrieve the EMDKManager correctly this code requires
        // an instance of EMDKMAnager.IEMDKListener which is why 
        // we can pass the current instance via 'this'
        EMDKResults results = EMDKManager.GetEMDKManager(context, this);
        if (results.StatusCode != EMDKResults.STATUS_CODE.Success)
        {
            // If there is a problem initializing throw an exception
            throw new InvalidOperationException("Unable to initialize EMDK Manager");
        }
    }

    public void OnOpened(EMDKManager manager)
    {
        // set the instance of the manager to the passed in value
        _emdkManager = manager;
    }

    public void OnClosed()
    {
        if (_emdkManager != null)
        {
            // Not that this is calling 'Release' instead of 'Dispose'
            _emdkManager.Release();
            _emdkManager = null;
        }
    }
}

 There is a lot of code to digest here, let's break it all down

  • Retrieve the EMDKManager
  • Garbage Collection

Initialize the EMDKManager

The EMDK library requires an instance of

  • Android Context
  • IEMDKListener

public BarcodeScannerManager(Context context)
{
	InitializeEMDK(context);
}

private void InitializeEMDK(Context context)
{
	var results = EMDKManager.GetEMDKManager(context, this);
	if (results.StatusCode != EMDKResults.STATUS_CODE.Success)
		throw new InvalidOperationException("Unable to initialize EMDK Manager");
}

The InitializeEMDK() method invokes the GetEMDKManager which handles creating an instance of the EMDKManager internally and then uses the callback API OnOpened() to pass a valid instance back to our BarcodeScannerManager.

Our wrapper doesn't create a direct instance, it is only retrieved via callback as the instantiation is handled by the native Android EMDK Library.

Retrieve the EMDKManager

Since our BarcodeScanningManager implements the IEMDKListener when we request the EMDKManager the created instance will be sent to our code via the OnOpened() API.

EMDKManager _emdkManager;
public void OnOpened(EMDKManager manager)
{
	_emdkManager = manager;
}

Garbage Collection

When it is time to Dispose the BarcodeScannerManager our code need to be sure it disposes and releases any memory that may be held on. If this is not done a memory leak will occur which will start causing-side effects. 

When an object is unused but still holds a lock onto an event or other objects it uses internally, the Garbage Collector will not deallocate the data.

public void OnClosed()
{
    if (_emdkManager != null)
    {
        // Not that this is calling 'Release' instead of 'Dispose'
        _emdkManager.Release();
        _emdkManager = null;
    }
}

Initialize Barcode Scanning

Now that EMDKManager is initialized it can be configured to use the barcode scanner. Since the EMDK Library can be used for more than just barcode scanning, it needs to turn on the barcode scanning capabilities.

Add a new InitializeBarcodeManager() method and make sure to invoke it when the Manager is initialized.

EMDKManager _emdkManager;
BarcodeManager _barcodeManager;
Scanner _scanner;

public void OnOpened(EMDKManager manager)
{
    // set the instance of the manager to the passed in value
    _emdkManager = manager;

    // We can only initialize the barcode manager once we retrieve
    // the EMDK Manager, which is invoked by via callback in the
    // EMDK library
    InitializeBarcodeManager();
}

void InitializeBarcodeManager()
{
    _barcodeManager = (BarcodeManager)_emdkManager?.GetInstance(EMDKManager.FEATURE_TYPE.Barcode);
    if (_barcodeManager == null)
        return;

    if (_barcodeManager.SupportedDevicesInfo?.Count > 0)
        _scanner = _barcodeManager.GetDevice(BarcodeManager.DeviceIdentifier.Default);
}

This is where the BarcodeScannerManager can tell the EMDK Library what features to use including what type of scanner to use. The EMDK library doesn't just allow you to use the embeded barcode scanner, but also connected bluetooth barcode scanners. The initialization logic here allows the manager to define what scanner to use.

Release The Scanner Object

Now that we have some new EMDK objects, they need to be properly disposed and released so the Garbage Collector can properly dispose of the BarcodeScannerManager. During the OnClosed() method, just ensure that the Scanner and is properly disposed.

public void OnClosed()
{
    if (_scanner != null)
    {
        // Ensure the scanner is not active
        _scanner.Disable();
        _scanner.Release();
        _scanner = null;
    }

    if (_emdkManager != null)
    {
        // Not that this is calling 'Release' instead of 'Dispose'
        _emdkManager.Release();
        _emdkManager = null;
    }
}

Implement Specification

Earlier we defined a custom Barcode Scanning Specification and now that we have everything configured with our Scanner and EMDK it is time to start implenting our interface. Go ahead an update our class inheritance to include IBarcodeScanningManager and create the default implementations.

public class BarcodeScannerManager : Java.Lang.Object, EMDKManager.IEMDKListener, IBarcodeScannerManager
{
    // omitted manager code from above for brevity
    public event EventHandler<Scanner.DataEventArgs> ScanReceived;
    public bool IsEnabled { get; private set; }

    public void Enable()
    {
        // Todo - implement enable scanner logic
    }

    public void Disable()
    {
        // Todo - implement disable scanner logic
    }
}

ScanReceived Event Handler

The event handler will be useful later to listen for scans that EMDK library receives from the hardware scanner. We don't need to do anything else at this point.

Enable Scanner

There are a few moving parts while enabling the scanner, but all of our actions will be using the Scanner object we created earlier in the initialization logic. We can simplify the complexity here to a few steps

  1. Check if the Scanner is initialized
  2. Start listening to Scanner Status Event
  3. Start listening to Scan Received Event
  4. Enable Scanner
  5. Configure Scanner to only activate on hardware button press

public void Enable()
{
    // Just in case the manager isn't initialized, check for null
    if (_scanner == null)
        return;

    // Wire-up EMDK scanner events
    _scanner.Data += OnScanReceived;
    _scanner.Status += OnStatusChanged;

    // Enable the scanner
    _scanner.Enable();

    // Define the scanner to trigger by the hardware scanner button
    _scanner.TriggerType = Scanner.TriggerTypes.Hard;

    // Update the state of the scanner property
    IsScannerEnabled = true;
}

private void OnScanReceived(object sender, Scanner.DataEventArgs args)
{
    // Pass the EMDK events to the our new event handler
    ScanReceived?.Invoke(sender, args);
}

private void OnStatusChanged(object sender, Scanner.StatusEventArgs args)
{
    if (args?.P0?.State == StatusData.ScannerStates.Idle)
    {
        // Wait for 100ms before starting the next scan listen
        Task.Delay(100);

        // When the device idle's it is time to listen for a new scan
        _scanner.Read();
    }
}

Why Null Check?

 The EMDK library can be very touchy and on a development device it can get into an invalid state very easily. Especially if you are working on an app or use an app that doesn't have a complete EMDK installation. I recommend doing greedy null checks whenever possible to prevent runtime errors.

ScanReceived Event

In our demo here we wrap the EMDK Scanner.StatusEventArgs, you can create a separate model abstraction if you choose, but for this sample we just expose the existing event arguments.

Disable Scanner

It is time to disable the scanner, this is going to be easier than the Enable() method but we still need to make sure we deallocate our event listeners otherwise there will be memory leaks. You don't want to have duplicate scan events being sent to your business logic.

  1. deallocate event listener for Scanner Status Event
  2. deallocate event listener for Scan Received Event
  3. Disable Scanner

public void DisableScanner()
{
    if (_scanner == null)
        return;

    // Deallocate event listeners to prevent memory leaks
    _scanner.Data -= OnScanReceived;
    _scanner.Status -= OnStatusChanged;

    // Notify the EMDK Library to turn the barcode scanner off
    _scanner.Disable();

    // Reset the status of the manager
    IsScannerEnabled = false;
}

Complete EMDK Wrapper

If you have been following along you deserve a break, the EMDK Wrapper is not simple and there is a lot of keep track of. Take a look at the complete BarcodeScannerManager below and verify that your code is executing the same commands.

If you are just looking for the code sample to see if this stuff works, here is the complete BarcodeScannerManager. You should be able to drop this into your project, include the Symbol.XamarinEMDK NuGet and it should work.

public class BarcodeScannerManager : Java.Lang.Object, EMDKManager.IEMDKListener, IBarcodeScannerManager
{
    // EMDK variables
    private EMDKManager _emdkManager;
    private BarcodeManager _barcodeManager;
    private Scanner _scanner;
    
    // IBarcodeScannerManager Properties
    public event EventHandler<Scanner.DataEventArgs> ScanReceived;
    public bool IsScannerEnabled { get; private set; }

    public BarcodeScannerManager(Context context)
    {
        InitializeEMDK(context);
    }

    void InitializeEMDK(Context context)
    {
        // To retrieve the EMDKManager correctly this code requires
        // an instance of EMDKMAnager.IEMDKListener which is why 
        // we can pass the current instance via 'this'
        EMDKResults results = EMDKManager.GetEMDKManager(context, this);
        if (results.StatusCode != EMDKResults.STATUS_CODE.Success)
        {
            // If there is a problem initializing throw an exception
            throw new InvalidOperationException("Unable to initialize EMDK Manager");
        }
    }

    void InitializeBarcodeManager()
    {
        _barcodeManager = (BarcodeManager)_emdkManager?.GetInstance(EMDKManager.FEATURE_TYPE.Barcode);
        if (_barcodeManager == null)
            return;

        if (_barcodeManager.SupportedDevicesInfo?.Count > 0)
            _scanner = _barcodeManager.GetDevice(BarcodeManager.DeviceIdentifier.Default);
    }

    public void OnClosed()
    {
        if (_scanner != null)
        {
            // Ensure the scanner is not active
            _scanner.Disable();
            _scanner.Release();
            _scanner = null;
        }
 
        if (_emdkManager != null)
        {
            // Not that this is calling 'Release' instead of 'Dispose'
            _emdkManager.Release();
            _emdkManager = null;
        }
    }

    public void OnOpened(EMDKManager manager)
    {
        _emdkManager = manager;

        // We can only initialize the barcode manager once we retrieve
        // the EMDK Manager, which is invoked by via callback in the
        // EMDK library
        InitializeBarcodeManager();
    }

    public void Enable()
    {
        // Just in case the manager isn't initialized, check for null
        if (_scanner == null)
            return;
 
        // Wire-up EMDK scanner events
        _scanner.Data += OnScanReceived;
        _scanner.Status += OnStatusChanged;
 
        // Enable the scanner
        _scanner.Enable();
 
        // Define the scanner to trigger by the hardware scanner button
        _scanner.TriggerType = Scanner.TriggerTypes.Hard;
 
        // Update the state of the scanner property
        IsScannerEnabled = true;
    }

    public void Disable()
    {
        if (_scanner == null)
            return;
 
        // Deallocate event listeners to prevent memory leaks
        _scanner.Data -= OnScanReceived;
        _scanner.Status -= OnStatusChanged;
 
        // Notify the EMDK Library to turn the barcode scanner off
        _scanner.Disable();
 
        // Reset the status of the manager
        IsScannerEnabled = false;
    }

    void OnScanReceived(object sender, Scanner.DataEventArgs args)
    {
        // Pass the EMDK events to the our new event handler
        ScanReceived?.Invoke(sender, args);
    }
 
    void OnStatusChanged(object sender, Scanner.StatusEventArgs args)
    {
        if (args?.P0?.State == StatusData.ScannerStates.Idle)
        {
            // Wait for 100ms before starting the next scan listen
            Task.Delay(100);
 
            // When the device idle's it is time to listen for a new scan
            _scanner.Read();
        }
    }
}

Confused?

It is okay if the code sample above is confusing, there is a lot going on. All of this code is explained above, if you have questions or something is missing please leave a comment.

Connect Wrapper to UI

Now that the BarcodeScannerManager is complete we can now connect the MainActivity and start listening for barcode scans.

Instantiate the Wrapper

Start by instantiating the BarcodeScannerManager in the MainActivity

[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity, View.IOnClickListener
{
    IBarcodeScannerManager _scannerManager;
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.activity_main);

        _scannerManager = new BarcodeScannerManager(this);
        _scanner.ScanReceived += OnScanReceived;        
    }

    void OnScanReceived(object snder, Scanner.DataEventArgs args)
    {
        // Handle scan data
    }
}

Garbage Collection

As we have been discussing with every new piece of code from the EMDK Library it is very important with this hardware integration to always clean-up memory when you are done. Leveraging the OnDestroyed() method we will dispose of our manager and clean up the object. This will allow the Garbage Collector to properly delete the memory being used.

protected override void OnDestroy()
{
    base.OnDestroy();

    _scannerManager.ScanReceived -= OnScanReceived;
    _scannerManager.Dispose();
    _scannerManager = null;
}

Listen for Button Click

Implement the button click listener to toggle the barcode scanner on tap.

var toggleScannerButton = FindViewById<Button>(Resource.Id.toggle_scanner_button);

// The MainActivity should implement the View.IOnClickListener
// which allows us to pass 'this' instance as our event listener
toggleScannerButton.SetOnClickListener(this);

Enable/Disable Scanner On Click

public void OnClick(View v)
{
    if (_scannerManager.IsScannerEnabled)
        _scannerManager.DisableScanner();
    else
        _scannerManager.EnableScanner();
}

Write Barcode Data to TextView

Using the ScanRecieved event we can print out the barcode data to the screen. Earlier we created a TextView in the MainActivity layout. Let's print the barcode data.

void OnScanReceived(object sender, Scanner.DataEventArgs args)
{
    var scanDataCollection = args.P0;
    if (scanDataCollection?.Result == ScannerResults.Success)
    {
        var textView = FindViewById<TextView>(Resource.Id.last_scan_received);
        if (textView != null)
            textView.Text = scanDataCollection.GetScanData()[0].Data;
    }
}

Complete MainActivity

Here is the complete MainActivity we used in this example, you should be able to drag and drop this file into your project as long as you created the exact same activity_main.xml layout from earlier.

[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity, View.IOnClickListener
{
    IBarcodeScannerManager _scannerManager;
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.activity_main);

        _scannerManager = new BarcodeScannerManager(this);
        _scannerManager.ScanReceived += OnScanReceived;

        var toggleScannerButton = FindViewById<Button>(Resource.Id.toggle_scanner_button);
        toggleScannerButton.SetOnClickListener(this);
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();

        _scannerManager.ScanReceived -= OnScanReceived;
        _scannerManager.Dispose();
        _scannerManager = null;
    }

    void OnScanReceived(object sender, Scanner.DataEventArgs args)
    {
        var scanDataCollection = args.P0;
        if (scanDataCollection?.Result == ScannerResults.Success)
        {
            var textView = FindViewById<TextView>(Resource.Id.last_scan_received);
            if (textView != null)
                textView.Text = scanDataCollection.GetScanData()[0].Data;
        }
    }

    public void OnClick(View v)
    {
        if (_scannerManager.IsScannerEnabled)
            _scannerManager.DisableScanner();
        else
            _scannerManager.EnableScanner();
    }
}

Conclusion

If you followed along you should be able to run your app and scan barcodes after enabling the scanner. This example should be enough to get you started with the EMDK Library, but if you want to properly configure your scanner outside of the default configuration there is much more work to do. This is why I believe Zebra recommends using DataWedge over EMDK.

DataWedge vs EMDK

 With a working knowledge of how to use the EMDK library I can say that I prefer it as it gives me the most control from scanning integration to configuration of the device. EMDK will be more work for custom configurations than DataWedge, which is more plug-and-play.

In EMDK integrations you will need to create custom pages and update your profile manually. Where as in DataWedge there is a plugin built-in so you don't need to create the configuration pages.

Pro-Tips and Troubleshooting

This is not an easy integration, don't get frustrated if this doesn't work correctly on the first try. If you do run into some problems these troubleshooting areas may help.

Scanner Corrupted

It is very common for the Scanner to get into an invalid state that will prevent the EMDK library from working correctly. This may happen from your app that is developement or another app on the device. 

If you start running into odd behaviors with the scanner when you believe it should work, try rebooting the device. I commonly do this during developement as it is easy to allocate the scanner and the app terminates without deallocating it. If this happens I find it easier to just reboot.

Memory Leaks

I recommend following greedy memory management stragies with any library that touches the EMDK library and your wrapper.

  • Deallocate event listeners as often as possible
  • Dispose/Release all objects from EMDK Library
  • Allow Garbage Collection by setting objects to null 

Sleep/Destoryed

Scanner state is very important to manage correctly. Consider you have 2 or more apps that leverage the hardware scanner, only one app can access the scanner at a time. It is considered a best practice to disable the scanner when the app is not in use

  • Sleep
  • App Exit

Both of these events needs to properly disable the scanner, which will all another app to access the barcode scanner

Code Sample

If you want to see this in action, check out the working sample project I have on GitHub

Update 1 - Xamarin.Forms Support?

I was asked on LinkedIn if this will work for Xamarin.Forms or is it only for Xamarin Android (LinkedIn Comment)

This example is targeting Xamarin Android specifically the EMDK library, which is only available on Zebra Devices with EMDK capabilities. This won't work on a standard Android Device or even an iOS Device.

If you want to use Xamarin.Forms to build your application and your Android Target is a Zebra Device with EMDK you can absolutely use this code. You will need to create a custom abstarction for the event handler, but all of this code will work in a shared context. You will need to come up with your custom barcode scanning implementation for your other platforms.

 

-Happy Coding


Share

Tags

XamarinAndroidZebraTC70xEMDKBarcode ScannerWrapper