Bläddra i källkod

Version 4.5-rc1

Threema 5 år sedan
förälder
incheckning
de0c80e203
44 ändrade filer med 1928 tillägg och 719 borttagningar
  1. 47 0
      app/assets/license.html
  2. 5 9
      app/build.gradle
  3. 2 0
      app/jni/Android.mk
  4. 2 263
      app/jni/nacl/curve25519-jni.c
  5. 451 0
      app/jni/nacl/smult_donna-c64.c
  6. 864 0
      app/jni/nacl/smult_donna.c
  7. 11 4
      app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java
  8. 18 25
      app/src/main/java/ch/threema/app/activities/SendMediaActivity.java
  9. 17 7
      app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java
  10. 1 17
      app/src/main/java/ch/threema/app/adapters/SendMediaGridAdapter.java
  11. 1 1
      app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java
  12. 65 19
      app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java
  13. 4 2
      app/src/main/java/ch/threema/app/fragments/MyIDFragment.java
  14. 0 1
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachAdapter.java
  15. 38 14
      app/src/main/java/ch/threema/app/mediaattacher/MediaAttachViewModel.java
  16. 0 2
      app/src/main/java/ch/threema/app/mediaattacher/MediaRepository.java
  17. 43 33
      app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java
  18. 5 13
      app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemEntity.java
  19. 5 0
      app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemsDAO.java
  20. 3 15
      app/src/main/java/ch/threema/app/mediaattacher/data/LabeledMediaItemEntity.java
  21. 4 4
      app/src/main/java/ch/threema/app/mediaattacher/data/LabeledMediaItemsDAO.java
  22. 45 0
      app/src/main/java/ch/threema/app/mediaattacher/data/MediaItemEntity.java
  23. 2 2
      app/src/main/java/ch/threema/app/mediaattacher/data/MediaItemsRoomDatabase.java
  24. 44 46
      app/src/main/java/ch/threema/app/mediaattacher/labeling/ImageLabelingWorker.java
  25. 21 9
      app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java
  26. 13 22
      app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java
  27. 0 2
      app/src/main/java/ch/threema/app/utils/ContactUtil.java
  28. 2 2
      app/src/main/java/ch/threema/app/utils/RingtoneUtil.java
  29. 2 1
      app/src/main/java/ch/threema/storage/models/ContactModel.java
  30. 15 1
      app/src/main/res/layout/activity_send_media.xml
  31. 0 11
      app/src/main/res/layout/item_send_media.xml
  32. 0 49
      app/src/main/res/layout/popup_tooltip_bottom_left_image_resolution.xml
  33. 1 1
      app/src/main/res/layout/popup_tooltip_top_right.xml
  34. 1 0
      app/src/main/res/values-de/strings.xml
  35. 1 1
      app/src/main/res/values-it/qrscanner_strings.xml
  36. 11 1
      app/src/main/res/values-it/strings.xml
  37. 5 0
      app/src/main/res/values-it/webclient_strings.xml
  38. 19 4
      app/src/main/res/values-rm/strings.xml
  39. 11 1
      app/src/main/res/values-rm/webclient_strings.xml
  40. 1 1
      app/src/main/res/values-zh-rCN/poi_strings.xml
  41. 122 112
      app/src/main/res/values-zh-rCN/strings.xml
  42. 23 23
      app/src/main/res/values-zh-rCN/webclient_strings.xml
  43. 2 0
      app/src/main/res/values/strings.xml
  44. 1 1
      app/src/main/res/values/styles.xml

+ 47 - 0
app/assets/license.html

@@ -116,6 +116,53 @@ SUCH DAMAGE.</p>
 <p>Licensed under the Apache License, version 2.0 (copy below).</p>
 
 
+<h2>curve25519-donna</h2>
+
+<p>Copyright 2008, Google Inc. All rights reserved.</p>
+
+<p>Redistribution and use in source and binary forms, with or without modification, are permitted
+    provided that the following conditions are met:</p>
+
+<ul>
+    <li>Redistributions of source code must retain the above copyright notice, this list of
+        conditions and the following disclaimer.
+    </li>
+    <li>Redistributions in binary form must reproduce the above copyright notice, this list of
+        conditions and the following disclaimer in the documentation and/or other materials provided
+        with the distribution.
+    </li>
+    <li>Neither the name of Google Inc. nor the names of its contributors may be used to endorse or
+        promote products derived from this software without specific prior written permission.
+    </li>
+</ul>
+
+<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+    FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+    OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+    POSSIBILITY OF SUCH DAMAGE.</p>
+
+<p>curve25519-donna: Curve25519 elliptic curve, public key function</p>
+
+<p>http://code.google.com/p/curve25519-donna/</p>
+
+<p>Adam Langley agl@imperialviolet.org</p>
+
+<p>Derived from public domain C code by Daniel J. Bernstein djb@cr.yp.to</p>
+
+<p>More information about curve25519 can be found here http://cr.yp.to/ecdh.html</p>
+
+<p>djb's sample implementation of curve25519 is written in a special assembly language called qhasm
+    and uses the floating point registers.</p>
+
+<p>This is, almost, a clean room reimplementation from the curve25519 paper. It uses many of the
+    tricks described therein. Only the crecip function is taken from the sample implementation.</p>
+
+
 <h2>Emoji art supplied by <a href="http://emojione.com">EmojiOne</a></h2>
 
 <p>Licensed under Creative Commons License (CC-BY 4.0).</p>

+ 5 - 9
app/build.gradle

