diff --git a/mailbox-android/artwork/single_scan_left.svg b/mailbox-android/artwork/single_scan_left.svg
new file mode 100644
index 0000000000000000000000000000000000000000..67a41171dcc275a1c24b44c28a611e84d5ee6e04
--- /dev/null
+++ b/mailbox-android/artwork/single_scan_left.svg
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   id="svg947"
+   version="1.1"
+   viewBox="0 0 45.925076 33.828953"
+   height="33.828953mm"
+   width="45.925076mm">
+  <defs
+     id="defs941" />
+  <metadata
+     id="metadata944">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-64.727926,-18.525999)"
+     id="layer1">
+    <path
+       id="path4201"
+       d="m 105.13157,40.687883 -0.60567,-0.60567 -1.00006,-0.33804 c -0.54933,-0.18311 -1.22542,-0.42257 -1.50713,-0.52116 l -0.52116,-0.18311 0.49299,-0.0282 c 1.155,-0.0563 1.83109,-0.56341 2.0142,-1.5353 0.11269,-0.5775 0.15494,-2.43676 0.11269,-4.64816 -0.0282,-1.14091 -0.0282,-2.16914 0,-2.29591 0.0141,-0.12677 0.0704,-0.33805 0.12676,-0.4789 0.16903,-0.49299 0.0423,-1.67616 -0.26762,-2.47902 -0.0423,-0.126771 -0.26762,-0.59158 -0.49298,-1.042311 -0.59159,-1.155 -0.63384,-1.2536 -0.69018,-1.47896 -0.0704,-0.253539 -0.0282,-0.760609 0.0704,-0.957809 0.0986,-0.183101 0.30988,-0.408471 0.45073,-0.492981 0.18311,-0.0986 0.36622,0.0141 0.66201,0.40847 0.4789,0.633841 1.97194,2.732561 2.2114,3.12694 0.52115,0.84513 0.84511,1.577561 1.12682,2.648051 0.0986,0.35213 0.26762,0.98597 0.3803,1.42262 0.11269,0.43664 0.38031,1.4367 0.57751,2.22548 l 0.36621,1.4367 0.64793,0.73244 c 0.36621,0.40847 0.81695,0.91555 1.01414,1.12683 0.1972,0.22536 0.35214,0.42255 0.35214,0.45073 0,0.0422 -4.85945,4.127 -4.91578,4.127 0.0141,-0.0141 -0.26763,-0.29579 -0.60567,-0.61976 z m -13.226128,-2.30999 c -0.183108,-0.0845 -0.309878,-0.1972 -0.408475,-0.32396 -0.29579,-0.38031 -0.281707,0.33804 -0.26762,-9.64846 l 0.01408,-9.014611 0.0986,-0.16902 c 0.140856,-0.26762 0.281707,-0.40848 0.521155,-0.54933 l 0.225367,-0.12677 5.324258,-0.0141 c 5.986263,-0.0141 5.549623,-0.0282 5.929923,0.30988 0.12677,0.11268 0.25354,0.2817 0.30988,0.40847 0.0986,0.22537 0.0986,0.22537 0.11268,2.00012 l 0.0141,1.77475 -0.25354,-0.0141 c -0.19719,-0.0141 -0.29579,0 -0.45073,0.0704 -0.2817,0.140851 -0.54932,0.408471 -0.71835,0.718351 l -0.14086,0.28171 v -1.802931 -1.80292 h -4.732645 -4.732671 v 7.22578 7.225771 h 4.732671 4.732675 l 0.0141,-4.84535 c 0.0141,-4.648161 0.0141,-4.845361 0.0845,-4.63408 0.0422,0.11269 0.25353,0.563419 0.4789,0.98598 0.77469,1.49304 0.76061,1.39444 0.76061,6.64827 0,3.88755 -0.0141,4.2256 -0.23945,4.66225 -0.15494,0.30987 -0.38031,0.52115 -0.71836,0.66201 l -0.23945,0.0986 -5.084803,-0.0141 -5.098891,0.0141 -0.267621,-0.12676 z m 5.97218,-0.71835 c 0.408474,-0.18311 0.633841,-0.52116 0.619755,-0.92963 0,-0.5775 -0.436645,-1.01415 -1.000059,-1.01415 -0.295791,0 -0.507071,0.0845 -0.732436,0.30988 -0.309877,0.30987 -0.39439,0.76061 -0.18311,1.16908 0.0986,0.19719 0.352134,0.42256 0.563412,0.49299 0.21128,0.0704 0.549328,0.0704 0.732438,-0.0282 z"
+       style="stroke-width:0.1408533" />
+    <g
+       id="g12"
+       transform="matrix(0.14085331,0,0,0.14085331,59.664122,-30.386687)">
+      <path
+         d="m 256.5,410.2 h 2.3 v 2.3 h 2.3 v 2.3 h -11.5 v -2.3 h 2.3 v -4.6 h 4.6 z m 30,20.8 h 2.3 v -2.3 h -2.3 z m -27.7,-20.8 h 2.3 v -2.3 h -2.3 z m 32.3,20.8 h 2.3 v -2.3 h -2.3 z m -27.7,2.3 H 268 V 431 h -4.6 z m 20.8,0 V 431 h -2.3 v 2.3 z m -9.2,0 h 2.3 v -4.6 H 275 Z m -18.5,-30 v 2.3 h 6.9 v -2.3 z m -2.3,2.3 v -2.3 h -4.6 v 4.6 h 2.3 v -2.3 z m 6.9,-4.6 H 245 v -16.1 h 16.1 z m -2.3,-13.8 h -11.5 v 11.5 h 11.5 z m -9.2,41.5 h 6.9 v -6.9 h -6.9 z m 23,-2.4 v 2.3 h 2.3 v -2.3 z m -16.1,-36.8 h -6.9 v 6.9 h 6.9 z m 36.9,-4.6 V 401 h -16.1 v -16.1 z m -2.3,2.3 h -11.5 v 11.5 h 11.5 z M 245,417.1 h 16.1 v 16.1 H 245 Z m 2.3,13.9 h 11.5 v -11.5 h -11.5 z m 0,-27.7 H 245 v 11.5 h 2.3 z m 30,11.5 v 2.3 h 2.3 v -2.3 z m -7,9.3 v -2.3 H 268 v 2.3 h -4.6 v 4.6 h 4.6 v 2.3 h 2.3 v -4.6 h 2.3 v -2.3 z m -6.9,-30 h 4.6 v -2.3 h -4.6 z m 18.5,18.4 h 4.6 v 2.3 h 2.3 v -6.9 h -2.3 v -4.6 h -2.3 v 6.9 h -6.9 v 2.3 h 2.3 v 2.3 h 2.3 z m 2.3,6.9 h -2.3 v -2.3 h -2.3 v 4.6 h -6.9 v 2.3 h 4.6 v 4.6 h 2.3 v 2.3 h 2.3 v -4.6 h 9.2 V 424 h -6.9 z m 0,0 h 2.3 v -4.6 h -2.3 z m -18.5,0 v -2.3 h 2.3 v -2.3 h 2.3 v -2.3 h 2.3 v -4.6 h 6.9 v -4.6 h -2.3 v 2.3 H 275 v -9.2 h -2.3 v -4.6 h 2.3 v -6.9 h -2.3 v 4.6 h -2.3 v -4.6 h -6.9 v 4.6 h 2.3 v -2.3 h 2.3 v 4.6 h 2.3 v 6.9 h 2.3 v 2.3 h -2.3 v 4.6 H 268 V 401 h -2.3 v -2.3 h -2.3 v 4.6 h 2.3 v 2.3 h -2.3 v 6.9 h 2.3 v -4.6 h 2.3 v 4.6 h -2.3 v 2.3 h -2.3 v 6.9 h 4.6 v -2.3 z m 25.4,2.3 v -2.3 h -4.6 v 2.3 z m -2.3,-32.2 h -6.9 v 6.9 h 6.9 z m -18.5,29.9 h 4.6 v -2.3 h -2.3 v -2.3 h -2.3 z m 4.7,-4.6 v -2.3 h -2.3 v 2.3 z m 13.8,-9.2 h 4.6 v -2.3 h -4.6 z m 2.3,18.5 h 2.3 v -2.3 h -2.3 z m 0,-13.9 h 2.3 v -2.3 h -2.3 z M 270.3,398.7 H 268 v 2.3 h 2.3 z m 0,0"
+         id="path10" />
+    </g>
+    <path
+       style="fill:#ffffff;stroke-width:0.11463147"
+       d="m 84.684129,41.768943 c 0.332434,-0.14902 0.515845,-0.42414 0.50438,-0.75657 0,-0.46999 -0.355357,-0.82535 -0.813884,-0.82535 -0.240726,0 -0.412673,0.0688 -0.596081,0.25219 -0.252189,0.25219 -0.320969,0.61901 -0.149023,0.95144 0.08024,0.16048 0.286579,0.3439 0.458527,0.40122 0.171946,0.0573 0.44706,0.0573 0.596081,-0.0229 z"
+       id="path4201-3" />
+    <path
+       style="fill:none;fill-opacity:1;stroke:#faf800;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 81.570254,22.330762 11.180312,-1.85531 v 14.451551 h 9.465344 l -2e-5,-14.451551 h -9.465324 z"
+       id="path2320" />
+    <path
+       style="fill:none;stroke:#e3f500;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 92.750566,34.927003 81.570254,22.330762 Z"
+       id="path2322" />
+    <path
+       style="fill:none;stroke:#eaf600;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 81.570254,22.330762 20.645636,-1.85531 z"
+       id="path2324" />
+    <path
+       style="fill:#ffff3f;fill-opacity:1;stroke:#def400;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 81.570254,22.330762 C 102.21591,34.927003 102.21591,34.927003 102.21591,34.927003 Z"
+       id="path2326" />
+    <g
+       id="layer1-36"
+       transform="matrix(0.26458333,0,0,0.26458333,74.179616,-80.938799)">
+      <g
+         id="g12-6"
+         transform="matrix(0.43325279,0,0,0.43325279,91.730541,226.47373)" />
+      <path
+         id="path45"
+         d="M 6.3197128,399.9791 H 53.486093 v 72.01294 H 6.3899028 Z"
+         style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    </g>
+    <path
+       style="stroke-width:0.11463147"
+       d="m 88.553764,28.771453 v -1.467281 h -3.851597 -3.85162 v 5.880591 5.8806 h 3.85162 3.851616 l 0.01146,-3.94332 m 1.253099,6.156561 c -0.126095,0.252189 -0.335965,0.715199 -0.955061,0.61815 l -0.194871,0.0802 -4.138197,-0.0115 -4.149659,0.0115 -0.217803,-0.10317 -2.3e-5,1.1e-5 c -0.149019,-0.0688 -0.252191,-0.160481 -0.332431,-0.263651 -0.240728,-0.3095 -0.229264,0.27512 -0.217799,-7.85225 l 0.01146,-7.336421 0.08024,-0.13756 c 0.114631,-0.2178 0.22926,-0.33243 0.424135,-0.447059 l 0.183413,-0.10317 4.333068,-0.0115 c 4.871836,-0.0115 4.516479,-0.0229 4.825986,0.25219 0.103167,0.0917 0.206333,0.22926 0.252188,0.33243 0.08024,0.18341 0.08024,0.18341 0.0917,1.62777"
+       id="path54" />
+    <path
+       d="m 83.326564,32.932583 h 0.263652 v 0.26365 h 0.263652 v 0.26365 h -1.318263 v -0.26365 h 0.263653 v -0.5273 h 0.527306 z m 3.438944,2.38433 h 0.263652 v -0.26365 h -0.263652 z m -3.175292,-2.38433 h 0.263652 v -0.26365 h -0.263652 z m 3.702596,2.38433 h 0.263653 v -0.26365 h -0.263653 z m -3.175291,0.26366 h 0.527304 v -0.26366 h -0.527304 z m 2.384334,0 v -0.26366 h -0.263652 v 0.26366 z m -1.05461,0 h 0.263652 v -0.52731 h -0.263652 z m -2.120681,-3.43895 v 0.26365 h 0.790957 v -0.26365 z m -0.263654,0.26365 v -0.26365 h -0.527305 v 0.52731 h 0.263653 v -0.26366 z m 0.790958,-0.5273 h -1.845567 v -1.84557 h 1.845567 z m -0.263652,-1.58192 h -1.318263 v 1.31827 h 1.318263 z m -1.054611,4.75721 h 0.790959 v -0.79096 h -0.790959 z m 2.636525,-0.27511 v 0.26365 h 0.263653 v -0.26365 z m -1.845566,-4.21844 h -0.790959 v 0.79095 h 0.790959 z m 4.229901,-0.52731 v 1.84557 h -1.845568 v -1.84557 z m -0.263653,0.26365 h -1.318261 v 1.31827 h 1.318261 z m -5.284511,3.42749 h 1.845567 v 1.84556 h -1.845567 z m 0.263652,1.59337 h 1.318263 v -1.31826 h -1.318263 z m 0,-3.17529 h -0.263652 v 1.31826 h 0.263652 z m 3.438944,1.31826 v 0.26366 h 0.263654 v -0.26366 z m -0.802419,1.06608 v -0.26366 h -0.263653 v 0.26366 h -0.527304 v 0.5273 h 0.527304 v 0.26365 h 0.263653 v -0.5273 h 0.263652 v -0.26365 z m -0.790957,-3.43895 h 0.527304 v -0.26365 h -0.527304 z m 2.120682,2.10922 h 0.527305 v 0.26365 h 0.263652 v -0.79095 h -0.263652 v -0.52731 h -0.263653 v 0.79096 h -0.790958 v 0.26365 h 0.263654 v 0.26365 h 0.263652 z m 0.263652,0.79096 h -0.263652 v -0.26365 h -0.263652 v 0.5273 h -0.790958 v 0.26365 h 0.527304 v 0.52731 h 0.263654 v 0.26365 h 0.263652 v -0.5273 h 1.054609 v -0.26366 h -0.790957 z m 0,0 h 0.263653 v -0.52731 h -0.263653 z m -2.120682,0 v -0.26365 h 0.263652 v -0.26366 h 0.263653 v -0.26365 h 0.263652 v -0.5273 h 0.790957 v -0.52731 h -0.263652 v 0.26365 h -0.25219 v -1.05461 h -0.263652 v -0.5273 h 0.263652 v -0.79096 h -0.263652 v 0.52731 H 84.91994 v -0.52731 h -0.790956 v 0.52731 h 0.263652 v -0.26366 h 0.263652 v 0.52731 h 0.263652 v 0.79096 h 0.263653 v 0.26365 H 84.91994 v 0.5273 h -0.275115 v -0.5273 h -0.263652 v -0.26365 h -0.263652 v 0.5273 h 0.263652 v 0.26365 h -0.263652 v 0.79096 h 0.263652 v -0.5273 h 0.263652 v 0.5273 h -0.263652 v 0.26365 h -0.263652 v 0.79096 h 0.527304 v -0.26365 z m 2.911639,0.26365 v -0.26365 h -0.527304 v 0.26365 z m -0.263652,-3.69113 h -0.790957 v 0.79095 h 0.790957 z m -2.120682,3.42748 h 0.527305 v -0.26365 H 85.17213 v -0.26366 h -0.263652 z m 0.538767,-0.52731 v -0.26365 h -0.263652 v 0.26365 z m 1.581915,-1.05461 h 0.527305 v -0.26365 H 87.02916 Z m 0.263652,2.12069 h 0.263653 v -0.26366 h -0.263653 z m 0,-1.59338 h 0.263653 v -0.26365 h -0.263653 z m -2.384334,-1.31826 h -0.263653 v 0.26365 h 0.263653 z m 0,0"
+       id="path10-7"
+       style="stroke-width:0.11463147" />
+    <path
+       id="path1142"
+       d="m 72.007595,51.537803 0.798536,-0.798539 1.318511,-0.44569 c 0.724254,-0.241419 1.615642,-0.55712 1.987054,-0.68711 l 0.687112,-0.241419 -0.649971,-0.0371 c -1.522789,-0.0743 -2.414177,-0.74282 -2.655595,-2.024192 -0.148564,-0.7614 -0.204276,-3.212719 -0.148564,-6.1283 0.03714,-1.50422 0.03714,-2.85987 0,-3.027 -0.01857,-0.16714 -0.09285,-0.4457 -0.167136,-0.6314 -0.222847,-0.64998 -0.05571,-2.2099 0.352842,-3.26843 0.05571,-0.16713 0.352841,-0.77996 0.64997,-1.37422 0.779965,-1.52279 0.835677,-1.65279 0.909959,-1.94991 0.09285,-0.33427 0.03714,-1.00282 -0.09285,-1.2628 -0.129994,-0.24142 -0.408553,-0.53855 -0.594259,-0.64997 -0.241417,-0.13 -0.482835,0.0186 -0.872817,0.53854 -0.631401,0.83568 -2.599883,3.6027 -2.915584,4.12267 -0.687112,1.11424 -1.114235,2.07991 -1.485647,3.49127 -0.129994,0.46427 -0.352841,1.29995 -0.501406,1.87563 -0.148565,0.57569 -0.501406,1.8942 -0.761394,2.93416 l -0.482836,1.894201 -0.854247,0.965669 c -0.482835,0.53854 -1.077094,1.207081 -1.337083,1.48565 -0.259988,0.278551 -0.464264,0.55711 -0.464264,0.594249 0,0.0557 6.406854,5.441191 6.481137,5.441191 -0.01857,-0.0186 0.371412,-0.389988 0.798535,-0.81711 z"
+       style="stroke-width:0.1857059" />
+    <path
+       id="path1140"
+       d="m 89.44538,48.492224 c 0.241418,-0.111421 0.408553,-0.259979 0.538547,-0.427119 0.389983,-0.501402 0.371412,0.445698 0.352842,-12.720852 l -0.01857,-11.885181 -0.129994,-0.22285 c -0.185706,-0.35284 -0.371411,-0.53854 -0.687111,-0.72425 l -0.29713,-0.16714 -7.019683,-0.0186 c -7.892502,-0.0186 -7.316813,-0.0371 -7.818219,0.40855 -0.167136,0.14857 -0.334271,0.37141 -0.408553,0.53855 -0.129995,0.29713 -0.129995,0.29713 -0.148565,2.63702 l -0.01857,2.339901 0.334271,-0.0186 c 0.259988,-0.0186 0.389982,0 0.594258,0.0929 0.371412,0.1857 0.724253,0.538541 0.9471,0.947101 l 0.185706,0.371409 v -2.377041 -2.37703 h 6.239721 6.239719 v 9.526711 9.52671 H 82.11 75.870281 l -0.01857,-6.38828 c -0.01857,-6.1283 -0.01857,-6.38828 -0.111424,-6.10973 -0.05571,0.14857 -0.33427,0.74283 -0.6314,1.29995 -1.021382,1.96848 -1.002812,1.83848 -1.002812,8.76531 0,5.12549 0.01857,5.571181 0.315701,6.146869 0.204276,0.408551 0.501405,0.687113 0.947099,0.872821 l 0.3157,0.12999 6.722554,0.0186 6.722555,0.0186 0.3157,-0.204279 z"
+       style="fill:#800000;stroke-width:0.1857059" />
+    <path
+       id="path4201-1"
+       d="m 81.571449,47.545124 c -0.538547,-0.241411 -0.835676,-0.687109 -0.817106,-1.22565 0,-0.7614 0.575688,-1.33709 1.318512,-1.33709 0.389983,0 0.668541,0.111429 0.965671,0.40855 0.408554,0.408559 0.519977,1.002819 0.241417,1.54136 -0.129994,0.25999 -0.464263,0.55712 -0.742823,0.64997 -0.278559,0.0929 -0.705683,0.0929 -0.965671,-0.0371 z"
+       style="fill:#ffffff;stroke-width:0.1857059" />
+    <path
+       id="path5402"
+       d="m 92.750563,20.475452 8e-6,14.451471 9.465329,8e-5 -1e-5,-14.451631 z"
+       style="opacity:0.5;fill:#ffff3f;fill-opacity:1;stroke:#000000;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle
index 9484df25e818642306667c69928429b2e71a13f3..14959aada4aae49b5f60bff6ddd82e30f4a1701d 100644
--- a/mailbox-android/build.gradle
+++ b/mailbox-android/build.gradle
@@ -66,6 +66,7 @@ dependencies {
 	androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.0.2"
 	androidTestCompileOnly 'javax.annotation:jsr250-api:1.0'
 	androidTestImplementation 'junit:junit:4.12'
+	implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
 }
 
 def getStdout = { command, defaultValue ->
diff --git a/mailbox-android/src/main/AndroidManifest.xml b/mailbox-android/src/main/AndroidManifest.xml
index f0393787f38624b1e8d862fc5969e1cb5c77a8ef..21284bbb2758b241f49bb986c840163d3fd4e2c4 100644
--- a/mailbox-android/src/main/AndroidManifest.xml
+++ b/mailbox-android/src/main/AndroidManifest.xml
@@ -100,7 +100,7 @@
 		</activity>
 
 		<activity
-			android:name=".android.keyagreement.MailboxExchangeActivity"
+			android:name=".android.keyagreement.KeyAgreementActivity"
 			android:label="@string/pairing_title"
 			android:parentActivityName=".android.navdrawer.NavDrawerActivity"
 			android:theme="@style/BriarTheme.NoActionBar">
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityComponent.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityComponent.java
index a3cb355f346d2ba3e5cbd030161659f8add74f18..852e2320354e447a8d1038acab98ef6e22dfcfe3 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityComponent.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityComponent.java
@@ -7,11 +7,11 @@ import org.briarproject.mailbox.android.StartupFailureActivity;
 import org.briarproject.mailbox.android.fragment.ScreenFilterDialogFragment;
 import org.briarproject.mailbox.android.keyagreement.ContactExchangeErrorFragment;
 import org.briarproject.mailbox.android.keyagreement.KeyAgreementActivity;
-import org.briarproject.mailbox.android.keyagreement.KeyAgreementFragment;
-import org.briarproject.mailbox.android.keyagreement.MailboxExchangeActivity;
-import org.briarproject.mailbox.android.login.IntroFragment;
+import org.briarproject.mailbox.android.keyagreement.ScanQrCodeFragment;
+import org.briarproject.mailbox.android.keyagreement.ShowQrCodeFragment;
 import org.briarproject.mailbox.android.login.ChangePasswordActivity;
 import org.briarproject.mailbox.android.login.DozeFragment;
+import org.briarproject.mailbox.android.login.IntroFragment;
 import org.briarproject.mailbox.android.login.OpenDatabaseActivity;
 import org.briarproject.mailbox.android.login.PasswordActivity;
 import org.briarproject.mailbox.android.login.PasswordFragment;
@@ -51,8 +51,6 @@ public interface ActivityComponent {
 
 	void inject(KeyAgreementActivity activity);
 
-	void inject(MailboxExchangeActivity activity);
-
 	void inject(SettingsActivity activity);
 
 	void inject(ChangePasswordActivity activity);
@@ -71,8 +69,6 @@ public interface ActivityComponent {
 	void inject(
 			org.briarproject.mailbox.android.keyagreement.IntroFragment fragment);
 
-	void inject(KeyAgreementFragment fragment);
-
 	void inject(SettingsFragment fragment);
 
 	void inject(ScreenFilterDialogFragment fragment);
@@ -80,4 +76,8 @@ public interface ActivityComponent {
 	void inject(ContactExchangeErrorFragment fragment);
 
 	void inject(OverviewFragment overviewFragment);
+
+	void inject(ScanQrCodeFragment scanQrCodeFragment);
+
+	void inject(ShowQrCodeFragment showQrCodeFragment);
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityModule.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityModule.java
index 8dde48a5ce724dcae81fb139a94b67c3c7dfa9d7..59451e78541978092f068a915b28aacf996510f4 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityModule.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/activity/ActivityModule.java
@@ -6,6 +6,8 @@ import org.briarproject.mailbox.android.controller.BriarController;
 import org.briarproject.mailbox.android.controller.BriarControllerImpl;
 import org.briarproject.mailbox.android.controller.DbController;
 import org.briarproject.mailbox.android.controller.DbControllerImpl;
+import org.briarproject.mailbox.android.keyagreement.KeyAgreementController;
+import org.briarproject.mailbox.android.keyagreement.KeyAgreementControllerImpl;
 import org.briarproject.mailbox.android.login.PasswordController;
 import org.briarproject.mailbox.android.login.PasswordControllerImpl;
 import org.briarproject.mailbox.android.login.SetupController;
@@ -46,6 +48,13 @@ public class ActivityModule {
 		return setupController;
 	}
 
+	@ActivityScope
+	@Provides
+	KeyAgreementController provideKeyAgreementController(
+			KeyAgreementControllerImpl keyAgreementController) {
+		return keyAgreementController;
+	}
+
 	@ActivityScope
 	@Provides
 	PasswordController providePasswordController(
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementActivity.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementActivity.java
index f0d36a9dc3b9c618684afb07de082f618429f596..cdf35210d02ffa7ef6bbe5cc8bf5f79ea490eefe 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementActivity.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementActivity.java
@@ -17,7 +17,16 @@ import android.support.v7.widget.Toolbar;
 import android.view.MenuItem;
 import android.widget.Toast;
 
+import org.briarproject.bramble.api.contact.ContactExchangeListener;
+import org.briarproject.bramble.api.contact.ContactExchangeTask;
+import org.briarproject.bramble.api.event.Event;
 import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent;
@@ -29,9 +38,9 @@ import org.briarproject.mailbox.android.activity.BriarActivity;
 import org.briarproject.mailbox.android.fragment.BaseFragment;
 import org.briarproject.mailbox.android.fragment.BaseFragment.BaseFragmentListener;
 import org.briarproject.mailbox.android.keyagreement.IntroFragment.IntroScreenSeenListener;
-import org.briarproject.mailbox.android.keyagreement.KeyAgreementFragment.KeyAgreementEventListener;
 import org.briarproject.mailbox.android.util.UiUtils;
 
+import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
@@ -50,25 +59,30 @@ import static org.briarproject.mailbox.android.activity.RequestCodes.REQUEST_PER
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
-public abstract class KeyAgreementActivity extends BriarActivity implements
-		BaseFragmentListener, IntroScreenSeenListener,
-		KeyAgreementEventListener {
-
-	private enum BluetoothState {
-		UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED
-	}
+public class KeyAgreementActivity extends BriarActivity implements
+		BaseFragmentListener, IntroScreenSeenListener, EventListener,
+		KeyAgreementEventListener, ContactExchangeListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(KeyAgreementActivity.class.getName());
-
 	@Inject
 	EventBus eventBus;
+	@Inject
+	@IoExecutor
+	Executor ioExecutor;
+	@Inject
+	volatile ContactExchangeTask contactExchangeTask;
+	@Inject
+	volatile IdentityManager identityManager;
+	@Inject
+	volatile KeyAgreementController keyAgreementController;
 
 	private boolean isResumed = false, enableWasRequested = false;
 	private boolean continueClicked, gotCameraPermission;
 	private BluetoothState bluetoothState = BluetoothState.UNKNOWN;
 	private BroadcastReceiver bluetoothReceiver = null;
 
+
 	@Override
 	public void injectActivity(ActivityComponent component) {
 		component.inject(this);
@@ -78,7 +92,10 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 	@Override
 	public void onCreate(@Nullable Bundle state) {
 		super.onCreate(state);
+		keyAgreementController.onStart(state != null);
 		setContentView(R.layout.activity_fragment_container_toolbar);
+		// Disable screen timeout
+		findViewById(R.id.fragmentContainer).setKeepScreenOn(true);
 		Toolbar toolbar = findViewById(R.id.toolbar);
 		setSupportActionBar(toolbar);
 		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -113,7 +130,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		isResumed = true;
 		// Workaround for
 		// https://code.google.com/p/android/issues/detail?id=190966
-		if (canShowQrCodeFragment()) showQrCodeFragment();
+		if (canShowQrCodeFragment()) showCameraFragment();
 	}
 
 	private boolean canShowQrCodeFragment() {
@@ -123,6 +140,20 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 				&& bluetoothState != BluetoothState.WAITING;
 	}
 
+	@Override
+	public void onStart() {
+		super.onStart();
+		eventBus.addListener(this);
+		//	keyAgreementController.onStart();
+	}
+
+	@Override
+	protected void onStop() {
+		super.onStop();
+		eventBus.removeListener(this);
+//		keyAgreementController.onStop();
+	}
+
 	@Override
 	protected void onPause() {
 		super.onPause();
@@ -134,7 +165,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		continueClicked = true;
 		if (checkPermissions()) {
 			if (shouldRequestEnableBluetooth()) requestEnableBluetooth();
-			else if (canShowQrCodeFragment()) showQrCodeFragment();
+			else if (canShowQrCodeFragment()) showCameraFragment();
 		}
 	}
 
@@ -164,7 +195,7 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 			eventBus.broadcast(new BluetoothEnabledEvent());
 			enableWasRequested = false;
 		}
-		if (canShowQrCodeFragment()) showQrCodeFragment();
+		if (canShowQrCodeFragment()) showCameraFragment();
 	}
 
 	@Override
@@ -174,12 +205,24 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 			setBluetoothState(BluetoothState.REFUSED);
 	}
 
-	private void showQrCodeFragment() {
+	private void showCameraFragment() {
 		continueClicked = false;
 		// FIXME #824
 		FragmentManager fm = getSupportFragmentManager();
-		if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) {
-			BaseFragment f = KeyAgreementFragment.newInstance();
+		if (fm.findFragmentByTag(ScanQrCodeFragment.TAG) == null) {
+			BaseFragment f = ScanQrCodeFragment.newInstance();
+			fm.beginTransaction()
+					.replace(R.id.fragmentContainer, f, f.getUniqueTag())
+					.addToBackStack(f.getUniqueTag())
+					.commit();
+		}
+	}
+
+	public void showQrCodeFragment() {
+		// FIXME #824
+		FragmentManager fm = getSupportFragmentManager();
+		if (fm.findFragmentByTag(ShowQrCodeFragment.TAG) == null) {
+			BaseFragment f = ShowQrCodeFragment.newInstance();
 			fm.beginTransaction()
 					.replace(R.id.fragmentContainer, f, f.getUniqueTag())
 					.addToBackStack(f.getUniqueTag())
@@ -255,6 +298,61 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		}
 	}
 
+	@Override
+	public void contactExchangeSucceeded(Author remoteAuthor) {
+		runOnUiThreadUnlessDestroyed(() -> {
+			String contactName = remoteAuthor.getName();
+			String format = getString(R.string.contact_added_toast);
+			String text = String.format(format, contactName);
+			Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG)
+					.show();
+			supportFinishAfterTransition();
+		});
+	}
+
+	@Override
+	public void duplicateContact(Author remoteAuthor) {
+		runOnUiThreadUnlessDestroyed(() -> {
+			String contactName = remoteAuthor.getName();
+			String format = getString(R.string.contact_already_exists);
+			String text = String.format(format, contactName);
+			Toast.makeText(KeyAgreementActivity.this, text, LENGTH_LONG)
+					.show();
+			finish();
+		});
+	}
+
+	@Override
+	public void finish() {
+		keyAgreementController.onStop();
+	}
+
+	@Override
+	public void contactExchangeFailed() {
+		showErrorExplanationFragmentAndReset();
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof KeyAgreementFailedEvent) {
+			showErrorExplanationFragmentAndReset();
+		} else if (e instanceof KeyAgreementAbortedEvent) {
+			showErrorExplanationFragmentAndReset();
+		}
+	}
+
+	private void showErrorExplanationFragmentAndReset() {
+		runOnUiThreadUnlessDestroyed(
+				() -> {
+					keyAgreementController.reset();
+					showErrorFragment(R.string.connection_error_explanation);
+				});
+	}
+
+	private enum BluetoothState {
+		UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED
+	}
+
 	private class BluetoothStateReceiver extends BroadcastReceiver {
 
 		@Override
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementController.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementController.java
new file mode 100644
index 0000000000000000000000000000000000000000..5efdf76bb1ca0bafa1fdee5145df25c208887789
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementController.java
@@ -0,0 +1,35 @@
+package org.briarproject.mailbox.android.keyagreement;
+
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.contact.ContactExchangeListener;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
+import org.briarproject.bramble.api.keyagreement.Payload;
+import org.briarproject.mailbox.android.controller.handler.UiResultHandler;
+
+public interface KeyAgreementController {
+	/**
+	 * Call this when your lifecycle starts,
+	 * so the listener will be called when information changes.
+	 * @param resuming
+	 */
+	@UiThread
+	void onStart(boolean resuming);
+
+	/**
+	 * Call this when your lifecycle stops,
+	 * so that the controller knows it can stops listening to events.
+	 */
+	@UiThread
+	void onStop();
+
+	void getQrCode(UiResultHandler<Payload> resultHandler);
+
+	void setRemoteQrCode(Payload remotePayload);
+
+	void startContactExchange(KeyAgreementResult result,
+			ContactExchangeListener listener);
+
+	void reset();
+
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementControllerImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementControllerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0f82bdca0ec7082327b54293c27bba049443f0f
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementControllerImpl.java
@@ -0,0 +1,158 @@
+package org.briarproject.mailbox.android.keyagreement;
+
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.contact.ContactExchangeListener;
+import org.briarproject.bramble.api.contact.ContactExchangeTask;
+import org.briarproject.bramble.api.db.DatabaseExecutor;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.event.EventListener;
+import org.briarproject.bramble.api.identity.IdentityManager;
+import org.briarproject.bramble.api.identity.LocalAuthor;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
+import org.briarproject.bramble.api.keyagreement.Payload;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.mailbox.android.controller.handler.UiResultHandler;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.api.contact.ContactType.MAILBOX_OWNER;
+import static org.briarproject.bramble.api.contact.ContactType.PRIVATE_MAILBOX;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+public class KeyAgreementControllerImpl implements KeyAgreementController,
+		EventListener {
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyAgreementControllerImpl.class.getName());
+
+	private final Executor dbExecutor, ioExecutor;
+	private final EventBus eventBus;
+	private final Provider<KeyAgreementTask> keyAgreementTaskProvider;
+	private volatile KeyAgreementTask task;
+	private volatile Payload localPayload;
+	private volatile CountDownLatch waitingForTask;
+	private IdentityManager identityManager;
+	private final ContactExchangeTask contactExchangeTask;
+
+	@Inject
+	KeyAgreementControllerImpl(@DatabaseExecutor Executor dbExecutor,
+			@IoExecutor Executor ioExecutor,
+			EventBus eventBus,
+			Provider<KeyAgreementTask> keyAgreementTaskProvider,
+			IdentityManager identityManager,
+			ContactExchangeTask contactExchangeTask) {
+		this.dbExecutor = dbExecutor;
+		this.ioExecutor = ioExecutor;
+		this.eventBus = eventBus;
+		this.keyAgreementTaskProvider = keyAgreementTaskProvider;
+		this.identityManager = identityManager;
+		this.contactExchangeTask = contactExchangeTask;
+	}
+
+	@Override
+	public void onStart(boolean resuming) {
+		eventBus.addListener(this);
+		if (!resuming) startListening();
+	}
+
+	@Override
+	public void onStop() {
+		eventBus.removeListener(this);
+		stopListening();
+	}
+
+	@Override
+	public void reset() {
+		localPayload = null;
+		startListening();
+	}
+
+	@UiThread
+	private void startListening() {
+		waitingForTask = new CountDownLatch(1);
+		KeyAgreementTask oldTask = task;
+		KeyAgreementTask newTask = keyAgreementTaskProvider.get();
+		task = newTask;
+		ioExecutor.execute(() -> {
+			if (oldTask != null) oldTask.stopListening();
+			newTask.listen();
+		});
+	}
+
+	@UiThread
+	private void stopListening() {
+		KeyAgreementTask oldTask = task;
+		ioExecutor.execute(() -> {
+			if (oldTask != null) oldTask.stopListening();
+		});
+	}
+
+	@Override
+	public synchronized void getQrCode(UiResultHandler<Payload> resultHandler) {
+		ioExecutor.execute(() -> {
+			try {
+				waitingForTask.await();
+			} catch (InterruptedException e) {
+				//TODO: Log
+				return;
+			}
+			resultHandler.onResultUi(localPayload);
+		});
+	}
+
+	@Override
+	public void setRemoteQrCode(Payload remotePayload) {
+		ioExecutor.execute(() -> {
+			try {
+				waitingForTask.await();
+			} catch (InterruptedException e) {
+				//TODO: Log
+				return;
+			}
+			task.connectAndRunProtocol(remotePayload);
+		});
+	}
+
+	@Override
+	public void startContactExchange(KeyAgreementResult result,
+			ContactExchangeListener listener) {
+		dbExecutor.execute(() -> {
+			LocalAuthor localAuthor;
+			// Load the local pseudonym
+			try {
+				localAuthor = identityManager.getLocalAuthor();
+			} catch (DbException e) {
+				logException(LOG, WARNING, e);
+				listener.contactExchangeFailed();
+				return;
+			}
+
+			// Exchange contact details
+			contactExchangeTask.startExchange(listener,
+					localAuthor, result.getMasterKey(),
+					result.getConnection(), result.getTransportId(),
+					result.wasAlice(), MAILBOX_OWNER, PRIVATE_MAILBOX);
+		});
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof KeyAgreementListeningEvent) {
+			localPayload =
+					((KeyAgreementListeningEvent) e).getLocalPayload();
+			waitingForTask.countDown();
+		}
+
+	}
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementEventListener.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementEventListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f02e12dd00fd0a910da90a43aa874d473fa1466
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementEventListener.java
@@ -0,0 +1,10 @@
+package org.briarproject.mailbox.android.keyagreement;
+
+import org.briarproject.bramble.api.contact.ContactExchangeListener;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface KeyAgreementEventListener extends ContactExchangeListener {
+
+	void showQrCodeFragment();
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementFragment.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementFragment.java
deleted file mode 100644
index 1603de2c29f3db4cf13e4203002b73bab860a368..0000000000000000000000000000000000000000
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/KeyAgreementFragment.java
+++ /dev/null
@@ -1,371 +0,0 @@
-package org.briarproject.mailbox.android.keyagreement;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.os.Bundle;
-import android.support.annotation.UiThread;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.LinearLayout.LayoutParams;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.google.zxing.Result;
-
-import org.briarproject.bramble.api.UnsupportedVersionException;
-import org.briarproject.bramble.api.event.Event;
-import org.briarproject.bramble.api.event.EventBus;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
-import org.briarproject.bramble.api.keyagreement.Payload;
-import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
-import org.briarproject.bramble.api.keyagreement.PayloadParser;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementAbortedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFailedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent;
-import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
-import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
-import org.briarproject.mailbox.R;
-import org.briarproject.mailbox.android.activity.ActivityComponent;
-import org.briarproject.mailbox.android.fragment.BaseEventFragment;
-import org.briarproject.mailbox.android.view.QrCodeView;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-import javax.inject.Provider;
-
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.view.View.INVISIBLE;
-import static android.view.View.VISIBLE;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.widget.LinearLayout.HORIZONTAL;
-import static android.widget.Toast.LENGTH_LONG;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.bramble.util.LogUtils.logException;
-
-@MethodsNotNullByDefault
-@ParametersNotNullByDefault
-public class KeyAgreementFragment extends BaseEventFragment
-		implements QrCodeDecoder.ResultCallback, QrCodeView.FullscreenListener {
-
-	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");
-
-	@Inject
-	Provider<KeyAgreementTask> keyAgreementTaskProvider;
-	@Inject
-	PayloadEncoder payloadEncoder;
-	@Inject
-	PayloadParser payloadParser;
-	@Inject
-	@IoExecutor
-	Executor ioExecutor;
-	@Inject
-	EventBus eventBus;
-
-	private CameraView cameraView;
-	private LinearLayout cameraOverlay;
-	private View statusView;
-	private QrCodeView qrCodeView;
-	private TextView status;
-
-	private boolean gotRemotePayload;
-	private volatile boolean gotLocalPayload;
-	private KeyAgreementTask task;
-	private KeyAgreementEventListener listener;
-
-	public static KeyAgreementFragment newInstance() {
-		Bundle args = new Bundle();
-		KeyAgreementFragment fragment = new KeyAgreementFragment();
-		fragment.setArguments(args);
-		return fragment;
-	}
-
-	@Override
-	public void onAttach(Context context) {
-		super.onAttach(context);
-		listener = (KeyAgreementEventListener) context;
-	}
-
-	@Override
-	public void injectFragment(ActivityComponent component) {
-		component.inject(this);
-	}
-
-	@Override
-	public String getUniqueTag() {
-		return TAG;
-	}
-
-	@Nullable
-	@Override
-	public View onCreateView(LayoutInflater inflater,
-			@Nullable ViewGroup container,
-			@Nullable Bundle savedInstanceState) {
-		return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
-				false);
-	}
-
-	@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);
-		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));
-	}
-
-	@Override
-	public void onStart() {
-		super.onStart();
-		try {
-			cameraView.start();
-		} catch (CameraException e) {
-			logCameraExceptionAndFinish(e);
-		}
-		startListening();
-	}
-
-	@Override
-	public void setFullscreen(boolean fullscreen) {
-		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
-	public void onStop() {
-		super.onStop();
-		stopListening();
-		try {
-			cameraView.stop();
-		} catch (CameraException e) {
-			logCameraExceptionAndFinish(e);
-		}
-	}
-
-	@UiThread
-	private void logCameraExceptionAndFinish(CameraException e) {
-		logException(LOG, WARNING, e);
-		Toast.makeText(getActivity(), R.string.camera_error,
-				LENGTH_LONG).show();
-		finish();
-	}
-
-	@UiThread
-	private void startListening() {
-		KeyAgreementTask oldTask = task;
-		KeyAgreementTask newTask = keyAgreementTaskProvider.get();
-		task = newTask;
-		ioExecutor.execute(() -> {
-			if (oldTask != null) oldTask.stopListening();
-			newTask.listen();
-		});
-	}
-
-	@UiThread
-	private void stopListening() {
-		KeyAgreementTask oldTask = task;
-		ioExecutor.execute(() -> {
-			if (oldTask != null) oldTask.stopListening();
-		});
-	}
-
-	@UiThread
-	private void reset() {
-		// If we've stopped the camera view, restart it
-		if (gotRemotePayload) {
-			try {
-				cameraView.start();
-			} catch (CameraException e) {
-				logCameraExceptionAndFinish(e);
-				return;
-			}
-		}
-		statusView.setVisibility(INVISIBLE);
-		cameraView.setVisibility(VISIBLE);
-		gotRemotePayload = false;
-		gotLocalPayload = false;
-		startListening();
-	}
-
-	@UiThread
-	private void qrCodeScanned(String content) {
-		try {
-			byte[] payloadBytes = content.getBytes(ISO_8859_1);
-			if (LOG.isLoggable(INFO))
-				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);
-			task.connectAndRunProtocol(remotePayload);
-		} catch (UnsupportedVersionException e) {
-			reset();
-			String msg = getString(R.string.qr_code_unsupported,
-					getString(R.string.app_name));
-			showNextFragment(ContactExchangeErrorFragment.newInstance(msg));
-		} catch (CameraException e) {
-			logCameraExceptionAndFinish(e);
-		} catch (IOException | IllegalArgumentException e) {
-			LOG.log(WARNING, "QR Code Invalid", e);
-			reset();
-			Toast.makeText(getActivity(), R.string.qr_code_invalid,
-					LENGTH_LONG).show();
-		}
-	}
-
-	@Override
-	public void eventOccurred(Event e) {
-		if (e instanceof KeyAgreementListeningEvent) {
-			KeyAgreementListeningEvent event = (KeyAgreementListeningEvent) e;
-			gotLocalPayload = true;
-			setQrCode(event.getLocalPayload());
-		} else if (e instanceof KeyAgreementFailedEvent) {
-			keyAgreementFailed();
-		} else if (e instanceof KeyAgreementWaitingEvent) {
-			keyAgreementWaiting();
-		} else if (e instanceof KeyAgreementStartedEvent) {
-			keyAgreementStarted();
-		} else if (e instanceof KeyAgreementAbortedEvent) {
-			KeyAgreementAbortedEvent event = (KeyAgreementAbortedEvent) e;
-			keyAgreementAborted(event.didRemoteAbort());
-		} else if (e instanceof KeyAgreementFinishedEvent) {
-			keyAgreementFinished(((KeyAgreementFinishedEvent) e).getResult());
-		}
-	}
-
-	private void keyAgreementFailed() {
-		runOnUiThreadUnlessDestroyed(() -> {
-			reset();
-			listener.keyAgreementFailed();
-		});
-	}
-
-	private void keyAgreementWaiting() {
-		runOnUiThreadUnlessDestroyed(
-				() -> status.setText(listener.keyAgreementWaiting()));
-	}
-
-	private void keyAgreementStarted() {
-		runOnUiThreadUnlessDestroyed(() -> {
-			qrCodeView.setVisibility(INVISIBLE);
-			statusView.setVisibility(VISIBLE);
-			status.setText(listener.keyAgreementStarted());
-		});
-	}
-
-	private void keyAgreementAborted(boolean remoteAborted) {
-		runOnUiThreadUnlessDestroyed(() -> {
-			reset();
-			listener.keyAgreementAborted(remoteAborted);
-		});
-	}
-
-	private void keyAgreementFinished(KeyAgreementResult result) {
-		runOnUiThreadUnlessDestroyed(() -> {
-			statusView.setVisibility(VISIBLE);
-			status.setText(listener.keyAgreementFinished(result));
-		});
-	}
-
-	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(() -> {
-			LOG.info("Got result from decoder");
-			// Ignore results until the KeyAgreementTask is ready
-			if (!gotLocalPayload) return;
-			if (!gotRemotePayload) qrCodeScanned(result.getText());
-		});
-	}
-
-	@Override
-	protected void finish() {
-		getActivity().getSupportFragmentManager().popBackStack();
-	}
-
-	@NotNullByDefault
-	interface KeyAgreementEventListener {
-
-		@UiThread
-		void keyAgreementFailed();
-
-		// Should return a string to be displayed as status.
-		@UiThread
-		@Nullable
-		String keyAgreementWaiting();
-
-		// Should return a string to be displayed as status.
-		@UiThread
-		@Nullable
-		String keyAgreementStarted();
-
-		// Will show an error fragment.
-		@UiThread
-		void keyAgreementAborted(boolean remoteAborted);
-
-		// Should return a string to be displayed as status.
-		@UiThread
-		@Nullable
-		String keyAgreementFinished(KeyAgreementResult result);
-	}
-}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/MailboxExchangeActivity.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/MailboxExchangeActivity.java
deleted file mode 100644
index add34227bdd5d9084d2bb6c19896359e7fb64c8e..0000000000000000000000000000000000000000
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/MailboxExchangeActivity.java
+++ /dev/null
@@ -1,132 +0,0 @@
-package org.briarproject.mailbox.android.keyagreement;
-
-import android.os.Bundle;
-import android.support.annotation.UiThread;
-import android.widget.Toast;
-
-import org.briarproject.bramble.api.contact.ContactExchangeListener;
-import org.briarproject.bramble.api.contact.ContactExchangeTask;
-import org.briarproject.bramble.api.db.DbException;
-import org.briarproject.bramble.api.identity.Author;
-import org.briarproject.bramble.api.identity.IdentityManager;
-import org.briarproject.bramble.api.identity.LocalAuthor;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
-import org.briarproject.mailbox.R;
-import org.briarproject.mailbox.android.activity.ActivityComponent;
-
-import java.util.logging.Logger;
-
-import javax.annotation.Nullable;
-import javax.inject.Inject;
-
-import static android.widget.Toast.LENGTH_LONG;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.bramble.api.contact.ContactType.MAILBOX_OWNER;
-import static org.briarproject.bramble.api.contact.ContactType.PRIVATE_MAILBOX;
-import static org.briarproject.bramble.util.LogUtils.logException;
-
-public class MailboxExchangeActivity extends KeyAgreementActivity implements
-		ContactExchangeListener {
-
-	private static final Logger LOG =
-			Logger.getLogger(MailboxExchangeActivity.class.getName());
-
-	// Fields that are accessed from background threads must be volatile
-	@Inject
-	volatile ContactExchangeTask contactExchangeTask;
-	@Inject
-	volatile IdentityManager identityManager;
-
-	@Override
-	public void injectActivity(ActivityComponent component) {
-		component.inject(this);
-	}
-
-	@Override
-	public void onCreate(@Nullable Bundle state) {
-		super.onCreate(state);
-		getSupportActionBar().setTitle(R.string.pairing_title);
-	}
-
-	protected void startContactExchange(KeyAgreementResult result) {
-		runOnDbThread(() -> {
-			LocalAuthor localAuthor;
-			// Load the local pseudonym
-			try {
-				localAuthor = identityManager.getLocalAuthor();
-			} catch (DbException e) {
-				logException(LOG, WARNING, e);
-				contactExchangeFailed();
-				return;
-			}
-
-			// Exchange contact details
-			contactExchangeTask.startExchange(MailboxExchangeActivity.this,
-					localAuthor, result.getMasterKey(),
-					result.getConnection(), result.getTransportId(),
-					result.wasAlice(), PRIVATE_MAILBOX, MAILBOX_OWNER);
-		});
-	}
-
-	@Override
-	public void contactExchangeSucceeded(Author remoteAuthor) {
-		runOnUiThreadUnlessDestroyed(() -> {
-			String contactName = remoteAuthor.getName();
-			String format = getString(R.string.contact_added_toast);
-			String text = String.format(format, contactName);
-			Toast.makeText(MailboxExchangeActivity.this, text, LENGTH_LONG)
-					.show();
-			supportFinishAfterTransition();
-		});
-	}
-
-	@Override
-	public void duplicateContact(Author remoteAuthor) {
-		runOnUiThreadUnlessDestroyed(() -> {
-			String contactName = remoteAuthor.getName();
-			String format = getString(R.string.contact_already_exists);
-			String text = String.format(format, contactName);
-			Toast.makeText(MailboxExchangeActivity.this, text, LENGTH_LONG)
-					.show();
-			finish();
-		});
-	}
-
-	@Override
-	public void contactExchangeFailed() {
-		runOnUiThreadUnlessDestroyed(() -> {
-			showErrorFragment(R.string.connection_error_explanation);
-		});
-	}
-
-	@UiThread
-	@Override
-	public void keyAgreementFailed() {
-		showErrorFragment(R.string.connection_error_explanation);
-	}
-
-	@UiThread
-	@Override
-	public String keyAgreementWaiting() {
-		return getString(R.string.waiting_for_contact_to_scan);
-	}
-
-	@UiThread
-	@Override
-	public String keyAgreementStarted() {
-		return getString(R.string.authenticating_with_device);
-	}
-
-	@UiThread
-	@Override
-	public void keyAgreementAborted(boolean remoteAborted) {
-		showErrorFragment(R.string.connection_error_explanation);
-	}
-
-	@UiThread
-	@Override
-	public String keyAgreementFinished(KeyAgreementResult result) {
-		startContactExchange(result);
-		return getString(R.string.exchanging_contact_details);
-	}
-}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/ScanQrCodeFragment.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/ScanQrCodeFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..f89f5fdd4eae5217074bc471a41a08ade5195e27
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/ScanQrCodeFragment.java
@@ -0,0 +1,159 @@
+package org.briarproject.mailbox.android.keyagreement;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.UiThread;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import com.google.zxing.Result;
+
+import org.briarproject.bramble.api.UnsupportedVersionException;
+import org.briarproject.bramble.api.keyagreement.Payload;
+import org.briarproject.bramble.api.keyagreement.PayloadParser;
+import org.briarproject.mailbox.R;
+import org.briarproject.mailbox.android.activity.ActivityComponent;
+import org.briarproject.mailbox.android.fragment.BaseFragment;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.logging.Logger;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.widget.Toast.LENGTH_LONG;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+public class ScanQrCodeFragment extends BaseFragment
+		implements QrCodeDecoder.ResultCallback {
+
+	static final String TAG = ScanQrCodeFragment.class.getName();
+
+	private static final Logger LOG = Logger.getLogger(TAG);
+	private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+	@Inject
+	PayloadParser payloadParser;
+	@Inject
+	KeyAgreementController keyAgreementController;
+
+	private CameraView cameraView;
+	private KeyAgreementEventListener listener;
+	private volatile boolean gotRemotePayload;
+
+	public static ScanQrCodeFragment newInstance() {
+		Bundle args = new Bundle();
+		ScanQrCodeFragment fragment = new ScanQrCodeFragment();
+		fragment.setArguments(args);
+		return fragment;
+	}
+
+	@Override
+	public void onAttach(Context context) {
+		super.onAttach(context);
+		listener = (KeyAgreementEventListener) context;
+	}
+
+	@Override
+	public void injectFragment(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	@Nullable
+	@Override
+	public View onCreateView(LayoutInflater inflater,
+			@Nullable ViewGroup container,
+			@Nullable Bundle savedInstanceState) {
+		return inflater.inflate(R.layout.fragment_keyagreement_scan, container,
+				false);
+	}
+
+	@Override
+	public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+		super.onViewCreated(view, savedInstanceState);
+		cameraView = view.findViewById(R.id.camera_view);
+	}
+
+	@Override
+	public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+		super.onActivityCreated(savedInstanceState);
+		getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
+		cameraView.setPreviewConsumer(new QrCodeDecoder(this));
+	}
+
+	@Override
+	public void onStart() {
+		super.onStart();
+		try {
+			cameraView.start();
+		} catch (CameraException e) {
+			logCameraExceptionAndFinish(e);
+		}
+	}
+
+	@Override
+	public void onStop() {
+		super.onStop();
+		try {
+			cameraView.stop();
+		} catch (CameraException e) {
+			logCameraExceptionAndFinish(e);
+		}
+	}
+
+	@UiThread
+	private void logCameraExceptionAndFinish(CameraException e) {
+		logException(LOG, WARNING, e);
+		Toast.makeText(getActivity(), R.string.camera_error,
+				LENGTH_LONG).show();
+		finish();
+	}
+
+	@Override
+	public void handleResult(Result result) {
+		runOnUiThreadUnlessDestroyed(() -> {
+			LOG.info("Got result from decoder");
+			if (!gotRemotePayload) qrCodeScanned(result.getText());
+		});
+	}
+
+	@UiThread
+	private void qrCodeScanned(String content) {
+		try {
+			byte[] payloadBytes = content.getBytes(ISO_8859_1);
+			if (LOG.isLoggable(INFO))
+				LOG.info("Remote payload is " + payloadBytes.length + " bytes");
+			Payload remotePayload = payloadParser.parse(payloadBytes);
+			gotRemotePayload = true;
+			cameraView.stop();
+			keyAgreementController.setRemoteQrCode(remotePayload);
+			listener.showQrCodeFragment();
+		} catch (UnsupportedVersionException e) {
+			String msg = getString(R.string.qr_code_unsupported,
+					getString(R.string.app_name));
+			showNextFragment(ContactExchangeErrorFragment.newInstance(msg));
+		} catch (CameraException e) {
+			logCameraExceptionAndFinish(e);
+		} catch (IOException | IllegalArgumentException e) {
+			LOG.log(WARNING, "QR Code Invalid", e);
+			Toast.makeText(getActivity(), R.string.qr_code_invalid,
+					LENGTH_LONG).show();
+		}
+	}
+
+	@Override
+	protected void finish() {
+		getActivity().getSupportFragmentManager().popBackStack();
+	}
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/ShowQrCodeFragment.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/ShowQrCodeFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..1a96035bb0f5aa02fdfc608a97a9ae6dbd16d3a7
--- /dev/null
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/keyagreement/ShowQrCodeFragment.java
@@ -0,0 +1,180 @@
+package org.briarproject.mailbox.android.keyagreement;
+
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.LinearInterpolator;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementConstants;
+import org.briarproject.bramble.api.keyagreement.Payload;
+import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent;
+import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
+import org.briarproject.mailbox.R;
+import org.briarproject.mailbox.android.activity.ActivityComponent;
+import org.briarproject.mailbox.android.controller.handler.UiResultHandler;
+import org.briarproject.mailbox.android.fragment.BaseEventFragment;
+import org.briarproject.mailbox.android.view.QrCodeView;
+
+import java.nio.charset.Charset;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+import static java.util.logging.Level.INFO;
+
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
+public class ShowQrCodeFragment extends BaseEventFragment {
+	static final String TAG = ShowQrCodeFragment.class.getName();
+
+	private static final Logger LOG = Logger.getLogger(TAG);
+	private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");
+
+	@Inject
+	PayloadEncoder payloadEncoder;
+	@Inject
+	@IoExecutor
+	Executor ioExecutor;
+	@Inject
+	KeyAgreementController keyAgreementController;
+
+	private QrCodeView qrCodeView;
+	private TextView status;
+	private ProgressBar progressBar;
+	private Animator animation;
+	private TextView instruction;
+
+	private KeyAgreementEventListener listener;
+
+	public static ShowQrCodeFragment newInstance() {
+		Bundle args = new Bundle();
+		ShowQrCodeFragment fragment = new ShowQrCodeFragment();
+		fragment.setArguments(args);
+		return fragment;
+	}
+
+	@Override
+	public void onAttach(Context context) {
+		super.onAttach(context);
+		listener = (KeyAgreementEventListener) context;
+	}
+
+	@Override
+	public void injectFragment(ActivityComponent component) {
+		component.inject(this);
+	}
+
+	@Override
+	public String getUniqueTag() {
+		return TAG;
+	}
+
+	@Nullable
+	@Override
+	public View onCreateView(LayoutInflater inflater,
+			@Nullable ViewGroup container,
+			@Nullable Bundle savedInstanceState) {
+		return inflater.inflate(R.layout.fragment_keyagreement_qr, container,
+				false);
+	}
+
+	@Override
+	public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+		super.onViewCreated(view, savedInstanceState);
+		qrCodeView = view.findViewById(R.id.qr_code_view);
+		status = view.findViewById(R.id.status);
+		instruction = view.findViewById(R.id.instruction);
+		progressBar = view.findViewById(R.id.timer);
+	}
+
+	@Override
+	public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+		super.onActivityCreated(savedInstanceState);
+		getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
+		keyAgreementController.getQrCode(new UiResultHandler<Payload>(this) {
+			@Override
+			public void onResultUi(@Nonnull Payload payload) {
+				showQrCode(payload);
+			}
+		});
+	}
+
+	private void showQrCode(Payload payload) {
+		Context context = getContext();
+		if (context == null) return;
+		DisplayMetrics dm = context.getResources().getDisplayMetrics();
+		ioExecutor.execute(() -> {
+			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);
+			Bitmap qrCode = QrCodeUtils.createQrCode(dm, content);
+			runOnUiThreadUnlessDestroyed(() -> {
+				qrCodeView.setQrCode(qrCode);
+				startCountdown();
+			});
+		});
+	}
+
+	private void startCountdown() {
+		animation = ObjectAnimator.ofInt(progressBar, "progress", 100, 0);
+		animation.setDuration(KeyAgreementConstants.CONNECTION_TIMEOUT);
+		animation.setInterpolator(new LinearInterpolator());
+		animation.start();
+	}
+
+	@Override
+	protected void finish() {
+		getActivity().getSupportFragmentManager().popBackStack();
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof KeyAgreementWaitingEvent) {
+			runOnUiThreadUnlessDestroyed(() -> {
+				if (animation != null) {
+					animation.cancel();
+					animation.start();
+				}
+			});
+		} else if (e instanceof KeyAgreementStartedEvent) {
+			runOnUiThreadUnlessDestroyed(() -> {
+				animation.cancel();
+				progressBar.setVisibility(INVISIBLE);
+				instruction.setVisibility(INVISIBLE);
+				qrCodeView.setVisibility(INVISIBLE);
+				status.setVisibility(VISIBLE);
+				status.setText(R.string.authenticating_with_device);
+			});
+		} else if (e instanceof KeyAgreementFinishedEvent) {
+			runOnUiThreadUnlessDestroyed(() -> {
+				keyAgreementController.startContactExchange(
+						((KeyAgreementFinishedEvent) e).getResult(), listener);
+				status.setText(R.string.exchanging_contact_details);
+			});
+		}
+	}
+}
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/overview/OverviewFragment.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/overview/OverviewFragment.java
index a0b8f2d73507b8af8a30fadd162aedf44a325ef0..e986e095528de0515260237b54869cb1e6ca0d86 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/overview/OverviewFragment.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/overview/OverviewFragment.java
@@ -24,7 +24,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.mailbox.R;
 import org.briarproject.mailbox.android.activity.ActivityComponent;
 import org.briarproject.mailbox.android.fragment.BaseEventFragment;
