Second part of BQP UI improvements: zxing viewfinder.

This commit is contained in:
str4d
2016-05-06 14:20:10 +01:00
parent 0d16dd0358
commit 0e32139a89
6 changed files with 437 additions and 5 deletions

View File

@@ -10,6 +10,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.briarproject.android.util.ViewfinderView
android:id="@+id/viewfinder_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -43,4 +43,10 @@
<color name="spinner_border">#61000000</color> <!-- 38% Black -->
<color name="spinner_arrow">@color/briar_blue_dark</color>
<!-- ViewfinderView -->
<color name="possible_result_points">#c0ffbd21</color> <!-- Material Yellow 700 with alpha -->
<color name="result_view">#b0000000</color>
<color name="viewfinder_laser">#d50000</color> <!-- Red accent 700 -->
<color name="viewfinder_mask">#60000000</color>
</resources>

View File

@@ -19,6 +19,8 @@ import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.Result;
import com.google.zxing.ResultPoint;
import com.google.zxing.ResultPointCallback;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
@@ -27,6 +29,7 @@ import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.util.CameraView;
import org.briarproject.android.util.QrCodeDecoder;
import org.briarproject.android.util.QrCodeUtils;
import org.briarproject.android.util.ViewfinderView;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.KeyAgreementAbortedEvent;
import org.briarproject.api.event.KeyAgreementFailedEvent;
@@ -55,7 +58,7 @@ import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
public class ShowQrCodeFragment extends BaseEventFragment
implements QrCodeDecoder.ResultCallback {
implements QrCodeDecoder.ResultCallback, ResultPointCallback {
public static final String TAG = "ShowQrCodeFragment";
@@ -75,6 +78,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
protected Executor ioExecutor;
private CameraView cameraView;
private ViewfinderView viewfinderView;
private View statusView;
private TextView status;
private ImageView qrCode;
@@ -109,9 +113,13 @@ public class ShowQrCodeFragment extends BaseEventFragment
super.onViewCreated(view, savedInstanceState);
cameraView = (CameraView) view.findViewById(R.id.camera_view);
viewfinderView =
(ViewfinderView) view.findViewById(R.id.viewfinder_view);
statusView = view.findViewById(R.id.status_container);
status = (TextView) view.findViewById(R.id.connect_status);
qrCode = (ImageView) view.findViewById(R.id.qr_code);
viewfinderView.setFrameProvider(cameraView);
}
@Override
@@ -120,7 +128,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
decoder = new QrCodeDecoder(this);
decoder = new QrCodeDecoder(this, this);
}
@Override
@@ -219,6 +227,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
getActivity().finish();
} else {
cameraView.start(camera, decoder, 0);
viewfinderView.drawViewfinder();
}
}
};
@@ -355,6 +364,11 @@ public class ShowQrCodeFragment extends BaseEventFragment
});
}
@Override
public void foundPossibleResultPoint(ResultPoint point) {
viewfinderView.addPossibleResultPoint(point);
}
private class BluetoothStateReceiver extends BroadcastReceiver {
@Override

View File

@@ -1,6 +1,8 @@
package org.briarproject.android.util;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo;
@@ -13,6 +15,7 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;
@@ -31,17 +34,25 @@ import static java.util.logging.Level.WARNING;
@SuppressWarnings("deprecation")
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
AutoFocusCallback {
AutoFocusCallback, ViewfinderView.FrameProvider {
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
private static final int MIN_FRAME_SIZE = 240;
private static final int MAX_FRAME_SIZE = 675; // = 5/8 * 1080
private static final Logger LOG =
Logger.getLogger(CameraView.class.getName());
private Camera camera = null;
private Rect framingRect;
private Rect framingRectInPreview;
private Rect framingRectInSensor;
private PreviewConsumer previewConsumer = null;
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
private boolean autoFocus = false, surfaceExists = false;
private Point cameraResolution;
private final Object cameraResolutionLock = new Object();
public CameraView(Context context) {
super(context);
}
@@ -184,6 +195,24 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
LOG.info("No suitable focus mode");
}
params.setZoom(0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
List<Camera.Area> areas = new ArrayList<>();
areas.add(new Camera.Area(getFramingRectInSensor(), 1000));
if (params.getMaxNumFocusAreas() > 0) {
if (LOG.isLoggable(INFO)) {
LOG.info("Focus areas supported: " +
params.getMaxNumFocusAreas());
}
params.setFocusAreas(areas);
}
if (params.getMaxNumMeteringAreas() > 0) {
if (LOG.isLoggable(INFO)) {
LOG.info("Metering areas supported: " +
params.getMaxNumMeteringAreas());
}
params.setMeteringAreas(areas);
}
}
}
private void setPreviewSize(Parameters params) {
@@ -222,6 +251,13 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
if (LOG.isLoggable(INFO))
LOG.info("Best size " + bestSize.width + "x" + bestSize.height);
params.setPreviewSize(bestSize.width, bestSize.height);
synchronized (cameraResolutionLock) {
cameraResolution = new Point(bestSize.width, bestSize.height);
}
} else {
synchronized (cameraResolutionLock) {
cameraResolution = null;
}
}
}
@@ -276,4 +312,152 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
LOG.log(WARNING, "Error retrying auto focus", e);
}
}
/**
* Calculates the framing rect which the UI should draw to show the user where to place the
* barcode. This target helps with alignment as well as forces the user to hold the device
* far enough away to ensure the image will be in focus.
*
* @return The rectangle to draw on screen in window coordinates.
*/
@Override
public Rect getFramingRect() {
if (framingRect == null) {
framingRect = calculateFramingRect(true);
if (LOG.isLoggable(INFO))
LOG.info("Calculated framing rect: " + framingRect);
}
return framingRect;
}
/**
* Calculates the framing rect which the UI should draw to show the user where to place the
* barcode. This target helps with alignment as well as forces the user to hold the device
* far enough away to ensure the image will be in focus.
* <p/>
* Adapted from the Zxing Barcode Scanner.
*
* @return The rectangle to draw on screen in window coordinates.
*/
private Rect calculateFramingRect(boolean withOrientation) {
if (camera == null) {
return null;
}
if (surfaceWidth == 0 || surfaceHeight == 0) {
// Called early, before the surface is ready
return null;
}
boolean portrait =
withOrientation && displayOrientation % 180 == 90;
int size = findDesiredDimensionInRange(
portrait ? surfaceWidth : surfaceHeight,
portrait ? surfaceHeight / 2 : surfaceWidth / 2,
MIN_FRAME_SIZE, MAX_FRAME_SIZE);
int leftOffset = portrait ?
(surfaceWidth - size) / 2 :
((surfaceWidth / 2) - size) / 2;
int topOffset = portrait ?
((surfaceHeight / 2) - size) / 2 :
(surfaceHeight - size) / 2;
return new Rect(leftOffset, topOffset, leftOffset + size,
topOffset + size);
}
/**
* Calculates the square that fits best inside the given region.
*/
private static int findDesiredDimensionInRange(int side1, int side2,
int hardMin, int hardMax) {
if (LOG.isLoggable(INFO))
LOG.info("Finding framing dimension, side1 = " + side1 +
", side2 = " + side2);
int minSide = Math.min(side1, side2);
int dim = 5 * minSide / 8; // Target 5/8 of smallest side
if (dim < hardMin) {
if (hardMin > minSide) {
if (LOG.isLoggable(INFO))
LOG.info("Returning minimum side length: " + minSide);
return minSide;
} else {
if (LOG.isLoggable(INFO))
LOG.info("Returning hard minimum: " + hardMin);
return hardMin;
}
}
if (dim > hardMax) {
if (LOG.isLoggable(INFO))
LOG.info("Returning hard maximum: " + hardMax);
return hardMax;
}
if (LOG.isLoggable(INFO))
LOG.info("Returning desired dimension: " + dim);
return dim;
}
/**
* Like {@link #getFramingRect} but coordinates are in terms of the preview
* frame, not UI / screen.
* <p/>
* Adapted from the Zxing Barcode Scanner.
*
* @return {@link Rect} expressing QR code scan area in terms of the preview size
*/
@Override
public Rect getFramingRectInPreview() {
if (framingRectInPreview == null) {
Rect framingRect = getFramingRect();
if (framingRect == null) {
return null;
}
Rect rect = new Rect(framingRect);
Point cameraResolution = getCameraResolution();
if (cameraResolution == null || surfaceWidth == 0 ||
surfaceHeight == 0) {
// Called early, before the surface is ready
return null;
}
rect.left = rect.left * cameraResolution.x / surfaceWidth;
rect.right = rect.right * cameraResolution.x / surfaceWidth;
rect.top = rect.top * cameraResolution.y / surfaceHeight;
rect.bottom = rect.bottom * cameraResolution.y / surfaceHeight;
framingRectInPreview = rect;
}
return framingRectInPreview;
}
private Point getCameraResolution() {
Point ret;
synchronized (cameraResolutionLock) {
ret = new Point(cameraResolution);
}
return ret;
}
/**
* Like {@link #getFramingRect} but coordinates are in terms of the sensor,
* not UI / screen (ie. it is independent of orientation)
*
* @return {@link Rect} expressing QR code scan area in terms of the sensor
*/
private Rect getFramingRectInSensor() {
if (framingRectInSensor == null) {
Rect framingRect = calculateFramingRect(false);
if (framingRect == null) {
return null;
}
Rect rect = new Rect(framingRect);
if (surfaceWidth == 0 || surfaceHeight == 0) {
// Called early, before the surface is ready
return null;
}
rect.left = (rect.left * 2000 / surfaceWidth) - 1000;
rect.right = (rect.right * 2000 / surfaceWidth) - 1000;
rect.top = (rect.top * 2000 / surfaceHeight) - 1000;
rect.bottom = (rect.bottom * 2000 / surfaceHeight) - 1000;
framingRectInSensor = rect;
}
return framingRectInSensor;
}
}

