The Quickest Way to Create an Android QR Code Scanner

A QR code scanner application is fundamentally composed of two key components: the camera preview and the QR code scanning functionality. While there are numerous QR code scanner apps available for download on the Google Play Store, building your own can be a more rewarding and educational experience. This article will guide you through the fastest way to create an Android QR code scanner, walking you through the step-by-step implementation of the camera preview and the seamless integration of a QR code scanning SDK.

Prerequisites

  • Dynamsoft Barcode Reader Trial License

    To use the Dynamsoft Barcode Reader, you’ll need a trial license key. Integrate it into your Java code as follows:

          BarcodeReader.initLicense(
              "LICENSE-KEY",
                  new DBRLicenseVerificationListener() {
                      @Override
                      public void DBRLicenseVerificationCallback(boolean isSuccessful, Exception e) {
                      }
                  });
    
  • Dynamsoft Maven Repository

    Add the Dynamsoft Maven repository to your project by modifying the settings.gradle file:

      dependencyResolutionManagement {
          repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
          repositories {
              ...
              maven{url "https://download2.dynamsoft.com/maven/aar"}
          }
      }
    
  • Camera Preview SDK

    • CameraX

      Google’s CameraX API simplifies camera app development, making it more accessible for beginners compared to the complex Android Camera2 API. To get started with CameraX, refer to the codelab tutorial.

      Add the CameraX dependencies in your build.gradle:

        dependencies {
            ...
            def camerax_version = "1.0.1"
            implementation "androidx.camera:camera-camera2:$camerax_version"
            implementation "androidx.camera:camera-lifecycle:$camerax_version"
            implementation "androidx.camera:camera-view:1.0.0-alpha27"
        }
      
    • Dynamsoft Camera Enhancer v2.3.0

      Dynamsoft Camera Enhancer is a powerful wrapper around the Android Camera2 API, offering advanced features like frame filtering for enhanced image quality. We’ll compare its performance with CameraX in this project.

      Include the following dependency in your build.gradle:

        dependencies {
            ...
            implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.3.0@aar'
        }
      
  • Dynamsoft Barcode Reader v9.x for QR Code Scanning

    The Dynamsoft Barcode Reader SDK supports a wide range of barcode formats, including both linear and 2D barcodes. To enable QR code scanning in your app, add this dependency to your app/build.gradle:

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

Creating an Android Camera Preview in 5 Minutes

Implementing Camera Preview with CameraX in Three Simple Steps

The official CameraX tutorial is provided in Kotlin, but here we’ll show you how to achieve the same in Java.

Step 1: Add Camera Permissions

In your AndroidManifest.xml, declare the necessary camera permissions:

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />

Step 2: Create the UI Layout for CameraX Preview

Define the layout in XML, including a PreviewView for the CameraX preview:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".CameraXActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/camerax_viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Step 3: Check and Request Camera Permissions

Ensure the app has the necessary permissions to access the camera, and if not, request them:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.camerax_main);
    previewView = findViewById(R.id.camerax_viewFinder);

    if (!CameraUtils.allPermissionsGranted(this)) {
        CameraUtils.getRuntimePermissions(this);
    } else {
        startCamera();
    }
}

private static String[] getRequiredPermissions(Context context) {
    try {
        PackageInfo info =
                context.getPackageManager()
                        .getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS);
        String[] ps = info.requestedPermissions;
        if (ps != null && ps.length > 0) {
            return ps;
        } else {
            return new String[0];
        }
    } catch (Exception e) {
        return new String[0];
    }
}

public static boolean allPermissionsGranted(Context context) {
    for (String permission : getRequiredPermissions(context)) {
        if (!isPermissionGranted(context, permission)) {
            return false;
        }
    }
    return true;
}

public static void getRuntimePermissions(Activity activity) {
    List<String> allNeededPermissions = new ArrayList<>();
    for (String permission : getRequiredPermissions(activity)) {
        if (!isPermissionGranted(activity, permission)) {
            allNeededPermissions.add(permission);
        }
    }

    if (!allNeededPermissions.isEmpty()) {
        ActivityCompat.requestPermissions(
                activity, allNeededPermissions.toArray(new String[0]), PERMISSION_REQUESTS);
    }
}

private static boolean isPermissionGranted(Context context, String permission) {
    if (ContextCompat.checkSelfPermission(context, permission)
            == PackageManager.PERMISSION_GRANTED) {
        Log.i(TAG, "Permission granted: " + permission);
        return true;
    }
    Log.i(TAG, "Permission NOT granted: " + permission);
    return false;
}

