diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt index afe7896894..f72d670910 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageSendLogDatabase.kt @@ -148,26 +148,33 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC ) } - fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageId: MessageId) { - if (!FeatureFlags.senderKey()) return + /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */ + fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageId: MessageId): Long { + if (!FeatureFlags.senderKey()) return -1 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices)) - insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, listOf(messageId)) + return insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, listOf(messageId)) } + + return -1 } - fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageIds: List) { - if (!FeatureFlags.senderKey()) return + /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */ + fun insertIfPossible(recipientId: RecipientId, sentTimestamp: Long, sendMessageResult: SendMessageResult, contentHint: ContentHint, messageIds: List): Long { + if (!FeatureFlags.senderKey()) return -1 if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { val recipientDevice = listOf(RecipientDevice(recipientId, sendMessageResult.success.devices)) - insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, messageIds) + return insert(recipientDevice, sentTimestamp, sendMessageResult.success.content.get(), contentHint, messageIds) } + + return -1 } - fun insertIfPossible(sentTimestamp: Long, possibleRecipients: List, results: List, contentHint: ContentHint, relatedMessageId: Long, isRelatedMessageMms: Boolean) { - if (!FeatureFlags.senderKey()) return + /** @return The ID of the inserted entry, or -1 if none was inserted. Can be used with [addRecipientToExistingEntryIfPossible] */ + fun insertIfPossible(sentTimestamp: Long, possibleRecipients: List, results: List, contentHint: ContentHint, messageId: MessageId): Long { + if (!FeatureFlags.senderKey()) return -1 val recipientsByUuid: Map = possibleRecipients.filter(Recipient::hasUuid).associateBy(Recipient::requireUuid, { it }) val recipientsByE164: Map = possibleRecipients.filter(Recipient::hasE164).associateBy(Recipient::requireE164, { it }) @@ -185,12 +192,40 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC RecipientDevice(recipient.id, result.success.devices) } + if (recipientDevices.isEmpty()) { + return -1 + } + val content: SignalServiceProtos.Content = results.first { it.isSuccess && it.success.content.isPresent }.success.content.get() - insert(recipientDevices, sentTimestamp, content, contentHint, listOf(MessageId(relatedMessageId, isRelatedMessageMms))) + return insert(recipientDevices, sentTimestamp, content, contentHint, listOf(messageId)) } - private fun insert(recipients: List, dateSent: Long, content: SignalServiceProtos.Content, contentHint: ContentHint, messageIds: List) { + fun addRecipientToExistingEntryIfPossible(payloadId: Long, recipientId: RecipientId, sendMessageResult: SendMessageResult) { + if (!FeatureFlags.senderKey()) return + + if (sendMessageResult.isSuccess && sendMessageResult.success.content.isPresent) { + val db = databaseHelper.writableDatabase + + db.beginTransaction() + try { + sendMessageResult.success.devices.forEach { device -> + val recipientValues = ContentValues().apply { + put(RecipientTable.PAYLOAD_ID, payloadId) + put(RecipientTable.RECIPIENT_ID, recipientId.serialize()) + put(RecipientTable.DEVICE, device) + } + + db.insert(RecipientTable.TABLE_NAME, null, recipientValues) + } + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + } + + private fun insert(recipients: List, dateSent: Long, content: SignalServiceProtos.Content, contentHint: ContentHint, messageIds: List): Long { val db = databaseHelper.writableDatabase db.beginTransaction() @@ -226,6 +261,8 @@ class MessageSendLogDatabase constructor(context: Context?, databaseHelper: SQLC } db.setTransactionSuccessful() + + return payloadId } finally { db.endTransaction() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java index fd7cd95feb..f09f6729d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -17,10 +17,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult; -import org.thoughtcrime.securesms.jobs.LeaveGroupJob; -import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; import org.thoughtcrime.securesms.profiles.AvatarHelper; @@ -187,51 +184,12 @@ final class GroupManagerV1 { @WorkerThread static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) { - Recipient groupRecipient = Recipient.externalGroupExact(context, groupId); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - Optional leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient); - - if (threadId != -1 && leaveMessage.isPresent()) { - try { - long id = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(leaveMessage.get(), threadId, false, null); - DatabaseFactory.getMmsDatabase(context).markAsSent(id, true); - } catch (MmsException e) { - Log.w(TAG, "Failed to insert leave message.", e); - } - ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient)); - - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - groupDatabase.setActive(groupId, false); - groupDatabase.remove(groupId, Recipient.self().getId()); - return true; - } else { - Log.i(TAG, "Group was already inactive. Skipping."); - return false; - } + return false; } @WorkerThread static boolean silentLeaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) { - if (DatabaseFactory.getGroupDatabase(context).isActive(groupId)) { - Recipient groupRecipient = Recipient.externalGroupExact(context, groupId); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - Optional leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient); - - if (threadId != -1 && leaveMessage.isPresent()) { - ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient)); - - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - groupDatabase.setActive(groupId, false); - groupDatabase.remove(groupId, Recipient.self().getId()); - return true; - } else { - Log.w(TAG, "Failed to leave group."); - return false; - } - } else { - Log.i(TAG, "Group was already inactive. Skipping."); - return true; - } + return false; } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java index 3602d14a10..682838011d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java @@ -9,8 +9,10 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -151,18 +153,18 @@ public class GroupCallUpdateSendJob extends BaseJob { private @NonNull List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations);; - SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(System.currentTimeMillis()) - .withGroupCallUpdate(new SignalServiceDataMessage.GroupCallUpdate(eraId)); + SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() + .withTimestamp(System.currentTimeMillis()) + .withGroupCallUpdate(new SignalServiceDataMessage.GroupCallUpdate(eraId)); - if (conversationRecipient.isGroup()) { - GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush()); - } + GroupUtil.setDataMessageGroupContext(context, dataMessage, conversationRecipient.requireGroupId().requirePush()); - List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build()); + List results = GroupSendUtil.sendUnresendableDataMessage(context, + conversationRecipient.requireGroupId().requireV2(), + destinations, + false, + ContentHint.DEFAULT, + dataMessage.build()); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index e2b55756e0..579c57fef2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -89,7 +89,6 @@ public final class JobManagerFactories { put(GroupCallPeekWorkerJob.KEY, new GroupCallPeekWorkerJob.Factory()); put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory()); put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory()); - put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory()); put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); put(LocalBackupJobApi29.KEY, new LocalBackupJobApi29.Factory()); put(MarkerJob.KEY, new MarkerJob.Factory()); @@ -205,6 +204,7 @@ public final class JobManagerFactories { put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory()); put("StorageSyncJob", new StorageSyncJob.Factory()); put("WakeGroupV2Job", new FailingJob.Factory()); + put("LeaveGroupJob", new FailingJob.Factory()); }}; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java deleted file mode 100644 index 4aa3568151..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LeaveGroupJob.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.jobmanager.Data; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.recipients.RecipientUtil; -import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.thoughtcrime.securesms.util.Base64; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.ContentHint; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Normally, we can do group leaves via {@link PushGroupSendJob}. However, that job relies on a - * message being present in the database, which is not true if the user selects a message request - * option that deletes and leaves at the same time. - * - * This job tracks all send state within the job and does not require a message in the database to - * work. - */ -public class LeaveGroupJob extends BaseJob { - - public static final String KEY = "LeaveGroupJob"; - - private static final String TAG = Log.tag(LeaveGroupJob.class); - - private static final String KEY_GROUP_ID = "group_id"; - private static final String KEY_GROUP_NAME = "name"; - private static final String KEY_MEMBERS = "members"; - private static final String KEY_RECIPIENTS = "recipients"; - - private final GroupId.Push groupId; - private final String name; - private final List members; - private final List recipients; - - public static @NonNull LeaveGroupJob create(@NonNull Recipient group) { - List members = Stream.of(group.resolve().getParticipants()).map(Recipient::getId).toList(); - members.remove(Recipient.self().getId()); - - return new LeaveGroupJob(group.getGroupId().get().requirePush(), - group.resolve().getDisplayName(ApplicationDependencies.getApplication()), - members, - members, - new Parameters.Builder() - .setQueue(group.getId().toQueueKey()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build()); - } - - private LeaveGroupJob(@NonNull GroupId.Push groupId, - @NonNull String name, - @NonNull List members, - @NonNull List recipients, - @NonNull Parameters parameters) - { - super(parameters); - this.groupId = groupId; - this.name = name; - this.members = Collections.unmodifiableList(members); - this.recipients = recipients; - } - - @Override - public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_GROUP_ID, Base64.encodeBytes(groupId.getDecodedId())) - .putString(KEY_GROUP_NAME, name) - .putString(KEY_MEMBERS, RecipientId.toSerializedList(members)) - .putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients)) - .build(); - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - protected void onRun() throws Exception { - List completions = deliver(context, groupId, name, members, recipients); - - for (Recipient completion : completions) { - recipients.remove(completion.getId()); - } - - Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size()); - - if (!recipients.isEmpty()) { - Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying."); - throw new RetryLaterException(); - } - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof IOException || e instanceof RetryLaterException; - } - - @Override - public void onFailure() { - } - - private static @NonNull List deliver(@NonNull Context context, - @NonNull GroupId.Push groupId, - @NonNull String name, - @NonNull List members, - @NonNull List destinations) - throws IOException, UntrustedIdentityException - { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddresses(context, destinations); - List memberAddresses = RecipientUtil.toSignalServiceAddresses(context, members); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, Stream.of(destinations).map(Recipient::resolved).toList()); - SignalServiceGroup serviceGroup = new SignalServiceGroup(SignalServiceGroup.Type.QUIT, groupId.getDecodedId(), name, memberAddresses, null); - SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(System.currentTimeMillis()) - .asGroupMessage(serviceGroup); - - - List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.DEFAULT, dataMessage.build()); - - return GroupSendJobHelper.getCompletedSends(context, results); - } - - public static class Factory implements Job.Factory { - @Override - public @NonNull LeaveGroupJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new LeaveGroupJob(GroupId.v1orThrow(Base64.decodeOrThrow(data.getString(KEY_GROUP_ID))), - data.getString(KEY_GROUP_NAME), - RecipientId.fromSerializedList(data.getString(KEY_MEMBERS)), - RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS)), - parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java index 1159119be7..31b03f9fe0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.messages.GroupSendUtil; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -107,7 +108,7 @@ public class ProfileKeySendJob extends BaseJob { } List destinations = Stream.of(recipients).map(Recipient::resolved).toList(); - List completions = deliver(conversationRecipient, destinations); + List completions = deliver(destinations); for (Recipient completion : completions) { recipients.remove(completion.getId()); @@ -147,20 +148,13 @@ public class ProfileKeySendJob extends BaseJob { } - private List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations); - SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() - .asProfileKeyUpdate(true) - .withTimestamp(System.currentTimeMillis()) - .withProfileKey(Recipient.self().resolve().getProfileKey()); + private List deliver(@NonNull List destinations) throws IOException, UntrustedIdentityException { + SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder() + .asProfileKeyUpdate(true) + .withTimestamp(System.currentTimeMillis()) + .withProfileKey(Recipient.self().resolve().getProfileKey()); - if (conversationRecipient.isGroup()) { - dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId())); - } - - List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.IMPLICIT, dataMessage.build()); + List results = GroupSendUtil.sendUnresendableDataMessage(context, null, destinations, false, ContentHint.IMPLICIT, dataMessage.build()); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 008ccd47ce..2424f0b188 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; @@ -292,8 +293,6 @@ public final class PushGroupSendJob extends PushSendJob { try { rotateSenderCertificateIfNecessary(); - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); GroupId.Push groupId = groupRecipient.requireGroupId().requirePush(); Optional profileKey = getProfileKey(groupRecipient); Optional quote = getQuoteFor(message); @@ -301,14 +300,11 @@ public final class PushGroupSendJob extends PushSendJob { List sharedContacts = getSharedContactsFor(message); List previews = getPreviewsFor(message); List mentions = getMentionsFor(message.getMentions()); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); List attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List attachmentPointers = getAttachmentPointersFor(attachments); boolean isRecipientUpdate = Stream.of(DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId)) .anyMatch(info -> info.getStatus() > GroupReceiptDatabase.STATUS_UNDELIVERED); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations); - if (message.isGroup()) { OutgoingGroupUpdateMessage groupMessage = (OutgoingGroupUpdateMessage) message; @@ -329,25 +325,9 @@ public final class PushGroupSendJob extends PushSendJob { .withExpiration(groupRecipient.getExpireMessages()) .asGroupMessage(group) .build(); - return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, messageId, true, groupDataMessage); + return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.IMPLICIT, new MessageId(messageId, true), groupDataMessage); } else { - MessageGroupContext.GroupV1Properties properties = groupMessage.requireGroupV1Properties(); - - GroupContext groupContext = properties.getGroupContext(); - SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0); - SignalServiceGroup.Type type = properties.isQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE; - List members = Stream.of(groupContext.getMembersE164List()) - .map(e164 -> new SignalServiceAddress(null, e164)) - .toList(); - SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar); - SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder() - .withTimestamp(message.getSentTimeMillis()) - .withExpiration(message.getRecipient().getExpireMessages()) - .asGroupMessage(group) - .build(); - - Log.i(TAG, JobLogger.format(this, "Beginning update send.")); - return messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.IMPLICIT, groupDataMessage); + throw new UndeliverableMessageException("Messages can no longer be sent to V1 groups!"); } } else { SignalServiceDataMessage.Builder builder = SignalServiceDataMessage.newBuilder() @@ -370,13 +350,13 @@ public final class PushGroupSendJob extends PushSendJob { Log.i(TAG, JobLogger.format(this, "Beginning message send.")); - if (groupRecipient.isPushV2Group()) { - return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), destinations, isRecipientUpdate, ContentHint.RESENDABLE, messageId, true, groupMessage); - } else { - List results = messageSender.sendDataMessage(addresses, unidentifiedAccess, isRecipientUpdate, ContentHint.RESENDABLE, groupMessage); - DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(groupMessage.getTimestamp(), destinations, results, ContentHint.RESENDABLE, messageId, true); - return results; - } + return GroupSendUtil.sendResendableDataMessage(context, + groupRecipient.getGroupId().transform(GroupId::requireV2).orNull(), + destinations, + isRecipientUpdate, + ContentHint.RESENDABLE, + new MessageId(messageId, true), + groupMessage); } } catch (ServerRejectedException e) { throw new UndeliverableMessageException(e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java index 116b7521d7..bfaa86e3e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java @@ -12,9 +12,11 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.net.NotPushRegisteredException; @@ -170,6 +172,11 @@ public class ReactionSendJob extends BaseJob { throw new AssertionError("We have a message, but couldn't find the thread!"); } + if (conversationRecipient.isPushV1Group() || conversationRecipient.isMmsGroup()) { + Log.w(TAG, "Cannot send reactions to legacy groups."); + return; + } + List destinations = Stream.of(recipients).map(Recipient::resolved).toList(); List completions = deliver(conversationRecipient, destinations, targetAuthor, targetSentTimestamp); @@ -227,20 +234,13 @@ public class ReactionSendJob extends BaseJob { } SignalServiceDataMessage dataMessage = dataMessageBuilder.build(); - - List results; - - if (conversationRecipient.isPushV2Group()) { - results = GroupSendUtil.sendResendableDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.RESENDABLE, messageId, isMms, dataMessageBuilder.build()); - } else { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations); - - results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.RESENDABLE, dataMessage); - - DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(dataMessage.getTimestamp(), destinations, results, ContentHint.RESENDABLE, messageId, isMms); - } + List results = GroupSendUtil.sendResendableDataMessage(context, + conversationRecipient.getGroupId().transform(GroupId::requireV2).orNull(), + destinations, + false, + ContentHint.RESENDABLE, + new MessageId(messageId, isMms), + dataMessage); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java index 8226a390f5..695869cd24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java @@ -12,8 +12,10 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.net.NotPushRegisteredException; @@ -183,20 +185,13 @@ public class RemoteDeleteSendJob extends BaseJob { } SignalServiceDataMessage dataMessage = dataMessageBuilder.build(); - - List results; - - if (conversationRecipient.isPushV2Group()) { - results = GroupSendUtil.sendResendableDataMessage(context, conversationRecipient.requireGroupId().requireV2(), destinations, false, ContentHint.RESENDABLE, messageId, isMms, dataMessage); - } else { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, destinations); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, destinations); - - results = messageSender.sendDataMessage(addresses, unidentifiedAccess, false, ContentHint.RESENDABLE, dataMessage); - - DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(dataMessage.getTimestamp(), destinations, results, ContentHint.RESENDABLE, messageId, isMms); - } + List results = GroupSendUtil.sendResendableDataMessage(context, + conversationRecipient.getGroupId().transform(GroupId::requireV2).orNull(), + destinations, + false, + ContentHint.RESENDABLE, + new MessageId(messageId, isMms), + dataMessage); return GroupSendJobHelper.getCompletedSends(context, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java index 6bab4cf3e6..f036c17a42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TypingSendJob.java @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; @@ -107,6 +108,16 @@ public class TypingSendJob extends BaseJob { return; } + if (recipient.isPushV1Group() || recipient.isMmsGroup()) { + Log.w(TAG, "Not sending typing indicators to unsupported groups."); + return; + } + + if (!recipient.isRegistered() || recipient.isForceSmsSelection()) { + Log.w(TAG, "Not sending typing indicators to non-Signal recipients."); + return; + } + List recipients = Collections.singletonList(recipient); Optional groupId = Optional.absent(); @@ -123,25 +134,11 @@ public class TypingSendJob extends BaseJob { SignalServiceTypingMessage typingMessage = new SignalServiceTypingMessage(typing ? Action.STARTED : Action.STOPPED, System.currentTimeMillis(), groupId); try { - if (recipient.isPushV2Group()) { - GroupSendUtil.sendTypingMessage(context, recipient.requireGroupId().requireV2(), recipients, typingMessage, this::isCanceled); - } else { - SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); - List> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipients); - - if (addresses.isEmpty()) { - Log.w(TAG, "No one to send typing indicators to"); - return; - } - - if (isCanceled()) { - Log.w(TAG, "Canceled before send!"); - return; - } - - messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled); - } + GroupSendUtil.sendTypingMessage(context, + recipient.getGroupId().transform(GroupId::requireV2).orNull(), + recipients, + typingMessage, + this::isCanceled); } catch (CancelationException e) { Log.w(TAG, "Canceled during send!"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java index ec4892a547..1c63a96e86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/GroupSendUtil.java @@ -10,6 +10,8 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.crypto.SenderKeyUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MessageSendLogDatabase; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -33,6 +35,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.DistributionId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.push.http.CancelationSignal; +import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener; import java.io.IOException; import java.util.ArrayList; @@ -45,6 +48,8 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; public final class GroupSendUtil { @@ -62,19 +67,19 @@ public final class GroupSendUtil { * * Messages sent this way, if failed to be decrypted by the receiving party, can be requested to be resent. * + * @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group. * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. */ public static List sendResendableDataMessage(@NonNull Context context, - @NonNull GroupId.V2 groupId, + @Nullable GroupId.V2 groupId, @NonNull List allTargets, boolean isRecipientUpdate, ContentHint contentHint, - long relatedMessageId, - boolean isRelatedMessageMms, + @NonNull MessageId messageId, @NonNull SignalServiceDataMessage message) throws IOException, UntrustedIdentityException { - return sendMessage(context, groupId, allTargets, isRecipientUpdate, DataSendOperation.resendable(message, contentHint, relatedMessageId, isRelatedMessageMms), null); + return sendMessage(context, groupId, allTargets, isRecipientUpdate, DataSendOperation.resendable(message, contentHint, messageId), null); } /** @@ -83,11 +88,12 @@ public final class GroupSendUtil { * * Messages sent this way, if failed to be decrypted by the receiving party, can *not* be requested to be resent. * + * @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group. * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. */ @WorkerThread public static List sendUnresendableDataMessage(@NonNull Context context, - @NonNull GroupId.V2 groupId, + @Nullable GroupId.V2 groupId, @NonNull List allTargets, boolean isRecipientUpdate, ContentHint contentHint, @@ -100,10 +106,12 @@ public final class GroupSendUtil { /** * Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of * {@link SendMessageResult}s just like we're used to. + * + * @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group. */ @WorkerThread public static List sendTypingMessage(@NonNull Context context, - @NonNull GroupId.V2 groupId, + @Nullable GroupId.V2 groupId, @NonNull List allTargets, @NonNull SignalServiceTypingMessage message, @Nullable CancelationSignal cancelationSignal) @@ -116,11 +124,12 @@ public final class GroupSendUtil { * Handles all of the logic of sending to a group. Will do sender key sends and legacy 1:1 sends as-needed, and give you back a list of * {@link SendMessageResult}s just like we're used to. * + * @param groupId The groupId of the group you're sending to, or null if you're sending to a collection of recipients not joined by a group. * @param isRecipientUpdate True if you've already sent this message to some recipients in the past, otherwise false. */ @WorkerThread private static List sendMessage(@NonNull Context context, - @NonNull GroupId.V2 groupId, + @Nullable GroupId.V2 groupId, @NonNull List allTargets, boolean isRecipientUpdate, @NonNull SendOperation sendOperation, @@ -147,7 +156,11 @@ public final class GroupSendUtil { } if (FeatureFlags.senderKey()) { - if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) { + if (groupId == null) { + Log.i(TAG, "Recipients not in a group. Using legacy."); + legacyTargets.addAll(senderKeyTargets); + senderKeyTargets.clear(); + } else if (Recipient.self().getSenderKeyCapability() != Recipient.Capability.SUPPORTED) { Log.i(TAG, "All of our devices do not support sender key. Using legacy."); legacyTargets.addAll(senderKeyTargets); senderKeyTargets.clear(); @@ -168,11 +181,11 @@ public final class GroupSendUtil { List allResults = new ArrayList<>(allTargets.size()); SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId); - if (senderKeyTargets.size() > 0) { - long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(context, distributionId); - long keyAge = System.currentTimeMillis() - keyCreateTime; + if (senderKeyTargets.size() > 0 && groupId != null) { + DistributionId distributionId = DatabaseFactory.getGroupDatabase(context).getOrCreateDistributionId(groupId); + long keyCreateTime = SenderKeyUtil.getCreateTimeForOurKey(context, distributionId); + long keyAge = System.currentTimeMillis() - keyCreateTime; if (keyCreateTime != -1 && keyAge > MAX_KEY_AGE) { Log.w(TAG, "Key is " + (keyAge) + " ms old (~" + TimeUnit.MILLISECONDS.toDays(keyAge) + " days). Rotating."); @@ -190,7 +203,7 @@ public final class GroupSendUtil { Log.d(TAG, "Successfully sent using sender key to " + successCount + "/" + targets.size() + " sender key targets."); if (sendOperation.shouldIncludeInMessageLog()) { - DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), senderKeyTargets, results, sendOperation.getContentHint(), sendOperation.getRelatedMessageId(), sendOperation.isRelatedMessageMms()); + DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), senderKeyTargets, results, sendOperation.getContentHint(), sendOperation.getRelatedMessageId()); } } catch (NoSessionException e) { Log.w(TAG, "No session. Falling back to legacy sends.", e); @@ -212,16 +225,29 @@ public final class GroupSendUtil { List> access = legacyTargets.stream().map(r -> recipients.getAccessPair(r.getId())).collect(Collectors.toList()); boolean recipientUpdate = isRecipientUpdate || allResults.size() > 0; - List results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, cancelationSignal); + + final MessageSendLogDatabase messageLogDatabase = DatabaseFactory.getMessageLogDatabase(context); + final AtomicLong entryId = new AtomicLong(-1); + final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog(); + + List results = sendOperation.sendLegacy(messageSender, targets, access, recipientUpdate, result -> { + if (!includeInMessageLog) { + return; + } + + synchronized (entryId) { + if (entryId.get() == -1) { + entryId.set(messageLogDatabase.insertIfPossible(recipients.requireRecipientId(result.getAddress()), sendOperation.getSentTimestamp(), result, sendOperation.getContentHint(), sendOperation.getRelatedMessageId())); + } else { + messageLogDatabase.addRecipientToExistingEntryIfPossible(entryId.get(), recipients.requireRecipientId(result.getAddress()), result); + } + } + }, cancelationSignal); allResults.addAll(results); int successCount = (int) results.stream().filter(SendMessageResult::isSuccess).count(); Log.d(TAG, "Successfully using 1:1 to " + successCount + "/" + targets.size() + " legacy targets."); - - if (sendOperation.shouldIncludeInMessageLog()) { - DatabaseFactory.getMessageLogDatabase(context).insertIfPossible(sendOperation.getSentTimestamp(), legacyTargets, results, sendOperation.getContentHint(), sendOperation.getRelatedMessageId(), sendOperation.isRelatedMessageMms()); - } } return allResults; @@ -240,37 +266,39 @@ public final class GroupSendUtil { @NonNull List targets, @NonNull List> access, boolean isRecipientUpdate, + @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException; @NonNull ContentHint getContentHint(); long getSentTimestamp(); boolean shouldIncludeInMessageLog(); - long getRelatedMessageId(); - boolean isRelatedMessageMms(); + @NonNull MessageId getRelatedMessageId(); } private static class DataSendOperation implements SendOperation { private final SignalServiceDataMessage message; private final ContentHint contentHint; - private final long relatedMessageId; - private final boolean isRelatedMessageMms; + private final MessageId relatedMessageId; private final boolean resendable; - public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, long relatedMessageId, boolean isRelatedMessageMms) { - return new DataSendOperation(message, contentHint, true, relatedMessageId, isRelatedMessageMms); + public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, @NonNull MessageId relatedMessageId) { + return new DataSendOperation(message, contentHint, true, relatedMessageId); } public static DataSendOperation unresendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint) { - return new DataSendOperation(message, contentHint, false, -1, false); + return new DataSendOperation(message, contentHint, false, null); } - private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, long relatedMessageId, boolean isRelatedMessageMms) { + private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, @Nullable MessageId relatedMessageId) { this.message = message; this.contentHint = contentHint; this.resendable = resendable; this.relatedMessageId = relatedMessageId; - this.isRelatedMessageMms = isRelatedMessageMms; + + if (resendable && relatedMessageId == null) { + throw new IllegalArgumentException("If a message is resendable, it must have a related message ID!"); + } } @Override @@ -289,10 +317,11 @@ public final class GroupSendUtil { @NonNull List targets, @NonNull List> access, boolean isRecipientUpdate, + @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { - return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message); + return messageSender.sendDataMessage(targets, access, isRecipientUpdate, contentHint, message, partialListener, cancelationSignal); } @Override @@ -311,13 +340,12 @@ public final class GroupSendUtil { } @Override - public long getRelatedMessageId() { - return relatedMessageId; - } - - @Override - public boolean isRelatedMessageMms() { - return isRelatedMessageMms; + public @NonNull MessageId getRelatedMessageId() { + if (relatedMessageId != null) { + return relatedMessageId; + } else { + throw new UnsupportedOperationException(); + } } } @@ -346,6 +374,7 @@ public final class GroupSendUtil { @NonNull List targets, @NonNull List> access, boolean isRecipientUpdate, + @Nullable PartialSendCompleteListener partialListener, @Nullable CancelationSignal cancelationSignal) throws IOException { @@ -369,13 +398,8 @@ public final class GroupSendUtil { } @Override - public long getRelatedMessageId() { - return -1; - } - - @Override - public boolean isRelatedMessageMms() { - return false; + public @NonNull MessageId getRelatedMessageId() { + throw new UnsupportedOperationException(); } } @@ -386,10 +410,23 @@ public final class GroupSendUtil { private final Map> accessById; private final Map addressById; + private final Map idByUuid; + private final Map idByE164; RecipientData(@NonNull Context context, @NonNull List recipients) throws IOException { this.accessById = UnidentifiedAccessUtil.getAccessMapFor(context, recipients); this.addressById = mapAddresses(context, recipients); + this.idByUuid = new HashMap<>(recipients.size()); + this.idByE164 = new HashMap<>(recipients.size()); + + for (Recipient recipient : recipients) { + if (recipient.hasUuid()) { + idByUuid.put(recipient.requireUuid(), recipient.getId()); + } + if (recipient.hasE164()) { + idByE164.put(recipient.requireE164(), recipient.getId()); + } + } } @NonNull SignalServiceAddress getAddress(@NonNull RecipientId id) { @@ -404,6 +441,16 @@ public final class GroupSendUtil { return Objects.requireNonNull(accessById.get(id)).get().getTargetUnidentifiedAccess().get(); } + @NonNull RecipientId requireRecipientId(@NonNull SignalServiceAddress address) { + if (address.getUuid().isPresent() && idByUuid.containsKey(address.getUuid().get())) { + return Objects.requireNonNull(idByUuid.get(address.getUuid().get())); + } else if (address.getNumber().isPresent() && idByE164.containsKey(address.getNumber().get())) { + return Objects.requireNonNull(idByE164.get(address.getNumber().get())); + } else { + throw new IllegalStateException(); + } + } + private static @NonNull Map mapAddresses(@NonNull Context context, @NonNull List recipients) throws IOException { List addresses = RecipientUtil.toSignalServiceAddressesFromResolved(context, recipients); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index db2e5c53d1..e32e7b73d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -287,8 +287,8 @@ public final class MessageContentProcessor { ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false)) .then(GroupV2UpdateSelfProfileKeyJob.withQueueLimits(groupId.get().requireV2())) .enqueue(); - } else if (!sender.isPushV2Group()) { - Log.i(TAG, "Message was to a 1:1 or GV1 chat. Ensuring this user has our profile key."); + } else if (!sender.isGroup()) { + Log.i(TAG, "Message was to a 1:1. Ensuring this user has our profile key."); ApplicationDependencies.getJobManager().startChain(new RefreshAttributesJob(false)) .then(ProfileKeySendJob.create(context, DatabaseFactory.getThreadDatabase(context).getThreadIdFor(sender), true)) .enqueue(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 428e051a8e..4c2bcc5ec8 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -108,6 +108,7 @@ import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevic import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException; import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory; import org.whispersystems.signalservice.internal.push.http.CancelationSignal; +import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteListener; import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec; import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider; import org.whispersystems.signalservice.internal.util.Util; @@ -273,7 +274,7 @@ public class SignalServiceMessageSender { Content content = createTypingContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.absent()); - sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, cancelationSignal); + sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), envelopeContent, true, null, cancelationSignal); } /** @@ -388,7 +389,7 @@ public class SignalServiceMessageSender { EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, ContentHint.IMPLICIT, Optional.of(groupId)); long timestamp = System.currentTimeMillis(); - return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null); + return sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null, null); } /** @@ -443,21 +444,22 @@ public class SignalServiceMessageSender { /** * Sends a message to a group using client-side fanout. * - * @param recipients The group members. - * @param message The group message. - * @throws IOException + * @param partialListener A listener that will be called when an individual send is completed. Will be invoked on an arbitrary background thread, *not* + * the calling thread. */ public List sendDataMessage(List recipients, List> unidentifiedAccess, boolean isRecipientUpdate, ContentHint contentHint, - SignalServiceDataMessage message) + SignalServiceDataMessage message, + PartialSendCompleteListener partialListener, + CancelationSignal cancelationSignal) throws IOException, UntrustedIdentityException { Content content = createMessageContent(message); EnvelopeContent envelopeContent = EnvelopeContent.encrypted(content, contentHint, message.getGroupId()); long timestamp = message.getTimestamp(); - List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, null); + List results = sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, envelopeContent, false, partialListener, cancelationSignal); boolean needsSyncInResults = false; for (SendMessageResult result : results) { @@ -1531,6 +1533,7 @@ public class SignalServiceMessageSender { long timestamp, EnvelopeContent content, boolean online, + PartialSendCompleteListener partialListener, CancelationSignal cancelationSignal) throws IOException { @@ -1544,7 +1547,13 @@ public class SignalServiceMessageSender { while (recipientIterator.hasNext()) { SignalServiceAddress recipient = recipientIterator.next(); Optional access = unidentifiedAccessIterator.next(); - futureResults.add(executor.submit(() -> sendMessage(recipient, access, timestamp, content, online, cancelationSignal))); + futureResults.add(executor.submit(() -> { + SendMessageResult result = sendMessage(recipient, access, timestamp, content, online, cancelationSignal); + if (partialListener != null) { + partialListener.onPartialSendComplete(result); + } + return result; + })); } List results = new ArrayList<>(futureResults.size()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/PartialSendCompleteListener.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/PartialSendCompleteListener.java new file mode 100644 index 0000000000..54a28afb21 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/PartialSendCompleteListener.java @@ -0,0 +1,10 @@ +package org.whispersystems.signalservice.internal.push.http; + +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +/** + * Used to let a listener know when each individual send in a collection of sends has been completed. + */ +public interface PartialSendCompleteListener { + void onPartialSendComplete(SendMessageResult result); +}