diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java index 3b0003fa82b55eb1c3ac252d442b349c8b8c167d..814bfe8b4a5594132cea1007db3bff04383dc4db 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/CameraView.java @@ -6,7 +6,6 @@ import android.hardware.Camera.AutoFocusCallback; import android.hardware.Camera.CameraInfo; import android.hardware.Camera.Parameters; import android.hardware.Camera.Size; -import android.os.Build; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.util.AttributeSet; @@ -22,6 +21,7 @@ import java.io.IOException; import java.util.List; 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.Parameters.FLASH_MODE_OFF; 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.SCENE_MODE_AUTO; 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.WARNING; @@ -41,7 +42,12 @@ import static java.util.logging.Level.WARNING; public class CameraView extends SurfaceView implements SurfaceHolder.Callback, 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 Logger LOG = Logger.getLogger(CameraView.class.getName()); @@ -49,6 +55,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, @Nullable private Camera camera = null; + private int cameraIndex = 0; private PreviewConsumer previewConsumer = null; private Surface surface = null; private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0; @@ -89,15 +96,32 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, } @UiThread - public void start() throws CameraException { + public void start(int rotationDegrees) throws CameraException { LOG.info("Opening camera"); 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) { throw new CameraException(e); } - if (camera == null) throw new CameraException("No back-facing camera"); - setDisplayOrientation(0); + setDisplayOrientation(rotationDegrees); // Use barcode scene mode if it's available Parameters params = camera.getParameters(); params = setSceneMode(camera, params); @@ -163,7 +187,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, private void startConsumer() throws CameraException { if (camera == null) throw new CameraException("Camera is null"); startAutoFocus(); - previewConsumer.start(camera); + previewConsumer.start(camera, cameraIndex); } @UiThread @@ -199,13 +223,17 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, } } + /** + * See {@link Camera#setDisplayOrientation(int)}. + */ @UiThread private void setDisplayOrientation(int rotationDegrees) throws CameraException { + if (camera == null) throw new CameraException("Camera is null"); int orientation; CameraInfo info = new CameraInfo(); try { - Camera.getCameraInfo(0, info); + Camera.getCameraInfo(cameraIndex, info); } catch (RuntimeException e) { throw new CameraException(e); } @@ -215,9 +243,11 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, } else { orientation = (info.orientation - rotationDegrees + 360) % 360; } - if (LOG.isLoggable(INFO)) - LOG.info("Display orientation " + orientation + " degrees"); - if (camera == null) throw new CameraException("Camera is null"); + if (LOG.isLoggable(INFO)) { + LOG.info("Screen rotation " + rotationDegrees + + " degrees, camera orientation " + orientation + + " degrees"); + } try { camera.setDisplayOrientation(orientation); } catch (RuntimeException e) { @@ -285,8 +315,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, @UiThread private void setVideoStabilisation(Parameters params) { - if (Build.VERSION.SDK_INT >= 15 && - params.isVideoStabilizationSupported()) { + if (SDK_INT >= 15 && params.isVideoStabilizationSupported()) { params.setVideoStabilization(true); } } @@ -313,6 +342,8 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, @UiThread private void setPreviewSize(Parameters params) { 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; boolean rotatePreview = displayOrientation % 180 == 90; List<Size> sizes = params.getSupportedPreviewSizes(); @@ -323,11 +354,12 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, int height = rotatePreview ? size.width : size.height; float ratio = (float) width / height; float stretch = Math.max(ratio / idealRatio, idealRatio / ratio); - int pixels = width * height; - float score = pixels / stretch; + float pixels = width * height; + float zoom = Math.max(pixels / IDEAL_PIXELS, IDEAL_PIXELS / pixels); + float score = 1 / (stretch * zoom); if (LOG.isLoggable(INFO)) { LOG.info("Size " + size.width + "x" + size.height - + ", stretch " + stretch + ", pixels " + pixels + + ", stretch " + stretch + ", zoom " + zoom + ", score " + score); } if (bestSize == null || score > bestScore) { @@ -358,7 +390,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, } catch (RuntimeException e) { throw new CameraException(e); } - if (Build.VERSION.SDK_INT >= 15) { + if (SDK_INT >= 15) { LOG.info("Video stabilisation enabled: " + params.getVideoStabilization()); } @@ -389,8 +421,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, surface.release(); } surface = holder.getSurface(); - // Start the preview when the camera and the surface are both ready - if (camera != null && !previewStarted) startPreview(holder); + // We'll start the preview when surfaceChanged() is called } @Override @@ -416,7 +447,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback, surfaceWidth = w; surfaceHeight = h; if (camera == null) return; // We are stopped - stopPreview(); + if (previewStarted) stopPreview(); try { Parameters params = camera.getParameters(); setPreviewSize(params); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java index 9e5d7a5e9239d1c402e93210960f8f59de09ebec..11735caebbc78716f206df22750644943a490484 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/PreviewConsumer.java @@ -10,7 +10,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; interface PreviewConsumer { @UiThread - void start(Camera camera); + void start(Camera camera, int cameraIndex); @UiThread void stop(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java index 17a14f34782b336229ce093d3489aca0179706ba..e191c640500a6af4afec18aed69a04a1eaaeb85f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeDecoder.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android.keyagreement; import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; import android.hardware.Camera.PreviewCallback; import android.hardware.Camera.Size; import android.os.AsyncTask; @@ -38,20 +39,23 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback { private final ResultCallback callback; private Camera camera = null; + private int cameraIndex = 0; QrCodeDecoder(ResultCallback callback) { this.callback = callback; } @Override - public void start(Camera camera) { + public void start(Camera camera, int cameraIndex) { this.camera = camera; + this.cameraIndex = cameraIndex; askForPreviewFrame(); } @Override public void stop() { camera = null; + cameraIndex = 0; } @UiThread @@ -63,46 +67,65 @@ class QrCodeDecoder implements PreviewConsumer, PreviewCallback { @Override public void onPreviewFrame(byte[] data, Camera camera) { if (camera == this.camera) { + LOG.info("Got preview frame"); try { 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) { 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 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.width = width; this.height = height; + this.orientation = orientation; } @Override protected Void doInBackground(Void... params) { long now = System.currentTimeMillis(); - LuminanceSource src = new PlanarYUVLuminanceSource(data, width, - height, 0, 0, width, height, false); - BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src)); + BinaryBitmap bitmap = binarize(data, width, height, orientation); Result result; try { result = reader.decode(bitmap, singletonMap(CHARACTER_SET, "ISO8859_1")); + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Decoding barcode took " + duration + " ms"); } 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) { - return null; // Preview data did not match width and height + LOG.warning("Invalid preview frame"); + return null; } finally { reader.reset(); } - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Decoding barcode took " + duration + " ms"); callback.handleResult(result); return null; } @@ -113,6 +136,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 interface ResultCallback { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java index a4099c298dd843a920c872559b462a63b58a8541..00f1a51fa0875336605d09420bd4ec07ccf4e1e9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/QrCodeUtils.java @@ -32,21 +32,24 @@ class QrCodeUtils { // Generate QR code BitMatrix encoded = new QRCodeWriter().encode(input, QR_CODE, smallestDimen, smallestDimen); - // Convert QR code to Bitmap - 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; + return renderQrCode(encoded); } catch (WriterException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); 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; + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java index 24f0fb1be24859668161b8cb23ea22f98113399b..b89f630d105a40418fad4ee9f6e43a3ddcb047e5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java @@ -2,11 +2,14 @@ package org.briarproject.briar.android.keyagreement; import android.content.Context; import android.graphics.Bitmap; +import android.hardware.Camera; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.UiThread; import android.util.DisplayMetrics; +import android.view.Display; import android.view.LayoutInflater; +import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; @@ -138,13 +141,32 @@ public class ShowQrCodeFragment extends BaseEventFragment public void onStart() { super.onStart(); try { - cameraView.start(); + cameraView.start(getScreenRotationDegrees()); } catch (CameraException e) { logCameraExceptionAndFinish(e); } startListening(); } + /** + * 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 public void onStop() { super.onStop(); @@ -200,6 +222,7 @@ public class ShowQrCodeFragment extends BaseEventFragment LOG.info("Remote payload is " + payloadBytes.length + " bytes"); Payload remotePayload = payloadParser.parse(payloadBytes); gotRemotePayload = true; + cameraView.stop(); cameraView.setVisibility(INVISIBLE); statusView.setVisibility(VISIBLE); status.setText(R.string.connecting_to_device); @@ -209,6 +232,8 @@ public class ShowQrCodeFragment extends BaseEventFragment String msg = getString(R.string.qr_code_unsupported, getString(R.string.app_name)); showNextFragment(ErrorFragment.newInstance(msg)); + } catch (CameraException e) { + logCameraExceptionAndFinish(e); } catch (IOException | IllegalArgumentException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, "QR Code Invalid", e); reset(); diff --git a/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml b/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml index 155721bb4aa1f0fc0e73f47c13f3fb948c176abd..2c98d297489b9d371b64a9276657a3b419604916 100644 --- a/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml +++ b/briar-android/src/main/res/layout-land/fragment_keyagreement_id.xml @@ -1,79 +1,89 @@ <?xml version="1.0" encoding="utf-8"?> -<LinearLayout +<ScrollView + android:id="@+id/scrollView" 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" android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="horizontal"> + android:layout_height="wrap_content" + android:orientation="vertical"> - <ImageView - android:id="@+id/imageView" - android:layout_width="0dp" + <android.support.constraint.ConstraintLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - 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"/> + android:padding="@dimen/margin_large"> - <ScrollView - android:id="@+id/scrollView" - android:layout_width="0dp" - android:layout_height="match_parent" - android:layout_weight="1"> - - <LinearLayout - android:layout_width="match_parent" + <ImageView + android:id="@+id/diagram" + android:layout_width="0dp" android:layout_height="wrap_content" - android:orientation="vertical" - 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: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 - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@drawable/border_explanation" - android:orientation="vertical" - android:padding="@dimen/margin_large"> - - <ImageView - 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"/> + <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 - 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"/> + <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"/> - </LinearLayout> + <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"/> - <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"/> + <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"/> - </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" + app:layout_constraintTop_toBottomOf="@id/barrier" + app:layout_constraintBottom_toBottomOf="parent"/> - </ScrollView> + </android.support.constraint.ConstraintLayout> -</LinearLayout> \ No newline at end of file +</ScrollView> \ No newline at end of file diff --git a/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml b/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml new file mode 100644 index 0000000000000000000000000000000000000000..e9a174c77812fdc41074268acede649e1c3a08e8 --- /dev/null +++ b/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml @@ -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> diff --git a/briar-android/src/main/res/layout/fragment_keyagreement_id.xml b/briar-android/src/main/res/layout/fragment_keyagreement_id.xml index 61041ef2f49d55f846fdc1ead7acb4b835a00801..dc584118261da93611c8f53471cdeb12695706bd 100644 --- a/briar-android/src/main/res/layout/fragment_keyagreement_id.xml +++ b/briar-android/src/main/res/layout/fragment_keyagreement_id.xml @@ -2,56 +2,62 @@ <ScrollView android:id="@+id/scrollView" 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" 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_height="wrap_content" - android:orientation="vertical" - 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:padding="@dimen/margin_large"> <ImageView - android:id="@+id/imageView" - android:layout_width="match_parent" + android:id="@+id/diagram" + android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_gravity="center_vertical" android:adjustViewBounds="true" + android:paddingBottom="@dimen/margin_large" android:scaleType="fitCenter" android:src="@drawable/qr_code_intro" - tools:ignore="ContentDescription"/> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/explanationImage"/> - <LinearLayout - android:layout_width="match_parent" + <ImageView + android:id="@+id/explanationImage" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/margin_small" - android:layout_marginRight="@dimen/margin_small" - android:layout_marginTop="@dimen/margin_xlarge" - android:background="@drawable/border_explanation" - android:orientation="vertical" - android:padding="@dimen/margin_large"> - - <ImageView - 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"/> + 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 - 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"/> + <TextView + android:id="@+id/explanationText" + style="@style/BriarTextBody" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/margin_large" + android:text="@string/face_to_face" + app:layout_constraintTop_toBottomOf="@id/explanationImage" + app:layout_constraintBottom_toTopOf="@id/continueButton"/> - </LinearLayout> + <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"/> <Button android:id="@+id/continueButton" @@ -60,8 +66,10 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" 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> \ No newline at end of file