Step 4: Start the Camera Preview

Finally, initialize and start the camera preview:

private void startCamera() {
    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
            ProcessCameraProvider.getInstance(getApplication());
    cameraProviderFuture.addListener(
            () -> {
                try {
                    ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

                    Preview.Builder builder = new Preview.Builder();
                    Preview previewUseCase = builder.build();
                    previewUseCase.setSurfaceProvider(previewView.getSurfaceProvider());
                    CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
                    cameraProvider.unbindAll();
                    cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase);
                } catch (ExecutionException | InterruptedException e) {
                    Log.e(TAG, "Unhandled exception", e);
                }
            },
            ContextCompat.getMainExecutor(getApplication()));
}

Implementing Camera Preview with Dynamsoft Camera Enhancer in Two Simple Steps

Dynamsoft Camera Enhancer allows you to implement camera preview with less code compared to CameraX while providing the same functionality.

Step 1: Create the UI Layout with the DCE Preview View

Define the layout in XML, including the DCECameraView for the camera preview:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".CameraXActivity">

    <com.dynamsoft.dce.DCECameraView
        android:id="@+id/dce_viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Step 2: Start the Camera Preview

Initialize and start the camera preview in the DceActivity:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.dce_main);
    previewView = findViewById(R.id.dce_viewFinder);

    cameraEnhancer = new CameraEnhancer(this);
    cameraEnhancer.setCameraView(previewView);
    cameraEnhancer.addListener(this);
}

@Override
protected void onResume() {
    super.onResume();
    try {
        cameraEnhancer.open();
    } catch (CameraEnhancerException e) {
        e.printStackTrace();
    }
}

@Override
protected void onPause() {
    super.onPause();
    try {
        cameraEnhancer.close();
    } catch (CameraEnhancerException e) {
        e.printStackTrace();
    }
}

Combining CameraX and Dynamsoft Camera Enhancer

To allow users to choose between CameraX and Dynamsoft Camera Enhancer, create an entry activity that launches the respective activity based on the user’s choice:

package com.example.qrcodescanner;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class EntryChoiceActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.entry_choice);

        findViewById(R.id.camerax_entry_point).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(EntryChoiceActivity.this, CameraXActivity.class);
                startActivity(intent);
            }
        });

        findViewById(R.id.dce_entry_point).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(EntryChoiceActivity.this, DceActivity.class);
                startActivity(intent);
            }
        });
    }
}

Turning Your Android Camera into a QR Code Scanner

To enable QR code scanning, you need to continuously capture camera preview frames and pass them to a QR code detector.

Setting the Camera Frame Callback

With CameraX, you can use the ImageAnalysis class to receive camera frames:

ImageAnalysis analysisUseCase = new ImageAnalysis.Builder().build();
analysisUseCase.setAnalyzer(cameraExecutor,
        imageProxy -> {
            // image processing
            // Must call close to keep receiving frames.
            imageProxy.close();
        });
cameraProvider.bindToLifecycle(this, cameraSelector, previewUseCase, analysisUseCase);

In contrast, Dynamsoft Camera Enhancer simplifies this process, providing a callback function similar to the one used in Android Camera API:

public class DceActivity extends AppCompatActivity implements DCEFrameListener {
    @Override
    public void frameOutputCallback(DCEFrame dceFrame, long l) {
        // image processing
    }
}

Decoding QR Codes

To decode QR codes, the process differs slightly between CameraX and Dynamsoft Camera Enhancer:

  • With CameraX: Convert the ByteBuffer to a byte[] and then call the decodeBuffer() method:

      analysisUseCase.setAnalyzer(cameraExecutor,
      imageProxy -> {
          TextResult[] results = null;
          ByteBuffer buffer = imageProxy.getPlanes()[0].getBuffer();
          int nRowStride = imageProxy.getPlanes()[0].getRowStride();
          int nPixelStride = imageProxy.getPlanes()[0].getPixelStride();
          int length = buffer.remaining();
          byte[] bytes = new byte[length];
          buffer.get(bytes);
          try {
              results = reader.decodeBuffer(bytes, imageProxy.getWidth(), imageProxy.getHeight(), nRowStride * nPixelStride, EnumImagePixelFormat.IPF_NV21, "");
          } catch (BarcodeReaderException e) {
              e.printStackTrace();
          }
        
          // Must call close to keep receiving frames.
          imageProxy.close();
      });
    
  • With Dynamsoft Camera Enhancer: Convert the DCEFrame to a Bitmap and then call the decodeBufferedImage() method:

      public void frameOutputCallback(DCEFrame dceFrame, long l) {
          TextResult[] results = null;
          try {
              results = reader.decodeBufferedImage(dceFrame.toBitmap(), "");
          } catch (BarcodeReaderException e) {
              e.printStackTrace();
          }
      }
    

    Android QR code scanner

