Android Barcode Scanner using USB Camera

Although almost every Android phone is equipped with cameras, some Android devices, like TV boxes and kiosks, may not have cameras built-in. An easy way to add extra camera functionality to these devices is by connecting with a USB camera if they support USB-OTG.

Starting from Android 9, the Android platform supports the use of plug-and-play USB cameras (that is, webcams) using the standard Android Camera2 API and the camera HIDL interface.1 But not all Android 9+ devices have this function enabled2 and old Android devices cannot use it.

There are other approaches. One is by using the video for the Linux kernel module, like this one, but it may have permission problems. Another is by using the USB host API, like this one, which does not require root access.

In this article, we are going to use the latter approach to create an Android barcode scanner using a USB camera. Dynamsoft Barcode Reader is used as the barcode reading SDK.

Creating an Android Barcode Scanner using USB Camera

Let’s create a new Android project using Android Studio and add functions like access to USB cameras and barcode scanning.

Tips:

Since we need to use USB-OTG to connect the Android device to a USB camera, we may not be able to debug it using a USB cable. We can configure ADB to debug wirelessly.

Connect the device to the computer first and run the following commands:

adb -d shell setprop service.adb.tcp.port 4444
adb -d tcpip 4444
adb connect 192.168.31.215:4444

After the connection is done, we can unplug the device and debug wirelessly.

Show USB Camera Preview

The UVCCamera library is used to have access to USB cameras.

Add dependencies

We need to compile the library with NDK by ourselves using the original repo. Here, for convenience, we can use precompiled AARs from this repo.

Download the AARs and put them in the app/libs folder.

In the project’s build.gradle, add the following lines:

allprojects {
    repositories {
        maven { url 'http://raw.github.com/saki4510t/libcommon/master/repository/' }
        flatDir {
            dirs 'libs'
        }
    }
}

In the app’s build.gradle, add the following lines:

dependencies {
    implementation(name: 'libuvccamera', ext: 'aar')
    implementation(name: 'usbCameraCommon', ext: 'aar')
    implementation("com.serenegiant:common:1.5.20") {
        exclude module: 'support-v4'
    }
}

Add Controls

Add a UVCCameraTextureView for camera preview and an ImageButton to select and open cameras.

XML:

<com.serenegiant.widget.UVCCameraTextureView
        android:id="@+id/camera_view"
        android:layout_width="407dp"
        android:layout_height="720dp"
        android:layout_centerInParent="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<ImageButton
    android:id="@+id/imageButton"
    android:layout_width="50dp"
    android:layout_height="50dp"
    android:layout_marginBottom="16dp"
    android:background="@mipmap/ic_transparent"
    android:scaleType="fitCenter"
    android:src="@drawable/btn_new_shutter"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

In MainActivity:

private CameraViewInterface mUVCCameraView;
private ImageButton mCameraButton;
@Override
protected void onCreate(final Bundle savedInstanceState) {
    //......
    mCameraButton = findViewById(R.id.imageButton);
    final View view = findViewById(R.id.camera_view);
    mUVCCameraView = (CameraViewInterface)view;
}

Monitor USB Events

The library has a wrapper around the USB Host API which we have talked about in the previous post. We are going to use it to monitor USB events.

  1. Initialize a USBMonitor instance.

     private USBMonitor mUSBMonitor;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         //......
         mUSBMonitor = new USBMonitor(this, mOnDeviceConnectListener);
     }
    
     private final USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {
         @Override
         public void onAttach(final UsbDevice device) {
             Toast.makeText(MainActivity.this, "USB_DEVICE_ATTACHED", Toast.LENGTH_SHORT).show();
         }
    
         @Override
         public void onConnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock, final boolean createNew) {
    
         }
    
         @Override
         public void onDisconnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock) {
    
         }
         @Override
         public void onDettach(final UsbDevice device) {
             Toast.makeText(MainActivity.this, "USB_DEVICE_DETACHED", Toast.LENGTH_SHORT).show();
         }
    
         @Override
         public void onCancel(final UsbDevice device) {
         }
     };
    
  2. Manage the life cycle.

     @Override
     protected void onStart() {
         super.onStart();
         mUSBMonitor.register();
     }
    
     @Override
     protected void onStop() {
         mUSBMonitor.unregister();
         super.onStop();
     }
    
     @Override
     public void onDestroy() {
         if (mUSBMonitor != null) {
             mUSBMonitor.destroy();
             mUSBMonitor = null;
         }
         super.onDestroy();
     }
    

Open USB Camera

