Merge branch '346-qr-code-optimisations' into 'maintenance-0.16'

Backport: Improve QR code scanning on phones with high res cameras and slow CPUs

See merge request akwizgran/briar!715
This commit is contained in:
akwizgran
2018-03-07 11:06:56 +00:00
8 changed files with 355 additions and 149 deletions

View File

@@ -6,7 +6,6 @@ import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.CameraInfo; import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters; import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size; import android.hardware.Camera.Size;
import android.os.Build;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -22,6 +21,7 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.logging.Logger; import java.util.logging.Logger;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK;
import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT; import static android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT;
import static android.hardware.Camera.Parameters.FLASH_MODE_OFF; import static android.hardware.Camera.Parameters.FLASH_MODE_OFF;
import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO; import static android.hardware.Camera.Parameters.FOCUS_MODE_AUTO;
@@ -32,6 +32,7 @@ import static android.hardware.Camera.Parameters.FOCUS_MODE_FIXED;
import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO; import static android.hardware.Camera.Parameters.FOCUS_MODE_MACRO;
import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO; import static android.hardware.Camera.Parameters.SCENE_MODE_AUTO;
import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE; import static android.hardware.Camera.Parameters.SCENE_MODE_BARCODE;
import static android.os.Build.VERSION.SDK_INT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -41,7 +42,12 @@ import static java.util.logging.Level.WARNING;
public class CameraView extends SurfaceView implements SurfaceHolder.Callback, public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
AutoFocusCallback, View.OnClickListener { AutoFocusCallback, View.OnClickListener {
// Heuristic for the ideal preview size - small previews don't have enough
// detail, large previews are slow to decode
private static final int IDEAL_PIXELS = 500 * 1000;
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(CameraView.class.getName()); Logger.getLogger(CameraView.class.getName());
@@ -49,6 +55,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@Nullable @Nullable
private Camera camera = null; private Camera camera = null;
private int cameraIndex = 0;
private PreviewConsumer previewConsumer = null; private PreviewConsumer previewConsumer = null;
private Surface surface = null; private Surface surface = null;
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0; private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
@@ -89,15 +96,32 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} }
@UiThread @UiThread
public void start() throws CameraException { public void start(int rotationDegrees) throws CameraException {
LOG.info("Opening camera"); LOG.info("Opening camera");
try { try {
camera = Camera.open(); int cameras = Camera.getNumberOfCameras();
if (cameras == 0) throw new CameraException("No camera");
// Try to find a back-facing camera
for (int i = 0; i < cameras; i++) {
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == CAMERA_FACING_BACK) {
LOG.info("Using back-facing camera");
camera = Camera.open(i);
cameraIndex = i;
break;
}
}
// If we can't find a back-facing camera, use a front-facing one
if (camera == null) {
LOG.info("Using front-facing camera");
camera = Camera.open(0);
cameraIndex = 0;
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
if (camera == null) throw new CameraException("No back-facing camera"); setDisplayOrientation(rotationDegrees);
setDisplayOrientation(0);
// Use barcode scene mode if it's available // Use barcode scene mode if it's available
Parameters params = camera.getParameters(); Parameters params = camera.getParameters();
params = setSceneMode(camera, params); params = setSceneMode(camera, params);
@@ -163,7 +187,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
private void startConsumer() throws CameraException { private void startConsumer() throws CameraException {
if (camera == null) throw new CameraException("Camera is null"); if (camera == null) throw new CameraException("Camera is null");
startAutoFocus(); startAutoFocus();
previewConsumer.start(camera); previewConsumer.start(camera, cameraIndex);
} }
@UiThread @UiThread
@@ -199,13 +223,17 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} }
} }
/**
* See {@link Camera#setDisplayOrientation(int)}.
*/
@UiThread @UiThread
private void setDisplayOrientation(int rotationDegrees) private void setDisplayOrientation(int rotationDegrees)
throws CameraException { throws CameraException {
if (camera == null) throw new CameraException("Camera is null");
int orientation; int orientation;
CameraInfo info = new CameraInfo(); CameraInfo info = new CameraInfo();
try { try {
Camera.getCameraInfo(0, info); Camera.getCameraInfo(cameraIndex, info);
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
@@ -215,9 +243,11 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} else { } else {
orientation = (info.orientation - rotationDegrees + 360) % 360; orientation = (info.orientation - rotationDegrees + 360) % 360;
} }
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO)) {
LOG.info("Display orientation " + orientation + " degrees"); LOG.info("Screen rotation " + rotationDegrees
if (camera == null) throw new CameraException("Camera is null"); + " degrees, camera orientation " + orientation
+ " degrees");
}
try { try {
camera.setDisplayOrientation(orientation); camera.setDisplayOrientation(orientation);
} catch (RuntimeException e) { } catch (RuntimeException e) {
@@ -285,8 +315,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void setVideoStabilisation(Parameters params) { private void setVideoStabilisation(Parameters params) {
if (Build.VERSION.SDK_INT >= 15 && if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) {
params.isVideoStabilizationSupported()) {
params.setVideoStabilization(true); params.setVideoStabilization(true);
} }
} }
@@ -313,6 +342,8 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
@UiThread @UiThread
private void setPreviewSize(Parameters params) { private void setPreviewSize(Parameters params) {
if (surfaceWidth == 0 || surfaceHeight == 0) return; if (surfaceWidth == 0 || surfaceHeight == 0) return;
// Choose a preview size that's close to the aspect ratio of the
// surface and close to the ideal size for decoding
float idealRatio = (float) surfaceWidth / surfaceHeight; float idealRatio = (float) surfaceWidth / surfaceHeight;
boolean rotatePreview = displayOrientation % 180 == 90; boolean rotatePreview = displayOrientation % 180 == 90;
List<Size> sizes = params.getSupportedPreviewSizes(); List<Size> sizes = params.getSupportedPreviewSizes();
@@ -323,11 +354,12 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
int height = rotatePreview ? size.width : size.height; int height = rotatePreview ? size.width : size.height;
float ratio = (float) width / height; float ratio = (float) width / height;
float stretch = Math.max(ratio / idealRatio, idealRatio / ratio); float stretch = Math.max(ratio / idealRatio, idealRatio / ratio);
int pixels = width * height; float pixels = width * height;
float score = pixels / stretch; float zoom = Math.max(pixels / IDEAL_PIXELS, IDEAL_PIXELS / pixels);
float score = 1 / (stretch * zoom);
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("Size " + size.width + "x" + size.height LOG.info("Size " + size.width + "x" + size.height
+ ", stretch " + stretch + ", pixels " + pixels + ", stretch " + stretch + ", zoom " + zoom
+ ", score " + score); + ", score " + score);
} }
if (bestSize == null || score > bestScore) { if (bestSize == null || score > bestScore) {
@@ -358,7 +390,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new CameraException(e); throw new CameraException(e);
} }
if (Build.VERSION.SDK_INT >= 15) { if (SDK_INT >= 15) {
LOG.info("Video stabilisation enabled: " LOG.info("Video stabilisation enabled: "
+ params.getVideoStabilization()); + params.getVideoStabilization());
} }
@@ -389,8 +421,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
surface.release(); surface.release();
} }
surface = holder.getSurface(); surface = holder.getSurface();
// Start the preview when the camera and the surface are both ready // We'll start the preview when surfaceChanged() is called
if (camera != null && !previewStarted) startPreview(holder);
} }
@Override @Override
@@ -416,7 +447,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
surfaceWidth = w; surfaceWidth = w;
surfaceHeight = h; surfaceHeight = h;
if (camera == null) return; // We are stopped if (camera == null) return; // We are stopped
stopPreview(); if (previewStarted) stopPreview();
try { try {
Parameters params = camera.getParameters(); Parameters params = camera.getParameters();
setPreviewSize(params); setPreviewSize(params);

View File

@@ -10,7 +10,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
interface PreviewConsumer { interface PreviewConsumer {
@UiThread @UiThread
void start(Camera camera); void start(Camera camera, int cameraIndex);
@UiThread @UiThread
void stop(); void stop();

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.keyagreement; package org.briarproject.briar.android.keyagreement;
import android.hardware.Camera; import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size; import android.hardware.Camera.Size;
import android.os.AsyncTask; import android.os.AsyncTask;
@@ -36,20 +37,23 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
private final ResultCallback callback; private final ResultCallback callback;
private Camera camera = null; private Camera camera = null;
private int cameraIndex = 0;
QrCodeDecoder(ResultCallback callback) { QrCodeDecoder(ResultCallback callback) {
this.callback = callback; this.callback = callback;
} }
@Override @Override
public void start(Camera camera) { public void start(Camera camera, int cameraIndex) {
this.camera = camera; this.camera = camera;
this.cameraIndex = cameraIndex;
askForPreviewFrame(); askForPreviewFrame();
} }
@Override @Override
public void stop() { public void stop() {
camera = null; camera = null;
cameraIndex = 0;
} }
@UiThread @UiThread
@@ -61,45 +65,64 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
@Override @Override
public void onPreviewFrame(byte[] data, Camera camera) { public void onPreviewFrame(byte[] data, Camera camera) {
if (camera == this.camera) { if (camera == this.camera) {
LOG.info("Got preview frame");
try { try {
Size size = camera.getParameters().getPreviewSize(); Size size = camera.getParameters().getPreviewSize();
new DecoderTask(data, size.width, size.height).execute(); // The preview should be in NV21 format: width * height bytes of
// Y followed by width * height / 2 bytes of interleaved U and V
if (data.length == size.width * size.height * 3 / 2) {
CameraInfo info = new CameraInfo();
Camera.getCameraInfo(cameraIndex, info);
new DecoderTask(data, size.width, size.height,
info.orientation).execute();
} else {
// Camera parameters have changed - ask for a new preview
LOG.info("Preview size does not match camera parameters");
askForPreviewFrame();
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOG.log(WARNING, "Error getting camera parameters.", e); LOG.log(WARNING, "Error getting camera parameters.", e);
} }
} else {
LOG.info("Camera has changed, ignoring preview frame");
} }
} }
private class DecoderTask extends AsyncTask<Void, Void, Void> { private class DecoderTask extends AsyncTask<Void, Void, Void> {
private final byte[] data; private final byte[] data;
private final int width, height; private final int width, height, orientation;
private DecoderTask(byte[] data, int width, int height) { private DecoderTask(byte[] data, int width, int height,
int orientation) {
this.data = data; this.data = data;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.orientation = orientation;
} }
@Override @Override
protected Void doInBackground(Void... params) { protected Void doInBackground(Void... params) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
LuminanceSource src = new PlanarYUVLuminanceSource(data, width, BinaryBitmap bitmap = binarize(data, width, height, orientation);
height, 0, 0, width, height, false);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
Result result = null; Result result = null;
try { try {
result = reader.decode(bitmap); result = reader.decode(bitmap);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Decoding barcode took " + duration + " ms");
} catch (ReaderException e) { } catch (ReaderException e) {
return null; // No barcode found // No barcode found
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("No barcode found after " + duration + " ms");
return null;
} catch (RuntimeException e) { } catch (RuntimeException e) {
return null; // Preview data did not match width and height LOG.warning("Invalid preview frame");
return null;
} finally { } finally {
reader.reset(); reader.reset();
} }
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Decoding barcode took " + duration + " ms");
callback.handleResult(result); callback.handleResult(result);
return null; return null;
} }
@@ -110,6 +133,19 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
} }
} }
private static BinaryBitmap binarize(byte[] data, int width, int height,
int orientation) {
// Crop to a square at the top (portrait) or left (landscape) of the
// screen - this will be faster to decode and should include
// everything visible in the viewfinder
int crop = Math.min(width, height);
int left = orientation >= 180 ? width - crop : 0;
int top = orientation >= 180 ? height - crop : 0;
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
height, left, top, crop, crop, false);
return new BinaryBitmap(new HybridBinarizer(src));
}
@NotNullByDefault @NotNullByDefault
interface ResultCallback { interface ResultCallback {

View File

@@ -32,21 +32,24 @@ class QrCodeUtils {
// Generate QR code // Generate QR code
BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE, BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE,
smallestDimen, smallestDimen); smallestDimen, smallestDimen);
// Convert QR code to Bitmap return renderQrCode(encoded);
int width = encoded.getWidth();
int height = encoded.getHeight();
int[] pixels = new int[width * height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pixels[y * width + x] = encoded.get(x, y) ? BLACK : WHITE;
}
}
Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888);
qr.setPixels(pixels, 0, width, 0, 0, width, height);
return qr;
} catch (WriterException e) { } catch (WriterException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null; return null;
} }
} }
private static Bitmap renderQrCode(BitMatrix matrix) {
int width = matrix.getWidth();
int height = matrix.getHeight();
int[] pixels = new int[width * height];
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
pixels[y * width + x] = matrix.get(x, y) ? BLACK : WHITE;
}
}
Bitmap qr = Bitmap.createBitmap(width, height, ARGB_8888);
qr.setPixels(pixels, 0, width, 0, 0, width, height);
return qr;
}
} }

