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.
This commit is contained in:
Moxie Marlinspike 2015-01-15 13:35:35 -08:00
parent 4397b55ceb
commit 00d7b5c284
76 changed files with 2395 additions and 721 deletions

View File

@ -125,10 +125,16 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".GroupCreateActivity"
android:windowSoftInputMode="stateVisible"
<activity android:name=".MessageDetailsActivity"
android:label="Message Details"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".GroupCreateActivity"
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"

View File

@ -20,9 +20,6 @@ repositories {
maven {
url "https://raw.github.com/whispersystems/maven/master/preferencefragment/releases/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
}
maven {
url "https://raw.github.com/whispersystems/maven/master/smil/releases/"
}
@ -77,40 +74,6 @@ dependencies {
compile project(':libtextsecure')
}
dependencyVerification {
verify = [
'me.leolin:ShortcutBadger:027977c718035e5997035e04e05152d6c72d94df645e8b7099a274ada722bd14',
'se.emilsjolander:stickylistheaders:89146b46c96fea0e40200474a2625cda10fe94891e4128f53cdb42375091b9b6',
'com.google.android.gms:play-services-base:832cb6b3130e871db6a412c4ab585656dbcc5e7948101f190186757785703f75',
'com.astuetz:pagerslidingtabstrip:f1641396732c7132a7abb837e482e5ee2b0ebb8d10813fc52bbaec2c15c184c2',
'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
'com.makeramen:roundedimageview:7dda2e78c406760e5c356ccce59b0df46b5b171cf18abb891998594405021548',
'com.afollestad:material-dialogs:ccb013e6572c86cfcca433855cf0dbfbff9b5e7bb9d1f504b761a6bc6f467b60',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
'com.android.support:appcompat-v7:5dbeb5316d0a6027d646ae552804c3baa5e3bd53f7f33db50904d51505c8a0e5',
'com.android.support:recyclerview-v7:e525ad3f33c84bb12b73d2dc975b55364a53f0f2d0697e043efba59ba73e22d2',
'com.melnykov:floatingactionbutton:0679ad9f7d61eb7aeab91e8dc56358cdedd5b1c1b9c48464499ffa05c40d3985',
'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4',
'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad',
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
'org.whispersystems:jobmanager:01f35586c43aa3806f1c18d3d6a5a972def98103ba1a5a9ca3eec08d15f974b7',
'org.whispersystems:libpastelog:3ccf00fe1597eb8ca1e5de99b17fc225387a1b80b5bbc00ec1bc4d4f3ea9cdde',
'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a',
'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff',
'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74',
'com.googlecode.libphonenumber:libphonenumber:eba17eae81dd622ea89a00a3a8c025b2f25d342e0d9644c5b62e16f15687c3ab',
'org.whispersystems:gson:08f4f7498455d1539c9233e5aac18e9b1805815ef29221572996508eb512fe51',
'org.whispersystems:axolotl-android:7617256d05aaecd7b5475cd55e42773d7079167a22ca48512bcb0f84f8473cc9',
'com.android.support:support-v4:703572d3015a088cc5604b7e38885af3d307c829d0c5ceaf8654ff41c71cd160',
'com.android.support:support-annotations:fdee2354787ef66b268e75958de3f7f6c4f8f325510a6dac9f49c929f83a63de',
'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f',
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
]
}
android {
compileSdkVersion 21
buildToolsVersion '21.1.2'
@ -173,6 +136,11 @@ android {
}
}
packagingOptions {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
}
lintOptions {
abortOnError false
}

View File

@ -13,15 +13,13 @@ apply plugin: 'maven'
repositories {
mavenCentral()
maven {
url "https://raw.github.com/whispersystems/maven/master/gson/releases/"
}
}
dependencies {
compile 'com.google.protobuf:protobuf-java:2.5.0'
compile 'com.googlecode.libphonenumber:libphonenumber:6.1'
compile 'org.whispersystems:gson:2.2.4'
compile 'com.fasterxml.jackson.core:jackson-databind:2.5.0'
compile 'org.whispersystems:axolotl-android:1.0.0'
compile 'com.squareup.okhttp:okhttp:2.2.0'
}

View File

@ -35,6 +35,8 @@ import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.api.push.TrustStore;
import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
import org.whispersystems.textsecure.api.push.exceptions.PushNetworkException;
import org.whispersystems.textsecure.internal.push.MismatchedDevices;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessage;
import org.whispersystems.textsecure.internal.push.OutgoingPushMessageList;
@ -176,6 +178,7 @@ public class TextSecureMessageSender {
{
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
List<NetworkFailureException> 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);
}
}

View File

@ -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<byte[]>, JsonDeserializer<byte[]>
{
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
@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<byte[]> {
@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());
}
}
}

View File

@ -24,12 +24,15 @@ public class EncapsulatedExceptions extends Throwable {
private final List<UntrustedIdentityException> untrustedIdentityExceptions;
private final List<UnregisteredUserException> unregisteredUserExceptions;
private final List<NetworkFailureException> networkExceptions;
public EncapsulatedExceptions(List<UntrustedIdentityException> untrustedIdentities,
List<UnregisteredUserException> unregisteredUsers)
List<UnregisteredUserException> unregisteredUsers,
List<NetworkFailureException> networkExceptions)
{
this.untrustedIdentityExceptions = untrustedIdentities;
this.unregisteredUserExceptions = unregisteredUsers;
this.networkExceptions = networkExceptions;
}
public List<UntrustedIdentityException> getUntrustedIdentityExceptions() {
@ -39,4 +42,8 @@ public class EncapsulatedExceptions extends Throwable {
public List<UnregisteredUserException> getUnregisteredUserExceptions() {
return unregisteredUserExceptions;
}
public List<NetworkFailureException> getNetworkExceptions() {
return networkExceptions;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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<Integer> missingDevices;
@JsonProperty
private List<Integer> extraDevices;
public List<Integer> getMissingDevices() {

View File

@ -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) {

View File

@ -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<OutgoingPushMessage> messages;
public OutgoingPushMessageList(String destination, long timestamp, String relay,

View File

@ -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<ECPublicKey> {
@Override
public void serialize(ECPublicKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize()));
}
}
private static class ECPublicKeyJsonAdapter
implements JsonSerializer<ECPublicKey>, JsonDeserializer<ECPublicKey>
{
private static class ECPublicKeyDeserializer extends JsonDeserializer<ECPublicKey> {
@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);
}
}
}
}

View File

@ -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<PreKeyResponseItem> 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<IdentityKey>, JsonDeserializer<IdentityKey>
{
@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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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<PreKeyEntity> 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);
}
}

View File

@ -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() {}

View File