Enhancing Frame Quality with Zoom and Torch Features

The accuracy of QR code recognition is heavily influenced by the quality of the input image. If the QR code is too small, zooming in can help by enlarging the image. If the image is too dark, turning on the torch can brighten it, improving detection accuracy. Both CameraX and Dynamsoft Camera Enhancer fully support these camera controls.

Implementing Android Camera Zoom

To enable zoom functionality, we’ll use a pinch gesture. The first step is to create a gesture detector and override the onTouchEvent() method:

public class ZoomController {
    public final static String TAG = "ZoomController";
    private float currentFactor = 1.0f;
    private float minZoomRatio = 1.0f, maxZoomRatio = 1.0f;
    private ZoomStatus zoomStatus;
    private ScaleGestureDetector scaleGestureDetector;
    private ScaleGestureDetector.OnScaleGestureListener scaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            Log.i(TAG, "onScale: " + detector.getScaleFactor());
            currentFactor = detector.getScaleFactor() * currentFactor;
            if (currentFactor < minZoomRatio) currentFactor = minZoomRatio;
            if (currentFactor > maxZoomRatio) currentFactor = maxZoomRatio;

            if (zoomStatus != null) {
                zoomStatus.onZoomChange(currentFactor);
            }

            return true;
        }

        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            return true;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
        }
    };

    public ZoomController(Activity activity) {
        scaleGestureDetector = new ScaleGestureDetector(activity, scaleGestureListener);
    }

    public interface ZoomStatus {
        void onZoomChange(float ratio);
    }

    public void addListener(ZoomStatus zoomStatus) {
        this.zoomStatus = zoomStatus;
    }

    public void initZoomRatio(float minZoomRatio, float maxZoomRatio) {
        this.minZoomRatio = minZoomRatio;
        this.maxZoomRatio = maxZoomRatio;
    }

    public boolean onTouchEvent(MotionEvent event) {
        return scaleGestureDetector.onTouchEvent(event);
    }
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    zoomController.onTouchEvent(event);
    return super.onTouchEvent(event);
}

When a pinch gesture is detected, the scale factor is used to adjust the zoom ratio.s

Setting Camera Zoom Ratio with CameraX

if (camera != null) {
    camera.getCameraControl().setZoomRatio(ratio);
}

Setting Camera Zoom Ratio with Dynamsoft Camera Enhancer

try {
    cameraEnhancer.setZoom(ratio);
} catch (CameraEnhancerException e) {
    e.printStackTrace();
}

Implementing Android Camera Torch

To automatically manage the torch, we’ll monitor the ambient light levels using the device’s light sensor.

public class AutoTorchController implements SensorEventListener {
    public final static String TAG = "AutoTorchController";
    private SensorManager sensorManager;
    private TorchStatus torchStatus;

    public interface TorchStatus {
        void onTorchChange(boolean status);
    }

    public AutoTorchController(Activity activity) {
        sensorManager = (SensorManager)activity.getSystemService(SENSOR_SERVICE);
    }

    public void onStart() {
        Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
        if(lightSensor != null){
            sensorManager.registerListener(
                    this,
                    lightSensor,
                    SensorManager.SENSOR_DELAY_NORMAL);

        }
    }

    public void onStop() {
        sensorManager.unregisterListener(this);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if(event.sensor.getType() == Sensor.TYPE_LIGHT){
            if (event.values[0] < 20) {
                if (torchStatus != null) torchStatus.onTorchChange(true);
            }
            else {
                if (torchStatus != null) torchStatus.onTorchChange(false);
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
    }

    public void addListener(TorchStatus torchStatus) {
        this.torchStatus = torchStatus;
    }
}

Toggling Camera Torch with CameraX

if (camera != null) camera.getCameraControl().enableTorch(status);

Toggling Camera Torch with Dynamsoft Camera Enhancer

if (status) {
    try {
        cameraEnhancer.turnOnTorch();
    } catch (CameraEnhancerException e) {
        e.printStackTrace();
    }
}
else {
    try {
        cameraEnhancer.turnOffTorch();
    } catch (CameraEnhancerException e) {
        e.printStackTrace();
    }
}

Download Dynamsoft Barcode Reader 30-Day Free Trial

Source Code

https://github.com/yushulx/android-camera-barcode-mrz-document-scanner/tree/main/examples/9.x/camerax_ml_qr