diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
index 50b5416877ea13f06630cd11cb5e8a586be8ab00..1d92a31f30348af7b9bf73f3d25e6cfd1469af7e 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java
@@ -29,7 +29,7 @@ import org.briarproject.briar.android.introduction.IntroductionMessageFragment;
 import org.briarproject.briar.android.keyagreement.ContactExchangeActivity;
 import org.briarproject.briar.android.keyagreement.IntroFragment;
 import org.briarproject.briar.android.keyagreement.KeyAgreementActivity;
-import org.briarproject.briar.android.keyagreement.ShowQrCodeFragment;
+import org.briarproject.briar.android.keyagreement.KeyAgreementFragment;
 import org.briarproject.briar.android.login.AuthorNameFragment;
 import org.briarproject.briar.android.login.ChangePasswordActivity;
 import org.briarproject.briar.android.login.DozeFragment;
@@ -188,7 +188,7 @@ public interface ActivityComponent {
 
 	void inject(IntroFragment fragment);
 
-	void inject(ShowQrCodeFragment fragment);
+	void inject(KeyAgreementFragment fragment);
 
 	void inject(ContactChooserFragment fragment);
 
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 aa6ba31ac18a06883559b3ad94e8be906b62bb29..d9cf8d01f7fd8d03000f69b7a0be8bfb661bad4e 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
@@ -9,10 +9,12 @@ import android.hardware.Camera.Size;
 import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.util.AttributeSet;
+import android.view.Display;
 import android.view.Surface;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
+import android.view.WindowManager;
 
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
@@ -21,6 +23,7 @@ import java.io.IOException;
 import java.util.List;
 import java.util.logging.Logger;
 
+import static android.content.Context.WINDOW_SERVICE;
 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;
@@ -97,7 +100,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
 	}
 
 	@UiThread