View File

@@ -6,14 +6,18 @@ import android.hardware.Camera.Size;
import android.os.AsyncTask;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.ResultPointCallback;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeReader;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import static java.util.logging.Level.INFO;
@@ -26,11 +30,14 @@ public class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
private final Reader reader = new QRCodeReader();
private final ResultCallback callback;
private final ResultPointCallback pointCallback;
private boolean stopped = false;
public QrCodeDecoder(ResultCallback callback) {
public QrCodeDecoder(ResultCallback callback,
ResultPointCallback pointCallback) {
this.callback = callback;
this.pointCallback = pointCallback;
}
public void start(Camera camera) {
@@ -72,9 +79,11 @@ public class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
height, 0, 0, width, height, false);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, pointCallback);
Result result = null;
try {
result = reader.decode(bitmap);
result = reader.decode(bitmap, hints);
} catch (ReaderException e) {
return null; // No barcode found
} catch (RuntimeException e) {

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2008 ZXing authors
* Copyright (C) 2016 Sublime Software Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.briarproject.android.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import com.google.zxing.ResultPoint;
import org.briarproject.R;
import java.util.ArrayList;
import java.util.List;
/**
* This view is overlaid on top of the camera preview. It adds the viewfinder
* rectangle and partial transparency outside it, as well as the laser scanner
* animation and result points.
*
* @author dswitkin@google.com (Daniel Switkin)
*/
public final class ViewfinderView extends View {
private static final int[] SCANNER_ALPHA =
{0, 64, 128, 192, 255, 192, 128, 64};
private static final long ANIMATION_DELAY = 80L;
private static final int CURRENT_POINT_OPACITY = 0xA0;
private static final int MAX_RESULT_POINTS = 20;
private static final int POINT_SIZE = 6;
private FrameProvider frameProvider;
private final Paint paint;
private Bitmap resultBitmap;
private final int maskColor;
private final int resultColor;
private final int laserColor;
private final int resultPointColor;
private int scannerAlpha;
private List<ResultPoint> possibleResultPoints;
private List<ResultPoint> lastPossibleResultPoints;
// This constructor is used when the class is built from an XML resource.
public ViewfinderView(Context context, AttributeSet attrs) {
super(context, attrs);
if (isInEditMode()) {
paint = null;
maskColor = 0;
resultColor = 0;
laserColor = 0;
resultPointColor = 0;
return;
}
// Initialize these once for performance rather than calling them every
// time in onDraw().
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
Resources resources = getResources();
maskColor = resources.getColor(R.color.viewfinder_mask);
resultColor = resources.getColor(R.color.result_view);
laserColor = resources.getColor(R.color.viewfinder_laser);
resultPointColor = resources.getColor(R.color.possible_result_points);
scannerAlpha = 0;
possibleResultPoints = new ArrayList<>(5);
lastPossibleResultPoints = null;
}
public void setFrameProvider(FrameProvider frameProvider) {
this.frameProvider = frameProvider;
}
@SuppressLint("DrawAllocation")
@Override
public void onDraw(Canvas canvas) {
if (frameProvider == null) {
return; // not ready yet, early draw before done configuring
}
Rect frame = this.frameProvider.getFramingRect();
Rect previewFrame = this.frameProvider.getFramingRectInPreview();
if (frame == null || previewFrame == null) {
return;
}
int width = canvas.getWidth();
int height = canvas.getHeight();
// Draw the exterior (i.e. outside the framing rect) darkened
paint.setColor(resultBitmap != null ? resultColor : maskColor);
canvas.drawRect(0, 0, width, frame.top, paint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1,
paint);
canvas.drawRect(0, frame.bottom + 1, width, height, paint);
if (resultBitmap != null) {
// Draw the opaque result bitmap over the scanning rectangle
paint.setAlpha(CURRENT_POINT_OPACITY);
canvas.drawBitmap(resultBitmap, null, frame, paint);
} else {
// Draw a red "laser scanner" line through the middle to show
// decoding is active
paint.setColor(laserColor);
paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
int middle = frame.height() / 2 + frame.top;
canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1,
middle + 2, paint);
float scaleX = frame.width() / (float) previewFrame.width();
float scaleY = frame.height() / (float) previewFrame.height();
List<ResultPoint> currentPossible = possibleResultPoints;
List<ResultPoint> currentLast = lastPossibleResultPoints;
int frameLeft = frame.left;
int frameTop = frame.top;
if (currentPossible.isEmpty()) {
lastPossibleResultPoints = null;
} else {
possibleResultPoints = new ArrayList<>(5);
lastPossibleResultPoints = currentPossible;
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setColor(resultPointColor);
synchronized (currentPossible) {
for (ResultPoint point : currentPossible) {
canvas.drawCircle(
frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
POINT_SIZE, paint);
}
}
}
if (currentLast != null) {
paint.setAlpha(CURRENT_POINT_OPACITY / 2);
paint.setColor(resultPointColor);
synchronized (currentLast) {
float radius = POINT_SIZE / 2.0f;
for (ResultPoint point : currentLast) {
canvas.drawCircle(
frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
radius, paint);
}
}
}
// Request another update at the animation interval, but only
// repaint the laser line, not the entire viewfinder mask.
postInvalidateDelayed(ANIMATION_DELAY,
frame.left - POINT_SIZE,
frame.top - POINT_SIZE,
frame.right + POINT_SIZE,
frame.bottom + POINT_SIZE);
}
}
public void drawViewfinder() {
Bitmap resultBitmap = this.resultBitmap;
this.resultBitmap = null;
if (resultBitmap != null) {
resultBitmap.recycle();
}
invalidate();
}
/**
* Draw a bitmap with the result points highlighted instead of the live
* scanning display.
*
* @param barcode An image of the decoded barcode.
*/
public void drawResultBitmap(Bitmap barcode) {
resultBitmap = barcode;
invalidate();
}
public void addPossibleResultPoint(ResultPoint point) {
List<ResultPoint> points = possibleResultPoints;
synchronized (points) {
points.add(point);
int size = points.size();
if (size > MAX_RESULT_POINTS) {
// trim it
points.subList(0, size - MAX_RESULT_POINTS / 2).clear();
}
}
}
public interface FrameProvider {
Rect getFramingRect();
Rect getFramingRectInPreview();
}
}