-import org.briarproject.mailbox.android.keyagreement.MailboxExchangeActivity;
+import org.briarproject.mailbox.android.keyagreement.KeyAgreementActivity;
 import org.briarproject.mailbox.android.viewmodels.MailboxOwnerStatusViewModel;
 
 import java.util.ArrayList;
@@ -155,7 +155,7 @@ public class OverviewFragment extends BaseEventFragment {
 		switch (item.getItemId()) {
 			case R.id.action_add_contact:
 				Intent intent =
-						new Intent(getContext(), MailboxExchangeActivity.class);
+						new Intent(getContext(), KeyAgreementActivity.class);
 				startActivity(intent);
 				return true;
 			default:
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/view/QrCodeView.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/view/QrCodeView.java
index d6dbc7ee56852151afe9881e0da14cdc4b7210d3..9c7426a6b0fb24743141a158b1c9bcc255aa87b2 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/view/QrCodeView.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/view/QrCodeView.java
@@ -15,49 +15,24 @@ import org.briarproject.mailbox.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.setFullscreen(fullscreen);
-                }
-        );
-    }
-
-    @UiThread
-    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 setFullscreen(boolean fullscreen);
-    }
+	private final ImageView qrCodeImageView;
+
+	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);
+	}
+
+	@UiThread
+	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);
+	}
 
 }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/viewmodels/BaseViewModel.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/viewmodels/BaseViewModel.java
