The Quickest Way to Create an Android QR Code Scanner
Tags: android, qrcode
Published: 2021-12-28 01:16:19

A QR code scanner application primarily consists of two parts: camera preview and QR code scanning. There are many Android QR code scanner apps downloadable in the Google Play Store. However, it is more interesting to create a QR code scanner by yourself than to use an existing one. This article aims to reveal the quickest way to create an Android QR code scanner. You will see how to step by step implement camera preview, as well as how to integrate QR code scanning SDK.


## Prerequisites

The following Android libraries are required in order to implement the QR code scanner. You can feel free to replace them with your own libraries. 

- Camera Preview SDK

    - CameraX
         
        Since [Android Camera2 API](https://developer.android.com/reference/android/hardware/camera2/package-summary) is extremely complicated for beginners, Google released [CameraX](https://developer.android.com/training/camerax) to simplify camera app development. The [codelab tutorial](https://developer.android.com/codelabs/camerax-getting-started#0) is a good starting point to learn CameraX. 
        
        **Installation**

        In `AndroidManifest.xml`, add the camera permission:

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

        In `app/build.gradle`, add the dependency:
        
        ```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
         
        Similar to CameraX, [Dynamsoft Camera Enhancer](https://www.dynamsoft.com/camera-enhancer/docs/introduction/) is also a wrapper for Android Camera2 API. In addition to basic camera capabilities, it features frame filtering for better image quality. We use Dynamsoft Camera Enhancer to make a comparison with CameraX.

        **Installation**

        In `settings.gradle`, add the custom maven repository:
        
        ```gradle
        dependencyResolutionManagement {
            repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
            repositories {
                ...
                maven{url "https://download.dynamsoft.com/maven/dce/aar"}
            }
        }
        ```

        In `app/build.gradle`, add the dependency:
        
        ```gradle
        dependencies {
            ...
            implementation 'com.dynamsoft:dynamsoftcameraenhancer:2.1.0@aar'
        }
        ```

- QR Code Scanning SDK
    - Dynamsoft Barcode Reader
            
        A barcode SDK that supports all mainstream linear barcode and 2D barcode formats.

        **Installation**

        In `settings.gradle`, add the custom maven repository:
        
        ```gradle
        dependencyResolutionManagement {
            repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
            repositories {
                ...
                maven{url "https://download.dynamsoft.com/maven/dbr/aar"}
            }
        }
        ```

        In `app/build.gradle`, add the dependency:
        
        ```gradle
        dependencies {
            ...
            implementation 'com.dynamsoft:dynamsoftbarcodereader:8.9.0@aar'
        }
        ```
        
        You also need a [license key](https://www.dynamsoft.com/customer/license/trialLicense/?product=dbr) to activate the barcode SDK.

## Creating Android Camera Preview within 5 Minutes

### Three Steps to Implement Camera Preview with CameraX
The official CameraX tutorial is written in Kotlin. Here we use Java.

1. Create the UI layout which contains the CameraX preview view:

    ```xml
    <?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>
    ```

2. Check and request camera permissions:
    
    ```java
    @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;
    }
    ```
3. Start the camera preview:
    
    ```java
    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()));
    }
    ```


### Two Steps to Implement Camera Preview with Dynamsoft Camera Enhancer
Using Dynamsoft Camera Enhancer, you can write less code than using CameraX to implement the same functionality.

1. Create the UI layout which contains the DCE preview view.
    
    ```xml
    <?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>
    ```
2. Start the camera preview:
    
    ```java
    @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();
        }
    }
    ```


### All in One
We create an entry activity to launch the CameraX and Dynamsoft Camera Enhancer respectively:

```java
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 Android Camera into QR Code Scanner
To scan QR code, we need to continuously get the camera preview frames and pass the frames to QR code detector.

### How to Set the Camera Frame Callback
When using CameraX, we can use `ImageAnalysis` class to receive the camera frames:

```java
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 is much easier. The callback function looks like the one used in Android Camera1:

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

The data types returned by their callback functions are different. Data type conversion is required for later use.

### Decoding QR Code
With CameraX, we firstly convert `ByteBuffer` to `byte[]` and then call the `decodeBuffer()` method:

```java
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();
});
```


Whereas with Dynamsoft Camera Enhancer, we get `Bitmap` from the `DCEFrame` and then call the `decodeBufferedImage()` method:

```java
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](https://www.dynamsoft.com/codepool/img/2021/12/android-qr-code-scanner.jpeg)

## Using Zoom and Torch to Boost the Frame Quality
The recognition accuracy is always affected by the input image quality. If the QR code is too small, we can zoom in the camera to scale up the image. If the input image is too dark, we can turn on the torch to brighten the image. Both of CameraX and Dynamsoft Camera Enhancer have completely supported the camera control.

### Android Camera Zoom
We use finger pinch gesture to trigger the zoom. Thus, the first step is to create the gesture detector and take over the `onTouchEvent()` method:

```java
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);
}
```

As the gesture is detected, we get the scale factor and take it as the zoom ratio.

**Set camera zoom ratio with CameraX**

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

**Set camera zoom ratio with Dynamsoft Camera Enhancer**

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

### Android Camera Torch
To turn on the torch automatically, we monitor the light value returned by the light sensor.

```java
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;
    }
}
```

**Toggle camera torch with CameraX**

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

**Toggle camera torch with Dynamsoft Camera Enhancer**

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

## Demo Video of Android QR Code Scanner

<video src="https://www.dynamsoft.com/codepool/img/2021/12/android-qr-code-scanner.mp4" controls="controls" muted="muted" style="max-height:640px;max-width:100%;"></video>


## Source Code
[https://github.com/yushulx/android-camera2-preview-qr-code-scanner](https://github.com/yushulx/android-camera2-preview-qr-code-scanner)