@ -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<PreKeyBundle> 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() {

View File

@ -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<Integer> staleDevices;
public List<Integer> getStaleDevices() {

View File

@ -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> T fromJson(String json, Class<T> 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<IdentityKey> {
@Override
public void serialize(IdentityKey value, JsonGenerator gen, SerializerProvider serializers)
throws IOException
{
gen.writeString(Base64.encodeBytesWithoutPadding(value.serialize()));
}
}
public static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
@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);
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<corners android:radius="2dp" />
<solid android:color="#FFD32F2F" />
</shape>
</item>
<item>
<shape>
<corners android:radius="2dp" />
<solid android:color="#FFF44336" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<corners android:radius="2dp" />
<solid android:color="#ff145c95" />
</shape>
</item>
<item>
<shape>
<corners android:radius="2dp" />
<solid android:color="#ff2090ea" />
</shape>
</item>
</selector>

View File

@ -47,7 +47,7 @@
<LinearLayout android:id="@+id/conversation_item_parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/triangle_tick"
android:background="?conversation_item_received_background"
android:orientation="vertical">
@ -111,7 +111,7 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dip"
android:paddingLeft="8dp"
android:paddingRight="5dp"
@ -153,7 +153,7 @@
<LinearLayout android:id="@+id/indicators_parent"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:orientation="vertical"
android:gravity="center_vertical"

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/conversation_item"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -17,7 +18,7 @@
<LinearLayout android:id="@+id/indicators_parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="left|center_vertical"
android:layout_marginLeft="6dp"
@ -28,25 +29,28 @@
android:id="@+id/sms_failed_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_action_warning_red"
android:src="@drawable/ic_error_red_24dp"
tools:visibility="visible"
android:visibility="gone"
android:contentDescription="@string/conversation_item_sent__send_failed_indicator_description"/>
android:contentDescription="@string/conversation_item_sent__send_failed_indicator_description" />
<ImageView
android:id="@+id/pending_approval_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_dialog_info_holo_light"
android:src="@drawable/ic_info_outline_grey600_24dp"
android:visibility="gone"
android:layout_gravity="center_vertical"
android:contentDescription="@string/conversation_item_sent__pending_approval_description"/>
android:contentDescription="@string/conversation_item_sent__pending_approval_description"
tools:visibility="visible" />
</LinearLayout>
<LinearLayout android:id="@+id/conversation_item_parent"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:background="?conversation_item_sent_background"
android:background="?conversation_item_sent_push_background"
android:paddingRight="10dip"
android:paddingLeft="10dip"
android:layout_marginLeft="50dp"
@ -62,7 +66,8 @@
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?conversation_sent_text_primary_color"
android:textColorLink="?conversation_sent_text_primary_color"
android:textSize="16sp" />
android:textSize="16sp"
tools:text="Lorem ipsum mango dolor coconut papaya" />
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mms_view"
@ -116,7 +121,7 @@
</LinearLayout>
<LinearLayout android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="0dip"
android:layout_gravity="right">

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout android:id="@+id/item_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?conversation_background"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:elevation="2dp" />
<ListView android:id="@+id/recipients_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
tools:listitem="@layout/conversation_item_details_item" />
</LinearLayout>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TableRow android:id="@+id/sent_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_details_header__sent"
android:gravity="right"
android:textStyle="bold" />
<TextView android:id="@+id/sent_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_details_table_row_pad"
tools:text="Jan 18, 9:29AM" />
</TableRow>
<TableRow android:id="@+id/received_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_details_header__received"
android:gravity="right"
android:textStyle="bold" />
<TextView android:id="@+id/received_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_details_table_row_pad"
tools:text="Jan 18, 9:31AM" />
</TableRow>
<TableRow android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_details_header__via"
android:gravity="right"
android:textStyle="bold" />
<TextView android:id="@+id/transport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_details_table_row_pad"
tools:text="Push (TextSecure)" />
</TableRow>
<TableRow android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/message_details_table_row_pad">
<TextView android:id="@+id/tofrom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/message_details_header__to"
android:textStyle="bold" />
</TableRow>
</TableLayout>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.MessageRecipientListItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.makeramen.RoundedImageView
android:id="@+id/contact_photo_image"
android:foreground="@drawable/contact_photo_background"
app:riv_oval="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_marginTop="3dp"
android:layout_marginBottom="3dp"
android:layout_marginRight="10dp"
android:cropToPadding="true"
android:scaleType="centerCrop"
tools:src="@drawable/ic_contact_picture"
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo" />
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dip"
android:layout_marginBottom="4dip"
android:layout_toRightOf="@id/contact_photo_image"
android:layout_centerVertical="true"
android:orientation="horizontal">
<LinearLayout android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:id="@+id/from"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/conversation_list_item_contact_color"
android:singleLine="true"
android:gravity="center_vertical"
android:layout_gravity="center_vertical"
tools:text="Jules Bonnot"
android:ellipsize="marquee" />
<TextView android:id="@+id/error_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FFF44336"
android:visibility="gone"
tools:visibility="visible"
tools:text="New identity" />
</LinearLayout>
<Button android:id="@+id/conflict_button"
android:layout_width="60sp"
android:layout_height="38sp"
style="@style/ErrorButton"
android:paddingLeft="10dp"
android:paddingRight="5dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_gravity="center_vertical"
android:drawableLeft="@drawable/ic_error_white_18dp"
android:text="FIX" />
<Button android:id="@+id/resend_button"
android:layout_width="85sp"
android:layout_height="38sp"
style="@style/InfoButton"
android:paddingLeft="10dp"
android:paddingRight="5dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:layout_gravity="center_vertical"
android:drawableLeft="@drawable/ic_refresh_white_18dp"
android:text="RESEND" />
</LinearLayout>
</org.thoughtcrime.securesms.MessageRecipientListItem>

View File

@ -17,4 +17,5 @@
<dimen name="conversation_activity_compose_padding">12dp</dimen>
<integer name="media_overview_cols">3</integer>
<dimen name="message_details_table_row_pad">10dp</dimen>
</resources>

View File

@ -59,10 +59,20 @@
<string name="AttachmentTypeSelectorAdapter_audio">Audio</string>
<string name="AttachmentTypeSelectorAdapter_contact">Contact info</string>
<!-- ConfirmIdentityDialog -->
<string name="ConfirmIdentityDialog_the_signature_on_this_key_exchange_is_different">The
identifying key material for %1$s has changed. This could either mean that someone is trying to
intercept your communication, or that %2$s simply re-installed TextSecure and now has a new
identity key.
</string>
<string name="ConfirmIdentityDialog_you_may_wish_to_verify_this_contact">You may wish to verify
this contact.
</string>
<!-- ConversationItem -->
<string name="ConversationItem_message_size_d_kb">Message size: %d KB</string>
<string name="ConversationItem_expires_s">Expires: %s</string>
<string name="ConversationItem_error_sending_message">Error sending message</string>
<string name="ConversationItem_error_not_delivered">Not delivered</string>
<string name="ConversationItem_view_secure_media_question">View secure media?</string>
<string name="ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning">This media has been stored in an encrypted database. Unfortunately, to view it with an external content viewer currently requires the data to be temporarily decrypted and written to disk. Are you sure that you would like to do this?</string>
<string name="ConversationItem_error_received_stale_key_exchange_message">Error, received stale key exchange message.</string>
@ -70,6 +80,7 @@
<string name="ConversationItem_group_action_left">%1$s has left the group.</string>
<string name="ConversationItem_click_to_approve_sms">Tap for SMS fallback</string>
<string name="ConversationItem_click_to_approve_mms">Tap for MMS fallback</string>
<string name="ConversationItem_click_for_details">Tap for details</string>
<string name="ConversationItem_click_to_approve_unencrypted">Tap for unsecured fallback</string>
<string name="ConversationItem_click_to_approve_sms_dialog_title">Fallback to SMS?</string>
<string name="ConversationItem_click_to_approve_mms_dialog_title">Fallback to MMS?</string>
@ -126,8 +137,8 @@
<string name="ConversationFragment_unable_to_write_to_sd_card_exclamation">Unable to write to storage!</string>
<string name="ConversationFragment_saving_attachment">Saving attachment</string>
<string name="ConversationFragment_saving_attachment_to_sd_card">Saving attachment to storage...</string>
<string name="ConversationFragment_pending">PENDING</string>
<string name="ConversationFragment_push">PUSH</string>
<string name="ConversationFragment_pending">Pending...</string>
<string name="ConversationFragment_push">Data (TextSecure)</string>
<string name="ConversationFragment_mms">MMS</string>
<string name="ConversationFragment_sms">SMS</string>
<string name="ConversationFragment_deleting">Deleting...</string>
@ -225,6 +236,10 @@
<string name="KeyScanningActivity_install_barcode_Scanner">Install Barcode Scanner?</string>
<string name="KeyScanningActivity_this_application_requires_barcode_scanner_would_you_like_to_install_it">TextSecure needs Barcode Scanner for QR codes.</string>
<!-- MessageDetailsRecipient -->
<string name="MessageDetailsRecipient_failed_to_send">Failed to send</string>
<string name="MessageDetailsRecipient_new_identity">New identity</string>
<!-- MmsDownloader -->
<string name="MmsDownloader_error_storing_mms">Error storing MMS!</string>
<string name="MmsDownloader_error_connecting_to_mms_provider">Error connecting to MMS provider...</string>
@ -642,6 +657,14 @@
<string name="verify_identity_activity__their_identity_they_read">Their identity (they read):</string>
<string name="verify_identity_activity__your_identity_you_read">Your identity (you read):</string>
<!-- message_details_header -->
<string name="message_details_header__sent">Sent</string>
<string name="message_details_header__received">Received</string>
<string name="message_details_header__via">Via</string>
<string name="message_details_header__to">To:</string>
<string name="message_details_header__from">From:</string>
<string name="message_details_header__with">With:</string>
<!-- AndroidManifest.xml -->
<string name="AndroidManifest__create_passphrase">Create passphrase</string>
<string name="AndroidManifest__enter_passphrase">Enter passphrase</string>

View File

@ -161,4 +161,19 @@
<item name="android:textColor">#ff999999</item>
</style>
<style name="MaterialButton">
<item name="android:elevation">1dp</item>
<item name="android:translationZ">1dp</item>
<item name="android:textColor">@color/white</item>
<item name="android:textSize">12sp</item>
</style>
<style name="InfoButton" parent="@style/MaterialButton">
<item name="android:background">@drawable/info_round</item>
</style>
<style name="ErrorButton" parent="@style/MaterialButton">
<item name="android:background">@drawable/error_round</item>
</style>
</resources>

View File

@ -72,9 +72,7 @@ public class ApplicationContext extends Application implements DependencyInjecto
return jobManager;
}
private void initializeRandomNumberFix() {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
PRNGFixes.apply();
}

View File

@ -0,0 +1,216 @@
package org.thoughtcrime.securesms;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.MmsAddressDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.PushDecryptJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
import java.io.IOException;
public class ConfirmIdentityDialog extends AlertDialog {
private static final String TAG = ConfirmIdentityDialog.class.getSimpleName();
private OnClickListener callback;
public ConfirmIdentityDialog(Context context,
MasterSecret masterSecret,
MessageRecord messageRecord,
IdentityKeyMismatch mismatch)
{
super(context);
Recipient recipient = RecipientFactory.getRecipientForId(context, mismatch.getRecipientId(), false);
String name = recipient.toShortString();
String introduction = String.format(context.getString(R.string.ConfirmIdentityDialog_the_signature_on_this_key_exchange_is_different), name, name);
SpannableString spannableString = new SpannableString(introduction + " " +
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_this_contact));
spannableString.setSpan(new VerifySpan(context, masterSecret, mismatch),
introduction.length()+1, spannableString.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setTitle(name);
setMessage(spannableString);
setButton(AlertDialog.BUTTON_POSITIVE, "Accept", new AcceptListener(masterSecret, messageRecord, mismatch));
setButton(AlertDialog.BUTTON_NEGATIVE, "Cancel", new CancelListener());
}
@Override
public void show() {
super.show();
((TextView)this.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
}
public void setCallback(OnClickListener callback) {
this.callback = callback;
}
private class AcceptListener implements OnClickListener {
private final MasterSecret masterSecret;
private final MessageRecord messageRecord;
private final IdentityKeyMismatch mismatch;
private AcceptListener(MasterSecret masterSecret, MessageRecord messageRecord, IdentityKeyMismatch mismatch) {
this.masterSecret = masterSecret;
this.messageRecord = messageRecord;
this.mismatch = mismatch;
}
@Override
public void onClick(DialogInterface dialog, int which) {
new AsyncTask<Void, Void, Void>()
{
@Override
protected Void doInBackground(Void... params) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
identityDatabase.saveIdentity(masterSecret,
mismatch.getRecipientId(),
mismatch.getIdentityKey());
processMessageRecord(messageRecord);
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
return null;
}
private void processMessageRecord(MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) processOutgoingMessageRecord(messageRecord);
else processIncomingMessageRecord(messageRecord);
}
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor, masterSecret);
MessageRecord record;
try {
while ((record = reader.getNext()) != null) {
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
if (mismatch.equals(recordMismatch)) {
processMessageRecord(record);
}
}
}
} finally {
if (reader != null)
reader.close();
}
}
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
MmsAddressDatabase mmsAddressDatabase = DatabaseFactory.getMmsAddressDatabase(getContext());
if (messageRecord.isMms()) {
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(),
mismatch.getIdentityKey());
Recipients recipients = mmsAddressDatabase.getRecipientsForId(messageRecord.getId());
if (recipients.isGroupRecipient()) MessageSender.resendGroupMessage(getContext(), masterSecret, messageRecord, mismatch.getRecipientId());
else MessageSender.resend(getContext(), masterSecret, messageRecord);
} else {
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(),
mismatch.getIdentityKey());
MessageSender.resend(getContext(), masterSecret, messageRecord);
}
}
private void processIncomingMessageRecord(MessageRecord messageRecord) {
try {
PushDatabase pushDatabase = DatabaseFactory.getPushDatabase(getContext());
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(),
mismatch.getIdentityKey());
TextSecureEnvelope envelope = new TextSecureEnvelope(PushMessageProtos.IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE,
messageRecord.getIndividualRecipient().getNumber(),
messageRecord.getRecipientDeviceId(), "",
messageRecord.getDateSent(),
Base64.decode(messageRecord.getBody().getBody()));
long pushId = pushDatabase.insert(envelope);
ApplicationContext.getInstance(getContext())
.getJobManager()
.add(new PushDecryptJob(getContext(), pushId, messageRecord.getId(),
messageRecord.getIndividualRecipient().getNumber()));
} catch (IOException e) {
throw new AssertionError(e);
}
}
}.execute();
if (callback != null) callback.onClick(null, 0);
}
}
private class CancelListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int which) {
if (callback != null) callback.onClick(null, 0);
}
}
private static class VerifySpan extends ClickableSpan {
private final Context context;
private final MasterSecret masterSecret;
private final IdentityKeyMismatch mismatch;
private VerifySpan(Context context, MasterSecret masterSecret, IdentityKeyMismatch mismatch) {
this.context = context;
this.masterSecret = masterSecret;
this.mismatch = mismatch;
}
@Override
public void onClick(View widget) {
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra("recipient", mismatch.getRecipientId());
intent.putExtra("master_secret", masterSecret);
intent.putExtra("remote_identity", new IdentityKeyParcelable(mismatch.getIdentityKey()));
context.startActivity(intent);
}
}
}

View File

@ -62,7 +62,6 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
private final Set<MessageRecord> batchSelected = Collections.synchronizedSet(new HashSet<MessageRecord>());
private final SelectionClickListener selectionClickListener;
private final Handler failedIconClickHandler;
private final Context context;
private final MasterSecret masterSecret;
private final boolean groupThread;
@ -70,13 +69,12 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
private final LayoutInflater inflater;
public ConversationAdapter(Context context, MasterSecret masterSecret, SelectionClickListener selectionClickListener,
Handler failedIconClickHandler, boolean groupThread, boolean pushDestination)
boolean groupThread, boolean pushDestination)
{
super(context, null, 0);
this.context = context;
this.masterSecret = masterSecret;
this.selectionClickListener = selectionClickListener;
this.failedIconClickHandler = failedIconClickHandler;
this.groupThread = groupThread;
this.pushDestination = pushDestination;
this.inflater = LayoutInflater.from(context);
@ -90,7 +88,7 @@ public class ConversationAdapter extends CursorAdapter implements AbsListView.Re
MessageRecord messageRecord = getMessageRecord(id, cursor, type);
item.set(masterSecret, messageRecord, batchSelected, selectionClickListener,
failedIconClickHandler, groupThread, pushDestination);
groupThread, pushDestination);
}
@Override

View File

@ -8,7 +8,6 @@ import android.content.Intent;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
@ -16,7 +15,6 @@ import android.support.v4.widget.CursorAdapter;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.view.ActionMode;
import android.text.ClipboardManager;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@ -30,6 +28,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.loaders.ConversationLoader;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -44,8 +43,6 @@ import org.thoughtcrime.securesms.util.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.LinkedList;
import java.util.List;
@ -103,7 +100,6 @@ public class ConversationFragment extends ListFragment
private void initializeListAdapter() {
if (this.recipients != null && this.threadId != -1) {
this.setListAdapter(new ConversationAdapter(getActivity(), masterSecret, selectionClickListener,
new FailedIconClickHandler(),
(!this.recipients.isSingleRecipient()) || this.recipients.isGroupRecipient(),
DirectoryHelper.isPushDestination(getActivity(), this.recipients)));
getListView().setRecyclerListener((ConversationAdapter)getListAdapter());
@ -218,46 +214,11 @@ public class ConversationFragment extends ListFragment
}
private void handleDisplayDetails(MessageRecord message) {
long dateReceived = message.getDateReceived();
long dateSent = message.getDateSent();
String transport;
if (message.isPending()) transport = getString(R.string.ConversationFragment_pending);
else if (message.isPush()) transport = getString(R.string.ConversationFragment_push);
else if (message.isMms()) transport = getString(R.string.ConversationFragment_mms);
else transport = getString(R.string.ConversationFragment_sms);
String dateFormatPattern;
if (DateFormat.is24HourFormat(getActivity().getApplicationContext())) {
dateFormatPattern = "EEE MMM d, yyyy '-' HH:mm:ss zzz";
} else {
dateFormatPattern = "EEE MMM d, yyyy '-' hh:mm:ss a zzz";
}
SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormatPattern);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.ConversationFragment_message_details);
builder.setIcon(Dialogs.resolveIcon(getActivity(), R.attr.dialog_info_icon));
builder.setCancelable(true);
if (dateReceived == dateSent || message.isOutgoing()) {
builder.setMessage(String.format(getActivity()
.getString(R.string.ConversationFragment_transport_s_sent_received_s),
transport,
dateFormatter.format(new Date(dateSent))));
} else {
builder.setMessage(String.format(getActivity()
.getString(R.string.ConversationFragment_sender_s_transport_s_sent_s_received_s),
message.getIndividualRecipient().getNumber(),
transport,
dateFormatter.format(new Date(dateSent)),
dateFormatter.format(new Date(dateReceived))));
}
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MASTER_SECRET_EXTRA, masterSecret);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, message.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
startActivity(intent);
}
private void handleForwardMessage(MessageRecord message) {
@ -315,15 +276,6 @@ public class ConversationFragment extends ListFragment
((CursorAdapter)getListAdapter()).changeCursor(null);
}
private class FailedIconClickHandler extends Handler {
@Override
public void handleMessage(android.os.Message message) {
if (listener != null) {
listener.setComposeText((String)message.obj);
}
}
}
public interface ConversationFragmentListener {
public void setComposeText(String text);
}

View File

