Adapt message requests to support invite flow.

This commit is contained in:
Alan Evans 2020-05-12 15:09:47 -03:00 committed by Alex Hart
parent d3d53e6099
commit eff564ad88
13 changed files with 169 additions and 33 deletions

View File

@ -153,6 +153,8 @@ import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.groups.ui.pendingmemberinvites.PendingMemberInvitesActivity;
@ -2190,6 +2192,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
return getRecipient() != null && getRecipient().isPushGroup();
}
private boolean isPushGroupV1Conversation() {
return getRecipient() != null && getRecipient().isPushV1Group();
}
private boolean isSmsForced() {
return sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
}
@ -2825,7 +2831,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onMessageRequest(@NonNull MessageRequestViewModel viewModel) {
messageRequestBottomView.setAcceptOnClickListener(v -> viewModel.onAccept());
messageRequestBottomView.setAcceptOnClickListener(v -> viewModel.onAccept(this::showGroupChangeErrorToast));
messageRequestBottomView.setDeleteOnClickListener(v -> onMessageRequestDeleteClicked(viewModel));
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
@ -2844,6 +2850,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
private void showGroupChangeErrorToast(@NonNull GroupChangeFailureReason e) {
Toast.makeText(this, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show();
}
@Override
public void handleReaction(@NonNull View maskTarget,
@NonNull MessageRecord messageRecord,
@ -3005,9 +3015,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void presentMessageRequestDisplayState(@NonNull MessageRequestViewModel.DisplayState displayState) {
if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA) || (isPushGroupConversation() && !isActiveGroup())) {
if (getIntent().hasExtra(TEXT_EXTRA) || getIntent().hasExtra(MEDIA_EXTRA) || getIntent().hasExtra(STICKER_EXTRA)) {
Log.d(TAG, "[presentMessageRequestDisplayState] Have extra, so ignoring provided state.");
messageRequestBottomView.setVisibility(View.GONE);
} else if (isPushGroupV1Conversation() && !isActiveGroup()) {
Log.d(TAG, "[presentMessageRequestDisplayState] Inactive push group V1, so ignoring provided state.");
messageRequestBottomView.setVisibility(View.GONE);
} else {
Log.d(TAG, "[presentMessageRequestDisplayState] " + displayState);
switch (displayState) {

View File

@ -496,6 +496,11 @@ public final class GroupDatabase extends Database {
}
}
@WorkerThread
public boolean isPendingMember(@NonNull GroupId.Push groupId, @NonNull Recipient recipient) {
return getGroup(groupId).transform(g -> g.isPendingMember(recipient)).or(false);
}
private static String serializeV2GroupMembers(@NonNull Context context, @NonNull DecryptedGroup decryptedGroup) {
List<RecipientId> groupMembers = new ArrayList<>(decryptedGroup.getMembersCount());
@ -707,6 +712,17 @@ public final class GroupDatabase extends Database {
return id.isV1() ? GroupAccessControl.ALL_MEMBERS : GroupAccessControl.ONLY_ADMINS;
}
}
boolean isPendingMember(@NonNull Recipient recipient) {
if (isV2Group()) {
Optional<UUID> uuid = recipient.getUuid();
if (uuid.isPresent()) {
return DecryptedGroupUtil.findPendingByUuid(requireV2GroupProperties().getDecryptedGroup().getPendingMembersList(), uuid.get())
.isPresent();
}
}
return false;
}
}
public static class V2GroupProperties {

View File

@ -31,6 +31,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
@ -51,6 +52,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import java.io.Closeable;
import java.io.IOException;
@ -61,6 +63,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class ThreadDatabase extends Database {
@ -761,12 +764,21 @@ public class ThreadDatabase extends Database {
RecipientId threadRecipientId = getRecipientIdForThreadId(record.getThreadId());
if (!messageRequestAccepted && threadRecipientId != null) {
boolean isPushGroup = Recipient.resolved(threadRecipientId).isPushGroup();
if (isPushGroup) {
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());
Recipient resolved = Recipient.resolved(threadRecipientId);
if (resolved.isPushGroup()) {
if (resolved.isPushV2Group()) {
DecryptedGroup decryptedGroup = DatabaseFactory.getGroupDatabase(context).requireGroup(resolved.requireGroupId().requireV2()).requireV2GroupProperties().getDecryptedGroup();
Optional<UUID> inviter = DecryptedGroupUtil.findInviter(decryptedGroup.getPendingMembersList(), Recipient.self().getUuid().get());
if (recipientId != null) {
return Extra.forGroupMessageRequest(recipientId);
RecipientId recipientId = inviter.isPresent() ? RecipientId.from(inviter.get(), null) : RecipientId.UNKNOWN;
return Extra.forGroupV2invite(recipientId);
} else {
RecipientId recipientId = DatabaseFactory.getMmsSmsDatabase(context).getGroupAddedBy(record.getThreadId());
if (recipientId != null) {
return Extra.forGroupMessageRequest(recipientId);
}
}
}
@ -903,6 +915,7 @@ public class ThreadDatabase extends Database {
@JsonProperty private final boolean isAlbum;
@JsonProperty private final boolean isRemoteDelete;
@JsonProperty private final boolean isMessageRequestAccepted;
@JsonProperty private final boolean isGv2Invite;
@JsonProperty private final String groupAddedBy;
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
@ -910,6 +923,7 @@ public class ThreadDatabase extends Database {
@JsonProperty("isAlbum") boolean isAlbum,
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
@JsonProperty("isGv2Invite") boolean isGv2Invite,
@JsonProperty("groupAddedBy") String groupAddedBy)
{
this.isRevealable = isRevealable;
@ -917,31 +931,36 @@ public class ThreadDatabase extends Database {
this.isAlbum = isAlbum;
this.isRemoteDelete = isRemoteDelete;
this.isMessageRequestAccepted = isMessageRequestAccepted;
this.isGv2Invite = isGv2Invite;
this.groupAddedBy = groupAddedBy;
}
public static @NonNull Extra forViewOnce() {
return new Extra(true, false, false, false, true, null);
return new Extra(true, false, false, false, true, false, null);
}
public static @NonNull Extra forSticker() {
return new Extra(false, true, false, false, true, null);
return new Extra(false, true, false, false, true, false, null);
}
public static @NonNull Extra forAlbum() {
return new Extra(false, false, true, false, true, null);
return new Extra(false, false, true, false, true, false, null);
}
public static @NonNull Extra forRemoteDelete() {
return new Extra(false, false, false, true, true, null);
return new Extra(false, false, false, true, true, false, null);
}
public static @NonNull Extra forMessageRequest() {
return new Extra(false, false, false, false, false, null);
return new Extra(false, false, false, false, false, false, null);
}
public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
return new Extra(false, false, false, false, false, recipientId.serialize());
return new Extra(false, false, false, false, false, false, recipientId.serialize());
}
public static @NonNull Extra forGroupV2invite(RecipientId recipientId) {
return new Extra(false, false, false, false, false, true, recipientId.serialize());
}
public boolean isViewOnce() {
@ -964,6 +983,10 @@ public class ThreadDatabase extends Database {
return isMessageRequestAccepted;
}
public boolean isGv2Invite() {
return isGv2Invite;
}
public @Nullable String getGroupAddedBy() {
return groupAddedBy;
}

View File

@ -19,13 +19,14 @@ package org.thoughtcrime.securesms.database.model;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SmsDatabase;
@ -79,7 +80,9 @@ public class ThreadRecord extends DisplayRecord {
@Override
public SpannableString getDisplayBody(@NonNull Context context) {
if (getGroupAddedBy() != null) {
return emphasisAdded(context.getString(R.string.ThreadRecord_s_added_you_to_the_group, Recipient.live(getGroupAddedBy()).get().getDisplayName(context)));
return emphasisAdded(context.getString(isGv2Invite() ? R.string.ThreadRecord_s_invited_you_to_the_group
: R.string.ThreadRecord_s_added_you_to_the_group,
Recipient.live(getGroupAddedBy()).get().getDisplayName(context)));
} else if (!isMessageRequestAccepted()) {
return emphasisAdded(context.getString(R.string.ThreadRecord_message_request));
} else if (isGroupUpdate()) {
@ -194,6 +197,10 @@ public class ThreadRecord extends DisplayRecord {
else return null;
}
public boolean isGv2Invite() {
return extra != null && extra.isGv2Invite();
}
public boolean isMessageRequestAccepted() {
if (extra != null) return extra.isMessageRequestAccepted();
else return true;

View File

@ -183,6 +183,8 @@ public final class GroupManager {
{
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.acceptInvite();
DatabaseFactory.getGroupDatabase(context)
.setActive(groupId, true);
}
}

View File

@ -41,6 +41,9 @@ public class ProfileKeySendJob extends BaseJob {
private final long threadId;
private final List<RecipientId> recipients;
/**
* Suitable for a 1:1 conversation or a GV1 group only.
*/
@WorkerThread
public static ProfileKeySendJob create(@NonNull Context context, long threadId) {
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
@ -49,6 +52,10 @@ public class ProfileKeySendJob extends BaseJob {
throw new AssertionError("We have a thread but no recipient!");
}
if (conversationRecipient.isPushV2Group()) {
throw new AssertionError("Do not send profile keys directly for GV2");
}
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
: Stream.of(conversationRecipient.getId()).toList();

View File

@ -5,13 +5,22 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
@ -20,14 +29,18 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Executor;
final class MessageRequestRepository {
private static final String TAG = Log.tag(MessageRequestRepository.class);
private final Context context;
private final Executor executor;
@ -48,14 +61,24 @@ final class MessageRequestRepository {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Optional<GroupDatabase.GroupRecord> groupRecord = groupDatabase.getGroup(recipientId);
onMemberCountLoaded.accept(groupRecord.transform(record -> {
if (record.isV2Group()) {
DecryptedGroup decryptedGroup = record.requireV2GroupProperties().getDecryptedGroup();
return new GroupMemberCount(decryptedGroup.getMembersCount(), decryptedGroup.getPendingMembersCount());
} else {
return new GroupMemberCount(record.getMembers().size(), 0);
}
}).or(GroupMemberCount.ZERO));
});
}
void getMessageRequestState(@NonNull Recipient recipient, long threadId, @NonNull Consumer<MessageRequestState> state) {
executor.execute(() -> {
if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) {
if (recipient.isPushV2Group()) {
boolean pendingMember = DatabaseFactory.getGroupDatabase(context)
.isPendingMember(recipient.requireGroupId().requireV2(), Recipient.self());
state.accept(pendingMember ? MessageRequestState.UNACCEPTED
: MessageRequestState.ACCEPTED);
} else if (!RecipientUtil.isMessageRequestAccepted(context, threadId)) {
state.accept(MessageRequestState.UNACCEPTED);
} else if (RecipientUtil.isPreMessageRequestThread(context, threadId) && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) {
state.accept(MessageRequestState.LEGACY);
@ -65,23 +88,46 @@ final class MessageRequestRepository {
});
}
void acceptMessageRequest(@NonNull LiveRecipient liveRecipient, long threadId, @NonNull Runnable onMessageRequestAccepted) {
void acceptMessageRequest(@NonNull LiveRecipient liveRecipient,
long threadId,
@NonNull Runnable onMessageRequestAccepted,
@NonNull GroupChangeErrorCallback mainThreadError)
{
GroupChangeErrorCallback error = e -> Util.runOnMain(() -> mainThreadError.onError(e));
executor.execute(()-> {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileSharing(liveRecipient.getId(), true);
liveRecipient.refresh();
if (liveRecipient.get().isPushV2Group()) {
try {
Log.i(TAG, "GV2 accepting invite");
GroupManager.acceptInvite(context, liveRecipient.get().requireGroupId().requireV2());
List<MessagingDatabase.MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context)
.setEntireThreadRead(threadId);
MessageNotifier.updateNotification(context);
MarkReadReceiver.process(context, messageIds);
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileSharing(liveRecipient.getId(), true);
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId()));
onMessageRequestAccepted.run();
} catch (GroupInsufficientRightsException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.NO_RIGHTS);
} catch (GroupChangeBusyException | GroupChangeFailedException | GroupNotAMemberException | IOException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.OTHER);
}
} else {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
recipientDatabase.setProfileSharing(liveRecipient.getId(), true);
MessageSender.sendProfileKey(context, threadId);
List<MessagingDatabase.MarkedMessageInfo> messageIds = DatabaseFactory.getThreadDatabase(context)
.setEntireThreadRead(threadId);
MessageNotifier.updateNotification(context);
MarkReadReceiver.process(context, messageIds);
if (TextSecurePreferences.isMultiDevice(context)) {
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(liveRecipient.getId()));
}
onMessageRequestAccepted.run();
}
MessageSender.sendProfileKey(context, threadId);
onMessageRequestAccepted.run();
});
}

View File

@ -11,6 +11,7 @@ import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
@ -89,10 +90,11 @@ public class MessageRequestViewModel extends ViewModel {
}
@MainThread
public void onAccept() {
public void onAccept(@NonNull GroupChangeErrorCallback error) {
repository.acceptMessageRequest(liveRecipient, threadId, () -> {
status.postValue(Status.ACCEPTED);
});
},
error);
}
@MainThread

View File

@ -65,7 +65,11 @@ public class MessageRequestsBottomView extends ConstraintLayout {
blockedButtons.setVisibility(VISIBLE);
} else {
if (recipient.isGroup()) {
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_join_the_group_s_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
if (recipient.isPushV2Group()) {
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_you_were_invited_to_join_the_group_s, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
} else {
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_join_the_group_s_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
}
} else {
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept, HtmlUtil.bold(recipient.getDisplayName(getContext()))), 0));
}

View File

@ -603,6 +603,11 @@ public class Recipient {
return groupId != null && groupId.isPush();
}
public boolean isPushV1Group() {
GroupId groupId = resolve().groupId;
return groupId != null && groupId.isV1();
}
public boolean isPushV2Group() {
GroupId groupId = resolve().groupId;
return groupId != null && groupId.isV2();

View File

@ -83,6 +83,9 @@ public class MessageSender {
private static final String TAG = MessageSender.class.getSimpleName();
/**
* Suitable for a 1:1 conversation or a GV1 group only.
*/
@WorkerThread
public static void sendProfileKey(final Context context, final long threadId) {
ApplicationDependencies.getJobManager().add(ProfileKeySendJob.create(context, threadId));

View File

@ -774,6 +774,7 @@
<string name="MessageRequestBottomView_unblock">Unblock</string>
<string name="MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept">Do you want to let %1$s message you? They won\'t know you\'ve seen their messages until you accept.</string>
<string name="MessageRequestBottomView_do_you_want_to_join_the_group_s_they_wont_know_youve_seen_their_messages_until_you_accept">Do you want to join the group %1$s? They won\'t know you\'ve seen their messages until you accept.</string>
<string name="MessageRequestBottomView_you_were_invited_to_join_the_group_s">You were invited to join the group %1$s. Do you want to let members of this group message you?</string>
<string name="MessageRequestBottomView_unblock_s_to_message_and_call_each_other">Unblock %1$s to message and call each other.</string>
<string name="MessageRequestBottomView_unblock_to_allow_group_members_to_add_you_to_this_group_again">Unblock to allow group members to add you to this group again.</string>
<string name="MessageRequestProfileView_member_of_one_group">Member of %1$s</string>
@ -1087,6 +1088,7 @@
<string name="ThreadRecord_message_could_not_be_processed">Message could not be processed</string>
<string name="ThreadRecord_message_request">Message Request</string>
<string name="ThreadRecord_s_added_you_to_the_group">%1$s added you to the group</string>
<string name="ThreadRecord_s_invited_you_to_the_group">%1$s invited you to the group</string>
<!-- UpdateApkReadyListener -->
<string name="UpdateApkReadyListener_Signal_update">Signal update</string>

View File

@ -252,6 +252,12 @@ public final class DecryptedGroupUtil {
return -1;
}
public static Optional<UUID> findInviter(List<DecryptedPendingMember> pendingMembersList, UUID uuid) {
return Optional.fromNullable(findPendingByUuid(pendingMembersList, uuid).transform(DecryptedPendingMember::getAddedByUuid)
.transform(UuidUtil::fromByteStringOrNull)
.orNull());
}
public static class NotAbleToApplyChangeException extends Throwable {
}
}