diff --git a/LICENSE.txt b/LICENSE.txt
index 1b5f2163e9d346d6e818fbf09a4769ffa8bb8c3a..3d615ecb5d4dfabc0df096c2e22a4319c35d1589 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -13,23 +13,6 @@
     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
--------------------------------------------------------------------------
-
-    All files under the directories briar-android/src, briar-api/src,
-    briar-core/src, briar-desktop/src and briar-test/src are licensed
-    under the Apache License, version 2.0 (the "License"); you may not
-    use these files except in compliance with the License.
-
-    You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-    Unless required by applicable law or agreed to in writing, software
-    distributed under the License is distributed on an "AS IS" BASIS,
-    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-    implied. See the License for the specific language governing
-    permissions and limitations under the License.
-
 -------------------------------------------------------------------------
 
                     GNU GENERAL PUBLIC LICENSE
diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml
index 5da35cdf462510dd5206de26c2692a3ed516497d..25437944268018098ab7ba77ac68f0d62322b8c6 100644
--- a/briar-android/AndroidManifest.xml
+++ b/briar-android/AndroidManifest.xml
@@ -134,7 +134,8 @@
 		<activity
 			android:name=".android.forum.ForumActivity"
 			android:label="@string/app_name"
-			android:parentActivityName=".android.NavDrawerActivity">
+			android:parentActivityName=".android.NavDrawerActivity"
+			android:windowSoftInputMode="adjustResize|stateHidden">
 			<meta-data
 				android:name="android.support.PARENT_ACTIVITY"
 				android:value=".android.NavDrawerActivity"
diff --git a/briar-android/artwork/ic_emoji_emoticons.svg b/briar-android/artwork/ic_emoji_emoticons.svg
new file mode 100644
index 0000000000000000000000000000000000000000..7ef60e16b09175f557649d9639d94107f41b7d78
--- /dev/null
+++ b/briar-android/artwork/ic_emoji_emoticons.svg
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<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"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   sodipodi:docname="ic_emoji_emoticons.svg">
+  <metadata
+     id="metadata8">
+    <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>
+  <defs
+     id="defs6" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1021"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="4.6354778"
+     inkscape:cx="47.926788"
+     inkscape:cy="24.127496"
+     inkscape:window-x="1440"
+     inkscape:window-y="23"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg2" />
+  <path
+     style="fill:#000000;fill-opacity:1"
+     d="m 15.483903,3.8556996 c -0.661546,0.040406 -0.536253,1.2125273 -0.08054,1.6240791 1.361771,1.4519837 1.747379,3.5080793 1.895646,5.4253553 0.109142,2.216286 -0.0846,4.555699 -1.171466,6.533591 -0.361828,0.731167 -1.339597,1.273078 -1.15283,2.195835 0.287109,1.037426 1.187031,0.242862 1.620751,-0.183708 1.991711,-1.742024 2.867744,-4.428018 2.93133,-7.013492 0.02009,-1.918049 -0.231841,-3.9213035 -1.212735,-5.6044037 -0.664187,-1.0906817 -1.39072,-2.2339438 -2.497355,-2.9193489 -0.127976,-0.045915 -0.238296,-0.06368 -0.332802,-0.057908 z M 5.9118212,7.6583077 A 1.3631614,1.3631614 0 0 0 4.54866,9.0214691 1.3631614,1.3631614 0 0 0 5.9118212,10.38463 1.3631614,1.3631614 0 0 0 7.2749824,9.0214691 1.3631614,1.3631614 0 0 0 5.9118212,7.6583077 Z m 3.0731032,3.0012183 0,2.044742 4.7710646,0 0,-2.044742 -4.7710646,0 z m -3.1496485,3.471136 a 1.3631614,1.3631614 0 0 0 -1.3631612,1.363161 1.3631614,1.3631614 0 0 0 1.3631612,1.363161 1.3631614,1.3631614 0 0 0 1.3631612,-1.363161 1.3631614,1.3631614 0 0 0 -1.3631612,-1.363161 z"
+     id="path4142"
+     inkscape:connector-curvature="0" />
+</svg>
diff --git a/briar-android/assets/emoji_activity.png b/briar-android/assets/emoji_activity.png
new file mode 100644
index 0000000000000000000000000000000000000000..908370dd15ec125d7b57b96ae932ee5fbe58a9d9
Binary files /dev/null and b/briar-android/assets/emoji_activity.png differ
diff --git a/briar-android/assets/emoji_animals_nature.png b/briar-android/assets/emoji_animals_nature.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a0661fe1ff7b9c82e2a36381ce303ceb1246693
Binary files /dev/null and b/briar-android/assets/emoji_animals_nature.png differ
diff --git a/briar-android/assets/emoji_flags.png b/briar-android/assets/emoji_flags.png
new file mode 100644
index 0000000000000000000000000000000000000000..80f6bbd0db5f9e5e4a9938a344f064372e894606
Binary files /dev/null and b/briar-android/assets/emoji_flags.png differ
diff --git a/briar-android/assets/emoji_food_drink.png b/briar-android/assets/emoji_food_drink.png
new file mode 100644
index 0000000000000000000000000000000000000000..33d7cd0a6849759c4ccb852a8938afd5354ec25b
Binary files /dev/null and b/briar-android/assets/emoji_food_drink.png differ
diff --git a/briar-android/assets/emoji_objects.png b/briar-android/assets/emoji_objects.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2b6dfbf9d7b20287d6e95a9739373f37f5eaaaa
Binary files /dev/null and b/briar-android/assets/emoji_objects.png differ
diff --git a/briar-android/assets/emoji_smiley_people.png b/briar-android/assets/emoji_smiley_people.png
new file mode 100644
index 0000000000000000000000000000000000000000..9325d703a8010ac1ca938f3a8b20836cc1efc4c0
Binary files /dev/null and b/briar-android/assets/emoji_smiley_people.png differ
diff --git a/briar-android/assets/emoji_symbols.png b/briar-android/assets/emoji_symbols.png
new file mode 100644
index 0000000000000000000000000000000000000000..e88275b2fc82bc0a03759a642186a8184c8746f9
Binary files /dev/null and b/briar-android/assets/emoji_symbols.png differ
diff --git a/briar-android/assets/emoji_travel_places.png b/briar-android/assets/emoji_travel_places.png
new file mode 100644
index 0000000000000000000000000000000000000000..16e2be37d680dd410b6856de655770f3c7cbbac8
Binary files /dev/null and b/briar-android/assets/emoji_travel_places.png differ
diff --git a/briar-android/build.gradle b/briar-android/build.gradle
index dc93b62fb5c70e59fb8a55513742493a91e9efc4..469df63b22a344d580c5f68e37b1d19cbdf57fb5 100644
--- a/briar-android/build.gradle
+++ b/briar-android/build.gradle
@@ -35,6 +35,8 @@ dependencies {
 	compile 'com.google.zxing:core:3.2.1'
 	apt 'com.google.dagger:dagger-compiler:2.0.2'
 	provided 'javax.annotation:jsr250-api:1.0'
+	compile 'com.jpardogo.materialtabstrip:library:1.1.0'
+	compile 'com.github.bumptech.glide:glide:3.7.0'
 
 	testCompile 'junit:junit:4.12'
 	testCompile 'net.jodah:concurrentunit:0.4.2'
@@ -60,6 +62,8 @@ dependencyVerification {
 			'com.android.support:support-vector-drawable:799bafe4c3de812386f0b291f744d5d6876452722dd40189b9ab87dbbf594ea1',
 			'com.android.support:recyclerview-v7:44040a888e23e0c93162a3377cfe06751080e3c22d369ab0d4301ef60d63b0fe',
 			'com.android.support:cardview-v7:4595f1c4a28cfa083b6c0920ad4d49e1c2ca4b8302a955e548f68eb63b74931b',
+			'com.jpardogo.materialtabstrip:library:24d19232b319f8c73e25793432357919a7ed972186f57a3b2c9093ea74ad8311',
+			'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b',
 	]
 }
 
diff --git a/briar-android/proguard-rules.txt b/briar-android/proguard-rules.txt
index 6a936b7e88129ba55c61805ce199eca7d83e41e7..94cfcea727efe8469baa5c5f3947d2e52ca9ca66 100644
--- a/briar-android/proguard-rules.txt
+++ b/briar-android/proguard-rules.txt
@@ -60,3 +60,8 @@
 -dontwarn java.nio.**
 -dontwarn org.codehaus.mojo.animal_sniffer.**
 -dontwarn org.slf4j.impl.**
+
+-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
+    **[] $VALUES;
+    public *;
+}
\ No newline at end of file
diff --git a/briar-android/res/drawable/ic_backspace_black.xml b/briar-android/res/drawable/ic_backspace_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b0224405a66c0fc1972b4469daecd4cd11c8db6a
--- /dev/null
+++ b/briar-android/res/drawable/ic_backspace_black.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:alpha="0.54"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M22,3L7,3c-0.69,0 -1.23,0.35 -1.59,0.88L0,12l5.41,8.11c0.36,0.53 0.9,0.89 1.59,0.89h15c1.1,0 2,-0.9 2,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM19,15.59L17.59,17 14,13.41 10.41,17 9,15.59 12.59,12 9,8.41 10.41,7 14,10.59 17.59,7 19,8.41 15.41,12 19,15.59z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_activity.xml b/briar-android/res/drawable/ic_emoji_activity.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cdec171106ede103ee3fe62d539818502c42b33d
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_activity.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M7.5,7.5C9.17,5.87 11.29,4.69 13.37,4.18C15.46,3.67 17.5,3.83 18.6,4C19.71,4.15 19.87,4.31 20.03,5.41C20.18,6.5 20.33,8.55 19.82,10.63C19.31,12.71 18.13,14.83 16.5,16.5C14.83,18.13 12.71,19.31 10.63,19.82C8.55,20.33 6.5,20.18 5.41,20.03C4.31,19.87 4.15,19.71 4,18.6C3.83,17.5 3.67,15.46 4.18,13.37C4.69,11.29 5.87,9.17 7.5,7.5M7.3,15.79L8.21,16.7L9.42,15.5L10.63,16.7L11.54,15.79L10.34,14.58L12,12.91L13.21,14.12L14.12,13.21L12.91,12L14.58,10.34L15.79,11.54L16.7,10.63L15.5,9.42L16.7,8.21L15.79,7.3L14.58,8.5L13.37,7.3L12.46,8.21L13.66,9.42L12,11.09L10.79,9.88L9.88,10.79L11.09,12L9.42,13.66L8.21,12.46L7.3,13.37L8.5,14.58L7.3,15.79Z"/>
+</vector>
\ No newline at end of file
diff --git a/briar-android/res/drawable/ic_emoji_animals_nature.xml b/briar-android/res/drawable/ic_emoji_animals_nature.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d63c313653b45af47964eb454c7f1c77683becc1
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_animals_nature.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M18.7,12.4c-0.28,-0.16 -0.57,-0.29 -0.86,-0.4 0.29,-0.11 0.58,-0.24 0.86,-0.4 1.92,-1.11 2.99,-3.12 3,-5.19 -1.79,-1.03 -4.07,-1.11 -6,0 -0.28,0.16 -0.54,0.35 -0.78,0.54 0.05,-0.31 0.08,-0.63 0.08,-0.95 0,-2.22 -1.21,-4.15 -3,-5.19C10.21,1.85 9,3.78 9,6c0,0.32 0.03,0.64 0.08,0.95 -0.24,-0.2 -0.5,-0.39 -0.78,-0.55 -1.92,-1.11 -4.2,-1.03 -6,0 0,2.07 1.07,4.08 3,5.19 0.28,0.16 0.57,0.29 0.86,0.4 -0.29,0.11 -0.58,0.24 -0.86,0.4 -1.92,1.11 -2.99,3.12 -3,5.19 1.79,1.03 4.07,1.11 6,0 0.28,-0.16 0.54,-0.35 0.78,-0.54 -0.05,0.32 -0.08,0.64 -0.08,0.96 0,2.22 1.21,4.15 3,5.19 1.79,-1.04 3,-2.97 3,-5.19 0,-0.32 -0.03,-0.64 -0.08,-0.95 0.24,0.2 0.5,0.38 0.78,0.54 1.92,1.11 4.2,1.03 6,0 -0.01,-2.07 -1.08,-4.08 -3,-5.19zM12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_emoticons.xml b/briar-android/res/drawable/ic_emoji_emoticons.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cb97f4e15aa631a1dfabfed6229c8521e0cd1d7f
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_emoticons.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M15.4839,3.8557 C14.8224,3.89611,14.9476,5.06823,15.4034,5.47978
+C16.7652,6.93176,17.1508,8.98786,17.299,10.9051
+C17.4081,13.1214,17.2144,15.4608,16.1275,17.4387
+C15.7657,18.1699,14.7879,18.7118,14.9747,19.6345
+C15.2618,20.6719,16.1617,19.8774,16.5955,19.4508
+C18.5872,17.7088,19.4632,15.0228,19.5268,12.4373
+C19.5469,10.5193,19.295,8.516,18.3141,6.8329
+C17.6499,5.74222,16.9234,4.59896,15.8167,3.91355
+C15.6887,3.86763,15.5784,3.84987,15.4839,3.85564 Z M5.91182,7.65831
+A1.3631614,1.3631614,0,0,0,4.54866,9.02147
+A1.3631614,1.3631614,0,0,0,5.91182,10.3846
+A1.3631614,1.3631614,0,0,0,7.27498,9.02147
+A1.3631614,1.3631614,0,0,0,5.91182,7.65831 Z M8.98492,10.6595 L8.98492,12.7042
+L13.756,12.7042 L13.756,10.6595 L8.98494,10.6595 Z M5.83527,14.1306
+A1.3631614,1.3631614,0,0,0,4.47211,15.4938
+A1.3631614,1.3631614,0,0,0,5.83527,16.857
+A1.3631614,1.3631614,0,0,0,7.19843,15.4938
+A1.3631614,1.3631614,0,0,0,5.83527,14.1306 Z"/>
+</vector>
\ No newline at end of file
diff --git a/briar-android/res/drawable/ic_emoji_flags.xml b/briar-android/res/drawable/ic_emoji_flags.xml
new file mode 100644
index 0000000000000000000000000000000000000000..68976f28c109287a74d3bb588877f69dc10275b7
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_flags.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M14.4,6L14,4H5v17h2v-7h5.6l0.4,2h7V6z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_food_drink.xml b/briar-android/res/drawable/ic_emoji_food_drink.xml
new file mode 100644
index 0000000000000000000000000000000000000000..958664070d98b1b8993907540d94dedf0a709ea6
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_food_drink.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M12,6c1.11,0 2,-0.9 2,-2 0,-0.38 -0.1,-0.73 -0.29,-1.03L12,0l-1.71,2.97c-0.19,0.3 -0.29,0.65 -0.29,1.03 0,1.1 0.9,2 2,2zM16.6,15.99l-1.07,-1.07 -1.08,1.07c-1.3,1.3 -3.58,1.31 -4.89,0l-1.07,-1.07 -1.09,1.07C6.75,16.64 5.88,17 4.96,17c-0.73,0 -1.4,-0.23 -1.96,-0.61L3,21c0,0.55 0.45,1 1,1h16c0.55,0 1,-0.45 1,-1v-4.61c-0.56,0.38 -1.23,0.61 -1.96,0.61 -0.92,0 -1.79,-0.36 -2.44,-1.01zM18,9h-5L13,7h-2v2L6,9c-1.66,0 -3,1.34 -3,3v1.54c0,1.08 0.88,1.96 1.96,1.96 0.52,0 1.02,-0.2 1.38,-0.57l2.14,-2.13 2.13,2.13c0.74,0.74 2.03,0.74 2.77,0l2.14,-2.13 2.13,2.13c0.37,0.37 0.86,0.57 1.38,0.57 1.08,0 1.96,-0.88 1.96,-1.96L20.99,12C21,10.34 19.66,9 18,9z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_objects.xml b/briar-android/res/drawable/ic_emoji_objects.xml
new file mode 100644
index 0000000000000000000000000000000000000000..70f0ec6ea51310594bbfbcc54adce7d05f00b457
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_objects.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24"
+        android:viewportWidth="24">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M5,16L3,5L8.5,12L12,5L15.5,12L21,5L19,16H5M19,19A1,1 0 0,1 18,20H6A1,1 0 0,1 5,19V18H19V19Z"/>
+</vector>
\ No newline at end of file
diff --git a/briar-android/res/drawable/ic_emoji_recent.xml b/briar-android/res/drawable/ic_emoji_recent.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3f891e2e43241fd849de538faea493738eed1521
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_recent.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_smiley_people.xml b/briar-android/res/drawable/ic_emoji_smiley_people.xml
new file mode 100644
index 0000000000000000000000000000000000000000..12dfdcd2e065a22ef7e5262179ea16bfb4d90e0f
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_smiley_people.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_symbols.xml b/briar-android/res/drawable/ic_emoji_symbols.xml
new file mode 100644
index 0000000000000000000000000000000000000000..28d8ff8cf50e639137b4cb93a06335230a7af4c1
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_symbols.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M12,7.77L18.39,18H5.61L12,7.77M12,4L2,20h20L12,4z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_toggle.xml b/briar-android/res/drawable/ic_emoji_toggle.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b497d47fd3d7181ab112b06ed61be9abc1840ff8
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_toggle.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:alpha="0.54"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5L6.89,14c0.8,2.04 2.78,3.5 5.11,3.5z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_emoji_travel_places.xml b/briar-android/res/drawable/ic_emoji_travel_places.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a0534fbbfc6d4b6a7df18f7caaf2239f6c09e7ed
--- /dev/null
+++ b/briar-android/res/drawable/ic_emoji_travel_places.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF2D3E50"
+		android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5h-11c-0.66,0 -1.21,0.42 -1.42,1.01L3,12v8c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h12v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-8l-2.08,-5.99zM6.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,13 6.5,13s1.5,0.67 1.5,1.5S7.33,16 6.5,16zM17.5,16c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM5,11l1.5,-4.5h11L19,11L5,11z"/>
+</vector>
diff --git a/briar-android/res/drawable/ic_keyboard_black.xml b/briar-android/res/drawable/ic_keyboard_black.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f1f26791ab498485f2d619a3e0477ce9a1bbe28d
--- /dev/null
+++ b/briar-android/res/drawable/ic_keyboard_black.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:alpha="0.54"
+        android:viewportHeight="24.0"
+        android:viewportWidth="24.0">
+	<path
+		android:fillColor="#FF000000"
+		android:pathData="M20,5L4,5c-1.1,0 -1.99,0.9 -1.99,2L2,17c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,7c0,-1.1 -0.9,-2 -2,-2zM11,8h2v2h-2L11,8zM11,11h2v2h-2v-2zM8,8h2v2L8,10L8,8zM8,11h2v2L8,13v-2zM7,13L5,13v-2h2v2zM7,10L5,10L5,8h2v2zM16,17L8,17v-2h8v2zM16,13h-2v-2h2v2zM16,10h-2L14,8h2v2zM19,13h-2v-2h2v2zM19,10h-2L17,8h2v2z"/>
+</vector>
diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml
index 8f0fa1a349091f735aea2c492be12237dded4a8f..cfd7cd4d27cd249b541dd1c0e7618adc9f300008 100644
--- a/briar-android/res/layout/activity_conversation.xml
+++ b/briar-android/res/layout/activity_conversation.xml
@@ -3,9 +3,9 @@
 	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:orientation="vertical"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
+	android:orientation="vertical"
 	tools:context=".android.contact.ConversationActivity">
 
 	<android.support.v7.widget.Toolbar
@@ -38,17 +38,18 @@
 
 	</android.support.v7.widget.Toolbar>
 
-	<org.briarproject.android.util.BriarRecyclerView
+	<org.briarproject.android.view.BriarRecyclerView
 		android:id="@+id/conversationView"
 		android:layout_width="match_parent"
 		android:layout_height="0dp"
-		android:layout_weight="1"/>
+		android:layout_weight="1"
+		android:background="@color/window_background"/>
 
-	<View style="@style/Divider.Horizontal"/>
-
-	<include
-		layout="@layout/text_input_field"
+	<org.briarproject.android.view.TextInputView
+		android:id="@+id/text_input_container"
 		android:layout_width="match_parent"
-		android:layout_height="wrap_content"/>
+		android:layout_height="wrap_content"
+		android:background="@color/button_bar_background"
+		android:elevation="@dimen/margin_tiny"/>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml
index 946ce34e8a3ac99193729a6f6354ab540935b1ca..f8e1b609ce4c0084c6b8bd6586e375c24289a1c5 100644
--- a/briar-android/res/layout/activity_forum.xml
+++ b/briar-android/res/layout/activity_forum.xml
@@ -6,16 +6,18 @@
 	android:layout_height="match_parent"
 	android:orientation="vertical">
 
-	<org.briarproject.android.util.BriarRecyclerView
+	<org.briarproject.android.view.BriarRecyclerView
 		android:id="@+id/forum_discussion_list"
 		android:layout_width="match_parent"
 		android:layout_height="0dp"
 		android:layout_weight="1"
 		app:scrollToEnd="false"/>
 
-	<include
-		layout="@layout/text_input_field"
+	<org.briarproject.android.view.TextInputView
+		android:id="@+id/text_input_container"
 		android:layout_width="match_parent"
-		android:layout_height="wrap_content"/>
+		android:layout_height="wrap_content"
+		android:background="@color/button_bar_background"
+		android:elevation="@dimen/margin_tiny"/>
 
 </LinearLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/activity_invitations.xml b/briar-android/res/layout/activity_invitations.xml
index 25cd76af43f190df62a3fb69ecae4705af87b7f0..4d3d61cea8a12c6d62b84193ee0866489f6d54e3 100644
--- a/briar-android/res/layout/activity_invitations.xml
+++ b/briar-android/res/layout/activity_invitations.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.util.BriarRecyclerView
+<org.briarproject.android.view.BriarRecyclerView
 	android:id="@+id/invitationsView"
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
diff --git a/briar-android/res/layout/activity_rss_feed_manage.xml b/briar-android/res/layout/activity_rss_feed_manage.xml
index a9fc464d9c1b0ec17177d723c7d397d86babe579..8c9a0c468181d7e78c4aa1c33c51476494017d0a 100644
--- a/briar-android/res/layout/activity_rss_feed_manage.xml
+++ b/briar-android/res/layout/activity_rss_feed_manage.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.util.BriarRecyclerView
+<org.briarproject.android.view.BriarRecyclerView
 	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"
diff --git a/briar-android/res/layout/activity_sharing_status.xml b/briar-android/res/layout/activity_sharing_status.xml
index efc4a8fc0890c75e89ca787d6de43e31f0a2e34d..48d2b7263a2bd3e27d48d5728c7fa43e81f5ebaf 100644
--- a/briar-android/res/layout/activity_sharing_status.xml
+++ b/briar-android/res/layout/activity_sharing_status.xml
@@ -19,7 +19,7 @@
 
 		<View style="@style/Divider.ForumList"/>
 
