From 00d7b5c2847e8ebd3884f8fb00201789edbdd127 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 15 Jan 2015 13:35:35 -0800 Subject: [PATCH] Better UX handling on identity key mismatches. 1) Migrate from GSON to Jackson everywhere. 2) Add support for storing identity key conflicts on message rows. 3) Add limited support for surfacing identity key conflicts in UI. --- AndroidManifest.xml | 10 +- build.gradle | 42 +-- libtextsecure/build.gradle | 6 +- .../api/TextSecureMessageSender.java | 10 +- .../api/push/SignedPreKeyEntity.java | 64 ++--- .../exceptions/EncapsulatedExceptions.java | 9 +- .../exceptions/NetworkFailureException.java | 15 ++ .../push/exceptions/PushNetworkException.java | 2 + .../internal/push/AccountAttributes.java | 7 + .../textsecure/internal/push/DeviceCode.java | 3 + .../internal/push/MismatchedDevices.java | 4 + .../internal/push/OutgoingPushMessage.java | 6 + .../push/OutgoingPushMessageList.java | 6 + .../internal/push/PreKeyEntity.java | 56 ++-- .../internal/push/PreKeyResponse.java | 57 ++-- .../internal/push/PreKeyResponseItem.java | 12 +- .../textsecure/internal/push/PreKeyState.java | 20 +- .../internal/push/PreKeyStatus.java | 3 + .../internal/push/PushServiceSocket.java | 60 +++-- .../internal/push/StaleDevices.java | 3 + .../textsecure/internal/util/JsonUtil.java | 75 ++++++ res/drawable-xxhdpi/ic_error_red_24dp.png | Bin 0 -> 974 bytes res/drawable-xxhdpi/ic_error_white_18dp.png | Bin 0 -> 659 bytes .../ic_info_outline_grey600_24dp.png | Bin 0 -> 1120 bytes res/drawable-xxhdpi/ic_refresh_white_18dp.png | Bin 0 -> 693 bytes res/drawable/error_round.xml | 16 ++ res/drawable/info_round.xml | 16 ++ res/layout/conversation_item_received.xml | 6 +- res/layout/conversation_item_sent.xml | 23 +- res/layout/message_details_activity.xml | 24 ++ res/layout/message_details_header.xml | 76 ++++++ res/layout/message_details_recipient.xml | 87 ++++++ res/values/dimens.xml | 1 + res/values/strings.xml | 29 +- res/values/styles.xml | 15 ++ .../securesms/ApplicationContext.java | 2 - .../securesms/ConfirmIdentityDialog.java | 216 +++++++++++++++ .../securesms/ConversationAdapter.java | 6 +- .../securesms/ConversationFragment.java | 60 +---- .../securesms/ConversationItem.java | 90 ++++--- .../securesms/ConversationListItem.java | 73 +---- .../securesms/MessageDetailsActivity.java | 255 ++++++++++++++++++ .../MessageDetailsRecipientAdapter.java | 58 ++++ .../securesms/MessageRecipientListItem.java | 177 ++++++++++++ .../securesms/ReceiveKeyActivity.java | 2 +- .../thoughtcrime/securesms/ShareListItem.java | 52 +--- .../securesms/crypto/PreKeyUtil.java | 17 +- .../securesms/database/Database.java | 6 +- .../securesms/database/DatabaseFactory.java | 41 +-- .../securesms/database/MessagingDatabase.java | 145 ++++++++++ .../database/MmsAddressDatabase.java | 26 ++ .../securesms/database/MmsDatabase.java | 84 +++++- .../securesms/database/MmsSmsColumns.java | 2 + .../securesms/database/MmsSmsDatabase.java | 59 +++- .../securesms/database/SmsDatabase.java | 72 +++-- .../database/documents/Document.java | 10 + .../documents/IdentityKeyMismatch.java | 85 ++++++ .../documents/IdentityKeyMismatchList.java | 31 +++ .../database/documents/NetworkFailure.java | 32 +++ .../documents/NetworkFailureList.java | 33 +++ .../loaders/MessageDetailsLoader.java | 47 ++++ .../database/model/MediaMmsMessageRecord.java | 10 +- .../database/model/MessageRecord.java | 41 ++- .../model/NotificationMmsMessageRecord.java | 7 +- .../database/model/SmsMessageRecord.java | 16 +- .../securesms/jobs/PushDecryptJob.java | 205 +++++++++----- .../securesms/jobs/PushGroupSendJob.java | 56 +++- .../securesms/jobs/PushMediaSendJob.java | 17 +- .../securesms/jobs/PushTextSendJob.java | 23 +- .../securesms/recipients/Recipients.java | 7 +- .../securesms/sms/MessageSender.java | 16 +- .../securesms/util/DateUtils.java | 17 ++ .../thoughtcrime/securesms/util/Emoji.java | 32 ++- .../securesms/util/JsonUtils.java | 33 +++ .../securesms/util/RecipientViewUtil.java | 83 ++++++ strip_play_services.gradle | 109 -------- 76 files changed, 2395 insertions(+), 721 deletions(-) create mode 100644 libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/NetworkFailureException.java create mode 100644 libtextsecure/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java create mode 100644 res/drawable-xxhdpi/ic_error_red_24dp.png create mode 100644 res/drawable-xxhdpi/ic_error_white_18dp.png create mode 100644 res/drawable-xxhdpi/ic_info_outline_grey600_24dp.png create mode 100644 res/drawable-xxhdpi/ic_refresh_white_18dp.png create mode 100644 res/drawable/error_round.xml create mode 100644 res/drawable/info_round.xml create mode 100644 res/layout/message_details_activity.xml create mode 100644 res/layout/message_details_header.xml create mode 100644 res/layout/message_details_recipient.xml create mode 100644 src/org/thoughtcrime/securesms/ConfirmIdentityDialog.java create mode 100644 src/org/thoughtcrime/securesms/MessageDetailsActivity.java create mode 100644 src/org/thoughtcrime/securesms/MessageDetailsRecipientAdapter.java create mode 100644 src/org/thoughtcrime/securesms/MessageRecipientListItem.java create mode 100644 src/org/thoughtcrime/securesms/database/MessagingDatabase.java create mode 100644 src/org/thoughtcrime/securesms/database/documents/Document.java create mode 100644 src/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatch.java create mode 100644 src/org/thoughtcrime/securesms/database/documents/IdentityKeyMismatchList.java create mode 100644 src/org/thoughtcrime/securesms/database/documents/NetworkFailure.java create mode 100644 src/org/thoughtcrime/securesms/database/documents/NetworkFailureList.java create mode 100644 src/org/thoughtcrime/securesms/database/loaders/MessageDetailsLoader.java create mode 100644 src/org/thoughtcrime/securesms/util/JsonUtils.java create mode 100644 src/org/thoughtcrime/securesms/util/RecipientViewUtil.java delete mode 100644 strip_play_services.gradle diff --git a/AndroidManifest.xml b/AndroidManifest.xml index bcc0dfd8f..615d007ef 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -125,10 +125,16 @@ android:launchMode="singleTask" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> - + + untrustedIdentities = new LinkedList<>(); List unregisteredUsers = new LinkedList<>(); + List networkExceptions = new LinkedList<>(); for (PushAddress recipient : recipients) { try { @@ -186,11 +189,14 @@ public class TextSecureMessageSender { } catch (UnregisteredUserException e) { Log.w(TAG, e); unregisteredUsers.add(e); + } catch (PushNetworkException e) { + Log.w(TAG, e); + networkExceptions.add(new NetworkFailureException(recipient.getNumber(), e)); } } - if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty()) { - throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers); + if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { + throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); } } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/SignedPreKeyEntity.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/SignedPreKeyEntity.java index 56077f863..663568a84 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/SignedPreKeyEntity.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/SignedPreKeyEntity.java @@ -16,24 +16,28 @@ */ package org.whispersystems.textsecure.api.push; -import com.google.thoughtcrimegson.GsonBuilder; -import com.google.thoughtcrimegson.JsonDeserializationContext; -import com.google.thoughtcrimegson.JsonDeserializer; -import com.google.thoughtcrimegson.JsonElement; -import com.google.thoughtcrimegson.JsonParseException; -import com.google.thoughtcrimegson.JsonPrimitive; -import com.google.thoughtcrimegson.JsonSerializationContext; -import com.google.thoughtcrimegson.JsonSerializer; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.libaxolotl.ecc.ECPublicKey; -import org.whispersystems.textsecure.internal.util.Base64; import org.whispersystems.textsecure.internal.push.PreKeyEntity; +import org.whispersystems.textsecure.internal.util.Base64; import java.io.IOException; -import java.lang.reflect.Type; public class SignedPreKeyEntity extends PreKeyEntity { + @JsonProperty + @JsonSerialize(using = ByteArraySerializer.class) + @JsonDeserialize(using = ByteArrayDeserializer.class) private byte[] signature; public SignedPreKeyEntity() {} @@ -47,42 +51,18 @@ public class SignedPreKeyEntity extends PreKeyEntity { return signature; } - public static String toJson(SignedPreKeyEntity entity) { - GsonBuilder builder = new GsonBuilder(); - return forBuilder(builder).create().toJson(entity); - } - - public static SignedPreKeyEntity fromJson(String serialized) { - GsonBuilder builder = new GsonBuilder(); - return forBuilder(builder).create().fromJson(serialized, SignedPreKeyEntity.class); - } - - public static GsonBuilder forBuilder(GsonBuilder builder) { - return PreKeyEntity.forBuilder(builder) - .registerTypeAdapter(byte[].class, new ByteArrayJsonAdapter()); - - } - - private static class ByteArrayJsonAdapter - implements JsonSerializer, JsonDeserializer - { + private static class ByteArraySerializer extends JsonSerializer { @Override - public JsonElement serialize(byte[] signature, Type type, - JsonSerializationContext jsonSerializationContext) - { - return new JsonPrimitive(Base64.encodeBytesWithoutPadding(signature)); + public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Base64.encodeBytesWithoutPadding(value)); } + } + + private static class ByteArrayDeserializer extends JsonDeserializer { @Override - public byte[] deserialize(JsonElement jsonElement, Type type, - JsonDeserializationContext jsonDeserializationContext) - throws JsonParseException - { - try { - return Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()); - } catch (IOException e) { - throw new JsonParseException(e); - } + public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return Base64.decodeWithoutPadding(p.getValueAsString()); } } } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/EncapsulatedExceptions.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/EncapsulatedExceptions.java index 0617b59a5..dc0f15030 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/EncapsulatedExceptions.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/EncapsulatedExceptions.java @@ -24,12 +24,15 @@ public class EncapsulatedExceptions extends Throwable { private final List untrustedIdentityExceptions; private final List unregisteredUserExceptions; + private final List networkExceptions; public EncapsulatedExceptions(List untrustedIdentities, - List unregisteredUsers) + List unregisteredUsers, + List networkExceptions) { this.untrustedIdentityExceptions = untrustedIdentities; this.unregisteredUserExceptions = unregisteredUsers; + this.networkExceptions = networkExceptions; } public List getUntrustedIdentityExceptions() { @@ -39,4 +42,8 @@ public class EncapsulatedExceptions extends Throwable { public List getUnregisteredUserExceptions() { return unregisteredUserExceptions; } + + public List getNetworkExceptions() { + return networkExceptions; + } } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/NetworkFailureException.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/NetworkFailureException.java new file mode 100644 index 000000000..bc9b42c5b --- /dev/null +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/NetworkFailureException.java @@ -0,0 +1,15 @@ +package org.whispersystems.textsecure.api.push.exceptions; + +public class NetworkFailureException extends Exception { + + private final String e164number; + + public NetworkFailureException(String e164number, Exception nested) { + super(nested); + this.e164number = e164number; + } + + public String getE164number() { + return e164number; + } +} diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/PushNetworkException.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/PushNetworkException.java index 0c7713c59..f875ba900 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/PushNetworkException.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/api/push/exceptions/PushNetworkException.java @@ -19,6 +19,7 @@ package org.whispersystems.textsecure.api.push.exceptions; import java.io.IOException; public class PushNetworkException extends IOException { + public PushNetworkException(Exception exception) { super(exception); } @@ -26,4 +27,5 @@ public class PushNetworkException extends IOException { public PushNetworkException(String s) { super(s); } + } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java index 88e72d01b..5d32b4837 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/AccountAttributes.java @@ -16,10 +16,17 @@ */ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + public class AccountAttributes { + @JsonProperty private String signalingKey; + + @JsonProperty private boolean supportsSms; + + @JsonProperty private int registrationId; public AccountAttributes(String signalingKey, boolean supportsSms, int registrationId) { diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/DeviceCode.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/DeviceCode.java index d2fc463ad..fcc14df84 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/DeviceCode.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/DeviceCode.java @@ -1,7 +1,10 @@ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + public class DeviceCode { + @JsonProperty private String verificationCode; public String getVerificationCode() { diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/MismatchedDevices.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/MismatchedDevices.java index 0f44b7c8c..5a459a378 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/MismatchedDevices.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/MismatchedDevices.java @@ -16,11 +16,15 @@ */ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public class MismatchedDevices { + @JsonProperty private List missingDevices; + @JsonProperty private List extraDevices; public List getMissingDevices() { diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java index 434de88ca..845399a6d 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessage.java @@ -17,14 +17,20 @@ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + import org.whispersystems.textsecure.api.push.PushAddress; import org.whispersystems.textsecure.internal.util.Base64; public class OutgoingPushMessage { + @JsonProperty private int type; + @JsonProperty private int destinationDeviceId; + @JsonProperty private int destinationRegistrationId; + @JsonProperty private String body; public OutgoingPushMessage(PushAddress address, int deviceId, PushBody body) { diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessageList.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessageList.java index 7adcb1ddc..6fa26eab0 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessageList.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/OutgoingPushMessageList.java @@ -16,16 +16,22 @@ */ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public class OutgoingPushMessageList { + @JsonProperty private String destination; + @JsonProperty private String relay; + @JsonProperty private long timestamp; + @JsonProperty private List messages; public OutgoingPushMessageList(String destination, long timestamp, String relay, diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyEntity.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyEntity.java index ae67da9f3..7215a1eb5 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyEntity.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyEntity.java @@ -16,14 +16,15 @@ */ package org.whispersystems.textsecure.internal.push; -import com.google.thoughtcrimegson.GsonBuilder; -import com.google.thoughtcrimegson.JsonDeserializationContext; -import com.google.thoughtcrimegson.JsonDeserializer; -import com.google.thoughtcrimegson.JsonElement; -import com.google.thoughtcrimegson.JsonParseException; -import com.google.thoughtcrimegson.JsonPrimitive; -import com.google.thoughtcrimegson.JsonSerializationContext; -import com.google.thoughtcrimegson.JsonSerializer; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.ecc.Curve; @@ -31,11 +32,15 @@ import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.textsecure.internal.util.Base64; import java.io.IOException; -import java.lang.reflect.Type; public class PreKeyEntity { - private int keyId; + @JsonProperty + private int keyId; + + @JsonProperty + @JsonSerialize(using = ECPublicKeySerializer.class) + @JsonDeserialize(using = ECPublicKeyDeserializer.class) private ECPublicKey publicKey; public PreKeyEntity() {} @@ -53,32 +58,21 @@ public class PreKeyEntity { return publicKey; } - public static GsonBuilder forBuilder(GsonBuilder builder) { - return builder.registerTypeAdapter(ECPublicKey.class, new ECPublicKeyJsonAdapter()); + private static class ECPublicKeySerializer extends JsonSerializer { + @Override + public void serialize(ECPublicKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize())); + } } - - private static class ECPublicKeyJsonAdapter - implements JsonSerializer, JsonDeserializer - { + private static class ECPublicKeyDeserializer extends JsonDeserializer { @Override - public JsonElement serialize(ECPublicKey preKeyPublic, Type type, - JsonSerializationContext jsonSerializationContext) - { - return new JsonPrimitive(Base64.encodeBytesWithoutPadding(preKeyPublic.serialize())); - } - - @Override - public ECPublicKey deserialize(JsonElement jsonElement, Type type, - JsonDeserializationContext jsonDeserializationContext) - throws JsonParseException - { + public ECPublicKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { try { - return Curve.decodePoint(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0); - } catch (InvalidKeyException | IOException e) { - throw new JsonParseException(e); + return Curve.decodePoint(Base64.decodeWithoutPadding(p.getValueAsString()), 0); + } catch (InvalidKeyException e) { + throw new IOException(e); } } } - } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponse.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponse.java index 7b50d4ea8..1db05f80a 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponse.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponse.java @@ -16,26 +16,32 @@ */ package org.whispersystems.textsecure.internal.push; -import com.google.thoughtcrimegson.GsonBuilder; -import com.google.thoughtcrimegson.JsonDeserializationContext; -import com.google.thoughtcrimegson.JsonDeserializer; -import com.google.thoughtcrimegson.JsonElement; -import com.google.thoughtcrimegson.JsonParseException; -import com.google.thoughtcrimegson.JsonPrimitive; -import com.google.thoughtcrimegson.JsonSerializationContext; -import com.google.thoughtcrimegson.JsonSerializer; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.textsecure.internal.util.Base64; +import org.whispersystems.textsecure.internal.util.JsonUtil; import java.io.IOException; -import java.lang.reflect.Type; import java.util.List; public class PreKeyResponse { - private IdentityKey identityKey; + @JsonProperty + @JsonSerialize(using = JsonUtil.IdentityKeySerializer.class) + @JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class) + private IdentityKey identityKey; + + @JsonProperty private List devices; public IdentityKey getIdentityKey() { @@ -46,36 +52,5 @@ public class PreKeyResponse { return devices; } - public static PreKeyResponse fromJson(String serialized) { - GsonBuilder builder = new GsonBuilder(); - return PreKeyResponseItem.forBuilder(builder) - .registerTypeAdapter(IdentityKey.class, new IdentityKeyJsonAdapter()) - .create().fromJson(serialized, PreKeyResponse.class); - } - - public static class IdentityKeyJsonAdapter - implements JsonSerializer, JsonDeserializer - { - @Override - public JsonElement serialize(IdentityKey identityKey, Type type, - JsonSerializationContext jsonSerializationContext) - { - return new JsonPrimitive(Base64.encodeBytesWithoutPadding(identityKey.serialize())); - } - - @Override - public IdentityKey deserialize(JsonElement jsonElement, Type type, - JsonDeserializationContext jsonDeserializationContext) - throws JsonParseException - { - try { - return new IdentityKey(Base64.decodeWithoutPadding(jsonElement.getAsJsonPrimitive().getAsString()), 0); - } catch (InvalidKeyException | IOException e) { - throw new JsonParseException(e); - } - } - } - - } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponseItem.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponseItem.java index 9400a2e09..6a5fe32ae 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponseItem.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyResponseItem.java @@ -16,15 +16,22 @@ */ package org.whispersystems.textsecure.internal.push; -import com.google.thoughtcrimegson.GsonBuilder; +import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.textsecure.api.push.SignedPreKeyEntity; public class PreKeyResponseItem { + @JsonProperty private int deviceId; + + @JsonProperty private int registrationId; + + @JsonProperty private SignedPreKeyEntity signedPreKey; + + @JsonProperty private PreKeyEntity preKey; public int getDeviceId() { @@ -43,7 +50,4 @@ public class PreKeyResponseItem { return preKey; } - public static GsonBuilder forBuilder(GsonBuilder builder) { - return SignedPreKeyEntity.forBuilder(builder); - } } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyState.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyState.java index cb19cb9c9..d3d88a9ae 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyState.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyState.java @@ -1,17 +1,29 @@ package org.whispersystems.textsecure.internal.push; -import com.google.thoughtcrimegson.GsonBuilder; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.textsecure.api.push.SignedPreKeyEntity; +import org.whispersystems.textsecure.internal.util.JsonUtil; import java.util.List; public class PreKeyState { + @JsonProperty + @JsonSerialize(using = JsonUtil.IdentityKeySerializer.class) + @JsonDeserialize(using = JsonUtil.IdentityKeyDeserializer.class) private IdentityKey identityKey; + + @JsonProperty private List preKeys; + + @JsonProperty private PreKeyEntity lastResortKey; + + @JsonProperty private SignedPreKeyEntity signedPreKey; @@ -24,10 +36,4 @@ public class PreKeyState { this.identityKey = identityKey; } - public static String toJson(PreKeyState state) { - GsonBuilder builder = new GsonBuilder(); - return SignedPreKeyEntity.forBuilder(builder) - .registerTypeAdapter(IdentityKey.class, new PreKeyResponse.IdentityKeyJsonAdapter()) - .create().toJson(state); - } } diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyStatus.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyStatus.java index 652f1ca41..cd53f7181 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyStatus.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PreKeyStatus.java @@ -16,8 +16,11 @@ */ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + public class PreKeyStatus { + @JsonProperty private int count; public PreKeyStatus() {} diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java index b303e9941..942f788f1 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/PushServiceSocket.java @@ -18,8 +18,7 @@ package org.whispersystems.textsecure.internal.push; import android.util.Log; -import com.google.thoughtcrimegson.Gson; -import com.google.thoughtcrimegson.JsonParseException; +import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.whispersystems.libaxolotl.IdentityKey; @@ -44,6 +43,7 @@ import org.whispersystems.textsecure.internal.push.exceptions.MismatchedDevicesE import org.whispersystems.textsecure.internal.push.exceptions.StaleDevicesException; import org.whispersystems.textsecure.internal.util.Base64; import org.whispersystems.textsecure.internal.util.BlacklistingTrustManager; +import org.whispersystems.textsecure.internal.util.JsonUtil; import org.whispersystems.textsecure.internal.util.Util; import java.io.File; @@ -115,17 +115,17 @@ public class PushServiceSocket { { AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId); makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode), - "PUT", new Gson().toJson(signalingKeyEntity)); + "PUT", JsonUtil.toJson(signalingKeyEntity)); } public String getNewDeviceVerificationCode() throws IOException { String responseText = makeRequest(PROVISIONING_CODE_PATH, "GET", null); - return new Gson().fromJson(responseText, DeviceCode.class).getVerificationCode(); + return JsonUtil.fromJson(responseText, DeviceCode.class).getVerificationCode(); } public void sendProvisioningMessage(String destination, byte[] body) throws IOException { makeRequest(String.format(PROVISIONING_MESSAGE_PATH, destination), "PUT", - new Gson().toJson(new ProvisioningMessage(Base64.encodeBytes(body)))); + JsonUtil.toJson(new ProvisioningMessage(Base64.encodeBytes(body)))); } public void sendReceipt(String destination, long messageId, String relay) throws IOException { @@ -140,7 +140,7 @@ public class PushServiceSocket { public void registerGcmId(String gcmRegistrationId) throws IOException { GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId, true); - makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration)); + makeRequest(REGISTER_GCM_PATH, "PUT", JsonUtil.toJson(registration)); } public void unregisterGcmId() throws IOException { @@ -151,11 +151,10 @@ public class PushServiceSocket { throws IOException { try { - String responseText = makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle)); + String responseText = makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle)); if (responseText == null) return new SendMessageResponse(false); - else return new Gson().fromJson(responseText, SendMessageResponse.class); - + else return JsonUtil.fromJson(responseText, SendMessageResponse.class); } catch (NotFoundException nfe) { throw new UnregisteredUserException(bundle.getDestination(), nfe); } @@ -184,13 +183,13 @@ public class PushServiceSocket { signedPreKey.getSignature()); makeRequest(String.format(PREKEY_PATH, ""), "PUT", - PreKeyState.toJson(new PreKeyState(entities, lastResortEntity, - signedPreKeyEntity, identityKey))); + JsonUtil.toJson(new PreKeyState(entities, lastResortEntity, + signedPreKeyEntity, identityKey))); } public int getAvailablePreKeys() throws IOException { String responseText = makeRequest(PREKEY_METADATA_PATH, "GET", null); - PreKeyStatus preKeyStatus = new Gson().fromJson(responseText, PreKeyStatus.class); + PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class); return preKeyStatus.getCount(); } @@ -209,7 +208,7 @@ public class PushServiceSocket { } String responseText = makeRequest(path, "GET", null); - PreKeyResponse response = PreKeyResponse.fromJson(responseText); + PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class); List bundles = new LinkedList<>(); for (PreKeyResponseItem device : response.getDevices()) { @@ -236,7 +235,7 @@ public class PushServiceSocket { } return bundles; - } catch (JsonParseException e) { + } catch (JsonUtil.JsonParseException e) { throw new IOException(e); } catch (NotFoundException nfe) { throw new UnregisteredUserException(destination.getNumber(), nfe); @@ -253,7 +252,7 @@ public class PushServiceSocket { } String responseText = makeRequest(path, "GET", null); - PreKeyResponse response = PreKeyResponse.fromJson(responseText); + PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class); if (response.getDevices() == null || response.getDevices().size() < 1) throw new IOException("Empty prekey list"); @@ -278,7 +277,7 @@ public class PushServiceSocket { return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey, signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey()); - } catch (JsonParseException e) { + } catch (JsonUtil.JsonParseException e) { throw new IOException(e); } catch (NotFoundException nfe) { throw new UnregisteredUserException(destination.getNumber(), nfe); @@ -288,7 +287,7 @@ public class PushServiceSocket { public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException { try { String responseText = makeRequest(SIGNED_PREKEY_PATH, "GET", null); - return SignedPreKeyEntity.fromJson(responseText); + return JsonUtil.fromJson(responseText, SignedPreKeyEntity.class); } catch (NotFoundException e) { Log.w("PushServiceSocket", e); return null; @@ -299,12 +298,12 @@ public class PushServiceSocket { SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), signedPreKey.getKeyPair().getPublicKey(), signedPreKey.getSignature()); - makeRequest(SIGNED_PREKEY_PATH, "PUT", SignedPreKeyEntity.toJson(signedPreKeyEntity)); + makeRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity)); } public long sendAttachment(PushAttachmentData attachment) throws IOException { String response = makeRequest(String.format(ATTACHMENT_PATH, ""), "GET", null); - AttachmentDescriptor attachmentKey = new Gson().fromJson(response, AttachmentDescriptor.class); + AttachmentDescriptor attachmentKey = JsonUtil.fromJson(response, AttachmentDescriptor.class); if (attachmentKey == null || attachmentKey.getLocation() == null) { throw new IOException("Server failed to allocate an attachment key!"); @@ -326,7 +325,7 @@ public class PushServiceSocket { } String response = makeRequest(path, "GET", null); - AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class); + AttachmentDescriptor descriptor = JsonUtil.fromJson(response, AttachmentDescriptor.class); Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation()); @@ -337,8 +336,8 @@ public class PushServiceSocket { throws NonSuccessfulResponseCodeException, PushNetworkException { ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens)); - String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", new Gson().toJson(contactTokenList)); - ContactTokenDetailsList activeTokens = new Gson().fromJson(response, ContactTokenDetailsList.class); + String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList)); + ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class); return activeTokens.getContacts(); } @@ -346,7 +345,7 @@ public class PushServiceSocket { public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException { try { String response = makeRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null); - return new Gson().fromJson(response, ContactTokenDetails.class); + return JsonUtil.fromJson(response, ContactTokenDetails.class); } catch (NotFoundException nfe) { return null; } @@ -463,14 +462,14 @@ public class PushServiceSocket { } catch (IOException e) { throw new PushNetworkException(e); } - throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class)); + throw new MismatchedDevicesException(JsonUtil.fromJson(response, MismatchedDevices.class)); case 410: try { response = Util.readFully(connection.getErrorStream()); } catch (IOException e) { throw new PushNetworkException(e); } - throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class)); + throw new StaleDevicesException(JsonUtil.fromJson(response, StaleDevices.class)); case 417: throw new ExpectationFailedException(); } @@ -524,9 +523,7 @@ public class PushServiceSocket { return connection; } catch (IOException e) { throw new PushNetworkException(e); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (KeyManagementException e) { + } catch (NoSuchAlgorithmException | KeyManagementException e) { throw new AssertionError(e); } } @@ -540,7 +537,11 @@ public class PushServiceSocket { } private static class GcmRegistrationId { + + @JsonProperty private String gcmRegistrationId; + + @JsonProperty private boolean webSocketChannel; public GcmRegistrationId() {} @@ -552,7 +553,10 @@ public class PushServiceSocket { } private static class AttachmentDescriptor { + @JsonProperty private long id; + + @JsonProperty private String location; public long getId() { diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/StaleDevices.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/StaleDevices.java index 34039dee4..6ea74ce2e 100644 --- a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/StaleDevices.java +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/push/StaleDevices.java @@ -16,10 +16,13 @@ */ package org.whispersystems.textsecure.internal.push; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.util.List; public class StaleDevices { + @JsonProperty private List staleDevices; public List getStaleDevices() { diff --git a/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java new file mode 100644 index 000000000..65e066f66 --- /dev/null +++ b/libtextsecure/src/main/java/org/whispersystems/textsecure/internal/util/JsonUtil.java @@ -0,0 +1,75 @@ +package org.whispersystems.textsecure.internal.util; + +import android.util.Log; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; + +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.InvalidKeyException; + +import java.io.IOException; + +public class JsonUtil { + + private static final String TAG = JsonUtil.class.getSimpleName(); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public static String toJson(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + Log.w(TAG, e); + return ""; + } + } + + public static T fromJson(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (IOException e) { + Log.w(TAG, e); + throw new JsonParseException(e); + } + } + + public static class JsonParseException extends RuntimeException { + public JsonParseException(Exception e) { + super(e); + } + } + + public static class IdentityKeySerializer extends JsonSerializer { + @Override + public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers) + throws IOException + { + gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize())); + } + } + + public static class IdentityKeyDeserializer extends JsonDeserializer { + @Override + public IdentityKey deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + try { + return new IdentityKey(Base64.decodeWithoutPadding(p.getValueAsString()), 0); + } catch (InvalidKeyException e) { + throw new IOException(e); + } + } + } + + +} diff --git a/res/drawable-xxhdpi/ic_error_red_24dp.png b/res/drawable-xxhdpi/ic_error_red_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0a272ad41aa5edc92ec1c7fd4a5f5aaeede1adf7 GIT binary patch literal 974 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=IjczVPIh72=EDUeJZ0oivIuq-@CnQEikacN`m}?8Mxi6 z7M^(U_Lm!%c+ugXhYH0Uw*2|M+0N;xz0}I365$`;2eZ$Z%*MBWH;?GKGrW7IPYPPG z_@K?kyaPWtT`T4vyZ3rtxl69d2CIM83;d__?_fREx5SyF*kidK&<4gNZ+DmQTCSV& zKn`btM`SSr1Gg{;GcwGYBLP%!(bL5-B;xSf83_E47 zoiD4 zt5TIonaUZN((U`3!7L%XqsUFPOGgUacVDQk16P0mlp59HhivMV#Xn<+d&7Ji#5o zQN{A)uyZZbq>mRG8vaydcsMZHm?$Z*^7nFj9CiNn-@&?i%Us=W=eEVOm8jiOeKbjv zk@e#RZpPM-HOZOEK_c9e)A%G0I~iwx_J6c3er|OCL=nEI8|Su~@KtS<-zvpsq>*0F zw((NF_pMSDZN9ZDzAuh(yb>s3etVI|mb6%<`$y;SKU=#kMzGM%{7rY++EedyZ2Od1 zwr`nh&9A)G>UnYHonwoOI*|Ros%EXv-lTQk zdU@q&P{k+n+p>2rJ@36K^WAXK?brNmi#M+8cRzgc%k%FaCZ^y2#QvJ8Z?o}1-etfT OXYh3Ob6Mw<&;$T=xRCn* literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_error_white_18dp.png b/res/drawable-xxhdpi/ic_error_white_18dp.png new file mode 100644 index 0000000000000000000000000000000000000000..036d5bed330b8470a9c4408645b78df6d0fc3432 GIT binary patch literal 659 zcmV;E0&M+>P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00Ik1L_t(o!|m9?YZE~f$MK}6q*9BhNK=X;h&qdH7_rlbQEkgvj6fME+y+Gr=Q@tW#o(ElR9YB*!@MFtIpkUSQ)En>EtJ zf@6c^D5DdLEC>B#QEpJdBo@;|{a^{+;3uycGR2Zu_{|S4nqZeawu4S;OzC2mX%aL| zQaadJz^7{eA(FkY5q=U@$_#s9eS9Fyyra(@yGz))<%S()M`L;Cir%Sm*cr=feOEIK z=AE!Xeu-c^?Gv-T@TTTlFvTF#jlCDc-tNba&=SL%98$5A_GUK6U{b{vMX?1HTNA~e ztJoJ&?6ZnhL@`UnY*B1i#p004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00Y=bL_t(&-tC%SOH@G=z;9@~)w&k4o6)QI0m46#iHiMM z@FmEHK1InIKNLTLP=X2x;y+5XdMT6$3btAj3F!-j=2KbAD!u6%ZBGj`_wL*~_wJq8 zU^%a6&iU=mIWu$4xfWJ+iB$ut0o8y?5+lVJOKh@Bo&p8(?6S!cW8CD-zXWP#m@EZ~ zE^@>N(wsLB)Wd8wMiC`TZ}>nLc^U|&B+Fa~6X*`VD#9sJ-_TGAP?FCM zlix@a(>$r}^WCw2i)2usMh8xTaZQ+#H1L392U0u;P}H$mU$_+L^lf9?X6daumegh* z*^In&`&}M#mUWx8NxwjMYzAhjtLW0KXWnMBqX+a+N_6kc(g(6=m);8Rb%6RL2jd!B;RncKLelT{ z23nU+{?fEou=)eCKzhO@-GPRsD>P;Vs6o1R@3{kIrN@G)+Csbq z-njwAq^Em_F`z4gmqSjgfl|`*%Utz!f}G&#h8k#0P+Kut5Iz$;jjDl`1hpZfKxx6# zf*NR3P)ivF8W22fsDX9`wH9sTe$(EljAF3u8+yXVIS^Ld^?Kxs&6E2^fJUUcFX2@NiRJEhuYOkm zphkWQ`Y*ievL4C7gV2E{CH*d)QaUTuO>Ln9U6$&;j9zWuE4kR_Y-m8|_$uiB;i_&s zW?6Ew7E(K=o>!9HMg9KJ1t~F$%!N|=EZSPjNk1*UL0bcEuHu&7lFi~RzfI3+n}IdY zSaOS9H>-@HeGb)|RC7eVh!4{YhSx=)p#u%{-|9<`0YRL#bdx;2z!w!pS(B zzu}F^yLsuDf2pOwi*gre*dG!kxyL(>Tu~0maMwM-!~WFbG&dM!fem)Zal{chcGzH^ m5w3Bn_AyEgs0LK6Kz{*Nu*P+6U1wnc0000004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00J#ZL_t(o!|j+sOB+!T$CuT_;H{0&n@BxMdn!G6%ON7A zx6*+2T(v^KfTzSrJPAT$CB3zk7Nk9S5Q-upqM~UlYSph$L<&}lissit*T>81nt2;p zO1twe2|NG!@m}7{&cwnBZdoBLgoUub3f9gb@A%3-C!FzvP2TgI+aBI&zpw^+Si;Me z>7~J?$bdgs7rWH5ZIYT|82rFuyh3UPVpv#Yo$rRQ7S>z=513<;99brq=fL%Jjav$3 z2y0B0ONG3b(I)Hh{%g)r3VlJU#F8<Gh1u*VNk)Xq`rlbf`>kxBC^4+0ZeWK1HSCdhB3K(0$9-CW%6KNZ2-c_ElXakk zus5n&G}b_+uPk38*m;SX3*vqD-GNO80sG~^a{nAG&|^HmAoD@MUOTX#HLw1~)y$Ej z&dXp=Ri<km8%cNuuqQil(@7I7gZs?V}$Bd` + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/info_round.xml b/res/drawable/info_round.xml new file mode 100644 index 000000000..668eaf3be --- /dev/null +++ b/res/drawable/info_round.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/conversation_item_received.xml b/res/layout/conversation_item_received.xml index 9705373de..578436bfd 100644 --- a/res/layout/conversation_item_received.xml +++ b/res/layout/conversation_item_received.xml @@ -47,7 +47,7 @@ @@ -111,7 +111,7 @@ + android:contentDescription="@string/conversation_item_sent__send_failed_indicator_description" /> + android:contentDescription="@string/conversation_item_sent__pending_approval_description" + tools:visibility="visible" /> + + android:textSize="16sp" + tools:text="Lorem ipsum mango dolor coconut papaya" /> diff --git a/res/layout/message_details_activity.xml b/res/layout/message_details_activity.xml new file mode 100644 index 000000000..39603606f --- /dev/null +++ b/res/layout/message_details_activity.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/res/layout/message_details_header.xml b/res/layout/message_details_header.xml new file mode 100644 index 000000000..c4ecaeea3 --- /dev/null +++ b/res/layout/message_details_header.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/message_details_recipient.xml b/res/layout/message_details_recipient.xml new file mode 100644 index 000000000..c5c2648cc --- /dev/null +++ b/res/layout/message_details_recipient.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + +