Barcode scanner using Google Vision

The implementation of the Google Vision api is not that hard, but there are a few fixes you’ll need to make your app production ready. Most importantly: the default implementation does not support autofocus which makes it very hard to scan a barcode.

Usage

startActivityForResult(new Intent(context, BarcodeActivity.class), REQUEST_CODE);

...

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
    String barcode = data.getStringExtra(BarcodeActivity.EXTRA_STR_QRCODE);
    ...
  }
}

BarcodeActivity.java

Barcode scanning activity. Returns the scanned value as soon as it is recognized, without checking if it is a suitable URI or not!

Possible results:

  • RESULT_OK - Code scanned, it will contain it as the value for {@link #EXTRA_STR_QRCODE} in the result intent.
  • RESULT_CANCEL - User canceled. Scanner closes.
package com.pixplicity.example.activities;

import android.Manifest.permission;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import android.view.SurfaceHolder;
import android.view.View;
import android.widget.TextView;

import com.google.android.gms.vision.CameraSource;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;
import com.google.android.gms.vision.barcode.BarcodeDetector.Builder;
import com.pixplicity.example.R;
import com.pixplicity.example.ui.CameraView;
import com.pixplicity.example.util.CameraFocusFix;

import java.io.IOException;

import androidx.core.app.ActivityCompat;

/**
 * Barcode scanning activity. Returns the scanned value as soon as it is recognized, <em>without checking if it is a suitable URI or not!</em>
 * <p>
 * Possible results:
 * <ul>
 * <li>RESULT_OK -- Code scanned, it will contain it as the value for {@link #EXTRA_STR_QRCODE} in the result intent.</li>
 * <li>RESULT_CANCEL -- User canceled. Scanner closes.</li>
 * </ul>
 */
public class BarcodeActivity extends Activity {

    public static final String EXTRA_STR_QRCODE = "extra_qr";

    private static final String TAG = BarcodeActivity.class.getSimpleName();

    protected CameraView mCameraView;
    protected TextView mMessageView;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.activity_camera);

        mCameraView = findViewById(R.id.camera_surfaceview);
        mMessageView = findViewById(R.id.tv_camera_error);

        final BarcodeDetector barcodeDetector = new Builder(this)
                .setBarcodeFormats(Barcode.QR_CODE)
                .build();
        final CameraSource cameraSource = new CameraSource
                .Builder(this, barcodeDetector)
                .setAutoFocusEnabled(true)
                .setRequestedFps(30f)
                .build();

        mCameraView.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                try {
                    if (ActivityCompat.checkSelfPermission(BarcodeActivity.this, permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                        return;
                    }

                    // Start preview
                    cameraSource.start(mCameraView.getHolder());

                    // Fix broken autofocus
                    CameraFocusFix.cameraFocus(cameraSource, android.hardware.Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);

                    // Fix stretched preview
                    mCameraView.setCameraSource(cameraSource);

                } catch (IOException ie) {
                    mMessageView.setText(R.string.error_camera_open);
                    Log.e(TAG, ie.getMessage());
                }
            }

            @Override
            public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            }

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                cameraSource.stop();
            }
        });

        barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
            @Override
            public void release() {
            }

            @Override
            public void receiveDetections(Detector.Detections<Barcode> detections) {
                final SparseArray<Barcode> barcodes = detections.getDetectedItems();
                if (barcodes.size() != 0) {
                    mMessageView.post(() -> {
                        Intent data = new Intent();
                        data.putExtra(EXTRA_STR_QRCODE, barcodes.valueAt(0).displayValue);
                        setResult(RESULT_OK, data);
                        finish();
                    });
                }
            }
        });
    }
}

CameraFocusFix.java

This utility class fixes the broken autofocus. Found here.

package com.pixplicity.example.util;

import android.hardware.Camera;
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
import android.view.SurfaceHolder;

import com.google.android.gms.vision.CameraSource;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;

/**
 * Source: https://gist.github.com/Gericop/7de0b9fdd7a444e53b5a
 */
