diff --git a/briar-android/res/layout/fragment_keyagreement_qr.xml b/briar-android/res/layout/fragment_keyagreement_qr.xml
index 5f1b175ed..aa901c673 100644
--- a/briar-android/res/layout/fragment_keyagreement_qr.xml
+++ b/briar-android/res/layout/fragment_keyagreement_qr.xml
@@ -10,6 +10,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
+
+
#61000000
@color/briar_blue_dark
+
+
+ #c0ffbd21
+ #b0000000
+ #d50000
+ #60000000
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
index 60c7ca85b..e16c4973d 100644
--- a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
+++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
@@ -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
diff --git a/briar-android/src/org/briarproject/android/util/CameraView.java b/briar-android/src/org/briarproject/android/util/CameraView.java
index d3690b189..dfec325af 100644
--- a/briar-android/src/org/briarproject/android/util/CameraView.java
+++ b/briar-android/src/org/briarproject/android/util/CameraView.java
@@ -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 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.
+ *
+ * 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.
+ *
+ * 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;
+ }
}
\ No newline at end of file
diff --git a/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java b/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java
index 207adf368..a6d65849a 100644
--- a/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java
+++ b/briar-android/src/org/briarproject/android/util/QrCodeDecoder.java
@@ -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 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) {
diff --git a/briar-android/src/org/briarproject/android/util/ViewfinderView.java b/briar-android/src/org/briarproject/android/util/ViewfinderView.java
new file mode 100644
index 000000000..0e1836392
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/util/ViewfinderView.java
@@ -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 possibleResultPoints;
+ private List 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 currentPossible = possibleResultPoints;
+ List 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 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();
+ }
+}