-	public void start(int rotationDegrees) throws CameraException {
+	public void start() throws CameraException {
 		LOG.info("Opening camera");
 		try {
 			int cameras = Camera.getNumberOfCameras();
@@ -122,7 +125,7 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
 		} catch (RuntimeException e) {
 			throw new CameraException(e);
 		}
-		setDisplayOrientation(rotationDegrees);
+		setDisplayOrientation(getScreenRotationDegrees());
 		// Use barcode scene mode if it's available
 		Parameters params = camera.getParameters();
 		params = setSceneMode(camera, params);
@@ -157,6 +160,27 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
 		camera = null;
 	}
 
+	/**
+	 * See {@link Camera#setDisplayOrientation(int)}.
+	 */
+	private int getScreenRotationDegrees() {
+		WindowManager wm =
+				(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
+		Display d = wm.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();
+		}
+	}
+
 	@UiThread
 	private void startPreview(SurfaceHolder holder) throws CameraException {
 		LOG.info("Starting preview");
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java
index 9ec105ac2b1aaafcf03b0175a169154affd7497b..0a25f92377e78ab52329fe4b1bb5dc7212b19d07 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java
@@ -193,8 +193,8 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		continueClicked = false;
 		// FIXME #824
 		FragmentManager fm = getSupportFragmentManager();
-		if (fm.findFragmentByTag(ShowQrCodeFragment.TAG) == null) {
-			BaseFragment f = ShowQrCodeFragment.newInstance();
+		if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) {
+			BaseFragment f = KeyAgreementFragment.newInstance();
 			fm.beginTransaction()
 					.replace(R.id.fragmentContainer, f, f.getUniqueTag())
 					.addToBackStack(f.getUniqueTag())
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/KeyAgreementFragment.java
similarity index 71%
rename from briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java
rename to briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementFragment.java
index 2c38043fbbb5f3de96d97955855ac3e4d6c14e91..4c4623d09d9629edfb1dfde5c5468a300212df9e 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/KeyAgreementFragment.java
@@ -2,18 +2,12 @@ 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;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.LinearLayout.LayoutParams;
 import android.widget.TextView;
@@ -41,6 +35,7 @@ import org.briarproject.briar.R;
 import org.briarproject.briar.android.activity.ActivityComponent;
 import org.briarproject.briar.android.fragment.BaseEventFragment;
 import org.briarproject.briar.android.fragment.ErrorFragment;
+import org.briarproject.briar.android.view.QrCodeView;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -63,10 +58,10 @@ import static org.briarproject.bramble.util.LogUtils.logException;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
-public class ShowQrCodeFragment extends BaseEventFragment
-		implements QrCodeDecoder.ResultCallback {
+public class KeyAgreementFragment extends BaseEventFragment
+		implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener {
 
-	static final String TAG = ShowQrCodeFragment.class.getName();
+	static final String TAG = KeyAgreementFragment.class.getName();
 
 	private static final Logger LOG = Logger.getLogger(TAG);
 	private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
@@ -84,21 +79,18 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	EventBus eventBus;
 
 	private CameraView cameraView;
+	private LinearLayout cameraOverlay;
 	private View statusView;
+	private QrCodeView qrCodeView;
 	private TextView status;
-	private View qrCodeContainer;
-	private ImageView qrCode;
-	private boolean fullscreen = false;
 
 	private boolean gotRemotePayload;
 	private volatile boolean gotLocalPayload;
 	private KeyAgreementTask task;
 
-	public static ShowQrCodeFragment newInstance() {
-
+	public static KeyAgreementFragment newInstance() {
 		Bundle args = new Bundle();
-
-		ShowQrCodeFragment fragment = new ShowQrCodeFragment();
+		KeyAgreementFragment fragment = new KeyAgreementFragment();
 		fragment.setArguments(args);
 		return fragment;
 	}
@@ -118,7 +110,6 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	public View onCreateView(LayoutInflater inflater,
 			@Nullable ViewGroup container,
 			@Nullable Bundle savedInstanceState) {
-
 		return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
 				false);
 	}
@@ -126,45 +117,17 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	@Override
 	public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
 		super.onViewCreated(view, savedInstanceState);
-
 		cameraView = view.findViewById(R.id.camera_view);
+		cameraOverlay = view.findViewById(R.id.camera_overlay);
 		statusView = view.findViewById(R.id.status_container);
 		status = view.findViewById(R.id.connect_status);
-		qrCodeContainer = view.findViewById(R.id.qr_code_container);
-		qrCode = view.findViewById(R.id.qr_code);
-		ImageView fullscreenButton = view.findViewById(R.id.fullscreen_button);
-		fullscreenButton.setOnClickListener(v -> {
-			LinearLayout cameraOverlay = view.findViewById(R.id.camera_overlay);
-			LayoutParams statusParams, qrCodeParams;
-			if (fullscreen) {
-				// Shrink the QR code container to fill half its parent
-				if (cameraOverlay.getOrientation() == HORIZONTAL) {
-					statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
-					qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
-				} else {
-					statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
-					qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
-				}
-				fullscreenButton.setImageResource(
-						R.drawable.ic_fullscreen_black_48dp);
-			} else {
-				// Grow the QR code container to fill its parent
-				statusParams = new LayoutParams(0, 0, 0f);
-				qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
-				fullscreenButton.setImageResource(
-						R.drawable.ic_fullscreen_exit_black_48dp);
-			}
-			statusView.setLayoutParams(statusParams);
-			qrCodeContainer.setLayoutParams(qrCodeParams);
-			cameraOverlay.invalidate();
-			fullscreen = !fullscreen;
-		});
+		qrCodeView = view.findViewById(R.id.qr_code_view);
+		qrCodeView.setFullscreenListener(this);
 	}
 
 	@Override
 	public void onActivityCreated(@Nullable Bundle savedInstanceState) {
 		super.onActivityCreated(savedInstanceState);
-
 		getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
 		cameraView.setPreviewConsumer(new QrCodeDecoder(this));
 	}
@@ -173,30 +136,33 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	public void onStart() {
 		super.onStart();
 		try {
-			cameraView.start(getScreenRotationDegrees());
+			cameraView.start();
 		} 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 toggleFullscreen(boolean fullscreen) {
+		LinearLayout.LayoutParams statusParams, qrCodeParams;
+		if (fullscreen) {
+			// Grow the QR code view to fill its parent
+			statusParams = new LayoutParams(0, 0, 0f);
+			qrCodeParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT, 1f);
+		} else {
+			// Shrink the QR code view to fill half its parent
+			if (cameraOverlay.getOrientation() == HORIZONTAL) {
+				statusParams = new LayoutParams(0, MATCH_PARENT, 1f);
+				qrCodeParams = new LayoutParams(0, MATCH_PARENT, 1f);
+			} else {
+				statusParams = new LayoutParams(MATCH_PARENT, 0, 1f);
+				qrCodeParams = new LayoutParams(MATCH_PARENT, 0, 1f);
+			}
 		}
+		statusView.setLayoutParams(statusParams);
+		qrCodeView.setLayoutParams(qrCodeParams);
+		cameraOverlay.invalidate();
 	}
 
 	@Override
@@ -242,7 +208,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
 		// If we've stopped the camera view, restart it
 		if (gotRemotePayload) {
 			try {
-				cameraView.start(getScreenRotationDegrees());
+				cameraView.start();
 			} catch (CameraException e) {
 				logCameraExceptionAndFinish(e);
 				return;
@@ -299,51 +265,10 @@ public class ShowQrCodeFragment extends BaseEventFragment
 			KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e;
 			keyAgreementAborted(event.didRemoteAbort());
 		} else if (e instanceof KeyAgreementFinishedEvent) {
-			runOnUiThreadUnlessDestroyed(() -> {
-				statusView.setVisibility(VISIBLE);
-				status.setText(R.string.exchanging_contact_details);
-			});
+			keyAgreementFinished();
 		}
 	}
 
-	@UiThread
-	private void generateBitmapQR(Payload payload) {
-		// Get narrowest screen dimension
-		Context context = getContext();
-		if (context == null) return;
-		DisplayMetrics dm = context.getResources().getDisplayMetrics();
-		new AsyncTask<Void, Void, Bitmap>() {
-
-			@Override
-			@Nullable
-			protected Bitmap doInBackground(Void... params) {
-				byte[] payloadBytes = payloadEncoder.encode(payload);
-				if (LOG.isLoggable(INFO)) {
-					LOG.info("Local payload is " + payloadBytes.length
-							+ " bytes");
-				}
-				// Use ISO 8859-1 to encode bytes directly as a string
-				String content = new String(payloadBytes, ISO_8859_1);
-				return QrCodeUtils.createQrCode(dm, content);
-			}
-
-			@Override
-			protected void onPostExecute(@Nullable Bitmap bitmap) {
-				if (bitmap != null && !isDetached()) {
-					qrCode.setImageBitmap(bitmap);
-					// Simple fade-in animation
-					AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
-					anim.setDuration(200);
-					qrCode.startAnimation(anim);
-				}
-			}
-		}.execute();
-	}
-
-	private void setQrCode(Payload localPayload) {
-		runOnUiThreadUnlessDestroyed(() -> generateBitmapQR(localPayload));
-	}
-
 	private void keyAgreementFailed() {
 		runOnUiThreadUnlessDestroyed(() -> {
 			reset();
@@ -353,25 +278,25 @@ public class ShowQrCodeFragment extends BaseEventFragment
 		});
 	}
 
-	private void keyAgreementWaiting() {
-		runOnUiThreadUnlessDestroyed(
-				() -> status.setText(R.string.waiting_for_contact_to_scan));
-	}
-
 	private void keyAgreementStarted() {
 		runOnUiThreadUnlessDestroyed(() -> {
-			qrCodeContainer.setVisibility(INVISIBLE);
+			qrCodeView.setVisibility(INVISIBLE);
 			statusView.setVisibility(VISIBLE);
 			status.setText(R.string.authenticating_with_device);
 		});
 	}
 
+	private void keyAgreementWaiting() {
+		runOnUiThreadUnlessDestroyed(
+				() -> status.setText(R.string.waiting_for_contact_to_scan));
+	}
+
 	private void keyAgreementAborted(boolean remoteAborted) {
 		runOnUiThreadUnlessDestroyed(() -> {
 			reset();
-			qrCodeContainer.setVisibility(VISIBLE);
+			qrCodeView.setVisibility(VISIBLE);
 			statusView.setVisibility(INVISIBLE);
-			status.setText(null);
+			status.setText("");
 			// TODO show abort somewhere persistent?
 			Toast.makeText(getActivity(),
 					remoteAborted ? R.string.connection_aborted_remote :
@@ -380,6 +305,30 @@ public class ShowQrCodeFragment extends BaseEventFragment
 		});
 	}
 
+	private void keyAgreementFinished() {
+		runOnUiThreadUnlessDestroyed(() -> {
+			statusView.setVisibility(VISIBLE);
+			status.setText(R.string.exchanging_contact_details);
+		});
+	}
+
+	private void setQrCode(Payload localPayload) {
+		Context context = getContext();
+		if (context == null) return;
+		DisplayMetrics dm = context.getResources().getDisplayMetrics();
+		ioExecutor.execute(() -> {
+			byte[] payloadBytes = payloadEncoder.encode(localPayload);
+			if (LOG.isLoggable(INFO)) {
+				LOG.info("Local payload is " + payloadBytes.length
+						+ " bytes");
+			}
+			// Use ISO 8859-1 to encode bytes directly as a string
+			String content = new String(payloadBytes, ISO_8859_1);
+			Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
+			runOnUiThreadUnlessDestroyed(() -> qrCodeView.setQrCode(qrCode));
+		});
+	}
+
 	@Override
 	public void handleResult(Result result) {
 		runOnUiThreadUnlessDestroyed(() -> {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/QrCodeView.java b/briar-android/src/main/java/org/briarproject/briar/android/view/QrCodeView.java
new file mode 100644
index 0000000000000000000000000000000000000000..c4649cf4fd5d04138051b2a9f9ffc7b9a16b1f8e
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/view/QrCodeView.java
@@ -0,0 +1,61 @@
+package org.briarproject.briar.android.view;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.animation.AlphaAnimation;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import org.briarproject.briar.R;
+
+public class QrCodeView extends FrameLayout {
+
+	private final ImageView qrCodeImageView;
+	private boolean fullscreen = false;
+	private FullscreenListener listener;
+
+	public QrCodeView(@NonNull Context context,
+			@Nullable AttributeSet attrs) {
+		super(context, attrs);
+		LayoutInflater inflater = (LayoutInflater) context
+				.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+		inflater.inflate(R.layout.qr_code_view, this, true);
+		qrCodeImageView = findViewById(R.id.qr_code);
+		ImageView fullscreenButton = findViewById(R.id.fullscreen_button);
+		fullscreenButton.setOnClickListener(v -> {
+					fullscreen = !fullscreen;
+					if (!fullscreen)
+						fullscreenButton.setImageResource(
+								R.drawable.ic_fullscreen_black_48dp);
+					else
+						fullscreenButton.setImageResource(
+								R.drawable.ic_fullscreen_exit_black_48dp);
+					if (listener != null)
+						listener.toggleFullscreen(fullscreen);
+				}
+		);
+	}
+
+	public void setQrCode(Bitmap qrCode) {
+		qrCodeImageView.setImageBitmap(qrCode);
+		// Simple fade-in animation
+		AlphaAnimation anim = new AlphaAnimation(0.0f, 1.0f);
+		anim.setDuration(200);
+		qrCodeImageView.startAnimation(anim);
+	}
+
+	@UiThread
+	public void setFullscreenListener(FullscreenListener listener) {
+		this.listener = listener;
+	}
+
+	public interface FullscreenListener {
+		void toggleFullscreen(boolean isFullscreen);
+	}
+
+}
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
deleted file mode 100644
index 9d378e5ba34f061e29f420e34ccaa376dd59be77..0000000000000000000000000000000000000000
--- a/briar-android/src/main/res/layout-land/fragment_keyagreement_qr.xml
+++ /dev/null
@@ -1,110 +0,0 @@
-<?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:baselineAligned="false">
-
-		<LinearLayout
-			android:id="@+id/status_container"
-			android:layout_width="0dp"
-			android:layout_height="match_parent"
-			android:layout_weight="1"
-			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
-			android:id="@+id/qr_code_container"
-			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"/>
-
-			<RelativeLayout
-				android:layout_width="match_parent"
-				android:layout_height="match_parent">
-
-				<ImageView
-					android:id="@+id/qr_code"
-					android:layout_width="match_parent"
-					android:layout_height="match_parent"
-					android:layout_centerInParent="true"
-					android:contentDescription="@string/qr_code"
-					android:scaleType="fitCenter"/>
-
-				<ImageView
-					android:id="@+id/fullscreen_button"
-					android:background="?selectableItemBackground"
-					android:src="@drawable/ic_fullscreen_black_48dp"
-					android:alpha="0.54"
-					android:layout_width="wrap_content"
-					android:layout_height="wrap_content"
-					android:layout_margin="@dimen/margin_small"
-					android:layout_alignParentBottom="true"
-					android:layout_alignParentRight="true"
-					android:layout_alignParentEnd="true"
-					android:contentDescription="@string/show_qr_code_fullscreen"/>
-			</RelativeLayout>
-		</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_qr.xml b/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml
index d9527df8fd386e966b5454398b6630ab4ff7f25f..f992ee2035b4fe1f8ce0d109b7f013e45996e0c2 100644
--- a/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml
+++ b/briar-android/src/main/res/layout/fragment_keyagreement_qr.xml
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <FrameLayout
 	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">
@@ -43,45 +42,11 @@
 				tools:text="Connection failed"/>
 		</LinearLayout>
 
-		<FrameLayout
-			android:id="@+id/qr_code_container"
+		<org.briarproject.briar.android.view.QrCodeView
+			android:id="@+id/qr_code_view"
 			android:layout_width="match_parent"
 			android:layout_height="0dp"
 			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"/>
-
-			<RelativeLayout
-				android:layout_width="match_parent"
-				android:layout_height="match_parent">
-
-				<ImageView
-					android:id="@+id/qr_code"
-					android:layout_width="match_parent"
-					android:layout_height="match_parent"
-					android:layout_centerInParent="true"
-					android:contentDescription="@string/qr_code"
-					android:scaleType="fitCenter"
-					tools:src="@drawable/startup_lock"/>
-
-				<ImageView
-					android:id="@+id/fullscreen_button"
-					android:layout_width="wrap_content"
-					android:layout_height="wrap_content"
-					android:layout_alignParentBottom="true"
-					android:layout_alignParentEnd="true"
-					android:layout_alignParentRight="true"
-					android:layout_margin="@dimen/margin_small"
-					android:alpha="0.54"
-					android:background="?selectableItemBackground"
-					android:contentDescription="@string/show_qr_code_fullscreen"
-					android:src="@drawable/ic_fullscreen_black_48dp"/>
-			</RelativeLayout>
-		</FrameLayout>
+			android:background="@android:color/white"/>
 	</LinearLayout>
 </FrameLayout>
diff --git a/briar-android/src/main/res/layout/qr_code_view.xml b/briar-android/src/main/res/layout/qr_code_view.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eb690fe976f437cca92a1be1f0175fad8a59b244
--- /dev/null
+++ b/briar-android/src/main/res/layout/qr_code_view.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge
+	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"
+	tools:showIn="@layout/list_item_forum">
+
+	<ProgressBar
+		style="?android:attr/progressBarStyleLarge"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_gravity="center"/>
+
+	<android.support.constraint.ConstraintLayout
+		android:layout_width="match_parent"
+		android:layout_height="match_parent">
+
+		<ImageView
+			android:id="@+id/qr_code"
+			android:layout_width="match_parent"
+			android:layout_height="match_parent"
+			android:contentDescription="@string/qr_code"
+			android:scaleType="fitCenter"
+			app:layout_constraintBottom_toBottomOf="parent"
+			app:layout_constraintLeft_toLeftOf="parent"
+			app:layout_constraintRight_toRightOf="parent"
+			app:layout_constraintTop_toTopOf="parent"
+			tools:src="@drawable/startup_lock"/>
+
+		<ImageView
+			android:id="@+id/fullscreen_button"
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:layout_margin="@dimen/margin_small"
+			android:alpha="0.54"
+			android:background="?selectableItemBackground"
+			android:contentDescription="@string/show_qr_code_fullscreen"
+			android:src="@drawable/ic_fullscreen_black_48dp"
+			app:layout_constraintBottom_toBottomOf="parent"
+			app:layout_constraintEnd_toEndOf="parent"
+			app:layout_constraintRight_toRightOf="parent"/>
+	</android.support.constraint.ConstraintLayout>
+</merge>