@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -119,7 +120,6 @@ public class ConversationItem extends LinearLayout {
private FutureTaskListener<SlideDeck> slideDeckListener;
private TypedArray backgroundDrawables;
private final FailedIconClickListener failedIconClickListener = new FailedIconClickListener();
private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener();
private final MmsPreferencesClickListener mmsPreferencesClickListener = new MmsPreferencesClickListener();
private final ClickListener clickListener = new ClickListener();
@ -158,20 +158,18 @@ public class ConversationItem extends LinearLayout {
this.backgroundDrawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
setOnClickListener(clickListener);
if (failedImage != null) failedImage.setOnClickListener(failedIconClickListener);
if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener);
if (mmsThumbnail != null) mmsThumbnail.setOnLongClickListener(new MultiSelectLongClickListener());
}
public void set(MasterSecret masterSecret, MessageRecord messageRecord,
Set<MessageRecord> batchSelected, SelectionClickListener selectionClickListener,
Handler failedIconHandler, boolean groupThread, boolean pushDestination)
boolean groupThread, boolean pushDestination)
{
this.masterSecret = masterSecret;
this.messageRecord = messageRecord;
this.batchSelected = batchSelected;
this.selectionClickListener = selectionClickListener;
this.failedIconHandler = failedIconHandler;
this.groupThread = groupThread;
this.pushDestination = pushDestination;
@ -223,10 +221,10 @@ public class ConversationItem extends LinearLayout {
if (messageRecord.isOutgoing()) {
final int background;
final int triangleBackground;
if (messageRecord.isPending() && pushDestination && !messageRecord.isForcedSms()) {
if ((messageRecord.isPending() || messageRecord.isFailed()) && pushDestination && !messageRecord.isForcedSms()) {
background = SENT_PUSH_PENDING;
triangleBackground = SENT_PUSH_PENDING_TRIANGLE;
} else if (messageRecord.isPending() || messageRecord.isPendingSmsFallback()) {
} else if (messageRecord.isPending() || messageRecord.isFailed() || messageRecord.isPendingSmsFallback()) {
background = SENT_SMS_PENDING;
triangleBackground = SENT_SMS_PENDING_TRIANGLE;
} else if (messageRecord.isPush()) {
@ -279,10 +277,9 @@ public class ConversationItem extends LinearLayout {
private void setStatusIcons(MessageRecord messageRecord) {
failedImage.setVisibility(messageRecord.isFailed() ? View.VISIBLE : View.GONE);
if (messageRecord.isOutgoing()) {
pendingIndicator.setVisibility(messageRecord.isPendingSmsFallback() ? View.VISIBLE : View.GONE);
indicatorText.setVisibility(messageRecord.isPendingSmsFallback() ? View.VISIBLE : View.GONE);
}
// pendingIndicator.setVisibility(View.GONE);
if (messageRecord.isOutgoing()) indicatorText.setVisibility(View.GONE);
secureImage.setVisibility(messageRecord.isSecure() ? View.VISIBLE : View.GONE);
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
deliveryImage.setVisibility(!messageRecord.isKeyExchange() && messageRecord.isDelivered() ? View.VISIBLE : View.GONE);
@ -291,25 +288,37 @@ public class ConversationItem extends LinearLayout {
mmsDownloadButton.setVisibility(View.GONE);
mmsDownloadingLabel.setVisibility(View.GONE);
if (messageRecord.isFailed()) {
dateText.setText(R.string.ConversationItem_error_sending_message);
} else if (messageRecord.isPendingSmsFallback() && indicatorText != null) {
dateText.setText("");
if (messageRecord.isPendingSecureSmsFallback()) {
if (messageRecord.isMms()) indicatorText.setText(R.string.ConversationItem_click_to_approve_mms);
else indicatorText.setText(R.string.ConversationItem_click_to_approve_sms);
} else {
indicatorText.setText(R.string.ConversationItem_click_to_approve_unencrypted);
}
} else if (messageRecord.isPending()) {
dateText.setText(" ··· ");
if (messageRecord.isFailed()) setFailedStatusIcons();
else if (messageRecord.isPendingSmsFallback()) setFallbackStatusIcons();
else if (messageRecord.isPending()) dateText.setText(" ··· ");
else setSentStatusIcons();
}
private void setSentStatusIcons() {
final long timestamp;
if (messageRecord.isPush()) timestamp = messageRecord.getDateSent();
else timestamp = messageRecord.getDateReceived();
dateText.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), timestamp));
}
private void setFailedStatusIcons() {
dateText.setText(R.string.ConversationItem_error_not_delivered);
indicatorText.setText(R.string.ConversationItem_click_for_details);
indicatorText.setVisibility(View.VISIBLE);
}
private void setFallbackStatusIcons() {
pendingIndicator.setVisibility(View.VISIBLE);
indicatorText.setVisibility(View.VISIBLE);
if (messageRecord.isPendingSecureSmsFallback()) {
if (messageRecord.isMms()) indicatorText.setText(R.string.ConversationItem_click_to_approve_mms);
else indicatorText.setText(R.string.ConversationItem_click_to_approve_sms);
} else {
final long timestamp;
if (messageRecord.isPush()) timestamp = messageRecord.getDateSent();
else timestamp = messageRecord.getDateReceived();
dateText.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), timestamp));
indicatorText.setText(R.string.ConversationItem_click_to_approve_unencrypted);
}
}
@ -323,7 +332,9 @@ public class ConversationItem extends LinearLayout {
}
private void setEvents(MessageRecord messageRecord) {
setClickable(messageRecord.isPendingSmsFallback() ||
setClickable(messageRecord.isPendingSmsFallback() ||
messageRecord.hasNetworkFailures() ||
messageRecord.isIdentityMismatchFailure() ||
(messageRecord.isKeyExchange() &&
!messageRecord.isCorruptedKeyExchange() &&
!messageRecord.isOutgoing()));
@ -529,7 +540,7 @@ public class ConversationItem extends LinearLayout {
private class MmsDownloadClickListener implements View.OnClickListener {
public void onClick(View v) {
NotificationMmsMessageRecord notificationRecord = (NotificationMmsMessageRecord)messageRecord;
Log.w("MmsDownloadClickListener", "Content location: " + new String(notificationRecord.getContentLocation()));
Log.w(TAG, "Content location: " + new String(notificationRecord.getContentLocation()));
mmsDownloadButton.setVisibility(View.GONE);
mmsDownloadingLabel.setVisibility(View.VISIBLE);
@ -562,13 +573,22 @@ public class ConversationItem extends LinearLayout {
private class ClickListener implements View.OnClickListener {
public void onClick(View v) {
if (messageRecord.isKeyExchange() &&
!messageRecord.isOutgoing() &&
!messageRecord.isProcessedKeyExchange() &&
!messageRecord.isStaleKeyExchange())
if (messageRecord.isIdentityMismatchFailure() || messageRecord.hasNetworkFailures()) {
Intent intent = new Intent(context, MessageDetailsActivity.class);
intent.putExtra(MessageDetailsActivity.MASTER_SECRET_EXTRA, masterSecret);
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, messageRecord.getId());
intent.putExtra(MessageDetailsActivity.TYPE_EXTRA, messageRecord.isMms() ? MmsSmsDatabase.MMS_TRANSPORT : MmsSmsDatabase.SMS_TRANSPORT);
intent.putExtra(MessageDetailsActivity.PUSH_EXTRA, pushDestination);
context.startActivity(intent);
} else if (messageRecord.isKeyExchange() &&
!messageRecord.isOutgoing() &&
!messageRecord.isProcessedKeyExchange() &&
!messageRecord.isStaleKeyExchange())
{
handleKeyExchangeClicked();
else if (messageRecord.isPendingSmsFallback())
} else if (messageRecord.isPendingSmsFallback()) {
handleMessageApproval();
}
}
}

View File

@ -17,17 +17,9 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Handler;
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.QuickContact;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
@ -39,6 +31,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Emoji;
import org.thoughtcrime.securesms.util.RecipientViewUtil;
import java.util.Set;
@ -66,7 +59,6 @@ public class ConversationListItem extends RelativeLayout
private TextView subjectView;
private TextView fromView;
private TextView dateView;
private long count;
private boolean read;
private ImageView contactPhotoImage;
@ -98,12 +90,11 @@ public class ConversationListItem extends RelativeLayout
this.selectedThreads = selectedThreads;
this.recipients = thread.getRecipients();
this.threadId = thread.getThreadId();
this.count = thread.getCount();
this.read = thread.isRead();
this.distributionType = thread.getDistributionType();
this.recipients.addListener(this);
this.fromView.setText(formatFrom(recipients, count, read));
this.fromView.setText(RecipientViewUtil.formatFrom(context, recipients, read));
this.subjectView.setText(Emoji.getInstance(context).emojify(thread.getDisplayBody(),
Emoji.EMOJI_SMALL,
@ -118,7 +109,7 @@ public class ConversationListItem extends RelativeLayout
}
setBackground(read, batchMode);
setContactPhoto(this.recipients.getPrimaryRecipient());
RecipientViewUtil.setContactPhoto(context, contactPhotoImage, recipients.getPrimaryRecipient(), true);
}
public void unbind() {
@ -130,28 +121,6 @@ public class ConversationListItem extends RelativeLayout
contactPhotoImage.setVisibility(View.VISIBLE);
}
private void setContactPhoto(final Recipient recipient) {
if (recipient == null) return;
contactPhotoImage.setImageBitmap(recipient.getContactPhoto());
if (!recipient.isGroupRecipient()) {
contactPhotoImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (recipient.getContactUri() != null) {
QuickContact.showQuickContact(context, contactPhotoImage, recipient.getContactUri(), QuickContact.MODE_LARGE, null);
} else {
Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, Uri.fromParts("tel", recipient.getNumber(), null));
context.startActivity(intent);
}
}
});
} else {
contactPhotoImage.setOnClickListener(null);
}
}
private void setBackground(boolean read, boolean batch) {
int[] attributes = new int[]{R.attr.conversation_list_item_background_selected,
R.attr.conversation_list_item_background_read,
@ -170,38 +139,6 @@ public class ConversationListItem extends RelativeLayout
drawables.recycle();
}
private CharSequence formatFrom(Recipients from, long count, boolean read) {
int attributes[] = new int[] {R.attr.conversation_list_item_count_color};
TypedArray colors = context.obtainStyledAttributes(attributes);
final String fromString;
final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName());
if (isUnnamedGroup) {
fromString = context.getString(R.string.ConversationActivity_unnamed_group);
} else {
fromString = from.toShortString();
}
SpannableStringBuilder builder = new SpannableStringBuilder(fromString);
final int typeface;
if (isUnnamedGroup) {
if (!read) typeface = Typeface.BOLD_ITALIC;
else typeface = Typeface.ITALIC;
} else if (!read) {
typeface = Typeface.BOLD;
} else {
typeface = Typeface.NORMAL;
}
builder.setSpan(new StyleSpan(typeface), 0, builder.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
colors.recycle();
return builder;
}
public Recipients getRecipients() {
return recipients;
}
@ -219,8 +156,8 @@ public class ConversationListItem extends RelativeLayout
handler.post(new Runnable() {
@Override
public void run() {
ConversationListItem.this.fromView.setText(formatFrom(recipients, count, read));
setContactPhoto(ConversationListItem.this.recipients.getPrimaryRecipient());
ConversationListItem.this.fromView.setText(RecipientViewUtil.formatFrom(context, recipients, read));
RecipientViewUtil.setContactPhoto(context, contactPhotoImage, recipients.getPrimaryRecipient(), true);
}
});
}

View File

@ -0,0 +1,255 @@
/**
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.loaders.MessageDetailsLoader;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DirectoryHelper;
import org.thoughtcrime.securesms.util.GroupUtil;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.LinkedList;
public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity implements LoaderCallbacks<Cursor> {
private final static String TAG = MessageDetailsActivity.class.getSimpleName();
public final static String MASTER_SECRET_EXTRA = "master_secret";
public final static String MESSAGE_ID_EXTRA = "message_id";
public final static String TYPE_EXTRA = "type";
public final static String PUSH_EXTRA = "push";
private MasterSecret masterSecret;
private ConversationItem conversationItem;
private ViewGroup itemParent;
private TextView sentDate;
private TextView receivedDate;
private View receivedContainer;
private TextView transport;
private TextView toFrom;
private ListView recipientsList;
private LayoutInflater inflater;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.message_details_activity);
initializeResources();
getSupportLoaderManager().initLoader(0, null, this);
}
private void initializeResources() {
inflater = LayoutInflater.from(this);
View header = inflater.inflate(R.layout.message_details_header, recipientsList, false);
masterSecret = getIntent().getParcelableExtra(MASTER_SECRET_EXTRA);
itemParent = (ViewGroup) findViewById(R.id.item_container );
recipientsList = (ListView ) findViewById(R.id.recipients_list);
sentDate = (TextView ) header.findViewById(R.id.sent_time);
receivedContainer = header.findViewById(R.id.received_container);
receivedDate = (TextView ) header.findViewById(R.id.received_time);
transport = (TextView ) header.findViewById(R.id.transport);
toFrom = (TextView ) header.findViewById(R.id.tofrom);
recipientsList.setHeaderDividersEnabled(false);
recipientsList.addHeaderView(header, null, false);
}
private void updateTransport(MessageRecord messageRecord) {
final String transportText;
if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
transportText = "-";
} else if (messageRecord.isPending()) {
transportText = getString(R.string.ConversationFragment_pending);
} else if (messageRecord.isPush()) {
transportText = getString(R.string.ConversationFragment_push);
} else if (messageRecord.isMms()) {
transportText = getString(R.string.ConversationFragment_mms);
} else {
transportText = getString(R.string.ConversationFragment_sms);
}
transport.setText(transportText);
}
private void updateTime(MessageRecord messageRecord) {
if (messageRecord.isPending() || messageRecord.isFailed()) {
sentDate.setText("-");
receivedContainer.setVisibility(View.GONE);
} else {
SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(this);
sentDate.setText(dateFormatter.format(new Date(messageRecord.getDateSent())));
if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
receivedDate.setText(dateFormatter.format(new Date(messageRecord.getDateReceived())));
receivedContainer.setVisibility(View.VISIBLE);
} else {
receivedContainer.setVisibility(View.GONE);
}
}
}
private void updateRecipients(MessageRecord messageRecord, Recipients recipients) {
final int toFromRes;
if (messageRecord.isMms() && !messageRecord.isPush() && !messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__with;
} else if (messageRecord.isOutgoing()) {
toFromRes = R.string.message_details_header__to;
} else {
toFromRes = R.string.message_details_header__from;
}
toFrom.setText(toFromRes);
conversationItem.set(masterSecret, messageRecord, new HashSet<MessageRecord>(), null,
recipients != messageRecord.getRecipients(),
DirectoryHelper.isPushDestination(this, recipients));
recipientsList.setAdapter(new MessageDetailsRecipientAdapter(this, masterSecret, messageRecord, recipients));
}
private void inflateMessageViewIfAbsent(MessageRecord messageRecord) {
if (conversationItem == null) {
if (messageRecord.isGroupAction()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_activity, itemParent, false);
} else if (messageRecord.isOutgoing()) {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_sent, itemParent, false);
} else {
conversationItem = (ConversationItem) inflater.inflate(R.layout.conversation_item_received, itemParent, false);
}
itemParent.addView(conversationItem);
}
}
private MessageRecord getMessageRecord(Context context, Cursor cursor, String type) {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsDatabase.Reader reader = smsDatabase.readerFor(masterSecret, cursor);
return reader.getNext();
case MmsSmsDatabase.MMS_TRANSPORT:
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
MmsDatabase.Reader mmsReader = mmsDatabase.readerFor(masterSecret, cursor);
return mmsReader.getNext();
default:
throw new AssertionError("no valid message type specified");
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new MessageDetailsLoader(this, getIntent().getStringExtra(TYPE_EXTRA),
getIntent().getLongExtra(MESSAGE_ID_EXTRA, -1));
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
final MessageRecord messageRecord = getMessageRecord(this, cursor, getIntent().getStringExtra(TYPE_EXTRA));
new MessageRecipientAsyncTask(this, messageRecord).execute();
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
recipientsList.setAdapter(null);
}
private class MessageRecipientAsyncTask extends AsyncTask<Void,Void,Recipients> {
private WeakReference<Context> weakContext;
private MessageRecord messageRecord;
public MessageRecipientAsyncTask(Context context, MessageRecord messageRecord) {
this.weakContext = new WeakReference<>(context);
this.messageRecord = messageRecord;
}
protected Context getContext() {
return weakContext.get();
}
@Override
public Recipients doInBackground(Void... voids) {
Context context = getContext();
if (context == null) {
Log.w(TAG, "associated context is destroyed, finishing early");
}
Recipients recipients;
final Recipients intermediaryRecipients;
if (messageRecord.isMms()) {
intermediaryRecipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageRecord.getId());
} else {
intermediaryRecipients = messageRecord.getRecipients();
}
if (!intermediaryRecipients.isGroupRecipient()) {
Log.w(TAG, "Recipient is not a group, resolving members immediately.");
recipients = intermediaryRecipients;
} else {
try {
String groupId = intermediaryRecipients.getPrimaryRecipient().getNumber();
recipients = DatabaseFactory.getGroupDatabase(context)
.getGroupMembers(GroupUtil.getDecodedId(groupId), false);
} catch (IOException e) {
Log.w(TAG, e);
recipients = new Recipients(new LinkedList<Recipient>());
}
}
return recipients;
}
@Override
public void onPostExecute(Recipients recipients) {
if (getContext() == null) {
Log.w(TAG, "AsyncTask finished with a destroyed context, leaving early.");
return;
}
inflateMessageViewIfAbsent(messageRecord);
updateRecipients(messageRecord, recipients);
updateTransport(messageRecord);
updateTime(messageRecord);
}
}
}

View File

@ -0,0 +1,58 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipients;
public class MessageDetailsRecipientAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
private Context context;
private MasterSecret masterSecret;
private MessageRecord record;
private Recipients recipients;
public MessageDetailsRecipientAdapter(Context context, MasterSecret masterSecret, MessageRecord record, Recipients recipients) {
this.context = context;
this.masterSecret = masterSecret;
this.record = record;
this.recipients = recipients;
}
@Override
public int getCount() {
return recipients.getRecipientsList().size();
}
@Override
public Object getItem(int position) {
return recipients.getRecipientsList().get(position);
}
@Override
public long getItemId(int position) {
return recipients.getRecipientsList().get(position).getRecipientId();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.message_details_recipient, parent, false);
}
((MessageRecipientListItem)convertView).set(masterSecret, record, recipients, position);
return convertView;
}
@Override
public void onMovedToScrapHeap(View view) {
((MessageRecipientListItem)view).unbind();
}
}

View File

@ -0,0 +1,177 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.RecipientViewUtil;
/**
* A simple view to show the recipients of a message
*
* @author Jake McGinty
*/
public class MessageRecipientListItem extends RelativeLayout
implements Recipient.RecipientModifiedListener
{
private final static String TAG = MessageRecipientListItem.class.getSimpleName();
private Recipient recipient;
private TextView fromView;
private TextView errorDescription;
private Button conflictButton;
private Button resendButton;
private ImageView contactPhotoImage;
private final Handler handler = new Handler();
public MessageRecipientListItem(Context context) {
super(context);
}
public MessageRecipientListItem(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
this.fromView = (TextView) findViewById(R.id.from);
this.errorDescription = (TextView) findViewById(R.id.error_description);
this.contactPhotoImage = (ImageView) findViewById(R.id.contact_photo_image);
this.conflictButton = (Button) findViewById(R.id.conflict_button);
this.resendButton = (Button) findViewById(R.id.resend_button);
}
public void set(final MasterSecret masterSecret, final MessageRecord record, final Recipients recipients, final int position) {
recipient = recipients.getRecipientsList().get(position);
recipient.addListener(this);
fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipient));
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, recipient, false);
setIssueIndicators(masterSecret, record);
}
private void setIssueIndicators(final MasterSecret masterSecret, final MessageRecord record) {
final NetworkFailure networkFailure = getNetworkFailure(record);
final IdentityKeyMismatch keyMismatch = networkFailure == null ? getKeyMismatch(record) : null;
String errorText = "";
if (networkFailure != null) {
errorText = getContext().getString(R.string.MessageDetailsRecipient_failed_to_send);
resendButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new ResendAsyncTask(masterSecret, record, networkFailure).execute();
}
});
} else if (keyMismatch != null) {
errorText = getContext().getString(R.string.MessageDetailsRecipient_new_identity);
conflictButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
new ConfirmIdentityDialog(getContext(), masterSecret, record, keyMismatch).show();
}
});
}
errorDescription.setText(errorText);
errorDescription.setVisibility(TextUtils.isEmpty(errorText) ? View.GONE : View.VISIBLE);
resendButton.setVisibility(networkFailure != null ? View.VISIBLE : View.GONE);
conflictButton.setVisibility(keyMismatch != null ? View.VISIBLE : View.GONE);
}
private NetworkFailure getNetworkFailure(final MessageRecord record) {
if (record.hasNetworkFailures()) {
for (final NetworkFailure failure : record.getNetworkFailures()) {
if (failure.getRecipientId() == recipient.getRecipientId()) {
return failure;
}
}
}
return null;
}
private IdentityKeyMismatch getKeyMismatch(final MessageRecord record) {
if (record.isIdentityMismatchFailure()) {
for (final IdentityKeyMismatch mismatch : record.getIdentityKeyMismatches()) {
if (mismatch.getRecipientId() == recipient.getRecipientId()) {
return mismatch;
}
}
}
return null;
}
public void unbind() {
if (this.recipient != null) this.recipient.removeListener(this);
}
@Override
public void onModified(final Recipient recipient) {
handler.post(new Runnable() {
@Override
public void run() {
fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipient));
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, recipient, false);
}
});
}
private class ResendAsyncTask extends AsyncTask<Void,Void,Void> {
private final MasterSecret masterSecret;
private final MessageRecord record;
private final NetworkFailure failure;
public ResendAsyncTask(MasterSecret masterSecret, MessageRecord record, NetworkFailure failure) {
this.masterSecret = masterSecret;
this.record = record;
this.failure = failure;
}
@Override
protected Void doInBackground(Void... params) {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
mmsDatabase.removeFailure(record.getId(), failure);
if (record.getRecipients().isGroupRecipient()) {
MessageSender.resendGroupMessage(getContext(), masterSecret, record, failure.getRecipientId());
} else {
MessageSender.resend(getContext(), masterSecret, record);
}
return null;
}
}
}