@@ -75,8 +75,8 @@ android {
         vectorDrawables.useSupportLibrary = true
         applicationId "ch.threema.app"
         testApplicationId 'ch.threema.app.test'
-        versionCode 660
-        versionName "4.5-beta6"
+        versionCode 661
+        versionName "4.5-rc1"
         resValue "string", "version_name_suffix", ""
         resValue "string", "app_name", "Threema"
         resValue "string", "uri_scheme", "threema"
@@ -141,7 +141,7 @@ android {
         }
         store_threema { }
         store_google_work {
-            versionName "4.5k-beta6"
+            versionName "4.5k-rc1"
             applicationId "ch.threema.app.work"
             testApplicationId 'ch.threema.app.work.test'
             resValue "string", "package_name", applicationId
@@ -178,7 +178,7 @@ android {
             buildConfigField "byte[]", "SERVER_PUBKEY_ALT", "new byte[] {(byte) 0x5a, (byte) 0x98, (byte) 0xf2, (byte) 0x3d, (byte) 0xe6, (byte) 0x56, (byte) 0x05, (byte) 0xd0, (byte) 0x50, (byte) 0xdc, (byte) 0x00, (byte) 0x64, (byte) 0xbe, (byte) 0x07, (byte) 0xdd, (byte) 0xdd, (byte) 0x81, (byte) 0x1d, (byte) 0xa1, (byte) 0x16, (byte) 0xa5, (byte) 0x43, (byte) 0xce, (byte) 0x43, (byte) 0xaa, (byte) 0x26, (byte) 0x87, (byte) 0xd1, (byte) 0x9f, (byte) 0x20, (byte) 0xaf, (byte) 0x3c }"
         }
         sandbox_work {
-            versionName "4.5k-beta6"
+            versionName "4.5k-rc1"
             applicationId "ch.threema.app.sandbox.work"
             testApplicationId 'ch.threema.app.sandbox.work.test'
 
@@ -208,7 +208,7 @@ android {
             ]
         }
         red { // Essentially like sandbox work, but with a different icon and accent color, used for internal testing
-            versionName "4.5r-beta6"
+            versionName "4.5r-rc1"
             applicationId "ch.threema.app.red"
             testApplicationId 'ch.threema.app.red.test'
 
@@ -476,10 +476,6 @@ dependencies {
     implementation 'com.google.zxing:core:3.3.3' // zxing 3.4 crashes on kitkat
     implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.13'
 
-    implementation "android.arch.lifecycle:common-java8:2.0.0"
-    implementation "android.arch.lifecycle:extensions:2.0.0"
-    implementation "android.arch.paging:runtime:1.0.1"
-
     // webclient dependencies
     implementation 'org.msgpack:msgpack-core:0.8.20'
     implementation 'com.neovisionaries:nv-websocket-client:2.9'

+ 2 - 0
app/jni/Android.mk

@@ -36,5 +36,7 @@ LOCAL_MODULE     := nacl-jni
 LOCAL_SRC_FILES  := $(LOCAL_PATH)/nacl/salsa20-jni.c
 LOCAL_SRC_FILES  += $(LOCAL_PATH)/nacl/poly1305-jni.c
 LOCAL_SRC_FILES  += $(LOCAL_PATH)/nacl/curve25519-jni.c
+LOCAL_SRC_FILES  += $(LOCAL_PATH)/nacl/smult_donna.c
+LOCAL_SRC_FILES  += $(LOCAL_PATH)/nacl/smult_donna-c64.c
 
 include $(BUILD_SHARED_LIBRARY)

+ 2 - 263
app/jni/nacl/curve25519-jni.c

@@ -22,9 +22,7 @@
 #include <string.h>
 #include <jni.h>
 
-int crypto_scalarmult(unsigned char *q,
-  		const unsigned char *n,
-		const unsigned char *p);
+int crypto_scalarmult_curve25519(uint8_t *mypublic, const uint8_t *secret, const uint8_t *basepoint);
 
 JNIEXPORT jint JNICALL Java_com_neilalexander_jnacl_crypto_curve25519_crypto_1scalarmult_1native(JNIEnv* env, jclass cls,
 	jbyteArray qarr, jbyteArray narr, jbyteArray parr) {
@@ -35,268 +33,9 @@ JNIEXPORT jint JNICALL Java_com_neilalexander_jnacl_crypto_curve25519_crypto_1sc
 	(*env)->GetByteArrayRegion(env, narr, 0, 32, n);
 	(*env)->GetByteArrayRegion(env, parr, 0, 32, p);
 
-	res = crypto_scalarmult((unsigned char *)q, (unsigned char *)n, (unsigned char *)p);
+	res = crypto_scalarmult_curve25519((unsigned char *)q, (unsigned char *)n, (unsigned char *)p);
 
 	(*env)->SetByteArrayRegion(env, qarr, 0, 32, q);
 
 	return res;
 }
-
-/* Public Domain code copied verbatim from NaCl below */
-
-static void add(unsigned int out[32],const unsigned int a[32],const unsigned int b[32])
-{
-  unsigned int j;
-  unsigned int u;
-  u = 0;
-  for (j = 0;j < 31;++j) { u += a[j] + b[j]; out[j] = u & 255; u >>= 8; }
-  u += a[31] + b[31]; out[31] = u;
-}
-
-static void sub(unsigned int out[32],const unsigned int a[32],const unsigned int b[32])
-{
-  unsigned int j;
-  unsigned int u;
-  u = 218;
-  for (j = 0;j < 31;++j) {
-    u += a[j] + 65280 - b[j];
-    out[j] = u & 255;
-    u >>= 8;
-  }
-  u += a[31] - b[31];
-  out[31] = u;
-}
-
-static void squeeze(unsigned int a[32])
-{
-  unsigned int j;
-  unsigned int u;
-  u = 0;
-  for (j = 0;j < 31;++j) { u += a[j]; a[j] = u & 255; u >>= 8; }
-  u += a[31]; a[31] = u & 127;
-  u = 19 * (u >> 7);
-  for (j = 0;j < 31;++j) { u += a[j]; a[j] = u & 255; u >>= 8; }
-  u += a[31]; a[31] = u;
-}
-
-static const unsigned int minusp[32] = {
- 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128
-} ;
-
-static void freeze(unsigned int a[32])
-{
-  unsigned int aorig[32];
-  unsigned int j;
-  unsigned int negative;
-
-  for (j = 0;j < 32;++j) aorig[j] = a[j];
-  add(a,a,minusp);
-  negative = -((a[31] >> 7) & 1);
-  for (j = 0;j < 32;++j) a[j] ^= negative & (aorig[j] ^ a[j]);
-}
-
-static void mult(unsigned int out[32],const unsigned int a[32],const unsigned int b[32])
-{
-  unsigned int i;
-  unsigned int j;
-  unsigned int u;
-
-  for (i = 0;i < 32;++i) {
-    u = 0;
-    for (j = 0;j <= i;++j) u += a[j] * b[i - j];
-    for (j = i + 1;j < 32;++j) u += 38 * a[j] * b[i + 32 - j];
-    out[i] = u;
-  }
-  squeeze(out);
-}
-
-static void mult121665(unsigned int out[32],const unsigned int a[32])
-{
-  unsigned int j;
-  unsigned int u;
-
-  u = 0;
-  for (j = 0;j < 31;++j) { u += 121665 * a[j]; out[j] = u & 255; u >>= 8; }
-  u += 121665 * a[31]; out[31] = u & 127;
-  u = 19 * (u >> 7);
-  for (j = 0;j < 31;++j) { u += out[j]; out[j] = u & 255; u >>= 8; }
-  u += out[j]; out[j] = u;
-}
-
-static void square(unsigned int out[32],const unsigned int a[32])
-{
-  unsigned int i;
-  unsigned int j;
-  unsigned int u;
-
-  for (i = 0;i < 32;++i) {
-    u = 0;
-    for (j = 0;j < i - j;++j) u += a[j] * a[i - j];
-    for (j = i + 1;j < i + 32 - j;++j) u += 38 * a[j] * a[i + 32 - j];
-    u *= 2;
-    if ((i & 1) == 0) {
-      u += a[i / 2] * a[i / 2];
-      u += 38 * a[i / 2 + 16] * a[i / 2 + 16];
-    }
-    out[i] = u;
-  }
-  squeeze(out);
-}
-
-static void select(unsigned int p[64],unsigned int q[64],const unsigned int r[64],const unsigned int s[64],unsigned int b)
-{
-  unsigned int j;
-  unsigned int t;
-  unsigned int bminus1;
-
-  bminus1 = b - 1;
-  for (j = 0;j < 64;++j) {
-    t = bminus1 & (r[j] ^ s[j]);
-    p[j] = s[j] ^ t;
-    q[j] = r[j] ^ t;
-  }
-}
-
-static void mainloop(unsigned int work[64],const unsigned char e[32])
-{
-  unsigned int xzm1[64];
-  unsigned int xzm[64];
-  unsigned int xzmb[64];
-  unsigned int xzm1b[64];
-  unsigned int xznb[64];
-  unsigned int xzn1b[64];
-  unsigned int a0[64];
-  unsigned int a1[64];
-  unsigned int b0[64];
-  unsigned int b1[64];
-  unsigned int c1[64];
-  unsigned int r[32];
-  unsigned int s[32];
-  unsigned int t[32];
-  unsigned int u[32];
-  unsigned int i;
-  unsigned int j;
-  unsigned int b;
-  int pos;
-
-  for (j = 0;j < 32;++j) xzm1[j] = work[j];
-  xzm1[32] = 1;
-  for (j = 33;j < 64;++j) xzm1[j] = 0;
-
-  xzm[0] = 1;
-  for (j = 1;j < 64;++j) xzm[j] = 0;
-
-  for (pos = 254;pos >= 0;--pos) {
-    b = e[pos / 8] >> (pos & 7);
-    b &= 1;
-    select(xzmb,xzm1b,xzm,xzm1,b);
-    add(a0,xzmb,xzmb + 32);
-    sub(a0 + 32,xzmb,xzmb + 32);
-    add(a1,xzm1b,xzm1b + 32);
-    sub(a1 + 32,xzm1b,xzm1b + 32);
-    square(b0,a0);
-    square(b0 + 32,a0 + 32);
-    mult(b1,a1,a0 + 32);
-    mult(b1 + 32,a1 + 32,a0);
-    add(c1,b1,b1 + 32);
-    sub(c1 + 32,b1,b1 + 32);
-    square(r,c1 + 32);
-    sub(s,b0,b0 + 32);
-    mult121665(t,s);
-    add(u,t,b0);
-    mult(xznb,b0,b0 + 32);
-    mult(xznb + 32,s,u);
-    square(xzn1b,c1);
-    mult(xzn1b + 32,r,work);
-    select(xzm,xzm1,xznb,xzn1b,b);
-  }
-
-  for (j = 0;j < 64;++j) work[j] = xzm[j];
-}
-
-static void recip(unsigned int out[32],const unsigned int z[32])
-{
-  unsigned int z2[32];
-  unsigned int z9[32];
-  unsigned int z11[32];
-  unsigned int z2_5_0[32];
-  unsigned int z2_10_0[32];
-  unsigned int z2_20_0[32];
-  unsigned int z2_50_0[32];
-  unsigned int z2_100_0[32];
-  unsigned int t0[32];
-  unsigned int t1[32];
-  int i;
-
-  /* 2 */ square(z2,z);
-  /* 4 */ square(t1,z2);
-  /* 8 */ square(t0,t1);
-  /* 9 */ mult(z9,t0,z);
-  /* 11 */ mult(z11,z9,z2);
-  /* 22 */ square(t0,z11);
-  /* 2^5 - 2^0 = 31 */ mult(z2_5_0,t0,z9);
-
-  /* 2^6 - 2^1 */ square(t0,z2_5_0);
-  /* 2^7 - 2^2 */ square(t1,t0);
-  /* 2^8 - 2^3 */ square(t0,t1);
-  /* 2^9 - 2^4 */ square(t1,t0);
-  /* 2^10 - 2^5 */ square(t0,t1);
-  /* 2^10 - 2^0 */ mult(z2_10_0,t0,z2_5_0);
-
-  /* 2^11 - 2^1 */ square(t0,z2_10_0);
-  /* 2^12 - 2^2 */ square(t1,t0);
-  /* 2^20 - 2^10 */ for (i = 2;i < 10;i += 2) { square(t0,t1); square(t1,t0); }
-  /* 2^20 - 2^0 */ mult(z2_20_0,t1,z2_10_0);
-
-  /* 2^21 - 2^1 */ square(t0,z2_20_0);
-  /* 2^22 - 2^2 */ square(t1,t0);
-  /* 2^40 - 2^20 */ for (i = 2;i < 20;i += 2) { square(t0,t1); square(t1,t0); }
-  /* 2^40 - 2^0 */ mult(t0,t1,z2_20_0);
-
-  /* 2^41 - 2^1 */ square(t1,t0);
-  /* 2^42 - 2^2 */ square(t0,t1);
-  /* 2^50 - 2^10 */ for (i = 2;i < 10;i += 2) { square(t1,t0); square(t0,t1); }
-  /* 2^50 - 2^0 */ mult(z2_50_0,t0,z2_10_0);
-
-  /* 2^51 - 2^1 */ square(t0,z2_50_0);
-  /* 2^52 - 2^2 */ square(t1,t0);
-  /* 2^100 - 2^50 */ for (i = 2;i < 50;i += 2) { square(t0,t1); square(t1,t0); }
-  /* 2^100 - 2^0 */ mult(z2_100_0,t1,z2_50_0);
-
-  /* 2^101 - 2^1 */ square(t1,z2_100_0);
-  /* 2^102 - 2^2 */ square(t0,t1);
-  /* 2^200 - 2^100 */ for (i = 2;i < 100;i += 2) { square(t1,t0); square(t0,t1); }
-  /* 2^200 - 2^0 */ mult(t1,t0,z2_100_0);
-
-  /* 2^201 - 2^1 */ square(t0,t1);
-  /* 2^202 - 2^2 */ square(t1,t0);
-  /* 2^250 - 2^50 */ for (i = 2;i < 50;i += 2) { square(t0,t1); square(t1,t0); }
-  /* 2^250 - 2^0 */ mult(t0,t1,z2_50_0);
-
-  /* 2^251 - 2^1 */ square(t1,t0);
-  /* 2^252 - 2^2 */ square(t0,t1);
-  /* 2^253 - 2^3 */ square(t1,t0);
-  /* 2^254 - 2^4 */ square(t0,t1);
-  /* 2^255 - 2^5 */ square(t1,t0);
-  /* 2^255 - 21 */ mult(out,t1,z11);
-}
-
-int crypto_scalarmult(unsigned char *q,
-  const unsigned char *n,
-  const unsigned char *p)
-{
-  unsigned int work[96];
-  unsigned char e[32];
-  unsigned int i;
-  for (i = 0;i < 32;++i) e[i] = n[i];
-  e[0] &= 248;
-  e[31] &= 127;
-  e[31] |= 64;
-  for (i = 0;i < 32;++i) work[i] = p[i];
-  mainloop(work,e);
-  recip(work + 32,work + 32);
-  mult(work + 64,work,work + 32);
-  freeze(work + 64);
-  for (i = 0;i < 32;++i) q[i] = work[64 + i];
-  return 0;
-}

+ 451 - 0
app/jni/nacl/smult_donna-c64.c

@@ -0,0 +1,451 @@
+/* Copyright 2008, Google Inc.
+ * All rights reserved.
+ *
+ * Code released into the public domain.
+ *
+ * curve25519-donna: Curve25519 elliptic curve, public key function
+ *
+ * http://code.google.com/p/curve25519-donna/
+ *
+ * Adam Langley <agl@imperialviolet.org>
+ *
+ * Derived from public domain C code by Daniel J. Bernstein <djb@cr.yp.to>
+ *
+ * More information about curve25519 can be found here
+ *   http://cr.yp.to/ecdh.html
+ *
+ * djb's sample implementation of curve25519 is written in a special assembly
+ * language called qhasm and uses the floating point registers.
+ *
+ * This is, almost, a clean room reimplementation from the curve25519 paper. It
+ * uses many of the tricks described therein. Only the crecip function is taken
+ * from the sample implementation.
+ */
+
+#if defined(__aarch64__) || defined(__x86_64__)
+
+#include <string.h>
+#include <stdint.h>
+
+typedef uint8_t u8;
+typedef uint64_t limb;
+typedef limb felem[5];
+// This is a special gcc mode for 128-bit integers. It's implemented on 64-bit
+// platforms only as far as I know.
+typedef unsigned uint128_t __attribute__((mode(TI)));
+
+#undef force_inline
+#define force_inline __attribute__((always_inline))
+
+/* Sum two numbers: output += in */
+static inline void force_inline
+fsum(limb *output, const limb *in) {
+  output[0] += in[0];
+  output[1] += in[1];
+  output[2] += in[2];
+  output[3] += in[3];
+  output[4] += in[4];
+}
+
+/* Find the difference of two numbers: output = in - output
+ * (note the order of the arguments!)
+ *
+ * Assumes that out[i] < 2**52
+ * On return, out[i] < 2**55
+ */
+static inline void force_inline
+fdifference_backwards(felem out, const felem in) {
+  /* 152 is 19 << 3 */
+  static const limb two54m152 = (((limb)1) << 54) - 152;
+  static const limb two54m8 = (((limb)1) << 54) - 8;
+
+  out[0] = in[0] + two54m152 - out[0];
+  out[1] = in[1] + two54m8 - out[1];
+  out[2] = in[2] + two54m8 - out[2];
+  out[3] = in[3] + two54m8 - out[3];
+  out[4] = in[4] + two54m8 - out[4];
+}
+
+/* Multiply a number by a scalar: output = in * scalar */
+static inline void force_inline
+fscalar_product(felem output, const felem in, const limb scalar) {
+  uint128_t a;
+
+  a = ((uint128_t) in[0]) * scalar;
+  output[0] = ((limb)a) & 0x7ffffffffffff;
+
+  a = ((uint128_t) in[1]) * scalar + ((limb) (a >> 51));
+  output[1] = ((limb)a) & 0x7ffffffffffff;
+
+  a = ((uint128_t) in[2]) * scalar + ((limb) (a >> 51));
+  output[2] = ((limb)a) & 0x7ffffffffffff;
+
+  a = ((uint128_t) in[3]) * scalar + ((limb) (a >> 51));
+  output[3] = ((limb)a) & 0x7ffffffffffff;
+
+  a = ((uint128_t) in[4]) * scalar + ((limb) (a >> 51));
+  output[4] = ((limb)a) & 0x7ffffffffffff;
+
+  output[0] += (a >> 51) * 19;
+}
+
+/* Multiply two numbers: output = in2 * in
+ *
+ * output must be distinct to both inputs. The inputs are reduced coefficient
+ * form, the output is not.
+ *
+ * Assumes that in[i] < 2**55 and likewise for in2.
+ * On return, output[i] < 2**52
+ */
+static inline void force_inline
+fmul(felem output, const felem in2, const felem in) {
+  uint128_t t[5];
+  limb r0,r1,r2,r3,r4,s0,s1,s2,s3,s4,c;
+
+  r0 = in[0];
+  r1 = in[1];
+  r2 = in[2];
+  r3 = in[3];
+  r4 = in[4];
+
+  s0 = in2[0];
+  s1 = in2[1];
+  s2 = in2[2];
+  s3 = in2[3];
+  s4 = in2[4];
+
+  t[0]  =  ((uint128_t) r0) * s0;
+  t[1]  =  ((uint128_t) r0) * s1 + ((uint128_t) r1) * s0;
+  t[2]  =  ((uint128_t) r0) * s2 + ((uint128_t) r2) * s0 + ((uint128_t) r1) * s1;
+  t[3]  =  ((uint128_t) r0) * s3 + ((uint128_t) r3) * s0 + ((uint128_t) r1) * s2 + ((uint128_t) r2) * s1;
+  t[4]  =  ((uint128_t) r0) * s4 + ((uint128_t) r4) * s0 + ((uint128_t) r3) * s1 + ((uint128_t) r1) * s3 + ((uint128_t) r2) * s2;
+
+  r4 *= 19;
+  r1 *= 19;
+  r2 *= 19;
+  r3 *= 19;
+
+  t[0] += ((uint128_t) r4) * s1 + ((uint128_t) r1) * s4 + ((uint128_t) r2) * s3 + ((uint128_t) r3) * s2;
+  t[1] += ((uint128_t) r4) * s2 + ((uint128_t) r2) * s4 + ((uint128_t) r3) * s3;
+  t[2] += ((uint128_t) r4) * s3 + ((uint128_t) r3) * s4;
+  t[3] += ((uint128_t) r4) * s4;
+
+                  r0 = (limb)t[0] & 0x7ffffffffffff; c = (limb)(t[0] >> 51);
+  t[1] += c;      r1 = (limb)t[1] & 0x7ffffffffffff; c = (limb)(t[1] >> 51);
+  t[2] += c;      r2 = (limb)t[2] & 0x7ffffffffffff; c = (limb)(t[2] >> 51);
+  t[3] += c;      r3 = (limb)t[3] & 0x7ffffffffffff; c = (limb)(t[3] >> 51);
+  t[4] += c;      r4 = (limb)t[4] & 0x7ffffffffffff; c = (limb)(t[4] >> 51);
+  r0 +=   c * 19; c = r0 >> 51; r0 = r0 & 0x7ffffffffffff;
+  r1 +=   c;      c = r1 >> 51; r1 = r1 & 0x7ffffffffffff;
+  r2 +=   c;
+
+  output[0] = r0;
+  output[1] = r1;
+  output[2] = r2;
+  output[3] = r3;
+  output[4] = r4;
+}
+
+static inline void force_inline
+fsquare_times(felem output, const felem in, limb count) {
+  uint128_t t[5];
+  limb r0,r1,r2,r3,r4,c;
+  limb d0,d1,d2,d4,d419;
+
+  r0 = in[0];
+  r1 = in[1];
+  r2 = in[2];
+  r3 = in[3];
+  r4 = in[4];
+
+  do {
+    d0 = r0 * 2;
+    d1 = r1 * 2;
+    d2 = r2 * 2 * 19;
+    d419 = r4 * 19;
+    d4 = d419 * 2;
+
+    t[0] = ((uint128_t) r0) * r0 + ((uint128_t) d4) * r1 + (((uint128_t) d2) * (r3     ));
+    t[1] = ((uint128_t) d0) * r1 + ((uint128_t) d4) * r2 + (((uint128_t) r3) * (r3 * 19));
+    t[2] = ((uint128_t) d0) * r2 + ((uint128_t) r1) * r1 + (((uint128_t) d4) * (r3     ));
+    t[3] = ((uint128_t) d0) * r3 + ((uint128_t) d1) * r2 + (((uint128_t) r4) * (d419   ));
+    t[4] = ((uint128_t) d0) * r4 + ((uint128_t) d1) * r3 + (((uint128_t) r2) * (r2     ));
+
+                    r0 = (limb)t[0] & 0x7ffffffffffff; c = (limb)(t[0] >> 51);
+    t[1] += c;      r1 = (limb)t[1] & 0x7ffffffffffff; c = (limb)(t[1] >> 51);
+    t[2] += c;      r2 = (limb)t[2] & 0x7ffffffffffff; c = (limb)(t[2] >> 51);
+    t[3] += c;      r3 = (limb)t[3] & 0x7ffffffffffff; c = (limb)(t[3] >> 51);
+    t[4] += c;      r4 = (limb)t[4] & 0x7ffffffffffff; c = (limb)(t[4] >> 51);
+    r0 +=   c * 19; c = r0 >> 51; r0 = r0 & 0x7ffffffffffff;
+    r1 +=   c;      c = r1 >> 51; r1 = r1 & 0x7ffffffffffff;
+    r2 +=   c;
+  } while(--count);
+
+  output[0] = r0;
+  output[1] = r1;
+  output[2] = r2;
+  output[3] = r3;
+  output[4] = r4;
+}
+
+/* Load a little-endian 64-bit number  */
+static limb
+load_limb(const u8 *in) {
+  return
+    ((limb)in[0]) |
+    (((limb)in[1]) << 8) |
+    (((limb)in[2]) << 16) |
+    (((limb)in[3]) << 24) |
+    (((limb)in[4]) << 32) |
+    (((limb)in[5]) << 40) |
+    (((limb)in[6]) << 48) |
+    (((limb)in[7]) << 56);
+}
+
+static void
+store_limb(u8 *out, limb in) {
+  out[0] = in & 0xff;
+  out[1] = (in >> 8) & 0xff;
+  out[2] = (in >> 16) & 0xff;
+  out[3] = (in >> 24) & 0xff;
+  out[4] = (in >> 32) & 0xff;
+  out[5] = (in >> 40) & 0xff;
+  out[6] = (in >> 48) & 0xff;
+  out[7] = (in >> 56) & 0xff;
+}
+
+/* Take a little-endian, 32-byte number and expand it into polynomial form */
+static void
+fexpand(limb *output, const u8 *in) {
+  output[0] = load_limb(in) & 0x7ffffffffffff;
+  output[1] = (load_limb(in+6) >> 3) & 0x7ffffffffffff;
+  output[2] = (load_limb(in+12) >> 6) & 0x7ffffffffffff;
+  output[3] = (load_limb(in+19) >> 1) & 0x7ffffffffffff;
+  output[4] = (load_limb(in+24) >> 12) & 0x7ffffffffffff;
+}
+
+/* Take a fully reduced polynomial form number and contract it into a
+ * little-endian, 32-byte array
+ */
+static void
+fcontract(u8 *output, const felem input) {
+  uint128_t t[5];
+
+  t[0] = input[0];
+  t[1] = input[1];
+  t[2] = input[2];
+  t[3] = input[3];
+  t[4] = input[4];
+
+  t[1] += t[0] >> 51; t[0] &= 0x7ffffffffffff;
+  t[2] += t[1] >> 51; t[1] &= 0x7ffffffffffff;
+  t[3] += t[2] >> 51; t[2] &= 0x7ffffffffffff;
+  t[4] += t[3] >> 51; t[3] &= 0x7ffffffffffff;
+  t[0] += 19 * (t[4] >> 51); t[4] &= 0x7ffffffffffff;
+
+  t[1] += t[0] >> 51; t[0] &= 0x7ffffffffffff;
+  t[2] += t[1] >> 51; t[1] &= 0x7ffffffffffff;
+  t[3] += t[2] >> 51; t[2] &= 0x7ffffffffffff;
+  t[4] += t[3] >> 51; t[3] &= 0x7ffffffffffff;
+  t[0] += 19 * (t[4] >> 51); t[4] &= 0x7ffffffffffff;
+
+  /* now t is between 0 and 2^255-1, properly carried. */
+  /* case 1: between 0 and 2^255-20. case 2: between 2^255-19 and 2^255-1. */
+
+  t[0] += 19;
+
+  t[1] += t[0] >> 51; t[0] &= 0x7ffffffffffff;
+  t[2] += t[1] >> 51; t[1] &= 0x7ffffffffffff;
+  t[3] += t[2] >> 51; t[2] &= 0x7ffffffffffff;
+  t[4] += t[3] >> 51; t[3] &= 0x7ffffffffffff;
+  t[0] += 19 * (t[4] >> 51); t[4] &= 0x7ffffffffffff;
+
+  /* now between 19 and 2^255-1 in both cases, and offset by 19. */
+
+  t[0] += 0x8000000000000 - 19;
+  t[1] += 0x8000000000000 - 1;
+  t[2] += 0x8000000000000 - 1;
+  t[3] += 0x8000000000000 - 1;
+  t[4] += 0x8000000000000 - 1;
+
+  /* now between 2^255 and 2^256-20, and offset by 2^255. */
+
+  t[1] += t[0] >> 51; t[0] &= 0x7ffffffffffff;
+  t[2] += t[1] >> 51; t[1] &= 0x7ffffffffffff;
+  t[3] += t[2] >> 51; t[2] &= 0x7ffffffffffff;
+  t[4] += t[3] >> 51; t[3] &= 0x7ffffffffffff;
+  t[4] &= 0x7ffffffffffff;
+
+  store_limb(output,    t[0] | (t[1] << 51));
+  store_limb(output+8,  (t[1] >> 13) | (t[2] << 38));
+  store_limb(output+16, (t[2] >> 26) | (t[3] << 25));
+  store_limb(output+24, (t[3] >> 39) | (t[4] << 12));
+}
+
+/* Input: Q, Q', Q-Q'
+ * Output: 2Q, Q+Q'
+ *
+ *   x2 z3: long form
+ *   x3 z3: long form
+ *   x z: short form, destroyed
+ *   xprime zprime: short form, destroyed
+ *   qmqp: short form, preserved
+ */
+static void
+fmonty(limb *x2, limb *z2, /* output 2Q */
+       limb *x3, limb *z3, /* output Q + Q' */
+       limb *x, limb *z,   /* input Q */
+       limb *xprime, limb *zprime, /* input Q' */
+       const limb *qmqp /* input Q - Q' */) {
+  limb origx[5], origxprime[5], zzz[5], xx[5], zz[5], xxprime[5],
+        zzprime[5], zzzprime[5];
+
+  memcpy(origx, x, 5 * sizeof(limb));
+  fsum(x, z);
+  fdifference_backwards(z, origx);  // does x - z
+
+  memcpy(origxprime, xprime, sizeof(limb) * 5);
+  fsum(xprime, zprime);
+  fdifference_backwards(zprime, origxprime);
+  fmul(xxprime, xprime, z);
+  fmul(zzprime, x, zprime);
+  memcpy(origxprime, xxprime, sizeof(limb) * 5);
+  fsum(xxprime, zzprime);
+  fdifference_backwards(zzprime, origxprime);
+  fsquare_times(x3, xxprime, 1);
+  fsquare_times(zzzprime, zzprime, 1);
+  fmul(z3, zzzprime, qmqp);
+
+  fsquare_times(xx, x, 1);
+  fsquare_times(zz, z, 1);
+  fmul(x2, xx, zz);
+  fdifference_backwards(zz, xx);  // does zz = xx - zz
+  fscalar_product(zzz, zz, 121665);
+  fsum(zzz, xx);
+  fmul(z2, zz, zzz);
+}
+
+// -----------------------------------------------------------------------------
+// Maybe swap the contents of two limb arrays (@a and @b), each @len elements
+// long. Perform the swap iff @swap is non-zero.
+//
+// This function performs the swap without leaking any side-channel
+// information.
+// -----------------------------------------------------------------------------
+static void
+swap_conditional(limb a[5], limb b[5], limb iswap) {
+  unsigned i;
+  const limb swap = -iswap;
+
+  for (i = 0; i < 5; ++i) {
+    const limb x = swap & (a[i] ^ b[i]);
+    a[i] ^= x;
+    b[i] ^= x;
+  }
+}
+
+/* Calculates nQ where Q is the x-coordinate of a point on the curve
+ *
+ *   resultx/resultz: the x coordinate of the resulting curve point (short form)
+ *   n: a little endian, 32-byte number
+ *   q: a point of the curve (short form)
+ */
+static void
+cmult(limb *resultx, limb *resultz, const u8 *n, const limb *q) {
+  limb a[5] = {0}, b[5] = {1}, c[5] = {1}, d[5] = {0};
+  limb *nqpqx = a, *nqpqz = b, *nqx = c, *nqz = d, *t;
+  limb e[5] = {0}, f[5] = {1}, g[5] = {0}, h[5] = {1};
+  limb *nqpqx2 = e, *nqpqz2 = f, *nqx2 = g, *nqz2 = h;
+
+  unsigned i, j;
+
+  memcpy(nqpqx, q, sizeof(limb) * 5);
+
+  for (i = 0; i < 32; ++i) {
+    u8 byte = n[31 - i];
+    for (j = 0; j < 8; ++j) {
+      const limb bit = byte >> 7;
+
+      swap_conditional(nqx, nqpqx, bit);
+      swap_conditional(nqz, nqpqz, bit);
+      fmonty(nqx2, nqz2,
+             nqpqx2, nqpqz2,
+             nqx, nqz,
+             nqpqx, nqpqz,
+             q);
+      swap_conditional(nqx2, nqpqx2, bit);
+      swap_conditional(nqz2, nqpqz2, bit);
+
+      t = nqx;
+      nqx = nqx2;
+      nqx2 = t;
+      t = nqz;
+      nqz = nqz2;
+      nqz2 = t;
+      t = nqpqx;
+      nqpqx = nqpqx2;
+      nqpqx2 = t;
+      t = nqpqz;
+      nqpqz = nqpqz2;
+      nqpqz2 = t;
+
+      byte <<= 1;
+    }
+  }
+
+  memcpy(resultx, nqx, sizeof(limb) * 5);
+  memcpy(resultz, nqz, sizeof(limb) * 5);
+}
+
+
+// -----------------------------------------------------------------------------
+// Shamelessly copied from djb's code, tightened a little
+// -----------------------------------------------------------------------------
+static void
+crecip(felem out, const felem z) {
+  felem a,t0,b,c;
+
+  /* 2 */ fsquare_times(a, z, 1); // a = 2
+  /* 8 */ fsquare_times(t0, a, 2);
+  /* 9 */ fmul(b, t0, z); // b = 9
+  /* 11 */ fmul(a, b, a); // a = 11
+  /* 22 */ fsquare_times(t0, a, 1);
+  /* 2^5 - 2^0 = 31 */ fmul(b, t0, b);
+  /* 2^10 - 2^5 */ fsquare_times(t0, b, 5);
+  /* 2^10 - 2^0 */ fmul(b, t0, b);
+  /* 2^20 - 2^10 */ fsquare_times(t0, b, 10);
+  /* 2^20 - 2^0 */ fmul(c, t0, b);
+  /* 2^40 - 2^20 */ fsquare_times(t0, c, 20);
+  /* 2^40 - 2^0 */ fmul(t0, t0, c);
+  /* 2^50 - 2^10 */ fsquare_times(t0, t0, 10);
+  /* 2^50 - 2^0 */ fmul(b, t0, b);
+  /* 2^100 - 2^50 */ fsquare_times(t0, b, 50);
+  /* 2^100 - 2^0 */ fmul(c, t0, b);
+  /* 2^200 - 2^100 */ fsquare_times(t0, c, 100);
+  /* 2^200 - 2^0 */ fmul(t0, t0, c);
+  /* 2^250 - 2^50 */ fsquare_times(t0, t0, 50);
+  /* 2^250 - 2^0 */ fmul(t0, t0, b);
+  /* 2^255 - 2^5 */ fsquare_times(t0, t0, 5);
+  /* 2^255 - 21 */ fmul(out, t0, a);
+}
+
+int
+crypto_scalarmult_curve25519(u8 *mypublic, const u8 *secret, const u8 *basepoint) {
+  limb bp[5], x[5], z[5], zmone[5];
+  uint8_t e[32];
+  int i;
+
+  for (i = 0;i < 32;++i) e[i] = secret[i];
+  e[0] &= 248;
+  e[31] &= 127;
+  e[31] |= 64;
+
+  fexpand(bp, basepoint);
+  cmult(x, z, e, bp);
+  crecip(zmone, z);
+  fmul(z, x, zmone);
+  fcontract(mypublic, z);
+  return 0;
+}
+
+#endif

+ 864 - 0
app/jni/nacl/smult_donna.c

@@ -0,0 +1,864 @@
+/* Copyright 2008, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * curve25519-donna: Curve25519 elliptic curve, public key function
+ *
+ * http://code.google.com/p/curve25519-donna/
+ *
+ * Adam Langley <agl@imperialviolet.org>
+ *
+ * Derived from public domain C code by Daniel J. Bernstein <djb@cr.yp.to>
+ *
+ * More information about curve25519 can be found here
+ *   http://cr.yp.to/ecdh.html
+ *
+ * djb's sample implementation of curve25519 is written in a special assembly
+ * language called qhasm and uses the floating point registers.
+ *
+ * This is, almost, a clean room reimplementation from the curve25519 paper. It
+ * uses many of the tricks described therein. Only the crecip function is taken
+ * from the sample implementation. */
+
+#if defined(__arm__) || defined(__i386__)
+
+#include <string.h>
+#include <stdint.h>
+
+#ifdef _MSC_VER
+#define inline __inline
+#endif
+
+typedef uint8_t u8;
+typedef int32_t s32;
+typedef int64_t limb;
+
+/* Field element representation:
+ *
+ * Field elements are written as an array of signed, 64-bit limbs, least
+ * significant first. The value of the field element is:
+ *   x[0] + 2^26·x[1] + x^51·x[2] + 2^102·x[3] + ...
+ *
+ * i.e. the limbs are 26, 25, 26, 25, ... bits wide. */
+
+/* Sum two numbers: output += in */
+static void fsum(limb *output, const limb *in) {
+  unsigned i;
+  for (i = 0; i < 10; i += 2) {
+    output[0+i] = output[0+i] + in[0+i];
+    output[1+i] = output[1+i] + in[1+i];
+  }
+}
+
+/* Find the difference of two numbers: output = in - output
+ * (note the order of the arguments!). */
+static void fdifference(limb *output, const limb *in) {
+  unsigned i;
+  for (i = 0; i < 10; ++i) {
+    output[i] = in[i] - output[i];
+  }
+}
+
+/* Multiply a number by a scalar: output = in * scalar */
+static void fscalar_product(limb *output, const limb *in, const limb scalar) {
+  unsigned i;
+  for (i = 0; i < 10; ++i) {
+    output[i] = in[i] * scalar;
+  }
+}
+
+/* Multiply two numbers: output = in2 * in
+ *
+ * output must be distinct to both inputs. The inputs are reduced coefficient
+ * form, the output is not.
+ *
+ * output[x] <= 14 * the largest product of the input limbs. */
+static void fproduct(limb *output, const limb *in2, const limb *in) {
+  output[0] =       ((limb) ((s32) in2[0])) * ((s32) in[0]);
+  output[1] =       ((limb) ((s32) in2[0])) * ((s32) in[1]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[0]);
+  output[2] =  2 *  ((limb) ((s32) in2[1])) * ((s32) in[1]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[0]);
+  output[3] =       ((limb) ((s32) in2[1])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[1]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[0]);
+  output[4] =       ((limb) ((s32) in2[2])) * ((s32) in[2]) +
+               2 * (((limb) ((s32) in2[1])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[1])) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[0]);
+  output[5] =       ((limb) ((s32) in2[2])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[1]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[0]);
+  output[6] =  2 * (((limb) ((s32) in2[3])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[1])) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[0]);
+  output[7] =       ((limb) ((s32) in2[3])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[1]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[0]);
+  output[8] =       ((limb) ((s32) in2[4])) * ((s32) in[4]) +
+               2 * (((limb) ((s32) in2[3])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[1])) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[0]);
+  output[9] =       ((limb) ((s32) in2[4])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[2]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[1]) +
+                    ((limb) ((s32) in2[0])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[0]);
+  output[10] = 2 * (((limb) ((s32) in2[5])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[1])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[1])) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[2]);
+  output[11] =      ((limb) ((s32) in2[5])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[4]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[3]) +
+                    ((limb) ((s32) in2[2])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[2]);
+  output[12] =      ((limb) ((s32) in2[6])) * ((s32) in[6]) +
+               2 * (((limb) ((s32) in2[5])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[3])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[3])) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[4]);
+  output[13] =      ((limb) ((s32) in2[6])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[7])) * ((s32) in[6]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[5]) +
+                    ((limb) ((s32) in2[4])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[4]);
+  output[14] = 2 * (((limb) ((s32) in2[7])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[5])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[5])) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[6]);
+  output[15] =      ((limb) ((s32) in2[7])) * ((s32) in[8]) +
+                    ((limb) ((s32) in2[8])) * ((s32) in[7]) +
+                    ((limb) ((s32) in2[6])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[6]);
+  output[16] =      ((limb) ((s32) in2[8])) * ((s32) in[8]) +
+               2 * (((limb) ((s32) in2[7])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[7]));
+  output[17] =      ((limb) ((s32) in2[8])) * ((s32) in[9]) +
+                    ((limb) ((s32) in2[9])) * ((s32) in[8]);
+  output[18] = 2 *  ((limb) ((s32) in2[9])) * ((s32) in[9]);
+}
+
+/* Reduce a long form to a short form by taking the input mod 2^255 - 19.
+ *
+ * On entry: |output[i]| < 14*2^54
+ * On exit: |output[0..8]| < 280*2^54 */
+static void freduce_degree(limb *output) {
+  /* Each of these shifts and adds ends up multiplying the value by 19.
+   *
+   * For output[0..8], the absolute entry value is < 14*2^54 and we add, at
+   * most, 19*14*2^54 thus, on exit, |output[0..8]| < 280*2^54. */
+  output[8] += output[18] << 4;
+  output[8] += output[18] << 1;
+  output[8] += output[18];
+  output[7] += output[17] << 4;
+  output[7] += output[17] << 1;
+  output[7] += output[17];
+  output[6] += output[16] << 4;
+  output[6] += output[16] << 1;
+  output[6] += output[16];
+  output[5] += output[15] << 4;
+  output[5] += output[15] << 1;
+  output[5] += output[15];
+  output[4] += output[14] << 4;
+  output[4] += output[14] << 1;
+  output[4] += output[14];
+  output[3] += output[13] << 4;
+  output[3] += output[13] << 1;
+  output[3] += output[13];
+  output[2] += output[12] << 4;
+  output[2] += output[12] << 1;
+  output[2] += output[12];
+  output[1] += output[11] << 4;
+  output[1] += output[11] << 1;
+  output[1] += output[11];
+  output[0] += output[10] << 4;
+  output[0] += output[10] << 1;
+  output[0] += output[10];
+}
+
+#if (-1 & 3) != 3
+#error "This code only works on a two's complement system"
+#endif
+
+/* return v / 2^26, using only shifts and adds.
+ *
+ * On entry: v can take any value. */
+static inline limb
+div_by_2_26(const limb v)
+{
+  /* High word of v; no shift needed. */
+  const uint32_t highword = (uint32_t) (((uint64_t) v) >> 32);
+  /* Set to all 1s if v was negative; else set to 0s. */
+  const int32_t sign = ((int32_t) highword) >> 31;
+  /* Set to 0x3ffffff if v was negative; else set to 0. */
+  const int32_t roundoff = ((uint32_t) sign) >> 6;
+  /* Should return v / (1<<26) */
+  return (v + roundoff) >> 26;
+}
+
+/* return v / (2^25), using only shifts and adds.
+ *
+ * On entry: v can take any value. */
+static inline limb
+div_by_2_25(const limb v)
+{
+  /* High word of v; no shift needed*/
+  const uint32_t highword = (uint32_t) (((uint64_t) v) >> 32);
+  /* Set to all 1s if v was negative; else set to 0s. */
+  const int32_t sign = ((int32_t) highword) >> 31;
+  /* Set to 0x1ffffff if v was negative; else set to 0. */
+  const int32_t roundoff = ((uint32_t) sign) >> 7;
+  /* Should return v / (1<<25) */
+  return (v + roundoff) >> 25;
+}
+
+/* Reduce all coefficients of the short form input so that |x| < 2^26.
+ *
+ * On entry: |output[i]| < 280*2^54 */
+static void freduce_coefficients(limb *output) {
+  unsigned i;
+
+  output[10] = 0;
+
+  for (i = 0; i < 10; i += 2) {
+    limb over = div_by_2_26(output[i]);
+    /* The entry condition (that |output[i]| < 280*2^54) means that over is, at
+     * most, 280*2^28 in the first iteration of this loop. This is added to the
+     * next limb and we can approximate the resulting bound of that limb by
+     * 281*2^54. */
+    output[i] -= over << 26;
+    output[i+1] += over;
+
+    /* For the first iteration, |output[i+1]| < 281*2^54, thus |over| <
+     * 281*2^29. When this is added to the next limb, the resulting bound can
+     * be approximated as 281*2^54.
+     *
+     * For subsequent iterations of the loop, 281*2^54 remains a conservative
+     * bound and no overflow occurs. */
+    over = div_by_2_25(output[i+1]);
+    output[i+1] -= over << 25;
+    output[i+2] += over;
+  }
+  /* Now |output[10]| < 281*2^29 and all other coefficients are reduced. */
+  output[0] += output[10] << 4;
+  output[0] += output[10] << 1;
+  output[0] += output[10];
+
+  output[10] = 0;
+
+  /* Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29
+   * So |over| will be no more than 2^16. */
+  {
+    limb over = div_by_2_26(output[0]);
+    output[0] -= over << 26;
+    output[1] += over;
+  }
+
+  /* Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The
+   * bound on |output[1]| is sufficient to meet our needs. */
+}
+
+/* A helpful wrapper around fproduct: output = in * in2.
+ *
+ * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.
+ *
+ * output must be distinct to both inputs. The output is reduced degree
+ * (indeed, one need only provide storage for 10 limbs) and |output[i]| < 2^26. */
+static void
+fmul(limb *output, const limb *in, const limb *in2) {
+  limb t[19];
+  fproduct(t, in, in2);
+  /* |t[i]| < 14*2^54 */
+  freduce_degree(t);
+  freduce_coefficients(t);
+  /* |t[i]| < 2^26 */
+  memcpy(output, t, sizeof(limb) * 10);
+}
+
+/* Square a number: output = in**2
+ *
+ * output must be distinct from the input. The inputs are reduced coefficient
+ * form, the output is not.
+ *
+ * output[x] <= 14 * the largest product of the input limbs. */
+static void fsquare_inner(limb *output, const limb *in) {
+  output[0] =       ((limb) ((s32) in[0])) * ((s32) in[0]);
+  output[1] =  2 *  ((limb) ((s32) in[0])) * ((s32) in[1]);
+  output[2] =  2 * (((limb) ((s32) in[1])) * ((s32) in[1]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[2]));
+  output[3] =  2 * (((limb) ((s32) in[1])) * ((s32) in[2]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[3]));
+  output[4] =       ((limb) ((s32) in[2])) * ((s32) in[2]) +
+               4 *  ((limb) ((s32) in[1])) * ((s32) in[3]) +
+               2 *  ((limb) ((s32) in[0])) * ((s32) in[4]);
+  output[5] =  2 * (((limb) ((s32) in[2])) * ((s32) in[3]) +
+                    ((limb) ((s32) in[1])) * ((s32) in[4]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[5]));
+  output[6] =  2 * (((limb) ((s32) in[3])) * ((s32) in[3]) +
+                    ((limb) ((s32) in[2])) * ((s32) in[4]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[6]) +
+               2 *  ((limb) ((s32) in[1])) * ((s32) in[5]));
+  output[7] =  2 * (((limb) ((s32) in[3])) * ((s32) in[4]) +
+                    ((limb) ((s32) in[2])) * ((s32) in[5]) +
+                    ((limb) ((s32) in[1])) * ((s32) in[6]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[7]));
+  output[8] =       ((limb) ((s32) in[4])) * ((s32) in[4]) +
+               2 * (((limb) ((s32) in[2])) * ((s32) in[6]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[8]) +
+               2 * (((limb) ((s32) in[1])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[3])) * ((s32) in[5])));
+  output[9] =  2 * (((limb) ((s32) in[4])) * ((s32) in[5]) +
+                    ((limb) ((s32) in[3])) * ((s32) in[6]) +
+                    ((limb) ((s32) in[2])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[1])) * ((s32) in[8]) +
+                    ((limb) ((s32) in[0])) * ((s32) in[9]));
+  output[10] = 2 * (((limb) ((s32) in[5])) * ((s32) in[5]) +
+                    ((limb) ((s32) in[4])) * ((s32) in[6]) +
+                    ((limb) ((s32) in[2])) * ((s32) in[8]) +
+               2 * (((limb) ((s32) in[3])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[1])) * ((s32) in[9])));
+  output[11] = 2 * (((limb) ((s32) in[5])) * ((s32) in[6]) +
+                    ((limb) ((s32) in[4])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[3])) * ((s32) in[8]) +
+                    ((limb) ((s32) in[2])) * ((s32) in[9]));
+  output[12] =      ((limb) ((s32) in[6])) * ((s32) in[6]) +
+               2 * (((limb) ((s32) in[4])) * ((s32) in[8]) +
+               2 * (((limb) ((s32) in[5])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[3])) * ((s32) in[9])));
+  output[13] = 2 * (((limb) ((s32) in[6])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[5])) * ((s32) in[8]) +
+                    ((limb) ((s32) in[4])) * ((s32) in[9]));
+  output[14] = 2 * (((limb) ((s32) in[7])) * ((s32) in[7]) +
+                    ((limb) ((s32) in[6])) * ((s32) in[8]) +
+               2 *  ((limb) ((s32) in[5])) * ((s32) in[9]));
+  output[15] = 2 * (((limb) ((s32) in[7])) * ((s32) in[8]) +
+                    ((limb) ((s32) in[6])) * ((s32) in[9]));
+  output[16] =      ((limb) ((s32) in[8])) * ((s32) in[8]) +
+               4 *  ((limb) ((s32) in[7])) * ((s32) in[9]);
+  output[17] = 2 *  ((limb) ((s32) in[8])) * ((s32) in[9]);
+  output[18] = 2 *  ((limb) ((s32) in[9])) * ((s32) in[9]);
+}
+
+/* fsquare sets output = in^2.
+ *
+ * On entry: The |in| argument is in reduced coefficients form and |in[i]| <
+ * 2^27.
+ *
+ * On exit: The |output| argument is in reduced coefficients form (indeed, one
+ * need only provide storage for 10 limbs) and |out[i]| < 2^26. */
+static void
+fsquare(limb *output, const limb *in) {
+  limb t[19];
+  fsquare_inner(t, in);
+  /* |t[i]| < 14*2^54 because the largest product of two limbs will be <
+   * 2^(27+27) and fsquare_inner adds together, at most, 14 of those
+   * products. */
+  freduce_degree(t);
+  freduce_coefficients(t);
+  /* |t[i]| < 2^26 */
+  memcpy(output, t, sizeof(limb) * 10);
+}
+
+/* Take a little-endian, 32-byte number and expand it into polynomial form */
+static void
+fexpand(limb *output, const u8 *input) {
+#define F(n,start,shift,mask) \
+  output[n] = ((((limb) input[start + 0]) | \
+                ((limb) input[start + 1]) << 8 | \
+                ((limb) input[start + 2]) << 16 | \
+                ((limb) input[start + 3]) << 24) >> shift) & mask;
+  F(0, 0, 0, 0x3ffffff);
+  F(1, 3, 2, 0x1ffffff);
+  F(2, 6, 3, 0x3ffffff);
+  F(3, 9, 5, 0x1ffffff);
+  F(4, 12, 6, 0x3ffffff);
+  F(5, 16, 0, 0x1ffffff);
+  F(6, 19, 1, 0x3ffffff);
+  F(7, 22, 3, 0x1ffffff);
+  F(8, 25, 4, 0x3ffffff);
+  F(9, 28, 6, 0x1ffffff);
+#undef F
+}
+
+#if (-32 >> 1) != -16
+#error "This code only works when >> does sign-extension on negative numbers"
+#endif
+
+/* s32_eq returns 0xffffffff iff a == b and zero otherwise. */
+static s32 s32_eq(s32 a, s32 b) {
+  a = ~(a ^ b);
+  a &= a << 16;
+  a &= a << 8;
+  a &= a << 4;
+  a &= a << 2;
+  a &= a << 1;
+  return a >> 31;
+}
+
+/* s32_gte returns 0xffffffff if a >= b and zero otherwise, where a and b are
+ * both non-negative. */
+static s32 s32_gte(s32 a, s32 b) {
+  a -= b;
+  /* a >= 0 iff a >= b. */
+  return ~(a >> 31);
+}
+
+/* Take a fully reduced polynomial form number and contract it into a
+ * little-endian, 32-byte array.
+ *
+ * On entry: |input_limbs[i]| < 2^26 */
+static void
+fcontract(u8 *output, limb *input_limbs) {
+  int i;
+  int j;
+  s32 input[10];
+  s32 mask;
+
+  /* |input_limbs[i]| < 2^26, so it's valid to convert to an s32. */
+  for (i = 0; i < 10; i++) {
+    input[i] = input_limbs[i];
+  }
+
+  for (j = 0; j < 2; ++j) {
+    for (i = 0; i < 9; ++i) {
+      if ((i & 1) == 1) {
+        /* This calculation is a time-invariant way to make input[i]
+         * non-negative by borrowing from the next-larger limb. */
+        const s32 mask = input[i] >> 31;
+        const s32 carry = -((input[i] & mask) >> 25);
+        input[i] = input[i] + (carry << 25);
+        input[i+1] = input[i+1] - carry;
+      } else {
+        const s32 mask = input[i] >> 31;
+        const s32 carry = -((input[i] & mask) >> 26);
+        input[i] = input[i] + (carry << 26);
+        input[i+1] = input[i+1] - carry;
+      }
+    }
+
+    /* There's no greater limb for input[9] to borrow from, but we can multiply
+     * by 19 and borrow from input[0], which is valid mod 2^255-19. */
+    {
+      const s32 mask = input[9] >> 31;
+      const s32 carry = -((input[9] & mask) >> 25);
+      input[9] = input[9] + (carry << 25);
+      input[0] = input[0] - (carry * 19);
+    }
+
+    /* After the first iteration, input[1..9] are non-negative and fit within
+     * 25 or 26 bits, depending on position. However, input[0] may be
+     * negative. */
+  }
+
+  /* The first borrow-propagation pass above ended with every limb
+     except (possibly) input[0] non-negative.
+
+     If input[0] was negative after the first pass, then it was because of a
+     carry from input[9]. On entry, input[9] < 2^26 so the carry was, at most,
+     one, since (2**26-1) >> 25 = 1. Thus input[0] >= -19.
+
+     In the second pass, each limb is decreased by at most one. Thus the second
+     borrow-propagation pass could only have wrapped around to decrease
+     input[0] again if the first pass left input[0] negative *and* input[1]
+     through input[9] were all zero.  In that case, input[1] is now 2^25 - 1,
+     and this last borrow-propagation step will leave input[1] non-negative. */
+  {
+    const s32 mask = input[0] >> 31;
+    const s32 carry = -((input[0] & mask) >> 26);
+    input[0] = input[0] + (carry << 26);
+    input[1] = input[1] - carry;
+  }
+
+  /* All input[i] are now non-negative. However, there might be values between
+   * 2^25 and 2^26 in a limb which is, nominally, 25 bits wide. */
+  for (j = 0; j < 2; j++) {
+    for (i = 0; i < 9; i++) {
+      if ((i & 1) == 1) {
+        const s32 carry = input[i] >> 25;
+        input[i] &= 0x1ffffff;
+        input[i+1] += carry;
+      } else {
+        const s32 carry = input[i] >> 26;
+        input[i] &= 0x3ffffff;
+        input[i+1] += carry;
+      }
+    }
+
+    {
+      const s32 carry = input[9] >> 25;
+      input[9] &= 0x1ffffff;
+      input[0] += 19*carry;
+    }
+  }
+
+  /* If the first carry-chain pass, just above, ended up with a carry from
+   * input[9], and that caused input[0] to be out-of-bounds, then input[0] was
+   * < 2^26 + 2*19, because the carry was, at most, two.
+   *
+   * If the second pass carried from input[9] again then input[0] is < 2*19 and
+   * the input[9] -> input[0] carry didn't push input[0] out of bounds. */
+
+  /* It still remains the case that input might be between 2^255-19 and 2^255.
+   * In this case, input[1..9] must take their maximum value and input[0] must
+   * be >= (2^255-19) & 0x3ffffff, which is 0x3ffffed. */
+  mask = s32_gte(input[0], 0x3ffffed);
+  for (i = 1; i < 10; i++) {
+    if ((i & 1) == 1) {
+      mask &= s32_eq(input[i], 0x1ffffff);
+    } else {
+      mask &= s32_eq(input[i], 0x3ffffff);
+    }
+  }
+
+  /* mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus
+   * this conditionally subtracts 2^255-19. */
+  input[0] -= mask & 0x3ffffed;
+
+  for (i = 1; i < 10; i++) {
+    if ((i & 1) == 1) {
+      input[i] -= mask & 0x1ffffff;
+    } else {
+      input[i] -= mask & 0x3ffffff;
+    }
+  }
+
+  input[1] <<= 2;
+  input[2] <<= 3;
+  input[3] <<= 5;
+  input[4] <<= 6;
+  input[6] <<= 1;
+  input[7] <<= 3;
+  input[8] <<= 4;
+  input[9] <<= 6;
+#define F(i, s) \
+  output[s+0] |=  input[i] & 0xff; \
+  output[s+1]  = (input[i] >> 8) & 0xff; \
+  output[s+2]  = (input[i] >> 16) & 0xff; \
+  output[s+3]  = (input[i] >> 24) & 0xff;
+  output[0] = 0;
+  output[16] = 0;
+  F(0,0);
+  F(1,3);
+  F(2,6);
+  F(3,9);
+  F(4,12);
+  F(5,16);
+  F(6,19);
+  F(7,22);
+  F(8,25);
+  F(9,28);
+#undef F
+}
+
+/* Input: Q, Q', Q-Q'
+ * Output: 2Q, Q+Q'
+ *
+ *   x2 z3: long form
+ *   x3 z3: long form
+ *   x z: short form, destroyed
+ *   xprime zprime: short form, destroyed
+ *   qmqp: short form, preserved
+ *
+ * On entry and exit, the absolute value of the limbs of all inputs and outputs
+ * are < 2^26. */
+static void fmonty(limb *x2, limb *z2,  /* output 2Q */
+                   limb *x3, limb *z3,  /* output Q + Q' */
+                   limb *x, limb *z,    /* input Q */
+                   limb *xprime, limb *zprime,  /* input Q' */
+                   const limb *qmqp /* input Q - Q' */) {
+  limb origx[10], origxprime[10], zzz[19], xx[19], zz[19], xxprime[19],
+        zzprime[19], zzzprime[19], xxxprime[19];
+
+  memcpy(origx, x, 10 * sizeof(limb));
+  fsum(x, z);
+  /* |x[i]| < 2^27 */
+  fdifference(z, origx);  /* does x - z */
+  /* |z[i]| < 2^27 */
+
+  memcpy(origxprime, xprime, sizeof(limb) * 10);
+  fsum(xprime, zprime);
+  /* |xprime[i]| < 2^27 */
+  fdifference(zprime, origxprime);
+  /* |zprime[i]| < 2^27 */
+  fproduct(xxprime, xprime, z);
+  /* |xxprime[i]| < 14*2^54: the largest product of two limbs will be <
+   * 2^(27+27) and fproduct adds together, at most, 14 of those products.
+   * (Approximating that to 2^58 doesn't work out.) */
+  fproduct(zzprime, x, zprime);
+  /* |zzprime[i]| < 14*2^54 */
+  freduce_degree(xxprime);
+  freduce_coefficients(xxprime);
+  /* |xxprime[i]| < 2^26 */
+  freduce_degree(zzprime);
+  freduce_coefficients(zzprime);
+  /* |zzprime[i]| < 2^26 */
+  memcpy(origxprime, xxprime, sizeof(limb) * 10);
+  fsum(xxprime, zzprime);
+  /* |xxprime[i]| < 2^27 */
+  fdifference(zzprime, origxprime);
+  /* |zzprime[i]| < 2^27 */
+  fsquare(xxxprime, xxprime);
+  /* |xxxprime[i]| < 2^26 */
+  fsquare(zzzprime, zzprime);
+  /* |zzzprime[i]| < 2^26 */
+  fproduct(zzprime, zzzprime, qmqp);
+  /* |zzprime[i]| < 14*2^52 */
+  freduce_degree(zzprime);
+  freduce_coefficients(zzprime);
+  /* |zzprime[i]| < 2^26 */
+  memcpy(x3, xxxprime, sizeof(limb) * 10);
+  memcpy(z3, zzprime, sizeof(limb) * 10);
+
+  fsquare(xx, x);
+  /* |xx[i]| < 2^26 */
+  fsquare(zz, z);
+  /* |zz[i]| < 2^26 */
+  fproduct(x2, xx, zz);
+  /* |x2[i]| < 14*2^52 */
+  freduce_degree(x2);
+  freduce_coefficients(x2);
+  /* |x2[i]| < 2^26 */
+  fdifference(zz, xx);  // does zz = xx - zz
+  /* |zz[i]| < 2^27 */
+  memset(zzz + 10, 0, sizeof(limb) * 9);
+  fscalar_product(zzz, zz, 121665);
+  /* |zzz[i]| < 2^(27+17) */
+  /* No need to call freduce_degree here:
+     fscalar_product doesn't increase the degree of its input. */
+  freduce_coefficients(zzz);
+  /* |zzz[i]| < 2^26 */
+  fsum(zzz, xx);
+  /* |zzz[i]| < 2^27 */
+  fproduct(z2, zz, zzz);
+  /* |z2[i]| < 14*2^(26+27) */
+  freduce_degree(z2);
+  freduce_coefficients(z2);
+  /* |z2|i| < 2^26 */
+}
+
+/* Conditionally swap two reduced-form limb arrays if 'iswap' is 1, but leave
+ * them unchanged if 'iswap' is 0.  Runs in data-invariant time to avoid
+ * side-channel attacks.
+ *
+ * NOTE that this function requires that 'iswap' be 1 or 0; other values give
+ * wrong results.  Also, the two limb arrays must be in reduced-coefficient,
+ * reduced-degree form: the values in a[10..19] or b[10..19] aren't swapped,
+ * and all all values in a[0..9],b[0..9] must have magnitude less than
+ * INT32_MAX. */
+static void
+swap_conditional(limb a[19], limb b[19], limb iswap) {
+  unsigned i;
+  const s32 swap = (s32) -iswap;
+
+  for (i = 0; i < 10; ++i) {
+    const s32 x = swap & ( ((s32)a[i]) ^ ((s32)b[i]) );
+    a[i] = ((s32)a[i]) ^ x;
+    b[i] = ((s32)b[i]) ^ x;
+  }
+}
+
+/* Calculates nQ where Q is the x-coordinate of a point on the curve
+ *
+ *   resultx/resultz: the x coordinate of the resulting curve point (short form)
+ *   n: a little endian, 32-byte number
+ *   q: a point of the curve (short form) */
+static void
+cmult(limb *resultx, limb *resultz, const u8 *n, const limb *q) {
+  limb a[19] = {0}, b[19] = {1}, c[19] = {1}, d[19] = {0};
+  limb *nqpqx = a, *nqpqz = b, *nqx = c, *nqz = d, *t;
+  limb e[19] = {0}, f[19] = {1}, g[19] = {0}, h[19] = {1};
+  limb *nqpqx2 = e, *nqpqz2 = f, *nqx2 = g, *nqz2 = h;
+
+  unsigned i, j;
+
+  memcpy(nqpqx, q, sizeof(limb) * 10);
+
+  for (i = 0; i < 32; ++i) {
+    u8 byte = n[31 - i];
+    for (j = 0; j < 8; ++j) {
+      const limb bit = byte >> 7;
+
+      swap_conditional(nqx, nqpqx, bit);
+      swap_conditional(nqz, nqpqz, bit);
+      fmonty(nqx2, nqz2,
+             nqpqx2, nqpqz2,
+             nqx, nqz,
+             nqpqx, nqpqz,
+             q);
+      swap_conditional(nqx2, nqpqx2, bit);
+      swap_conditional(nqz2, nqpqz2, bit);
+
+      t = nqx;
+      nqx = nqx2;
+      nqx2 = t;
+      t = nqz;
+      nqz = nqz2;
+      nqz2 = t;
+      t = nqpqx;
+      nqpqx = nqpqx2;
+      nqpqx2 = t;
+      t = nqpqz;
+      nqpqz = nqpqz2;
+      nqpqz2 = t;
+
+      byte <<= 1;
+    }
+  }
+
+  memcpy(resultx, nqx, sizeof(limb) * 10);
+  memcpy(resultz, nqz, sizeof(limb) * 10);
+}
+
+// -----------------------------------------------------------------------------
+// Shamelessly copied from djb's code
+// -----------------------------------------------------------------------------
+static void
+crecip(limb *out, const limb *z) {
+  limb z2[10];
+  limb z9[10];
+  limb z11[10];
+  limb z2_5_0[10];
+  limb z2_10_0[10];
+  limb z2_20_0[10];
+  limb z2_50_0[10];
+  limb z2_100_0[10];
+  limb t0[10];
+  limb t1[10];
+  int i;
+
+  /* 2 */ fsquare(z2,z);
+  /* 4 */ fsquare(t1,z2);
+  /* 8 */ fsquare(t0,t1);
+  /* 9 */ fmul(z9,t0,z);
+  /* 11 */ fmul(z11,z9,z2);
+  /* 22 */ fsquare(t0,z11);
+  /* 2^5 - 2^0 = 31 */ fmul(z2_5_0,t0,z9);
+
+  /* 2^6 - 2^1 */ fsquare(t0,z2_5_0);
+  /* 2^7 - 2^2 */ fsquare(t1,t0);
+  /* 2^8 - 2^3 */ fsquare(t0,t1);
+  /* 2^9 - 2^4 */ fsquare(t1,t0);
+  /* 2^10 - 2^5 */ fsquare(t0,t1);
+  /* 2^10 - 2^0 */ fmul(z2_10_0,t0,z2_5_0);
+
+  /* 2^11 - 2^1 */ fsquare(t0,z2_10_0);
+  /* 2^12 - 2^2 */ fsquare(t1,t0);
+  /* 2^20 - 2^10 */ for (i = 2;i < 10;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
+  /* 2^20 - 2^0 */ fmul(z2_20_0,t1,z2_10_0);
+
+  /* 2^21 - 2^1 */ fsquare(t0,z2_20_0);
+  /* 2^22 - 2^2 */ fsquare(t1,t0);
+  /* 2^40 - 2^20 */ for (i = 2;i < 20;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
+  /* 2^40 - 2^0 */ fmul(t0,t1,z2_20_0);
+
+  /* 2^41 - 2^1 */ fsquare(t1,t0);
+  /* 2^42 - 2^2 */ fsquare(t0,t1);
+  /* 2^50 - 2^10 */ for (i = 2;i < 10;i += 2) { fsquare(t1,t0); fsquare(t0,t1); }
+  /* 2^50 - 2^0 */ fmul(z2_50_0,t0,z2_10_0);
+
+  /* 2^51 - 2^1 */ fsquare(t0,z2_50_0);
+  /* 2^52 - 2^2 */ fsquare(t1,t0);
+  /* 2^100 - 2^50 */ for (i = 2;i < 50;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
+  /* 2^100 - 2^0 */ fmul(z2_100_0,t1,z2_50_0);
+
+  /* 2^101 - 2^1 */ fsquare(t1,z2_100_0);
+  /* 2^102 - 2^2 */ fsquare(t0,t1);
+  /* 2^200 - 2^100 */ for (i = 2;i < 100;i += 2) { fsquare(t1,t0); fsquare(t0,t1); }
+  /* 2^200 - 2^0 */ fmul(t1,t0,z2_100_0);
+
+  /* 2^201 - 2^1 */ fsquare(t0,t1);
+  /* 2^202 - 2^2 */ fsquare(t1,t0);
+  /* 2^250 - 2^50 */ for (i = 2;i < 50;i += 2) { fsquare(t0,t1); fsquare(t1,t0); }
+  /* 2^250 - 2^0 */ fmul(t0,t1,z2_50_0);
+
+  /* 2^251 - 2^1 */ fsquare(t1,t0);
+  /* 2^252 - 2^2 */ fsquare(t0,t1);
+  /* 2^253 - 2^3 */ fsquare(t1,t0);
+  /* 2^254 - 2^4 */ fsquare(t0,t1);
+  /* 2^255 - 2^5 */ fsquare(t1,t0);
+  /* 2^255 - 21 */ fmul(out,t1,z11);
+}
+
+int
+crypto_scalarmult_curve25519(u8 *mypublic, const u8 *secret, const u8 *basepoint) {
+  limb bp[10], x[10], z[11], zmone[10];
+  uint8_t e[32];
+  int i;
+
+  for (i = 0; i < 32; ++i) e[i] = secret[i];
+  e[0] &= 248;
+  e[31] &= 127;
+  e[31] |= 64;
+
+  fexpand(bp, basepoint);
+  cmult(x, z, e, bp);
+  crecip(zmone, z);
+  fmul(z, x, zmone);
+  fcontract(mypublic, z);
+  return 0;
+}
+
+#endif

+ 11 - 4
app/src/main/java/ch/threema/app/activities/ContactDetailActivity.java

@@ -833,15 +833,13 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 		if (TestUtil.empty(lookupKey)) {
 			return;
 		}
-		GenericProgressDialog.newInstance(R.string.please_wait, R.string.please_wait).show(getSupportFragmentManager(), "pleaseWait");
+		GenericProgressDialog.newInstance(-1, R.string.please_wait).show(getSupportFragmentManager(), "pleaseWait");
 
 		new Thread(new Runnable() {
 			@Override
 			public void run() {
 				try {
-
 					contactService.link(contact, lookupKey);
-
 				} catch (final Exception e) {
 				 	RuntimeUtil.runOnUiThread(new Runnable() {
 						@Override
@@ -885,16 +883,25 @@ public class ContactDetailActivity extends ThreemaToolbarActivity
 
 					}
 					if (contactUri != null) {
-						Cursor cursor = getContentResolver().query(contactUri, new String[]{ContactsContract.Contacts.LOOKUP_KEY}, null, null, null);
+						Cursor cursor = getContentResolver().query(contactUri, new String[]{
+							ContactsContract.Contacts._ID,
+							ContactsContract.Contacts.LOOKUP_KEY
+						}, null, null, null);
 
 						//get the lookup key
 						if (cursor != null) {
 							String lookupKey = null;
+							int contactId = 0;
 							if (cursor.moveToFirst()) {
 								lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
+								contactId = cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts._ID));
+
 							}
 							cursor.close();
 							if (!TestUtil.empty(lookupKey)) {
+								if (contactId != 0) {
+									lookupKey += "/" + contactId;
+								}
 								this.link(lookupKey);
 							}
 						}

+ 18 - 25
app/src/main/java/ch/threema/app/activities/SendMediaActivity.java

@@ -38,7 +38,6 @@ import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Handler;
 import android.provider.MediaStore;
 import android.text.Editable;
 import android.text.TextWatcher;
@@ -115,7 +114,6 @@ import ch.threema.app.utils.EditTextUtil;
 import ch.threema.app.utils.FileUtil;
 import ch.threema.app.utils.IntentDataUtil;
 import ch.threema.app.utils.MimeUtil;
-import ch.threema.app.utils.RuntimeUtil;
 import ch.threema.app.utils.TestUtil;
 import ch.threema.app.video.VideoTimelineCache;
 import ch.threema.base.ThreemaException;
@@ -174,6 +172,7 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 	private int bigImagePos = 0;
 	private boolean useExternalCamera;
 	private VideoEditView videoEditView;
+	private ImageButton settingsItem;
 
 	@Override
 	protected void onCreate(Bundle savedInstanceState) {
@@ -582,6 +581,13 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 				editImage();
 			}
 		});
+		settingsItem = findViewById(R.id.settings);
+		findViewById(R.id.settings).setOnClickListener(new DebouncedOnClickListener(IMAGE_ANIMATION_DURATION_MS) {
+			@Override
+			public void onDebouncedClick(View v) {
+				showSettingsDropDown(v, SendMediaActivity.this.mediaItems.get(bigImagePos));
+			}
+		});
 
 		this.editPanel = findViewById(R.id.edit_panel);
 
@@ -616,11 +622,6 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 			public void onDeleteKeyClicked(MediaItem item) {
 				removeItem(item);
 			}
-
-			@Override
-			public void onSettingsKeyClicked(View view, MediaItem item) {
-				showSettingsDropDown(view, item);
-			}
 		};
 
 		this.sendMediaGridAdapter = new SendMediaGridAdapter(
@@ -655,27 +656,16 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 
 	@UiThread
 	public void maybeShowImageResolutionTooltip() {
-		gridView.postDelayed(() -> {
-			if (sendMediaGridAdapter.holdsAdjustableImage() && !preferenceService.getIsImageResolutionTooltipShown()) {
+		editPanel.postDelayed(() -> {
+			if (settingsItem.getVisibility() == View.VISIBLE && !preferenceService.getIsImageResolutionTooltipShown()) {
 				int[] location = new int[2];
-				gridView.getLocationOnScreen(location);
-				location[0] += getResources().getDimensionPixelSize(R.dimen.grid_spacing);
-				location[1] += gridView.getHeight();
+				settingsItem.getLocationOnScreen(location);
+				location[1] -= (settingsItem.getHeight() / 5);
 
-				final TooltipPopup resolutionTooltipPopup = new TooltipPopup(this, R.string.preferences__image_resolution_tooltip_shown, R.layout.popup_tooltip_bottom_left_image_resolution, this, null);
-				resolutionTooltipPopup.show(this, gridView, getString(R.string.tooltip_image_resolution_hint), TooltipPopup.ALIGN_ABOVE_ANCHOR_ARROW_LEFT, location, 0);
+				final TooltipPopup resolutionTooltipPopup = new TooltipPopup(SendMediaActivity.this, R.string.preferences__image_resolution_tooltip_shown, R.layout.popup_tooltip_top_right, SendMediaActivity.this);
+				resolutionTooltipPopup.show(this, settingsItem, getString(R.string.tooltip_image_resolution_hint), TooltipPopup.ALIGN_BELOW_ANCHOR_ARROW_RIGHT, location, 6000);
 
-				new Handler().postDelayed(new Runnable() {
-					@Override
-					public void run() {
-						RuntimeUtil.runOnUiThread(new Runnable() {
-							@Override
-							public void run() {
-								resolutionTooltipPopup.dismissForever();
-							}
-						});
-					}
-				}, 4000);
+				preferenceService.setIsImageResolutionTooltipShown(true);
 			}
 		}, 2000);
 	}
@@ -1307,6 +1297,9 @@ public class SendMediaActivity extends ThreemaToolbarActivity implements
 		if (editPanel != null) {
 			if (mediaItems.size() > 0) {
 				boolean canEdit = mediaItems.get(position).getType() == TYPE_IMAGE || mediaItems.get(position).getType() == TYPE_IMAGE_CAM;
+				boolean canSettings = mediaItems.get(position).getType() == TYPE_IMAGE;
+
+				settingsItem.setVisibility(canSettings ? View.VISIBLE : View.GONE);
 				editPanel.setVisibility(canEdit ? View.VISIBLE : View.GONE);
 			} else {
 				editPanel.setVisibility(View.GONE);

+ 17 - 7
app/src/main/java/ch/threema/app/adapters/ComposeMessageAdapter.java

@@ -878,19 +878,29 @@ public class ComposeMessageAdapter extends ArrayAdapter<AbstractMessageModel> {
 	}
 
 	/**
-	 * Get adapter position of next available (i.e. downloaded) message of specified message type and with same incoming/outgoing status
+	 * Get adapter position of next available (i.e. downloaded) voice message with same incoming/outgoing status
 	 * @param messageModel of original message
-	 * @param messageType to limit search to or null if any message type will do
 	 * @return AbstractMessageModel of next message in adapter that matches the specified criteria or AbsListView.INVALID_POSITION if none is found
 	 */
-	public int getNextItem(AbstractMessageModel messageModel, MessageType messageType) {
+	public int getNextVoiceMessage(AbstractMessageModel messageModel) {
 		int index = values.indexOf(messageModel);
 		if (index < values.size() - 1) {
 			AbstractMessageModel nextMessage = values.get(index + 1);
-			if (messageType == null || nextMessage.getType() == messageType) {
-				if (messageModel.isOutbox() == nextMessage.isOutbox()) {
-					if (messageModel.isAvailable()) {
-						return index + 1;
+			if (nextMessage != null) {
+				boolean isVoiceMessage = nextMessage.getType() == MessageType.VOICEMESSAGE;
+				if (!isVoiceMessage) {
+					// new school voice messages
+					isVoiceMessage = nextMessage.getType() == MessageType.FILE &&
+						MimeUtil.isAudioFile(nextMessage.getFileData().getMimeType()) &&
+						nextMessage.getFileData().getRenderingType() == FileData.RENDERING_MEDIA &&
+						nextMessage.getFileData().isDownloaded();
+				}
+
+				if (isVoiceMessage) {
+					if (messageModel.isOutbox() == nextMessage.isOutbox()) {
+						if (messageModel.isAvailable()) {
+							return index + 1;
+						}
 					}
 				}
 			}

+ 1 - 17
app/src/main/java/ch/threema/app/adapters/SendMediaGridAdapter.java

@@ -62,7 +62,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 	private final LayoutInflater layoutInflater;
 	private final int itemWidth;
 	private final ClickListener clickListener;
-	private boolean holdsResolutionAdjustableImage = false;
 
 	public static final int VIEW_TYPE_NORMAL = 0;
 	public static final int VIEW_TYPE_ADD = 1;
@@ -83,7 +82,7 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 	}
 
 	public static class SendMediaHolder extends AbstractListItemHolder {
-		public ImageView imageView, deleteView, brokenView, settingsView;
+		public ImageView imageView, deleteView, brokenView;
 		public LinearLayout qualifierView;
 		public int itemType;
 	}
@@ -117,7 +116,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 		holder.qualifierView = itemView.findViewById(R.id.qualifier_view);
 		holder.deleteView = itemView.findViewById(R.id.delete_view);
 		holder.brokenView = itemView.findViewById(R.id.broken_view);
-		holder.settingsView = itemView.findViewById(R.id.settings_view);
 		holder.position = position;
 		holder.itemType = itemType;
 
@@ -127,7 +125,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 			final MediaItem item = items.get(position);
 
 			holder.deleteView.setOnClickListener(v -> clickListener.onDeleteKeyClicked(item));
-			holder.settingsView.setOnClickListener(v -> clickListener.onSettingsKeyClicked(v, item));
 			holder.brokenView.setVisibility(View.GONE);
 
 			Glide.with(context).load(item.getUri())
@@ -143,7 +140,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 					public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
 						if (item.getType() == MediaItem.TYPE_VIDEO_CAM || item.getType() == MediaItem.TYPE_VIDEO) {
 							holder.qualifierView.setVisibility(View.VISIBLE);
-							holder.settingsView.setVisibility(View.GONE);
 
 							AppCompatImageView imageView = holder.qualifierView.findViewById(R.id.video_icon);
 							imageView.setImageResource(R.drawable.ic_videocam_black_24dp);
@@ -157,7 +153,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 							}
 						} else if (item.getType() == MediaItem.TYPE_GIF) {
 							holder.qualifierView.setVisibility(View.VISIBLE);
-							holder.settingsView.setVisibility(View.GONE);
 
 							AppCompatImageView imageView = holder.qualifierView.findViewById(R.id.video_icon);
 							imageView.setImageResource(R.drawable.ic_gif_24dp);
@@ -165,7 +160,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 							holder.qualifierView.findViewById(R.id.video_duration_text).setVisibility(View.GONE);
 						} else {
 							holder.qualifierView.setVisibility(View.GONE);
-							holder.settingsView.setVisibility(item.getType() == MediaItem.TYPE_IMAGE ? View.VISIBLE: View.GONE);
 						}
 						return false;
 					}
@@ -177,15 +171,6 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 		return itemView;
 	}
 
-	public boolean holdsAdjustableImage() {
-		for (MediaItem item : items) {
-			if (item.getType() == MediaItem.TYPE_IMAGE) {
-				return true;
-			}
-		}
-		return false;
-	}
-
 	private void rotateAndFlipImageView(ImageView imageView, MediaItem item) {
 		imageView.setRotation(item.getRotation());
 
@@ -211,6 +196,5 @@ public class SendMediaGridAdapter extends BaseDynamicGridAdapter {
 
 	public interface ClickListener {
 		void onDeleteKeyClicked(MediaItem item);
-		void onSettingsKeyClicked(View view, MediaItem item);
 	}
 }

+ 1 - 1
app/src/main/java/ch/threema/app/fragments/ComposeMessageFragment.java

@@ -722,7 +722,7 @@ public class ComposeMessageFragment extends Fragment implements
 			// Play next audio message, if any
 			RuntimeUtil.runOnUiThread(() -> {
 				if (composeMessageAdapter != null) {
-					int index = composeMessageAdapter.getNextItem(messageModel, MessageType.VOICEMESSAGE);
+					int index = composeMessageAdapter.getNextVoiceMessage(messageModel);
 					if (index != AbsListView.INVALID_POSITION) {
 						View view = composeMessageAdapter.getView(index, null, null);
 

+ 65 - 19
app/src/main/java/ch/threema/app/fragments/ContactsSectionFragment.java

@@ -58,11 +58,14 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.widget.SearchView;
+import androidx.core.util.Pair;
 import androidx.core.view.MenuItemCompat;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@@ -157,6 +160,14 @@ public class ContactsSectionFragment
 
 	private String filterQuery;
 
+	/**
+	 * Simple POJO to hold the number of contacts that were added in the last 24h / 30d.
+	 */
+	private static class RecentlyAddedCounts {
+		int last24h = 0;
+		int last30d = 0;
+	}
+
 	// Contacts changed receiver
 	private BroadcastReceiver contactsChangedReceiver = new BroadcastReceiver() {
 		@Override
@@ -349,6 +360,41 @@ public class ContactsSectionFragment
 		}
 	};
 
+	/**
+	 * An AsyncTask that fetches contacts and add counts in the background.
+	 *
+	 * NOTE: The ContactService needs to be passed in as a parameter!
+	 */
+	private abstract static class FetchContactsTask extends AsyncTask<ContactService, Void, Pair<List<ContactModel>, RecentlyAddedCounts>> {
+		@Override
+		protected Pair<List<ContactModel>, RecentlyAddedCounts> doInBackground(ContactService... contactServices) {
+			final ContactService contactService = contactServices[0];
+
+			// Fetch contacts
+			final List<ContactModel> allContacts = contactService.getAll();
+
+			// Count new contacts
+			final RecentlyAddedCounts counts = new RecentlyAddedCounts();
+			long now = System.currentTimeMillis();
+			long delta24h = 1000L * 3600 * 24;
+			long delta30d = delta24h * 30;
+			for (ContactModel contact : allContacts) {
+				final Date dateCreated = contact.getDateCreated();
+				if (dateCreated == null) {
+					continue;
+				}
+				if (now - dateCreated.getTime() < delta24h) {
+					counts.last24h += 1;
+				}
+				if (now - dateCreated.getTime() < delta30d) {
+					counts.last30d += 1;
+				}
+			}
+
+			return new Pair<>(allContacts, counts);
+		}
+	}
+
 	@Override
 	public void onResume() {
 		logger.debug("*** onResume");
@@ -508,16 +554,13 @@ public class ContactsSectionFragment
 			return;
 		}
 
-		new AsyncTask<Void, Void, List<ContactModel>>() {
-			@Override
-			protected List<ContactModel> doInBackground(Void... voids) {
-				return contactService.getAll();
-			}
-
+		new FetchContactsTask() {
 			@Override
-			protected void onPostExecute(List<ContactModel> contactModels) {
+			protected void onPostExecute(Pair<List<ContactModel>, RecentlyAddedCounts> result) {
+				final List<ContactModel> contactModels = result.first;
+				final RecentlyAddedCounts counts = result.second;
 				if (contactModels != null) {
-					updateContactsCounter(contactModels.size());
+					updateContactsCounter(contactModels.size(), counts);
 					if (contactModels.size() > 0) {
 						((EmptyView) listView.getEmptyView()).setup(R.string.no_matching_contacts);
 					}
@@ -535,7 +578,7 @@ public class ContactsSectionFragment
 					}
 				}
 			}
-		}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+		}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, this.contactService);
 	}
 
 	@SuppressLint("StaticFieldLeak")
@@ -546,28 +589,31 @@ public class ContactsSectionFragment
 		}
 
 		if (contactListAdapter != null) {
-			new AsyncTask<Void, Void, List<ContactModel>>() {
+			new FetchContactsTask() {
 				@Override
-				protected List<ContactModel> doInBackground(Void... voids) {
-					return contactService.getAll();
-				}
+				protected void onPostExecute(Pair<List<ContactModel>, RecentlyAddedCounts> result) {
+					final List<ContactModel> contactModels = result.first;
+					final RecentlyAddedCounts counts = result.second;
 
-				@Override
-				protected void onPostExecute(List<ContactModel> contactModels) {
 					if (contactModels != null && contactListAdapter != null && isAdded()) {
-						updateContactsCounter(contactModels.size());
+						updateContactsCounter(contactModels.size(), counts);
 						contactListAdapter.updateData(contactModels);
 					}
 				}
-			}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+			}.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, this.contactService);
 		}
 	}
 
-	private void updateContactsCounter(int numContacts) {
+	private void updateContactsCounter(int numContacts, @Nullable RecentlyAddedCounts counts) {
 		if (getActivity() != null && listView != null && isAdded()) {
 			if (contactsCounterChip != null) {
 				if (numContacts > 1) {
-					contactsCounterChip.setText(numContacts + " " + getString(R.string.title_section2));
+					final StringBuilder builder = new StringBuilder();
+					builder.append(numContacts).append(" ").append(getString(R.string.title_section2));
+					if (counts != null) {
+						builder.append(" (+").append(counts.last30d).append(" / ").append(getString(R.string.thirty_days_abbrev)).append(")");
+					}
+					contactsCounterChip.setText(builder.toString());
 					contactsCounterChip.setVisibility(View.VISIBLE);
 				} else {
 					contactsCounterChip.setVisibility(View.GONE);

+ 4 - 2
app/src/main/java/ch/threema/app/fragments/MyIDFragment.java

@@ -45,6 +45,7 @@ import java.util.Date;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
 import androidx.appcompat.widget.AppCompatSpinner;
 import androidx.core.widget.NestedScrollView;
 import androidx.fragment.app.DialogFragment;
@@ -153,7 +154,7 @@ public class MyIDFragment extends MainFragment
 		}
 	};
 
-	private ProfileListener profileListener = new ProfileListener() {
+	private final ProfileListener profileListener = new ProfileListener() {
 		@Override
 		public void onAvatarChanged() {
 			// a profile picture has been set so it's safe to assume user wants others to see his pic
@@ -180,7 +181,7 @@ public class MyIDFragment extends MainFragment
 
 		@Override
 		public void onNicknameChanged(String newNickname) {
-			reloadNickname();
+			RuntimeUtil.runOnUiThread(() -> reloadNickname());
 		}
 	};
 
@@ -673,6 +674,7 @@ public class MyIDFragment extends MainFragment
 		}.execute();
 	}
 
+	@UiThread
 	private void reloadNickname() {
 		this.nicknameTextView.setText(!TestUtil.empty(userService.getPublicNickname()) ? userService.getPublicNickname() : userService.getIdentity());
 	}

+ 0 - 1
app/src/main/java/ch/threema/app/mediaattacher/MediaAttachAdapter.java

@@ -127,7 +127,6 @@ public class MediaAttachAdapter extends RecyclerView.Adapter<MediaAttachAdapter.
 					.addListener(new RequestListener<Drawable>() {
 						@Override
 						public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
-							logger.info("onLoad failed on item");
 							loadErrorIndicator.setVisibility(View.VISIBLE);
 							gifIndicator.setVisibility(View.GONE);
 							videoIndicator.setVisibility(View.GONE);

+ 38 - 14
app/src/main/java/ch/threema/app/mediaattacher/MediaAttachViewModel.java

@@ -32,6 +32,7 @@ import net.sqlcipher.database.SQLiteException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.lang.annotation.Retention;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -43,6 +44,7 @@ import java.util.Objects;
 import java.util.Set;
 
 import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
 import androidx.core.content.ContextCompat;
@@ -59,8 +61,8 @@ import ch.threema.app.collections.Functional;
 import ch.threema.app.collections.IPredicateNonNull;
 import ch.threema.app.mediaattacher.data.FailedMediaItemsDAO;
 import ch.threema.app.mediaattacher.data.ImageLabelListConverter;
+import ch.threema.app.mediaattacher.data.LabeledMediaItemsDAO;
 import ch.threema.app.mediaattacher.data.MediaItemsRoomDatabase;
-import ch.threema.app.mediaattacher.data.PersistentMediaItemsDAO;
 import ch.threema.app.mediaattacher.labeling.ImageLabelingWorker;
 import ch.threema.app.mediaattacher.labeling.ImageLabelsIndexHashMap;
 import ch.threema.app.utils.ConfigUtils;
@@ -70,6 +72,8 @@ import java8.util.concurrent.CompletableFuture;
 import java8.util.stream.Collectors;
 import java8.util.stream.StreamSupport;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
 /**
  * The view model used by the media attacher.
  *
@@ -99,7 +103,16 @@ public class MediaAttachViewModel extends AndroidViewModel {
 
 	private final String KEY_SELECTED_MEDIA = "suggestion_labels";
 	private final String KEY_TOOLBAR_TITLE = "toolbar_title";
-	private final String KEY_LABEL_QUERY = "label_query";
+	private final String KEY_RECENT_QUERY = "recent_query_string";
+	private final String KEY_RECENT_QUERY_TYPE = "recent_query_type";
+
+	@Retention(SOURCE)
+	@IntDef({FILTER_MEDIA_TYPE, FILTER_MEDIA_BUCKET, FILTER_MEDIA_LABEL, FILTER_MEDIA_SELECTED})
+	public @interface FilerType {}
+	public static final int FILTER_MEDIA_TYPE = 0;
+	public static final int FILTER_MEDIA_BUCKET = 1;
+	public static final int FILTER_MEDIA_LABEL = 2;
+	public static final int FILTER_MEDIA_SELECTED = 3;
 
 	public MediaAttachViewModel(@NonNull Application application, @NonNull SavedStateHandle savedState) {
 		super(application);
@@ -118,8 +131,11 @@ public class MediaAttachViewModel extends AndroidViewModel {
 		if (ContextCompat.checkSelfPermission(ThreemaApplication.getAppContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
 			this.fetchAllMediaFromRepository();
 			this.initialLoadDone.thenRunAsync(() -> {
-				// Update current media
-				currentMedia.postValue(this.allMedia.getValue());
+				Integer savedQuery = getLastQueryType();
+				// if no query has been set previously post all media to the ui grid live data directly, else we trigger the filter corresponding filter in MediaSelectionBaseActivity
+				if (savedQuery == null) {
+					currentMedia.postValue(this.allMedia.getValue());
+				}
 
 				SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application);
 
@@ -166,7 +182,7 @@ public class MediaAttachViewModel extends AndroidViewModel {
 	private void checkLabelingComplete() {
 		new Thread(() -> {
 			// Open database
-			final PersistentMediaItemsDAO mediaItemsDAO;
+			final LabeledMediaItemsDAO mediaItemsDAO;
 			final FailedMediaItemsDAO failedMediaItemsDAO;
 			try {
 				mediaItemsDAO = MediaItemsRoomDatabase.getDatabase(application).mediaItemsDAO();
@@ -190,7 +206,7 @@ public class MediaAttachViewModel extends AndroidViewModel {
 
 			final float labeledRatio = (float) labeledMediaCount / (float) totalMediaSize;
 			if (labeledRatio > 0.8) {
-				// More than 90% labeled. Good enough, but kick off the labeller anyways if we're not at 100%.
+				// More than 80% labeled. Good enough, but kick off the labeller anyways if we're not at 100%.
 				if (labeledMediaCount < totalMediaSize) {
 					this.startImageLabeler();
 				}
@@ -218,7 +234,7 @@ public class MediaAttachViewModel extends AndroidViewModel {
 				logger.info("Found {} distinct labels in database", translatedLabels.size());
 				suggestionLabels.postValue(sortedLabels);
 			} else {
-				logger.info("Less than 90% labeled, considering labels incomplete");
+				logger.info("Less than 80% labeled, considering labels incomplete");
 				this.startImageLabeler();
 				suggestionLabels.postValue(Collections.emptyList());
 			}
@@ -231,6 +247,7 @@ public class MediaAttachViewModel extends AndroidViewModel {
 	@UiThread
 	public void setAllMedia() {
 		currentMedia.setValue(this.allMedia.getValue());
+		clearLastQuery();
 	}
 
 	/**
@@ -253,9 +270,7 @@ public class MediaAttachViewModel extends AndroidViewModel {
 				filteredMedia.add(mediaItem);
 			}
 		}
-		if (currentMedia != null) {
-			currentMedia.setValue(filteredMedia);
-		}
+		currentMedia.setValue(filteredMedia);
 	}
 
 	/**
@@ -358,11 +373,20 @@ public class MediaAttachViewModel extends AndroidViewModel {
 		savedState.set(KEY_TOOLBAR_TITLE, toolBarTitle);
 	}
 
-	public String getLabelQuery() {
-		return savedState.get(KEY_LABEL_QUERY);
+	public String getLastQuery() {
+		return savedState.get(KEY_RECENT_QUERY);
+	}
+	public Integer getLastQueryType() {
+		return savedState.get(KEY_RECENT_QUERY_TYPE);
+	}
+
+	public void setlastQuery(@FilerType int type, String labelQuery) {
+		savedState.set(KEY_RECENT_QUERY, labelQuery);
+		savedState.set(KEY_RECENT_QUERY_TYPE, type);
 	}
 
-	public void setLabelQuery(String labelQuery) {
-		savedState.set(KEY_LABEL_QUERY, labelQuery);
+	public void clearLastQuery() {
+		savedState.set(KEY_RECENT_QUERY, null);
+		savedState.set(KEY_RECENT_QUERY_TYPE, null);
 	}
 }

+ 0 - 2
app/src/main/java/ch/threema/app/mediaattacher/MediaRepository.java

@@ -59,8 +59,6 @@ public class MediaRepository {
 	 */
 	@WorkerThread
 	public List<MediaAttachItem> getMediaFromMediaStore() {
-		logger.info("getMedia from repository");
-
 		final String[] imageProjection = this.getImageProjection();
 		final String[] videoProjection = this.getVideoProjection();
 

+ 43 - 33
app/src/main/java/ch/threema/app/mediaattacher/MediaSelectionBaseActivity.java

@@ -65,7 +65,6 @@ import org.slf4j.LoggerFactory;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.UiThread;
@@ -101,6 +100,10 @@ import ch.threema.app.utils.LocaleUtil;
 import ch.threema.localcrypto.MasterKey;
 
 import static android.view.inputmethod.EditorInfo.IME_FLAG_NO_EXTRACT_UI;
+import static ch.threema.app.mediaattacher.MediaAttachViewModel.FILTER_MEDIA_BUCKET;
+import static ch.threema.app.mediaattacher.MediaAttachViewModel.FILTER_MEDIA_LABEL;
+import static ch.threema.app.mediaattacher.MediaAttachViewModel.FILTER_MEDIA_SELECTED;
+import static ch.threema.app.mediaattacher.MediaAttachViewModel.FILTER_MEDIA_TYPE;
 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_DRAGGING;
 import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;
@@ -162,13 +165,13 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 
 	@Override
 	public void onDestroy() {
-		logger.debug("onDestroy");
 		super.onDestroy();
 	}
 
 	@UiThread
 	protected void handleSavedInstanceState(Bundle savedInstanceState){
 		if (savedInstanceState != null) {
+			onItemChecked(mediaAttachViewModel.getSelectedMediaItemsHashMap().size());
 			int bottomSheetStyleState = savedInstanceState.getInt(KEY_BOTTOM_SHEET_STATE);
 			if (bottomSheetStyleState != 0) {
 				final BottomSheetBehavior<ConstraintLayout> bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
@@ -289,6 +292,14 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 				if (labels != null && !labels.isEmpty()) {
 					this.labelSuggestions = labels;
 					this.onLabelingComplete();
+
+					// reset last recent label filter if activity was destroyed by the system due to memory pressure etc.
+					String savedQuery = mediaAttachViewModel.getLastQuery();
+					Integer savedQueryType = mediaAttachViewModel.getLastQueryType();
+					if (savedQueryType != null && savedQueryType == FILTER_MEDIA_LABEL) {
+						mediaAttachViewModel.setMediaByLabel(savedQuery);
+						searchView.clearFocus();
+					}
 				}
 			});
 		}
@@ -397,6 +408,26 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 				// Enable menu
 				menuTitleFrame.setOnClickListener(view -> bucketFilterMenu.show());
 			}
+
+			// reset last recent filter if activity was destroyed by the system due to memory pressure etc and we do not have to wait for suggestion labels.
+			String savedQuery = mediaAttachViewModel.getLastQuery();
+			Integer savedQueryType = mediaAttachViewModel.getLastQueryType();
+			if (savedQueryType != null) {
+				switch (savedQueryType) {
+					case FILTER_MEDIA_TYPE:
+						filterMediaByMimeType(savedQuery);
+						break;
+					case FILTER_MEDIA_BUCKET:
+						filterMediaByBucket(savedQuery);
+						break;
+					case FILTER_MEDIA_SELECTED:
+						filterMediaBySelectedItems();
+						break;
+					default:
+						menuTitle.setText(R.string.filter_by_album);
+						break;
+				}
+			}
 		});
 	}
 
@@ -409,26 +440,11 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		if (shouldShowMediaGrid()) {
 			// Observe the LiveData, passing in this activity as the LifecycleOwner and Observer.
 			mediaAttachViewModel.getCurrentMedia().observe(this, newMediaItems -> {
-				logger.info("getCurrentMedia {} new items", newMediaItems.size());
 				mediaAttachAdapter.setMediaItems(newMediaItems);
 
 				// Data loaded, we can now properly calculate the peek height
 				updatePeekHeight();
 			});
-
-			// if no media was set previously -> start labeling, else set control panel accordingly
-			// TODO (db): Use a better logic than an empty media list.
-			if (!Objects.requireNonNull(mediaAttachViewModel.getCurrentMedia().getValue()).isEmpty()) {
-				// activity is being rebuilt -> set previously selected items
-				onItemChecked(mediaAttachViewModel.getSelectedMediaItemsHashMap().size());
-				// labeling has been done previously -> set to previous query and set suggestions adapter
-				if (this.labelSuggestions != null && !this.labelSuggestions.isEmpty()) {
-					if (mediaAttachViewModel.getLabelQuery() != null) {
-						menuTitle.setText(R.string.filter_by_album);
-						searchView.setQuery(mediaAttachViewModel.getLabelQuery(), false);
-					}
-				}
-			}
 		}
 	}
 
@@ -468,7 +484,6 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 			@Override
 			public boolean onQueryTextSubmit(String query) {
 				mediaAttachViewModel.setMediaByLabel(query);
-				searchView.setQuery(query, false);
 				searchView.clearFocus();
 				return false;
 			}
@@ -484,13 +499,10 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 
 		View searchViewCloseButton = searchView.findViewById(androidx.appcompat.R.id.search_close_btn);
 		if (searchViewCloseButton != null) {
-			searchViewCloseButton.setOnClickListener(new View.OnClickListener() {
-				@Override
-				public void onClick(View v) {
-					mediaAttachViewModel.setAllMedia();
-					searchView.setQuery("", false);
-					searchView.requestFocus();
-				}
+			searchViewCloseButton.setOnClickListener(v -> {
+				mediaAttachViewModel.setAllMedia();
+				searchView.setQuery("", false);
+				searchView.requestFocus();
 			});
 		}
 
@@ -560,7 +572,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 					textView.setText(label);
 					view.setOnClickListener(view1 -> {
 						searchView.setQuery(label, true);
-						mediaAttachViewModel.setLabelQuery(label);
+						mediaAttachViewModel.setlastQuery(FILTER_MEDIA_LABEL, label);
 					});
 				}
 			};
@@ -572,16 +584,17 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 	protected void resetLabelSearch() {
 		searchView.setQuery("", false);
 		searchView.setIconified(true);
-		mediaAttachViewModel.setLabelQuery(null);
 	}
 
 	public void setAllResultsGrid() {
 		mediaAttachViewModel.setAllMedia();
+		mediaAttachViewModel.clearLastQuery();
 		resetLabelSearch();
 	}
 
 	public void filterMediaByBucket(String mediaBucket) {
 		mediaAttachViewModel.setMediaByBucket(mediaBucket);
+		mediaAttachViewModel.setlastQuery(FILTER_MEDIA_BUCKET, mediaBucket);
 		resetLabelSearch();
 	}
 
@@ -598,6 +611,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		if (mimeTypeIndex != 0) {
 			mediaAttachViewModel.setMediaByType(mimeTypeIndex);
 		}
+		mediaAttachViewModel.setlastQuery(FILTER_MEDIA_TYPE, mimeTypeTitle);
 		resetLabelSearch();
 	}
 
@@ -605,6 +619,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 		mediaAttachViewModel.setSelectedMedia();
 		searchItem.setEnabled(false);
 		searchItem.collapseActionView();
+		mediaAttachViewModel.setlastQuery(FILTER_MEDIA_SELECTED, null);
 		resetLabelSearch();
 	}
 
@@ -693,12 +708,7 @@ abstract public class MediaSelectionBaseActivity extends ThreemaActivity impleme
 				}
 
 				// Maybe show "new feature" tooltip
-				toolbar.postDelayed(new Runnable() {
-					@Override
-					public void run() {
-						maybeShowFirstTimeToolTip();
-					}
-				}, 1500);
+				toolbar.postDelayed(() -> maybeShowFirstTimeToolTip(), 1500);
 
 				isDragging = false;
 

+ 5 - 13
app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemEntity.java

@@ -21,28 +21,20 @@
 
 package ch.threema.app.mediaattacher.data;
 
+import androidx.room.ColumnInfo;
 import androidx.room.Entity;
-import androidx.room.PrimaryKey;
 
 @Entity(tableName = "failed_media_items")
-public class FailedMediaItemEntity {
-	@PrimaryKey
-	public int id;
+public class FailedMediaItemEntity extends MediaItemEntity {
+
+	@ColumnInfo(name = "timestamp")
 	private long timestamp;
 
 	public FailedMediaItemEntity(int id, long timestamp) {
-		this.id = id;
+		super(id);
 		this.timestamp = timestamp;
 	}
 
-	public int getId() {
-		return id;
-	}
-
-	public void setId(int id) {
-		this.id = id;
-	}
-
 	public long getTimestamp() {
 		return timestamp;
 	}

+ 5 - 0
app/src/main/java/ch/threema/app/mediaattacher/data/FailedMediaItemsDAO.java

@@ -21,6 +21,8 @@
 
 package ch.threema.app.mediaattacher.data;
 
+import java.util.List;
+
 import androidx.room.Dao;
 import androidx.room.Delete;
 import androidx.room.Insert;
@@ -41,4 +43,7 @@ public interface FailedMediaItemsDAO {
 
 	@Query("SELECT COUNT(*) FROM failed_media_items")
 	int getRowCount();
+
+	@Query("SELECT * FROM failed_media_items ORDER BY id ASC")
+	List<FailedMediaItemEntity> getAllItemsByAscIdOrder();
 }

+ 3 - 15
app/src/main/java/ch/threema/app/mediaattacher/data/PersistentMediaItem.java → app/src/main/java/ch/threema/app/mediaattacher/data/LabeledMediaItemEntity.java

@@ -25,30 +25,18 @@ import java.util.ArrayList;
 
 import androidx.room.ColumnInfo;
 import androidx.room.Entity;
-import androidx.room.PrimaryKey;
 
 @Entity(tableName = "media_items_table")
-public class PersistentMediaItem  {
-	@PrimaryKey
-	@ColumnInfo(name = "id")
-	private int id;
+public class LabeledMediaItemEntity extends MediaItemEntity {
 
 	@ColumnInfo(name = "labels")
 	private ArrayList<String> labels;
 
-	public PersistentMediaItem(int id, ArrayList<String> labels) {
-		this.id = id;
+	public LabeledMediaItemEntity(int id, ArrayList<String> labels) {
+		super(id);
 		this.labels = labels;
 	}
 
-	public int getId() {
-		return id;
-	}
-
-	public void setId(int id) {
-		this.id = id;
-	}
-
 	public ArrayList<String> getLabels() {
 		return labels;
 	}

+ 4 - 4
app/src/main/java/ch/threema/app/mediaattacher/data/PersistentMediaItemsDAO.java → app/src/main/java/ch/threema/app/mediaattacher/data/LabeledMediaItemsDAO.java

@@ -30,10 +30,10 @@ import androidx.room.OnConflictStrategy;
 import androidx.room.Query;
 
 @Dao
-public interface PersistentMediaItemsDAO {
+public interface LabeledMediaItemsDAO {
 
 	@Insert(onConflict = OnConflictStrategy.IGNORE)
-	void insert(PersistentMediaItem mediaItem);
+	void insert(LabeledMediaItemEntity mediaItem);
 
 	@Query("INSERT into media_items_table (id, labels) VALUES (:id, :labelList)")
 	void insert(int id, ArrayList<String> labelList);
@@ -45,10 +45,10 @@ public interface PersistentMediaItemsDAO {
 	void deleteMediaItemById(int id);
 
 	@Query("SELECT * FROM media_items_table")
-	List<PersistentMediaItem> getAll();
+	List<LabeledMediaItemEntity> getAll();
 
 	@Query("SELECT * FROM media_items_table ORDER BY id ASC")
-	List<PersistentMediaItem> getAllItemsByAscIdOrder();
+	List<LabeledMediaItemEntity> getAllItemsByAscIdOrder();
 
 	@Query("SELECT labels from media_items_table WHERE id = :id")
 	List<String> getMediaItemLabels(int id);

+ 45 - 0
app/src/main/java/ch/threema/app/mediaattacher/data/MediaItemEntity.java

@@ -0,0 +1,45 @@
+/*  _____ _
+ * |_   _| |_  _ _ ___ ___ _ __  __ _
+ *   | | | ' \| '_/ -_) -_) '  \/ _` |_
+ *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
+ *
+ * Threema for Android
+ * Copyright (c) 2021 Threema GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package ch.threema.app.mediaattacher.data;
+
+import androidx.room.ColumnInfo;
+import androidx.room.PrimaryKey;
+
+public class
+MediaItemEntity {
+
+	@PrimaryKey
+	@ColumnInfo(name = "id")
+	private int id;
+
+	public MediaItemEntity(int id) {
+		this.id = id;
+	}
+
+	public int getId() {
+		return id;
+	}
+
+	public void setId(int id) {
+		this.id = id;
+	}
+}

+ 2 - 2
app/src/main/java/ch/threema/app/mediaattacher/data/MediaItemsRoomDatabase.java

@@ -40,7 +40,7 @@ import ch.threema.app.ThreemaApplication;
 import ch.threema.localcrypto.MasterKeyLockedException;
 
 @Database(
-	entities = {PersistentMediaItem.class, FailedMediaItemEntity.class},
+	entities = {LabeledMediaItemEntity.class, FailedMediaItemEntity.class},
 	version = 2,
 	exportSchema = false
 )
@@ -50,7 +50,7 @@ public abstract class MediaItemsRoomDatabase extends RoomDatabase {
 
 	public static final String DATABASE_NAME = "media_items.db";
 
-	public abstract PersistentMediaItemsDAO mediaItemsDAO();
+	public abstract LabeledMediaItemsDAO mediaItemsDAO();
 	public abstract FailedMediaItemsDAO failedMediaItemsDAO();
 
 	private static volatile MediaItemsRoomDatabase db;

+ 44 - 46
app/src/main/java/ch/threema/app/mediaattacher/labeling/ImageLabelingWorker.java

@@ -41,7 +41,6 @@ import net.sqlcipher.database.SQLiteException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -64,13 +63,13 @@ import ch.threema.app.mediaattacher.MediaAttachItem;
 import ch.threema.app.mediaattacher.MediaRepository;
 import ch.threema.app.mediaattacher.data.FailedMediaItemEntity;
 import ch.threema.app.mediaattacher.data.FailedMediaItemsDAO;
+import ch.threema.app.mediaattacher.data.LabeledMediaItemEntity;
+import ch.threema.app.mediaattacher.data.LabeledMediaItemsDAO;
+import ch.threema.app.mediaattacher.data.MediaItemEntity;
 import ch.threema.app.mediaattacher.data.MediaItemsRoomDatabase;
-import ch.threema.app.mediaattacher.data.PersistentMediaItem;
-import ch.threema.app.mediaattacher.data.PersistentMediaItemsDAO;
 import ch.threema.app.services.NotificationService;
 import ch.threema.app.ui.MediaItem;
 import ch.threema.app.utils.RandomUtil;
-import ch.threema.client.Utils;
 import ch.threema.localcrypto.MasterKeyLockedException;
 import ch.threema.logging.ThreemaLogger;
 
@@ -91,7 +90,7 @@ public class ImageLabelingWorker extends Worker {
 	private final MediaRepository repository;
 
 	// Database
-	private final PersistentMediaItemsDAO mediaItemsDAO;
+	private final LabeledMediaItemsDAO mediaItemsDAO;
 	private final FailedMediaItemsDAO failedMediaDAO;
 
 	// Image labeling
@@ -154,7 +153,6 @@ public class ImageLabelingWorker extends Worker {
 		final int numCores = Runtime.getRuntime().availableProcessors();
 		final int minThreads = Math.min(numCores, 2);
 		final int maxThreads = Math.min(numCores, 4);
-		this.logger.debug("Starting thread pool");
 		this.executor = new ThreadPoolExecutor(
 			minThreads,
 			maxThreads,
@@ -260,7 +258,7 @@ public class ImageLabelingWorker extends Worker {
 				// We're only interested in image media
 				if (mediaCanBeLabeled(mediaItem)) {
 					if (failedMediaDAO.get(mediaItem.getId()) != null) {
-						logger.info("Item " + mediaItem.getId() + " failed to load previously. Skipping");
+						logger.info("Item {} in set label queue failed to load previously. Skipping", progress);
 						skippedCounter++;
 						continue;
 					}
@@ -279,24 +277,20 @@ public class ImageLabelingWorker extends Worker {
 					InputImage image;
 					try {
 						final Uri uri = mediaItem.getUri();
-						final String hashedFilename = Utils.byteArrayToSha256HexString(mediaItem.getDisplayName().getBytes(StandardCharsets.UTF_8));
 						imageFuture = executor.submit(() -> InputImage.fromFilePath(appContext, uri));
 
 						try {
-							// TODO(ANDR-1318): Make this logging less verbose!
-							logger.info("Loading image {}/{} ({})", this.progress, this.mediaCount, hashedFilename.substring(0, 8));
 							image = imageFuture.get(30, TimeUnit.SECONDS);    // give it a timeout of 30s, otherwise skip and remember bad item
-							logger.info("Loaded image {}/{} ({})", this.progress, this.mediaCount, hashedFilename.substring(0, 8));
 						} catch (TimeoutException e) {
 							imageFuture.cancel(true);
-							logger.info("Item " + hashedFilename.substring(0, 8) + " cannot be labeled due to timeout");
+							logger.info("Item {} in set label queue cannot be loaded from filepath in reasonable time, timeout triggered", progress);
 							failedMediaDAO.insert(new FailedMediaItemEntity(mediaItem.getId(), System.currentTimeMillis()));
 							skippedCounter++;
 							continue;
 						}
 
 						if (image.getHeight() < 32 || image.getWidth() < 32 ) {
-							logger.info("Item " + hashedFilename.substring(0, 8) + " cannot be labeled due to tiny size.");
+							logger.info("Item {} in set label queue loaded as InputImage due to tiny size. width or height < 32", progress);
 							failedMediaDAO.insert(new FailedMediaItemEntity(mediaItem.getId(), System.currentTimeMillis()));
 							skippedCounter++;
 							continue;
@@ -308,6 +302,7 @@ public class ImageLabelingWorker extends Worker {
 						if (e.getCause() != null) {
 							logger.warn("  Caused by: {}", e.getCause().getMessage());
 						}
+						skippedCounter++;
 						continue;
 					}
 
@@ -321,8 +316,7 @@ public class ImageLabelingWorker extends Worker {
 							for (ImageLabel label : labels) {
 								labelsListIndexes.add(String.valueOf(label.getIndex()));
 							}
-							logger.debug("Found {} labels for {}", labelsListIndexes.size(), mediaItem.getDisplayName());
-							mediaItemsDAO.insert(new PersistentMediaItem(mediaItem.getId(), labelsListIndexes));
+							mediaItemsDAO.insert(new LabeledMediaItemEntity(mediaItem.getId(), labelsListIndexes));
 						});
 
 					// We're waiting for the task to complete, because processing multiple images in parallel
@@ -340,7 +334,7 @@ public class ImageLabelingWorker extends Worker {
 						return Result.failure();
 					}
 
-					if (unlabeledCounter % 20 == 0) {
+					if (unlabeledCounter % 50 == 0) {
 						logger.info("Processed {} files…", unlabeledCounter);
 					}
 				}
@@ -359,30 +353,11 @@ public class ImageLabelingWorker extends Worker {
 				logger.info("Labeling work done after {}s, starting cleanup", secondsElapsedLabeling);
 			}
 
-			// Delete labels from database that belong to nonexistent media items
-			List<PersistentMediaItem> currentlyStoredLabels = mediaItemsDAO.getAllItemsByAscIdOrder();
-			if (currentlyStoredLabels != null) {
-				Collections.sort(allMediaCache, (o1, o2) -> Double.compare(o1.getId(), o2.getId()));
-				int indexStoredLabelsList = 0;
-				int indexStoredMediaItemsList = 0;
-				while (indexStoredLabelsList < currentlyStoredLabels.size() && indexStoredMediaItemsList < allMediaCache.size()) {
-					int storedItemIDCurrent = currentlyStoredLabels.get(indexStoredLabelsList).getId();
-					int retrievedItemIDCurrent = allMediaCache.get(indexStoredMediaItemsList).getId();
-					if (storedItemIDCurrent == retrievedItemIDCurrent) {
-						// Found match!
-						indexStoredLabelsList++;
-						indexStoredMediaItemsList++;
-					} else if (storedItemIDCurrent < retrievedItemIDCurrent) {
-						// No match, discard entry in labels database
-						logger.info("Deleting media labels for id {}", currentlyStoredLabels.get(indexStoredLabelsList).getId());
-						mediaItemsDAO.deleteMediaItemById(currentlyStoredLabels.get(indexStoredLabelsList).getId());
-						indexStoredLabelsList++;
-					} else {
-						// No match, discard first entry in long list
-						indexStoredMediaItemsList++;
-					}
-				}
-			}
+			// Delete labels from database that belong to meanwhile deleted media items
+			List<LabeledMediaItemEntity> currentlyStoredLabelItems = mediaItemsDAO.getAllItemsByAscIdOrder();
+			List<FailedMediaItemEntity> currentlyStoredBrokenItems = failedMediaDAO.getAllItemsByAscIdOrder();
+			cleanDBEntries(currentlyStoredLabelItems, allMediaCache);
+			cleanDBEntries(currentlyStoredBrokenItems, allMediaCache);
 
 			final long secondsElapsedTotal = (SystemClock.elapsedRealtime() - startTime) / 1000;
 			logger.info("Processing done, total duration was {}s", secondsElapsedTotal);
@@ -393,16 +368,39 @@ public class ImageLabelingWorker extends Worker {
 		}
 	}
 
-	private void onFinish() {
-		logger.debug("onFinish() called");
+	private void cleanDBEntries(List<? extends MediaItemEntity> currentlyStoredLabels, List<MediaAttachItem> allCurrentMedia) {
+		if (!currentlyStoredLabels.isEmpty() && allCurrentMedia != null) {
+			logger.info("clean db entries {}", currentlyStoredLabels.get(0).getClass());
+			Collections.sort(allCurrentMedia, (o1, o2) -> Double.compare(o1.getId(), o2.getId()));
+			int indexStoredLabelsList = 0;
+			int indexStoredMediaItemsList = 0;
+			while (indexStoredLabelsList < currentlyStoredLabels.size() && indexStoredMediaItemsList < allCurrentMedia.size()) {
+				int storedItemIDCurrent = currentlyStoredLabels.get(indexStoredLabelsList).getId();
+				int retrievedItemIDCurrent = allCurrentMedia.get(indexStoredMediaItemsList).getId();
+				if (storedItemIDCurrent == retrievedItemIDCurrent) {
+					// Found match!
+					indexStoredLabelsList++;
+					indexStoredMediaItemsList++;
+				} else if (storedItemIDCurrent < retrievedItemIDCurrent) {
+					// No match, discard entry in labels database
+					logger.info("Deleting media id {} entry in db ", currentlyStoredLabels.get(indexStoredLabelsList).getId());
+					mediaItemsDAO.deleteMediaItemById(currentlyStoredLabels.get(indexStoredLabelsList).getId());
+					indexStoredLabelsList++;
+				} else {
+					// No match, discard first entry in long list
+					indexStoredMediaItemsList++;
+				}
+			}
+		}
+	}
 
+	private void onFinish() {
 		// Shut down executor thread pool
-/*		if (!this.executor.isShutdown()) {
-			this.logger.debug("Shut down thread pool");
+		if (!this.executor.isShutdown()) {
+			this.logger.info("Shut down thread pool");
 			this.executor.shutdown();
 		}
-		// TODO this causes a DuplicateTaskCompletionException
-*/
+
 		if (this.cancelled) {
 			logger.info("Cancelled after processing {}/{} media files", this.progress, this.mediaCount);
 		} else {

+ 21 - 9
app/src/main/java/ch/threema/app/routines/SynchronizeContactsRoutine.java

@@ -210,11 +210,16 @@ public class SynchronizeContactsRoutine implements Runnable {
 				final ContactMatchKeyEmail matchKeyEmail = (ContactMatchKeyEmail)id.getValue().refObjectEmail;
 				final ContactMatchKeyPhone matchKeyPhone = (ContactMatchKeyPhone)id.getValue().refObjectMobileNo;
 
-				String contactId;
-				if (matchKeyEmail != null)
+				int contactId;
+				String lookupKey;
+				if (matchKeyEmail != null) {
 					contactId = matchKeyEmail.contactId;
-				else
+					lookupKey = matchKeyEmail.lookupKey;
+				}
+				else {
 					contactId = matchKeyPhone.contactId;
+					lookupKey = matchKeyPhone.lookupKey;
+				}
 
 				//try to get the contact
 				ContactModel contact = this.contactService.getByIdentity(id.getKey());
@@ -236,7 +241,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 				//update contact name
 				contact.setIsSynchronized(true);
 				contact.setIsHidden(false);
-				contact.setAndroidContactId(contactId);
+				contact.setAndroidContactId(contactId > 0 ? lookupKey + "/" + contactId : lookupKey); // It can optionally also have a "/" and last known contact ID appended after that. This "complete" format is an important optimization and is highly recommended.
 				AndroidContactUtil.getInstance().updateNameByAndroidContact(contact);
 				AndroidContactUtil.getInstance().updateAvatarByAndroidContact(contact);
 
@@ -246,7 +251,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 			}
 
 			if (preSynchronizedIdentities.size() > 0) {
-				logger.debug("degrade contact(s), found " + String.valueOf(preSynchronizedIdentities.size()) + " not synchronized contacts");
+				logger.debug("degrade contact(s). found " + String.valueOf(preSynchronizedIdentities.size()) + " not synchronized contacts");
 
 				List<ContactModel> contactModels = this.contactService.getByIdentities(preSynchronizedIdentities);
 				modifiedCount += this.contactService.save(
@@ -333,6 +338,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 					String id = contactCursor.getString(idColumnIndex);
 					if (Integer.parseInt(contactCursor.getString(hasPhoneNumberColumnIndex)) > 0) {
 						String lookupKey = contactCursor.getString(lookupKeyIndex);
+						int contactId = contactCursor.getInt(idColumnIndex);
 
 						try (Cursor phoneCursor = this.contentResolver.query(
 							ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
@@ -347,7 +353,8 @@ public class SynchronizeContactsRoutine implements Runnable {
 									String phoneNumber = phoneCursor.getString(numberColumnIndex);
 									if (!TestUtil.empty(phoneNumber)) {
 										ContactMatchKeyPhone matchKey = new ContactMatchKeyPhone();
-										matchKey.contactId = lookupKey;
+										matchKey.contactId = contactId;
+										matchKey.lookupKey = lookupKey;
 										matchKey.phoneNumber = phoneNumber;
 										phones.put(phoneNumber, matchKey);
 									}
@@ -373,6 +380,7 @@ public class SynchronizeContactsRoutine implements Runnable {
 		try (Cursor emailsCursor = this.contentResolver.query(
 			ContactsContract.CommonDataKinds.Email.CONTENT_URI,
 			new String[]{
+				ContactsContract.CommonDataKinds.Email._ID,
 				ContactsContract.CommonDataKinds.Email.LOOKUP_KEY,
 				ContactsContract.CommonDataKinds.Email.DATA
 			},
@@ -381,17 +389,20 @@ public class SynchronizeContactsRoutine implements Runnable {
 			null)) {
 
 			if (emailsCursor != null) {
+				int idColumnIndex = emailsCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email._ID);
 				final int lookupKeyColumnIndex = emailsCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.LOOKUP_KEY);
 				final int emailIndex = emailsCursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
 
 				while (emailsCursor.moveToNext()) {
-					String contactId = emailsCursor.getString(lookupKeyColumnIndex);
+					int contactId = emailsCursor.getInt(idColumnIndex);
+					String lookupKey = emailsCursor.getString(lookupKeyColumnIndex);
 					String email = emailsCursor.getString(emailIndex);
 
 //				logger.debug("id: " + contactId + " email: " + email + " inDefaultDirectory: " + inDefaultDirectory);
-					if (contactId != null && !TestUtil.empty(email)) {
+					if (lookupKey != null && !TestUtil.empty(email)) {
 						ContactMatchKeyEmail matchKey = new ContactMatchKeyEmail();
 						matchKey.contactId = contactId;
+						matchKey.lookupKey = lookupKey;
 						matchKey.email = email;
 						emails.put(email, matchKey);
 					}
@@ -404,7 +415,8 @@ public class SynchronizeContactsRoutine implements Runnable {
 
 
 	private class ContactMatchKey {
-		String contactId;
+		int contactId;
+		String lookupKey;
 	}
 
 	private class ContactMatchKeyEmail extends ContactMatchKey {

+ 13 - 22
app/src/main/java/ch/threema/app/utils/AndroidContactUtil.java

@@ -536,14 +536,8 @@ public class AndroidContactUtil {
 	}
 
 	public boolean updateNameByAndroidContact(@NonNull ContactModel contactModel) {
-		String androidContactId = contactModel.getAndroidContactId();
-
-		if(TestUtil.empty(androidContactId)) {
-			androidContactId = contactModel.getThreemaAndroidContactId();
-		}
-
 		Uri namedContactUri = ContactUtil.getAndroidContactUri(ThreemaApplication.getAppContext(), contactModel);
-		if(TestUtil.required(contactModel, namedContactUri) && !TestUtil.empty(androidContactId)) {
+		if(TestUtil.required(contactModel, namedContactUri)) {
 			ContactName contactName = this.getContactName(namedContactUri);
 			if(TestUtil.required(contactModel, contactName)) {
 				if(!TestUtil.compare(contactModel.getFirstName(), contactName.firstName)
@@ -554,9 +548,7 @@ public class AndroidContactUtil {
 					return true;
 				}
 			}
-
 		}
-
 		return false;
 	}
 
@@ -577,26 +569,25 @@ public class AndroidContactUtil {
 			if(nameCursor.moveToFirst()) {
 				long contactId = nameCursor.getLong(nameCursor.getColumnIndex(ContactsContract.Contacts._ID));
 				contactName = this.getContactNameFromId(contactId);
-			}
 
-			// fallback
-			if (contactName == null || (contactName.firstName == null && contactName.lastName == null)) {
-				//lastname, firstname
-				String alternativeSortKey = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.Contacts.SORT_KEY_ALTERNATIVE));
+				// fallback
+				if (contactName == null || (contactName.firstName == null && contactName.lastName == null)) {
+					//lastname, firstname
+					String alternativeSortKey = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.Contacts.SORT_KEY_ALTERNATIVE));
 
-				if (!TestUtil.empty(alternativeSortKey)) {
-					String[] lastNameFirstName = alternativeSortKey.split(",");
-					if (lastNameFirstName != null && lastNameFirstName.length == 2) {
-						String lastName = lastNameFirstName[0].trim();
-						String firstName = lastNameFirstName[1].trim();
+					if (!TestUtil.empty(alternativeSortKey)) {
+						String[] lastNameFirstName = alternativeSortKey.split(",");
+						if (lastNameFirstName != null && lastNameFirstName.length == 2) {
+							String lastName = lastNameFirstName[0].trim();
+							String firstName = lastNameFirstName[1].trim();
 
-						if (!TestUtil.compare(lastName, "") && !TestUtil.compare(firstName, "")) {
-							contactName = new ContactName(firstName, lastName);
+							if (!TestUtil.compare(lastName, "") && !TestUtil.compare(firstName, "")) {
+								contactName = new ContactName(firstName, lastName);
+							}
 						}
 					}
 				}
 			}
-
 			nameCursor.close();
 		}
 

+ 0 - 2
app/src/main/java/ch/threema/app/utils/ContactUtil.java

@@ -61,7 +61,6 @@ public class ContactUtil {
 	 */
 	public static Uri getAndroidContactUri(Context context, ContactModel contactModel) {
 		if (contactModel != null) {
-			// CIDs can be numeric only
 			String contactLookupKey = contactModel.getAndroidContactId();
 
 			if (TestUtil.empty(contactLookupKey)) {
@@ -203,7 +202,6 @@ public class ContactUtil {
 				//change to active is always allowed
 				case ACTIVE:
 					return true;
-//					return oldState == ContactModel.State.TEMPORARY;
 				case INACTIVE:
 					return oldState == ContactModel.State.TEMPORARY
 							|| oldState == ContactModel.State.ACTIVE;

+ 2 - 2
app/src/main/java/ch/threema/app/utils/RingtoneUtil.java

@@ -42,8 +42,8 @@ public class RingtoneUtil {
 			if (ringtone != null) {
 				try {
 					return ringtone.getTitle(context);
-				} catch (SecurityException e) {
-					return context.getString(R.string.error);
+				} catch (SecurityException | IllegalArgumentException e) {
+					return context.getString(R.string.no_filename);
 				}
 			}
 		}

+ 2 - 1
app/src/main/java/ch/threema/storage/models/ContactModel.java

@@ -25,6 +25,7 @@ import android.text.format.DateUtils;
 
 import java.util.Date;
 
+import androidx.annotation.Nullable;
 import ch.threema.base.Contact;
 
 public class ContactModel extends Contact implements ReceiverModel {
@@ -183,7 +184,7 @@ public class ContactModel extends Contact implements ReceiverModel {
 		return profilePicSent;
 	}
 
-	public Date getDateCreated() {
+	public @Nullable Date getDateCreated() {
 		return dateCreated;
 	}
 

+ 15 - 1
app/src/main/res/layout/activity_send_media.xml

@@ -127,9 +127,23 @@
 				android:contentDescription="@string/edit"
 				android:background="@drawable/selector_compose_button"
 				app:srcCompat="@drawable/ic_magic_wand_white_24dp"
+				app:layout_constraintTop_toTopOf="parent"
 				app:layout_constraintLeft_toRightOf="@id/crop"
-				app:layout_constraintRight_toRightOf="parent"
+				app:layout_constraintRight_toLeftOf="@+id/settings"
+				/>
+
+			<ImageButton
+				android:id="@+id/settings"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:tint="@android:color/white"
+				android:contentDescription="@string/menu_settings"
+				android:background="@drawable/selector_compose_button"
+				app:srcCompat="@drawable/ic_settings_outline_24dp"
 				app:layout_constraintTop_toTopOf="parent"
+				app:layout_constraintLeft_toRightOf="@id/edit"
+				app:layout_constraintRight_toRightOf="parent"
 				/>
 
 		</androidx.constraintlayout.widget.ConstraintLayout>

+ 0 - 11
app/src/main/res/layout/item_send_media.xml

@@ -37,17 +37,6 @@
 		app:srcCompat="@drawable/ic_close"
 		app:tint="@android:color/white" />
 
-	<ImageView
-		android:id="@+id/settings_view"
-		android:layout_width="24dp"
-		android:layout_height="24dp"
-		android:layout_margin="@dimen/send_items_grid_action_icon_margin"
-		android:layout_gravity="left|top"
-		android:contentDescription="@string/menu_settings"
-		android:scaleType="centerInside"
-		app:srcCompat="@drawable/ic_settings_outline_24dp"
-		app:tint="@android:color/white" />
-
 	<LinearLayout
 		android:id="@+id/qualifier_view"
 		android:layout_width="wrap_content"

+ 0 - 49
app/src/main/res/layout/popup_tooltip_bottom_left_image_resolution.xml

@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (c) 2020 Threema GmbH
-  ~ All rights reserved.
-  -->
-
-<LinearLayout
-	xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:app="http://schemas.android.com/apk/res-auto"
-	android:layout_width="wrap_content"
-	android:layout_height="wrap_content"
-	android:paddingTop="8dp"
-	android:paddingRight="8dp"
-	android:paddingLeft="10dp"
-	android:paddingBottom="18dp"
-	android:background="@drawable/bubble_tooltip_plain"
-	android:orientation="horizontal">
-
-	<ImageView
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_gravity="left|center_vertical"
-		android:layout_marginRight="8dp"
-		app:srcCompat="@drawable/exo_ic_settings"/>
-
-	<ch.threema.app.emojis.EmojiTextView
-		android:id="@+id/label"
-		android:layout_width="0dp"
-		android:layout_height="wrap_content"
-		android:layout_gravity="center_vertical"
-		android:textSize="@dimen/tooltip_text_size"
-		android:layout_weight="1"
-		android:textColor="@color/tooltip_text_color"/>
-
-	<ImageView
-		android:id="@+id/close_button"
-		android:layout_width="26sp"
-		android:layout_height="26sp"
-		android:padding="3sp"
-		android:layout_marginLeft="4dp"
-		android:layout_marginTop="1dp"
-		android:layout_gravity="right|center_vertical"
-		android:background="?android:selectableItemBackground"
-		android:contentDescription="@string/close"
-		android:clickable="true"
-		app:srcCompat="@drawable/ic_close"
-		app:tint="@android:color/white"/>
-
-</LinearLayout>

+ 1 - 1
app/src/main/res/layout/popup_tooltip_top_right.xml

@@ -32,6 +32,6 @@
 		android:contentDescription="@string/close"
 		android:clickable="true"
 		app:srcCompat="@drawable/ic_close"
-		android:tint="@android:color/white"/>
+		app:tint="@android:color/white" />
 
 </LinearLayout>

+ 1 - 0
app/src/main/res/values-de/strings.xml

@@ -1296,4 +1296,5 @@ sicheren Ort gesichert oder ausgedruckt haben.</string>
 	<string name="max_selectable_media_exceeded">Es können maximal %d Objekte aufs Mal versendet werden.</string>
 	<string name="ballot_created_successfully">Die Umfrage wurde erfolgreich erstellt.</string>
 	<string name="file_size">Dateigrösse</string>
+	<string name="thirty_days_abbrev">30d</string>
 </resources>

+ 1 - 1
app/src/main/res/values-it/qrscanner_strings.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-	<string name="msg_default_status">Inserire un codice QR all\'interno del rettangolo del mirino per la scansione.</string>
+    <string name="msg_default_status">Inserire un codice QR all\'interno del rettangolo del mirino per la scansione.</string>
     <string name="msg_camera_framework_bug">Impossibile accedere alla fotocamera Android. Ricorda di concedere il permesso di accesso alla fotocamera o riavvia il dispositivo.</string>
 </resources>

+ 11 - 1
app/src/main/res/values-it/strings.xml

@@ -1035,8 +1035,18 @@ messaggi ogni 15 minuti.</string>
     <string name="num_archived_chats">%d chat archiviate</string>
     <string name="continue_recording">Continua registrazione</string>
     <string name="whatsnew_title">Ecco %s 4.5</string>
-    <string name="whatsnew_headline">Da oggi %1$s è ancora più bello e più facile da usare. Poiché non è più dipendente da applicazioni esterne, la tua privacy è garantita.\n\nIl modernissimo design Material è più intuitivo: per effettuare le diverse azioni sono necessari meno passaggi.\n\nTocca \"Per saperne di più\" e scoprirai molto di più sulle nuove emozionanti caratteristiche di %1$s 4.0.</string>
+    <string name="whatsnew_headline">%1$s ti garantisce un\'esperienza mediale mai provata prima. \n\nLa selezione rapida dei file media è stata sottoposta a un redesign e offre, oltre a svariati filtri, la funzione opzionale di riconoscimento delle immagini basata sul machine learning.\n\nInoltre è stata introdotta la ricerca di testo a tappeto in tutte le chat, che rende più facile la citazione e molte altre cose.</string>
     <string name="whatsnew2_title">Quali sono le novità?</string>
+    <string name="whatsnew2_body"><![CDATA[<p><b>Selezione rapida dei media</b>: Clicca sulla graffetta per visualizzare i tuoi media in uno slider estraibile. È possibile disattivare questa funzione nelle impostazioni di chat: <i>Impostazioni / Chat / Selezione rapida media</i>.</p>
+<p><b>Riconoscimento immagini</b>: Cerca tra le tue immagini oggetti, attività e luoghi comuni. Il riconoscimento delle immagini si basa su un modello di machine learning locale e non invia dati né a Threema né a terzi. Poiché l’indicizzazione richiede tempo e risorse, il riconoscimento delle immagini è disattivato per default. Puoi attivarlo alla voce <i>Impostazioni Threema / Media & Memoria / Riconoscimento immagini</i>.</p>
+<p><b>Invio media</b>: Ora puoi inviare le tue immagini con varie risoluzioni senza dover cambiare le impostazioni generali.</p>
+<p><b>Video editor</b>: Ora puoi montare il video prima di inviarlo. Inoltre è stato migliorato il processo di conversione del video, che ora si svolge in background.</p>
+<p><b>Salvare nella galleria</b>: In base ai nuovi requisiti di Google a partire da Android 10 i file saranno salvati nei folder media generali del sistema (<i>immagini, video, musica,</i> e <i>file</i>).</p>
+<p><b>Ricerca globale</b>: Cerca il testo in tutte le chat. Avvia la ricerca globale dallo schermo di casa %1$s alla voce <i>Menu / Cerca nelle chat</i>.</p>
+<p><b>Citare un messaggio</b>: Con %1$s ora è possibile citare qualsiasi tipo di media, comprese immagini, video o messaggi vocali.</p>
+<p><b>100 nuovi emoji</b>: C’è anche l‘attesissima fondue &#129749;</p>
+<p><b>Testi lunghi</b>: Per rendere la chat più leggibile, i testi molto lunghi vengono visualizzati in forma accorciata e su richiesta possono essere visualizzati per intero. È inoltre possibile disattivare la formattazione.</p>
+]]></string>
     <string name="tooltip_identity_popup">Tocca qui per visualizzare rapidamente il tuo Threema ID o per scansionare gli ID di altre persone</string>
     <string name="tap_to_start">Tocca qui per iniziare %s adesso.</string>
     <string name="two_years">2 anni</string>

+ 5 - 0
app/src/main/res/values-it/webclient_strings.xml

@@ -26,12 +26,17 @@
     <string name="webclient_protocol_version_to_old">La tua app non supporta questa versione di Threema Web. Aggiorna Threema alla versione più recente.</string>
     <string name="webclient_protocol_version_too_new_selfhosted">La tua app non supporta questa versione di Threema Web. Chiedi al tuo amministratore Threema Web di aggiornare all\'ultima versione.</string>
     <string name="webclient_protocol_version_too_new_threema">La tua app non supporta questa versione di Threema Web. Usa una versione più recente di Threema Web.</string>
+    <string name="webclient_session_already_exists">La sessione del codice QR da te scansionato esiste già. Carica ancora una volta Threema Web nel tuo browser e riprova.</string>
     <string name="webclient_really_start_webclient_by_payload_body">Vuoi iniziare questa sessione di Threema Web ora?</string>
     <string name="webclient_cannot_restore">Impossibile ripristinare la sessione Threema Web</string>
     <string name="webclient_disabled">Threema Web non è attivato</string>
+    <string name="webclient_cannot_start">Impossibile avviare sessione di Threema Web</string>
+    <string name="webclient_constrained_by_mdm">Il server non è stato approvato dall\'amministratore.</string>
     <string name="webclient_clear_all_sessions">Elimina tutte le sessioni</string>
     <string name="webclient_clear_all_sessions_confirm">Sei sicuro/a di volere interrompere ed eliminare tutte le sessioni Threema Web?</string>
+    <string name="webclient_prefs_debug_tool_summary">Avvia questo strumento per eseguire il debug dei problemi di  connessione a Threema Web</string>
     <string name="webclient_diagnostics">Diagnostica Threema Web</string>
     <string name="webclient_diagnostics_start">Avvia</string>
     <string name="webclient_diagnostics_intro">Premi il pulsante Avvia per iniziare il test.</string>
+    <string name="webclient_diagnostics_done">Fatto. Se riscontri problemi di connessione a Threema Web, invia questo log al servizio di assistenza di Threema.</string>
 </resources>

+ 19 - 4
app/src/main/res/values-rm/strings.xml

@@ -11,10 +11,10 @@
     <string name="title_enter_id">Endatar l\'ID</string>
     <string name="title_invite_friend">Envidar amis</string>
     <string name="invite_via">Envidar via ...</string>
-    <string name="invite_email_subject">Threema. Il messenger segir che protegia tia e mia sfera privata.</string>
-    <string name="invite_email_body">Hallo\n\nTelechargia Threema, il messenger segir che protegia tia e mia sfera privata.\n\nTrametta fotos, videos, messadis discurrids e lieus; fa gruppas ed enquistas; trametta documents e datotecas en tut ils formats (PDF, GIF, MP3 eav.).\n\nLain en futur communitgar ensemen via Threema.\n\nTelechargia Threema qua: https://threema.ch/download\n\nChars salids</string>
+    <string name="invite_email_body">Hallo\n\nTelechargia %1$s, il messenger segir che protegia tia e mia sfera privata.\n\nTrametta fotos, videos, messadis discurrids e lieus; fa gruppas ed enquistas; trametta documents e datotecas en tut ils formats (PDF, GIF, MP3 eav.).\n\nLain en futur communitgar ensemen via %1$s.\n\nTelechargia %1$s qua: https://threema.ch/download\n\nChars salids\nhttps://threema.id/%2$s</string>
     <string name="invite_sms_body">Lain en futur communitgar ensemen via Threema. Telechargia qua Threema:
 https://threema.ch/download</string>
+    <string name="invite_email_subject">Threema. Il messenger segir che protegia tia e mia sfera privata.</string>
     <string name="enter_id_hint">Endatar l\'ID da Threema</string>
     <string name="account_links">Colliaziuns</string>
     <string name="menu_settings">Configuraziuns</string>
@@ -827,7 +827,6 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="prefs_auto_download_title">Telechargiar automaticamain las medias</string>
     <string name="prefs_auto_download_wifi">En il WLAN</string>
     <string name="prefs_auto_download_mobile">En raits mobilas</string>
-    <string name="rate_intro">Nus avain midà ad in nov design pli modern, perquai ch\'i na dat per ils emojis vegls nagins updates pli. Co plaschan ils novs emojis a tai?</string>
     <string name="rate_feedback_intro">Nus deplorain d\'udir quai. Schai p.pl. a nus tge che nus pudain far meglier.</string>
     <string name="rate_positive">Trametter recensiun</string>
     <string name="rate_title">Valitar ils novs emojis</string>
@@ -1010,6 +1009,17 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="delete">Stizzar</string>
     <string name="num_archived_chats">%d chats archivads</string>
     <string name="continue_recording">Registrar vinavant</string>
+    <string name="whatsnew_title">Tge è nov?</string>
+    <string name="whatsnew2_title">Tge è nov?</string>
+    <string name="whatsnew2_body"><![CDATA[<p><b>Selecziun sperta dals maletgs</b>: Clicca sin il fermagl per visualisar tias medias a moda survesaivla en in slider prolungabel. En las configuraziuns da chat pos ti era deactivar questa funcziun:<i>Configuraziuns / chat / selecziun sperta dals maletgs</i>.</p>
+<p><b>Tschertga da maletgs</b>: Tschertga objects, activitads e lieus currents en tes maletgs. L\'identificaziun da maletgs sa basa sin in model da machine learning local e na trametta naginas datas a nus u ad autras partidas terzas. Perquai che l\'indexaziun dovra relativamain bleras resursas e dura in mument, è la tschertga da maletgs deactivada sco standard. Ti pos activar ella sut <i>Configuraziuns / Medias ed arcun / Tschertga da maletgs</i>.</p>
+<p><b>Trametter medias</b>: Ti pos ussa trametter maletgs cun ina resoluziun individuala, senza stuair midar las configuraziuns globalas.</p>
+<p><b>Editur da videos</b>: Taglia ils videos avant che trametter els. Plinavant è il process da convertir videos vegnì meglierà, uschia ch\'el po lavurar davostiers.</p>
+<p><b>Memorisar en la gallaria</b>: Pervia da novas pretensiuns da Google vegnan - sin apparats cun Android 10 e sistems operativs pli actuals - las datotecas memorisadas en l\'ordinatur da medias general dal sistem (<i>Fotos, Videos, Musica,</i> e<i>Documents</i>).</p>
+<p><b>Tschertga globala</b>: Tschertga in text en tut ils chats. Ti pos tschertgar globalmain davent dal visur principal%1$s <i>Menu / Tschertga globala </i>.</p>
+<p><b>Citats</b>: %1$s pussibilitescha ussa da citar tut ils tips da medias, incl. maletgs, videos u messadis discurrids.</p>
+<p><b>100 emojis novs</b>: tranter auter il fondue &#129749; tant bramà;</p>
+<p><b>Texts lungs</b>: per avair la survista en il chat, vegnan scursanids messadis da text fitg lungs. Sin dumonda vegn visualisà l\'entir messadi. Igl è era pussaivel da deactivar las formataziuns.</p>]]></string>
     <string name="tooltip_identity_popup">Smatga qua per visualisar spert tia ID da Threema u per scannar las IDs dad autras persunas</string>
     <string name="tap_to_start">Smatga qua per aviar uss %s.</string>
     <string name="two_years">2 onns</string>
@@ -1061,7 +1071,7 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="prefs_working_days_enable_title">Regulaziun dal temp da paus</string>
     <string name="prefs_working_days_enable_sum">Na mussar nagins messadis e refusar telefonats da Threema ordaifer il temp da lavur</string>
     <string name="work_life_dnd_active">Temp da paus activ</string>
-	<string name="pencil">Rispli</string>
+    <string name="pencil">Rispli</string>
     <string name="warning">Avertiment</string>
     <string name="password_remember_warning">Tegna endament quai che ti endateschas qua! Cunquai che %s na memorisescha nagins pleds-clav sin servers, na pudain nus betg gidar tai, sche ti has emblidà il pin u la frasa d\'access.</string>
     <string name="safe_backup_tap_to_restart">Smatga sin il messadi per reaviar l\'app.</string>
@@ -1144,7 +1154,12 @@ Endatescha in pled-clav per tes backup da datas.</string>
     <string name="notification_channel_image_labeling_desc">Ils maletgs en la gallaria vegnan indexads davosvart</string>
     <string name="notification_image_labeling_desc">Indexaziun dals maletgs</string>
     <string name="no_media_found_global">Chattà naginas medias sin quest apparat</string>
+    <string name="prefs_sum_image_labeling">Pussibilitar la tschertga da chavazzins per ils maletgs publics en tia gallaria</string>
     <string name="prefs_image_labeling">Tschertga da maletgs</string>
     <string name="enable_formatting">Activar la formataziun</string>
+    <string name="original_file_no_longer_avilable">Nagin access pli a la datoteca originala. P.pl. trametter anc ina giada quest messadi.</string>
+    <string name="state_transcoding">vegn transcodà</string>
     <string name="importing_files">Las datotecas vegnan importadas</string>
+    <string name="tooltip_image_resolution_hint">Adattar individualmain la resoluziun</string>
+    <string name="image_labeling_stuck_error">Il process d\'indexaziun per la tschertga da maletgs è stada airi e vegnida interrutta. P.pl. empruvar pli tard anc ina giada.</string>
 </resources>

+ 11 - 1
app/src/main/res/values-rm/webclient_strings.xml

@@ -4,7 +4,7 @@
     <string name="webclient_init_session">Iniziar ina nova sesida</string>
     <string name="webclient_sessions_really_delete">Vuls ti propi stizzar questa sesida da Threema Web?</string>
     <string name="webclient_last_usage">Utilisà l\'ultima giada: %s</string>
-    <string name="webclient_created_at">Creà: %s (%s)</string>
+    <string name="webclient_created_at">Creà: %1$s (%2$s)</string>
     <string name="webclient_active_since">Activ dapi: %s</string>
     <string name="webclient_enable">Activar Threema Web</string>
     <string name="webclient_no_sessions_found">Avra il navigatur-web sin tes computer e chargia <b>https://web.threema.ch</b> per sa colliar. Clicca sin il button per scannar il code visualisà.</string>
@@ -26,7 +26,17 @@
     <string name="webclient_protocol_version_to_old">Tia app na sustegna betg questa versiun da Threema Web. P.pl. installar ina versiun pli nova da Threema.</string>
     <string name="webclient_protocol_version_too_new_selfhosted">Tia app na sustegna betg questa versiun da Threema Web. P.pl. dumandar l\'administratur da tes Threema Web d\'installar la versiun la pli nova.</string>
     <string name="webclient_protocol_version_too_new_threema">Tia app na sustegna betg questa versiun da Threema Web. P.pl. duvrar ina versiun pli nova da Threema Web.</string>
+    <string name="webclient_session_already_exists">La sesida dal code QR che ti has scannà exista gia. P.pl. rechargiar Threema Web en tes navigatur ed empruvar anc ina giada.</string>
     <string name="webclient_really_start_webclient_by_payload_body">Vuls ti cumenzar ussa questa sesida da Threema Web?</string>
+    <string name="webclient_cannot_restore">Betg pussaivel da restituir Threema Web</string>
+    <string name="webclient_disabled">Threema Web n\'è betg activà</string>
+    <string name="webclient_cannot_start">Betg pussaivel da cumenzar la sesida da Threema Web</string>
+    <string name="webclient_constrained_by_mdm">L\'administratur na sustegna betg il server.</string>
     <string name="webclient_clear_all_sessions">Stizzar tut las sesidas</string>
     <string name="webclient_clear_all_sessions_confirm">Vuls ti propi stizzar tut las sesidas da Threema Web?</string>
+    <string name="webclient_prefs_debug_tool_summary">Aviescha quest tool per diagnostitgar problems cun la creaziun d\'ina colliaziun da Threema Web.</string>
+    <string name="webclient_diagnostics">Tschertga da sbagls Threema Web</string>
+    <string name="webclient_diagnostics_start">Start</string>
+    <string name="webclient_diagnostics_intro">Clicca sin «Start» per iniziar il test.</string>
+    <string name="webclient_diagnostics_done">Finì. Sche ti has problems cun la creaziun d\'ina colliaziun da Threema Web, trametta quest log al support da Threema.</string>
 </resources>

+ 1 - 1
app/src/main/res/values-zh-rCN/poi_strings.xml

@@ -5,7 +5,7 @@
     <string name="town">镇</string>
     <string name="isolated_dwelling">隔离住宅</string>
     <string name="island">岛</string>
-    <string name="islet">市郊</string> <!-- whould not be same as suburb-->
+    <string name="islet">小岛</string>
     <string name="suburb">市郊</string>
     <string name="city">市</string>
     <string name="city_block">城市街区</string>

+ 122 - 112
app/src/main/res/values-zh-rCN/strings.xml

@@ -11,9 +11,9 @@
     <string name="title_enter_id">输入ID</string>
     <string name="title_invite_friend">邀请朋友</string>
     <string name="invite_via">通过…邀请朋友</string>
-    <string name="invite_email_body">您好,\n\n我使用%1$s,它是保护用户隐私的安全即时通讯程序。\n\n我的Threema ID:https://threema.id/%2$s\n\n让我们通过%1$s进行通信s!\n\n干杯\n</string>
+    <string name="invite_email_body">您好,\n\n我使用%1$s,它是保护用户隐私的安全即时通讯工具。\n\n我的Threema ID:https://threema.id/%2$s\n\n让我们通过%1$s进行通信s!\n\n干杯\n</string>
     <string name="invite_sms_body">嗨!让我们使用%1$s以安全和符合隐私的方式进行交流!我的Threema ID:https://threema.id/%2$s</string>
-    <string name="invite_email_subject">" Threema.保护我们隐私的安全信使"</string>
+    <string name="invite_email_subject">Threema,保护隐私的安全通讯工具。</string>
     <string name="enter_id_hint">输入Threema ID</string>
     <string name="account_links">关联账户</string>
     <string name="menu_settings">设定</string>
@@ -74,7 +74,7 @@
     <string name="prefs_sum_polling_on">定期检查是否有新消息(使用更多电量!)</string>
     <string name="prefs_sum_polling_off">仅在推送时检查新消息</string>
     <string name="prefs_logging">记录中</string>
-    <string name="prefs_title_message_log_switch">录到文件</string>
+    <string name="prefs_title_message_log_switch">录到文件</string>
     <string name="prefs_title_sum_message_log_on">事件和网络信息将记录到文件debug_log.txt中</string>
     <string name="prefs_title_sum_message_log_off">活动将不会被记录</string>
     <string name="prefs_reset_push">重置推送令牌</string>
@@ -149,7 +149,7 @@
     <string name="verify_failed_summary">验证您的手机号码失败。请确保输入的号码
 正确无误,并且已连接到移动网络,然后重试。</string>
     <string name="verify_failed_not_linked">验证您的手机号码失败。验证过程已中止。</string>
-    <string name="check_incoming_sms">检查传入的短信</string>
+    <string name="check_incoming_sms">检查收到的短信</string>
     <string name="backup_title">导出ID</string>
     <string name="backup_sum">导出您的Threema ID</string>
     <string name="backup_and_delete">备份和删除</string>
@@ -317,7 +317,7 @@ https://shop.threema.ch/retrieve_keys</string>
     <string name="state_read">已读</string>
     <string name="state_ack">同意</string>
     <string name="state_dec">不同意</string>
-    <string name="state_delivered">已送</string>
+    <string name="state_delivered">已送</string>
     <string name="state_sending">发送中</string>
     <string name="state_pending">待定</string>
     <string name="state_failed">失败了</string>
@@ -333,11 +333,11 @@ https://shop.threema.ch/retrieve_keys</string>
     <string name="creating_group">建立群组</string>
     <string name="updating_group">更新群组</string>
     <string name="status_create_group">群组已创建。</string>
-    <string name="status_rename_group">群组已重命名为«%1$s»</string>
+    <string name="status_rename_group">群组名称改为 \"%1$s\"</string>
     <string name="status_group_new_photo">群组已更新。</string>
-    <string name="status_group_new_member">«%1$s»已添加到群组。</string>
-    <string name="status_group_member_left">«%1$s»离开群组</string>
-    <string name="status_group_member_kicked">«%1$s» 已从群组中除。</string>
+    <string name="status_group_new_member">«%1$s» 已添加到群组。</string>
+    <string name="status_group_member_left">«%1$s» 离开群组</string>
+    <string name="status_group_member_kicked">«%1$s» 已从群组中除。</string>
     <string name="can_not_send_no_group_members">您无法将消息发送到空群组</string>
     <string name="you_are_not_a_member_of_this_group">您不是该群组的成员</string>
     <string name="can_not_delete_not_valid">无法删除无效的对象</string>
@@ -422,8 +422,8 @@ https://shop.threema.ch/retrieve_keys</string>
     <string name="edit_name_only">编辑名称</string>
     <string name="group_was_synchronized">群组已同步。</string>
     <string name="verification_level2_work_explain">内部联系人,由您的组织预先填充。</string>
-    <string name="verification_level3_work_explain">通过扫描二维码,您能亲自验证内部联系人的身份和公钥。</string>
-    <string name="verification_level3_explain">通过扫描二维码,您能亲自验证联系人的身份和公钥。</string>
+    <string name="verification_level3_work_explain">通过扫描QR码,您能亲自验证内部联系人的身份和公钥。</string>
+    <string name="verification_level3_explain">通过扫描QR码,您能亲自验证联系人的身份和公钥。</string>
     <string name="verification_level2_explain">联系人的电话号码和/或电子邮件地址包含在您的通讯录中。</string>
     <string name="verification_level1_explain">未知联系人;该联系人没有将电话号码或电子邮件地址链接到其ID,或者您的通讯录中没有这些联系人详细信息。</string>
     <string name="state_dialog_received">已收到</string>
@@ -529,7 +529,7 @@ https://myid.threema.ch/revoke撤消您的ID,以防丢失或被盗</string>
     <string name="rotate">旋转</string>
     <string name="remove">去掉</string>
     <string name="image_already_added">该图像已被添加。</string>
-    <string name="password_too_short" tools:ignore="PluralsCandidate">至少%d个字符</string>
+    <string name="password_too_short" tools:ignore="PluralsCandidate">至少%d个字符</string>
     <string name="mark_read">标记为已读</string>
     <string name="attach_document">文件</string>
     <string name="parent_directory">父母</string>
@@ -557,8 +557,8 @@ https://myid.threema.ch/revoke撤消您的ID,以防丢失或被盗</string>
     <string name="mime_word">文字文件</string>
     <string name="no_filename"><![CDATA[<没有文件名>]]></string>
     <string name="send_as_files">作为文件发送</string>
-    <string name="send_as_files_warning">传输未压缩的文件和图像可能会导致大量数据使用
-,这可能会导致您的移动服务提供商额外收费。我们建议
+    <string name="send_as_files_warning">传输未压缩的文件和图像可能会使用大量数据
+,而您的移动运营商也可能会收取额外费用。我们建议
 仅在WiFi网络中上传和下载文件。</string>
     <string name="prefs_theme">设计主题</string>
     <string name="list_theme_light">浅色(默认)</string>
@@ -567,14 +567,14 @@ https://myid.threema.ch/revoke撤消您的ID,以防丢失或被盗</string>
     <string name="prefs_sum_passphrase">需要密码才能解锁本地加密</string>
     <string name="prefs_title_masterkey_change_passphrase">更改密码</string>
     <string name="storage_total">内部存储空间</string>
-    <string name="storage_threema">Threema用的空间</string>
+    <string name="storage_threema">Threema用的空间</string>
     <string name="storage_total_free">总可用空间</string>
     <string name="storage_total_in_use">正在使用</string>
     <string name="one_year">1年</string>
     <string name="six_months">6个月</string>
     <string name="three_months">3个月</string>
     <string name="one_month">1个月</string>
-    <string name="one_week">1个月</string>
+    <string name="one_week">1星期</string>
     <string name="everything">一切</string>
     <string name="delete_media_files_time">删除以下媒体和文件:</string>
     <string name="storage_explain">如果可用空间不足,则可以删除较旧的加密媒体文件腾出
@@ -583,10 +583,10 @@ https://myid.threema.ch/revoke撤消您的ID,以防丢失或被盗</string>
     <string name="delete_date_confirm_message">如果继续,这些文件将被删除,并且仅
 保留缩略图。</string>
     <string name="media_files_deleted" tools:ignore="PluralsCandidate">已删除%d个媒体文件</string>
-    <string name="storage_management">储管理</string>
+    <string name="storage_management">储管理</string>
     <string name="media">媒体</string>
     <string name="prefs_storage_mgmt_title">清理媒体,文件和消息</string>
-    <string name="num_messages">讯息数</string>
+    <string name="num_messages">信息数量</string>
     <string name="delete_messages_explain">删除以下讯息:</string>
     <string name="delete_message">立即删除讯息</string>
     <string name="really_delete_messages">如果继续,这些讯息将被永久删除。您将无法
@@ -609,7 +609,7 @@ Threema支持的所有表情符号。</string>
     <string name="privacy_policy">隐私政策</string>
     <string name="save_group_changes">您想将更改保存到群组吗?</string>
     <string name="prefs_title_fontsize">字体大小</string>
-    <string name="fontsize_normal">定期</string>
+    <string name="fontsize_normal">正常</string>
     <string name="fontsize_large">大</string>
     <string name="fontsize_xlarge">超大</string>
     <string name="no_app_for_location">在此设备上找不到地图应用程序</string>
@@ -630,16 +630,16 @@ Threema支持的所有表情符号。</string>
     <string name="notifications_until">直到%s</string>
     <string name="notifications_mute">静音</string>
     <string name="notifications_choose_sound">选择声音</string>
-    <string name="error_video_conversion">转换视频出错。</string>
+    <string name="error_video_conversion">转换视频出错。</string>
     <string name="confirm_your_pin">确认密码</string>
-    <string name="too_many_incorrect_attempts">过多的错误尝试。请在%s秒后重试。</string>
+    <string name="too_many_incorrect_attempts">错误尝试次数太多。请在%s秒后重试。</string>
     <string name="no_lockscreen_set">未设置系统屏幕锁定。</string>
     <string name="on">开</string>
     <string name="off">关</string>
     <string name="new_wizard_select_country">选择你的国家</string>
     <string name="new_wizard_lets_get_started">让我们开始吧!</string>
     <string name="new_wizard_setup_threema">设置Threema</string>
-    <string name="new_wizard_restore_id_backup">恢复导出的ID</string>
+    <string name="new_wizard_restore_id_backup">还原导出的ID</string>
     <string name="new_wizard_welcome">欢迎来到Threema!</string>
     <string name="new_wizard_move_finger">在屏幕上移动手指</string>
     <string name="new_wizard_this_is_your_id">这是您的Threema ID:</string>
@@ -663,8 +663,8 @@ Threema支持的所有表情符号。</string>
     <string name="new_wizard_phone_invalid">您输入的电话号码无效。\n请先更正,然后再继续。</string>
     <string name="new_wizard_info_fingerprint">通过移动手指,您可以创建随机数据(称为熵),该数据用于生成
 与新的唯一Threema ID关联的密钥对。该密钥对包括一个的<b>公钥</b>是分配给
-您的朋友和<b>私钥</b>安全地存储在您的手机上。您的朋友将使用
-您的公共密钥加密发送给您的消息。只有私钥的所有者,其他人都不能解密这些消息。</string>
+您的朋友和<b>私钥</b>安全地存储在您的手机上。您的朋友将使用
+您的公共密钥加密发送给您的消息。只有私钥的所有者才能解密这些消息,其他人都不能解密。</string>
     <string name="new_wizard_info_id">您还创建了一个密钥对。公钥已安全地传输到我们的
 服务器。私钥永远不会离开您的设备。这样可以确保没有其他人可以阅读您的消息。</string>
     <string name="new_wizard_info_sync_contacts">如果启用此选项,Threema会将电子邮件地址和电话号码单向加密(散列),然后
@@ -673,11 +673,11 @@ Threema支持的所有表情符号。</string>
     <string name="new_wizard_info_link">通过提供您的电话号码和电子邮件地址,Threema可以帮助您的朋友
 自动在您的电话通讯录中找到您。数据将以
 单向加密(散列)形式存储在我们的服务器上。如果您想
-完全匿名使用Threema,则只需跳过此步骤。</string>
+完全匿名使用Threema,则可以跳过此步骤。</string>
     <string name="new_wizard_info_link_phone_only">通过提供您的电话号码,Threema可以帮助您的朋友
 自动在您的电话通讯录中找到您。该号码将以
 单向加密(散列)形式存储在我们的服务器上。如果您想
-完全匿名使用Threema,则只需跳过此步骤。</string>
+完全匿名使用Threema,则可以跳过此步骤。</string>
     <string name="new_wizard_info_nickname">昵称在某些设备上的推式通知中使用,或作为
 在您的通讯录中尚未拥有您的用户标识您的其他方式。我们建议
 仅提供您的名字或化名。如果您未设置昵称,则默认情况下,我们将使用您的Threema ID。</string>
@@ -691,7 +691,7 @@ Threema支持的所有表情符号。</string>
     <string name="status_ballot_user_first_vote">«%1$s» 对«%2$s»进行了投票</string>
     <string name="status_ballot_user_modified_vote">«%1$s»更改了«%2$s»的投票</string>
     <string name="status_ballot_all_votes">已完成“«%1$s»的投票</string>
-    <string name="restore">恢复</string>
+    <string name="restore">还原</string>
     <string name="new_wizard_anonymous_confirm">您没有输入手机号码或电子邮件地址来链接到
 Threema ID。您不会出现在朋友的联系人列表中。您真的要
 匿名使用Threema吗?</string>
@@ -700,7 +700,7 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
 匿名使用Threema吗?</string>
     <string name="new_wizard_scan_id_backup">或扫描ID导出的QR码</string>
     <string name="error_saving_file">保存文件时出错。检查权限。</string>
-    <string name="wait_one_minute">请至少等待10分钟以使SMS到达,然后再请求呼叫。</string>
+    <string name="wait_one_minute">请求通话前,请至少等待10分钟,直至短信到达。</string>
     <string name="backup_id">导出的ID</string>
     <string name="backup_data">数据备份</string>
     <string name="really_leave_group_admin_message">您是该群组的管理员。如果现在离开,群组将
@@ -717,13 +717,13 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="media_gallery_files">档案</string>
     <string name="prefs_gif_autoplay">自动播放GIF动画</string>
     <string name="media_gallery_audio">语音留言</string>
-    <string name="action_clone_group">语音留言</string>
-    <string name="clone_group_message">这将创建一个该组的克隆,并由您作为管理员。继续?</string>
-    <string name="prefs_proximity_sensor">使用近传感器</string>
-    <string name="prefs_proximity_sensor_explain">如果覆盖了近传感器,请使用听筒播放语音消息</string>
+    <string name="action_clone_group">克隆群组</string>
+    <string name="clone_group_message">这将创建一个该组的克隆,并由您作为管理员。是否继续?</string>
+    <string name="prefs_proximity_sensor">使用近距离传感器</string>
+    <string name="prefs_proximity_sensor_explain">如果覆盖了近距离传感器,请使用听筒播放语音消息</string>
     <string name="error_creating_group">建立/更新群组时发生错误</string>
     <string name="no_media_found_generic">在此聊天中找不到媒体</string>
-    <string name="max_images_reached" tools:ignore="PluralsCandidate">最 %d个项目可以一次发送</string>
+    <string name="max_images_reached" tools:ignore="PluralsCandidate">最 %d个项目可以一次发送</string>
     <string name="enter_description">请在此处描述错误/问题。</string>
     <string name="add_caption_hint">添加可选字幕</string>
     <string name="disable">禁用</string>
@@ -733,10 +733,10 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="hide_chat">私人聊天</string>
     <string name="really_hide_chat_message">您想将此聊天标记为私人吗?使用菜单切换消息列表中私人聊天的可见性。</string>
     <string name="chat_hidden">聊天标记为私人</string>
-    <string name="title_show_private_chats">聊天标记为私人</string>
+    <string name="title_show_private_chats">显示私人聊天</string>
     <string name="title_hide_private_chats">隐藏私人聊天</string>
-    <string name="chat_visible">聊天不再是私的</string>
-    <string name="prefs_title_locking_mechanism">锁紧机构</string>
+    <string name="chat_visible">聊天不再是私的</string>
+    <string name="prefs_title_locking_mechanism">锁定装置</string>
     <string name="lock_option_none">没有</string>
     <string name="lock_option_pin">销</string>
     <string name="lock_option_screenlock">系统屏幕锁定</string>
@@ -751,7 +751,7 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="grace_ten_minutes">10分钟</string>
     <string name="grace_thirty_minutes">30分钟</string>
     <string name="grace_never">从不(手动)</string>
-    <string name="never">不</string>
+    <string name="never">不</string>
     <string name="unhide_chats_confirm">如果立即删除访问保护,您的私人聊天将再次可见。</string>
     <string name="selection_counter_label" tools:ignore="PluralsCandidate">已选择%d张图片</string>
     <string name="verification_started">验证开始</string>
@@ -761,7 +761,7 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="prefs_title_direct_share">直接分享</string>
     <string name="prefs_sum_direct_share">与其他应用共享时显示最近的聊天记录</string>
     <string name="restore_disable_energy_saving">请将设备连接至充电器并禁用所有节能选项,以防止设备中断备份或还原过程。</string>
-    <string name="draft">草</string>
+    <string name="draft">草稿</string>
     <string name="prefs_bigger_single_emojis">更大的单个表情符号</string>
     <string name="wizard1_sync_work">同步Threema Work数据…</string>
     <string name="notification_hidden_text">隐藏的内容</string>
@@ -795,54 +795,54 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="edit">编辑</string>
     <string name="discard_changes">您要放弃更改吗?</string>
     <string name="prefs_title_network">网络</string>
-    <string name="prefs_title_ipv6_preferred">消息的IPv6</string>
+    <string name="prefs_title_ipv6_preferred">以iPv6发送信息</string>
     <string name="prefs_ipv6_preferred_off">仅使用IPv4连接</string>
     <string name="prefs_ipv6_preferred_on">在IPv4上优先使用IPv6连接</string>
-    <string name="prefs_title_ipv6_webrtc_allowed">用于通话和网络的IPv6</string>
-    <string name="prefs_ipv6_webrtc_allowed_on">允许IPv6进行Threema呼叫和Threema Web</string>
-    <string name="prefs_ipv6_webrtc_allowed_off">禁止将IPv6用于Threema呼叫和Threema Web</string>
-    <string name="ipv6_requires_restart">要应用此设置,该应用需要重新启动。</string>
+    <string name="prefs_title_ipv6_webrtc_allowed">以IPv6通话和上网</string>
+    <string name="prefs_ipv6_webrtc_allowed_on">允许IPv6进行Threema 通话和Threema 网页版</string>
+    <string name="prefs_ipv6_webrtc_allowed_off">禁止将IPv6用于Threema 通话和Threema 网页版</string>
+    <string name="ipv6_requires_restart">要应用此设置,需要重新启动应用程序。</string>
     <string name="ipv6_restart_now">现在重启</string>
-    <string name="on_cap"></string>
+    <string name="on_cap"></string>
     <string name="off_cap">关</string>
     <string name="share_chat">分享聊天</string>
     <string name="flip">翻转</string>
     <string name="to_front">往前</string>
-    <string name="play"></string>
+    <string name="play">播放</string>
     <string name="pause">暂停</string>
     <string name="retry">重试</string>
     <string name="voice_message_record">录制语音留言</string>
     <string name="open_navdrawer">打开导航抽屉</string>
     <string name="profile_picture">个人资料图片</string>
-    <string name="profile_picture_release">谁可以看到您的个人资料图片?</string>
+    <string name="profile_picture_release">谁可以看到您的个人头像?</string>
     <string name="picrelease_nobody">没有人</string>
     <string name="picrelease_selected">选定的联系人</string>
     <string name="picrelease_everyone">选定的联系人</string>
-    <string name="prefs_title_receive_profilepics">显示个人资料图片</string>
-    <string name="prefs_sum_receive_profilepics_off">隐藏联系人提供的个人资料照片</string>
-    <string name="prefs_sum_receive_profilepics_on">隐藏联系人提供的个人资料照片</string>
-    <string name="prefs_sum_receive_profilepics_recipients_list">当您向他们发送消息时,在此列表中选择的联系人将收到您的个人资料照片。</string>
+    <string name="prefs_title_receive_profilepics">显示个人头像</string>
+    <string name="prefs_sum_receive_profilepics_off">隐藏联系人提供的个人头像</string>
+    <string name="prefs_sum_receive_profilepics_on">隐藏联系人提供的个人头像</string>
+    <string name="prefs_sum_receive_profilepics_recipients_list">当您向列表中选择的联系人发信息时,他们将看到您的个人头像。</string>
     <string name="menu_send_profilpic">添加为个人资料照片收件人</string>
     <string name="menu_send_profilpic_off">删除为个人资料照片收件人</string>
     <string name="menu_send_profilpic_now">立即发送头像</string>
-    <string name="profile_picture_sent">个人资料图片已发送</string>
+    <string name="profile_picture_sent">个人头像已发送</string>
     <string name="sending_messages">正在发送…</string>
     <string name="backup_data_media_confirm">将大型媒体文件保存到本地ZIP备份可能会超出设备的CPU和内存容量,并且可能需要很长时间。备份运行时,Threema将不会发送或接收消息。继续吗?</string>
     <string name="backup_data_cancelled">备份已取消</string>
     <string name="service_manager_not_available">无法启动Threema。请关闭设备,然后再打开。</string>
     <string name="message_sent">讯息已发送</string>
-    <string name="threema_call">Threema话</string>
+    <string name="threema_call">Threema话</string>
     <string name="threema_message_to">给%s的讯息</string>
     <string name="threema_call_with">致电%s</string>
-    <string name="prefs_title_voip">Threema话</string>
+    <string name="prefs_title_voip">Threema话</string>
     <string name="prefs_title_force_turn">一律中继电话</string>
     <string name="prefs_summary_force_turn_off">如果可能,建立直接连接,并且仅通过Threema服务器将呼叫中继到未经验证的联系人。可能会暴露您的IP地址。</string>
     <string name="prefs_summary_force_turn_on">通过Threema服务器强制中继所有呼叫,从而保护您的IP地址。可能会影响通话质量。</string>
     <string name="permission_record_audio_required">要拨打加密电话并发送语音消息,请允许使用麦克风。</string>
     <string name="prefs_voice_call_notifications">语音通话</string>
     <string name="prefs_voice_call_sound">铃声</string>
-    <string name="prefs_sum_voice_call_sound">选择来电Threema铃声</string>
-    <string name="prefs_sum_voice_call_vibrate">在Threema来电中振动</string>
+    <string name="prefs_sum_voice_call_sound">选择Threema通话铃声</string>
+    <string name="prefs_sum_voice_call_vibrate">收到 Threema 通话时振动</string>
     <string name="prefs_title_voip_enable">启用Threema通话</string>
     <string name="webclient_invalid_push_token_message">需要手动启动</string>
     <string name="threema_work_contact">Threema工作联系</string>
@@ -854,36 +854,36 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="prefs_title_device_info">设备信息</string>
     <string name="notifications_disabled_title">通知已禁用</string>
     <string name="notifications_disabled_text">在系统设置中已禁用Threema的通知。您将不会收到有关新消息的通知。</string>
-    <string name="notifications_disabled_settings">编辑系统设</string>
+    <string name="notifications_disabled_settings">编辑系统设</string>
     <string name="error_attaching_files">添加附件时出错。</string>
     <string name="prefs_fix_powermanager_problems">禁用电源限制</string>
     <string name="prefs_fix_powermanager_problems_desc">允许Threema在后台运行,以便即使不在活动状态也可以接收消息。</string>
     <string name="disable_powermanager_explain">在接下来的屏幕中,确保对«%s» 进行保护或排除在手机的电源管理限制之外。完成操作后,请点按“返回”按钮。</string>
-    <string name="disable_autostart_explain">在以下屏幕中,确保可以自动启动的应用程序列表中包含«%s» 。完成后,点击“返回”按钮。</string>
+    <string name="disable_autostart_explain">在下一个画面中,确保«%s»在自动启动的应用程序列表中 。完成后,点击“返回”按钮。</string>
     <string name="notification_priority_default">低</string>
     <string name="notification_priority_high">高</string>
     <string name="notification_priority_max">最高</string>
     <string name="prefs_title_notification_priority">优先</string>
-    <string name="pin"></string>
+    <string name="pin">固定</string>
     <string name="unpin">取消固定</string>
-    <string name="location_services_disabled">定位服务已禁用。您现在要启用它们吗?</string>
+    <string name="location_services_disabled">定位服务已禁用。您现在要启用吗?</string>
     <string name="send_location">发送位置</string>
     <string name="unknown_address">未知地址</string>
     <string name="your_location">您的位置</string>
     <string name="network_blocked_title">后台数据已禁用</string>
     <string name="network_blocked_body">%s无法在后台接收消息。点按此处可启用设置中的背景数据。</string>
-    <string name="reply_later">我们回聊</string>
-    <string name="reply_on_my_way">我在路上</string>
+    <string name="reply_later">待会再聊</string>
+    <string name="reply_on_my_way">我马上就到</string>
     <string name="reply_thank_you">谢谢</string>
     <string name="reply_youre_welcome">别客气</string>
     <string name="prefs_auto_download_title">自动下载媒体</string>
-    <string name="prefs_auto_download_wifi">在WiFi上</string>
-    <string name="prefs_auto_download_mobile">关于移动数据</string>
+    <string name="prefs_auto_download_wifi">使用WiFi</string>
+    <string name="prefs_auto_download_mobile">使用移动数据</string>
     <string name="rate_intro">您如何评价Threema?</string>
     <string name="rate_feedback_intro">请让我们知道我们可以做些什么来改进(可选)。</string>
     <string name="rate_positive">发送评论</string>
-    <string name="rate_title">Threema</string>
-    <string name="rate_thank_you">谢谢您,对于您的评论!</string>
+    <string name="rate_title">评价Threema</string>
+    <string name="rate_thank_you">谢谢您的评价!</string>
     <string name="disabled_by_policy_short">管理员已禁用功能</string>
     <string name="rate_forward_to_play_store">您还要在Google Play上给我们评分吗?</string>
     <string name="rate_error">无法将您的评论发送到服务器。请重试之前,请确保您已连接到互联网。</string>
@@ -892,23 +892,23 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="minus">减去</string>
     <string name="plus">加</string>
     <string name="switched_off">关</string>
-    <string name="switched_on"></string>
+    <string name="switched_on"></string>
     <string name="title_tab_work_users">Threema Work用户</string>
     <string name="no_matching_work_contacts">找不到经过管理员验证的Threema Work联系人</string>
     <string name="all">所有</string>
     <string name="webclient_session_stop_all">关闭所有</string>
-    <string name="webclient_running_sessions">%d Threema个Web会话正在运行</string>
+    <string name="webclient_running_sessions">%d 个Threema 网页版会话正在运行</string>
     <string name="passphrase_service_name">密码服务</string>
     <string name="passphrase_service_description">密码解锁时的通知</string>
-    <string name="webclient_service_description">Threema Web会话处于活动状态时的通知</string>
+    <string name="webclient_service_description">Threema 网页版会话处于活动状态时的通知</string>
     <string name="prefs_title_accept_privacy_policy">接受隐私权政策</string>
     <string name="privacy_policy_explain">%1$s比其他任何Messenger都更加严格地保护您的隐私。在我们的%2$s中查找更多信息。</string>
     <string name="privacy_policy_check_confirm">要使用%s,请接受隐私政策。\n\n(根据欧盟法规2016/679的要求)</string>
     <string name="prefs_title_incognito_keyboard">请求隐身键盘</string>
-    <string name="prefs_sum_incognito_keyboard">禁用数据收集以获取个性化建议(如果键盘支持)</string>
+    <string name="prefs_sum_incognito_keyboard">禁止收集个性化建议的数据(如果键盘支持)。</string>
     <string name="tooltip_mentions">在键盘上键入@字符以直接寻址或提及该组的成员。</string>
     <string name="tooltip_imagepaint">发挥创意!在发送之前,点击魔术棒按钮进行涂抹或在照片上添加贴纸和文字。</string>
-    <string name="call_ongoing">通话</string>
+    <string name="call_ongoing">正在进行的通话</string>
     <string name="ballot_received_votes">获得的投票数:%1$d / %2$d</string>
     <string name="quote_not_found">找不到引用的消息</string>
     <string name="ballot_secret">秘密</string>
@@ -916,10 +916,10 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="passwords_dont_match">密码不匹配</string>
     <string name="test_unsuccessful">测试失败</string>
     <string name="preparing_threema_safe">准备Threema Safe</string>
-    <string name="disable_powermanager_title">功率限制</string>
+    <string name="disable_powermanager_title">电源限制</string>
     <string name="disable_autostart_title">自动开启</string>
     <string name="unchanged">不变的</string>
-    <string name="safe_learn_more_button">学到更多</string>
+    <string name="safe_learn_more_button">了解更多</string>
     <string name="safe_enable_explain">您需要聊天的所有内容仅存储在设备上。因为我们没有保存您的帐户,所以如果您丢失了手机或意外删除了数据,我们将无法为您提供帮助。\n\nThreema Safe在您选择的安全服务器上,以匿名方式创建所有重要数据的自动备份,包括您的密钥,联系人列表和群组成员身份。</string>
     <string name="safe_disable_confirm">您真的要在不启用Threema Safe的情况下继续吗?</string>
     <string name="safe_configure_choose_password">请选择一个强密码。您将需要此密码来还原Threema Safe备份。</string>
@@ -927,7 +927,7 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="safe_configure_server_explain">您可以使用Threema的服务器,也可以指定要使用的第三方备份服务器。</string>
     <string name="safe_use_default_server">使用默认服务器</string>
     <string name="safe_test_server">测试连通服务器</string>
-    <string name="safe_advanced_options">专家设</string>
+    <string name="safe_advanced_options">专家设</string>
     <string name="safe_enter_password">请输入您的Threema Safe密码</string>
     <string name="safe_threema_id">您的Threema ID</string>
     <string name="safe_restore_enter_id">请输入您要恢复的Threema ID</string>
@@ -951,10 +951,10 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="prefs_fix_background_data">启用背景数据</string>
     <string name="prefs_fix_background_data_desc">要在Threema处于后台时接收消息,请同时启用“后台数据”和“不受限制的数据使用”</string>
     <string name="prefs_fix_device">解决设备配置问题</string>
-    <string name="safe_successful">成功</string>
-    <string name="safe_unsuccessful">成功的</string>
+    <string name="safe_successful">成功</string>
+    <string name="safe_unsuccessful">失败</string>
     <string name="safe_upload_failed">上传失败</string>
-    <string name="safe_upload_size_exceeded">服务器的上传大小超出</string>
+    <string name="safe_upload_size_exceeded">超出服务器的上传最大限制</string>
     <string name="safe_connection_error">连接错误</string>
     <string name="safe_server_name">服务器名称</string>
     <string name="safe_max_backup_size">最大备份大小</string>
@@ -964,18 +964,18 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="backup_other_restore_options">其他还原选项</string>
     <string name="safe_size">备份大小</string>
     <string name="safe_version_mismatch">此备份的版本高于受支持的版本。请更新到该应用的最新版本。</string>
-    <string name="safe_restore_failed">恢复失败</string>
-    <string name="safe_failed_notification">Threema安全备份连续%d天失败。点击此处进行检查。</string>
-    <string name="safe_restore">恢复Threema安全</string>
+    <string name="safe_restore_failed">还原失败</string>
+    <string name="safe_failed_notification">Threema Safe 备份连续%d天失败。点击此处进行检查。</string>
+    <string name="safe_restore">还原Threema Safe</string>
     <string name="backup_restore_in_progress">正在进行备份或还原。检查通知以获取状态信息。</string>
-    <string name="restore_error_body">还原未成功完成</string>
+    <string name="restore_error_body">还原未完成</string>
     <string name="forgot_your_id">忘记ID了吗?</string>
     <string name="restore_success_body">恢复成功完成</string>
     <string name="work_data_sync">资料同步</string>
     <string name="private_contact">私人联络人</string>
     <string name="ringtone_selection_default">默认值(%s)</string>
-    <string name="work_data_sync_desc">Threema工作同步</string>
-    <string name="ballot_not_connected">在结束民意调查之前,请确保Threema在线。</string>
+    <string name="work_data_sync_desc">Threema Work 同步</string>
+    <string name="ballot_not_connected">在结束投票之前,请确保Threema在线。</string>
     <string name="empty_chat_title">清空聊天记录</string>
     <string name="empty_chat_confirm">此聊天中的所有消息将被删除。继续?</string>
     <string name="emptying_chat">清空聊天室</string>
@@ -987,23 +987,23 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="notification_setting_ignored">任何变化都将被忽略!</string>
     <string name="notification_channel_alerts">警告和错误</string>
     <string name="notification_channel_notices">告示</string>
-    <string name="chat_updates">即时通讯更新</string>
+    <string name="chat_updates">聊天室更新</string>
     <string name="backup_or_restore_progress">备份和还原进度</string>
     <string name="tooltip_export_id">点击此处立即共享或打印您的加密Threema ID</string>
     <string name="downloading">正在下载</string>
     <string name="today">今天</string>
-    <string name="restore_data_cancelled">恢复已取消</string>
+    <string name="restore_data_cancelled">还原已取消</string>
     <string name="safe_change_password">更改密码</string>
     <string name="safe_configure_choose_password_title">选择一个密码</string>
     <string name="password_bad_explain">Threema Safe所选的密码不安全,攻击者很容易猜到。请选择另一个。提示:使用由多个单词组成的密码。</string>
-    <string name="safe_password_updated">Threema安全密码已更新。</string>
-    <string name="safe_activated">Threema Safe现在已激活。</string>
+    <string name="safe_password_updated">Threema Safe 密码已更新。</string>
+    <string name="safe_activated">Threema Safe 现在已激活。</string>
     <string name="restore_zip_invalid_file">备份文件无效。</string>
     <string name="push_token_cleared">推送令牌已清除</string>
     <string name="insert_date">插入日期</string>
     <string name="add_answer">添加答案</string>
     <string name="title_cannot_be_empty">投票标题不能为空</string>
-    <string name="voip_disabled">Threema话被禁用</string>
+    <string name="voip_disabled">Threema话被禁用</string>
     <string name="hide_chat_enter_message_explain">此聊天标记为私人聊天。要输入该密码,请先设置访问保护。</string>
     <string name="unknown">未知</string>
     <string name="miui_notification_title">有关MIUI 10通知的重要通知</string>
@@ -1015,27 +1015,27 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="safe_configure_server_credentials_title">身份验证(可选)</string>
     <string name="username_hint">用户名</string>
     <string name="lock_option_biometric">生物识别</string>
-    <string name="biometric_enter_authentication">要解锁,请验证</string>
+    <string name="biometric_enter_authentication">请认证以解锁</string>
     <string name="biometric_authentication_failed">认证失败</string>
     <string name="biometric_authentication_successful">成功认证</string>
     <string name="work_safe_forced_explain">您的管理员为您的设备启用了Threema Safe。</string>
     <string name="pin_locked_cannot_send">该应用程序已锁定。无法发送。</string>
     <string name="prefs_summary_hide_screenshots_notice">出于隐私考虑,在安全设置中启用“应用锁定”后,始终会阻止缩略图和屏幕截图</string>
     <string name="work_select_categories">选择类别</string>
-    <string name="my_profile">我的简历</string>
+    <string name="my_profile">我的个人资料</string>
     <string name="message_too_long">讯息太长。无法发送。</string>
     <string name="database_migration_no_space">数据库迁移失败:设备上没有足够的空间。</string>
     <string name="advanced_options">高级选项</string>
     <string name="url_warning_body">您将要打开的链接的主机名可疑:\n\n显示的主机名:<b>%s</b>\n实际的主机名:<b>%s</b>\n\n这可能是试图欺骗您打开网站的尝试假装是别的东西。\n\n您仍然要继续吗?</string>
     <string name="url_warning_title">网络钓鱼警告</string>
-    <string name="permission_camera_qr_required">要扫描QR码,Threema需要访问相机</string>
+    <string name="permission_camera_qr_required">Threema需要访问相机以扫描QR码</string>
     <string name="voice_action_title">语音动作</string>
     <string name="voice_action_body">语音操作正在处理中</string>
-    <string name="permission_camera_photo_required">要拍照,请允许使用相机</string>
+    <string name="permission_camera_photo_required">请允许使用相机权限以拍照</string>
     <string name="global_search">搜索聊天</string>
     <string name="global_search_empty_view_text">输入至少两个字符以搜索所有消息</string>
     <string name="my_id">我的ID</string>
-    <string name="profile_picture_and_nickname">个人资料图片和昵称</string>
+    <string name="profile_picture_and_nickname">个人头像和昵称</string>
     <string name="lp_select_this_place">选择这个地方</string>
     <string name="lp_or_select_nearby">或选择附近的地方</string>
     <string name="lp_use_this_location">发送这个位置?</string>
@@ -1063,7 +1063,17 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="num_archived_chats">%d个已存档的聊天</string>
     <string name="continue_recording">继续录音</string>
     <string name="whatsnew_title">欢迎使用%s 4.5</string>
-    <string name="whatsnew2_title">什么是新的?</string>
+    <string name="whatsnew_headline">现在,%1$s 更美观,更易于使用,同时通过消除对外部应用程序的依赖性进一步保护了您的隐私。\n\n现代的Material设计更加直观,只需更少的水龙头即可完成工作。\n\n点击“了解更多信息”,以了解有关 %1$s 4.0中一些令人兴奋的新功能的更多信息。</string>
+    <string name="whatsnew2_title">有什么新消息?</string>
+    <string name="whatsnew2_body"><![CDATA[<p><b>媒体抽屉</b>:点击回形针图标,在可滚动的抽屉中浏览您的媒体文件。如果您不想让抽屉自动打开最新的媒体,请在聊天设置中禁用图片快速选择选项,地址是<i>设置/聊天/媒体快速选择</i>.</p>。
+<p><b>图像搜索</b>:搜索您的图像中的常见对象、活动和地点。<br>图像识别基于本地机器学习模型,不向Threema的服务器或任何第三方发送数据。由于分析图像是一项相当昂贵的任务,可能需要很长时间,该选项默认为禁用。你可以在<i>设置/媒体和存储/图像搜索</i>中找到它。</p>
+<p><b>发送媒体文件</b>:发送具有单独分辨率的图像,而无需更改全局设置。</p> <p><b>发送媒体文件:发送具有单独分辨率的图像,而无需更改全局设置。</p>
+<p><b>视频编辑器</b>:在发送前修剪视频。此外,视频转码过程已得到改进,现在可在后台工作。</p>
+<p><b>保存到图库</b>。在 Android 10 及以上版本中,由于谷歌提出了新的 \"范围存储 \"要求,媒体现在将存储在系统的<i>图片、视频、音乐</i>和<i>文件</i>文件夹中。</p>
+<p><b>全局搜索</b>:在所有聊天记录中搜索文本。只需在%1$s的主屏幕上点击<i>菜单/搜索聊天记录</i>即可。</p>。
+<p><b>引用</b>:%1$s现在允许您引用任何类型的媒体,包括图像、视频和语音消息。</p>
+<p><b>100个新表情</b>。检查期待已久的火锅&#129749;</p>。
+<p><b>大文本</b>:为了保持聊天界面的精简性,有大量文本的消息会以截断的聊天气泡的方式显示,并可根据需要展开。</p>]]></string>
     <string name="tooltip_identity_popup">点按此处可快速显示您的Threema ID或扫描其他人的ID</string>
     <string name="tap_to_start">点击此处立即开始%s。</string>
     <string name="two_years">2年</string>
@@ -1072,38 +1082,38 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="prefs_sum_show_unread_badge">在消息图标旁边显示一个标志,指示未读消息的数量</string>
     <string name="prefs_title_show_unread_badge">未读邮件徽章</string>
     <string name="pinning_not_trusted">证书固定失败。请检查是否在设备的凭据存储中安装并激活了“委托根证书颁发机构-G2”。</string>
-    <string name="pinning_failed">证书固定失败。可能发生中间人攻击。如果您安装了广告拦截器,内容过滤器或防火墙应用程序,例如《 AdGuard》,请在Threema上将其禁用。</string>
+    <string name="pinning_failed">证书固定失败。可能发生中间人攻击。如果您安装了广告拦截器,内容过滤器或防火墙应用程序,例如《 AdGuard》,请禁用后重开Threema。</string>
     <string name="open_myid_popup">打开快速访问弹出窗口</string>
-    <string name="logo">标/滚动到顶部</string>
-    <string name="quote_subj_end">结束报价</string>
+    <string name="logo">标/滚动到顶部</string>
+    <string name="quote_subj_end">结束引用</string>
     <string name="quote_subj">引用</string>
     <string name="duration">持续时间</string>
     <string name="seconds">秒</string>
     <string name="minutes">分钟</string>
     <string name="and">和</string>
     <string name="edit_type_content_description">查看或编辑%s %s</string>
-    <string name="group">组</string>
+    <string name="group">组</string>
     <string name="send_location_privacy_policy_v4_0"><![CDATA[<p>我们的隐私权政策已更新,以反映以下更改: </p> <p> %1$s不再依赖Google Play和Google Maps提供地图和POI数据。</p>请在<a href="%2$s">此处</a>查看完整的隐私政策。]]></string>
-    <string name="play_services_not_installed_unable_to_use_push">未安装Play服务。无法切换到“推”。</string>
+    <string name="play_services_not_installed_unable_to_use_push">未安装Google Play服务。无法切换到“推”。</string>
     <string name="unable_to_get_current_location">无法确定当前位置。</string>
     <string name="lp_search_place_min_chars">请输入至少三个字符以搜索地点。</string>
     <string name="lp_search_place_no_matches">找不到匹配的地方。请修改您的查询。</string>
     <string name="wallpaper_default">默认墙纸</string>
     <string name="wallpaper_gallery">从图库中选择</string>
-    <string name="wallpaper_none">空背景</string>
+    <string name="wallpaper_none">空背景</string>
     <string name="wallpaper_threema">%s壁纸</string>
     <string name="message_id">讯息编号</string>
     <string name="mime_type">MIME类型</string>
     <string name="password_does_not_comply">密码不符合管理员设置的准则。</string>
-    <string name="audio_mute_due_to_focus_loss">由于失去焦点,音频已暂时静音</string>
-    <string name="restore_data_backup_explain">要恢复数据备份,请首先从“我的资料”屏幕中删除您的Threema ID。\n\n应用程序重新启动时,选择“从备份还原”,“其他还原选项”,“数据备份”,然后选择要还原的数据备份文件。</string>
+    <string name="audio_mute_due_to_focus_loss">由于其他应用正在播放音频,目前音频已暂时静音</string>
+    <string name="restore_data_backup_explain">要还原数据备份,请首先从“我的资料”屏幕中删除您的Threema ID。\n\n应用程序重新启动时,选择“从备份还原”,“其他还原选项”,“数据备份”,然后选择要还原的数据备份文件。</string>
     <string name="audio_focus_loss_complete">由于完全失去音频焦点,呼叫已断开。</string>
     <string name="tap_for_picture_hold_for_video">点按可观看图片,按住可观看视频</string>
     <string name="sending_media">发送媒体</string>
     <string name="permission_record_video_audio_required">要录制视频,请允许使用麦克风</string>
     <string name="media_files">档案</string>
     <string name="auto_download_limit_explain">大于%s的视频和文件将始终按需下载</string>
-    <string name="quoted_message_deleted">引用消息不再可用</string>
+    <string name="quoted_message_deleted">引用消息现时不再可用</string>
     <string name="searching">正在搜寻…</string>
     <string name="prefs_work_life_balance">请勿打扰</string>
     <string name="prefs_title_working_days">工作日</string>
@@ -1117,7 +1127,7 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="work_life_dnd_active">下班时间活跃</string>
     <string name="pencil">铅笔</string>
     <string name="warning">警告</string>
-    <string name="password_remember_warning">记住您在这里输入的内容!由于%s不会在服务器上保存任何密码,因此,如果您忘记了PIN或密码,我们将无济于事。</string>
+    <string name="password_remember_warning">记住您在这里输入的内容!由于%s不会在服务器上保存任何密码,因此,如果您忘记了PIN或密码,我们无法帮助您。</string>
     <string name="safe_backup_tap_to_restart">点击显示的系统通知以立即重新启动应用程序。如果看不到通知,请向下滑动手机的通知栏。</string>
     <string name="send_to_support">发送给Threema支持</string>
     <string name="menu_legal">法律</string>
@@ -1140,18 +1150,18 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="permission_camera_videocall_required">进行视频通话时,请允许使用摄像头</string>
     <string name="feedback">反馈</string>
     <string name="tooltip_voip_enable_speakerphone">按此处打开免提电话</string>
-    <string name="ballot_open">公开民意调查</string>
+    <string name="ballot_open">公开投票</string>
     <string name="translators">译者</string>
     <string name="credits">积分</string>
     <string name="translators_thanks">非常感谢我们的志愿者翻译</string>
-    <string name="ballot_window_hide">隐藏打开的民意调查</string>
-    <string name="ballot_window_show">显示公开民意测验</string>
+    <string name="ballot_window_hide">隐藏公开的投票</string>
+    <string name="ballot_window_show">显示公开的投票</string>
     <string name="tooltip_video_call">除了语音通话,Threema现在还提供端到端的加密视频通话</string>
     <string name="tooltip_voip_other_party_video_on">对方发起了视频通话。点此处也可以打开相机。</string>
     <string name="tooltip_voip_other_party_video_disabled">对方使用的应用程序版本不支持或不允许进行视频通话</string>
     <string name="video_calls_new">新增:视频通话</string>
     <string name="biometrics_not_enrolled">系统未注册任何生物识别信息</string>
-    <string name="biometrics_not_avilable">生物识别在系统上不可用。</string>
+    <string name="biometrics_not_avilable">生物识别在系统上不可用。</string>
     <string name="biometrics_no_permission">缺少访问生物识别数据或硬件的权限</string>
     <string name="verification_settings_desc">点是联系人验证级别的指示器。</string>
     <string name="verification_levels_title">验证级别</string>
@@ -1162,7 +1172,7 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="insert_datetime">插入日期和时间</string>
     <string name="prefs_sum_disable_smart_replies">禁止Android通知中的智能回复</string>
     <string name="prefs_title_disable_smart_replies">禁用智能回复</string>
-    <string name="url_warning_body_alt">您即将打开的链接的主机名可疑。\n\n这可能是试图欺骗您打开一个假装其他网站的尝试。\n\n您仍然要继续吗?</string>
+    <string name="url_warning_body_alt">您即将打开的链接的主机名可疑。\n\n这可能是试图欺骗您打开一个假装成其他网站的虛假网页。\n\n您仍然要继续吗?</string>
     <string name="read_on">继续阅读…</string>
     <string name="forward_text">转发文字</string>
     <string name="an_error_occurred_during_send">发送一个或多个消息时发生错误。</string>
@@ -1172,11 +1182,11 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="tooltip_image_labeling">要在图库中找到任何特定的视觉媒体,只需输入一个描述内容的术语,然后从已标识的标签列表中进行选择。</string>
     <string name="selected_media">您的选择</string>
     <string name="attach_gif">Gif</string>
-    <string name="attach_gallery">画廊</string>
+    <string name="attach_gallery">图库</string>
     <string name="attach_picture">图片</string>
     <string name="attach_video">视频</string>
     <string name="attach_location">位置</string>
-    <string name="image_labeling_toolbar_title">过滤</string>
+    <string name="image_labeling_toolbar_title">过滤方式</string>
     <string name="no_labels_info">找不到匹配项</string>
     <string name="image_label_query_hint">对象,位置,活动?</string>
     <string name="filter_by_album">按相册过滤</string>
@@ -1196,13 +1206,13 @@ Threema ID。您不会出现在朋友的联系人列表中。您真的要
     <string name="media_gallery_gifs">GIF</string>
     <string name="notification_channel_image_labeling">图像索引进度</string>
     <string name="notification_channel_image_labeling_desc">公共图库图片在后台进行索引中</string>
-    <string name="notification_image_labeling_desc">媒体库索引中</string>
+    <string name="notification_image_labeling_desc">媒体库正在索引中</string>
     <string name="no_media_found_global">此设备上找不到媒体</string>
     <string name="prefs_sum_image_labeling">在您的图库中启用关键字搜索公共图片</string>
     <string name="prefs_image_labeling">图片搜索</string>
     <string name="enable_formatting">启用格式化</string>
     <string name="original_file_no_longer_avilable">原文件已无法访问,请重新发送信息。</string>
-    <string name="state_transcoding">转码中</string>
+    <string name="state_transcoding">正在转码中</string>
     <string name="importing_files">导入文件</string>
     <string name="tooltip_image_resolution_hint">调整图像分辨率。</string>
     <string name="image_labeling_stuck_error">图像搜索的索引过程遇到问題,已取消。请稍后重试。</string>

+ 23 - 23
app/src/main/res/values-zh-rCN/webclient_strings.xml

@@ -2,41 +2,41 @@
 <resources>
     <string name="webclient_qr_scan_error">无效的QR码数据</string>
     <string name="webclient_init_session">发起新的会话</string>
-    <string name="webclient_sessions_really_delete">您真的要删除Threema Web会话吗?</string>
+    <string name="webclient_sessions_really_delete">您真的要删除Threema 网页版会话吗?</string>
     <string name="webclient_last_usage">上次使用:%s</string>
     <string name="webclient_created_at">创建于: %1$s (%2$s)</string>
     <string name="webclient_active_since">活动日期:%s</string>
-    <string name="webclient_enable">启动 Threema Web</string>
+    <string name="webclient_enable">启动 Threema 网页版</string>
     <string name="webclient_no_sessions_found">要进行连接,请在PC浏览器中打开<b> https://web.threema.ch </b>,然后点击下面的按钮以扫描代码</string>
     <string name="webclient_session_rename">重命名会话</string>
     <string name="webclient_session_label">新名字</string>
-    <string name="webclient_session_start">开始会</string>
-    <string name="webclient_session_stop">停止会</string>
-    <string name="webclient_persistent">持久的</string>
-    <string name="webclient_disposable">一次性</string>
+    <string name="webclient_session_start">开始会</string>
+    <string name="webclient_session_stop">停止会</string>
+    <string name="webclient_persistent">持</string>
+    <string name="webclient_disposable">一次性</string>
     <string name="webclient_unnamed_session">未命名的会话</string>
     <string name="webclient_session_remove">删除会话</string>
     <string name="webclient_welcome_title">从您的PC聊天!</string>
-    <string name="webclient_welcome_explain">Threema Web允许您从PC或笔记本电脑聊天,同时拥有对所有联系人,媒体和聊天甚至过去对话的完全访问权限。\n\n<b>手机和PC之间的所有通信都是完全端到端加密的</b>,如果手机和PC都在同一网络上,则使用直接连接。\n\n请注意:Threema Web处于活动状态时,可能会导致电池消耗更多。您可以随时启用或禁用它。</string>
-    <string name="webclient_launch">立即启动Threema Web</string>
+    <string name="webclient_welcome_explain">Threema 网页版允许您从PC或笔记本电脑聊天,同时拥有对所有联系人,媒体和聊天甚至过去对话的完全访问权限。\n\n<b>手机和PC之间的所有通信都是完全端到端加密的</b>,如果手机和PC都在同一网络上,则使用直接连接。\n\n请注意:Threema 网页版处于活动状态时,可能会导致电池消耗更多。您可以随时启用或禁用它。</string>
+    <string name="webclient_launch">立即启动Threema 网页版</string>
     <string name="webclient_qr_scan_message">请扫描PC上显示的QR码。</string>
-    <string name="webclient_invalid_qr_code">无效的Threema Web QR码</string>
-    <string name="webclient_new_connection_toast">Threema网络会议开始</string>
+    <string name="webclient_invalid_qr_code">无效的Threema 网页版 QR码</string>
+    <string name="webclient_new_connection_toast">Threema 网页版会话开始</string>
     <string name="webclient_protocol_error">协议错误</string>
-    <string name="webclient_protocol_version_to_old">您的应用程序不支持此版本的Threema Web。请更新Threema为最新版本。</string>
-    <string name="webclient_protocol_version_too_new_selfhosted">您的应用程序不支持此版本的Threema Web。请要求您的Threema Web实例的管理员升级到最新版本。</string>
-    <string name="webclient_protocol_version_too_new_threema">您的应用程序不支持此版本的Threema Web。请使用较新版本的Threema Web。</string>
-    <string name="webclient_session_already_exists">您扫描的QR码会话已经存在。请在您的Web浏览器中重新加载Threema Web,然后重试。</string>
-    <string name="webclient_really_start_webclient_by_payload_body">您想立即开始这个Threema Web会话吗?</string>
-    <string name="webclient_cannot_restore">无法还原Threema Web会话</string>
-    <string name="webclient_disabled">Threema Web未启用</string>
-    <string name="webclient_cannot_start">无法启动Threema Web会话</string>
+    <string name="webclient_protocol_version_to_old">您的应用程序不支持此版本的Threema 网页版。请更新Threema为最新版本。</string>
+    <string name="webclient_protocol_version_too_new_selfhosted">您的应用程序不支持此版本的Threema 网页版。请要求您的Threema Web实例的管理员升级到最新版本。</string>
+    <string name="webclient_protocol_version_too_new_threema">您的应用程序不支持此版本的Threema 网页版。请使用较新版本的Threema 网页版。</string>
+    <string name="webclient_session_already_exists">您扫描的QR码会话已经存在。请在您的Web浏览器中重新加载Threema 网页版,然后重试。</string>
+    <string name="webclient_really_start_webclient_by_payload_body">您想立即开始这个Threema 网页版会话吗?</string>
+    <string name="webclient_cannot_restore">无法还原Threema 网页版会话</string>
+    <string name="webclient_disabled">Threema 网页版未启用</string>
+    <string name="webclient_cannot_start">无法启动Threema 网页版会话</string>
     <string name="webclient_constrained_by_mdm">服务器未经管理员批准。</string>
     <string name="webclient_clear_all_sessions">清除所有会话</string>
-    <string name="webclient_clear_all_sessions_confirm">您确定要停止并删除所有Threema Web会话吗?</string>
-    <string name="webclient_prefs_debug_tool_summary">启动此工具调试与设置Threema Web连接有关的问题</string>
-    <string name="webclient_diagnostics">Threema Web诊断</string>
+    <string name="webclient_clear_all_sessions_confirm">您确定要停止并删除所有Threema 网页版会话吗?</string>
+    <string name="webclient_prefs_debug_tool_summary">启动此工具调试与设置Threema 网页版连接有关的问题</string>
+    <string name="webclient_diagnostics">Threema 网页版诊断</string>
     <string name="webclient_diagnostics_start">开始</string>
-    <string name="webclient_diagnostics_intro">点击《开始》启动测试。</string>
-    <string name="webclient_diagnostics_done">做完了 如果您遇到Threema Web连接建立问题,请将此日志发送给Threema支持。</string>
+    <string name="webclient_diagnostics_intro">点击\"开始\"启动测试。</string>
+    <string name="webclient_diagnostics_done">大功告成。如果您遇到Threema 网页版连接建立问题,请将此日志发送给Threema 支持。</string>
 </resources>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1224,4 +1224,6 @@
 	<string name="image_labeling_stuck_error">The indexing process for image search got stuck and was cancelled. Please retry later.</string>
 	<string name="ballot_created_successfully">The poll was successfully created.</string>
 	<string name="file_size">File size</string>
+	<!-- Abbreviation for "30 days", shown at the bottom of the contact list -->
+	<string name="thirty_days_abbrev">30d</string>
 </resources>

+ 1 - 1
app/src/main/res/values/styles.xml

@@ -710,7 +710,7 @@
 	</style>
 
 	<style name="Threema.PopupMenuStyle" parent="@style/Widget.AppCompat.PopupMenu">
-		<item name="dropdownListPreferredItemHeight">34dp</item>
+		<item name="dropdownListPreferredItemHeight">38dp</item>
 		<item name="android:paddingLeft">0dp</item>
 		<item name="android:paddingRight">-4dp</item>
 		<item name="android:layout_marginRight">0dp</item>