While Line of Business applications have the ability to leverage Zebra's unique device capabilities through EMDK-for-Android APIs, use and practice of such APIs are typically in complex apps with multiple activities. Since most sample and tutorial posted on the web that illustrate these APIs are in abbreviated single-activity app, incorporating code snippets and dev techniques from these into multi-activity applications can lead to excessive redundant code and management of EMDK instances can become challenging given the Android Lifecycle flow.

 

To get an appreciation of amount of code required for a use case of simply enabling the scanner and capture decoded barcode data through EMDK APIs, one would typically have to code for the following steps.

  • Initialize the EMDK Manager
  • Get the Barcode Manager
  • Select and enable the scanner for scanning
  • Configure the scanner settings and enable/disable barcode symbologies
  • Register for receiving the scanner status and scanned data via callback
  • Receive the scanner status and scanned data.

 

Given the level of work involved, most would tend to agree that writing code for the EMDK interface in every UI Activity, that requires barcode scanning, is not good practice. It would hence result in a convolutely structured app with high proportion of redundant code, which in turn becomes laborious to support and performance negatively impacted. Since most developer aspires creating clean, well architected and streamlined apps, first impression of EMDK-for-Android can be viewed as impractical.

 

Oddly enough, developers who encounter these types of issues are often suggested to take the easier route, such as using the DataWedge utility instead of incorporating the EMDK libraries. Essentially, DataWedge is a background service that manages the scanner on your behalf and can be invoked with minimal to no code at all. While this can be a viable practice for some applications, others may need use of a more robust scheme allowing for full scanner control. EMDK-for-Android offers this as well as granular control of other hardware capabilities, specifically through EMDK features such as; SimulScanManager, ProfileManager and Scan&PairManager.

 

Developers looking to implement the full feature-set of EMDK-for-Android in an efficient manner typically considers using a separate individual Class. Also, referenced as a Wrapper Class. This blog helps describe how to employ EMDK libraries in such a fashion. The approach to take is not to implement the EMDK interface in the MainActivity or any UI Activities, rather create a separate class (for example, BarcodeScanner). An Interface class can be used to enable the UI Activities to make bidirectional calls to and from the BarcodeScanner class. The diagram below attempts to illustrate a simple but viable structure. We will breakdown and discuss this arrangement in greater detail.

 

Screen Shot 2017-12-31 at 12.43.01 PM.png

Scanner Event Interface Class

As illustrated in the diagram, an interface class is created to work as a go between the BarcodeScanner class and UI activities. For reference, the sample below uses IOnScannerEvent as the interface class name. It essentially captures 3 events; onDataScanned(), onStatusUpdate() and onError(). These pass-through function-calls contain information about Scanner Status and Data from Scanned Barcodes. These reflect the 2 main functions of the EMDK.

 

Since we don’t display errors in the UI activity, we're not passing error data. While we do log errors in logcat, one can for intuitive purposes, add an argument to the onError() event so to pass through specific error related data.

 

package com.BarcodeClassSample;

public interface IOnScannerEvent {

    //Function is called to pass scanned data
    void onDataScanned(String scanData);

    //Function is called to pass scanner status
    void onStatusUpdate(String scanStatus);

   //Function to be called upon error or exception
    void onError();
}

 

 

UI Activity Enabling the Scanner

For all UI activities requiring barcode data and scanner status captured and populated, should implement the IOnScannerEvent interface class that was just referenced above. By way of the UI activity "implements" statement, the 3 events (onDataScanned(), onStatusUpdate() and onError()) from the Interface class are made available here. These methods are called automatically upon their respective events occurring, which are controlled by the BarcodeScanner class. We will cover the BarcodeScanner class in the next segment.

 


@Override
public void onDataScanned(String scanData) {
    DataView.append(scanData + "\n");
}

@Override
public void onStatusUpdate(String scanStatus) {
    StatusView.setText(scanStatus);
}

@Override
public void onError() {
    System.
out.println (TAG + "onError() ");
}

 

Following the onCreate() method of the UI activity, the onResume() method calls .getInstance() method from within the BarcodeScanner class followed by the .registerUIobject() method. In both cases, the UI activity object (this) is passed along to the BarcodeScanner class so to reference with when passing along scanner data to the calling UI activity.

 

 

@Override
protected void onResume(){
   
super.onResume();
    BarcodeScanner.getInstance(
this);
    BarcodeScanner.registerUIobject(
this);
}

 

The onPause() method just calls .unregisterUIobject() method in the BarcodeScanner class. This method simply resets the variable holding the current UI activity object by setting it to null.

 


@Override
public void onPause() {
    
super.onPause();
     BarcodeScanner.unregisterUIobject();
}

 