-		<org.briarproject.android.util.BriarRecyclerView
+		<org.briarproject.android.view.BriarRecyclerView
 			android:id="@+id/sharedByView"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
@@ -36,7 +36,7 @@
 
 		<View style="@style/Divider.ForumList"/>
 
-		<org.briarproject.android.util.BriarRecyclerView
+		<org.briarproject.android.view.BriarRecyclerView
 			android:id="@+id/sharedWithView"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/author_view.xml b/briar-android/res/layout/author_view.xml
index 405376fac9b46eeec2780eddbf3e44d9a356dc9c..42a5655a5062b11ebff29ef6bfd4b28a74438e90 100644
--- a/briar-android/res/layout/author_view.xml
+++ b/briar-android/res/layout/author_view.xml
@@ -37,7 +37,7 @@
 		android:textSize="@dimen/text_size_small"
 		tools:text="Author Name"/>
 
-	<org.briarproject.android.util.TrustIndicatorView
+	<org.briarproject.android.view.TrustIndicatorView
 		android:id="@+id/trustIndicator"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/emoji_drawer.xml b/briar-android/res/layout/emoji_drawer.xml
new file mode 100644
index 0000000000000000000000000000000000000000..37f3723339a2cf728ecbe4ae61d59f5916275cce
--- /dev/null
+++ b/briar-android/res/layout/emoji_drawer.xml
@@ -0,0 +1,53 @@
+<?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">
+
+	<View
+		style="@style/Divider.Horizontal"/>
+
+	<LinearLayout
+		android:layout_width="match_parent"
+		android:layout_height="40dp"
+		android:background="@color/emoji_pager_background"
+		android:orientation="horizontal">
+
+		<com.astuetz.PagerSlidingTabStrip
+			android:id="@+id/tabs"
+			android:layout_width="0dp"
+			android:layout_height="match_parent"
+			android:layout_weight="1"
+			app:pstsIndicatorColor="@color/briar_accent"
+			app:pstsIndicatorHeight="@dimen/emoji_drawer_indicator_height"
+			app:pstsShouldExpand="true"
+			app:pstsTabPaddingLeftRight="@dimen/emoji_drawer_left_right_padding"/>
+
+		<View
+			android:layout_width="@dimen/margin_separator"
+			android:layout_height="match_parent"
+			android:layout_marginBottom="10dp"
+			android:layout_marginTop="10dp"
+			android:background="@color/divider"/>
+
+		<org.thoughtcrime.securesms.components.RepeatableImageKey
+			android:id="@+id/backspace"
+			android:layout_width="wrap_content"
+			android:layout_height="match_parent"
+			android:background="@color/emoji_pager_background"
+			android:paddingLeft="@dimen/margin_medium"
+			android:paddingRight="@dimen/margin_medium"
+			android:src="@drawable/ic_backspace_black"/>
+
+	</LinearLayout>
+
+	<View
+		style="@style/Divider.Horizontal"/>
+
+	<android.support.v4.view.ViewPager
+		android:id="@+id/emoji_pager"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:background="@color/window_background"
+		android:visibility="visible"/>
+
+</merge>
diff --git a/briar-android/res/layout/emoji_grid_layout.xml b/briar-android/res/layout/emoji_grid_layout.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c1486e2dcb94a9f7ff14ce5dc7bc4504528372da
--- /dev/null
+++ b/briar-android/res/layout/emoji_grid_layout.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<GridView
+		android:id="@+id/emoji"
+		android:layout_width="fill_parent"
+		android:layout_height="match_parent"
+		android:columnWidth="@dimen/emoji_drawer_size"
+		android:gravity="center"
+		android:horizontalSpacing="0dp"
+		android:numColumns="auto_fit"
+		android:stretchMode="columnWidth"
+		android:verticalSpacing="0dp"
+		android:visibility="visible"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/briar-android/res/layout/fragment_blog.xml b/briar-android/res/layout/fragment_blog.xml
index c0ac19fbe0eac0b76817a2c7bc9bb8566f6aa840..bfc1c777a24cf170c6addd1cdc0c37b1ebe7e810 100644
--- a/briar-android/res/layout/fragment_blog.xml
+++ b/briar-android/res/layout/fragment_blog.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.util.BriarRecyclerView
+<org.briarproject.android.view.BriarRecyclerView
 	android:id="@+id/postList"
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:app="http://schemas.android.com/apk/res-auto"
diff --git a/briar-android/res/layout/fragment_contact_list.xml b/briar-android/res/layout/fragment_contact_list.xml
index 787d4dff960f06c4f8d8b0c4d56fe73aa61baef0..de3bddebe14b129d135d400ace09f2d6081a9f4e 100644
--- a/briar-android/res/layout/fragment_contact_list.xml
+++ b/briar-android/res/layout/fragment_contact_list.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.util.BriarRecyclerView
+<org.briarproject.android.view.BriarRecyclerView
 		xmlns:android="http://schemas.android.com/apk/res/android"
 		android:id="@+id/contactList"
 		android:layout_width="match_parent"
diff --git a/briar-android/res/layout/fragment_forum_list.xml b/briar-android/res/layout/fragment_forum_list.xml
index bb4e4ae761bc2d706a7e8e21636305f5271ba60e..ca4fb452052bd7180b1b4c9dc4e9f63a454e97de 100644
--- a/briar-android/res/layout/fragment_forum_list.xml
+++ b/briar-android/res/layout/fragment_forum_list.xml
@@ -5,11 +5,11 @@
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
-	<org.briarproject.android.util.BriarRecyclerView
+	<org.briarproject.android.view.BriarRecyclerView
 		android:id="@+id/forumList"
 		xmlns:android="http://schemas.android.com/apk/res/android"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent"
-		app:layout_behavior="org.briarproject.android.util.BriarRecyclerViewBehavior"/>
+		app:layout_behavior="org.briarproject.android.view.BriarRecyclerViewBehavior"/>
 
 </android.support.design.widget.CoordinatorLayout>
diff --git a/briar-android/res/layout/fragment_keyagreement_qr.xml b/briar-android/res/layout/fragment_keyagreement_qr.xml
index 5f1b175ed567f5c794729da1aa04516f2996a9c6..993045ef9b3b78d10dc2193a86e9c18cad8a8024 100644
--- a/briar-android/res/layout/fragment_keyagreement_qr.xml
+++ b/briar-android/res/layout/fragment_keyagreement_qr.xml
@@ -5,7 +5,7 @@
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
-	<org.briarproject.android.util.CameraView
+	<org.briarproject.android.view.CameraView
 		android:id="@+id/camera_view"
 		android:layout_width="match_parent"
 		android:layout_height="match_parent"/>
diff --git a/briar-android/res/layout/introduction_contact_chooser.xml b/briar-android/res/layout/introduction_contact_chooser.xml
index 8363191ac0db145c3086706f2f9408fbc479a1f6..4a99ecbed9378374dfb47cf3e3d355d52e564569 100644
--- a/briar-android/res/layout/introduction_contact_chooser.xml
+++ b/briar-android/res/layout/introduction_contact_chooser.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.briarproject.android.util.BriarRecyclerView
+<org.briarproject.android.view.BriarRecyclerView
 	xmlns:android="http://schemas.android.com/apk/res/android"
 	xmlns:tools="http://schemas.android.com/tools"
 	android:id="@+id/contactList"
diff --git a/briar-android/res/layout/list_item_blog.xml b/briar-android/res/layout/list_item_blog.xml
index 1fe1f22db024383b68baea541fbfea8e11eb88e3..24127ced5f70ac916727cc76d014aaff267fbb4f 100644
--- a/briar-android/res/layout/list_item_blog.xml
+++ b/briar-android/res/layout/list_item_blog.xml
@@ -8,7 +8,7 @@
 	android:layout_marginStart="@dimen/listitem_horizontal_margin"
 	android:background="?attr/selectableItemBackground">
 
-	<org.briarproject.android.util.TextAvatarView
+	<org.briarproject.android.view.TextAvatarView
 		android:id="@+id/avatarView"
 		android:layout_width="@dimen/listitem_picture_frame_size"
 		android:layout_height="@dimen/listitem_picture_frame_size"
diff --git a/briar-android/res/layout/list_item_blog_comment.xml b/briar-android/res/layout/list_item_blog_comment.xml
index 1be478ab2fd9f60b3e7235034c1ee8f9e52544d3..c6ff62916ed93058a1951af0010a87fe242242ce 100644
--- a/briar-android/res/layout/list_item_blog_comment.xml
+++ b/briar-android/res/layout/list_item_blog_comment.xml
@@ -11,7 +11,7 @@
 		android:id="@+id/inputDivider"
 		style="@style/Divider.Horizontal"/>
 
-	<org.briarproject.android.util.AuthorView
+	<org.briarproject.android.view.AuthorView
 		android:id="@+id/authorView"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/list_item_blog_post.xml b/briar-android/res/layout/list_item_blog_post.xml
index d3cd14d489ab70c9b6dced4a313af848eb68807a..0b6803eedb1a9cb792e133d6f76220d016ac8173 100644
--- a/briar-android/res/layout/list_item_blog_post.xml
+++ b/briar-android/res/layout/list_item_blog_post.xml
@@ -19,7 +19,7 @@
 			android:layout_height="wrap_content"
 			android:padding="@dimen/listitem_vertical_margin">
 
-			<org.briarproject.android.util.AuthorView
+			<org.briarproject.android.view.AuthorView
 				android:id="@+id/rebloggerView"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
@@ -29,7 +29,7 @@
 				android:layout_toLeftOf="@+id/commentView"
 				app:persona="reblogger"/>
 
-			<org.briarproject.android.util.AuthorView
+			<org.briarproject.android.view.AuthorView
 				android:id="@+id/authorView"
 				android:layout_width="wrap_content"
 				android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/list_item_forum.xml b/briar-android/res/layout/list_item_forum.xml
index 0652bec6b01927253e4bbe35986de7a1ace3cb24..3dc74083fe11a6aed64df05066e110193ce5ff27 100644
--- a/briar-android/res/layout/list_item_forum.xml
+++ b/briar-android/res/layout/list_item_forum.xml
@@ -8,7 +8,7 @@
 	android:layout_marginStart="@dimen/listitem_horizontal_margin"
 	android:background="?attr/selectableItemBackground">
 
-	<org.briarproject.android.util.TextAvatarView
+	<org.briarproject.android.view.TextAvatarView
 		android:id="@+id/avatarView"
 		android:layout_width="@dimen/listitem_picture_frame_size"
 		android:layout_height="@dimen/listitem_picture_frame_size"
diff --git a/briar-android/res/layout/forum_discussion_cell.xml b/briar-android/res/layout/list_item_forum_post.xml
similarity index 72%
rename from briar-android/res/layout/forum_discussion_cell.xml
rename to briar-android/res/layout/list_item_forum_post.xml
index fc6e1a20adae36e573632e38439200360b3eb671..71b64b3194ecf611b172c665f6fbaacbf5f12708 100644
--- a/briar-android/res/layout/forum_discussion_cell.xml
+++ b/briar-android/res/layout/list_item_forum_post.xml
@@ -72,7 +72,7 @@
 		android:layout_marginLeft="@dimen/margin_medium"
 		android:layout_weight="1">
 
-		<TextView
+		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 			android:id="@+id/text"
 			android:layout_width="match_parent"
 			android:layout_height="wrap_content"
@@ -85,59 +85,13 @@
 			android:textColor="@color/briar_text_primary"
 			tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."/>
 
-		<de.hdodenhof.circleimageview.CircleImageView
-			android:id="@+id/avatar"
-			android:layout_width="@dimen/forum_avatar_size"
-			android:layout_height="@dimen/forum_avatar_size"
-			android:layout_alignLeft="@id/text"
-			android:layout_below="@id/text"
-			android:layout_marginRight="@dimen/margin_small"
-			android:layout_marginTop="@dimen/margin_small"
-			android:src="@drawable/ic_launcher"
-			app:civ_border_color="@color/briar_primary"
-			app:civ_border_width="@dimen/avatar_border_width"
-			tools:src="@drawable/ic_launcher"
-			/>
-
-		<TextView
+		<org.briarproject.android.view.AuthorView
 			android:id="@+id/author"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:layout_toRightOf="@id/avatar"
-			android:layout_alignBottom="@+id/avatar"
-			android:layout_alignTop="@+id/avatar"
-			android:gravity="center"
-			android:ellipsize="end"
-			android:maxLines="1"
-			android:textSize="@dimen/text_size_tiny"
-			tools:text="John Smith"/>
-
-		<org.briarproject.android.util.TrustIndicatorView
-			android:id="@+id/trustIndicator"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_alignBottom="@+id/avatar"
-			android:layout_alignTop="@+id/avatar"
-			android:scaleType="center"
-			android:layout_marginLeft="@dimen/margin_small"
-			android:layout_marginStart="@dimen/margin_small"
-			android:layout_toRightOf="@+id/author"
-			tools:src="@drawable/trust_indicator_verified"/>
-
-		<TextView
-			android:id="@+id/date"
-			android:layout_width="wrap_content"
-			android:layout_height="wrap_content"
-			android:layout_alignBottom="@+id/avatar"
-			android:layout_alignTop="@+id/avatar"
-			android:gravity="center"
-			android:layout_toRightOf="@+id/trustIndicator"
-			android:layout_marginLeft="@dimen/margin_small"
-			android:layout_marginStart="@dimen/margin_small"
-			android:ellipsize="end"
-			android:maxLines="1"
-			android:textSize="@dimen/text_size_tiny"
-			tools:text="09:09"/>
+			android:layout_alignLeft="@id/text"
+			android:layout_below="@id/text"
+			app:persona="commenter"/>
 
 		<ImageView
 			android:id="@+id/chevron"
diff --git a/briar-android/res/layout/list_item_introduction_in.xml b/briar-android/res/layout/list_item_introduction_in.xml
index b3a74b6c9b3fb698c00dde72a18e836be9725162..de94d9d788c1a6e020a84cbad177c3ddde4ee573 100644
--- a/briar-android/res/layout/list_item_introduction_in.xml
+++ b/briar-android/res/layout/list_item_introduction_in.xml
@@ -6,7 +6,7 @@
 	android:layout_height="wrap_content"
 	android:orientation="vertical">
 
-	<TextView
+	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 		android:id="@+id/msgBody"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/list_item_invitations.xml b/briar-android/res/layout/list_item_invitations.xml
index 536c8080d1e09cacf217117ae9f26e811244ebd4..6cfa235b6bf06e6ecd475d5396cb955d102f3a1f 100644
--- a/briar-android/res/layout/list_item_invitations.xml
+++ b/briar-android/res/layout/list_item_invitations.xml
@@ -9,7 +9,7 @@
 	android:background="?attr/selectableItemBackground"
 	android:paddingTop="@dimen/listitem_horizontal_margin">
 
-	<org.briarproject.android.util.TextAvatarView
+	<org.briarproject.android.view.TextAvatarView
 		android:id="@+id/avatarView"
 		android:layout_width="@dimen/listitem_picture_frame_size"
 		android:layout_height="@dimen/listitem_picture_frame_size"
diff --git a/briar-android/res/layout/list_item_msg_in.xml b/briar-android/res/layout/list_item_msg_in.xml
index 1858c1cc8693786c933093d20379bcc5425d6b9f..3c18ca1a88c15d4e3f2c22a97c109343c68c6f43 100644
--- a/briar-android/res/layout/list_item_msg_in.xml
+++ b/briar-android/res/layout/list_item_msg_in.xml
@@ -10,7 +10,7 @@
 	android:background="@drawable/msg_in"
 	android:orientation="vertical">
 
-	<TextView
+	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 		android:id="@+id/msgBody"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/list_item_msg_notice_out.xml b/briar-android/res/layout/list_item_msg_notice_out.xml
index 1418e0603251ad97eef8c11e490bf780e3cce331..6efb99c523316d9da267f1e44d9f2a9231a537a0 100644
--- a/briar-android/res/layout/list_item_msg_notice_out.xml
+++ b/briar-android/res/layout/list_item_msg_notice_out.xml
@@ -6,7 +6,7 @@
 	android:layout_height="wrap_content"
 	android:orientation="vertical">
 
-	<TextView
+	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 		android:id="@+id/msgBody"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/list_item_msg_out.xml b/briar-android/res/layout/list_item_msg_out.xml
index 34de5a9dd240b30144d90f96dce0be5aa806b15e..7b7f4f65cee607313a5bee6079cd26806337d3e3 100644
--- a/briar-android/res/layout/list_item_msg_out.xml
+++ b/briar-android/res/layout/list_item_msg_out.xml
@@ -15,7 +15,7 @@
 		android:layout_marginRight="@dimen/message_bubble_margin_tail"
 		android:background="@drawable/msg_out">
 
-		<TextView
+		<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 			android:id="@+id/msgBody"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/list_item_shareable_invitation_in.xml b/briar-android/res/layout/list_item_shareable_invitation_in.xml
index 5d707e3612fdd95c74ddfcb3c0189a466596e57a..f70ac34a0e89570d675c862a7f72c03c2fa83b21 100644
--- a/briar-android/res/layout/list_item_shareable_invitation_in.xml
+++ b/briar-android/res/layout/list_item_shareable_invitation_in.xml
@@ -6,7 +6,7 @@
 	android:layout_height="wrap_content"
 	android:orientation="vertical">
 
-	<TextView
+	<org.thoughtcrime.securesms.components.emoji.EmojiTextView
 		android:id="@+id/msgBody"
 		android:layout_width="match_parent"
 		android:layout_height="wrap_content"
diff --git a/briar-android/res/layout/text_input_field.xml b/briar-android/res/layout/text_input_field.xml
deleted file mode 100644
index 8b60f7b449e150f74565d0a7a7df7048f94347b8..0000000000000000000000000000000000000000
--- a/briar-android/res/layout/text_input_field.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-	android:id="@+id/text_input_container"
-	xmlns:android="http://schemas.android.com/apk/res/android"
-	android:layout_width="match_parent"
-	android:layout_height="wrap_content"
-	android:background="@color/button_bar_background"
-	android:elevation="@dimen/margin_tiny"
-	android:gravity="bottom"
-	android:orientation="horizontal"
-	android:paddingLeft="@dimen/margin_large"
-	android:paddingStart="@dimen/margin_large">
-
-	<EditText
-		android:id="@+id/input_text"
-		android:layout_width="0dp"
-		android:layout_height="wrap_content"
-		android:layout_marginTop="@dimen/margin_small"
-		android:layout_weight="1"
-		android:inputType="textMultiLine|textCapSentences"
-		android:maxLines="5"/>
-
-	<ImageView
-		android:id="@+id/btn_send"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_margin="@dimen/margin_medium"
-		android:background="?attr/selectableItemBackground"
-		android:clickable="true"
-		android:contentDescription="@string/send"
-		android:onClick="sendMessage"
-		android:src="@drawable/social_send_now_white"
-		android:tint="@color/briar_primary"
-		/>
-
-</LinearLayout>
diff --git a/briar-android/res/layout/text_input_view.xml b/briar-android/res/layout/text_input_view.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6b1aeba51528e188c326dbc208ba59b8868e9aef
--- /dev/null
+++ b/briar-android/res/layout/text_input_view.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge
+	xmlns:android="http://schemas.android.com/apk/res/android"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	tools:showIn="@layout/activity_conversation">
+
+	<View
+		style="@style/Divider.Horizontal"
+		android:layout_alignParentTop="true"/>
+
+	<org.thoughtcrime.securesms.components.emoji.EmojiToggle
+		android:id="@+id/emoji_toggle"
+		android:layout_width="42dp"
+		android:layout_height="42dp"
+		android:layout_above="@+id/emoji_drawer"
+		android:layout_alignBottom="@+id/input_text"
+		android:layout_alignParentLeft="true"
+		android:background="?attr/selectableItemBackground"
+		android:padding="@dimen/margin_small"
+		android:scaleType="center"/>
+
+	<org.thoughtcrime.securesms.components.emoji.EmojiEditText
+		android:id="@+id/input_text"
+		android:layout_width="wrap_content"
+		android:layout_height="wrap_content"
+		android:layout_toLeftOf="@+id/btn_send"
+		android:layout_toRightOf="@+id/emoji_toggle"
+		android:inputType="textMultiLine|textCapSentences"
+		android:maxLines="5"
+		android:minHeight="42dp"/>
+
+	<ImageButton
+		android:id="@+id/btn_send"
+		android:layout_width="wrap_content"
+		android:layout_height="42dp"
+		android:layout_above="@+id/emoji_drawer"
+		android:layout_alignBottom="@+id/input_text"
+		android:layout_alignParentRight="true"
+		android:background="?attr/selectableItemBackground"
+		android:clickable="true"
+		android:contentDescription="@string/send"
+		android:enabled="false"
+		android:padding="@dimen/margin_small"
+		android:src="@drawable/social_send_now_white"
+		android:tint="@color/briar_primary"/>
+
+	<org.thoughtcrime.securesms.components.emoji.EmojiDrawer
+		android:id="@+id/emoji_drawer"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_below="@+id/input_text"
+		android:visibility="gone"/>
+
+</merge>
diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml
index 0ac3140eeef7b00dadaa8cddaf3302d4d34c22eb..daf3f8b3b2b07393fc0fb0993dd6931ac21b25ee 100644
--- a/briar-android/res/values/color.xml
+++ b/briar-android/res/values/color.xml
@@ -27,6 +27,9 @@
 	<color name="briar_text_tertiary_inverse">#80ffffff</color>
 	<color name="briar_button_positive">#06b9ff</color>
 	<color name="briar_button_negative">#ff0000</color>
+	<color name="emoji_text_color">#ff000000</color>
+
+	<color name="emoji_pager_background">@color/window_background</color>
 
 	<!-- this is needed as preference_category_material layout uses this color as the text color -->
 	<color name="preference_fallback_accent_color">@color/briar_accent</color>
diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml
index c8e7eca7d0dfa6c3a8b2a51123547edf60fb748c..f09530cc4b1300b0e72b533f7e3177196ed7a571 100644
--- a/briar-android/res/values/dimens.xml
+++ b/briar-android/res/values/dimens.xml
@@ -46,4 +46,17 @@
 	<dimen name="blogs_avatar_icon_size">15dp</dimen>
 	<dimen name="blogs_avatar_comment_size">20dp</dimen>
 
+	<!-- Emoji -->
+	<dimen name="conversation_item_body_text_size">16sp</dimen>
+	<dimen name="emoji_drawer_size">32sp</dimen>
+	<dimen name="emoji_drawer_indicator_height">2dp</dimen>
+	<dimen name="emoji_drawer_item_padding">5dp</dimen>
+	<dimen name="emoji_drawer_left_right_padding">2dp</dimen>
+
+	<!-- Keyboard Sizes -->
+	<dimen name="min_keyboard_size">50dp</dimen>
+	<dimen name="default_custom_keyboard_size">220dp</dimen>
+	<dimen name="min_custom_keyboard_size">110dp</dimen>
+	<dimen name="min_custom_keyboard_top_margin">170dp</dimen>
+
 </resources>