View File

@@ -6,12 +6,15 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.hardware.Camera;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.util.Base64; import android.util.Base64;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.Display;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.AlphaAnimation; import android.view.animation.AlphaAnimation;
@@ -144,7 +147,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
try { try {
cameraView.start(); cameraView.start(getScreenRotationDegrees());
} catch (CameraException e) { } catch (CameraException e) {
logCameraExceptionAndFinish(e); logCameraExceptionAndFinish(e);
} }
@@ -164,6 +167,25 @@ public class ShowQrCodeFragment extends BaseEventFragment
} }
} }
/**
* See {@link Camera#setDisplayOrientation(int)}.
*/
private int getScreenRotationDegrees() {
Display d = getActivity().getWindowManager().getDefaultDisplay();
switch (d.getRotation()) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
default:
throw new AssertionError();
}
}
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();

View File

@@ -1,79 +1,89 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <ScrollView
android:id="@+id/scrollView"
xmlns:android="http://schemas.android.com/apk/res/android" 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal"> android:orientation="vertical">
<ImageView <android.support.constraint.ConstraintLayout
android:id="@+id/imageView" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:padding="@dimen/margin_large">
android:layout_weight="1"
android:adjustViewBounds="true"
android:paddingBottom="@dimen/margin_activity_vertical"
android:paddingEnd="@dimen/margin_activity_horizontal"
android:paddingLeft="@dimen/margin_activity_horizontal"
android:paddingRight="@dimen/margin_activity_horizontal"
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical"
android:scaleType="fitCenter"
android:src="@drawable/qr_code_intro"/>
<ScrollView <ImageView
android:id="@+id/scrollView" android:id="@+id/diagram"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:layout_weight="1"> android:layout_weight="1"
android:adjustViewBounds="true"
android:padding="@dimen/margin_medium"
android:scaleType="fitCenter"
android:src="@drawable/qr_code_intro"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/explanationText"
app:layout_constraintBottom_toBottomOf="@id/explanationText"/>
<LinearLayout <ImageView
android:id="@+id/explanationImage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:adjustViewBounds="true"
android:paddingTop="@dimen/margin_large"
android:paddingLeft="@dimen/margin_large"
android:paddingRight="@dimen/margin_large"
android:scaleType="fitCenter"
android:src="@drawable/qr_code_explanation"
tools:ignore="ContentDescription"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/diagram"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/explanationText"/>
<TextView
android:id="@+id/explanationText"
style="@style/BriarTextBody"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/margin_large"
android:text="@string/face_to_face"
app:layout_constraintTop_toBottomOf="@id/explanationImage"
app:layout_constraintStart_toEndOf="@id/diagram"
app:layout_constraintEnd_toEndOf="parent"/>
<View
android:id="@+id/explanationBorder"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/border_explanation"
app:layout_constraintTop_toTopOf="@id/explanationImage"
app:layout_constraintStart_toStartOf="@id/explanationImage"
app:layout_constraintEnd_toEndOf="@id/explanationImage"
app:layout_constraintBottom_toBottomOf="@id/explanationText"/>
<android.support.constraint.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="diagram,explanationBorder"/>
<Button
android:id="@+id/continueButton"
style="@style/BriarButton.Default"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:layout_gravity="center_horizontal"
android:paddingBottom="@dimen/margin_activity_vertical" android:layout_marginTop="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_activity_horizontal" android:text="@string/continue_button"
android:paddingLeft="@dimen/margin_activity_horizontal" app:layout_constraintTop_toBottomOf="@id/barrier"
android:paddingRight="@dimen/margin_activity_horizontal" app:layout_constraintBottom_toBottomOf="parent"/>
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<LinearLayout </android.support.constraint.ConstraintLayout>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/border_explanation"
android:orientation="vertical"
android:padding="@dimen/margin_large">
<ImageView </ScrollView>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:padding="@dimen/margin_medium"
android:src="@drawable/qr_code_explanation"
tools:ignore="ContentDescription"/>
<TextView
style="@style/BriarTextBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:text="@string/face_to_face"/>
</LinearLayout>
<Button
android:id="@+id/continueButton"
style="@style/BriarButton.Default"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/margin_medium"
android:text="@string/continue_button"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,96 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<org.briarproject.briar.android.keyagreement.CameraView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:id="@+id/camera_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout
android:id="@+id/status_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/background_light"
android:gravity="center"
android:orientation="vertical"
android:padding="@dimen/margin_medium"
android:visibility="invisible">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/connect_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="Connection failed"/>
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/white">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<ImageView
android:id="@+id/qr_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:layout_gravity="center"/>
</FrameLayout>
</LinearLayout>
<RelativeLayout
android:id="@+id/container_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:visibility="invisible">
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/title_progress_bar"
android:layout_centerHorizontal="true"/>
<TextView
android:id="@+id/title_progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:paddingTop="@dimen/margin_large"
tools:text="@string/waiting_for_contact_to_scan"/>
</RelativeLayout>
</FrameLayout>