public class CameraFocusFix {
    /**
     * Custom annotation to allow only valid focus modes.
     */
    @StringDef({
            Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
            Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
            Camera.Parameters.FOCUS_MODE_AUTO,
            Camera.Parameters.FOCUS_MODE_EDOF,
            Camera.Parameters.FOCUS_MODE_FIXED,
            Camera.Parameters.FOCUS_MODE_INFINITY,
            Camera.Parameters.FOCUS_MODE_MACRO
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface FocusMode {
    }

    /**
     * <p>
     * Sets the Mobile Vision API provided {@link com.google.android.gms.vision.CameraSource}'s
     * focus mode. Use {@link Camera.Parameters#FOCUS_MODE_CONTINUOUS_PICTURE} or
     * {@link Camera.Parameters#FOCUS_MODE_CONTINUOUS_VIDEO} for continuous autofocus.
     * </p>
     * <p>
     * Note that the CameraSource's {@link CameraSource#start()} or
     * {@link CameraSource#start(SurfaceHolder)} has to be called and the camera image has to be
     * showing prior using this method as the CameraSource only creates the camera after calling
     * one of those methods and the camera is not available immediately. You could implement some
     * kind of a callback method for the SurfaceHolder that notifies you when the imaging is ready
     * or use a direct action (e.g. button press) to set the focus mode.
     * </p>
     * <p>
     * Check out <a href="https://github.com/googlesamples/android-vision/blob/master/face/multi-tracker/app/src/main/java/com/google/android/gms/samples/vision/face/multitracker/ui/camera/CameraSourcePreview.java#L84">CameraSourcePreview.java</a>
     * which contains the method <code>startIfReady()</code> that has the following line:
     * <blockquote><code>mCameraSource.start(mSurfaceView.getHolder());</code></blockquote><br>
     * After this call you can use our <code>cameraFocus(...)</code> method because the camera is ready.
     * </p>
     *
     * @param cameraSource The CameraSource built with {@link com.google.android.gms.vision.CameraSource.Builder}.
     * @param focusMode    The focus mode. See {@link android.hardware.Camera.Parameters} for possible values.
     * @return true if the camera's focus is set; false otherwise.
     * @see com.google.android.gms.vision.CameraSource
     * @see android.hardware.Camera.Parameters
     */
    public static boolean cameraFocus(@NonNull CameraSource cameraSource, @FocusMode @NonNull String focusMode) {
        Field[] declaredFields = CameraSource.class.getDeclaredFields();

        for (Field field : declaredFields) {
            if (field.getType() == Camera.class) {
                field.setAccessible(true);
                try {
                    Camera camera = (Camera) field.get(cameraSource);
                    if (camera != null) {
                        Camera.Parameters params = camera.getParameters();

                        if (!params.getSupportedFocusModes().contains(focusMode)) {
                            return false;
                        }

                        params.setFocusMode(focusMode);
                        camera.setParameters(params);
                        return true;
                    }

                    return false;
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

                break;
            }
        }

        return false;
    }
}

CameraView.java

This SurfaceView ensures the preview is not stretched or squished. This surface is only useful for the barcode scanner (it uses CameraSource and should not be used for taking pictures (there are more elaborate examples for that, which pick the best preview size, etc.)

package com.pixplicity.example.ui;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.SurfaceView;

import com.google.android.gms.common.images.Size;
import com.google.android.gms.vision.CameraSource;

/**
 * This SurfaceView ensures the preview is not stretched or squished.
 * This surface is only useful for the barcode scanner (it uses {@link CameraSource}
 * and should not be used for taking pictures (there are more elaborate examples for that,
 * which pick the best preview size, etc.)
 */
public class CameraView extends SurfaceView {

    private Size mPreviewSize;

    public CameraView(Context context) {
        super(context);
    }

    public CameraView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CameraView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public void setCameraSource(CameraSource source) {
        mPreviewSize = source.getPreviewSize();
        requestLayout();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        int newWidth = width, newHeight = height;

        if (mPreviewSize != null) {

            float camRatio = (float) mPreviewSize.getWidth() / (float) mPreviewSize.getHeight();
            float screenRatio = (float) width / (float) height;

            if (camRatio < screenRatio) {
                newHeight = (int) (width / camRatio);
                setY(getY() - .5f * (newHeight - height));
            } else {
                newWidth = (int) (height / camRatio);
                setX(getX() - .5f * (newWidth - width));
            }
        }
        setMeasuredDimension(newWidth, newHeight);
    }
}

Resource files

activity_camera.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root_camera"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black"
    android:fitsSystemWindows="false"
    tools:context="com.pixplicity.example.activities.BarcodeActivity">

    <com.pixplicity.example.ui.CameraView
        android:id="@+id/camera_surfaceview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:alpha="0"
        android:visibility="gone" />

    <com.pixplicity.example.ui.RatioLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:layout_margin="64dp"
        android:background="@drawable/bg_viewfinder"
        android:maxHeight="@dimen/max_viewfinder"
        android:maxWidth="@dimen/max_viewfinder">

        <com.pixplicity.fontview.FontTextView
            android:id="@+id/tv_camera_error"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:gravity="center"
            android:text="@string/error_camera_open"
            android:textColor="@color/white"
            android:visibility="invisible" />

        <com.pixplicity.fontview.FontTextView
            android:id="@+id/tv_instructions"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center|bottom"
            android:layout_margin="19sp"
            android:gravity="center"
            android:text="@string/scan_qr_code"
            android:textColor="@color/white"
            android:textSize="19sp" />

    </com.pixplicity.example.ui.RatioLayout>
  
</FrameLayout>

bg_viewfinder.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="8dp" />
    <stroke
        android:width="3dp"
        android:color="@color/white"
        android:dashGap="8dp"
        android:dashWidth="8dp" />
</shape>