diff --git a/build.gradle b/build.gradle index 464083acbf..0ccd2fc52d 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ dependencies { compile 'org.whispersystems:jobmanager:1.0.2' compile 'org.whispersystems:libpastelog:1.0.7' compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - compile 'org.whispersystems:signal-service-android:2.4.0' + compile 'org.whispersystems:signal-service-android:2.4.1' compile 'com.h6ah4i.android.compat:mulsellistprefcompat:1.0.0' compile 'com.google.zxing:core:3.2.1' @@ -131,7 +131,7 @@ dependencyVerification { 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88', 'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb', - 'org.whispersystems:signal-service-android:b018f599189b0321426a29c7f21e88e5c8c14debe613593ed728d1f522f85fe6', + 'org.whispersystems:signal-service-android:b5449d6d74c16256591f675e599b1e588d13342ec8f52e2e598af0fa1e919259', 'com.h6ah4i.android.compat:mulsellistprefcompat:47167c5cb796de1a854788e9ff318358e36c8fb88123baaa6e38fb78511dfabe', 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', 'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729', @@ -140,23 +140,24 @@ dependencyVerification { 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', - 'org.whispersystems:signal-service-java:dda6c3b15872fee7a8980e0898a813aca6a603f8dc142d5354f30e2cc005ea17', 'org.whispersystems:signal-protocol-android:1b4b9d557c8eaf861797ff683990d482d4aa8e9f23d9b17ff0cc67a02f38cb19', + 'org.whispersystems:signal-service-java:4a55341f3a340a59c541d06fb80638a770d7ba9c2eac727757bea42c0a341ec5', 'com.google.android.gms:play-services-basement:e1d29b21e02fd2a63e5a31807415cbb17a59568e27e3254181c01ffae10659bf', + 'org.whispersystems:curve25519-android:bf6c34223d45d2f2813a8efcab9923caf99115115c760c9acea680bcb42d23c0', + 'org.whispersystems:signal-protocol-java:a835cd0609cf116a74651bd0aa748db9392bba48c2d2af787757b8a1b50d131c', 'com.googlecode.libphonenumber:libphonenumber:9625de9d2270e9a280ff4e6d9ef3106573fb4828773fd32c9b7614f4e17d2811', 'com.google.protobuf:protobuf-java:e0c1c64575c005601725e7c6a02cebf9e1285e888f756b2a1d73ffa8d725cc74', 'com.squareup.okhttp:okhttp:89b7f63e2e5b6c410266abc14f50fe52ea8d2d8a57260829e499b1cd9f0e61af', 'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d', - 'org.whispersystems:curve25519-android:bf6c34223d45d2f2813a8efcab9923caf99115115c760c9acea680bcb42d23c0', - 'org.whispersystems:signal-protocol-java:a835cd0609cf116a74651bd0aa748db9392bba48c2d2af787757b8a1b50d131c', + 'org.whispersystems:curve25519-java:00f1d4919f759055f41f7853a3d475dc7c8decf0dbf045ae93414f8f23b066cc', 'com.squareup.okio:okio:5e1098bd3fdee4c3347f5ab815b40ba851e4ab1b348c5e49a5b0362f0ce6e978', 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', - 'org.whispersystems:curve25519-java:00f1d4919f759055f41f7853a3d475dc7c8decf0dbf045ae93414f8f23b066cc', 'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423', ] } + android { compileSdkVersion 22 buildToolsVersion '23.0.2' diff --git a/src/org/thoughtcrime/securesms/database/GroupDatabase.java b/src/org/thoughtcrime/securesms/database/GroupDatabase.java index af5506c7de..5eedd027e0 100644 --- a/src/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/src/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -81,6 +81,10 @@ public class GroupDatabase extends Database { return record; } + public boolean isUnknownGroup(byte[] groupId) { + return getGroup(groupId) == null; + } + public Reader getGroupsFilteredByTitle(String constraint) { Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, TITLE + " LIKE ?", new String[]{"%" + constraint + "%"}, @@ -129,6 +133,7 @@ public class GroupDatabase extends Database { contentValues.put(ACTIVE, 1); databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); + notifyConversationListListeners(); } public void update(byte[] groupId, String title, SignalServiceAttachmentPointer avatar) { @@ -147,6 +152,7 @@ public class GroupDatabase extends Database { RecipientFactory.clearCache(context); notifyDatabaseListeners(); + notifyConversationListListeners(); } public void updateTitle(byte[] groupId, String title) { diff --git a/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java index 42fd91c779..46605418ee 100644 --- a/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/TextSecureCommunicationModule.java @@ -15,11 +15,13 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.PushGroupSendJob; +import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.jobs.PushMediaSendJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushTextSendJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; +import org.thoughtcrime.securesms.jobs.RequestGroupInfoJob; import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.TextSecurePushTrustStore; import org.thoughtcrime.securesms.service.MessageRetrievalService; @@ -49,7 +51,9 @@ import dagger.Provides; MultiDeviceBlockedUpdateJob.class, DeviceListFragment.class, RefreshAttributesJob.class, - GcmRefreshJob.class}) + GcmRefreshJob.class, + RequestGroupInfoJob.class, + PushGroupUpdateJob.class}) public class TextSecureCommunicationModule { private final Context context; diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 3a740115c3..cfd6911d8f 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; +import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.recipients.RecipientFactory; @@ -29,6 +30,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; import java.util.HashSet; import java.util.LinkedList; @@ -61,12 +63,14 @@ public class GroupMessageProcessor { byte[] id = group.getGroupId(); GroupRecord record = database.getGroup(id); - if (record != null && group.getType() == SignalServiceGroup.Type.UPDATE) { + if (record != null && group.getType() == Type.UPDATE) { return handleGroupUpdate(context, masterSecret, envelope, group, record, outgoing); - } else if (record == null && group.getType() == SignalServiceGroup.Type.UPDATE) { + } else if (record == null && group.getType() == Type.UPDATE) { return handleGroupCreate(context, masterSecret, envelope, group, outgoing); - } else if (record != null && group.getType() == SignalServiceGroup.Type.QUIT) { + } else if (record != null && group.getType() == Type.QUIT) { return handleGroupLeave(context, masterSecret, envelope, group, record, outgoing); + } else if (record != null && group.getType() == Type.REQUEST_INFO) { + return handleGroupInfoRequest(context, envelope, group, record); } else { Log.w(TAG, "Received unknown type, ignoring..."); return null; @@ -144,6 +148,20 @@ public class GroupMessageProcessor { return storeMessage(context, masterSecret, envelope, group, builder.build(), outgoing); } + private static Long handleGroupInfoRequest(@NonNull Context context, + @NonNull SignalServiceEnvelope envelope, + @NonNull SignalServiceGroup group, + @NonNull GroupRecord record) + { + if (record.getMembers().contains(envelope.getSource())) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new PushGroupUpdateJob(context, envelope.getSource(), group.getGroupId())); + } + + return null; + } + private static Long handleGroupLeave(@NonNull Context context, @NonNull MasterSecretUnion masterSecret, @NonNull SignalServiceEnvelope envelope, diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 2ff8993a87..d6c3f410b7 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; @@ -132,9 +133,10 @@ public class PushDecryptJob extends ContextJob { private void handleMessage(MasterSecretUnion masterSecret, SignalServiceEnvelope envelope, Optional smsMessageId) { try { - SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); - SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context)); - SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); + SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context)); + SignalServiceCipher cipher = new SignalServiceCipher(localAddress, axolotlStore); SignalServiceContent content = cipher.decrypt(envelope); @@ -146,6 +148,10 @@ public class PushDecryptJob extends ContextJob { else if (message.isExpirationUpdate()) handleExpirationUpdate(masterSecret, envelope, message, smsMessageId); else if (message.getAttachments().isPresent()) handleMediaMessage(masterSecret, envelope, message, smsMessageId); else handleTextMessage(masterSecret, envelope, message, smsMessageId); + + if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(message.getGroupInfo().get().getGroupId())) { + handleUnknownGroupMessage(envelope, message.getGroupInfo().get()); + } } else if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); @@ -221,6 +227,14 @@ public class PushDecryptJob extends ContextJob { } } + private void handleUnknownGroupMessage(@NonNull SignalServiceEnvelope envelope, + @NonNull SignalServiceGroup group) + { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new RequestGroupInfoJob(context, envelope.getSource(), group.getGroupId())); + } + private void handleExpirationUpdate(@NonNull MasterSecretUnion masterSecret, @NonNull SignalServiceEnvelope envelope, @NonNull SignalServiceDataMessage message, @@ -254,6 +268,8 @@ public class PushDecryptJob extends ContextJob { @NonNull Optional smsMessageId) throws MmsException { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + Long threadId; if (message.getMessage().isGroupUpdate()) { @@ -266,6 +282,10 @@ public class PushDecryptJob extends ContextJob { threadId = handleSynchronizeSentTextMessage(masterSecret, message, smsMessageId); } + if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(message.getMessage().getGroupInfo().get().getGroupId())) { + handleUnknownGroupMessage(envelope, message.getMessage().getGroupInfo().get()); + } + if (threadId != null) { DatabaseFactory.getThreadDatabase(getContext()).setRead(threadId); MessageNotifier.updateNotification(getContext(), masterSecret.getMasterSecret().orNull()); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java new file mode 100644 index 0000000000..607a4c2a9b --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupUpdateJob.java @@ -0,0 +1,97 @@ +package org.thoughtcrime.securesms.jobs; + + +import android.content.Context; +import android.util.Log; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import javax.inject.Inject; + +public class PushGroupUpdateJob extends ContextJob implements InjectableType { + + private static final String TAG = RequestGroupInfoJob.class.getSimpleName(); + + private static final long serialVersionUID = 0L; + + @Inject transient TextSecureMessageSenderFactory messageSenderFactory; + + private final String source; + private final byte[] groupId; + + + public PushGroupUpdateJob(Context context, String source, byte[] groupId) { + super(context, JobParameters.newBuilder() + .withPersistence() + .withRequirement(new NetworkRequirement(context)) + .withRetryCount(50) + .create()); + + this.source = source; + this.groupId = groupId; + } + + @Override + public void onAdded() {} + + @Override + public void onRun() throws IOException, UntrustedIdentityException { + SignalServiceMessageSender messageSender = messageSenderFactory.create(); + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + GroupRecord record = groupDatabase.getGroup(groupId); + + if (record == null) { + Log.w(TAG, "No information for group record info request: " + new String(groupId)); + return; + } + + SignalServiceAttachment avatar = SignalServiceAttachmentStream.newStreamBuilder() + .withContentType("image/jpeg") + .withStream(new ByteArrayInputStream(record.getAvatar())) + .withLength(record.getAvatar().length) + .build(); + + SignalServiceGroup groupContext = SignalServiceGroup.newBuilder(Type.UPDATE) + .withAvatar(avatar) + .withId(groupId) + .withMembers(record.getMembers()) + .withName(record.getTitle()) + .build(); + + SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() + .asGroupMessage(groupContext) + .withTimestamp(System.currentTimeMillis()) + .build(); + + messageSender.sendMessage(new SignalServiceAddress(source), message); + } + + @Override + public boolean onShouldRetry(Exception e) { + Log.w(TAG, e); + return e instanceof PushNetworkException; + } + + @Override + public void onCanceled() { + + } +} diff --git a/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java new file mode 100644 index 0000000000..4dae72c424 --- /dev/null +++ b/src/org/thoughtcrime/securesms/jobs/RequestGroupInfoJob.java @@ -0,0 +1,74 @@ +package org.thoughtcrime.securesms.jobs; + +import android.content.Context; +import android.support.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.InjectableType; +import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule; +import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule.TextSecureMessageSenderFactory; +import org.whispersystems.jobqueue.Job; +import org.whispersystems.jobqueue.JobParameters; +import org.whispersystems.jobqueue.requirements.NetworkRequirement; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup.Type; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.io.IOException; + +import javax.inject.Inject; + +public class RequestGroupInfoJob extends ContextJob implements InjectableType { + + private static final String TAG = RequestGroupInfoJob.class.getSimpleName(); + + private static final long serialVersionUID = 0L; + + @Inject transient TextSecureMessageSenderFactory messageSenderFactory; + + private final String source; + private final byte[] groupId; + + public RequestGroupInfoJob(@NonNull Context context, @NonNull String source, @NonNull byte[] groupId) { + super(context, JobParameters.newBuilder() + .withRequirement(new NetworkRequirement(context)) + .withPersistence() + .withRetryCount(50) + .create()); + + this.source = source; + this.groupId = groupId; + } + + @Override + public void onAdded() {} + + @Override + public void onRun() throws IOException, UntrustedIdentityException { + SignalServiceMessageSender messageSender = messageSenderFactory.create(); + + SignalServiceGroup group = SignalServiceGroup.newBuilder(Type.REQUEST_INFO) + .withId(groupId) + .build(); + + SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() + .asGroupMessage(group) + .withTimestamp(System.currentTimeMillis()) + .build(); + + messageSender.sendMessage(new SignalServiceAddress(source), message); + } + + @Override + public boolean onShouldRetry(Exception e) { + return e instanceof PushNetworkException; + } + + @Override + public void onCanceled() { + + } +}