The UVCCameraHandler class is used to handle USB cameras.

  1. Initialize the handler.

     /**
      * set true if you want to record movie using MediaSurfaceEncoder
      * (writing frame data into Surface camera from MediaCodec
      *  by almost same way as USBCameratest2)
      * set false if you want to record movie using MediaVideoEncoder
      */
     private static final boolean USE_SURFACE_ENCODER = false;
     /**
      * preview resolution(width)
      * if your camera does not support specific resolution and mode,
      * {@link UVCCamera#setPreviewSize(int, int, int)} throw exception
      */
     private static final int PREVIEW_WIDTH = 640; // 640
     /**
      * preview resolution(height)
      * if your camera does not support specific resolution and mode,
      * {@link UVCCamera#setPreviewSize(int, int, int)} throw exception
      */
     private static final int PREVIEW_HEIGHT = 480; //480
     /**
      * preview mode
      * if your camera does not support specific resolution and mode,
      * {@link UVCCamera#setPreviewSize(int, int, int)} throw exception
      * 0:YUYV, other:MJPEG
      */
     private static final int PREVIEW_MODE = 0; // YUV
     private UVCCameraHandler mCameraHandler;
      @Override
     protected void onCreate(Bundle savedInstanceState) {
         //......
         mCameraHandler = UVCCameraHandler.createHandler(this, mUVCCameraView,
                 USE_SURFACE_ENCODER ? 0 : 1, PREVIEW_WIDTH, PREVIEW_HEIGHT, PREVIEW_MODE);
     }
    
  2. Add an OnClickListener for the image button to start a camera selection dialog. The preview will be started after a camera is chosen.

     protected void onCreate(Bundle savedInstanceState) {
         //......
         mCameraButton.setOnClickListener(mOnClickListener);
         //......
     }
    
     private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
         @Override
         public void onClick(final View view) {
             if ((mCameraHandler != null) && !mCameraHandler.isOpened()) {
                 CameraDialog.showDialog(MainActivity.this);
             } else {
                 mCameraHandler.close();
             }
         }
     };
    
     private final USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {
         @Override
         public void onConnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock, final boolean createNew) {
             if (mCameraHandler != null) {
                 mCameraHandler.open(ctrlBlock);
                 startPreview();
             }
         }
     }
    
     private void startPreview() {
         if (mCameraHandler != null) {
             final SurfaceTexture st = mUVCCameraView.getSurfaceTexture();
             mCameraHandler.startPreview(new Surface(st));
         }
     }
    

    The activity has to be modified as well to extend the library’s BaseActivity:

     public class MainActivity extends BaseActivity implements CameraDialog.CameraDialogParent
    

    You may find that the preview is distorted to fill the screen. You can keep its aspect ratio using the following code:

     mUVCCameraView.setAspectRatio(PREVIEW_WIDTH / (double)PREVIEW_HEIGHT);
    

Read Barcodes from USB Camera Preview

Now that we can show camera preview, we can use Dynamsoft Barcode Reader to read barcodes.

Add dependencies

In the project’s build.gradle, add the following lines:

allprojects {
    repositories {
        maven {
            url "https://download2.dynamsoft.com/maven/dbr/aar"
        }
    }
}

In the app’s build.gradle, add the following lines:

dependencies {
    implementation 'com.dynamsoft:dynamsoftbarcodereader:8.8.0@aar'
}

Initialize Dynamsoft Barcode Reader

Initialize Dynamsoft Barcode Reader with a license. You can apply for a 30-day free trial license here.

private BarcodeReader barcodeReader;
protected void onCreate(Bundle savedInstanceState) {
    //......
    try {
        barcodeReader = new BarcodeReader("<license key>");
    } catch (BarcodeReaderException e) {
        e.printStackTrace();
    }
}

Create a Timer to decode frames

When a camera is connected, start decoding using a timer.

private Timer timer = null;
private void startDecoding(){
    TimerTask task = new TimerTask() {
        @Override
        public void run() {
            Bitmap bmp = mUVCCameraView.captureStillImage();
            decode(bmp);
        }
    };
    timer = new Timer();
    timer.scheduleAtFixedRate(task, 1000, 100);
}
private void stopDecoding(){
    if (timer != null){
        timer.cancel();
        timer = null;
    }
}

private final USBMonitor.OnDeviceConnectListener mOnDeviceConnectListener = new USBMonitor.OnDeviceConnectListener() {
    //......
    @Override
    public void onConnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock, final boolean createNew) {
        if (mCameraHandler != null) {
            mCameraHandler.open(ctrlBlock);
            startPreview();
            startDecoding(); //newly added
        }

    }

    @Override
    public void onDisconnect(final UsbDevice device, final USBMonitor.UsbControlBlock ctrlBlock) {
        stopDecoding();  //newly added
    }
}

The preview is captured as bitmap for Dynamsoft Barcode Reader to decode. The result will be displayed in a TextView.

private void decode(final Bitmap bitmap){
    try {
        final TextResult[] results = barcodeReader.decodeBufferedImage(bitmap,"");
        Log.d("DBR",String.valueOf(results.length));
        runOnUiThread(
            new Runnable() {
                @Override
                public void run() {
                    StringBuilder sb = new StringBuilder();
                    sb.append("Found ");
                    sb.append(results.length);
                    sb.append(" barcodes:");
                    sb.append("\n");
                    for (TextResult tr : results){
                        sb.append(tr.barcodeText);
                        sb.append("\n");
                    }
                    resultTextView.setText(sb.toString());
                }
            }
        );
    } catch (Exception e){
        e.printStackTrace();
    }
}

All right, the barcode scanner is now finished. Here is a video of the final result:

Compatibility issue

On Android 9+, the library may not work if the targetSdkVersion is set larger than 27. You can check out this post to learn more.

Source Code

https://github.com/xulihang/dynamsoft-samples/tree/main/dbr/Android/USBCamera

References