diff --git a/briar-android/res/values/emoji.xml b/briar-android/res/values/emoji.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ab8e62d011f28e504cc41b3f8c78d371226021d0
--- /dev/null
+++ b/briar-android/res/values/emoji.xml
@@ -0,0 +1,1352 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <array
+        name="emoji_symbols"
+        format="string">
+        <item>1f3c1</item>
+        <item>1f3f3</item>
+        <item>1f3f4</item>
+        <item>1f6a9</item>
+        <item>1f3e7</item>
+        <item>1f6ae</item>
+        <item>1f6b0</item>
+        <item>267f</item>
+        <item>1f6b9</item>
+        <item>1f6ba</item>
+        <item>1f6bb</item>
+        <item>1f6bc</item>
+        <item>1f6be</item>
+        <item>1f6c2</item>
+        <item>1f6c3</item>
+        <item>1f6c4</item>
+        <item>1f6c5</item>
+        <item>26a0</item>
+        <item>1f6b8</item>
+        <item>26d4</item>
+        <item>1f6ab</item>
+        <item>1f6b3</item>
+        <item>1f6ad</item>
+        <item>1f6af</item>
+        <item>1f6b1</item>
+        <item>1f6b7</item>
+        <item>2622</item>
+        <item>2623</item>
+        <item>2b06</item>
+        <item>2197</item>
+        <item>27a1</item>
+        <item>2198</item>
+        <item>2b07</item>
+        <item>2199</item>
+        <item>2b05</item>
+        <item>2196</item>
+        <item>2195</item>
+        <item>2194</item>
+        <item>21a9</item>
+        <item>21aa</item>
+        <item>2934</item>
+        <item>2935</item>
+        <item>1f503</item>
+        <item>1f504</item>
+        <item>1f519</item>
+        <item>1f51a</item>
+        <item>1f51b</item>
+        <item>1f51c</item>
+        <item>1f51d</item>
+        <item>1f6d0</item>
+        <item>269b</item>
+        <item>1f549</item>
+        <item>2721</item>
+        <item>2638</item>
+        <item>262f</item>
+        <item>271d</item>
+        <item>2626</item>
+        <item>262a</item>
+        <item>262e</item>
+        <item>1f54e</item>
+        <item>1f52f</item>
+        <item>267b</item>
+        <item>1f4db</item>
+        <item>269c</item>
+        <item>1f530</item>
+        <item>1f531</item>
+        <item>2b55</item>
+        <item>2705</item>
+        <item>2611</item>
+        <item>2714</item>
+        <item>2716</item>
+        <item>274c</item>
+        <item>274e</item>
+        <item>2795</item>
+        <item>2796</item>
+        <item>2797</item>
+        <item>27b0</item>
+        <item>27bf</item>
+        <item>303d</item>
+        <item>2733</item>
+        <item>2734</item>
+        <item>2747</item>
+        <item>1f4b1</item>
+        <item>1f4b2</item>
+        <item>203c</item>
+        <item>2049</item>
+        <item>2753</item>
+        <item>2754</item>
+        <item>2755</item>
+        <item>2757</item>
+        <item>3030</item>
+        <item>a9</item>
+        <item>ae</item>
+        <item>2122</item>
+        <item>2648</item>
+        <item>2649</item>
+        <item>264a</item>
+        <item>264b</item>
+        <item>264c</item>
+        <item>264d</item>
+        <item>264e</item>
+        <item>264f</item>
+        <item>2650</item>
+        <item>2651</item>
+        <item>2652</item>
+        <item>2653</item>
+        <item>26ce</item>
+        <item>1f500</item>
+        <item>1f501</item>
+        <item>1f502</item>
+        <item>25b6</item>
+        <item>23e9</item>
+        <item>23ed</item>
+        <item>23ef</item>
+        <item>25c0</item>
+        <item>23ea</item>
+        <item>23ee</item>
+        <item>1f53c</item>
+        <item>23eb</item>
+        <item>1f53d</item>
+        <item>23ec</item>
+        <item>23f8</item>
+        <item>23f9</item>
+        <item>23fa</item>
+        <item>23cf</item>
+        <item>1f3a6</item>
+        <item>1f505</item>
+        <item>1f506</item>
+        <item>1f4f6</item>
+        <item>1f4f5</item>
+        <item>1f4f3</item>
+        <item>1f4f4</item>
+        <item>23,20e3</item>
+        <item>2a,20e3</item>
+        <item>30,20e3</item>
+        <item>31,20e3</item>
+        <item>32,20e3</item>
+        <item>33,20e3</item>
+        <item>34,20e3</item>
+        <item>35,20e3</item>
+        <item>36,20e3</item>
+        <item>37,20e3</item>
+        <item>38,20e3</item>
+        <item>39,20e3</item>
+        <item>1f51f</item>
+        <item>1f4af</item>
+        <item>1f51e</item>
+        <item>1f520</item>
+        <item>1f521</item>
+        <item>1f522</item>
+        <item>1f523</item>
+        <item>1f524</item>
+        <item>1f170</item>
+        <item>1f18e</item>
+        <item>1f171</item>
+        <item>1f191</item>
+        <item>1f192</item>
+        <item>1f193</item>
+        <item>2139</item>
+        <item>1f194</item>
+        <item>24c2</item>
+        <item>1f195</item>
+        <item>1f196</item>
+        <item>1f17e</item>
+        <item>1f197</item>
+        <item>1f17f</item>
+        <item>1f198</item>
+        <item>1f199</item>
+        <item>1f19a</item>
+        <item>1f201</item>
+        <item>1f202</item>
+        <item>1f237</item>
+        <item>1f236</item>
+        <item>1f22f</item>
+        <item>1f250</item>
+        <item>1f239</item>
+        <item>1f21a</item>
+        <item>1f232</item>
+        <item>1f251</item>
+        <item>1f238</item>
+        <item>1f234</item>
+        <item>1f233</item>
+        <item>3297</item>
+        <item>3299</item>
+        <item>1f23a</item>
+        <item>1f235</item>
+        <item>25aa</item>
+        <item>25ab</item>
+        <item>25fb</item>
+        <item>25fc</item>
+        <item>25fd</item>
+        <item>25fe</item>
+        <item>2b1b</item>
+        <item>2b1c</item>
+        <item>1f536</item>
+        <item>1f537</item>
+        <item>1f538</item>
+        <item>1f539</item>
+        <item>1f53a</item>
+        <item>1f53b</item>
+        <item>1f4a0</item>
+        <item>1f518</item>
+        <item>1f532</item>
+        <item>1f533</item>
+        <item>26aa</item>
+        <item>26ab</item>
+        <item>1f534</item>
+        <item>1f535</item>
+    </array>
+    <array
+        name="emoji_animals_nature"
+        format="string">
+        <item>1f435</item>
+        <item>1f412</item>
+        <item>1f436</item>
+        <item>1f415</item>
+        <item>1f429</item>
+        <item>1f43a</item>
+        <item>1f431</item>
+        <item>1f408</item>
+        <item>1f981</item>
+        <item>1f42f</item>
+        <item>1f405</item>
+        <item>1f406</item>
+        <item>1f434</item>
+        <item>1f40e</item>
+        <item>1f984</item>
+        <item>1f42e</item>
+        <item>1f402</item>
+        <item>1f403</item>
+        <item>1f404</item>
+        <item>1f437</item>
+        <item>1f416</item>
+        <item>1f417</item>
+        <item>1f43d</item>
+        <item>1f40f</item>
+        <item>1f411</item>
+        <item>1f410</item>
+        <item>1f42a</item>
+        <item>1f42b</item>
+        <item>1f418</item>
+        <item>1f42d</item>
+        <item>1f401</item>
+        <item>1f400</item>
+        <item>1f439</item>
+        <item>1f430</item>
+        <item>1f407</item>
+        <item>1f43f</item>
+        <item>1f43b</item>
+        <item>1f428</item>
+        <item>1f43c</item>
+        <item>1f43e</item>
+        <item>1f983</item>
+        <item>1f414</item>
+        <item>1f413</item>
+        <item>1f423</item>
+        <item>1f424</item>
+        <item>1f425</item>
+        <item>1f426</item>
+        <item>1f427</item>
+        <item>1f54a</item>
+        <item>1f438</item>
+        <item>1f40a</item>
+        <item>1f422</item>
+        <item>1f40d</item>
+        <item>1f432</item>
+        <item>1f409</item>
+        <item>1f433</item>
+        <item>1f40b</item>
+        <item>1f42c</item>
+        <item>1f41f</item>
+        <item>1f420</item>
+        <item>1f421</item>
+        <item>1f419</item>
+        <item>1f41a</item>
+        <item>1f980</item>
+        <item>1f40c</item>
+        <item>1f41b</item>
+        <item>1f41c</item>
+        <item>1f41d</item>
+        <item>1f41e</item>
+        <item>1f577</item>
+        <item>1f578</item>
+        <item>1f982</item>
+        <item>1f490</item>
+        <item>1f338</item>
+        <item>1f4ae</item>
+        <item>1f3f5</item>
+        <item>1f339</item>
+        <item>1f33a</item>
+        <item>1f33b</item>
+        <item>1f33c</item>
+        <item>1f337</item>
+        <item>2618</item>
+        <item>1f331</item>
+        <item>1f332</item>
+        <item>1f333</item>
+        <item>1f334</item>
+        <item>1f335</item>
+        <item>1f33e</item>
+        <item>1f33f</item>
+        <item>1f340</item>
+        <item>1f341</item>
+        <item>1f342</item>
+        <item>1f343</item>
+    </array>
+    <array
+        name="emoji_smiley_people"
+        format="string">
+        <item>1f600</item>
+        <item>1f601</item>
+        <item>1f602</item>
+        <item>1f603</item>
+        <item>1f604</item>
+        <item>1f605</item>
+        <item>1f606</item>
+        <item>1f609</item>
+        <item>1f60a</item>
+        <item>1f60b</item>
+        <item>1f60e</item>
+        <item>1f60d</item>
+        <item>1f618</item>
+        <item>1f617</item>
+        <item>1f619</item>
+        <item>1f61a</item>
+        <item>263a</item>
+        <item>1f642</item>
+        <item>1f917</item>
+        <item>1f607</item>
+        <item>1f914</item>
+        <item>1f610</item>
+        <item>1f611</item>
+        <item>1f636</item>
+        <item>1f644</item>
+        <item>1f60f</item>
+        <item>1f623</item>
+        <item>1f625</item>
+        <item>1f62e</item>
+        <item>1f910</item>
+        <item>1f62f</item>
+        <item>1f62a</item>
+        <item>1f62b</item>
+        <item>1f634</item>
+        <item>1f60c</item>
+        <item>1f913</item>
+        <item>1f61b</item>
+        <item>1f61c</item>
+        <item>1f61d</item>
+        <item>2639</item>
+        <item>1f641</item>
+        <item>1f612</item>
+        <item>1f613</item>
+        <item>1f614</item>
+        <item>1f615</item>
+        <item>1f616</item>
+        <item>1f643</item>
+        <item>1f637</item>
+        <item>1f912</item>
+        <item>1f915</item>
+        <item>1f911</item>
+        <item>1f632</item>
+        <item>1f61e</item>
+        <item>1f61f</item>
+        <item>1f624</item>
+        <item>1f622</item>
+        <item>1f62d</item>
+        <item>1f626</item>
+        <item>1f627</item>
+        <item>1f628</item>
+        <item>1f629</item>
+        <item>1f62c</item>
+        <item>1f630</item>
+        <item>1f631</item>
+        <item>1f633</item>
+        <item>1f635</item>
+        <item>1f621</item>
+        <item>1f620</item>
+        <item>1f608</item>
+        <item>1f47f</item>
+        <item>1f479</item>
+        <item>1f47a</item>
+        <item>1f480</item>
+        <item>2620</item>
+        <item>1f47b</item>
+        <item>1f47d</item>
+        <item>1f47e</item>
+        <item>1f916</item>
+        <item>1f4a9</item>
+        <item>1f63a</item>
+        <item>1f638</item>
+        <item>1f639</item>
+        <item>1f63b</item>
+        <item>1f63c</item>
+        <item>1f63d</item>
+        <item>1f640</item>
+        <item>1f63f</item>
+        <item>1f63e</item>
+        <item>1f648</item>
+        <item>1f649</item>
+        <item>1f64a</item>
+        <item>1f466</item>
+        <item>1f467</item>
+        <item>1f468</item>
+        <item>1f469</item>
+        <item>1f474</item>
+        <item>1f475</item>
+        <item>1f476</item>
+        <item>1f471</item>
+        <item>1f46e</item>
+        <item>1f472</item>
+        <item>1f473</item>
+        <item>1f477</item>
+        <item>26d1</item>
+        <item>1f478</item>
+        <item>1f482</item>
+        <item>1f575</item>
+        <item>1f385</item>
+        <item>1f47c</item>
+        <item>1f46f</item>
+        <item>1f486</item>
+        <item>1f487</item>
+        <item>1f470</item>
+        <item>1f64d</item>
+        <item>1f64e</item>
+        <item>1f645</item>
+        <item>1f646</item>
+        <item>1f481</item>
+        <item>1f64b</item>
+        <item>1f647</item>
+        <item>1f64c</item>
+        <item>1f64f</item>
+        <item>1f5e3</item>
+        <item>1f464</item>
+        <item>1f465</item>
+        <item>1f6b6</item>
+        <item>1f3c3</item>
+        <item>1f483</item>
+        <item>1f574</item>
+        <item>1f46b</item>
+        <item>1f46c</item>
+        <item>1f46d</item>
+        <item>1f48f</item>
+        <item>1f468,200d,2764,fe0f,200d,1f48b,200d,1f468</item>
+        <item>1f469,200d,2764,fe0f,200d,1f48b,200d,1f469</item>
+        <item>1f491</item>
+        <item>1f468,200d,2764,fe0f,200d,1f468</item>
+        <item>1f469,200d,2764,fe0f,200d,1f469</item>
+        <item>1f46a</item>
+        <item>1f468,200d,1f468,200d,1f466</item>
+        <item>1f468,200d,1f468,200d,1f466,200d,1f466</item>
+        <item>1f468,200d,1f468,200d,1f467</item>
+        <item>1f468,200d,1f468,200d,1f467,200d,1f466</item>
+        <item>1f468,200d,1f468,200d,1f467,200d,1f467</item>
+        <item>1f468,200d,1f469,200d,1f466</item>
+        <item>1f468,200d,1f469,200d,1f466,200d,1f466</item>
+        <item>1f468,200d,1f469,200d,1f467</item>
+        <item>1f468,200d,1f469,200d,1f467,200d,1f466</item>
+        <item>1f468,200d,1f469,200d,1f467,200d,1f467</item>
+        <item>1f469,200d,1f469,200d,1f466</item>
+        <item>1f469,200d,1f469,200d,1f466,200d,1f466</item>
+        <item>1f469,200d,1f469,200d,1f467</item>
+        <item>1f469,200d,1f469,200d,1f467,200d,1f466</item>
+        <item>1f469,200d,1f469,200d,1f467,200d,1f467</item>
+        <item>1f3fb</item>
+        <item>1f3fc</item>
+        <item>1f3fd</item>
+        <item>1f3fe</item>
+        <item>1f3ff</item>
+        <item>1f4aa</item>
+        <item>1f448</item>
+        <item>1f449</item>
+        <item>261d</item>
+        <item>1f446</item>
+        <item>1f595</item>
+        <item>1f447</item>
+        <item>270c</item>
+        <item>1f596</item>
+        <item>1f918</item>
+        <item>1f590</item>
+        <item>270a</item>
+        <item>270b</item>
+        <item>1f44a</item>
+        <item>1f44c</item>
+        <item>1f44d</item>
+        <item>1f44e</item>
+        <item>1f44b</item>
+        <item>1f44f</item>
+        <item>1f450</item>
+        <item>270d</item>
+        <item>1f485</item>
+        <item>1f442</item>
+        <item>1f443</item>
+        <item>1f463</item>
+        <item>1f440</item>
+        <item>1f441</item>
+        <item>1f445</item>
+        <item>1f444</item>
+        <item>1f48b</item>
+        <item>1f498</item>
+        <item>2764</item>
+        <item>1f493</item>
+        <item>1f494</item>
+        <item>1f495</item>
+        <item>1f496</item>
+        <item>1f497</item>
+        <item>1f499</item>
+        <item>1f49a</item>
+        <item>1f49b</item>
+        <item>1f49c</item>
+        <item>1f49d</item>
+        <item>1f49e</item>
+        <item>1f49f</item>
+        <item>2763</item>
+        <item>1f48c</item>
+        <item>1f4a4</item>
+        <item>1f4a2</item>
+        <item>1f4a3</item>
+        <item>1f4a5</item>
+        <item>1f4a6</item>
+        <item>1f4a8</item>
+        <item>1f4ab</item>
+        <item>1f4ac</item>
+        <item>1f5e8</item>
+        <item>1f5ef</item>
+        <item>1f4ad</item>
+        <item>1f441,200d,1f5e8</item>
+        <item>1f573</item>
+        <item>1f453</item>
+        <item>1f576</item>
+        <item>1f454</item>
+        <item>1f455</item>
+        <item>1f456</item>
+        <item>1f457</item>
+        <item>1f458</item>
+        <item>1f459</item>
+        <item>1f45a</item>
+        <item>1f45b</item>
+        <item>1f45c</item>
+        <item>1f45d</item>
+        <item>1f6cd</item>
+        <item>1f392</item>
+        <item>1f45e</item>
+        <item>1f45f</item>
+        <item>1f460</item>
+        <item>1f461</item>
+        <item>1f462</item>
+        <item>1f451</item>
+        <item>1f452</item>
+        <item>1f3a9</item>
+        <item>1f393</item>
+        <item>1f4ff</item>
+        <item>1f484</item>
+        <item>1f48d</item>
+        <item>1f48e</item>
+    </array>
+    <array
+        name="emoji_food_drink"
+        format="string">
+        <item>1f347</item>
+        <item>1f348</item>
+        <item>1f349</item>
+        <item>1f34a</item>
+        <item>1f34b</item>
+        <item>1f34c</item>
+        <item>1f34d</item>
+        <item>1f34e</item>
+        <item>1f34f</item>
+        <item>1f350</item>
+        <item>1f351</item>
+        <item>1f352</item>
+        <item>1f353</item>
+        <item>1f345</item>
+        <item>1f346</item>
+        <item>1f33d</item>
+        <item>1f336</item>
+        <item>1f344</item>
+        <item>1f330</item>
+        <item>1f35e</item>
+        <item>1f9c0</item>
+        <item>1f356</item>
+        <item>1f357</item>
+        <item>1f354</item>
+        <item>1f35f</item>
+        <item>1f355</item>
+        <item>1f32d</item>
+        <item>1f32e</item>
+        <item>1f32f</item>
+        <item>1f37f</item>
+        <item>1f372</item>
+        <item>1f371</item>
+        <item>1f358</item>
+        <item>1f359</item>
+        <item>1f35a</item>
+        <item>1f35b</item>
+        <item>1f35c</item>
+        <item>1f35d</item>
+        <item>1f360</item>
+        <item>1f362</item>
+        <item>1f363</item>
+        <item>1f364</item>
+        <item>1f365</item>
+        <item>1f361</item>
+        <item>1f366</item>
+        <item>1f367</item>
+        <item>1f368</item>
+        <item>1f369</item>
+        <item>1f36a</item>
+        <item>1f382</item>
+        <item>1f370</item>
+        <item>1f36b</item>
+        <item>1f36c</item>
+        <item>1f36d</item>
+        <item>1f36e</item>
+        <item>1f36f</item>
+        <item>1f37c</item>
+        <item>2615</item>
+        <item>1f375</item>
+        <item>1f376</item>
+        <item>1f37e</item>
+        <item>1f377</item>
+        <item>1f378</item>
+        <item>1f379</item>
+        <item>1f37a</item>
+        <item>1f37b</item>
+        <item>1f37d</item>
+        <item>1f374</item>
+        <item>1f373</item>
+        <item>1f3fa</item>
+    </array>
+    <array
+        name="emoji_objects"
+        format="string">
+        <item>1f507</item>
+        <item>1f508</item>
+        <item>1f509</item>
+        <item>1f50a</item>
+        <item>1f4e2</item>
+        <item>1f4e3</item>
+        <item>1f4ef</item>
+        <item>1f514</item>
+        <item>1f515</item>
+        <item>1f3bc</item>
+        <item>1f3b5</item>
+        <item>1f3b6</item>
+        <item>1f399</item>
+        <item>1f39a</item>
+        <item>1f39b</item>
+        <item>1f3a4</item>
+        <item>1f3a7</item>
+        <item>1f3b7</item>
+        <item>1f3b8</item>
+        <item>1f3b9</item>
+        <item>1f3ba</item>
+        <item>1f3bb</item>
+        <item>1f4fb</item>
+        <item>1f4f1</item>
+        <item>1f4f2</item>
+        <item>260e</item>
+        <item>1f4de</item>
+        <item>1f4df</item>
+        <item>1f4e0</item>
+        <item>1f50b</item>
+        <item>1f50c</item>
+        <item>1f4bb</item>
+        <item>1f5a5</item>
+        <item>1f5a8</item>
+        <item>2328</item>
+        <item>1f5b1</item>
+        <item>1f5b2</item>
+        <item>1f4bd</item>
+        <item>1f4be</item>
+        <item>1f4bf</item>
+        <item>1f4c0</item>
+        <item>1f3a5</item>
+        <item>1f3ac</item>
+        <item>1f4fd</item>
+        <item>1f4fa</item>
+        <item>1f4f7</item>
+        <item>1f4f8</item>
+        <item>1f4f9</item>
+        <item>1f4fc</item>
+        <item>1f50d</item>
+        <item>1f50e</item>
+        <item>1f52c</item>
+        <item>1f52d</item>
+        <item>1f4e1</item>
+        <item>1f56f</item>
+        <item>1f4a1</item>
+        <item>1f526</item>
+        <item>1f3ee</item>
+        <item>1f4d4</item>
+        <item>1f4d5</item>
+        <item>1f4d6</item>
+        <item>1f4d7</item>
+        <item>1f4d8</item>
+        <item>1f4d9</item>
+        <item>1f4da</item>
+        <item>1f4d3</item>
+        <item>1f4d2</item>
+        <item>1f4c3</item>
+        <item>1f4dc</item>
+        <item>1f4c4</item>
+        <item>1f4f0</item>
+        <item>1f5de</item>
+        <item>1f4d1</item>
+        <item>1f516</item>
+        <item>1f4b0</item>
+        <item>1f4b4</item>
+        <item>1f4b5</item>
+        <item>1f4b6</item>
+        <item>1f4b7</item>
+        <item>1f4b8</item>
+        <item>1f4b3</item>
+        <item>1f4b9</item>
+        <item>2709</item>
+        <item>1f4e7</item>
+        <item>1f4e8</item>
+        <item>1f4e9</item>
+        <item>1f4e4</item>
+        <item>1f4e5</item>
+        <item>1f4e6</item>
+        <item>1f4eb</item>
+        <item>1f4ea</item>
+        <item>1f4ec</item>
+        <item>1f4ed</item>
+        <item>1f4ee</item>
+        <item>1f5f3</item>
+        <item>270f</item>
+        <item>2712</item>
+        <item>1f58b</item>
+        <item>1f58a</item>
+        <item>1f58c</item>
+        <item>1f58d</item>
+        <item>1f4dd</item>
+        <item>1f4bc</item>
+        <item>1f4c1</item>
+        <item>1f4c2</item>
+        <item>1f5c2</item>
+        <item>1f4c5</item>
+        <item>1f4c6</item>
+        <item>1f5d2</item>
+        <item>1f5d3</item>
+        <item>1f4c7</item>
+        <item>1f4c8</item>
+        <item>1f4c9</item>
+        <item>1f4ca</item>
+        <item>1f4cb</item>
+        <item>1f4cc</item>
+        <item>1f4cd</item>
+        <item>1f4ce</item>
+        <item>1f587</item>
+        <item>1f4cf</item>
+        <item>1f4d0</item>
+        <item>2702</item>
+        <item>1f5c3</item>
+        <item>1f5c4</item>
+        <item>1f5d1</item>
+        <item>1f512</item>
+        <item>1f513</item>
+        <item>1f50f</item>
+        <item>1f510</item>
+        <item>1f511</item>
+        <item>1f5dd</item>
+        <item>1f528</item>
+        <item>26cf</item>
+        <item>2692</item>
+        <item>1f6e0</item>
+        <item>1f527</item>
+        <item>1f529</item>
+        <item>2699</item>
+        <item>1f5dc</item>
+        <item>2697</item>
+        <item>2696</item>
+        <item>1f517</item>
+        <item>26d3</item>
+        <item>1f489</item>
+        <item>1f48a</item>
+        <item>1f5e1</item>
+        <item>1f52a</item>
+        <item>2694</item>
+        <item>1f52b</item>
+        <item>1f6e1</item>
+        <item>1f3f9</item>
+        <item>1f6ac</item>
+        <item>26b0</item>
+        <item>26b1</item>
+        <item>1f5ff</item>
+        <item>1f6e2</item>
+        <item>1f52e</item>
+    </array>
+    <array
+        name="emoji_activity"
+        format="string">
+        <item>1f383</item>
+        <item>1f384</item>
+        <item>1f386</item>
+        <item>1f387</item>
+        <item>2728</item>
+        <item>1f388</item>
+        <item>1f389</item>
+        <item>1f38a</item>
+        <item>1f38b</item>
+        <item>1f38c</item>
+        <item>1f38d</item>
+        <item>1f38e</item>
+        <item>1f38f</item>
+        <item>1f390</item>
+        <item>1f391</item>
+        <item>1f380</item>
+        <item>1f381</item>
+        <item>1f396</item>
+        <item>1f397</item>
+        <item>1f39e</item>
+        <item>1f39f</item>
+        <item>1f3ab</item>
+        <item>1f3f7</item>
+        <item>26bd</item>
+        <item>26be</item>
+        <item>1f3c0</item>
+        <item>1f3c8</item>
+        <item>1f3c9</item>
+        <item>1f3be</item>
+        <item>1f3b1</item>
+        <item>1f3b3</item>
+        <item>26f3</item>
+        <item>1f3cc</item>
+        <item>26f8</item>
+        <item>1f3a3</item>
+        <item>1f3bd</item>
+        <item>1f3bf</item>
+        <item>26f7</item>
+        <item>1f3c2</item>
+        <item>1f3c4</item>
+        <item>1f3c7</item>
+        <item>1f3ca</item>
+        <item>26f9</item>
+        <item>1f3cb</item>
+        <item>1f6b4</item>
+        <item>1f6b5</item>
+        <item>1f3ce</item>
+        <item>1f3cd</item>
+        <item>1f3c5</item>
+        <item>1f3c6</item>
+        <item>1f3cf</item>
+        <item>1f3d0</item>
+        <item>1f3d1</item>
+        <item>1f3d2</item>
+        <item>1f3d3</item>
+        <item>1f3f8</item>
+        <item>1f3af</item>
+        <item>1f3ae</item>
+        <item>1f579</item>
+        <item>1f3b2</item>
+        <item>2660</item>
+        <item>2665</item>
+        <item>2666</item>
+        <item>2663</item>
+        <item>1f0cf</item>
+        <item>1f004</item>
+        <item>1f3b4</item>
+    </array>
+    <array
+        name="emoji_travel_places"
+        format="string">
+        <item>1f30d</item>
+        <item>1f30e</item>
+        <item>1f30f</item>
+        <item>1f310</item>
+        <item>1f5fa</item>
+        <item>1f3d4</item>
+        <item>26f0</item>
+        <item>1f30b</item>
+        <item>1f5fb</item>
+        <item>1f3d5</item>
+        <item>1f3d6</item>
+        <item>1f3dc</item>
+        <item>1f3dd</item>
+        <item>1f3de</item>
+        <item>1f3df</item>
+        <item>1f3db</item>
+        <item>1f3d7</item>
+        <item>1f3d8</item>
+        <item>1f3d9</item>
+        <item>1f3da</item>
+        <item>1f3e0</item>
+        <item>1f3e1</item>
+        <item>26ea</item>
+        <item>1f54b</item>
+        <item>1f54c</item>
+        <item>1f54d</item>
+        <item>26e9</item>
+        <item>1f3e2</item>
+        <item>1f3e3</item>
+        <item>1f3e4</item>
+        <item>1f3e5</item>
+        <item>1f3e6</item>
+        <item>1f3e8</item>
+        <item>1f3e9</item>
+        <item>1f3ea</item>
+        <item>1f3eb</item>
+        <item>1f3ec</item>
+        <item>1f3ed</item>
+        <item>1f3ef</item>
+        <item>1f3f0</item>
+        <item>1f492</item>
+        <item>1f5fc</item>
+        <item>1f5fd</item>
+        <item>1f5fe</item>
+        <item>26f2</item>
+        <item>26fa</item>
+        <item>1f301</item>
+        <item>1f303</item>
+        <item>1f304</item>
+        <item>1f305</item>
+        <item>1f306</item>
+        <item>1f307</item>
+        <item>1f309</item>
+        <item>2668</item>
+        <item>1f30c</item>
+        <item>1f3a0</item>
+        <item>1f3a1</item>
+        <item>1f3a2</item>
+        <item>1f488</item>
+        <item>1f3aa</item>
+        <item>1f3ad</item>
+        <item>1f5bc</item>
+        <item>1f3a8</item>
+        <item>1f3b0</item>
+        <item>1f682</item>
+        <item>1f683</item>
+        <item>1f684</item>
+        <item>1f685</item>
+        <item>1f686</item>
+        <item>1f687</item>
+        <item>1f688</item>
+        <item>1f689</item>
+        <item>1f68a</item>
+        <item>1f69d</item>
+        <item>1f69e</item>
+        <item>1f68b</item>
+        <item>1f68c</item>
+        <item>1f68d</item>
+        <item>1f68e</item>
+        <item>1f68f</item>
+        <item>1f690</item>
+        <item>1f691</item>
+        <item>1f692</item>
+        <item>1f693</item>
+        <item>1f694</item>
+        <item>1f695</item>
+        <item>1f696</item>
+        <item>1f697</item>
+        <item>1f698</item>
+        <item>1f699</item>
+        <item>1f69a</item>
+        <item>1f69b</item>
+        <item>1f69c</item>
+        <item>1f6b2</item>
+        <item>26fd</item>
+        <item>1f6e3</item>
+        <item>1f6e4</item>
+        <item>1f6a8</item>
+        <item>1f6a5</item>
+        <item>1f6a6</item>
+        <item>1f6a7</item>
+        <item>2693</item>
+        <item>26f5</item>
+        <item>1f6a3</item>
+        <item>1f6a4</item>
+        <item>1f6f3</item>
+        <item>26f4</item>
+        <item>1f6e5</item>
+        <item>1f6a2</item>
+        <item>2708</item>
+        <item>1f6e9</item>
+        <item>1f6eb</item>
+        <item>1f6ec</item>
+        <item>1f4ba</item>
+        <item>1f681</item>
+        <item>1f69f</item>
+        <item>1f6a0</item>
+        <item>1f6a1</item>
+        <item>1f680</item>
+        <item>1f6f0</item>
+        <item>1f6ce</item>
+        <item>1f6aa</item>
+        <item>1f6cc</item>
+        <item>1f6cf</item>
+        <item>1f6cb</item>
+        <item>1f6bd</item>
+        <item>1f6bf</item>
+        <item>1f6c0</item>
+        <item>1f6c1</item>
+        <item>231b</item>
+        <item>23f3</item>
+        <item>231a</item>
+        <item>23f0</item>
+        <item>23f1</item>
+        <item>23f2</item>
+        <item>1f570</item>
+        <item>1f55b</item>
+        <item>1f567</item>
+        <item>1f550</item>
+        <item>1f55c</item>
+        <item>1f551</item>
+        <item>1f55d</item>
+        <item>1f552</item>
+        <item>1f55e</item>
+        <item>1f553</item>
+        <item>1f55f</item>
+        <item>1f554</item>
+        <item>1f560</item>
+        <item>1f555</item>
+        <item>1f561</item>
+        <item>1f556</item>
+        <item>1f562</item>
+        <item>1f557</item>
+        <item>1f563</item>
+        <item>1f558</item>
+        <item>1f564</item>
+        <item>1f559</item>
+        <item>1f565</item>
+        <item>1f55a</item>
+        <item>1f566</item>
+        <item>1f311</item>
+        <item>1f312</item>
+        <item>1f313</item>
+        <item>1f314</item>
+        <item>1f315</item>
+        <item>1f316</item>
+        <item>1f317</item>
+        <item>1f318</item>
+        <item>1f319</item>
+        <item>1f31a</item>
+        <item>1f31b</item>
+        <item>1f31c</item>
+        <item>1f321</item>
+        <item>2600</item>
+        <item>1f31d</item>
+        <item>1f31e</item>
+        <item>2b50</item>
+        <item>1f31f</item>
+        <item>1f320</item>
+        <item>2601</item>
+        <item>26c5</item>
+        <item>26c8</item>
+        <item>1f324</item>
+        <item>1f325</item>
+        <item>1f326</item>
+        <item>1f327</item>
+        <item>1f328</item>
+        <item>1f329</item>
+        <item>1f32a</item>
+        <item>1f32b</item>
+        <item>1f32c</item>
+        <item>1f300</item>
+        <item>1f308</item>
+        <item>1f302</item>
+        <item>2602</item>
+        <item>2614</item>
+        <item>26f1</item>
+        <item>26a1</item>
+        <item>2744</item>
+        <item>2603</item>
+        <item>26c4</item>
+        <item>2604</item>
+        <item>1f525</item>
+        <item>1f4a7</item>
+        <item>1f30a</item>
+    </array>
+    <array
+        name="emoji_flags"
+        format="string">
+        <item>1f1e6,1f1e8</item>
+        <item>1f1e6,1f1e9</item>
+        <item>1f1e6,1f1ea</item>
+        <item>1f1e6,1f1eb</item>
+        <item>1f1e6,1f1ec</item>
+        <item>1f1e6,1f1ee</item>
+        <item>1f1e6,1f1f1</item>
+        <item>1f1e6,1f1f2</item>
+        <item>1f1e6,1f1f4</item>
+        <item>1f1e6,1f1f6</item>
+        <item>1f1e6,1f1f7</item>
+        <item>1f1e6,1f1f8</item>
+        <item>1f1e6,1f1f9</item>
+        <item>1f1e6,1f1fa</item>
+        <item>1f1e6,1f1fc</item>
+        <item>1f1e6,1f1fd</item>
+        <item>1f1e6,1f1ff</item>
+        <item>1f1e7,1f1e6</item>
+        <item>1f1e7,1f1e7</item>
+        <item>1f1e7,1f1e9</item>
+        <item>1f1e7,1f1ea</item>
+        <item>1f1e7,1f1eb</item>
+        <item>1f1e7,1f1ec</item>
+        <item>1f1e7,1f1ed</item>
+        <item>1f1e7,1f1ee</item>
+        <item>1f1e7,1f1ef</item>
+        <item>1f1e7,1f1f1</item>
+        <item>1f1e7,1f1f2</item>
+        <item>1f1e7,1f1f3</item>
+        <item>1f1e7,1f1f4</item>
+        <item>1f1e7,1f1f6</item>
+        <item>1f1e7,1f1f7</item>
+        <item>1f1e7,1f1f8</item>
+        <item>1f1e7,1f1f9</item>
+        <item>1f1e7,1f1fb</item>
+        <item>1f1e7,1f1fc</item>
+        <item>1f1e7,1f1fe</item>
+        <item>1f1e7,1f1ff</item>
+        <item>1f1e8,1f1e6</item>
+        <item>1f1e8,1f1e8</item>
+        <item>1f1e8,1f1e9</item>
+        <item>1f1e8,1f1eb</item>
+        <item>1f1e8,1f1ec</item>
+        <item>1f1e8,1f1ed</item>
+        <item>1f1e8,1f1ee</item>
+        <item>1f1e8,1f1f0</item>
+        <item>1f1e8,1f1f1</item>
+        <item>1f1e8,1f1f2</item>
+        <item>1f1e8,1f1f3</item>
+        <item>1f1e8,1f1f4</item>
+        <item>1f1e8,1f1f5</item>
+        <item>1f1e8,1f1f7</item>
+        <item>1f1e8,1f1fa</item>
+        <item>1f1e8,1f1fb</item>
+        <item>1f1e8,1f1fc</item>
+        <item>1f1e8,1f1fd</item>
+        <item>1f1e8,1f1fe</item>
+        <item>1f1e8,1f1ff</item>
+        <item>1f1e9,1f1ea</item>
+        <item>1f1e9,1f1ec</item>
+        <item>1f1e9,1f1ef</item>
+        <item>1f1e9,1f1f0</item>
+        <item>1f1e9,1f1f2</item>
+        <item>1f1e9,1f1f4</item>
+        <item>1f1e9,1f1ff</item>
+        <item>1f1ea,1f1e6</item>
+        <item>1f1ea,1f1e8</item>
+        <item>1f1ea,1f1ea</item>
+        <item>1f1ea,1f1ec</item>
+        <item>1f1ea,1f1ed</item>
+        <item>1f1ea,1f1f7</item>
+        <item>1f1ea,1f1f8</item>
+        <item>1f1ea,1f1f9</item>
+        <item>1f1ea,1f1fa</item>
+        <item>1f1eb,1f1ee</item>
+        <item>1f1eb,1f1ef</item>
+        <item>1f1eb,1f1f0</item>
+        <item>1f1eb,1f1f2</item>
+        <item>1f1eb,1f1f4</item>
+        <item>1f1eb,1f1f7</item>
+        <item>1f1ec,1f1e6</item>
+        <item>1f1ec,1f1e7</item>
+        <item>1f1ec,1f1e9</item>
+        <item>1f1ec,1f1ea</item>
+        <item>1f1ec,1f1eb</item>
+        <item>1f1ec,1f1ec</item>
+        <item>1f1ec,1f1ed</item>
+        <item>1f1ec,1f1ee</item>
+        <item>1f1ec,1f1f1</item>
+        <item>1f1ec,1f1f2</item>
+        <item>1f1ec,1f1f3</item>
+        <item>1f1ec,1f1f5</item>
+        <item>1f1ec,1f1f6</item>
+        <item>1f1ec,1f1f7</item>
+        <item>1f1ec,1f1f8</item>
+        <item>1f1ec,1f1f9</item>
+        <item>1f1ec,1f1fa</item>
+        <item>1f1ec,1f1fc</item>
+        <item>1f1ec,1f1fe</item>
+        <item>1f1ed,1f1f0</item>
+        <item>1f1ed,1f1f2</item>
+        <item>1f1ed,1f1f3</item>
+        <item>1f1ed,1f1f7</item>
+        <item>1f1ed,1f1f9</item>
+        <item>1f1ed,1f1fa</item>
+        <item>1f1ee,1f1e8</item>
+        <item>1f1ee,1f1e9</item>
+        <item>1f1ee,1f1ea</item>
+        <item>1f1ee,1f1f1</item>
+        <item>1f1ee,1f1f2</item>
+        <item>1f1ee,1f1f3</item>
+        <item>1f1ee,1f1f4</item>
+        <item>1f1ee,1f1f6</item>
+        <item>1f1ee,1f1f7</item>
+        <item>1f1ee,1f1f8</item>
+        <item>1f1ee,1f1f9</item>
+        <item>1f1ef,1f1ea</item>
+        <item>1f1ef,1f1f2</item>
+        <item>1f1ef,1f1f4</item>
+        <item>1f1ef,1f1f5</item>
+        <item>1f1f0,1f1ea</item>
+        <item>1f1f0,1f1ec</item>
+        <item>1f1f0,1f1ed</item>
+        <item>1f1f0,1f1ee</item>
+        <item>1f1f0,1f1f2</item>
+        <item>1f1f0,1f1f3</item>
+        <item>1f1f0,1f1f5</item>
+        <item>1f1f0,1f1f7</item>
+        <item>1f1f0,1f1fc</item>
+        <item>1f1f0,1f1fe</item>
+        <item>1f1f0,1f1ff</item>
+        <item>1f1f1,1f1e6</item>
+        <item>1f1f1,1f1e7</item>
+        <item>1f1f1,1f1e8</item>
+        <item>1f1f1,1f1ee</item>
+        <item>1f1f1,1f1f0</item>
+        <item>1f1f1,1f1f7</item>
+        <item>1f1f1,1f1f8</item>
+        <item>1f1f1,1f1f9</item>
+        <item>1f1f1,1f1fa</item>
+        <item>1f1f1,1f1fb</item>
+        <item>1f1f1,1f1fe</item>
+        <item>1f1f2,1f1e6</item>
+        <item>1f1f2,1f1e8</item>
+        <item>1f1f2,1f1e9</item>
+        <item>1f1f2,1f1ea</item>
+        <item>1f1f2,1f1eb</item>
+        <item>1f1f2,1f1ec</item>
+        <item>1f1f2,1f1ed</item>
+        <item>1f1f2,1f1f0</item>
+        <item>1f1f2,1f1f1</item>
+        <item>1f1f2,1f1f2</item>
+        <item>1f1f2,1f1f3</item>
+        <item>1f1f2,1f1f4</item>
+        <item>1f1f2,1f1f5</item>
+        <item>1f1f2,1f1f6</item>
+        <item>1f1f2,1f1f7</item>
+        <item>1f1f2,1f1f8</item>
+        <item>1f1f2,1f1f9</item>
+        <item>1f1f2,1f1fa</item>
+        <item>1f1f2,1f1fb</item>
+        <item>1f1f2,1f1fc</item>
+        <item>1f1f2,1f1fd</item>
+        <item>1f1f2,1f1fe</item>
+        <item>1f1f2,1f1ff</item>
+        <item>1f1f3,1f1e6</item>
+        <item>1f1f3,1f1e8</item>
+        <item>1f1f3,1f1ea</item>
+        <item>1f1f3,1f1eb</item>
+        <item>1f1f3,1f1ec</item>
+        <item>1f1f3,1f1ee</item>
+        <item>1f1f3,1f1f1</item>
+        <item>1f1f3,1f1f4</item>
+        <item>1f1f3,1f1f5</item>
+        <item>1f1f3,1f1f7</item>
+        <item>1f1f3,1f1fa</item>
+        <item>1f1f3,1f1ff</item>
+        <item>1f1f4,1f1f2</item>
+        <item>1f1f5,1f1e6</item>
+        <item>1f1f5,1f1ea</item>
+        <item>1f1f5,1f1eb</item>
+        <item>1f1f5,1f1ec</item>
+        <item>1f1f5,1f1ed</item>
+        <item>1f1f5,1f1f0</item>
+        <item>1f1f5,1f1f1</item>
+        <item>1f1f5,1f1f2</item>
+        <item>1f1f5,1f1f3</item>
+        <item>1f1f5,1f1f7</item>
+        <item>1f1f5,1f1f8</item>
+        <item>1f1f5,1f1f9</item>
+        <item>1f1f5,1f1fc</item>
+        <item>1f1f5,1f1fe</item>
+        <item>1f1f6,1f1e6</item>
+        <item>1f1f7,1f1ea</item>
+        <item>1f1f7,1f1f4</item>
+        <item>1f1f7,1f1f8</item>
+        <item>1f1f7,1f1fa</item>
+        <item>1f1f7,1f1fc</item>
+        <item>1f1f8,1f1e6</item>
+        <item>1f1f8,1f1e7</item>
+        <item>1f1f8,1f1e8</item>
+        <item>1f1f8,1f1e9</item>
+        <item>1f1f8,1f1ea</item>
+        <item>1f1f8,1f1ec</item>
+        <item>1f1f8,1f1ed</item>
+        <item>1f1f8,1f1ee</item>
+        <item>1f1f8,1f1ef</item>
+        <item>1f1f8,1f1f0</item>
+        <item>1f1f8,1f1f1</item>
+        <item>1f1f8,1f1f2</item>
+        <item>1f1f8,1f1f3</item>
+        <item>1f1f8,1f1f4</item>
+        <item>1f1f8,1f1f7</item>
+        <item>1f1f8,1f1f8</item>
+        <item>1f1f8,1f1f9</item>
+        <item>1f1f8,1f1fb</item>
+        <item>1f1f8,1f1fd</item>
+        <item>1f1f8,1f1fe</item>
+        <item>1f1f8,1f1ff</item>
+        <item>1f1f9,1f1e6</item>
+        <item>1f1f9,1f1e8</item>
+        <item>1f1f9,1f1e9</item>
+        <item>1f1f9,1f1eb</item>
+        <item>1f1f9,1f1ec</item>
+        <item>1f1f9,1f1ed</item>
+        <item>1f1f9,1f1ef</item>
+        <item>1f1f9,1f1f0</item>
+        <item>1f1f9,1f1f1</item>
+        <item>1f1f9,1f1f2</item>
+        <item>1f1f9,1f1f3</item>
+        <item>1f1f9,1f1f4</item>
+        <item>1f1f9,1f1f7</item>
+        <item>1f1f9,1f1f9</item>
+        <item>1f1f9,1f1fb</item>
+        <item>1f1f9,1f1fc</item>
+        <item>1f1f9,1f1ff</item>
+        <item>1f1fa,1f1e6</item>
+        <item>1f1fa,1f1ec</item>
+        <item>1f1fa,1f1f2</item>
+        <item>1f1fa,1f1f8</item>
+        <item>1f1fa,1f1fe</item>
+        <item>1f1fa,1f1ff</item>
+        <item>1f1fb,1f1e6</item>
+        <item>1f1fb,1f1e8</item>
+        <item>1f1fb,1f1ea</item>
+        <item>1f1fb,1f1ec</item>
+        <item>1f1fb,1f1ee</item>
+        <item>1f1fb,1f1f3</item>
+        <item>1f1fb,1f1fa</item>
+        <item>1f1fc,1f1eb</item>
+        <item>1f1fc,1f1f8</item>
+        <item>1f1fd,1f1f0</item>
+        <item>1f1fe,1f1ea</item>
+        <item>1f1fe,1f1f9</item>
+        <item>1f1ff,1f1e6</item>
+        <item>1f1ff,1f1f2</item>
+        <item>1f1ff,1f1fc</item>
+    </array>
+</resources>
diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java
index baee29d41e90080be85f317de5c3130415935ff9..3fbe8d3f27d289d0ada890690b6ca6c565a08c64 100644
--- a/briar-android/src/org/briarproject/android/ActivityComponent.java
+++ b/briar-android/src/org/briarproject/android/ActivityComponent.java
@@ -40,6 +40,8 @@ import org.briarproject.android.sharing.ShareForumActivity;
 import org.briarproject.android.sharing.ShareForumMessageFragment;
 import org.briarproject.android.sharing.SharingStatusBlogActivity;
 import org.briarproject.android.sharing.SharingStatusForumActivity;