View File

@ -236,7 +236,7 @@ public class ReceiveKeyActivity extends BaseActivity {
ApplicationContext.getInstance(context)
.getJobManager()
.add(new PushDecryptJob(context, pushId, message.getSender()));
.add(new PushDecryptJob(context, pushId, messageId, message.getSender()));
smsDatabase.deleteMessage(messageId);
} catch (IOException e) {

View File

@ -17,20 +17,11 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.QuickContact;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
@ -39,11 +30,7 @@ import com.makeramen.RoundedImageView;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Emoji;
import java.util.Set;
import org.thoughtcrime.securesms.util.RecipientViewUtil;
/**
* A simple view to show the recipients of an open conversation
@ -51,7 +38,7 @@ import java.util.Set;
* @author Jake McGinty
*/
public class ShareListItem extends RelativeLayout
implements Recipient.RecipientModifiedListener
implements Recipient.RecipientModifiedListener
{
private final static String TAG = ShareListItem.class.getSimpleName();
@ -87,21 +74,16 @@ public class ShareListItem extends RelativeLayout
this.distributionType = thread.getDistributionType();
this.recipients.addListener(this);
this.fromView.setText(formatFrom(recipients));
this.fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipients));
setBackground();
setContactPhoto(this.recipients.getPrimaryRecipient());
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, this.recipients.getPrimaryRecipient(), false);
}
public void unbind() {
if (this.recipients != null) this.recipients.removeListener(this);
}
private void setContactPhoto(final Recipient recipient) {
if (recipient == null) return;
contactPhotoImage.setImageBitmap(recipient.getContactPhoto());
}
private void setBackground() {
int[] attributes = new int[]{R.attr.conversation_list_item_background_read};
TypedArray drawables = context.obtainStyledAttributes(attributes);
@ -111,26 +93,6 @@ public class ShareListItem extends RelativeLayout
drawables.recycle();
}
private CharSequence formatFrom(Recipients from) {
final String fromString;
final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName());
if (isUnnamedGroup) {
fromString = context.getString(R.string.ConversationActivity_unnamed_group);
} else {
fromString = from.toShortString();
}
SpannableStringBuilder builder = new SpannableStringBuilder(fromString);
final int typeface;
if (isUnnamedGroup) typeface = Typeface.ITALIC;
else typeface = Typeface.NORMAL;
builder.setSpan(new StyleSpan(typeface), 0, builder.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
return builder;
}
public Recipients getRecipients() {
return recipients;
}
@ -148,8 +110,8 @@ public class ShareListItem extends RelativeLayout
handler.post(new Runnable() {
@Override
public void run() {
fromView.setText(formatFrom(recipients));
setContactPhoto(recipients.getPrimaryRecipient());
fromView.setText(RecipientViewUtil.formatFrom(getContext(), recipients));
RecipientViewUtil.setContactPhoto(getContext(), contactPhotoImage, recipients.getPrimaryRecipient(), false);
}
});
}

View File