View File

@@ -2,56 +2,62 @@
<ScrollView <ScrollView
android:id="@+id/scrollView" android:id="@+id/scrollView"
xmlns:android="http://schemas.android.com/apk/res/android" 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout <android.support.constraint.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:padding="@dimen/margin_large">
android:paddingBottom="@dimen/margin_activity_vertical"
android:paddingEnd="@dimen/margin_activity_horizontal"
android:paddingLeft="@dimen/margin_activity_horizontal"
android:paddingRight="@dimen/margin_activity_horizontal"
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<ImageView <ImageView
android:id="@+id/imageView" android:id="@+id/diagram"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:paddingBottom="@dimen/margin_large"
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/qr_code_intro" android:src="@drawable/qr_code_intro"
tools:ignore="ContentDescription"/> app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/explanationImage"/>
<LinearLayout <ImageView
android:id="@+id/explanationImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:paddingTop="@dimen/margin_large"
android:paddingLeft="@dimen/margin_large"
android:paddingRight="@dimen/margin_large"
android:scaleType="fitCenter"
android:src="@drawable/qr_code_explanation"
tools:ignore="ContentDescription"
app:layout_constraintTop_toBottomOf="@id/diagram"
app:layout_constraintBottom_toTopOf="@id/explanationText"/>
<TextView
android:id="@+id/explanationText"
style="@style/BriarTextBody"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small" android:padding="@dimen/margin_large"
android:layout_marginRight="@dimen/margin_small" android:text="@string/face_to_face"
android:layout_marginTop="@dimen/margin_xlarge" app:layout_constraintTop_toBottomOf="@id/explanationImage"
app:layout_constraintBottom_toTopOf="@id/continueButton"/>
<View
android:id="@+id/explanationBorder"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/border_explanation" android:background="@drawable/border_explanation"
android:orientation="vertical" app:layout_constraintTop_toTopOf="@id/explanationImage"
android:padding="@dimen/margin_large"> app:layout_constraintStart_toStartOf="@id/explanationImage"
app:layout_constraintEnd_toEndOf="@id/explanationImage"
<ImageView app:layout_constraintBottom_toBottomOf="@id/explanationText"/>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:padding="@dimen/margin_medium"
android:src="@drawable/qr_code_explanation"
android:contentDescription="@string/face_to_face"/>
<TextView
style="@style/BriarTextBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:text="@string/face_to_face"/>
</LinearLayout>
<Button <Button
android:id="@+id/continueButton" android:id="@+id/continueButton"
@@ -60,8 +66,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginTop="@dimen/margin_medium" android:layout_marginTop="@dimen/margin_medium"
android:text="@string/continue_button"/> android:text="@string/continue_button"
app:layout_constraintTop_toBottomOf="@id/explanationText"
app:layout_constraintBottom_toBottomOf="parent"/>
</LinearLayout> </android.support.constraint.ConstraintLayout>
</ScrollView> </ScrollView>