index c2e3a1b15465ca06147037b57bac9abb61d90570..2f00729cbe7912603736ac3114fef2631fc49c3b 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/viewmodels/BaseViewModel.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/viewmodels/BaseViewModel.java
@@ -9,9 +9,7 @@ import org.briarproject.mailbox.android.MailboxApplication;
 
 public abstract class BaseViewModel extends AndroidViewModel {
 
-	public BaseViewModel(
-			@NonNull
-					Application application) {
+	public BaseViewModel(@NonNull Application application) {
 		super(application);
 		AndroidComponent applicationComponent =
 				((MailboxApplication) getApplication())
@@ -22,7 +20,6 @@ public abstract class BaseViewModel extends AndroidViewModel {
 						.androidComponent(applicationComponent)
 						.viewModelModule(new ViewModelModule())
 						.build();
-
 		injectViewModel(viewModelComponent);
 
 	}
diff --git a/mailbox-android/src/main/res/layout/fragment_keyagreement_qr.xml b/mailbox-android/src/main/res/layout/fragment_keyagreement_qr.xml
index cc53978b5ec509d17d7d1e68afb2943267a43d33..449f5b905d5ab535aa492c997e3a85da684824bd 100644
--- a/mailbox-android/src/main/res/layout/fragment_keyagreement_qr.xml
+++ b/mailbox-android/src/main/res/layout/fragment_keyagreement_qr.xml
@@ -1,52 +1,61 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
+<androidx.constraintlayout.widget.ConstraintLayout
 	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:tools="http://schemas.android.com/tools"
+	xmlns:app="http://schemas.android.com/apk/res-auto"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
-	<org.briarproject.mailbox.android.keyagreement.CameraView
-		android:id="@+id/camera_view"
+
+	<org.briarproject.mailbox.android.view.QrCodeView
+		android:id="@+id/qr_code_view"
+		android:layout_width="match_parent"
+		android:layout_height="0dp"
+		android:background="@android:color/white"
+		app:layout_constraintBottom_toTopOf="@+id/instruction"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent"/>
+
+
+	<TextView
+		android:id="@+id/instruction"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent"/>
+		android:layout_height="wrap_content"
+		android:background="@color/window_background"
+		android:gravity="center"
+		android:paddingBottom="@dimen/margin_large"
+		android:paddingTop="@dimen/margin_large"
+		android:text="Use your Briar phone to scan this Qr Code"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toBottomOf="@+id/qr_code_view"/>
 
-	<LinearLayout
-		android:id="@+id/camera_overlay"
+	<ProgressBar
+		android:id="@+id/timer"
+		style="@style/Widget.AppCompat.ProgressBar.Horizontal"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:background="@android:color/transparent"
+		android:max="100"
+		android:progress="100"
+		android:rotation="180"
+		app:layout_constraintTop_toTopOf="@+id/instruction"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"/>
+
+	<TextView
+		android:id="@+id/status"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent"
-		android:baselineAligned="false"
-		android:orientation="vertical">
-
-		<LinearLayout
-			android:id="@+id/status_container"
-			android:layout_width="match_parent"
-			android:layout_height="0dp"
-			android:layout_weight="1"
-			android:gravity="center"
-			android:orientation="vertical"
-			android:padding="@dimen/margin_medium"
-			android:visibility="invisible"
-			tools:visibility="visible">
-
-			<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>
-
-		<org.briarproject.mailbox.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"/>
-	</LinearLayout>
-</FrameLayout>
+		android:gravity="center"
+		android:padding="@dimen/margin_large"
+		android:text="Exchanging contact details"
+		android:visibility="invisible"
+		app:layout_constraintBottom_toTopOf="@+id/timer"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent"/>
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/mailbox-android/src/main/res/layout/fragment_keyagreement_scan.xml b/mailbox-android/src/main/res/layout/fragment_keyagreement_scan.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ffa6162ebe57e19461751d393c08cd3b7f467d9c
--- /dev/null
+++ b/mailbox-android/src/main/res/layout/fragment_keyagreement_scan.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+	android:id="@+id/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">
+
+	<org.briarproject.mailbox.android.keyagreement.CameraView
+		android:id="@+id/camera_view"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent"/>
+
+	<TextView
+		android:id="@+id/instruction"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:background="@color/window_background"
+		android:gravity="center"
+		android:paddingBottom="@dimen/margin_large"
+		android:paddingTop="@dimen/margin_large"
+		android:text="Scan the QrCode on your Briar phone"
+		app:layout_constraintBottom_toBottomOf="parent"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		tools:text="Scan the QrCode on your Briar phone"/>
+
+
+</android.support.constraint.ConstraintLayout>
diff --git a/mailbox-android/src/main/res/layout/qr_code_view.xml b/mailbox-android/src/main/res/layout/qr_code_view.xml
index 9062428d0e6b3456b06b1e91a8e952cd3963e44d..03f3dde245d4923001836f43a6791b1e3e91fb44 100644
--- a/mailbox-android/src/main/res/layout/qr_code_view.xml
+++ b/mailbox-android/src/main/res/layout/qr_code_view.xml
@@ -9,7 +9,10 @@
 		style="?android:attr/progressBarStyleLarge"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
-		android:layout_gravity="center"/>
+		android:layout_gravity="center"
+		app:layout_constraintEnd_toEndOf="parent"
+		app:layout_constraintStart_toStartOf="parent"
+		app:layout_constraintTop_toTopOf="parent"/>
 
 	<android.support.constraint.ConstraintLayout
 		android:layout_width="match_parent"
@@ -20,20 +23,11 @@
 			android:layout_width="match_parent"
 			android:layout_height="match_parent"
 			android:contentDescription="@string/qr_code"
-			android:scaleType="fitCenter"/>
-
-		<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"
+			android:scaleType="fitCenter"
 			app:layout_constraintBottom_toBottomOf="parent"
 			app:layout_constraintEnd_toEndOf="parent"
-			app:layout_constraintRight_toRightOf="parent"/>
+			app:layout_constraintStart_toStartOf="parent"
+			app:layout_constraintTop_toTopOf="parent"/>
 
 	</android.support.constraint.ConstraintLayout>
 </merge>