@ -20,9 +20,10 @@ package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
@ -30,10 +31,10 @@ import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.Curve25519;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.util.Medium;
import java.io.File;
@ -109,7 +110,7 @@ public class PreKeyUtil {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(nextFile);
fout.write(new Gson().toJson(new PreKeyIndex(id)).getBytes());
fout.write(JsonUtils.toJson(new PreKeyIndex(id)).getBytes());
fout.close();
} catch (IOException e) {
Log.w("PreKeyUtil", e);
@ -120,7 +121,7 @@ public class PreKeyUtil {
try {
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(nextFile);
fout.write(new Gson().toJson(new SignedPreKeyIndex(id)).getBytes());
fout.write(JsonUtils.toJson(new SignedPreKeyIndex(id)).getBytes());
fout.close();
} catch (IOException e) {
Log.w("PreKeyUtil", e);
@ -135,7 +136,7 @@ public class PreKeyUtil {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
PreKeyIndex index = new Gson().fromJson(reader, PreKeyIndex.class);
PreKeyIndex index = JsonUtils.fromJson(reader, PreKeyIndex.class);
reader.close();
return index.nextPreKeyId;
}
@ -153,7 +154,7 @@ public class PreKeyUtil {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
SignedPreKeyIndex index = new Gson().fromJson(reader, SignedPreKeyIndex.class);
SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class);
reader.close();
return index.nextSignedPreKeyId;
}
@ -183,6 +184,7 @@ public class PreKeyUtil {
private static class PreKeyIndex {
public static final String FILE_NAME = "index.dat";
@JsonProperty
private int nextPreKeyId;
public PreKeyIndex() {}
@ -195,6 +197,7 @@ public class PreKeyUtil {
private static class SignedPreKeyIndex {
public static final String FILE_NAME = "index.dat";
@JsonProperty
private int nextSignedPreKeyId;
public SignedPreKeyIndex() {}

View File

@ -25,9 +25,9 @@ import java.util.Set;
public abstract class Database {
protected static final String ID_WHERE = "_id = ?";
private static final String CONVERSATION_URI = "content://textsecure/thread/";
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
protected static final String ID_WHERE = "_id = ?";
private static final String CONVERSATION_URI = "content://textsecure/thread/";
private static final String CONVERSATION_LIST_URI = "content://textsecure/conversation-list";
protected SQLiteOpenHelper databaseHelper;
protected final Context context;

View File

@ -45,21 +45,22 @@ import ws.com.google.android.mms.ContentType;
public class DatabaseFactory {
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
private static final int INTRODUCED_INDEXES_VERSION = 3;
private static final int INTRODUCED_DATE_SENT_VERSION = 4;
private static final int INTRODUCED_DRAFTS_VERSION = 5;
private static final int INTRODUCED_NEW_TYPES_VERSION = 6;
private static final int INTRODUCED_MMS_BODY_VERSION = 7;
private static final int INTRODUCED_MMS_FROM_VERSION = 8;
private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
private static final int INTRODUCED_GROUP_DATABASE_VERSION = 11;
private static final int INTRODUCED_PUSH_FIX_VERSION = 12;
private static final int INTRODUCED_DELIVERY_RECEIPTS = 13;
private static final int INTRODUCED_PART_DATA_SIZE_VERSION = 14;
private static final int INTRODUCED_THUMBNAILS_VERSION = 15;
private static final int DATABASE_VERSION = 15;
private static final int INTRODUCED_IDENTITIES_VERSION = 2;
private static final int INTRODUCED_INDEXES_VERSION = 3;
private static final int INTRODUCED_DATE_SENT_VERSION = 4;
private static final int INTRODUCED_DRAFTS_VERSION = 5;
private static final int INTRODUCED_NEW_TYPES_VERSION = 6;
private static final int INTRODUCED_MMS_BODY_VERSION = 7;
private static final int INTRODUCED_MMS_FROM_VERSION = 8;
private static final int INTRODUCED_TOFU_IDENTITY_VERSION = 9;
private static final int INTRODUCED_PUSH_DATABASE_VERSION = 10;
private static final int INTRODUCED_GROUP_DATABASE_VERSION = 11;
private static final int INTRODUCED_PUSH_FIX_VERSION = 12;
private static final int INTRODUCED_DELIVERY_RECEIPTS = 13;
private static final int INTRODUCED_PART_DATA_SIZE_VERSION = 14;
private static final int INTRODUCED_THUMBNAILS_VERSION = 15;
private static final int INTRODUCED_IDENTITY_COLUMN_VERSION = 16;
private static final int DATABASE_VERSION = 16;
private static final String DATABASE_NAME = "messages.db";
private static final Object lock = new Object();
@ -706,8 +707,14 @@ public class DatabaseFactory {
}
if (oldVersion < INTRODUCED_THUMBNAILS_VERSION) {
db.execSQL("ALTER TABLE part ADD COLUMN thumbnail TEXT");
db.execSQL("ALTER TABLE part ADD COLUMN aspect_ratio REAL");
db.execSQL("ALTER TABLE part ADD COLUMN thumbnail TEXT;");
db.execSQL("ALTER TABLE part ADD COLUMN aspect_ratio REAL;");
}
if (oldVersion < INTRODUCED_IDENTITY_COLUMN_VERSION) {
db.execSQL("ALTER TABLE sms ADD COLUMN mismatched_identities TEXT");
db.execSQL("ALTER TABLE mms ADD COLUMN mismatched_identities TEXT");
db.execSQL("ALTER TABLE mms ADD COLUMN network_failures TEXT");
}
db.setTransactionSuccessful();

View File

@ -0,0 +1,145 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import android.util.Log;
import org.thoughtcrime.securesms.database.documents.Document;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.whispersystems.libaxolotl.IdentityKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public abstract class MessagingDatabase extends Database implements MmsSmsColumns {
private static final String TAG = MessagingDatabase.class.getSimpleName();
public MessagingDatabase(Context context, SQLiteOpenHelper databaseHelper) {
super(context, databaseHelper);
}
protected abstract String getTableName();
public void addMismatchedIdentity(long messageId, long recipientId, IdentityKey identityKey) {
try {
addToDocument(messageId, MISMATCHED_IDENTITIES,
new IdentityKeyMismatch(recipientId, identityKey),
IdentityKeyMismatchList.class);
} catch (IOException e) {
Log.w(TAG, e);
}
}
public void removeMismatchedIdentity(long messageId, long recipientId, IdentityKey identityKey) {
try {
removeFromDocument(messageId, MISMATCHED_IDENTITIES,
new IdentityKeyMismatch(recipientId, identityKey),
IdentityKeyMismatchList.class);
} catch (IOException e) {
Log.w(TAG, e);
}
}
protected <D extends Document<I>, I> void removeFromDocument(long messageId, String column, I object, Class<D> clazz) throws IOException {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction();
try {
D document = getDocument(database, messageId, column, clazz);
Iterator<I> iterator = document.getList().iterator();
while (iterator.hasNext()) {
I item = iterator.next();
if (item.equals(object)) {
iterator.remove();
break;
}
}
setDocument(database, messageId, column, document);
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
protected <T extends Document<I>, I> void addToDocument(long messageId, String column, final I object, Class<T> clazz) throws IOException {
List<I> list = new ArrayList<I>() {{
add(object);
}};
addToDocument(messageId, column, list, clazz);
}
protected <T extends Document<I>, I> void addToDocument(long messageId, String column, List<I> objects, Class<T> clazz) throws IOException {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.beginTransaction();
try {
T document = getDocument(database, messageId, column, clazz);
document.getList().addAll(objects);
setDocument(database, messageId, column, document);
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
private void setDocument(SQLiteDatabase database, long messageId, String column, Document document) throws IOException {
ContentValues contentValues = new ContentValues();
if (document == null || document.size() == 0) {
contentValues.put(column, (String)null);
} else {
contentValues.put(column, JsonUtils.toJson(document));
}
database.update(getTableName(), contentValues, ID_WHERE, new String[] {String.valueOf(messageId)});
}
private <D extends Document> D getDocument(SQLiteDatabase database, long messageId,
String column, Class<D> clazz)
{
Cursor cursor = null;
try {
cursor = database.query(getTableName(), new String[] {column},
ID_WHERE, new String[] {String.valueOf(messageId)},
null, null, null);
if (cursor != null && cursor.moveToNext()) {
String document = cursor.getString(cursor.getColumnIndexOrThrow(column));
try {
if (!TextUtils.isEmpty(document)) {
return JsonUtils.fromJson(document, clazz);
}
} catch (IOException e) {
Log.w(TAG, e);
}
}
try {
return clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new AssertionError(e);
}
} finally {
if (cursor != null)
cursor.close();
}
}
}

View File

@ -23,6 +23,11 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import ws.com.google.android.mms.pdu.CharacterSets;
import ws.com.google.android.mms.pdu.EncodedStringValue;
import ws.com.google.android.mms.pdu.PduHeaders;
@ -33,6 +38,8 @@ import java.util.List;
public class MmsAddressDatabase extends Database {
private static final String TAG = MmsAddressDatabase.class.getSimpleName();
private static final String TABLE_NAME = "mms_addresses";
private static final String ID = "_id";
private static final String MMS_ID = "mms_id";
@ -127,6 +134,25 @@ public class MmsAddressDatabase extends Database {
return results;
}
public Recipients getRecipientsForId(long messageId) {
List<String> numbers = getAddressesForId(messageId);
List<Recipient> results = new LinkedList<>();
for (String number : numbers) {
if (!PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR.equals(number)) {
try {
results.add(RecipientFactory.getRecipientsFromString(context, number, false)
.getPrimaryRecipient());
} catch (RecipientFormattingException e) {
Log.w(TAG, e);
}
}
}
return new Recipients(results);
}
public void deleteAddressesForId(long messageId) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
database.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {messageId+""});

View File

@ -21,7 +21,6 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.Bitmap;
import android.net.Uri;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@ -34,6 +33,10 @@ import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.model.DisplayRecord;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
@ -50,6 +53,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -59,10 +63,12 @@ import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -88,7 +94,9 @@ import static org.thoughtcrime.securesms.util.Util.canonicalizeNumberOrGroup;
// 2) How many queries do we make? calling getMediaMessageForId() from within an existing query
// seems wasteful.
public class MmsDatabase extends Database implements MmsSmsColumns {
public class MmsDatabase extends MessagingDatabase {
private static final String TAG = MmsDatabase.class.getSimpleName();
public static final String TABLE_NAME = "mms";
static final String DATE_SENT = "date";
@ -119,6 +127,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
private static final String DELIVERY_TIME = "d_tm";
private static final String DELIVERY_REPORT = "d_rpt";
static final String PART_COUNT = "part_count";
static final String NETWORK_FAILURE = "network_failures";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
THREAD_ID + " INTEGER, " + DATE_SENT + " INTEGER, " + DATE_RECEIVED + " INTEGER, " + MESSAGE_BOX + " INTEGER, " +
@ -132,7 +141,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
STATUS + " INTEGER, " + TRANSACTION_ID + " TEXT, " + RETRIEVE_STATUS + " INTEGER, " +
RETRIEVE_TEXT + " TEXT, " + RETRIEVE_TEXT_CS + " INTEGER, " + READ_STATUS + " INTEGER, " +
CONTENT_CLASS + " INTEGER, " + RESPONSE_TEXT + " TEXT, " + DELIVERY_TIME + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + DELIVERY_REPORT + " INTEGER);";
RECEIPT_COUNT + " INTEGER DEFAULT 0, " + MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " +
NETWORK_FAILURE + " TEXT DEFAULT NULL," + DELIVERY_REPORT + " INTEGER);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@ -150,7 +160,7 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
MESSAGE_SIZE, PRIORITY, REPORT_ALLOWED, STATUS, TRANSACTION_ID, RETRIEVE_STATUS,
RETRIEVE_TEXT, RETRIEVE_TEXT_CS, READ_STATUS, CONTENT_CLASS, RESPONSE_TEXT,
DELIVERY_TIME, DELIVERY_REPORT, BODY, PART_COUNT, ADDRESS, ADDRESS_DEVICE_ID,
RECEIPT_COUNT
RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE
};
public static final ExecutorService slideResolver = org.thoughtcrime.securesms.util.Util.newSingleThreadedLifoExecutor();
@ -164,6 +174,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
}
@Override
protected String getTableName() {
return TABLE_NAME;
}
public int getMessageCountForThread(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = null;
@ -181,6 +196,22 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
return 0;
}
public void addFailures(long messageId, List<NetworkFailure> failure) {
try {
addToDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList.class);
} catch (IOException e) {
Log.w(TAG, e);
}
}
public void removeFailure(long messageId, NetworkFailure failure) {
try {
removeFromDocument(messageId, NETWORK_FAILURE, failure, NetworkFailureList.class);
} catch (IOException e) {
Log.w(TAG, e);
}
}
public void incrementDeliveryReceiptCount(String address, long timestamp) {
MmsAddressDatabase addressDatabase = DatabaseFactory.getMmsAddressDatabase(context);
SQLiteDatabase database = databaseHelper.getWritableDatabase();
@ -319,6 +350,14 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
}
}
public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MMS_PROJECTION, ID_WHERE, new String[] {messageId+""},
null, null, null);
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
return cursor;
}
public void updateResponseStatus(long messageId, int status) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues();
@ -330,8 +369,8 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
private void updateMailboxBitmask(long id, long maskOff, long maskOn) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME +
" SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
" WHERE " + ID + " = ?", new String[] {id + ""});
" SET " + MESSAGE_BOX + " = (" + MESSAGE_BOX + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + " )" +
" WHERE " + ID + " = ?", new String[] {id + ""});
}
public void markAsOutbox(long messageId) {
@ -1008,13 +1047,18 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.RECEIPT_COUNT));
DisplayRecord.Body body = getBody(cursor);
int partCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.PART_COUNT));
Recipients recipients = getRecipientsFor(address);
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.MISMATCHED_IDENTITIES));
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.NETWORK_FAILURE));
Recipients recipients = getRecipientsFor(address);
List<IdentityKeyMismatch> mismatches = getMismatchedIdentities(mismatchDocument);
List<NetworkFailure> networkFailures = getFailures(networkDocument);
ListenableFutureTask<SlideDeck> slideDeck = getSlideDeck(masterSecret, id);
return new MediaMmsMessageRecord(context, id, recipients, recipients.getPrimaryRecipient(),
addressDeviceId, dateSent, dateReceived, receiptCount,
threadId, body, slideDeck, partCount, box);
threadId, body, slideDeck, partCount, box, mismatches, networkFailures);
}
private Recipients getRecipientsFor(String address) {
@ -1036,6 +1080,30 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
}
}
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {
if (!TextUtils.isEmpty(document)) {
try {
return JsonUtils.fromJson(document, IdentityKeyMismatchList.class).getList();
} catch (IOException e) {
Log.w(TAG, e);
}
}
return new LinkedList<>();
}
private List<NetworkFailure> getFailures(String document) {
if (!TextUtils.isEmpty(document)) {
try {
return JsonUtils.fromJson(document, NetworkFailureList.class).getList();
} catch (IOException ioe) {
Log.w(TAG, ioe);
}
}
return new LinkedList<>();
}
private DisplayRecord.Body getBody(Cursor cursor) {
try {
String body = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.BODY));

View File

@ -11,6 +11,7 @@ public interface MmsSmsColumns {
public static final String ADDRESS = "address";
public static final String ADDRESS_DEVICE_ID = "address_device_id";
public static final String RECEIPT_COUNT = "delivery_receipt_count";
public static final String MISMATCHED_IDENTITIES = "mismatched_identities";
public static class Types {
protected static final long TOTAL_MASK = 0xFFFFFFFF;
@ -37,6 +38,7 @@ public interface MmsSmsColumns {
protected static final long MESSAGE_FORCE_SMS_BIT = 0x40;
// Key Exchange Information
protected static final long KEY_EXCHANGE_MASK = 0xFF00;
protected static final long KEY_EXCHANGE_BIT = 0x8000;
protected static final long KEY_EXCHANGE_STALE_BIT = 0x4000;
protected static final long KEY_EXCHANGE_PROCESSED_BIT = 0x2000;

View File

@ -23,8 +23,8 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.util.Log;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import java.util.HashSet;
import java.util.Set;
@ -49,13 +49,39 @@ public class MmsSmsDatabase extends Database {
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
Cursor cursor = queryTables(projection, selection, order, null, null);
Cursor cursor = queryTables(projection, selection, selection, order, null, null);
setNotifyConverationListeners(cursor, threadId);
return cursor;
}
public Cursor getIdentityConflictMessagesForThread(long threadId) {
String[] projection = {MmsSmsColumns.ID, SmsDatabase.BODY, SmsDatabase.TYPE,
MmsSmsColumns.THREAD_ID,
SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT,
MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX,
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId + " AND " + MmsSmsColumns.MISMATCHED_IDENTITIES + " IS NOT NULL";
Cursor cursor = queryTables(projection, selection, selection, order, null, null);
setNotifyConverationListeners(cursor, threadId);
return cursor;
@ -71,12 +97,14 @@ public class MmsSmsDatabase extends Database {
SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;
return queryTables(projection, selection, order, null, "1");
return queryTables(projection, selection, selection, order, null, "1");
}
public Cursor getUnread() {
@ -89,12 +117,14 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY,
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
MmsDatabase.STATUS, MmsSmsColumns.RECEIPT_COUNT,
MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC";
String selection = MmsSmsColumns.READ + " = 0";
return queryTables(projection, selection, order, null, null);
return queryTables(projection, selection, selection, order, null, null);
}
public int getConversationCount(long threadId) {
@ -109,7 +139,7 @@ public class MmsSmsDatabase extends Database {
DatabaseFactory.getMmsDatabase(context).incrementDeliveryReceiptCount(address, timestamp);
}
private Cursor queryTables(String[] projection, String selection, String order, String groupBy, String limit) {
private Cursor queryTables(String[] projection, String smsSelection, String mmsSelection, String order, String groupBy, String limit) {
String[] mmsProjection = {MmsDatabase.DATE_SENT + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
MmsDatabase.DATE_RECEIVED + " * 1000 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
MmsSmsColumns.ID, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID,
@ -117,7 +147,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
String[] smsProjection = {SmsDatabase.DATE_SENT + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " * 1 AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@ -126,7 +157,8 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT,
MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID,
MmsDatabase.MESSAGE_SIZE, MmsDatabase.EXPIRY, MmsDatabase.STATUS,
MmsSmsColumns.RECEIPT_COUNT, TRANSPORT};
MmsSmsColumns.RECEIPT_COUNT, MmsSmsColumns.MISMATCHED_IDENTITIES,
MmsDatabase.NETWORK_FAILURE, TRANSPORT};
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
@ -146,6 +178,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS);
mmsColumnsPresent.add(MmsSmsColumns.ADDRESS_DEVICE_ID);
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
mmsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_TYPE);
mmsColumnsPresent.add(MmsDatabase.MESSAGE_BOX);
mmsColumnsPresent.add(MmsDatabase.DATE_SENT);
@ -156,6 +189,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.MESSAGE_SIZE);
mmsColumnsPresent.add(MmsDatabase.EXPIRY);
mmsColumnsPresent.add(MmsDatabase.STATUS);
mmsColumnsPresent.add(MmsDatabase.NETWORK_FAILURE);
Set<String> smsColumnsPresent = new HashSet<String>();
smsColumnsPresent.add(MmsSmsColumns.ID);
@ -165,14 +199,15 @@ public class MmsSmsDatabase extends Database {
smsColumnsPresent.add(MmsSmsColumns.READ);
smsColumnsPresent.add(MmsSmsColumns.THREAD_ID);
smsColumnsPresent.add(MmsSmsColumns.RECEIPT_COUNT);
smsColumnsPresent.add(MmsSmsColumns.MISMATCHED_IDENTITIES);
smsColumnsPresent.add(SmsDatabase.TYPE);
smsColumnsPresent.add(SmsDatabase.SUBJECT);
smsColumnsPresent.add(SmsDatabase.DATE_SENT);
smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED);
smsColumnsPresent.add(SmsDatabase.STATUS);
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 2, MMS_TRANSPORT, selection, null, null, null);
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 2, SMS_TRANSPORT, selection, null, null, null);
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 2, MMS_TRANSPORT, mmsSelection, null, null, null);
String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 2, SMS_TRANSPORT, smsSelection, null, null, null);
SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] {smsSubQuery, mmsSubQuery}, order, null);