A noteworthy piece of information here is the necessity of managing the EMDK instance. Since only one instance of EMDKManager can be used in an application, all features including EMDKManager must be released before exiting calling activities or the application. The event typically used for releasing resources is onDestroy(). However, this event is not triggered upon a suspend/resume condiion so the better choice would be to use the onStop() event. In the onStop() event, specifically, we go ahead and de-initialize the scanner and release the EMDK resources. The methods that perform this are called from within the BarcodeScanner class, .deInitScanner() and .releaseEmdk().

 

 

@Override
public void onStop() {
    super.onStop();
    BarcodeScanner.deInitScanner();

    BarcodeScanner.releaseEmdk();
}

 

BarcodeScanner Class

The purpose for the BarcodeScanner class is to enable UI activities within an application with barcode scanning. All EMDK methods associated with capturing barcode data and scanner status events reside here. Since all necessary EMDK calls are performed in this class and nowhere else, it provides a single location for whenever code changes or refinements are desired. Thus, producing a more efficient and better architected app with little to no code redundancy.

 

As noted above, when BarcodeScanner.getInstance() is called from within the UI Activity, its context is passed on to the BarcodeScanner class. Essentially, this is the entry point to the class. A new BarcodeScanner object is created using the current UI Activity context and gets held in the mBarcodeScanner variable.

 


public static BarcodeScanner getInstance(Context context) {
    
if (mBarcodeScanner == null) {
        
mBarcodeScanner = new BarcodeScanner(context);
    
}
    
return mBarcodeScanner;
}

 

Within the BarcodeScanner() method we then call EMDKManager so that the EMDK service can be initialized and checked to see if it is ready. We’re also creating a new Handler(Looper.getMainLooper()) object for receiving and handling messages. This enables us to move data from scanner object to an object in the UI thread. The Handler object is stored in mScanHandler variable. We’re also creating a new IOnScannerEventRunnable() object to support a Runnable class, which is designed for background processing. This object will be stored in mEventRunnable variable.

 


private BarcodeScanner(Context context) {
   EMDKResults results = EMDKManager.getEMDKManager(context, this);
    
if (results.statusCode != EMDKResults.STATUS_CODE.SUCCESS) {
         System.
out.println (TAG + "EMDKManager Request Failed");
     }

    
mScanHandler
= new Handler(Looper.getMainLooper());
    
mEventRunnable
= new IOnScannerEventRunnable();
}

 

By now the EMDK service should be connected and as a result it will automatically trigger the EMDK onOpened() method. Here we get a reference to the EMDKManager. This event is used to pass the EMDKManager instance to a global variable, emdkManager. We’ll use that instance to create the BarcodeManager object which in turn is used to enable scanning. This work is done in the initializeScanner() method called from here.

 


public void onOpened(EMDKManager emdkManager) {
    
this.emdkManager = emdkManager;
    
initializeScanner();
}

 

The EMDK Data/Status listeners to support their respective callbacks are added in the  initializeScanner() method. Here we also enable the scanner by using the barcodeManager object. First, the EMDK BarcodeManager.DeviceIdentifier is used to create the scanner object. Then we enable the scan engine (scanner hardware) by running the scanner.enable() method. In this sample, we use the default scanner of the device to scan barcodes and set the hard (physical) buttons to trigger scanning action. This method simply gets the scanner ready for use.

 

 
private void initializeScanner() {
    
try {
        
barcodeManager
= (BarcodeManager) emdkManager.getInstance(EMDKManager.FEATURE_TYPE.BARCODE);
        
scanner
= barcodeManager.getDevice(BarcodeManager.DeviceIdentifier.DEFAULT);
        
scanner
.addDataListener(this);
        
scanner.addStatusListener(this);
        
scanner.triggerType = Scanner.TriggerType.HARD;
        
scanner
.enable();
    
} catch (ScannerException e) {
         System.
out.println (TAG + "initializeScanner() - ScannerException " + e.getMessage());
         e.printStackTrace();
     }
}

 

Once the scanner is initialized or the scan trigger is pressed subsequently, the EMDK onStatus() method is automatically triggered through the StatusListener. The purpose for this callback method is to return status of the scanner. In order to scan a barcode, a scanner.read() must be submitted. A read can be submitted from within onData() or onStatus() events. If called within onStatus(), as illustrated below, it should be called only when IDLE status is received. If submitted while another read is pending, the method call will fail, so checking for isReadPending() is recommended.

 

Since EMDK callbacks are asynchronous by nature and since scanner status change occurs rapidly, it can create a condition whereby scanner status get misaligned with the order in progress of populating the UI thread. So we introduce a brief delay immediately before the next read() is submitted. This has little to no impact on barcode decoding performance. This applies only to apps that display or process scanner status.

 

With every status change we go ahead and populate the current UI activity with the updated status. This is done by calling the callIOnScannerEvent() method which is created to manage background threads.

 