+import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
+import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
 
 import dagger.Component;
 
@@ -114,6 +116,9 @@ public interface ActivityComponent {
 
 	void inject(RssFeedManageActivity activity);
 
+	void inject(EmojiProvider emojiProvider);
+	void inject(RecentEmojiPageModel recentEmojiPageModel);
+
 	// Fragments
 	void inject(ContactListFragment fragment);
 	void inject(ForumListFragment fragment);
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
index 4ff2695e3d8ddfbefbaba41dc4952c683048865a..c2bb93fbb6dbfb49c02e3c4fd19b434b8ac5c9f4 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java
@@ -25,7 +25,7 @@ import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.sharing.ShareBlogActivity;
 import org.briarproject.android.sharing.SharingStatusBlogActivity;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.blogs.BlogPostHeader;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.Author;
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
index b3e55f8482f2c812d82ea76fd228bd0cb294bbfa..590a24fba8db3e64bc001cb5288623cbf34161d6 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java
@@ -15,7 +15,7 @@ import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.util.AndroidUtils;
-import org.briarproject.android.util.TextAvatarView;
+import org.briarproject.android.view.TextAvatarView;
 import org.briarproject.api.blogs.Blog;
 import org.briarproject.api.sync.GroupId;
 
diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java
index d870db1370f04851180dac7c4b4d9b75cab3b3a2..a25bcd76306234e9b80cda1039099e8918d854d0 100644
--- a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java
+++ b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java
@@ -18,7 +18,7 @@ import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
-import org.briarproject.android.util.AuthorView;
+import org.briarproject.android.view.AuthorView;
 import org.briarproject.api.blogs.BlogCommentHeader;
 import org.briarproject.api.blogs.BlogPostHeader;
 import org.briarproject.api.identity.Author;
diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java
index c7ed7062211c8169284d52089d007522bb7b2f06..a8a8cea9d7e71bfb566133494d97a73240deb3e7 100644
--- a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java
+++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java
@@ -22,7 +22,7 @@ import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
 import org.briarproject.android.controller.handler.UiResultExceptionHandler;
 import org.briarproject.android.controller.handler.UiResultHandler;
 import org.briarproject.android.fragment.BaseFragment;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.blogs.Blog;
 import org.briarproject.api.blogs.BlogPostHeader;
 import org.briarproject.api.db.DbException;
diff --git a/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java b/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java
index 69c50dde66a78669e83cefcdd211e768bb9c633e..60230717bbc2263001c8a505b523ea352206be3b 100644
--- a/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java
+++ b/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java
@@ -14,7 +14,7 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.blogs.RssFeedAdapter.RssFeedListener;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.feed.Feed;
 import org.briarproject.api.feed.FeedManager;
diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
index bead9c4db6c61e226db713f546155dd44e7122c0..312184f77d9f6d19d22cdc8b8c1d0b5095e11ea4 100644
--- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
+++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java
@@ -20,7 +20,7 @@ import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.fragment.BaseFragment;
 import org.briarproject.android.keyagreement.KeyAgreementActivity;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.blogs.BlogSharingManager;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index a85723601f0390936947b2d139659d604516f202..3055777301450e5826bd96c2326bcb208f23d374 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -16,9 +16,6 @@ import android.util.SparseArray;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.EditText;
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -27,8 +24,11 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.api.AndroidNotificationManager;
+import org.briarproject.android.contact.ConversationAdapter.IntroductionHandler;
 import org.briarproject.android.introduction.IntroductionActivity;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.android.view.TextInputView;
+import org.briarproject.android.view.TextInputView.TextInputListener;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.blogs.BlogSharingManager;
 import org.briarproject.api.clients.SessionId;
@@ -93,8 +93,7 @@ import static org.briarproject.android.contact.ConversationItem.IncomingItem;
 import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
 
 public class ConversationActivity extends BriarActivity
-		implements EventListener, OnClickListener,
-		ConversationAdapter.IntroductionHandler {
+		implements EventListener, IntroductionHandler, TextInputListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(ConversationActivity.class.getName());
@@ -113,8 +112,7 @@ public class ConversationActivity extends BriarActivity
 	private ImageView toolbarStatus;
 	private TextView toolbarTitle;
 	private BriarRecyclerView list;
-	private EditText content;
-	private View sendButton;
+	private TextInputView textInputView;
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject
@@ -139,6 +137,7 @@ public class ConversationActivity extends BriarActivity
 	private volatile boolean connected = false;
 	private volatile Map<MessageId, byte[]> bodyCache = new HashMap<>();
 
+	@SuppressWarnings("ConstantConditions")
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
@@ -177,13 +176,8 @@ public class ConversationActivity extends BriarActivity
 		list.setAdapter(adapter);
 		list.setEmptyText(getString(R.string.no_private_messages));
 
-		content = (EditText) findViewById(R.id.input_text);
-		sendButton = findViewById(R.id.btn_send);
-		if (sendButton != null) {
-			// Enabled after loading the conversation
-			sendButton.setEnabled(false);
-			sendButton.setOnClickListener(this);
-		}
+		textInputView = (TextInputView) findViewById(R.id.text_input_container);
+		textInputView.setListener(this);
 	}
 
 	@Override
@@ -262,6 +256,10 @@ public class ConversationActivity extends BriarActivity
 
 	@Override
 	public void onBackPressed() {
+		if (textInputView.isEmojiDrawerOpen()) {
+			textInputView.hideEmojiDrawer();
+			return;
+		}
 		// FIXME disabled exit transition, because it doesn't work for some reason #318
 		//supportFinishAfterTransition();
 		finish();
@@ -367,7 +365,7 @@ public class ConversationActivity extends BriarActivity
 		runOnUiThread(new Runnable() {
 			@Override
 			public void run() {
-				sendButton.setEnabled(true);
+				textInputView.setSendButtonEnabled(true);
 				if (headers.isEmpty() && introductions.isEmpty() &&
 						invitations.isEmpty()) {
 					// we have no messages,
@@ -637,14 +635,12 @@ public class ConversationActivity extends BriarActivity
 	}
 
 	@Override
-	public void onClick(View view) {
+	public void onSendClick(String text) {
 		markMessagesRead();
-		String message = content.getText().toString();
-		if (message.equals("")) return;
+		if (text.equals("")) return;
 		long timestamp = System.currentTimeMillis();
 		timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
-		createMessage(StringUtils.toUtf8(message), timestamp);
-		content.setText("");
+		createMessage(StringUtils.toUtf8(text), timestamp);
 	}
 
 	private long getMinTimestampForNewMessage() {
diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
index 5f141948fed7b374c4f9941c3be479a5ba9d10c1..6b8c5a2b5e9d1e4cf0a09465b96b57c9693b604b 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java
@@ -21,8 +21,6 @@ import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.EditText;
-import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -31,11 +29,13 @@ import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.controller.handler.UiResultHandler;
+import org.briarproject.android.forum.ForumController.ForumPostListener;
 import org.briarproject.android.sharing.ShareForumActivity;
 import org.briarproject.android.sharing.SharingStatusForumActivity;
-import org.briarproject.android.util.AndroidUtils;
-import org.briarproject.android.util.BriarRecyclerView;
-import org.briarproject.android.util.TrustIndicatorView;
+import org.briarproject.android.view.AuthorView;
+import org.briarproject.android.view.BriarRecyclerView;
+import org.briarproject.android.view.TextInputView;
+import org.briarproject.android.view.TextInputView.TextInputListener;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
@@ -48,8 +48,6 @@ import java.util.Map;
 
 import javax.inject.Inject;
 
-import im.delight.android.identicons.IdenticonDrawable;
-
 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
 import static android.support.v7.widget.RecyclerView.NO_POSITION;
@@ -59,7 +57,7 @@ import static android.view.View.VISIBLE;
 import static android.widget.Toast.LENGTH_SHORT;
 
 public class ForumActivity extends BriarActivity implements
-		ForumController.ForumPostListener {
+		ForumPostListener, TextInputListener {
 
 	static final String FORUM_NAME = "briar.FORUM_NAME";
 
@@ -80,8 +78,7 @@ public class ForumActivity extends BriarActivity implements
 	protected ForumAdapter forumAdapter;
 
 	private BriarRecyclerView recyclerView;
-	private EditText textInput;
-	private ViewGroup inputContainer;
+	private TextInputView textInput;
 	private LinearLayoutManager linearLayoutManager;
 
 	private volatile GroupId groupId = null;
@@ -101,9 +98,9 @@ public class ForumActivity extends BriarActivity implements
 
 		forumAdapter = new ForumAdapter();
 
-		inputContainer = (ViewGroup) findViewById(R.id.text_input_container);
-		inputContainer.setVisibility(GONE);
-		textInput = (EditText) findViewById(R.id.input_text);
+		textInput = (TextInputView) findViewById(R.id.text_input_container);
+		textInput.setVisibility(GONE);
+		textInput.setListener(this);
 		recyclerView =
 				(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
 		recyclerView.setAdapter(forumAdapter);
@@ -140,7 +137,7 @@ public class ForumActivity extends BriarActivity implements
 	@Override
 	protected void onRestoreInstanceState(Bundle savedInstanceState) {
 		super.onRestoreInstanceState(savedInstanceState);
-		inputContainer.setVisibility(
+		textInput.setVisibility(
 				savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ?
 						VISIBLE : GONE);
 	}
@@ -150,7 +147,7 @@ public class ForumActivity extends BriarActivity implements
 	protected void onSaveInstanceState(Bundle outState) {
 		super.onSaveInstanceState(outState);
 		outState.putBoolean(KEY_INPUT_VISIBILITY,
-				inputContainer.getVisibility() == VISIBLE);
+				textInput.getVisibility() == VISIBLE);
 		ForumEntry replyEntry = forumAdapter.getReplyEntry();
 		if (replyEntry != null) {
 			outState.putByteArray(KEY_REPLY_ID,
@@ -190,8 +187,10 @@ public class ForumActivity extends BriarActivity implements
 
 	@Override
 	public void onBackPressed() {
-		if (inputContainer.getVisibility() == VISIBLE) {
-			inputContainer.setVisibility(GONE);
+		if (textInput.isEmojiDrawerOpen()) {
+			textInput.hideEmojiDrawer();
+		} else if (textInput.getVisibility() == VISIBLE) {
+			textInput.setVisibility(GONE);
 			forumAdapter.setReplyEntry(null);
 		} else {
 			super.onBackPressed();
@@ -202,8 +201,8 @@ public class ForumActivity extends BriarActivity implements
 		// An animation here would be an overkill because of the keyboard
 		// popping up.
 		// only clear the text when the input container was not visible
-		if (inputContainer.getVisibility() != VISIBLE) {
-			inputContainer.setVisibility(VISIBLE);
+		if (textInput.getVisibility() != VISIBLE) {
+			textInput.setVisibility(VISIBLE);
 			textInput.setText("");
 		}
 		textInput.requestFocus();
@@ -260,8 +259,8 @@ public class ForumActivity extends BriarActivity implements
 		recyclerView.stopPeriodicUpdate();
 	}
 
-	public void sendMessage(View view) {
-		String text = textInput.getText().toString();
+	@Override
+	public void onSendClick(String text) {
 		if (text.trim().length() == 0)
 			return;
 		if (forumController.getForum() == null) return;
@@ -274,7 +273,7 @@ public class ForumActivity extends BriarActivity implements
 					replyEntry.getMessageId());
 		}
 		hideSoftKeyboard(textInput);
-		inputContainer.setVisibility(GONE);
+		textInput.setVisibility(GONE);
 		forumAdapter.setReplyEntry(null);
 	}
 
@@ -334,10 +333,9 @@ public class ForumActivity extends BriarActivity implements
 
 	static class ForumViewHolder extends RecyclerView.ViewHolder {
 
-		final TextView textView, lvlText, authorText, dateText, repliesText;
+		final TextView textView, lvlText, repliesText;
+		final AuthorView author;
 		final View[] lvls;
-		public final ImageView avatar;
-		final TrustIndicatorView trust;
 		final View chevron, replyButton;
 		final ViewGroup cell;
 		final View topDivider;
@@ -347,8 +345,7 @@ public class ForumActivity extends BriarActivity implements
 
 			textView = (TextView) v.findViewById(R.id.text);
 			lvlText = (TextView) v.findViewById(R.id.nested_line_text);
-			authorText = (TextView) v.findViewById(R.id.author);
-			dateText = (TextView) v.findViewById(R.id.date);
+			author = (AuthorView) v.findViewById(R.id.author);
 			repliesText = (TextView) v.findViewById(R.id.replies);
 			int[] nestedLineIds = {
 					R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
@@ -358,8 +355,6 @@ public class ForumActivity extends BriarActivity implements
 			for (int i = 0; i < lvls.length; i++) {
 				lvls[i] = v.findViewById(nestedLineIds[i]);
 			}
-			avatar = (ImageView) v.findViewById(R.id.avatar);
-			trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator);
 			chevron = v.findViewById(R.id.chevron);
 			replyButton = v.findViewById(R.id.btn_reply);
 			cell = (ViewGroup) v.findViewById(R.id.forum_cell);
@@ -604,7 +599,7 @@ public class ForumActivity extends BriarActivity implements
 		public ForumViewHolder onCreateViewHolder(ViewGroup parent,
 				int viewType) {
 			View v = LayoutInflater.from(parent.getContext())
-					.inflate(R.layout.forum_discussion_cell, parent, false);
+					.inflate(R.layout.list_item_forum_post, parent, false);
 			return new ForumViewHolder(v);
 		}
 
@@ -635,10 +630,9 @@ public class ForumActivity extends BriarActivity implements
 			} else {
 				ui.lvlText.setVisibility(GONE);
 			}
-			ui.authorText.setText(data.getAuthor());
-			ui.dateText.setText(AndroidUtils
-					.formatDate(ForumActivity.this, data.getTimestamp()));
-			ui.trust.setTrustLevel(data.getStatus());
+			ui.author.setAuthor(data.getAuthor());
+			ui.author.setDate(data.getTimestamp());
+			ui.author.setAuthorStatus(data.getStatus());
 
 			int replies = getReplyCount(data);
 			if (replies == 0) {
@@ -648,8 +642,6 @@ public class ForumActivity extends BriarActivity implements
 						.getQuantityString(R.plurals.message_replies, replies,
 								replies));
 			}
-			ui.avatar.setImageDrawable(
-					new IdenticonDrawable(data.getAuthorId().getBytes()));
 
 			if (hasDescendants(data)) {
 				ui.chevron.setVisibility(VISIBLE);
diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
index b3f2aed40ba472efd6256072799288f36525b61c..f809d72394ddf9144ff1fc0642b32d34459673dd 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumEntry.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java
@@ -1,6 +1,7 @@
 package org.briarproject.android.forum;
 
 import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.sync.MessageId;
@@ -11,26 +12,24 @@ public class ForumEntry {
 	private final String text;
 	private final int level;
 	private final long timestamp;
-	private final String author;
-	private final AuthorId authorId;
+	private final Author author;
 	private Status status;
 	private boolean isShowingDescendants = true;
 	private boolean isRead = true;
 
 	ForumEntry(ForumPostHeader h, String text, int level) {
-		this(h.getId(), text, level, h.getTimestamp(), h.getAuthor().getName(),
-				h.getAuthor().getId(), h.getAuthorStatus());
+		this(h.getId(), text, level, h.getTimestamp(), h.getAuthor(),
+				h.getAuthorStatus());
 		this.isRead = h.isRead();
 	}
 
 	public ForumEntry(MessageId messageId, String text, int level,
-			long timestamp, String author, AuthorId authorId, Status status) {
+			long timestamp, Author author, Status status) {
 		this.messageId = messageId;
 		this.text = text;
 		this.level = level;
 		this.timestamp = timestamp;
 		this.author = author;
-		this.authorId = authorId;
 		this.status = status;
 	}
 
@@ -46,14 +45,10 @@ public class ForumEntry {
 		return timestamp;
 	}
 
-	public String getAuthor() {
+	public Author getAuthor() {
 		return author;
 	}
 
-	AuthorId getAuthorId() {
-		return authorId;
-	}
-
 	public Status getStatus() {
 		return status;
 	}
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
index 88759804aefa31203ede1b5047da0ab2becd6e48..d37f23a02933377cf35ee112103910c2abe6d585 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java
@@ -13,7 +13,7 @@ import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.util.AndroidUtils;
-import org.briarproject.android.util.TextAvatarView;
+import org.briarproject.android.view.TextAvatarView;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.sync.GroupId;
 
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
index 11c9bd293099b9dc9c4e9b429055c03d0a5b4ea4..fc9f63b37d7436c8a8499e553d0f0779e12cd1ef 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
@@ -18,7 +18,7 @@ import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.api.AndroidNotificationManager;
 import org.briarproject.android.fragment.BaseEventFragment;
 import org.briarproject.android.sharing.InvitationsForumActivity;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.event.ContactRemovedEvent;
diff --git a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
index d443a65b83ddde537ee4795333af0ca7fea78f79..a5b89405bf703b0bcf619a9226f58b371f4fb840 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java
@@ -3,7 +3,8 @@ package org.briarproject.android.forum;
 import org.briarproject.android.controller.handler.ResultHandler;
 import org.briarproject.api.UniqueId;
 import org.briarproject.api.forum.Forum;
-import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
@@ -20,34 +21,27 @@ import static org.briarproject.api.identity.Author.Status.UNVERIFIED;
 
 public class ForumTestControllerImpl implements ForumController {
 
+	@Inject
+	AuthorFactory authorFactory;
+
 	private static final Logger LOG =
 			Logger.getLogger(ForumControllerImpl.class.getName());
 
-	private final static String[] AUTHORS = {
-			"Guðmundur",
-			"Jónas",
-			"Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen",
-			"Baldur Friðrik",
-			"Anna Katrín",
-			"Þór",
-			"Anna Þorbjörg",
-			"Guðrún",
-			"Helga",
-			"Haraldur"
+	private final Author[] AUTHORS = {
+			authorFactory.createAuthor("Guðmundur", new byte[42]),
+			authorFactory.createAuthor("Jónas", new byte[42]),
+			authorFactory.createAuthor(
+					"Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen",
+					new byte[42]),
+			authorFactory.createAuthor("Baldur Friðrik", new byte[42]),
+			authorFactory.createAuthor("Anna Katrín", new byte[42]),
+			authorFactory.createAuthor("Þór", new byte[42]),
+			authorFactory.createAuthor("Anna Þorbjörg", new byte[42]),
+			authorFactory.createAuthor("Guðrún", new byte[42]),
+			authorFactory.createAuthor("Helga", new byte[42]),
+			authorFactory.createAuthor("Haraldur", new byte[42])
 	};
 
-	private final static AuthorId[] AUTHOR_ID = new AuthorId[AUTHORS.length];
-
-	static {
-		SecureRandom random = new SecureRandom();
-		for (int i = 0; i < AUTHOR_ID.length; i++) {
-			byte[] b = new byte[UniqueId.LENGTH];
-			random.nextBytes(b);
-			AUTHOR_ID[i] = new AuthorId(b);
-
-		}
-	}
-
 	private final static String SAGA =
 			"Það er upphaf á sögu þessari að Hákon konungur " +
 					"Aðalsteinsfóstri réð fyrir Noregi og var þetta á ofanverðum " +
@@ -117,8 +111,7 @@ public class ForumTestControllerImpl implements ForumController {
 			random.nextBytes(b);
 			forumEntries[e] =
 					new ForumEntry(new MessageId(b), SAGA.substring(0, i[e]),
-							l[e], timestamp, AUTHORS[authorIndex],
-							AUTHOR_ID[authorIndex], UNVERIFIED);
+							l[e], timestamp, AUTHORS[authorIndex], UNVERIFIED);
 		}
 		LOG.info("forum entries: " + forumEntries.length);
 		resultHandler.onResult(true);
diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
index bbbb14e460b70ce95416bd60b61d1a591102fa4b..ce25c8836ac5618799b7080453b1d1b8cdeaa674 100644
--- a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
+++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java
@@ -17,7 +17,7 @@ import org.briarproject.android.contact.ContactListAdapter;
 import org.briarproject.android.contact.ContactListItem;
 import org.briarproject.android.contact.ConversationItem;
 import org.briarproject.android.fragment.BaseFragment;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
index 268b16b85ecc8822b0ae808fdc742bfa58540cd7..ef8dcc55ebf9f4fca1ae931b7d239a0bbfa02d04 100644
--- a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
+++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
@@ -23,7 +23,7 @@ import org.briarproject.R;
 import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.api.AndroidExecutor;
 import org.briarproject.android.fragment.BaseEventFragment;
-import org.briarproject.android.util.CameraView;
+import org.briarproject.android.view.CameraView;
 import org.briarproject.android.util.QrCodeDecoder;
 import org.briarproject.android.util.QrCodeUtils;
 import org.briarproject.api.event.Event;
diff --git a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
index a1671e2e24b16dfb78487e59b88b55ab704ba8cf..951b49c09809d6eced379b3ac61ea5cfe5f1d69b 100644
--- a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
+++ b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java
@@ -17,7 +17,7 @@ import org.briarproject.android.ActivityComponent;
 import org.briarproject.android.contact.BaseContactListAdapter;
 import org.briarproject.android.contact.ContactListItem;
 import org.briarproject.android.fragment.BaseFragment;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
index f4c988454856d1f8f59c582f37f6639cc4fda225..00852f7e9cbb66b90e20cf9fa310b0c7cb27f105 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java
@@ -10,7 +10,7 @@ import android.widget.Button;
 import android.widget.TextView;
 
 import org.briarproject.R;
-import org.briarproject.android.util.TextAvatarView;
+import org.briarproject.android.view.TextAvatarView;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.sharing.InvitationItem;
 import org.briarproject.util.StringUtils;
diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
index fc6761e058ee516f8cb7f5ed7da6cee0b6d19189..c7d99b14bd0f8990fd0d8617be028fd00e20dce9 100644
--- a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java
@@ -7,7 +7,7 @@ import android.widget.Toast;
 
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
diff --git a/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java b/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java
index 6a67448c9e2f911f5e3a5a5a4a2992758c91c572..0d69fe6d293dfce17e233b07def8bf030a649ae2 100644
--- a/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java
+++ b/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java
@@ -8,7 +8,7 @@ import android.view.MenuItem;
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.contact.ContactListItem;
-import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.android.view.BriarRecyclerView;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.identity.IdentityManager;
diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
index 759a25e6b47b56bb36af1cb51fba0bd364f3dd89..319c1854fc7a25cb738934cbcafd9dc1fdf38c0d 100644
--- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java
+++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java
@@ -15,7 +15,6 @@ import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.format.DateUtils;
-import android.text.method.LinkMovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.URLSpan;
@@ -23,6 +22,7 @@ import android.view.View;
 import android.widget.TextView;
 
 import org.briarproject.R;
+import org.briarproject.android.view.ArticleMovementMethod;
 import org.briarproject.android.widget.LinkDialogFragment;
 import org.briarproject.util.IoUtils;
 import org.briarproject.util.StringUtils;
diff --git a/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java b/briar-android/src/org/briarproject/android/view/ArticleMovementMethod.java
similarity index 97%
rename from briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java
rename to briar-android/src/org/briarproject/android/view/ArticleMovementMethod.java
index ad148a719d9e78e6e7f822c6724bd7bec9268804..d02ca1081e7904d00c7ca0d4c9d24c37e7bd7680 100644
--- a/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java
+++ b/briar-android/src/org/briarproject/android/view/ArticleMovementMethod.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.text.Layout;
 import android.text.Spannable;
diff --git a/briar-android/src/org/briarproject/android/util/AuthorView.java b/briar-android/src/org/briarproject/android/view/AuthorView.java
similarity index 95%
rename from briar-android/src/org/briarproject/android/util/AuthorView.java
rename to briar-android/src/org/briarproject/android/view/AuthorView.java
index 45ab5d34741fc519a45c92f2c371b99851b1b004..40f8e50693e96fc29f25b385c6cfc0242c535dbe 100644
--- a/briar-android/src/org/briarproject/android/util/AuthorView.java
+++ b/briar-android/src/org/briarproject/android/view/AuthorView.java
@@ -1,10 +1,11 @@
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.TypedArray;
 import android.graphics.Typeface;
 import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
 import android.support.v4.app.ActivityOptionsCompat;
 import android.support.v4.content.ContextCompat;
 import android.util.AttributeSet;
@@ -18,6 +19,8 @@ import android.widget.TextView;
 
 import org.briarproject.R;
 import org.briarproject.android.blogs.BlogActivity;
+import org.briarproject.android.util.AndroidUtils;
+import org.briarproject.android.view.TrustIndicatorView;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.Author.Status;
 import org.briarproject.api.sync.GroupId;
@@ -34,6 +37,7 @@ import static android.util.TypedValue.COMPLEX_UNIT_PX;
 import static org.briarproject.android.BriarActivity.GROUP_ID;
 import static org.briarproject.api.identity.Author.Status.OURSELVES;
 
+@UiThread
 public class AuthorView extends RelativeLayout {
 
 	private final CircleImageView avatar;
diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/view/BriarRecyclerView.java
similarity index 96%
rename from briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
rename to briar-android/src/org/briarproject/android/view/BriarRecyclerView.java
index affbe9e13b62331b7672de62a10323ab4d7a646a..40f455404d96575b3a5a838361b01c9cf2bb7a81 100644
--- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java
+++ b/briar-android/src/org/briarproject/android/view/BriarRecyclerView.java
@@ -1,7 +1,8 @@
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.content.Context;
 import android.content.res.TypedArray;
+import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.Adapter;
 import android.util.AttributeSet;
@@ -34,11 +35,11 @@ public class BriarRecyclerView extends FrameLayout {
 		this(context, null, 0);
 	}
 
-	public BriarRecyclerView(Context context, AttributeSet attrs) {
+	public BriarRecyclerView(Context context, @Nullable AttributeSet attrs) {
 		this(context, attrs, 0);
 	}
 
-	public BriarRecyclerView(Context context, AttributeSet attrs,
+	public BriarRecyclerView(Context context, @Nullable AttributeSet attrs,
 			int defStyle) {
 		super(context, attrs, defStyle);
 
diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerViewBehavior.java b/briar-android/src/org/briarproject/android/view/BriarRecyclerViewBehavior.java
similarity index 96%
rename from briar-android/src/org/briarproject/android/util/BriarRecyclerViewBehavior.java
rename to briar-android/src/org/briarproject/android/view/BriarRecyclerViewBehavior.java
index fc57a27f0f300b519bd2ae094587597350c71a83..cfaa352cd430dfa8895f6ebf48a06a61947ecfb4 100644
--- a/briar-android/src/org/briarproject/android/util/BriarRecyclerViewBehavior.java
+++ b/briar-android/src/org/briarproject/android/view/BriarRecyclerViewBehavior.java
@@ -1,4 +1,4 @@
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.content.Context;
 import android.support.design.widget.CoordinatorLayout;
diff --git a/briar-android/src/org/briarproject/android/util/CameraView.java b/briar-android/src/org/briarproject/android/view/CameraView.java
similarity index 98%
rename from briar-android/src/org/briarproject/android/util/CameraView.java
rename to briar-android/src/org/briarproject/android/view/CameraView.java
index 79ea7eaddf21a36139608d7a2115d8da12517a72..0ce1ad13627837ac2a3bda936a99ff8bc3eb16b0 100644
--- a/briar-android/src/org/briarproject/android/util/CameraView.java
+++ b/briar-android/src/org/briarproject/android/view/CameraView.java
@@ -1,4 +1,4 @@
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.content.Context;
 import android.hardware.Camera;
@@ -12,6 +12,8 @@ import android.util.DisplayMetrics;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 
+import org.briarproject.android.util.PreviewConsumer;
+
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
diff --git a/briar-android/src/org/briarproject/android/util/TextAvatarView.java b/briar-android/src/org/briarproject/android/view/TextAvatarView.java
similarity index 92%
rename from briar-android/src/org/briarproject/android/util/TextAvatarView.java
rename to briar-android/src/org/briarproject/android/view/TextAvatarView.java
index b48d0e7ad1a0280b30f544677ad7ed1297d7357c..70ab3f3981bc18138cce89dfee9a5d6c791c0199 100644
--- a/briar-android/src/org/briarproject/android/util/TextAvatarView.java
+++ b/briar-android/src/org/briarproject/android/view/TextAvatarView.java
@@ -1,8 +1,10 @@
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.widget.AppCompatTextView;
 import android.util.AttributeSet;
@@ -16,6 +18,7 @@ import org.briarproject.api.identity.Author;
 import de.hdodenhof.circleimageview.CircleImageView;
 import im.delight.android.identicons.IdenticonDrawable;
 
+@UiThread
 public class TextAvatarView extends FrameLayout {
 
 	final private AppCompatTextView character;
@@ -23,7 +26,7 @@ public class TextAvatarView extends FrameLayout {
 	final private TextView badge;
 	private int unreadCount;
 
-	public TextAvatarView(Context context, AttributeSet attrs) {
+	public TextAvatarView(Context context, @Nullable AttributeSet attrs) {
 		super(context, attrs);
 
 		LayoutInflater inflater = (LayoutInflater) context
diff --git a/briar-android/src/org/briarproject/android/view/TextInputView.java b/briar-android/src/org/briarproject/android/view/TextInputView.java
new file mode 100644
index 0000000000000000000000000000000000000000..e257a1915efe96a9272023a6ba2ebefa1256003e
--- /dev/null
+++ b/briar-android/src/org/briarproject/android/view/TextInputView.java
@@ -0,0 +1,171 @@
+package org.briarproject.android.view;
+
+import android.content.Context;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.UiThread;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import org.briarproject.R;
+import org.thoughtcrime.securesms.components.KeyboardAwareRelativeLayout;
+import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
+import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiEventListener;
+import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
+import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
+
+import java.util.logging.Logger;
+
+import static android.content.Context.INPUT_METHOD_SERVICE;
+
+@UiThread
+public class TextInputView extends KeyboardAwareRelativeLayout
+		implements EmojiEventListener {
+
+	private static final String TAG = TextInputView.class.getName();
+	private static final Logger LOG = Logger.getLogger(TAG);
+
+	private EmojiEditText editText;
+	private View sendButton;
+	private EmojiDrawer emojiDrawer;
+
+	private TextInputListener listener;
+
+	public TextInputView(Context context) {
+		this(context, null);
+	}
+
+	public TextInputView(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public TextInputView(Context context, @Nullable AttributeSet attrs,
+			int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+
+		LayoutInflater inflater = (LayoutInflater) context
+				.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+		inflater.inflate(R.layout.text_input_view, this, true);
+
+		// find views
+		EmojiToggle emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle);
+		editText = (EmojiEditText) findViewById(R.id.input_text);
+		emojiDrawer = (EmojiDrawer) findViewById(R.id.emoji_drawer);
+		sendButton = findViewById(R.id.btn_send);
+
+		emojiToggle.attach(emojiDrawer);
+		emojiToggle.setOnClickListener(new OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				onEmojiToggleClicked();
+			}
+		});
+		editText.setOnClickListener(new OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				showSoftKeyboard();
+			}
+		});
+		sendButton.setOnClickListener(new OnClickListener() {
+			@Override
+			public void onClick(View v) {
+				if (listener != null) {
+					listener.onSendClick(editText.getText().toString());
+					editText.setText("");
+				}
+			}
+		});
+		emojiDrawer.setEmojiEventListener(this);
+	}
+
+	@Override
+	public void onKeyEvent(KeyEvent keyEvent) {
+		editText.dispatchKeyEvent(keyEvent);
+	}
+
+	@Override
+	public void onEmojiSelected(String emoji) {
+		editText.insertEmoji(emoji);
+	}
+
+	private void onEmojiToggleClicked() {
+		if (isEmojiDrawerOpen()) {
+			showSoftKeyboard();
+		} else {
+			showEmojiDrawer();
+		}
+	}
+
+	public void setText(String text) {
+		editText.setText(text);
+	}
+
+	public void setHint(@StringRes int res) {
+		editText.setHint(res);
+	}
+
+	public void setSendButtonEnabled(boolean enabled) {
+		sendButton.setEnabled(enabled);
+	}
+
+	public void setListener(TextInputListener listener) {
+		this.listener = listener;
+	}
+
+	public interface TextInputListener {
+		void onSendClick(String text);
+	}
+
+	public void showSoftKeyboard() {
+		if (isKeyboardOpen()) return;
+
+		if (emojiDrawer.isShowing()) {
+			postOnKeyboardOpen(new Runnable() {
+				@Override
+				public void run() {
+					hideEmojiDrawer();
+				}
+			});
+		}
+		editText.post(new Runnable() {
+			@Override
+			public void run() {
+				editText.requestFocus();
+				Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
+				((InputMethodManager) o).showSoftInput(editText, 0);
+			}
+		});
+	}
+
+	public void hideSoftKeyboard() {
+		IBinder token = editText.getWindowToken();
+		Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
+		((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
+	}
+
+	public void showEmojiDrawer() {
+		if (isKeyboardOpen()) {
+			postOnKeyboardClose(new Runnable() {
+				@Override public void run() {
+					emojiDrawer.show(getKeyboardHeight());
+				}
+			});
+			hideSoftKeyboard();
+		} else {
+			emojiDrawer.show(getKeyboardHeight());
+		}
+	}
+
+	public void hideEmojiDrawer() {
+		emojiDrawer.hide();
+	}
+
+	public boolean isEmojiDrawerOpen() {
+		return emojiDrawer.isShowing();
+	}
+
+}
diff --git a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java b/briar-android/src/org/briarproject/android/view/TrustIndicatorView.java
similarity index 92%
rename from briar-android/src/org/briarproject/android/util/TrustIndicatorView.java
rename to briar-android/src/org/briarproject/android/view/TrustIndicatorView.java
index 97d8e7763609a175d86a9e18199098239720bfbf..5a88acdf6850fc5244e1c690e8fb897a60cd9d67 100644
--- a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java
+++ b/briar-android/src/org/briarproject/android/view/TrustIndicatorView.java
@@ -1,6 +1,7 @@
-package org.briarproject.android.util;
+package org.briarproject.android.view;
 
 import android.content.Context;
+import android.support.annotation.UiThread;
 import android.support.v4.content.ContextCompat;
 import android.util.AttributeSet;
 import android.widget.ImageView;
@@ -10,6 +11,7 @@ import org.briarproject.api.identity.Author.Status;
 
 import static org.briarproject.api.identity.Author.Status.OURSELVES;
 
+@UiThread
 public class TrustIndicatorView extends ImageView {
 
 	public TrustIndicatorView(Context context) {
diff --git a/briar-android/src/org/thoughtcrime/securesms/LICENSE b/briar-android/src/org/thoughtcrime/securesms/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..94a045322262546cfb9d72561e1d587b5c2ffb1e
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/LICENSE
@@ -0,0 +1,621 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/KeyboardAwareRelativeLayout.java b/briar-android/src/org/thoughtcrime/securesms/components/KeyboardAwareRelativeLayout.java
new file mode 100644
index 0000000000000000000000000000000000000000..5217078252324fab5d8e0ce730cbfca0e5ec25f3
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/KeyboardAwareRelativeLayout.java
@@ -0,0 +1,270 @@
+package org.thoughtcrime.securesms.components;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Rect;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.RelativeLayout;
+
+import org.briarproject.R;
+
+import java.lang.reflect.Field;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import static android.content.Context.WINDOW_SERVICE;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+/**
+ * RelativeLayout that, when a view container, will report back when it thinks
+ * a soft keyboard has been opened and what its height would be.
+ */
+@UiThread
+public class KeyboardAwareRelativeLayout extends RelativeLayout {
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyboardAwareRelativeLayout.class.getName());
+
+	private final Rect rect = new Rect();
+	private final Set<OnKeyboardHiddenListener> hiddenListeners =
+			new HashSet<>();
+	private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
+	private final int minKeyboardSize;
+	private final int minCustomKeyboardSize;
+	private final int defaultCustomKeyboardSize;
+	private final int minCustomKeyboardTopMargin;
+	private final int statusBarHeight;
+
+	private int viewInset;
+
+	private boolean keyboardOpen = false;
+	private int rotation = -1;
+
+	public KeyboardAwareRelativeLayout(Context context) {
+		this(context, null);
+	}
+
+	public KeyboardAwareRelativeLayout(Context context,
+			@Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public KeyboardAwareRelativeLayout(Context context,
+			@Nullable AttributeSet attrs, int defStyle) {
+		super(context, attrs, defStyle);
+		final int statusBarRes = getResources()
+				.getIdentifier("status_bar_height", "dimen", "android");
+		minKeyboardSize =
+				getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
+		minCustomKeyboardSize = getResources()
+				.getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
+		defaultCustomKeyboardSize = getResources()
+				.getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
+		minCustomKeyboardTopMargin = getResources()
+				.getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin);
+		statusBarHeight = statusBarRes > 0 ?
+				getResources().getDimensionPixelSize(statusBarRes) : 0;
+		viewInset = getViewInset();
+	}
+
+	@Override
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+		updateRotation();
+		updateKeyboardState();
+		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+	}
+
+	private void updateRotation() {
+		int oldRotation = rotation;
+		rotation = getDeviceRotation();
+		if (oldRotation != rotation) {
+			LOG.info("Rotation changed");
+			onKeyboardClose();
+		}
+	}
+
+	private void updateKeyboardState() {
+		if (isLandscape()) {
+			if (keyboardOpen) onKeyboardClose();
+			return;
+		}
+
+		if (viewInset == 0 && Build.VERSION.SDK_INT >= 21)
+			viewInset = getViewInset();
+		int availableHeight =
+				getRootView().getHeight() - statusBarHeight - viewInset;
+		getWindowVisibleDisplayFrame(rect);
+
+		int keyboardHeight = availableHeight - (rect.bottom - rect.top);
+
+		if (keyboardHeight > minKeyboardSize) {
+			if (getKeyboardHeight() != keyboardHeight)
+				setKeyboardPortraitHeight(keyboardHeight);
+			if (!keyboardOpen) onKeyboardOpen(keyboardHeight);
+		} else if (keyboardOpen) {
+			onKeyboardClose();
+		}
+	}
+
+	@TargetApi(21)
+	private int getViewInset() {
+		try {
+			Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
+			attachInfoField.setAccessible(true);
+			Object attachInfo = attachInfoField.get(this);
+			if (attachInfo != null) {
+				Field stableInsetsField =
+						attachInfo.getClass().getDeclaredField("mStableInsets");
+				stableInsetsField.setAccessible(true);
+				Rect insets = (Rect) stableInsetsField.get(attachInfo);
+				return insets.bottom;
+			}
+		} catch (NoSuchFieldException e) {
+			LOG.log(WARNING,
+					"field reflection error when measuring view inset", e);
+		} catch (IllegalAccessException e) {
+			LOG.log(WARNING,
+					"access reflection error when measuring view inset", e);
+		}
+		return 0;
+	}
+
+	protected void onKeyboardOpen(int keyboardHeight) {
+		if (LOG.isLoggable(INFO))
+			LOG.info("onKeyboardOpen(" + keyboardHeight + ")");
+		keyboardOpen = true;
+
+		notifyShownListeners();
+	}
+
+	protected void onKeyboardClose() {
+		LOG.info("onKeyboardClose()");
+		keyboardOpen = false;
+		notifyHiddenListeners();
+	}
+
+	public boolean isKeyboardOpen() {
+		return keyboardOpen;
+	}
+
+	public int getKeyboardHeight() {
+		return isLandscape() ? getKeyboardLandscapeHeight() :
+				getKeyboardPortraitHeight();
+	}
+
+	public boolean isLandscape() {
+		int rotation = getDeviceRotation();
+		return rotation == ROTATION_90 || rotation == ROTATION_270;
+	}
+
+	private int getDeviceRotation() {
+		WindowManager windowManager =
+				(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
+		return windowManager.getDefaultDisplay().getRotation();
+	}
+
+	private int getKeyboardLandscapeHeight() {
+		return Math.max(getHeight(), getRootView().getHeight()) / 2;
+	}
+
+	private int getKeyboardPortraitHeight() {
+		SharedPreferences prefs =
+				PreferenceManager.getDefaultSharedPreferences(getContext());
+		int keyboardHeight = prefs.getInt("keyboard_height_portrait",
+				defaultCustomKeyboardSize);
+		return clamp(keyboardHeight, minCustomKeyboardSize,
+				getRootView().getHeight() - minCustomKeyboardTopMargin);
+	}
+
+	private int clamp(int value, int min, int max) {
+		return Math.min(Math.max(value, min), max);
+	}
+
+	private void setKeyboardPortraitHeight(int height) {
+		SharedPreferences prefs =
+				PreferenceManager.getDefaultSharedPreferences(getContext());
+		prefs.edit().putInt("keyboard_height_portrait", height).apply();
+	}
+
+	public void postOnKeyboardClose(final Runnable runnable) {
+		if (keyboardOpen) {
+			addOnKeyboardHiddenListener(new OnKeyboardHiddenListener() {
+				@Override
+				public void onKeyboardHidden() {
+					removeOnKeyboardHiddenListener(this);
+					runnable.run();
+				}
+			});
+		} else {
+			runnable.run();
+		}
+	}
+
+	public void postOnKeyboardOpen(final Runnable runnable) {
+		if (!keyboardOpen) {
+			addOnKeyboardShownListener(new OnKeyboardShownListener() {
+				@Override
+				public void onKeyboardShown() {
+					removeOnKeyboardShownListener(this);
+					runnable.run();
+				}
+			});
+		} else {
+			runnable.run();
+		}
+	}
+
+	public void addOnKeyboardHiddenListener(OnKeyboardHiddenListener listener) {
+		hiddenListeners.add(listener);
+	}
+
+	public void removeOnKeyboardHiddenListener(
+			OnKeyboardHiddenListener listener) {
+		hiddenListeners.remove(listener);
+	}
+
+	public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
+		shownListeners.add(listener);
+	}
+
+	public void removeOnKeyboardShownListener(
+			OnKeyboardShownListener listener) {
+		shownListeners.remove(listener);
+	}
+
+	private void notifyHiddenListeners() {
+		// Make a copy as listeners may remove themselves when called
+		Set<OnKeyboardHiddenListener> listeners =
+				new HashSet<>(hiddenListeners);
+		for (OnKeyboardHiddenListener listener : listeners) {
+			listener.onKeyboardHidden();
+		}
+	}
+
+	private void notifyShownListeners() {
+		// Make a copy as listeners may remove themselves when called
+		Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
+		for (OnKeyboardShownListener listener : listeners) {
+			listener.onKeyboardShown();
+		}
+	}
+
+	public interface OnKeyboardHiddenListener {
+		void onKeyboardHidden();
+	}
+
+	public interface OnKeyboardShownListener {
+		void onKeyboardShown();
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/RepeatableImageKey.java b/briar-android/src/org/thoughtcrime/securesms/components/RepeatableImageKey.java
new file mode 100644
index 0000000000000000000000000000000000000000..453f82f8cf0d79feeca1b74fd27625f30c74ac4c
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/RepeatableImageKey.java
@@ -0,0 +1,94 @@
+package org.thoughtcrime.securesms.components;
+
+import android.content.Context;
+import android.support.annotation.UiThread;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.ImageButton;
+
+import static android.view.HapticFeedbackConstants.KEYBOARD_TAP;
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
+
+@UiThread
+public class RepeatableImageKey extends ImageButton {
+
+	private KeyEventListener listener;
+
+	public RepeatableImageKey(Context context) {
+		super(context);
+		init();
+	}
+
+	public RepeatableImageKey(Context context, AttributeSet attrs) {
+		super(context, attrs);
+		init();
+	}
+
+	public RepeatableImageKey(Context context, AttributeSet attrs,
+			int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+		init();
+	}
+
+	private void init() {
+		setOnClickListener(new RepeaterClickListener());
+		setOnTouchListener(new RepeaterTouchListener());
+	}
+
+	public void setOnKeyEventListener(KeyEventListener listener) {
+		this.listener = listener;
+	}
+
+	private void notifyListener() {
+		if (listener != null) listener.onKeyEvent();
+	}
+
+	private class RepeaterClickListener implements OnClickListener {
+		@Override
+		public void onClick(View v) {
+			notifyListener();
+		}
+	}
+
+	private class Repeater implements Runnable {
+		@Override
+		public void run() {
+			notifyListener();
+			postDelayed(this, ViewConfiguration.getKeyRepeatDelay());
+		}
+	}
+
+	private class RepeaterTouchListener implements OnTouchListener {
+
+		private final Repeater repeater;
+
+		private RepeaterTouchListener() {
+			repeater = new Repeater();
+		}
+
+		@Override
+		public boolean onTouch(View view, MotionEvent motionEvent) {
+			switch (motionEvent.getAction()) {
+				case ACTION_DOWN:
+					view.postDelayed(repeater,
+							ViewConfiguration.getKeyRepeatTimeout());
+					performHapticFeedback(KEYBOARD_TAP);
+					return false;
+				case ACTION_CANCEL:
+				case ACTION_UP:
+					view.removeCallbacks(repeater);
+					return false;
+				default:
+					return false;
+			}
+		}
+	}
+
+	public interface KeyEventListener {
+		void onKeyEvent();
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/AnimatingImageSpan.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/AnimatingImageSpan.java
new file mode 100644
index 0000000000000000000000000000000000000000..28a71933f274d115dbf4e438a4db2c79ffcc257f
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/AnimatingImageSpan.java
@@ -0,0 +1,15 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Drawable.Callback;
+import android.support.annotation.UiThread;
+import android.text.style.ImageSpan;
+
+@UiThread
+class AnimatingImageSpan extends ImageSpan {
+
+	AnimatingImageSpan(Drawable drawable, Callback callback) {
+		super(drawable, ALIGN_BOTTOM);
+		drawable.setCallback(callback);
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java
new file mode 100644
index 0000000000000000000000000000000000000000..69234600f6b1c5e70799b4cf7b30edf0bc98d791
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java
@@ -0,0 +1,201 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.astuetz.PagerSlidingTabStrip;
+import com.astuetz.PagerSlidingTabStrip.CustomTabProvider;
+
+import org.briarproject.R;
+import org.thoughtcrime.securesms.components.RepeatableImageKey;
+import org.thoughtcrime.securesms.components.RepeatableImageKey.KeyEventListener;
+import org.thoughtcrime.securesms.components.emoji.EmojiPageView.EmojiSelectionListener;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static android.view.KeyEvent.ACTION_DOWN;
+import static android.view.KeyEvent.KEYCODE_DEL;
+import static android.widget.ImageView.ScaleType.CENTER_INSIDE;
+import static java.util.logging.Level.INFO;
+
+@UiThread
+public class EmojiDrawer extends LinearLayout {
+
+	private static final Logger LOG =
+			Logger.getLogger(EmojiDrawer.class.getName());
+	private static final KeyEvent DELETE_KEY_EVENT =
+			new KeyEvent(ACTION_DOWN, KEYCODE_DEL);
+
+	private ViewPager pager;
+	private List<EmojiPageModel> models;
+	private PagerSlidingTabStrip strip;
+	private RecentEmojiPageModel recentModel;
+	private EmojiEventListener listener;
+	private EmojiDrawerListener drawerListener;
+
+	public EmojiDrawer(Context context) {
+		this(context, null);
+	}
+
+	public EmojiDrawer(Context context, @Nullable AttributeSet attrs) {
+		super(context, attrs);
+		setOrientation(VERTICAL);
+	}
+
+	private void initView() {
+		final View v = LayoutInflater.from(getContext())
+				.inflate(R.layout.emoji_drawer, this, true);
+		initializeResources(v);
+		initializePageModels();
+		initializeEmojiGrid();
+	}
+
+	public void setEmojiEventListener(EmojiEventListener listener) {
+		this.listener = listener;
+	}
+
+	public void setDrawerListener(EmojiDrawerListener listener) {
+		this.drawerListener = listener;
+	}
+
+	private void initializeResources(View v) {
+		this.pager = (ViewPager) v.findViewById(R.id.emoji_pager);
+		this.strip = (PagerSlidingTabStrip) v.findViewById(R.id.tabs);
+
+		RepeatableImageKey backspace =
+				(RepeatableImageKey) v.findViewById(R.id.backspace);
+		backspace.setOnKeyEventListener(new KeyEventListener() {
+			@Override
+			public void onKeyEvent() {
+				if (listener != null) listener.onKeyEvent(DELETE_KEY_EVENT);
+			}
+		});
+	}
+
+	public boolean isShowing() {
+		return getVisibility() == VISIBLE;
+	}
+
+	public void show(int height) {
+		if (this.pager == null) initView();
+		ViewGroup.LayoutParams params = getLayoutParams();
+		params.height = height;
+		if (LOG.isLoggable(INFO))
+			LOG.info("Showing emoji drawer with height " + params.height);
+		setLayoutParams(params);
+		setVisibility(VISIBLE);
+		if (drawerListener != null) drawerListener.onShown();
+	}
+
+	public void hide() {
+		setVisibility(GONE);
+		if (drawerListener != null) drawerListener.onHidden();
+	}
+
+	private void initializeEmojiGrid() {
+		pager.setAdapter(new EmojiPagerAdapter(getContext(),
+				models,
+				new EmojiSelectionListener() {
+					@Override
+					public void onEmojiSelected(String emoji) {
+						recentModel.onCodePointSelected(emoji);
+						if (listener != null) listener.onEmojiSelected(emoji);
+					}
+				}));
+
+		if (recentModel.getEmoji().length == 0) {
+			pager.setCurrentItem(1);
+		}
+		strip.setViewPager(pager);
+	}
+
+	private void initializePageModels() {
+		this.models = new LinkedList<>();
+		this.recentModel = new RecentEmojiPageModel(getContext());
+		this.models.add(recentModel);
+		this.models.addAll(EmojiProvider.getInstance(getContext())
+				.getStaticPages());
+	}
+
+	public static class EmojiPagerAdapter extends PagerAdapter
+			implements CustomTabProvider {
+		private Context context;
+		private List<EmojiPageModel> pages;
+		private EmojiSelectionListener listener;
+
+		private EmojiPagerAdapter(@NonNull Context context,
+				@NonNull List<EmojiPageModel> pages,
+				@Nullable EmojiSelectionListener listener) {
+			super();
+			this.context = context;
+			this.pages = pages;
+			this.listener = listener;
+		}
+
+		@Override
+		public int getCount() {
+			return pages.size();
+		}
+
+		@Override
+		public Object instantiateItem(ViewGroup container, int position) {
+			EmojiPageView page = new EmojiPageView(context);
+			page.setModel(pages.get(position));
+			page.setEmojiSelectedListener(listener);
+			container.addView(page);
+			return page;
+		}
+
+		@Override
+		public void destroyItem(ViewGroup container, int position,
+				Object object) {
+			container.removeView((View) object);
+		}
+
+		@Override
+		public boolean isViewFromObject(View view, Object object) {
+			return view == object;
+		}
+
+		@Override
+		public View getCustomTabView(ViewGroup viewGroup, int i) {
+			ImageView image = new ImageView(context);
+			image.setScaleType(CENTER_INSIDE);
+			image.setImageResource(pages.get(i).getIcon());
+			return image;
+		}
+
+		@Override
+		public void tabSelected(View view) {
+			view.animate().setDuration(300).alpha(1);
+		}
+
+		@Override
+		public void tabUnselected(View view) {
+			view.animate().setDuration(400).alpha(0.4f);
+		}
+	}
+
+	public interface EmojiEventListener extends EmojiSelectionListener {
+		void onKeyEvent(KeyEvent keyEvent);
+	}
+
+	public interface EmojiDrawerListener {
+		void onShown();
+
+		void onHidden();
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java
new file mode 100644
index 0000000000000000000000000000000000000000..a8a25671ed4821551c53ee4eec5f80063f7061fd
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java
@@ -0,0 +1,47 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.AppCompatEditText;
+import android.text.InputFilter;
+import android.util.AttributeSet;
+
+import org.briarproject.R;
+import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
+
+@UiThread
+public class EmojiEditText extends AppCompatEditText {
+
+	public EmojiEditText(Context context) {
+		this(context, null);
+	}
+
+	public EmojiEditText(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, R.attr.editTextStyle);
+	}
+
+	public EmojiEditText(Context context, @Nullable AttributeSet attrs,
+			int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+		// this ensures the view is redrawn when invalidated
+		setLayerType(LAYER_TYPE_SOFTWARE, null);
+		setFilters(new InputFilter[] {new EmojiFilter(this)});
+	}
+
+	public void insertEmoji(String emoji) {
+		final int start = getSelectionStart();
+		final int end = getSelectionEnd();
+
+		getText().replace(Math.min(start, end), Math.max(start, end), emoji);
+		setSelection(start + emoji.length());
+	}
+
+	@Override
+	public void invalidateDrawable(@NonNull Drawable drawable) {
+		if (drawable instanceof EmojiDrawable) invalidate();
+		else super.invalidateDrawable(drawable);
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..603544869f954448a22f35059f34b61198853e17
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java
@@ -0,0 +1,35 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.text.InputFilter;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.widget.TextView;
+
+@UiThread
+class EmojiFilter implements InputFilter {
+
+	private final TextView view;
+
+	public EmojiFilter(TextView view) {
+		this.view = view;
+	}
+
+	@Nullable
+	@Override
+	public CharSequence filter(CharSequence source, int start, int end,
+			Spanned dest, int dstart, int dend) {
+
+		char[] v = new char[end - start];
+		TextUtils.getChars(source, start, end, v, 0);
+		Spannable emojified = EmojiProvider.getInstance(view.getContext())
+				.emojify(new String(v), view);
+		if (source instanceof Spanned && emojified != null) {
+			TextUtils.copySpansFrom((Spanned) source, start, end, null,
+					emojified, 0);
+		}
+		return emojified;
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e0b899d54fddb7cb928a081515a018d482469e5
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java
@@ -0,0 +1,19 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+interface EmojiPageModel {
+
+	@DrawableRes
+	int getIcon();
+
+	@NonNull
+	String[] getEmoji();
+
+	boolean hasSpriteMap();
+
+	@Nullable
+	String getSprite();
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java
new file mode 100644
index 0000000000000000000000000000000000000000..fa5e97c8eef6983e1fda830c524c5eb2ee3345ae
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java
@@ -0,0 +1,115 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.GridView;
+
+import org.briarproject.R;
+
+@UiThread
+public class EmojiPageView extends FrameLayout {
+
+	private final GridView grid;
+
+	private EmojiSelectionListener listener;
+
+	public EmojiPageView(Context context) {
+		this(context, null);
+	}
+
+	public EmojiPageView(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public EmojiPageView(Context context, @Nullable AttributeSet attrs,
+			int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+		final View view = LayoutInflater.from(getContext())
+				.inflate(R.layout.emoji_grid_layout, this, true);
+		grid = (GridView) view.findViewById(R.id.emoji);
+		grid.setColumnWidth(getResources()
+				.getDimensionPixelSize(R.dimen.emoji_drawer_size) + 2 *
+				getResources().getDimensionPixelSize(
+						R.dimen.emoji_drawer_item_padding));
+		grid.setOnItemClickListener(new OnItemClickListener() {
+			@Override
+			public void onItemClick(AdapterView<?> parent, View view,
+					int position, long id) {
+				if (listener != null)
+					listener.onEmojiSelected(((EmojiView) view).getEmoji());
+			}
+		});
+	}
+
+	public void setModel(EmojiPageModel model) {
+		grid.setAdapter(new EmojiGridAdapter(getContext(), model));
+	}
+
+	public void setEmojiSelectedListener(EmojiSelectionListener listener) {
+		this.listener = listener;
+	}
+
+	private static class EmojiGridAdapter extends BaseAdapter {
+
+		private final Context context;
+		private final EmojiPageModel model;
+		private final int emojiSize;
+
+		private EmojiGridAdapter(Context context, EmojiPageModel model) {
+			this.context = context;
+			this.model = model;
+			emojiSize = (int) context.getResources()
+					.getDimension(R.dimen.emoji_drawer_size);
+		}
+
+		@Override
+		public int getCount() {
+			return model.getEmoji().length;
+		}
+
+		@Nullable
+		@Override
+		public Object getItem(int position) {
+			return null;
+		}
+
+		@Override
+		public long getItemId(int position) {
+			return position;
+		}
+
+		@Override
+		public View getView(int position, View convertView, ViewGroup parent) {
+			EmojiView view;
+			int pad = context.getResources()
+					.getDimensionPixelSize(R.dimen.emoji_drawer_item_padding);
+			if (convertView != null && convertView instanceof EmojiView) {
+				view = (EmojiView) convertView;
+			} else {
+				EmojiView emojiView = new EmojiView(context);
+				emojiView.setPadding(pad, pad, pad, pad);
+				emojiView.setLayoutParams(
+						new AbsListView.LayoutParams(emojiSize + 2 * pad,
+								emojiSize + 2 * pad));
+				view = emojiView;
+			}
+
+			view.setEmoji(model.getEmoji()[position]);
+			return view;
+		}
+	}
+
+	interface EmojiSelectionListener {
+		void onEmojiSelected(String emoji);
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPages.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPages.java
new file mode 100644
index 0000000000000000000000000000000000000000..0544de13af91fd1374ce81b5e0033bb151b4115f
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPages.java
@@ -0,0 +1,65 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+
+import org.briarproject.R;
+
+import java.util.Arrays;
+import java.util.List;
+
+class EmojiPages {
+
+	static List<EmojiPageModel> getPages(Context ctx) {
+		return Arrays.<EmojiPageModel>asList(
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_smiley_people,
+						R.array.emoji_smiley_people,
+						"emoji_smiley_people.png"),
+				new StaticEmojiPageModel(ctx,
+						R.drawable.ic_emoji_animals_nature,
+						R.array.emoji_animals_nature,
+						"emoji_animals_nature.png"),
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_food_drink,
+						R.array.emoji_food_drink,
+						"emoji_food_drink.png"),
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_travel_places,
+						R.array.emoji_travel_places,
+						"emoji_travel_places.png"),
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_activity,
+						R.array.emoji_activity,
+						"emoji_activity.png"),
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_objects,
+						R.array.emoji_objects,
+						"emoji_objects.png"),
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_symbols,
+						R.array.emoji_symbols,
+						"emoji_symbols.png"),
+				new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_flags,
+						R.array.emoji_flags,
+						"emoji_flags.png"),
+
+				new StaticEmojiPageModel(R.drawable.ic_emoji_emoticons,
+						new String[] {
+								":-)", ";-)", "(-:", ":->", ":-D", "\\o/",
+								":-P", "B-)", ":-$", ":-*", "O:-)", "=-O",
+								"O_O", "O_o", "o_O", ":O", ":-!", ":-x",
+								":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(",
+								"^.^", "^_^", "\\(\u02c6\u02da\u02c6)/",
+								"\u30fd(\u00b0\u25c7\u00b0 )\u30ce",
+								"\u00af\\(\u00b0_o)/\u00af",
+								"\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)",
+								"(>_<)", "(\u2565\ufe4f\u2565)",
+								"(\u261e\uff9f\u30ee\uff9f)\u261e",
+								"\u261c(\uff9f\u30ee\uff9f\u261c)",
+								"\u261c(\u2312\u25bd\u2312)\u261e",
+								"(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35",
+								"\u253b\u2501\u253b",
+								"\u252c\u2500\u252c",
+								"\u30ce(\u00b0\u2013\u00b0\u30ce)",
+								"(^._.^)\uff89",
+								"\u0e05^\u2022\ufecc\u2022^\u0e05",
+								"(\u2022_\u2022)",
+								" \u25a0-\u25a0\u00ac <(\u2022_\u2022) ",
+								"(\u25a0_\u25a0\u00ac)"
+						}, null));
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ea99864e9755732f8555d27c481f06fc3436afb
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java
@@ -0,0 +1,310 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.util.SparseArray;
+import android.widget.TextView;
+
+import org.briarproject.R;
+import org.briarproject.android.BaseActivity;
+import org.briarproject.android.api.AndroidExecutor;
+import org.thoughtcrime.securesms.components.util.FutureTaskListener;
+import org.thoughtcrime.securesms.components.util.ListenableFutureTask;
+import org.thoughtcrime.securesms.util.BitmapDecodingException;
+import org.thoughtcrime.securesms.util.BitmapUtil;
+
+import java.io.IOException;
+import java.lang.ref.SoftReference;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+
+import static android.graphics.PixelFormat.TRANSLUCENT;
+import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
+public class EmojiProvider {
+
+	private static volatile EmojiProvider INSTANCE = null;
+
+	private static final Paint PAINT =
+			new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
+
+	@Inject
+	AndroidExecutor androidExecutor;
+
+	private static final Logger LOG =
+			Logger.getLogger(EmojiProvider.class.getName());
+
+	private final SparseArray<DrawInfo> offsets = new SparseArray<>();
+
+	private static final Pattern EMOJI_RANGE = Pattern.compile(
+			//     0x203c,0x2049  0x20a0-0x32ff          0x1f00-0x1fff              0xfe4e5-0xfe4ee
+			//   |=== !!, ?! ===||==== misc ===||========= emoticons =======||========== flags ==========|
+			"[\\u203c\\u2049\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83f\\udfff\\udbb9\\udce5-\\udbb9\\udcee]");
+
+	private static final int EMOJI_RAW_HEIGHT = 64;
+	private static final int EMOJI_RAW_WIDTH = 64;
+	private static final int EMOJI_VERT_PAD = 0;
+	private static final int EMOJI_PER_ROW = 32;
+
+	private final Context context;
+	private final float decodeScale, verticalPad;
+	private final List<EmojiPageModel> staticPages;
+
+	static EmojiProvider getInstance(Context context) {
+		if (INSTANCE == null) {
+			synchronized (EmojiProvider.class) {
+				if (INSTANCE == null) {
+					LOG.info("Creating new instance of EmojiProvider");
+					INSTANCE = new EmojiProvider(context);
+					((BaseActivity) context).getActivityComponent()
+							.inject(INSTANCE);
+				}
+			}
+		}
+		return INSTANCE;
+	}
+
+	private EmojiProvider(Context context) {
+		this.context = context.getApplicationContext();
+		float drawerSize =
+				context.getResources().getDimension(R.dimen.emoji_drawer_size);
+		decodeScale = Math.min(1f, drawerSize / EMOJI_RAW_HEIGHT);
+		verticalPad = EMOJI_VERT_PAD * this.decodeScale;
+		staticPages = EmojiPages.getPages(context);
+		for (EmojiPageModel page : staticPages) {
+			if (page.hasSpriteMap()) {
+				final EmojiPageBitmap pageBitmap = new EmojiPageBitmap(page);
+				for (int i = 0; i < page.getEmoji().length; i++) {
+					offsets.put(Character.codePointAt(page.getEmoji()[i], 0),
+							new DrawInfo(pageBitmap, i));
+				}
+			}
+		}
+	}
+
+	@Nullable
+	Spannable emojify(@Nullable CharSequence text,
+			@NonNull TextView tv) {
+		if (text == null) return null;
+		Matcher matches = EMOJI_RANGE.matcher(text);
+		SpannableStringBuilder builder = new SpannableStringBuilder(text);
+
+		while (matches.find()) {
+			int codePoint = matches.group().codePointAt(0);
+			Drawable drawable = getEmojiDrawable(codePoint);
+			if (drawable != null) {
+				builder.setSpan(new EmojiSpan(drawable, tv), matches.start(),
+						matches.end(), SPAN_EXCLUSIVE_EXCLUSIVE);
+			}
+		}
+		return builder;
+	}
+
+	@Nullable
+	Drawable getEmojiDrawable(int emojiCode) {
+		return getEmojiDrawable(offsets.get(emojiCode));
+	}
+
+	@Nullable
+	private Drawable getEmojiDrawable(DrawInfo drawInfo) {
+		if (drawInfo == null) {
+			return null;
+		}
+
+		final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale);
+		drawInfo.page.get().addListener(new FutureTaskListener<Bitmap>() {
+			@Override
+			public void onSuccess(final Bitmap result) {
+				androidExecutor.runOnUiThread(new Runnable() {
+					@Override
+					public void run() {
+						drawable.setBitmap(result);
+					}
+				});
+			}
+
+			@Override
+			public void onFailure(Throwable error) {
+				if (LOG.isLoggable(WARNING))
+					LOG.log(WARNING, error.toString(), error);
+			}
+		});
+		return drawable;
+	}
+
+	List<EmojiPageModel> getStaticPages() {
+		return staticPages;
+	}
+
+
+	class EmojiDrawable extends Drawable {
+
+		private final DrawInfo info;
+		private final float intrinsicWidth, intrinsicHeight;
+
+		private Bitmap bmp;
+
+		private EmojiDrawable(DrawInfo info, float decodeScale) {
+			this.info = info;
+			intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale;
+			intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale;
+		}
+
+		@Override
+		public int getIntrinsicWidth() {
+			return (int) intrinsicWidth;
+		}
+
+		@Override
+		public int getIntrinsicHeight() {
+			return (int) intrinsicHeight;
+		}
+
+		@Override
+		public void draw(@NonNull Canvas canvas) {
+			if (bmp == null) {
+				return;
+			}
+
+			int row = info.index / EMOJI_PER_ROW;
+			int rowIndex = info.index % EMOJI_PER_ROW;
+
+			int left = (int) (rowIndex * intrinsicWidth);
+			int top = (int) (row * intrinsicHeight + row * verticalPad);
+			int right = (int) ((rowIndex + 1) * intrinsicWidth);
+			int bottom =
+					(int) ((row + 1) * intrinsicHeight + row * verticalPad);
+			canvas.drawBitmap(bmp, new Rect(left, top, right, bottom),
+					getBounds(), PAINT);
+		}
+
+		void setBitmap(Bitmap bitmap) {
+			if (bmp == null || !bmp.sameAs(bitmap)) {
+				bmp = bitmap;
+				invalidateSelf();
+			}
+		}
+
+		@Override
+		public int getOpacity() {
+			return TRANSLUCENT;
+		}
+
+		@Override
+		public void setAlpha(int alpha) {
+		}
+
+		@Override
+		public void setColorFilter(ColorFilter cf) {
+		}
+	}
+
+
+	private static class DrawInfo {
+
+		private final EmojiPageBitmap page;
+		private final int index;
+
+		private DrawInfo(EmojiPageBitmap page, int index) {
+			this.page = page;
+			this.index = index;
+		}
+
+		@Override
+		public String toString() {
+			return "DrawInfo{ " + "page = " + page + ", index = " + index + '}';
+		}
+	}
+
+	private class EmojiPageBitmap {
+
+		private final EmojiPageModel model;
+
+		private ListenableFutureTask<Bitmap> task;
+
+		private volatile SoftReference<Bitmap> bitmapReference;
+
+		private EmojiPageBitmap(EmojiPageModel model) {
+			this.model = model;
+		}
+
+		private ListenableFutureTask<Bitmap> get() {
+			if (bitmapReference != null && bitmapReference.get() != null) {
+				return new ListenableFutureTask<>(bitmapReference.get());
+			} else if (task != null) {
+				return task;
+			} else {
+				Callable<Bitmap> callable = new Callable<Bitmap>() {
+					@Override
+					@Nullable
+					public Bitmap call() throws Exception {
+						try {
+							if (LOG.isLoggable(INFO))
+								LOG.info("Loading page " + model.getSprite());
+							return loadPage();
+						} catch (IOException ioe) {
+							LOG.log(WARNING, ioe.toString(), ioe);
+						}
+						return null;
+					}
+				};
+				task = new ListenableFutureTask<>(callable);
+				new AsyncTask<Void, Void, Void>() {
+					@Override
+					protected Void doInBackground(Void... params) {
+						task.run();
+						return null;
+					}
+
+					@Override
+					protected void onPostExecute(Void aVoid) {
+						task = null;
+					}
+				}.execute();
+			}
+			return task;
+		}
+
+		private Bitmap loadPage() throws IOException {
+			if (bitmapReference != null && bitmapReference.get() != null)
+				return bitmapReference.get();
+
+			try {
+				final Bitmap bitmap = BitmapUtil.createScaledBitmap(context,
+						"file:///android_asset/" + model.getSprite(),
+						decodeScale);
+				bitmapReference = new SoftReference<>(bitmap);
+				if (LOG.isLoggable(INFO))
+					LOG.info("Loaded page " + model.getSprite());
+				return bitmap;
+			} catch (BitmapDecodingException e) {
+				LOG.log(WARNING, e.toString(), e);
+				throw new IOException(e);
+			}
+		}
+
+		@Nullable
+		@Override
+		public String toString() {
+			return model.getSprite();
+		}
+	}
+
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java
new file mode 100644
index 0000000000000000000000000000000000000000..6168ee2733501703687499453ff3f0725a8679ff
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java
@@ -0,0 +1,40 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.UiThread;
+import android.widget.TextView;
+
+import org.briarproject.R;
+
+@UiThread
+class EmojiSpan extends AnimatingImageSpan {
+
+	private final int size;
+	private final FontMetricsInt fm;
+
+	EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) {
+		super(drawable, tv);
+		fm = tv.getPaint().getFontMetricsInt();
+		size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent)
+				: tv.getResources().getDimensionPixelSize(
+				R.dimen.conversation_item_body_text_size);
+		getDrawable().setBounds(0, 0, size, size);
+	}
+
+	@Override
+	public int getSize(Paint paint, CharSequence text, int start, int end,
+			FontMetricsInt fm) {
+		if (fm != null && this.fm != null) {
+			fm.ascent = this.fm.ascent;
+			fm.descent = this.fm.descent;
+			fm.top = this.fm.top;
+			fm.bottom = this.fm.bottom;
+			return size;
+		} else {
+			return super.getSize(paint, text, start, end, fm);
+		}
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d10a6393df448b2655151033c635c8e277a7f39
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
@@ -0,0 +1,95 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
+
+import static android.text.TextUtils.TruncateAt.END;
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.widget.TextView.BufferType.SPANNABLE;
+
+@UiThread
+public class EmojiTextView extends TextView {
+
+	private CharSequence source;
+	private boolean needsEllipsizing;
+
+	public EmojiTextView(Context context) {
+		this(context, null);
+	}
+
+	public EmojiTextView(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public EmojiTextView(Context context, @Nullable AttributeSet attrs,
+			int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+		// this ensures the view is redrawn when invalidated
+		setLayerType(LAYER_TYPE_SOFTWARE, null);
+	}
+
+	@Override
+	public void setText(@Nullable CharSequence text, BufferType type) {
+		source = EmojiProvider.getInstance(getContext()).emojify(text, this);
+		setTextEllipsized(source);
+	}
+
+	private void setTextEllipsized(final @Nullable CharSequence source) {
+		super.setText(needsEllipsizing ? ellipsize(source) : source, SPANNABLE);
+	}
+
+	@Override
+	public void invalidateDrawable(@NonNull Drawable drawable) {
+		if (drawable instanceof EmojiDrawable) invalidate();
+		else super.invalidateDrawable(drawable);
+	}
+
+	@Override
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+		final int size = MeasureSpec.getSize(widthMeasureSpec);
+		final int mode = MeasureSpec.getMode(widthMeasureSpec);
+		if (getEllipsize() == END &&
+				!TextUtils.isEmpty(source) &&
+				(mode == AT_MOST || mode == EXACTLY) &&
+				getPaint().breakText(source, 0, source.length() - 1, true, size,
+						null) != source.length()) {
+			needsEllipsizing = true;
+			FontMetricsInt font = getPaint().getFontMetricsInt();
+			int height = Math.abs(font.top - font.bottom);
+			super.onMeasure(MeasureSpec.makeMeasureSpec(size, EXACTLY),
+					MeasureSpec.makeMeasureSpec(height, EXACTLY));
+		} else {
+			needsEllipsizing = false;
+			super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+		}
+	}
+
+	@Override
+	protected void onLayout(boolean changed, int left, int top, int right,
+			int bottom) {
+		if (changed) setTextEllipsized(source);
+		super.onLayout(changed, left, top, right, bottom);
+	}
+
+	@Nullable
+	public CharSequence ellipsize(@Nullable CharSequence text) {
+		if (TextUtils.isEmpty(text) || getWidth() == 0 ||
+				getEllipsize() != END) {
+			return text;
+		} else {
+			return TextUtils.ellipsize(text, getPaint(),
+					getWidth() - getPaddingRight() - getPaddingLeft(), END);
+		}
+	}
+
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1f47224320cd306a5a68ab2ed88a0e562f44697
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java
@@ -0,0 +1,60 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.widget.ImageButton;
+
+import org.briarproject.R;
+import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiDrawerListener;
+
+@UiThread
+public class EmojiToggle extends ImageButton implements EmojiDrawerListener {
+
+	private final Drawable emojiToggle;
+	private final Drawable imeToggle;
+
+	public EmojiToggle(Context context) {
+		this(context, null);
+	}
+
+	public EmojiToggle(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public EmojiToggle(Context context, @Nullable AttributeSet attrs,
+			int defStyle) {
+		super(context, attrs, defStyle);
+
+		emojiToggle = ContextCompat
+				.getDrawable(getContext(), R.drawable.ic_emoji_toggle);
+		imeToggle = ContextCompat
+				.getDrawable(getContext(), R.drawable.ic_keyboard_black);
+		setToEmoji();
+	}
+
+	public void setToEmoji() {
+		setImageDrawable(emojiToggle);
+	}
+
+	public void setToIme() {
+		setImageDrawable(imeToggle);
+	}
+
+	public void attach(EmojiDrawer drawer) {
+		drawer.setDrawerListener(this);
+	}
+
+	@Override
+	public void onShown() {
+		setToIme();
+	}
+
+	@Override
+	public void onHidden() {
+		setToEmoji();
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java
new file mode 100644
index 0000000000000000000000000000000000000000..c4f064d2ad9e5e2300d7730323ceb80934d30202
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java
@@ -0,0 +1,88 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.briarproject.R;
+
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.graphics.Paint.Align.CENTER;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+
+@UiThread
+public class EmojiView extends View implements Drawable.Callback {
+
+	private final Paint paint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
+
+	private String emoji;
+	private Drawable drawable;
+
+	public EmojiView(Context context) {
+		this(context, null);
+	}
+
+	public EmojiView(Context context, @Nullable AttributeSet attrs) {
+		this(context, attrs, 0);
+	}
+
+	public EmojiView(Context context, @Nullable AttributeSet attrs,
+			int defStyleAttr) {
+		super(context, attrs, defStyleAttr);
+	}
+
+	public void setEmoji(String emoji) {
+		this.emoji = emoji;
+		this.drawable = EmojiProvider.getInstance(getContext())
+				.getEmojiDrawable(Character.codePointAt(emoji, 0));
+		postInvalidate();
+	}
+
+	public String getEmoji() {
+		return emoji;
+	}
+
+	@Override
+	protected void onDraw(Canvas canvas) {
+		if (drawable != null) {
+			drawable.setBounds(getPaddingLeft(),
+					getPaddingTop(),
+					getWidth() - getPaddingRight(),
+					getHeight() - getPaddingBottom());
+			drawable.setCallback(this);
+			drawable.draw(canvas);
+		} else {
+			float targetFontSize =
+					0.75f * getHeight() - getPaddingTop() - getPaddingBottom();
+			paint.setTextSize(targetFontSize);
+			paint.setColor(ContextCompat
+					.getColor(getContext(), R.color.emoji_text_color));
+			paint.setTextAlign(CENTER);
+			int xPos = (canvas.getWidth() / 2);
+			int yPos = (int) ((canvas.getHeight() / 2) -
+					((paint.descent() + paint.ascent()) / 2));
+
+			float overflow = paint.measureText(emoji) /
+					(getWidth() - getPaddingLeft() - getPaddingRight());
+			if (overflow > 1f) {
+				paint.setTextSize(targetFontSize / overflow);
+				yPos = (int) ((canvas.getHeight() / 2) -
+						((paint.descent() + paint.ascent()) / 2));
+			}
+			canvas.drawText(emoji, xPos, yPos, paint);
+		}
+	}
+
+	@Override
+	public void invalidateDrawable(@NonNull Drawable drawable) {
+		super.invalidateDrawable(drawable);
+		postInvalidate();
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..1c16d9c87732d8b3c1101f375bcba91fdab2d287
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
@@ -0,0 +1,140 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import org.briarproject.R;
+import org.briarproject.android.BaseActivity;
+import org.briarproject.android.controller.DbController;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.settings.Settings;
+import org.briarproject.api.settings.SettingsManager;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE;
+
+@UiThread
+public class RecentEmojiPageModel implements EmojiPageModel {
+
+	private static final Logger LOG =
+			Logger.getLogger(RecentEmojiPageModel.class.getName());
+
+	private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent";
+	private static final int EMOJI_LRU_SIZE = 50;
+
+	private final LinkedHashSet<String> recentlyUsed;
+	private Settings settings;
+
+	@Inject
+	SettingsManager settingsManager;
+	@Inject
+	DbController dbController;
+
+	RecentEmojiPageModel(Context context) {
+		if (!(context instanceof BaseActivity)) {
+			throw new IllegalArgumentException(
+					"Needs to be created from BaseActivity");
+		}
+		((BaseActivity) context).getActivityComponent().inject(this);
+		recentlyUsed = getPersistedCache();
+	}
+
+	private LinkedHashSet<String> getPersistedCache() {
+		String serialized;
+		try {
+			settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
+			serialized = settings.get(EMOJI_LRU_PREFERENCE);
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			serialized = null;
+		}
+		return deserialize(serialized);
+	}
+
+	@DrawableRes
+	@Override
+	public int getIcon() {
+		return R.drawable.ic_emoji_recent;
+	}
+
+	@NonNull
+	@Override
+	public String[] getEmoji() {
+		return toReversePrimitiveArray(recentlyUsed);
+	}
+
+	@Override
+	public boolean hasSpriteMap() {
+		return false;
+	}
+
+	@Override
+	public String getSprite() {
+		return null;
+	}
+
+	void onCodePointSelected(String emoji) {
+		recentlyUsed.remove(emoji);
+		recentlyUsed.add(emoji);
+
+		if (recentlyUsed.size() > EMOJI_LRU_SIZE) {
+			Iterator<String> iterator = recentlyUsed.iterator();
+			iterator.next();
+			iterator.remove();
+		}
+		save(recentlyUsed);
+	}
+
+	private String serialize(LinkedHashSet<String> emojis) {
+		String result = "";
+		for (String emoji : emojis) {
+			result += emoji + ";";
+		}
+		if (!emojis.isEmpty())
+			result = result.substring(0, result.length() - 1);
+		return result;
+	}
+
+	private LinkedHashSet<String> deserialize(@Nullable String str) {
+		String[] list = str == null ? new String[] {} : str.split(";");
+		LinkedHashSet<String> result = new LinkedHashSet<>(list.length);
+		Collections.addAll(result, list);
+		return result;
+	}
+
+	private void save(final LinkedHashSet<String> recentlyUsed) {
+		dbController.runOnDbThread(new Runnable() {
+			@Override
+			public void run() {
+				String serialized = serialize(recentlyUsed);
+				settings.put(EMOJI_LRU_PREFERENCE, serialized);
+				try {
+					settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private String[] toReversePrimitiveArray(
+			@NonNull LinkedHashSet<String> emojiSet) {
+		String[] emojis = new String[emojiSet.size()];
+		int i = emojiSet.size() - 1;
+		for (String emoji : emojiSet) {
+			emojis[i--] = emoji;
+		}
+		return emojis;
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java
new file mode 100644
index 0000000000000000000000000000000000000000..688ee3ae7a4f778c4c79bd30e3b8616679252082
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java
@@ -0,0 +1,73 @@
+package org.thoughtcrime.securesms.components.emoji;
+
+import android.content.Context;
+import android.support.annotation.ArrayRes;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+@UiThread
+class StaticEmojiPageModel implements EmojiPageModel {
+
+	@DrawableRes
+	private final int icon;
+	@NonNull
+	private final String[] emoji;
+	@Nullable
+	private final String sprite;
+
+	StaticEmojiPageModel(@DrawableRes int icon, @NonNull String[] emoji,
+			@Nullable String sprite) {
+		this.icon = icon;
+		this.emoji = emoji;
+		this.sprite = sprite;
+	}
+
+	StaticEmojiPageModel(Context ctx, @DrawableRes int icon,
+			@ArrayRes int res, @Nullable String sprite) {
+		this(icon, getEmoji(ctx, res), sprite);
+	}
+
+	@DrawableRes
+	@Override
+	public int getIcon() {
+		return icon;
+	}
+
+	@Override
+	@NonNull
+	public String[] getEmoji() {
+		return emoji;
+	}
+
+	@Override
+	public boolean hasSpriteMap() {
+		return sprite != null;
+	}
+
+	@Nullable
+	@Override
+	public String getSprite() {
+		return sprite;
+	}
+
+	@NonNull
+	private static String[] getEmoji(Context ctx, @ArrayRes int res) {
+		String[] rawStrings = ctx.getResources().getStringArray(res);
+		String[] emoji = new String[rawStrings.length];
+		int i = 0;
+		for (String codePoint : rawStrings) {
+			String[] bytes = codePoint.split(",");
+			int[] codePoints = new int[bytes.length];
+			int j = 0;
+			for (String b : bytes) {
+				codePoints[j] = Integer.valueOf(b, 16);
+			}
+			emoji[i] = new String(codePoints, 0, codePoints.length);
+			i++;
+		}
+		return emoji;
+	}
+
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/util/FutureTaskListener.java b/briar-android/src/org/thoughtcrime/securesms/components/util/FutureTaskListener.java
new file mode 100644
index 0000000000000000000000000000000000000000..80da2eccf6420c4c9f597eccbba39035cb0b7759
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/util/FutureTaskListener.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (C) 2014 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.thoughtcrime.securesms.components.util;
+
+public interface FutureTaskListener<V> {
+	void onSuccess(V result);
+
+	void onFailure(Throwable error);
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/components/util/ListenableFutureTask.java b/briar-android/src/org/thoughtcrime/securesms/components/util/ListenableFutureTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..1283148846464c4e8098a3cca1dc16b0a1eb10ad
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/components/util/ListenableFutureTask.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright (C) 2014 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.thoughtcrime.securesms.components.util;
+
+import android.support.annotation.Nullable;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+public class ListenableFutureTask<V> extends FutureTask<V> {
+
+	private final List<FutureTaskListener<V>> listeners = new LinkedList<>();
+
+	@Nullable
+	private final Object identifier;
+
+	public ListenableFutureTask(Callable<V> callable) {
+		this(callable, null);
+	}
+
+	private ListenableFutureTask(Callable<V> callable,
+			@Nullable Object identifier) {
+		super(callable);
+		this.identifier = identifier;
+	}
+
+	public ListenableFutureTask(final V result) {
+		this(result, null);
+	}
+
+	private ListenableFutureTask(final V result, @Nullable Object identifier) {
+		super(new Callable<V>() {
+			@Override
+			public V call() throws Exception {
+				return result;
+			}
+		});
+		this.identifier = identifier;
+		this.run();
+	}
+
+	public synchronized void addListener(FutureTaskListener<V> listener) {
+		if (this.isDone()) {
+			callback(listener);
+		} else {
+			this.listeners.add(listener);
+		}
+	}
+
+	public synchronized void removeListener(FutureTaskListener<V> listener) {
+		this.listeners.remove(listener);
+	}
+
+	@Override
+	protected synchronized void done() {
+		callback();
+	}
+
+	private void callback() {
+		for (FutureTaskListener<V> listener : listeners) {
+			callback(listener);
+		}
+	}
+
+	private void callback(FutureTaskListener<V> listener) {
+		if (listener != null) {
+			try {
+				listener.onSuccess(get());
+			} catch (InterruptedException e) {
+				throw new AssertionError(e);
+			} catch (ExecutionException e) {
+				listener.onFailure(e);
+			}
+		}
+	}
+
+	@Override
+	public boolean equals(Object other) {
+		if (other != null && other instanceof ListenableFutureTask &&
+				this.identifier != null) {
+			return identifier.equals(other);
+		} else {
+			return super.equals(other);
+		}
+	}
+
+	@Override
+	public int hashCode() {
+		if (identifier != null) return identifier.hashCode();
+		else return super.hashCode();
+	}
+
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/util/BitmapDecodingException.java b/briar-android/src/org/thoughtcrime/securesms/util/BitmapDecodingException.java
new file mode 100644
index 0000000000000000000000000000000000000000..777fdfb2ead9fe8cc8c140432cbc35ff522281c4
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/util/BitmapDecodingException.java
@@ -0,0 +1,12 @@
+package org.thoughtcrime.securesms.util;
+
+public class BitmapDecodingException extends Exception {
+
+	BitmapDecodingException(String s) {
+		super(s);
+	}
+
+	BitmapDecodingException(Exception nested) {
+		super(nested);
+	}
+}
diff --git a/briar-android/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/briar-android/src/org/thoughtcrime/securesms/util/BitmapUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bc0ed00f95d93721f923863bc475303fb086f01
--- /dev/null
+++ b/briar-android/src/org/thoughtcrime/securesms/util/BitmapUtil.java
@@ -0,0 +1,95 @@
+package org.thoughtcrime.securesms.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Pair;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.load.DecodeFormat;
+import com.bumptech.glide.load.engine.Resource;
+import com.bumptech.glide.load.resource.bitmap.BitmapResource;
+import com.bumptech.glide.load.resource.bitmap.Downsampler;
+import com.bumptech.glide.load.resource.bitmap.FitCenter;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+
+public class BitmapUtil {
+
+	private static final Logger LOG =
+			Logger.getLogger(BitmapUtil.class.getName());
+
+	private static <T> InputStream getInputStreamForModel(Context context,
+			T model)
+			throws BitmapDecodingException {
+		try {
+			return Glide.buildStreamModelLoader(model, context)
+					.getResourceFetcher(model, -1, -1)
+					.loadData(Priority.NORMAL);
+		} catch (Exception e) {
+			throw new BitmapDecodingException(e);
+		}
+	}
+
+	private static <T> Bitmap createScaledBitmapInto(Context context, T model,
+			int width, int height)
+			throws BitmapDecodingException {
+		final Bitmap rough = Downsampler.AT_LEAST
+				.decode(getInputStreamForModel(context, model),
+						Glide.get(context).getBitmapPool(),
+						width, height, DecodeFormat.PREFER_RGB_565);
+
+		final Resource<Bitmap> resource = BitmapResource
+				.obtain(rough, Glide.get(context).getBitmapPool());
+		final Resource<Bitmap> result =
+				new FitCenter(context).transform(resource, width, height);
+
+		if (result == null) {
+			throw new BitmapDecodingException("unable to transform Bitmap");
+		}
+		return result.get();
+	}
+
+	public static <T> Bitmap createScaledBitmap(Context context, T model,
+			float scale) throws BitmapDecodingException {
+		Pair<Integer, Integer> dimens =
+				getDimensions(getInputStreamForModel(context, model));
+		return createScaledBitmapInto(context, model,
+				(int) (dimens.first * scale), (int) (dimens.second * scale));
+	}
+
+	private static BitmapFactory.Options getImageDimensions(
+			InputStream inputStream)
+			throws BitmapDecodingException {
+		BitmapFactory.Options options = new BitmapFactory.Options();
+		options.inJustDecodeBounds = true;
+		BufferedInputStream fis = new BufferedInputStream(inputStream);
+		BitmapFactory.decodeStream(fis, null, options);
+		try {
+			fis.close();
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+
+		if (options.outWidth == -1 || options.outHeight == -1) {
+			throw new BitmapDecodingException(
+					"Failed to decode image dimensions: " + options.outWidth +
+							", " + options.outHeight);
+		}
+
+		return options;
+	}
+
+	private static Pair<Integer, Integer> getDimensions(InputStream inputStream)
+			throws BitmapDecodingException {
+		BitmapFactory.Options options = getImageDimensions(inputStream);
+		return new Pair<>(options.outWidth, options.outHeight);
+	}
+
+}
diff --git a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
index 38f64c5b25241af34913ec05aa92559f324a3add..e61b557beb392dbfedac9dd9620f0fd5ade0417b 100644
--- a/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
+++ b/briar-android/test/java/org/briarproject/android/forum/ForumActivityTest.java
@@ -29,6 +29,8 @@ import java.util.List;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
+import static org.briarproject.api.identity.Author.Status.UNKNOWN;
+import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -77,11 +79,12 @@ public class ForumActivityTest {
 	private List<ForumEntry> getDummyData() {
 		ForumEntry[] forumEntries = new ForumEntry[6];
 		for (int i = 0; i < forumEntries.length; i++) {
-			forumEntries[i] =
-					new ForumEntry(new MessageId(TestUtils.getRandomId()),
-							AUTHORS[i], LEVELS[i], System.currentTimeMillis(),
-							AUTHORS[i], new AuthorId(TestUtils.getRandomId()),
-							Author.Status.UNKNOWN);
+			AuthorId authorId = new AuthorId(TestUtils.getRandomId());
+			byte[] publicKey = TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+			Author author = new Author(authorId, AUTHORS[i], publicKey);
+			forumEntries[i] = new ForumEntry(
+					new MessageId(TestUtils.getRandomId()), AUTHORS[i],
+					LEVELS[i], System.currentTimeMillis(), author, UNKNOWN);
 		}
 		return new ArrayList<>(Arrays.asList(forumEntries));
 	}
diff --git a/build.gradle b/build.gradle
index ab0a691b0e22064db01ab17e8ee05bae150267f6..6896cc0d6878db512b32a8e07e218298fc0560cf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,7 @@ buildscript {
 	}
 
 	dependencies {
-		classpath 'com.android.tools.build:gradle:2.1.3'
+		classpath 'com.android.tools.build:gradle:2.2.0'
 		classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
 		classpath 'de.undercouch:gradle-download-task:2.1.0'
 		classpath files('briar-core/libs/gradle-witness.jar')