View File

@ -28,6 +28,8 @@ import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
import org.thoughtcrime.securesms.database.model.DisplayRecord;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
@ -39,9 +41,13 @@ import org.thoughtcrime.securesms.sms.IncomingGroupMessage;
import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.whispersystems.jobqueue.JobManager;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static org.thoughtcrime.securesms.util.Util.canonicalizeNumber;
@ -52,7 +58,9 @@ import static org.thoughtcrime.securesms.util.Util.canonicalizeNumber;
* @author Moxie Marlinspike
*/
public class SmsDatabase extends Database implements MmsSmsColumns {
public class SmsDatabase extends MessagingDatabase {
private static final String TAG = SmsDatabase.class.getSimpleName();
public static final String TABLE_NAME = "sms";
public static final String PERSON = "person";
@ -70,7 +78,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
DATE_RECEIVED + " INTEGER, " + DATE_SENT + " INTEGER, " + PROTOCOL + " INTEGER, " + READ + " INTEGER DEFAULT 0, " +
STATUS + " INTEGER DEFAULT -1," + TYPE + " INTEGER, " + REPLY_PATH_PRESENT + " INTEGER, " +
RECEIPT_COUNT + " INTEGER DEFAULT 0," + SUBJECT + " TEXT, " + BODY + " TEXT, " +
SERVICE_CENTER + " TEXT);";
MISMATCHED_IDENTITIES + " TEXT DEFAULT NULL, " + SERVICE_CENTER + " TEXT);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@ -85,7 +93,8 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
DATE_RECEIVED + " AS " + NORMALIZED_DATE_RECEIVED,
DATE_SENT + " AS " + NORMALIZED_DATE_SENT,
PROTOCOL, READ, STATUS, TYPE,
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, RECEIPT_COUNT,
MISMATCHED_IDENTITIES
};
private final JobManager jobManager;
@ -95,6 +104,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
this.jobManager = ApplicationContext.getInstance(context).getJobManager();
}
protected String getTableName() {
return TABLE_NAME;
}
private void updateTypeBitmask(long id, long maskOff, long maskOn) {
Log.w("MessageDatabase", "Updating ID: " + id + " to base type: " + maskOn);
@ -162,6 +175,14 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
return 0;
}
public void markAsEndSession(long id) {
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.END_SESSION_BIT);
}
public void markAsPreKeyBundle(long id) {
updateTypeBitmask(id, Types.KEY_EXCHANGE_MASK, Types.KEY_EXCHANGE_BIT | Types.KEY_EXCHANGE_BUNDLE_BIT);
}
public void markAsStaleKeyExchange(long id) {
updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_STALE_BIT);
}
@ -303,9 +324,9 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
protected void updateMessageBodyAndType(long messageId, String body, long maskOff, long maskOn) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.execSQL("UPDATE " + TABLE_NAME + " SET " + BODY + " = ?, " +
TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " +
"WHERE " + ID + " = ?",
new String[] {body, messageId+""});
TYPE + " = (" + TYPE + " & " + (Types.TOTAL_MASK - maskOff) + " | " + maskOn + ") " +
"WHERE " + ID + " = ?",
new String[] {body, messageId + ""});
long threadId = getThreadIdForMessage(messageId);
@ -487,9 +508,11 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
}
public Cursor getMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
return db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[] {messageId+""},
null, null, null);
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""},
null, null, null);
setNotifyConverationListeners(cursor, getThreadIdForMessage(messageId));
return cursor;
}
public void deleteMessage(long messageId) {
@ -516,7 +539,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)");
db.delete(TABLE_NAME, where, new String[] {threadId+""});
db.delete(TABLE_NAME, where, new String[] {threadId + ""});
}
/*package*/ void deleteThreads(Set<Long> threadIds) {
@ -606,14 +629,17 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.THREAD_ID));
int status = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.STATUS));
int receiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.RECEIPT_COUNT));
Recipients recipients = getRecipientsFor(address);
DisplayRecord.Body body = getBody(cursor);
String mismatchDocument = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.MISMATCHED_IDENTITIES));
return new SmsMessageRecord(context, messageId, body, recipients,
recipients.getPrimaryRecipient(),
addressDeviceId,
dateSent, dateReceived, receiptCount, type,
threadId, status);
List<IdentityKeyMismatch> mismatches = getMismatches(mismatchDocument);
Recipients recipients = getRecipientsFor(address);
DisplayRecord.Body body = getBody(cursor);
return new SmsMessageRecord(context, messageId, body, recipients,
recipients.getPrimaryRecipient(),
addressDeviceId,
dateSent, dateReceived, receiptCount, type,
threadId, status, mismatches);
}
private Recipients getRecipientsFor(String address) {
@ -631,6 +657,18 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
}
}
private List<IdentityKeyMismatch> getMismatches(String document) {
try {
if (!TextUtils.isEmpty(document)) {
return JsonUtils.fromJson(document, IdentityKeyMismatchList.class).getList();
}
} catch (IOException e) {
Log.w(TAG, e);
}
return new LinkedList<>();
}
protected DisplayRecord.Body getBody(Cursor cursor) {
long type = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.TYPE));
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));

View File

@ -0,0 +1,10 @@
package org.thoughtcrime.securesms.database.documents;
import java.util.List;
public interface Document<T> {
public int size();
public List<T> getList();
}

View File

@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.database.documents;
import android.util.Log;
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.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import java.io.IOException;
public class IdentityKeyMismatch {
private static final String TAG = IdentityKeyMismatch.class.getSimpleName();
@JsonProperty(value = "r")
private long recipientId;
@JsonProperty(value = "k")
@JsonSerialize(using = IdentityKeySerializer.class)
@JsonDeserialize(using = IdentityKeyDeserializer.class)
private IdentityKey identityKey;
public IdentityKeyMismatch() {}
public IdentityKeyMismatch(long recipientId, IdentityKey identityKey) {
this.recipientId = recipientId;
this.identityKey = identityKey;
}
public long getRecipientId() {
return recipientId;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof IdentityKeyMismatch)) {
return false;
}
IdentityKeyMismatch that = (IdentityKeyMismatch)other;
return that.recipientId == this.recipientId && that.identityKey.equals(this.identityKey);
}
@Override
public int hashCode() {
return (int)recipientId ^ identityKey.hashCode();
}
private static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
@Override
public void serialize(IdentityKey value, JsonGenerator jsonGenerator, SerializerProvider serializers)
throws IOException
{
jsonGenerator.writeString(Base64.encodeBytes(value.serialize()));
}
}
private static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
@Override
public IdentityKey deserialize(JsonParser jsonParser, DeserializationContext ctxt)
throws IOException
{
try {
return new IdentityKey(Base64.decode(jsonParser.getValueAsString()), 0);
} catch (InvalidKeyException e) {
Log.w(TAG, e);
throw new IOException(e);
}
}
}
}

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.database.documents;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class IdentityKeyMismatchList implements Document<IdentityKeyMismatch> {
@JsonProperty(value = "m")
private List<IdentityKeyMismatch> mismatches;
public IdentityKeyMismatchList() {
this.mismatches = new LinkedList<>();
}
public IdentityKeyMismatchList(List<IdentityKeyMismatch> mismatches) {
this.mismatches = mismatches;
}
@Override
public int size() {
if (mismatches == null) return 0;
else return mismatches.size();
}
@Override
public List<IdentityKeyMismatch> getList() {
return mismatches;
}
}

View File