@Override
public void onStatus(StatusData statusData) {
     String statusStr =
"";
     StatusData.ScannerStates state = statusData.getState();
    
switch (state) {
        
case IDLE: //Scanner is IDLE - this is when to request a read
            
statusStr = "Scanner enabled and idle";
            
try {
                
if (scanner.isEnabled() && !scanner.isReadPending()) {
                    
try {
                         Thread.sleep(
100);
                     }
catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                    
scanner.read();
                 }
             }
catch (ScannerException e) {
                 System.
out.println (TAG + "onStatus() - ScannerException " + e.getMessage());
                 e.printStackTrace();
                 statusStr = e.getMessage();
             }
            
break;
        
case SCANNING: //Scanner is SCANNING
            
statusStr = "Scanner beam is on, aim at the barcode";
            
break;
        
case WAITING: //Scanner is waiting for trigger press
            
statusStr = "Waiting for trigger, press to scan barcode";
            
break;
        
case DISABLED: //Scanner is disabled
            
statusStr = "Scanner is not enabled";
            
break;
        
case ERROR: //Error occurred
            
statusStr = "Error occurred during scanning";
            
break;
     }
    
//Return result to populate UI thread
    
callIOnScannerEvent(I_ON_STATUS, null, statusStr);
}

 

Whenever a barcode is scanned, its data will be received in the onData() callback method. Conversely to the scanner status event, this callback is facilitated through EMDK’s DataListener. Here is where we obtain decoded barcode data and process it in the desirable format then populate the UI thread. As we did for the scanner status event, we will send the barcode data in a background thread managed by the callIOnScannerEvent() method.

 

 
@Override
public void onData(ScanDataCollection scanDataCollection) {
    
if (scanDataCollection != null && scanDataCollection.getResult() == ScannerResults.SUCCESS) {
         ArrayList<ScanDataCollection.ScanData> scanData = scanDataCollection.getScanData();
        
if (scanData != null && scanData.size() > 0) {
            
final ScanDataCollection.ScanData data = scanData.get(0);
             callIOnScannerEvent(
I_ON_DATA, data.getData(), null);
         }
     }
}

 

As mentioned earlier, barcode data and scanner status will populate the UI thread in a background thread. We achieve this by implementing the Runnable interface. In preparation, we need to setup the details, such as the interfaceID, barcode data and scanned status. We declared several int variables for calling the Interface class with the proper function and respective data. There are as follows: int I_ON_DATA = 0; int I_ON_STATUS = 1; int I_ON_ERROR = 2;

Since the call handles both barcode data and scanner status and because they are mutually exclusive, one passing arguments will contain a value of null. To ellaborate, if the interfaceID is I_ON_DATA, the “data” argument would contain barcode data but “status” would contain null. Conversely, if interfaceID is I_ON_STATUS, the “status” argument would contain scanner status but “data” would contain null. However, in the event interfaceID is I_ON_ERROR, both the “data” and “status” arguments will contain null.

 

private void callIOnScannerEvent(int interfaceId, String data, String status) {
    
if (mUIactivity != null) {
        
mEventRunnable.setDetails(interfaceId, data, status);
        
mScanHandler.post(mEventRunnable);
     }
}

 

As referenced earlier, a Runnable interface is implemented by the IOnScannerEventRunnable class. The setDetails()method is called by callIOnScannerEvent() method upon every scanner event, be it status or barcode data.  The class also defines the no arguments run() method that connects to the current UI thread and executes the appropriate scanner event with its associated data.

 

private static class IOnScannerEventRunnable implements Runnable {
    
private int mInterfaceId = 0;
    
private String mBarcodeData = "";
    
private String mBarcodeStatus = "";

    
public void setDetails(int id, String data, String statusStr) {
        
mInterfaceId = id;
        
mBarcodeData = data;
        
mBarcodeStatus = statusStr;
     }

    
@Override
    
public void run() {
        
if(mUIactivity!=null) {
            
switch (mInterfaceId) {
                
case I_ON_DATA:
                    
mUIactivity.onDataScanned(mBarcodeData);
                    
break;
                
case I_ON_STATUS:
                    
mUIactivity.onStatusUpdate(mBarcodeStatus);
                    
break;
                
case I_ON_ERROR:
                    
mUIactivity.onError();
                    
break;
             }
         }
     }
}

 

Other Considerations

Amalgamation of the above-noted code snippets do not compose a complete Android project. Its purpose is to highlight key components and considerations for properly implementing EMDK libraries and use of the BarcodeManager API in a multi-activity application.

 

Sample Application

To help illustrate use of the BarcodeManager APIs as discussed in this blog, I have created and attached below a sample app for reference. To note, this sample app is purely for demonstration purposes only. Any comments/questions can be posted here.