I have found some good looking code examples (like this one: http://www.codepool.biz/ocr-barcode-twain/how-to-implement-a-simple-barcode-scan-application-on-android.html) that describe how to use the ZXing library to decode QR codes in an android app.
However, there seem to be classes missing from the ZXing 2.3 core.jar file such as BufferedImageLuminanceSource
which is used in most of the code I have found. That being the case, I decided to try to reverse-engineer pieces of the barcode scanner app to fit and came up with the following:
The Result variable rawResult
in the onPreviewFrame()
method is always null. What am I doing wrong?
EDIT: Integrating ZXing using Intents is impossible for this project, just FYI.
Layout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:drawable/btn_default"
tools:context=".RedactedFragment" >
<SurfaceView
android:id="@+id/cameraViewfinder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />
</FrameLayout>
RedactedFragment.java:
import java.util.Map;
import java.util.TreeMap;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.os.Bundle;
import android.app.Activity;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.widget.Toast;
/**
* An {@link android.app.Fragment} subclass for the ###{@link RedactedActivity}### which will serve as the fourth {@link Fragment} to be displayed inside the ###{@link RedactedActivity}###.
*/
public class RedactedFragment extends Fragment implements Callback,
PreviewCallback, OnClickListener, OnLayoutChangeListener {
FragmentSwapperIFace act;
Camera cam;
boolean cameraReady = false;
SurfaceView cameraViewfinder;
SurfaceHolder surfaceHolder;
int wx, hy;
public RedactedFragment() {
// Required empty public constructor
}
/**
* The {@link Fragment#onPause()} method is overridden to ensure that the camera is released cleanly by this {@link Fragment}
* @see android.app.Fragment#onPause()
*/
@Override
public void onPause() {
// TODO Auto-generated method stub
super.onPause();
cam.setPreviewCallback(null);
cam.release();
}
/**
* The {@link Fragment#onResume()} method is overridden to ensure that the camera is properly setup and that the {@link PreviewCallback} is added to it.
* @see android.app.Fragment#onResume()
*/
@Override
public void onResume() {
// TODO Auto-generated method stub
super.onResume();
cam = Camera.open();
cam.setPreviewCallback(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View v = inflater.inflate(R.layout.fragment_qrscan, container, false);
cameraViewfinder = (SurfaceView) v.findViewById(R.id.cameraViewfinder);
surfaceHolder = cameraViewfinder.getHolder();
surfaceHolder.addCallback(this);
cameraViewfinder.addOnLayoutChangeListener(this);
cameraViewfinder.setOnClickListener(this);
return v;
}
/**
* This method is overridden to add the {@link FragmentSwapperIFace} for requesting that the parent {@link Activity} swap this part of the UI for a different {@link Fragment}
* @see android.app.Fragment#onAttach(android.app.Activity)
*/
@Override
public void onAttach(Activity activity) {
// TODO Auto-generated method stub
super.onAttach(activity);
try {
act = (FragmentSwapperIFace) activity;
} catch (Exception e) {
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
Camera.Parameters parameters = cam.getParameters();
Camera.Size size = getBestPreviewSize(width, height, parameters);
if (size != null) {
parameters.setPreviewSize(size.width, size.height);
cam.setParameters(parameters);
cam.startPreview();
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
cam.setPreviewDisplay(holder);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder arg0) {
// TODO Auto-generated method stub
}
@Override
public void onClick(View v) {
act.swap(this, new ResultFragment(), R.id.layoutFragment_qr_fragment);
}
/**
* Method borrowed from the ZXing team's barcode scanner app
* @return {@link Camera.Size}
*/
private Camera.Size getBestPreviewSize(int width, int height,
Camera.Parameters parameters) {
Camera.Size result = null;
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width <= width && size.height <= height) {
if (result == null) {
result = size;
} else {
int resultArea = result.width * result.height;
int newArea = size.width * size.height;
if (newArea > resultArea) {
result = size;
}
}
}
}
return (result);
}
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
if (!cameraReady)
return;
Rect rect = getFramingRect();
PlanarYUVLuminanceSource src = new PlanarYUVLuminanceSource(data, camera.getParameters().getPictureSize().width, camera.getParameters().getPictureSize().height, rect.left, rect.top, camera.getParameters().getPictureSize().width, camera.getParameters().getPictureSize().height, true);
Result rawResult = null;
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
Map<DecodeHintType, String> hints = new TreeMap<DecodeHintType, String>();
hints.put(DecodeHintType.CHARACTER_SET, "utf-8");
try {
rawResult = new QRCodeReader().decode(bitmap);
} catch (Exception e) {
e.printStackTrace();
}
if(rawResult == null){
Toast.makeText(getActivity(), "rawResult is null", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(getActivity(), rawResult.getText(), Toast.LENGTH_SHORT).show();
}
}
/**
* This method determines the size of the {@link SurfaceView} that the camera preview will be displayed on
* @return {@link Rect} a rectangle of the dimensions of the {@link SurfaceView}
* @author Original method by the ZXing team
*/
public Rect getFramingRect() {
Rect framingRect;
if (cam == null)
return null;
framingRect = new Rect(0, 0, wx, hy);
return framingRect;
}
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (v.getId() == cameraViewfinder.getId()) {
wx = v.getWidth();
hy = v.getHeight();
cameraReady = true;
}
}
}
UPDATE: I have now copied this fragment into its own test app with the same operating conditions and it is working in the test app. There is plenty of memory available on the test device and the fragment is instantiated in exactly the same way in the test app and the real app. Why is the new QRCodeReader.decode(bitmap)
method always returning null in the real app using the same code?