diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 3d6259fa76..c80af282ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -97,7 +97,7 @@ public class GroupReceiptDatabase extends Database { } public void setSkipped(Collection recipients, long mmsId) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); + SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { 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 1cd6c0285c..15953f54b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupCallUpdateSendJob.java @@ -167,7 +167,7 @@ public class GroupCallUpdateSendJob extends BaseJob { results.add(ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(dataMessage)); } - return GroupSendJobHelper.getCompletedSends(destinations, results); + return GroupSendJobHelper.getCompletedSends(destinations, results).completed; } public static class Factory implements Job.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java index b8322a2fde..02240ab5bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java @@ -4,11 +4,14 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.RecipientAccessList; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.messages.SendMessageResult; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; final class GroupSendJobHelper { @@ -18,9 +21,10 @@ final class GroupSendJobHelper { private GroupSendJobHelper() { } - static List getCompletedSends(@NonNull List possibleRecipients, @NonNull Collection results) { + static @NonNull SendResult getCompletedSends(@NonNull List possibleRecipients, @NonNull Collection results) { RecipientAccessList accessList = new RecipientAccessList(possibleRecipients); List completions = new ArrayList<>(results.size()); + List skipped = new ArrayList<>(); for (SendMessageResult sendMessageResult : results) { Recipient recipient = accessList.requireByAddress(sendMessageResult.getAddress()); @@ -31,6 +35,7 @@ final class GroupSendJobHelper { if (sendMessageResult.isUnregisteredFailure()) { Log.w(TAG, "Unregistered failure for " + recipient.getId()); + skipped.add(recipient.getId()); } if (sendMessageResult.getSuccess() != null || @@ -41,6 +46,16 @@ final class GroupSendJobHelper { } } - return completions; + return new SendResult(completions, skipped); + } + + public static class SendResult { + public final List completed; + public final List skipped; + + public SendResult(@NonNull List completed, @NonNull List skipped) { + this.completed = completed; + this.skipped = skipped; + } } } 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 2941e45052..9caa67349a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileKeySendJob.java @@ -150,7 +150,7 @@ public class ProfileKeySendJob extends BaseJob { List results = GroupSendUtil.sendUnresendableDataMessage(context, null, destinations, false, ContentHint.IMPLICIT, dataMessage.build()); - return GroupSendJobHelper.getCompletedSends(destinations, results); + return GroupSendJobHelper.getCompletedSends(destinations, results).completed; } public static class Factory implements Job.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java index 6ba3f060b8..3a689cac1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushDistributionListSendJob.java @@ -38,6 +38,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -151,7 +152,7 @@ public final class PushDistributionListSendJob extends PushSendJob { List results = deliver(message, target); Log.i(TAG, JobLogger.format(this, "Finished send.")); - PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, target, existingNetworkFailures, existingIdentityMismatches); + PushGroupSendJob.processGroupMessageResults(context, messageId, -1, null, message, results, target, Collections.emptyList(), existingNetworkFailures, existingIdentityMismatches); } catch (UntrustedIdentityException | UndeliverableMessageException e) { warn(TAG, String.valueOf(message.getSentTimeMillis()), e); database.markAsSentFailed(messageId); 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 ca60b4fe23..77a0698b75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.RecipientAccessList; +import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -60,6 +61,7 @@ import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedExcept import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -183,14 +185,22 @@ public final class PushGroupSendJob extends PushSendJob { RecipientUtil.shareProfileIfFirstSecureMessage(context, groupRecipient); } - List target; + List target; + List skipped = new ArrayList<>(); - if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient)); - else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).distinct().map(Recipient::resolved).toList(); - else target = Stream.of(getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId)).distinctBy(Recipient::getId).toList(); + if (filterRecipient != null) { + target = Collections.singletonList(Recipient.resolved(filterRecipient)); + } else if (!existingNetworkFailures.isEmpty()) { + target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).distinct().map(Recipient::resolved).toList(); + } else { + GroupRecipientResult result = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId); + + target = result.target; + skipped = result.skipped; + } List results = deliver(message, groupRecipient, target); - processGroupMessageResults(context, messageId, threadId, groupRecipient, message, results, target, existingNetworkFailures, existingIdentityMismatches); + processGroupMessageResults(context, messageId, threadId, groupRecipient, message, results, target, skipped, existingNetworkFailures, existingIdentityMismatches); Log.i(TAG, JobLogger.format(this, "Finished send.")); } catch (UntrustedIdentityException | UndeliverableMessageException e) { @@ -334,6 +344,7 @@ public final class PushGroupSendJob extends PushSendJob { @NonNull OutgoingMediaMessage message, @NonNull List results, @NonNull List target, + @NonNull List skipped, @NonNull Set existingNetworkFailures, @NonNull Set existingIdentityMismatches) throws RetryLaterException, ProofRequiredException @@ -350,6 +361,9 @@ public final class PushGroupSendJob extends PushSendJob { List resolvedNetworkFailures = Stream.of(existingNetworkFailures).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList(); List resolvedIdentityFailures = Stream.of(existingIdentityMismatches).filter(failure -> successIds.contains(failure.getRecipientId(context))).toList(); List unregisteredRecipients = Stream.of(results).filter(SendMessageResult::isUnregisteredFailure).map(result -> RecipientId.from(result.getAddress())).toList(); + List skippedRecipients = new ArrayList<>(unregisteredRecipients); + + skippedRecipients.addAll(skipped); if (networkFailures.size() > 0 || identityMismatches.size() > 0 || proofRequired != null || unregisteredRecipients.size() > 0) { Log.w(TAG, String.format(Locale.US, "Failed to send to some recipients. Network: %d, Identity: %d, ProofRequired: %s, Unregistered: %d", @@ -380,6 +394,10 @@ public final class PushGroupSendJob extends PushSendJob { markAttachmentsUploaded(messageId, message); + if (skippedRecipients.size() > 0) { + SignalDatabase.groupReceipts().setSkipped(skippedRecipients, messageId); + } + if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) { database.markExpireStarted(messageId); ApplicationDependencies.getExpiringMessageManager() @@ -414,25 +432,40 @@ public final class PushGroupSendJob extends PushSendJob { } } - private static @NonNull List getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) { + private static @NonNull GroupRecipientResult getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) { List destinations = SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId); + List possible; + if (!destinations.isEmpty()) { - return RecipientUtil.getEligibleForSending(Stream.of(destinations) - .map(GroupReceiptInfo::getRecipientId) - .map(Recipient::resolved) - .toList()); - } - - List members = Stream.of(SignalDatabase.groups().getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)) - .map(Recipient::resolve) - .toList(); - - if (members.size() > 0) { + possible = Stream.of(destinations) + .map(GroupReceiptInfo::getRecipientId) + .map(Recipient::resolved) + .distinctBy(Recipient::getId) + .toList(); + } else { Log.w(TAG, "No destinations found for group message " + groupId + " using current group membership"); + possible = Stream.of(SignalDatabase.groups() + .getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)) + .map(Recipient::resolve) + .distinctBy(Recipient::getId) + .toList(); } - return RecipientUtil.getEligibleForSending(members); + List eligible = RecipientUtil.getEligibleForSending(possible); + List skipped = Stream.of(SetUtil.difference(possible, eligible)).map(Recipient::getId).toList(); + + return new GroupRecipientResult(eligible, skipped); + } + + private static class GroupRecipientResult { + private final List target; + private final List skipped; + + private GroupRecipientResult(@NonNull List target, @NonNull List skipped) { + this.target = target; + this.skipped = skipped; + } } public static class Factory implements Job.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java index ccaa7472d1..fe50488782 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSilentUpdateSendJob.java @@ -178,7 +178,7 @@ public final class PushGroupSilentUpdateSendJob extends BaseJob { List results = GroupSendUtil.sendUnresendableDataMessage(context, groupId, destinations, false, ContentHint.IMPLICIT, groupDataMessage); - return GroupSendJobHelper.getCompletedSends(destinations, results); + return GroupSendJobHelper.getCompletedSends(destinations, results).completed; } public static class Factory implements Job.Factory { 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 d4f709e941..302aa4f398 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java @@ -248,7 +248,7 @@ public class ReactionSendJob extends BaseJob { results.add(ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(dataMessage)); } - return GroupSendJobHelper.getCompletedSends(destinations, results); + return GroupSendJobHelper.getCompletedSends(destinations, results).completed; } private static SignalServiceDataMessage.Reaction buildReaction(@NonNull Context context, 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 69121077b5..98df08a797 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java @@ -23,6 +23,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.GroupUtil; +import org.thoughtcrime.securesms.util.SetUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; @@ -65,7 +67,7 @@ public class RemoteDeleteSendJob extends BaseJob { throw new AssertionError("We have a message, but couldn't find the thread!"); } - List recipients = conversationRecipient.isGroup() ? Stream.of(RecipientUtil.getEligibleForSending(conversationRecipient.getParticipants())).map(Recipient::getId).toList() + List recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList() : Stream.of(conversationRecipient.getId()).toList(); recipients.remove(Recipient.self().getId()); @@ -137,14 +139,27 @@ public class RemoteDeleteSendJob extends BaseJob { throw new IllegalStateException("Cannot delete a message that isn't yours!"); } - List destinations = Stream.of(recipients).map(Recipient::resolved).toList(); - List completions = deliver(conversationRecipient, destinations, targetSentTimestamp); + List possible = Stream.of(recipients).map(Recipient::resolved).toList(); + List eligible = RecipientUtil.getEligibleForSending(Stream.of(recipients).map(Recipient::resolved).toList()); + List skipped = Stream.of(SetUtil.difference(possible, eligible)).map(Recipient::getId).toList(); - for (Recipient completion : completions) { + GroupSendJobHelper.SendResult sendResult = deliver(conversationRecipient, eligible, targetSentTimestamp); + + for (Recipient completion : sendResult.completed) { recipients.remove(completion.getId()); } - Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size()); + for (RecipientId skip : skipped) { + recipients.remove(skip); + } + + List totalSkips = Util.join(skipped, sendResult.skipped); + + Log.i(TAG, "Completed now: " + sendResult.completed.size() + ", Skipped: " + totalSkips.size() + ", Remaining: " + recipients.size()); + + if (totalSkips.size() > 0 && isMms && message.getRecipient().isGroup()) { + SignalDatabase.groupReceipts().setSkipped(totalSkips, messageId); + } if (recipients.isEmpty()) { db.markAsSent(messageId, true); @@ -167,7 +182,7 @@ public class RemoteDeleteSendJob extends BaseJob { Log.w(TAG, "Failed to send remote delete to all recipients! (" + (initialRecipientCount - recipients.size() + "/" + initialRecipientCount + ")") ); } - private @NonNull List deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations, long targetSentTimestamp) + private @NonNull GroupSendJobHelper.SendResult deliver(@NonNull Recipient conversationRecipient, @NonNull List destinations, long targetSentTimestamp) throws IOException, UntrustedIdentityException { SignalServiceDataMessage.Builder dataMessageBuilder = SignalServiceDataMessage.newBuilder() @@ -187,9 +202,6 @@ public class RemoteDeleteSendJob extends BaseJob { new MessageId(messageId, isMms), dataMessage); - List blockedIds = Stream.of(conversationRecipient.getParticipants()).filter(Recipient::isBlocked).map(Recipient::getId).toList(); - SignalDatabase.groupReceipts().setSkipped(blockedIds, messageId); - return GroupSendJobHelper.getCompletedSends(destinations, results); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java index d859e9e0e6..cb8b30beec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetails.java @@ -63,6 +63,7 @@ final class MessageDetails { break; case SKIPPED: skipped.add(status); + break; } } } else {