@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.database.documents;
import com.fasterxml.jackson.annotation.JsonProperty;
public class NetworkFailure {
@JsonProperty(value = "r")
private long recipientId;
public NetworkFailure(long recipientId) {
this.recipientId = recipientId;
}
public NetworkFailure() {}
public long getRecipientId() {
return recipientId;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof NetworkFailure)) return false;
NetworkFailure that = (NetworkFailure)other;
return this.recipientId == that.recipientId;
}
@Override
public int hashCode() {
return (int)recipientId;
}
}

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class NetworkFailureList implements Document<NetworkFailure> {
@JsonProperty(value = "l")
private List<NetworkFailure> failures;
public NetworkFailureList() {
this.failures = new LinkedList<>();
}
public NetworkFailureList(List<NetworkFailure> failures) {
this.failures = failures;
}
@Override
public int size() {
if (failures == null) return 0;
else return failures.size();
}
@Override
@JsonIgnore
public List<NetworkFailure> getList() {
return failures;
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
public class MessageDetailsLoader extends AbstractCursorLoader {
private final String type;
private final long messageId;
public MessageDetailsLoader(Context context, String type, long messageId) {
super(context);
this.type = type;
this.messageId = messageId;
}
@Override
public Cursor getCursor() {
switch (type) {
case MmsSmsDatabase.SMS_TRANSPORT:
return DatabaseFactory.getEncryptingSmsDatabase(context).getMessage(messageId);
case MmsSmsDatabase.MMS_TRANSPORT:
return DatabaseFactory.getMmsDatabase(context).getMessage(messageId);
default:
throw new AssertionError("no valid message type specified");
}
}
}

View File

@ -22,6 +22,8 @@ import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.mms.MediaNotFoundException;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
@ -30,6 +32,7 @@ import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import java.util.List;
import java.util.concurrent.ExecutionException;
/**
@ -52,10 +55,13 @@ public class MediaMmsMessageRecord extends MessageRecord {
long dateSent, long dateReceived, int deliveredCount,
long threadId, Body body,
ListenableFutureTask<SlideDeck> slideDeck,
int partCount, long mailbox)
int partCount, long mailbox,
List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> failures)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, deliveredCount, mailbox);
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, deliveredCount, mailbox,
mismatches, failures);
this.context = context.getApplicationContext();
this.partCount = partCount;

View File

@ -19,18 +19,20 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.GroupUtil;
import java.util.List;
/**
* The base class for message record models that are displayed in
* conversations, as opposed to models that are displayed in a thread list.
@ -48,16 +50,20 @@ public abstract class MessageRecord extends DisplayRecord {
private static final int MAX_DISPLAY_LENGTH = 2000;
private final Recipient individualRecipient;
private final int recipientDeviceId;
private final long id;
private final int deliveryStatus;
private final int receiptCount;
private final Recipient individualRecipient;
private final int recipientDeviceId;
private final long id;
private final int deliveryStatus;
private final int receiptCount;
private final List<IdentityKeyMismatch> mismatches;
private final List<NetworkFailure> networkFailures;
MessageRecord(Context context, long id, Body body, Recipients recipients,
Recipient individualRecipient, int recipientDeviceId,
long dateSent, long dateReceived, long threadId,
int deliveryStatus, int receiptCount, long type)
int deliveryStatus, int receiptCount, long type,
List<IdentityKeyMismatch> mismatches,
List<NetworkFailure> networkFailures)
{
super(context, body, recipients, dateSent, dateReceived, threadId, type);
this.id = id;
@ -65,6 +71,8 @@ public abstract class MessageRecord extends DisplayRecord {
this.recipientDeviceId = recipientDeviceId;
this.deliveryStatus = deliveryStatus;
this.receiptCount = receiptCount;
this.mismatches = mismatches;
this.networkFailures = networkFailures;
}
public abstract boolean isMms();
@ -145,6 +153,10 @@ public abstract class MessageRecord extends DisplayRecord {
return SmsDatabase.Types.isPendingSmsFallbackType(type);
}
public boolean isIdentityMismatchFailure() {
return mismatches != null && !mismatches.isEmpty();
}
public boolean isPendingSecureSmsFallback() {
return SmsDatabase.Types.isPendingSecureSmsFallbackType(type);
}
@ -181,6 +193,18 @@ public abstract class MessageRecord extends DisplayRecord {
return type;
}
public List<IdentityKeyMismatch> getIdentityKeyMismatches() {
return mismatches;
}
public List<NetworkFailure> getNetworkFailures() {
return networkFailures;
}
public boolean hasNetworkFailures() {
return networkFailures != null && !networkFailures.isEmpty();
}
protected SpannableString emphasisAdded(String sequence) {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@ -199,4 +223,5 @@ public abstract class MessageRecord extends DisplayRecord {
public int hashCode() {
return (int)getId();
}
}

View File

@ -21,9 +21,13 @@ import android.text.SpannableString;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.LinkedList;
/**
* Represents the message record model for MMS messages that are
* notifications (ie: they're pointers to undownloaded media).
@ -47,7 +51,8 @@ public class NotificationMmsMessageRecord extends MessageRecord {
long expiry, int status, byte[] transactionId, long mailbox)
{
super(context, id, new Body("", true), recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, receiptCount, mailbox);
dateSent, dateReceived, threadId, DELIVERY_STATUS_NONE, receiptCount, mailbox,
new LinkedList<IdentityKeyMismatch>(), new LinkedList<NetworkFailure>());
this.contentLocation = contentLocation;
this.messageSize = messageSize;

View File

@ -23,10 +23,15 @@ import android.text.SpannableString;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.protocol.Tag;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
import java.util.LinkedList;
import java.util.List;
/**
* The message record model which represents standard SMS messages.
*
@ -43,10 +48,11 @@ public class SmsMessageRecord extends MessageRecord {
long dateSent, long dateReceived,
int receiptCount,
long type, long threadId,
int status)
int status, List<IdentityKeyMismatch> mismatches)
{
super(context, id, body, recipients, individualRecipient, recipientDeviceId,
dateSent, dateReceived, threadId, receiptCount, getGenericDeliveryStatus(status), type);
dateSent, dateReceived, threadId, receiptCount, getGenericDeliveryStatus(status), type,
mismatches, new LinkedList<NetworkFailure>());
}
public long getType() {
@ -55,7 +61,9 @@ public class SmsMessageRecord extends MessageRecord {
@Override
public SpannableString getDisplayBody() {
if (isProcessedKeyExchange()) {
if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (isProcessedKeyExchange()) {
return new SpannableString("");
} else if (isStaleKeyExchange()) {
return emphasisAdded(context.getString(R.string.ConversationItem_error_received_stale_key_exchange_message));
@ -73,8 +81,6 @@ public class SmsMessageRecord extends MessageRecord {
return new SpannableString("");
} else if (isKeyExchange() && !isOutgoing()) {
return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_click_to_process));
} else if (SmsDatabase.Types.isFailedDecryptType(type)) {
return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message));
} else if (SmsDatabase.Types.isDuplicateMessageType(type)) {
return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message));
} else if (SmsDatabase.Types.isDecryptInProgressType(type)) {

View File

@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.jobqueue.JobParameters;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
@ -37,13 +38,14 @@ import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.util.guava.Optional;
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
import org.whispersystems.textsecure.api.messages.TextSecureEnvelope;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
import ws.com.google.android.mms.MmsException;
@ -52,14 +54,20 @@ public class PushDecryptJob extends MasterSecretJob {
public static final String TAG = PushDecryptJob.class.getSimpleName();
private final long messageId;
private final long smsMessageId;
public PushDecryptJob(Context context, long messageId, String sender) {
public PushDecryptJob(Context context, long pushMessageId, String sender) {
this(context, pushMessageId, -1, sender);
}
public PushDecryptJob(Context context, long pushMessageId, long smsMessageId, String sender) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withRequirement(new MasterSecretRequirement(context))
.withGroupId(sender)
.create());
this.messageId = messageId;
this.messageId = pushMessageId;
this.smsMessageId = smsMessageId;
}
@Override
@ -74,7 +82,7 @@ public class PushDecryptJob extends MasterSecretJob {
PushDatabase database = DatabaseFactory.getPushDatabase(context);
TextSecureEnvelope envelope = database.get(messageId);
handleMessage(masterSecret, envelope);
handleMessage(masterSecret, envelope, smsMessageId);
database.delete(messageId);
}
@ -88,7 +96,7 @@ public class PushDecryptJob extends MasterSecretJob {
}
private void handleMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
private void handleMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
try {
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
@ -98,59 +106,72 @@ public class PushDecryptJob extends MasterSecretJob {
TextSecureMessage message = cipher.decrypt(envelope);
if (message.isEndSession()) handleEndSessionMessage(masterSecret, recipientId, envelope, message);
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message);
else handleTextMessage(masterSecret, envelope, message);
if (message.isEndSession()) handleEndSessionMessage(masterSecret, recipientId, envelope, message, smsMessageId);
else if (message.isGroupUpdate()) handleGroupMessage(masterSecret, envelope, message, smsMessageId);
else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId);
else handleTextMessage(masterSecret, envelope, message, smsMessageId);
if (envelope.isPreKeyWhisperMessage()) {
ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context));
}
} catch (InvalidVersionException e) {
Log.w(TAG, e);
handleInvalidVersionMessage(masterSecret, envelope);
handleInvalidVersionMessage(masterSecret, envelope, smsMessageId);
} catch (InvalidMessageException | InvalidKeyIdException | InvalidKeyException | MmsException | RecipientFormattingException e) {
Log.w(TAG, e);
handleCorruptMessage(masterSecret, envelope);
handleCorruptMessage(masterSecret, envelope, smsMessageId);
} catch (NoSessionException e) {
Log.w(TAG, e);
handleNoSessionMessage(masterSecret, envelope);
handleNoSessionMessage(masterSecret, envelope, smsMessageId);
} catch (LegacyMessageException e) {
Log.w(TAG, e);
handleLegacyMessage(masterSecret, envelope);
handleLegacyMessage(masterSecret, envelope, smsMessageId);
} catch (DuplicateMessageException e) {
Log.w(TAG, e);
handleDuplicateMessage(masterSecret, envelope);
handleDuplicateMessage(masterSecret, envelope, smsMessageId);
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
handleUntrustedIdentityMessage(masterSecret, envelope);
handleUntrustedIdentityMessage(masterSecret, envelope, smsMessageId);
}
}
private void handleEndSessionMessage(MasterSecret masterSecret, long recipientId,
TextSecureEnvelope envelope, TextSecureMessage message)
TextSecureEnvelope envelope, TextSecureMessage message,
long smsMessageId)
{
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(),
"", Optional.<TextSecureGroup>absent());
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
IncomingTextMessage incomingTextMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(),
"", Optional.<TextSecureGroup>absent());
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage);
long threadId;
if (smsMessageId <= 0) {
IncomingEndSessionMessage incomingEndSessionMessage = new IncomingEndSessionMessage(incomingTextMessage);
Pair<Long, Long> messageAndThreadId = smsDatabase.insertMessageInbox(masterSecret, incomingEndSessionMessage);
threadId = messageAndThreadId.second;
} else {
smsDatabase.markAsEndSession(smsMessageId);
threadId = smsDatabase.getThreadIdForMessage(smsMessageId);
}
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
sessionStore.deleteAllSessions(recipientId);
SecurityEvent.broadcastSecurityUpdateEvent(context, messageAndThreadId.second);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
SecurityEvent.broadcastSecurityUpdateEvent(context, threadId);
MessageNotifier.updateNotification(context, masterSecret, threadId);
}
private void handleGroupMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message) {
private void handleGroupMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message, long smsMessageId) {
GroupMessageProcessor.process(context, masterSecret, envelope, message);
if (smsMessageId > 0) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId);
}
}
private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message)
private void handleMediaMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message, long smsMessageId)
throws MmsException
{
String localNumber = TextSecurePreferences.getLocalNumber(context);
@ -174,73 +195,123 @@ public class PushDecryptJob extends MasterSecretJob {
.getJobManager()
.add(new AttachmentDownloadJob(context, messageAndThreadId.first));
if (smsMessageId >= 0) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId);
}
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleTextMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, TextSecureMessage message) {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
private void handleTextMessage(MasterSecret masterSecret, TextSecureEnvelope envelope,
TextSecureMessage message, long smsMessageId)
{
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
String body = message.getBody().isPresent() ? message.getBody().get() : "";
if (smsMessageId > 0) {
database.updateBundleMessageBody(masterSecret, smsMessageId, body);
} else {
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(),
envelope.getSourceDevice(),
message.getTimestamp(), body,
message.getGroupInfo());
if (message.isSecure()) {
textMessage = new IncomingEncryptedMessage(textMessage, body);
if (message.isSecure()) {
textMessage = new IncomingEncryptedMessage(textMessage, body);
}
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, textMessage);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
}
private void handleInvalidVersionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsInvalidVersionKeyExchange(messageAndThreadId.first);
private void handleInvalidVersionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsInvalidVersionKeyExchange(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsInvalidVersionKeyExchange(smsMessageId);
}
}
private void handleCorruptMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptFailed(messageAndThreadId.first);
private void handleCorruptMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsDecryptFailed(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsDecryptFailed(smsMessageId);
}
}
private void handleNoSessionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsNoSession(messageAndThreadId.first);
private void handleNoSessionMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsNoSession(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsNoSession(smsMessageId);
}
}
private void handleLegacyMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
DatabaseFactory.getEncryptingSmsDatabase(context).markAsLegacyVersion(messageAndThreadId.first);
private void handleLegacyMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
EncryptingSmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
if (smsMessageId <= 0) {
Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
smsDatabase.markAsLegacyVersion(messageAndThreadId.first);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
smsDatabase.markAsLegacyVersion(smsMessageId);
}
}
private void handleDuplicateMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
// Let's start ignoring these now.
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
// DatabaseFactory.getEncryptingSmsDatabase(context).markAsDecryptDuplicate(messageAndThreadId.first);
private void handleDuplicateMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
// Let's start ignoring these now
// SmsDatabase smsDatabase = DatabaseFactory.getEncryptingSmsDatabase(context);
//
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
// if (smsMessageId <= 0) {
// Pair<Long, Long> messageAndThreadId = insertPlaceholder(masterSecret, envelope);
// smsDatabase.markAsDecryptDuplicate(messageAndThreadId.first);
// MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
// } else {
// smsDatabase.markAsDecryptDuplicate(smsMessageId);
// }
}
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, TextSecureEnvelope envelope) {
String encoded = Base64.encodeBytes(envelope.getMessage());
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded,
Optional.<TextSecureGroup>absent());
private void handleUntrustedIdentityMessage(MasterSecret masterSecret, TextSecureEnvelope envelope, long smsMessageId) {
try {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, envelope.getSource(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
PreKeyWhisperMessage whisperMessage = new PreKeyWhisperMessage(envelope.getMessage());
IdentityKey identityKey = whisperMessage.getIdentityKey();
String encoded = Base64.encodeBytes(envelope.getMessage());
IncomingTextMessage textMessage = new IncomingTextMessage(envelope.getSource(), envelope.getSourceDevice(),
envelope.getTimestamp(), encoded,
Optional.<TextSecureGroup>absent());
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
Pair<Long, Long> messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context)
.insertMessageInbox(masterSecret, bundleMessage);
if (smsMessageId <= 0) {
IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded);
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, bundleMessage);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
database.addMismatchedIdentity(messageAndThreadId.first, recipientId, identityKey);
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
} else {
database.updateMessageBody(masterSecret, smsMessageId, encoded);
database.markAsPreKeyBundle(smsMessageId);
database.addMismatchedIdentity(smsMessageId, recipientId, identityKey);
}
} catch (RecipientFormattingException | InvalidMessageException | InvalidVersionException e) {
throw new AssertionError(e);
}
}
private Pair<Long, Long> insertPlaceholder(MasterSecret masterSecret, TextSecureEnvelope envelope) {

View File

@ -8,13 +8,14 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirement;
import org.thoughtcrime.securesms.mms.PartParser;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.whispersystems.jobqueue.JobParameters;
@ -25,9 +26,11 @@ import org.whispersystems.textsecure.api.messages.TextSecureAttachment;
import org.whispersystems.textsecure.api.messages.TextSecureGroup;
import org.whispersystems.textsecure.api.messages.TextSecureMessage;
import org.whispersystems.textsecure.api.push.PushAddress;
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException;
import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.textsecure.api.util.InvalidNumberException;
import org.whispersystems.textsecure.internal.push.PushMessageProtos;
import java.io.IOException;
import java.util.LinkedList;
@ -47,8 +50,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
@Inject transient TextSecureMessageSenderFactory messageSenderFactory;
private final long messageId;
private final long filterRecipientId;
public PushGroupSendJob(Context context, long messageId, String destination) {
public PushGroupSendJob(Context context, long messageId, String destination, long filterRecipientId) {
super(context, JobParameters.newBuilder()
.withPersistence()
.withGroupId(destination)
@ -57,21 +61,25 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
.withRetryCount(5)
.create());
this.messageId = messageId;
this.messageId = messageId;
this.filterRecipientId = filterRecipientId;
}
@Override
public void onAdded() {
DatabaseFactory.getMmsDatabase(context)
.markAsSending(messageId);
}
@Override
public void onSend(MasterSecret masterSecret) throws MmsException, IOException, NoSuchMessageException {
public void onSend(MasterSecret masterSecret)
throws MmsException, IOException, NoSuchMessageException, RecipientFormattingException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
try {
deliver(masterSecret, message);
deliver(masterSecret, message, filterRecipientId);
database.markAsPush(messageId);
database.markAsSecure(messageId);
@ -82,16 +90,27 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (EncapsulatedExceptions e) {
Log.w(TAG, e);
if (!e.getUnregisteredUserExceptions().isEmpty()) {
database.markAsSentFailed(messageId);
List<NetworkFailure> failures = new LinkedList<>();
for (NetworkFailureException nfe : e.getNetworkExceptions()) {
Recipient recipient = RecipientFactory.getRecipientsFromString(context, nfe.getE164number(), false).getPrimaryRecipient();
failures.add(new NetworkFailure(recipient.getRecipientId()));
}
// for (UnregisteredUserException uue : e.getUnregisteredUserExceptions()) {
// Recipient recipient = RecipientFactory.getRecipientsFromString(context, uue.getE164Number(), false).getPrimaryRecipient();
// failures.add(new NetworkFailure(recipient.getRecipientId(), NetworkFailure.UNREGISTERED_FAILURE));
// }
for (UntrustedIdentityException uie : e.getUntrustedIdentityExceptions()) {
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
database.markAsSentFailed(messageId);
Recipient recipient = RecipientFactory.getRecipientsFromString(context, uie.getE164Number(), false).getPrimaryRecipient();
database.addMismatchedIdentity(messageId, recipient.getRecipientId(), uie.getIdentityKey());
}
database.addFailures(messageId, failures);
database.markAsSentFailed(messageId);
database.markAsPush(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
}
}
@ -107,14 +126,17 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
DatabaseFactory.getMmsDatabase(context).markAsSentFailed(messageId);
}
private void deliver(MasterSecret masterSecret, SendReq message)
private void deliver(MasterSecret masterSecret, SendReq message, long filterRecipientId)
throws IOException, RecipientFormattingException, InvalidNumberException, EncapsulatedExceptions
{
TextSecureMessageSender messageSender = messageSenderFactory.create(masterSecret);
byte[] groupId = GroupUtil.getDecodedId(message.getTo()[0].getString());
Recipients recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, false);
List<PushAddress> addresses = getPushAddresses(recipients);
List<TextSecureAttachment> attachments = getAttachments(masterSecret, message);
List<PushAddress> addresses;
if (filterRecipientId >= 0) addresses = getPushAddresses(filterRecipientId);
else addresses = getPushAddresses(recipients);
if (MmsSmsColumns.Types.isGroupUpdate(message.getDatabaseMessageBox()) ||
MmsSmsColumns.Types.isGroupQuit(message.getDatabaseMessageBox()))
@ -149,4 +171,10 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
return addresses;
}
private List<PushAddress> getPushAddresses(long filterRecipientId) throws InvalidNumberException {
List<PushAddress> addresses = new LinkedList<>();
addresses.add(getPushAddress(RecipientFactory.getRecipientForId(context, filterRecipientId, false)));
return addresses;
}
}

View File

@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.PartParser;
@ -16,7 +17,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
@ -55,12 +55,15 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
@Override
public void onAdded() {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
mmsDatabase.markAsSending(messageId);
mmsDatabase.markAsPush(messageId);
}
@Override
public void onSend(MasterSecret masterSecret)
throws RetryLaterException, MmsException, NoSuchMessageException, UndeliverableMessageException
throws RetryLaterException, MmsException, NoSuchMessageException,
UndeliverableMessageException, RecipientFormattingException
{
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
SendReq message = database.getOutgoingMessage(masterSecret, messageId);
@ -80,9 +83,13 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
database.markAsPendingSecureSmsFallback(messageId);
notifyMediaMessageDeliveryFailed(context, messageId);
} catch (UntrustedIdentityException uie) {
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(message.getTo()[0].getString(), uie.getIdentityKey());
DatabaseFactory.getEncryptingSmsDatabase(context).insertMessageInbox(masterSecret, identityUpdateMessage);
Log.w(TAG, uie);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, uie.getE164Number(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
database.addMismatchedIdentity(messageId, recipientId, uie.getIdentityKey());
database.markAsSentFailed(messageId);
database.markAsPush(messageId);
}
}

View File

@ -9,12 +9,14 @@ import org.thoughtcrime.securesms.crypto.storage.TextSecureAxolotlStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.InjectableType;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.sms.IncomingIdentityUpdateMessage;
import org.thoughtcrime.securesms.transport.InsecureFallbackApprovalException;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
@ -47,14 +49,16 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
@Override
public void onAdded() {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
smsDatabase.markAsSending(messageId);
smsDatabase.markAsPush(messageId);
}
@Override
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
String destination = record.getIndividualRecipient().getNumber();
public void onSend(MasterSecret masterSecret) throws NoSuchMessageException, RetryLaterException, RecipientFormattingException {
EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context);
SmsMessageRecord record = database.getMessage(masterSecret, messageId);
String destination = record.getIndividualRecipient().getNumber();
try {
Log.w(TAG, "Sending message: " + messageId);
@ -74,9 +78,12 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
MessageNotifier.notifyMessageDeliveryFailed(context, record.getRecipients(), record.getThreadId());
} catch (UntrustedIdentityException e) {
Log.w(TAG, e);
IncomingIdentityUpdateMessage identityUpdateMessage = IncomingIdentityUpdateMessage.createFor(e.getE164Number(), e.getIdentityKey());
database.insertMessageInbox(masterSecret, identityUpdateMessage);
Recipients recipients = RecipientFactory.getRecipientsFromString(context, e.getE164Number(), false);
long recipientId = recipients.getPrimaryRecipient().getRecipientId();
database.addMismatchedIdentity(record.getId(), recipientId, e.getIdentityKey());
database.markAsSentFailed(record.getId());
database.markAsPush(record.getId());
}
}

View File

@ -28,7 +28,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class Recipients {
public class Recipients implements Iterable<Recipient> {
private List<Recipient> recipients;
@ -165,4 +165,9 @@ public class Recipients {
public int describeContents() {
return 0;
}
@Override
public Iterator<Recipient> iterator() {
return recipients.iterator();
}
}

View File

@ -111,16 +111,24 @@ public class MessageSender {
}
}
public static void resendGroupMessage(Context context, MasterSecret masterSecret, MessageRecord messageRecord, long filterRecipientId) {
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
Recipients recipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageRecord.getId());
sendGroupPush(context, recipients, messageRecord.getId(), filterRecipientId);
}
public static void resend(Context context, MasterSecret masterSecret, MessageRecord messageRecord) {
try {
Recipients recipients = messageRecord.getRecipients();
long messageId = messageRecord.getId();
boolean forceSms = messageRecord.isForcedSms();
boolean keyExchange = messageRecord.isKeyExchange();
if (messageRecord.isMms()) {
Recipients recipients = DatabaseFactory.getMmsAddressDatabase(context).getRecipientsForId(messageId);
sendMediaMessage(context, masterSecret, recipients, forceSms, messageId);
} else {
Recipients recipients = messageRecord.getRecipients();
sendTextMessage(context, recipients, forceSms, keyExchange, messageId);
}
} catch (MmsException e) {
@ -135,7 +143,7 @@ public class MessageSender {
if (!forceSms && isSelfSend(context, recipients)) {
sendMediaSelf(context, masterSecret, messageId);
} else if (isGroupPushSend(recipients)) {
sendGroupPush(context, recipients, messageId);
sendGroupPush(context, recipients, messageId, -1);
} else if (!forceSms && isPushMediaSend(context, recipients)) {
sendMediaPush(context, recipients, messageId);
} else {
@ -186,9 +194,9 @@ public class MessageSender {
jobManager.add(new PushMediaSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
}
private static void sendGroupPush(Context context, Recipients recipients, long messageId) {
private static void sendGroupPush(Context context, Recipients recipients, long messageId, long filterRecipientId) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
jobManager.add(new PushGroupSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber()));
jobManager.add(new PushGroupSendJob(context, messageId, recipients.getPrimaryRecipient().getNumber(), filterRecipientId));
}
private static void sendSms(Context context, Recipients recipients, long messageId) {

View File

@ -17,6 +17,10 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.text.format.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Locale;
import org.thoughtcrime.securesms.R;
@ -71,4 +75,17 @@ public class DateUtils extends android.text.format.DateUtils {
return DateUtils.formatDateTime(c, timestamp, formatFlags);
}
}
public static SimpleDateFormat getDetailedDateFormatter(Context context) {
String dateFormatPattern;
if (DateFormat.is24HourFormat(context)) {
dateFormatPattern = "MMM d, yyyy HH:mm:ss zzz";
} else {
dateFormatPattern = "MMM d, yyyy hh:mm:ssa zzz";
}
return new SimpleDateFormat(dateFormatPattern, Locale.getDefault());
}
}

View File

@ -19,14 +19,12 @@ import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.reflect.TypeToken;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.thoughtcrime.securesms.R;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.concurrent.ExecutorService;
@ -35,6 +33,8 @@ import java.util.regex.Pattern;
public class Emoji {
private static final String TAG = Emoji.class.getSimpleName();
private static ExecutorService executor = Util.newSingleThreadedLifoExecutor();
public static final int[][] PAGES = {
@ -303,9 +303,16 @@ public class Emoji {
}
String serialized = prefs.getString(EMOJI_LRU_PREFERENCE, "[]");
Type type = new TypeToken<LinkedHashSet<String>>() {
}.getType();
recentlyUsed = new Gson().fromJson(serialized, type);
try {
recentlyUsed = JsonUtils.getMapper().readValue(serialized, TypeFactory.defaultInstance()
.constructCollectionType(LinkedHashSet.class, String.class));
} catch (IOException e) {
Log.w(TAG, e);
recentlyUsed = new LinkedHashSet<>();
}
recentlyUsed = new LinkedHashSet<>();
}
public static String[] getRecentlyUsed(Context context) {
@ -337,10 +344,15 @@ public class Emoji {
@Override
protected Void doInBackground(Void... params) {
String serialized = new Gson().toJson(latestRecentlyUsed);
prefs.edit()
.putString(EMOJI_LRU_PREFERENCE, serialized)
.apply();
try {
String serialized = JsonUtils.toJson(latestRecentlyUsed);
prefs.edit()
.putString(EMOJI_LRU_PREFERENCE, serialized)
.apply();
} catch (IOException e) {
Log.w(TAG, e);
}
return null;
}
}.execute();

View File

@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.util;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public static <T> T fromJson(String serialized, Class<T> clazz) throws IOException {
return objectMapper.readValue(serialized, clazz);
}
public static <T> T fromJson(InputStreamReader serialized, Class<T> clazz) throws IOException {
return objectMapper.readValue(serialized, clazz);
}
public static String toJson(Object object) throws IOException {
return objectMapper.writeValueAsString(object);
}
public static ObjectMapper getMapper() {
return objectMapper;
}
}

View File

@ -0,0 +1,83 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Typeface;
import android.net.Uri;
import android.provider.Contacts.Intents;
import android.provider.ContactsContract.QuickContact;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.view.View;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.Recipients;
public class RecipientViewUtil {
public static CharSequence formatFrom(Context context, Recipient recipient) {
return formatFrom(context, new Recipients(recipient));
}
public static CharSequence formatFrom(Context context, Recipients from) {
return formatFrom(context, from, true);
}
public static CharSequence formatFrom(Context context, Recipients from, boolean read) {
int attributes[] = new int[] {R.attr.conversation_list_item_count_color};
TypedArray colors = context.obtainStyledAttributes(attributes);
final String fromString;
final boolean isUnnamedGroup = from.isGroupRecipient() && TextUtils.isEmpty(from.getPrimaryRecipient().getName());
if (isUnnamedGroup) {
fromString = context.getString(R.string.ConversationActivity_unnamed_group);
} else {
fromString = from.toShortString();
}
SpannableStringBuilder builder = new SpannableStringBuilder(fromString);
final int typeface;
if (isUnnamedGroup) {
if (!read) typeface = Typeface.BOLD_ITALIC;
else typeface = Typeface.ITALIC;
} else if (!read) {
typeface = Typeface.BOLD;
} else {
typeface = Typeface.NORMAL;
}
builder.setSpan(new StyleSpan(typeface), 0, builder.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
colors.recycle();
return builder;
}
public static void setContactPhoto(final Context context, final ImageView imageView, final Recipient recipient, boolean showQuickContact) {
if (recipient == null) return;
imageView.setImageBitmap(recipient.getContactPhoto());
if (!recipient.isGroupRecipient() && showQuickContact) {
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (recipient.getContactUri() != null) {
QuickContact.showQuickContact(context, imageView, recipient.getContactUri(), QuickContact.MODE_LARGE, null);
} else {
Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, Uri.fromParts("tel", recipient.getNumber(), null));
context.startActivity(intent);
}
}
});
} else {
imageView.setOnClickListener(null);
}
}
}

View File

@ -1,109 +0,0 @@
// adapted from https://gist.github.com/CedricGatay/4e21ce855bd2562f9257
// which was adapted from https://gist.github.com/dmarcato/d7c91b94214acd936e42
def toCamelCase(String string) {
String result = ""
string.findAll("[^\\W]+") { String word ->
result += word.capitalize()
}
return result
}
afterEvaluate { project ->
Configuration runtimeConfiguration = project.configurations.getByName('compile')
println runtimeConfiguration
ResolutionResult resolution = runtimeConfiguration.incoming.resolutionResult
// Forces resolve of configuration
ModuleVersionIdentifier module = resolution.getAllComponents().find {
it.moduleVersion.name.equals("play-services")
}.moduleVersion
def playServicesLibName = toCamelCase("${module.group} ${module.name} ${module.version}")
String prepareTaskName = "prepare${playServicesLibName}Library"
File playServiceRootFolder = project.tasks.find { it.name.equals(prepareTaskName) }.explodedDir
def tmpDir = new File(project.buildDir, 'intermediates/tmp')
tmpDir.mkdirs()
def libFile = new File(tmpDir, "${playServicesLibName}.marker")
def strippedClassFileName = "${playServicesLibName}.jar"
def classesStrippedJar = new File(tmpDir, strippedClassFileName)
def packageToExclude = [
"com/google/ads/**",
//"com/google/android/gms/actions/**",
"com/google/android/gms/ads/**",
"com/google/android/gms/analytics/**",
//"com/google/android/gms/appindexing/**",
//"com/google/android/gms/appstate/**",
//"com/google/android/gms/auth/**",
//"com/google/android/gms/cast/**",
"com/google/android/gms/drive/**",
"com/google/android/gms/fitness/**",
"com/google/android/gms/games/**",
//"com/google/android/gms/gcm/**",
//"com/google/android/gms/identity/**",
"com/google/android/gms/location/**",
"com/google/android/gms/maps/**",
"com/google/android/gms/panorama/**",
"com/google/android/gms/plus/**",
//"com/google/android/gms/security/**",
//"com/google/android/gms/tagmanager/**",
"com/google/android/gms/wallet/**",
"com/google/android/gms/wearable/**"
]
Task stripPlayServices = project.tasks.create(name: 'stripPlayServices', group: "Strip") {
inputs.files new File(playServiceRootFolder, "classes.jar")
outputs.dir playServiceRootFolder
description 'Strip useless packages from Google Play Services library to avoid reaching dex limit'
doLast {
def packageExcludesAsString = packageToExclude.join(",")
if (libFile.exists()
&& libFile.text == packageExcludesAsString
&& classesStrippedJar.exists()){
println "Play services already stripped"
copy {
from(file(classesStrippedJar))
into(file(playServiceRootFolder))
rename { fileName ->
fileName = "classes.jar"
}
}
}else {
copy {
from(file(new File(playServiceRootFolder, "classes.jar")))
into(file(playServiceRootFolder))
rename { fileName ->
fileName = "classes_orig.jar"
}
}
tasks.create(name: "stripPlayServices" + module.version, type: Jar) {
destinationDir = playServiceRootFolder
archiveName = "classes.jar"
from(zipTree(new File(playServiceRootFolder, "classes_orig.jar"))) {
exclude packageToExclude
}
}.execute()
delete file(new File(playServiceRootFolder, "classes_orig.jar"))
copy {
from(file(new File(playServiceRootFolder, "classes.jar")))
into(file(tmpDir))
rename { fileName ->
fileName = strippedClassFileName
}
}
libFile.text = packageExcludesAsString
}
}
}
project.tasks.findAll {
it.name.startsWith('prepare') && it.name.endsWith('Dependencies')
}.each { Task task ->
task.dependsOn stripPlayServices
}
}