mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 19:29:54 +01:00
Add basic profile spoofing detection.
This commit is contained in:
@@ -42,6 +42,7 @@ import android.provider.ContactsContract;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
@@ -68,8 +69,10 @@ import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
@@ -207,6 +210,9 @@ import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
@@ -244,8 +250,10 @@ import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SmsUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
@@ -342,6 +350,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
protected Stub<ReminderView> reminderView;
|
||||
private Stub<UnverifiedBannerView> unverifiedBannerView;
|
||||
private Stub<GroupShareProfileView> groupShareProfileView;
|
||||
private Stub<ReviewBannerView> reviewBanner;
|
||||
private TypingStatusTextWatcher typingTextWatcher;
|
||||
private ConversationSearchBottomBar searchNav;
|
||||
private MenuItem searchViewItem;
|
||||
@@ -1829,6 +1838,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
|
||||
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
|
||||
groupShareProfileView = ViewUtil.findStubById(this, R.id.group_share_profile_view_stub);
|
||||
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
|
||||
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
|
||||
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
|
||||
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
|
||||
@@ -1997,6 +2007,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
groupViewModel = ViewModelProviders.of(this, new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
|
||||
recipient.observe(this, groupViewModel::onRecipientChange);
|
||||
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
||||
groupViewModel.getReviewState().observe(this, this::presentGroupReviewBanner);
|
||||
}
|
||||
|
||||
private void initializeMentionsViewModel() {
|
||||
@@ -3067,6 +3078,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
|
||||
messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
|
||||
|
||||
viewModel.getRequestReviewDisplayState().observe(this, this::presentRequestReviewBanner);
|
||||
viewModel.getMessageData().observe(this, this::presentMessageRequestBottomViewTo);
|
||||
viewModel.getMessageRequestDisplayState().observe(this, this::presentMessageRequestDisplayState);
|
||||
viewModel.getFailures().observe(this, this::showGroupChangeErrorToast);
|
||||
@@ -3092,6 +3104,42 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
});
|
||||
}
|
||||
|
||||
private void presentRequestReviewBanner(@NonNull MessageRequestViewModel.RequestReviewDisplayState state) {
|
||||
switch (state) {
|
||||
case SHOWN:
|
||||
reviewBanner.get().setVisibility(View.VISIBLE);
|
||||
|
||||
CharSequence message = new SpannableStringBuilder().append(SpanUtil.bold(getString(R.string.ConversationFragment__review_requests_carefully)))
|
||||
.append(" ")
|
||||
.append(getString(R.string.ConversationFragment__signal_found_another_contact_with_the_same_name));
|
||||
|
||||
reviewBanner.get().setBannerMessage(message);
|
||||
|
||||
Drawable drawable = Objects.requireNonNull(ThemeUtil.getThemedDrawable(this, R.attr.menu_info_icon)).mutate();
|
||||
DrawableCompat.setTint(drawable, ThemeUtil.getThemedColor(this, R.attr.icon_tint));
|
||||
|
||||
reviewBanner.get().setBannerIcon(drawable);
|
||||
reviewBanner.get().setOnClickListener(unused -> handleReviewRequest(recipient.getId()));
|
||||
break;
|
||||
case HIDDEN:
|
||||
reviewBanner.get().setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void presentGroupReviewBanner(@NonNull ConversationGroupViewModel.ReviewState groupReviewState) {
|
||||
if (groupReviewState.getCount() > 0) {
|
||||
reviewBanner.get().setVisibility(View.VISIBLE);
|
||||
reviewBanner.get().setBannerMessage(getString(R.string.ConversationFragment__d_group_members_have_the_same_name, groupReviewState.getCount()));
|
||||
reviewBanner.get().setBannerRecipient(groupReviewState.getRecipient());
|
||||
reviewBanner.get().setOnClickListener(unused -> handleReviewGroupMembers(groupReviewState.getGroupId()));
|
||||
} else if (reviewBanner.resolved()) {
|
||||
reviewBanner.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showMessageRequestBusy() {
|
||||
messageRequestBottomView.showBusy();
|
||||
}
|
||||
@@ -3100,6 +3148,24 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||
messageRequestBottomView.hideBusy();
|
||||
}
|
||||
|
||||
private void handleReviewGroupMembers(@Nullable GroupId.V2 groupId) {
|
||||
if (groupId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReviewCardDialogFragment.createForReviewMembers(groupId)
|
||||
.show(getSupportFragmentManager(), null);
|
||||
}
|
||||
|
||||
private void handleReviewRequest(@NonNull RecipientId recipientId) {
|
||||
if (recipientId == Recipient.UNKNOWN.getId()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReviewCardDialogFragment.createForReviewRequest(recipientId)
|
||||
.show(getSupportFragmentManager(), null);
|
||||
}
|
||||
|
||||
private void showGroupChangeErrorToast(@NonNull GroupChangeFailureReason e) {
|
||||
Toast.makeText(this, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@@ -10,14 +10,19 @@ import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
@@ -25,6 +30,8 @@ import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
@@ -32,15 +39,31 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<GroupDatabase.MemberLevel> selfMembershipLevel;
|
||||
private final LiveData<Integer> actionableRequestingMembers;
|
||||
private final LiveData<ReviewState> reviewState;
|
||||
|
||||
private ConversationGroupViewModel() {
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
|
||||
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
|
||||
if (record != null && record.isV2Group()) {
|
||||
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
|
||||
.map(ReviewRecipient::getRecipient)
|
||||
.toList();
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
|
||||
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
|
||||
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
|
||||
duplicates,
|
||||
(record, dups) -> dups.isEmpty()
|
||||
? ReviewState.EMPTY
|
||||
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
|
||||
|
||||
}
|
||||
|
||||
void onRecipientChange(Recipient recipient) {
|
||||
@@ -62,6 +85,10 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
return selfMembershipLevel;
|
||||
}
|
||||
|
||||
public LiveData<ReviewState> getReviewState() {
|
||||
return reviewState;
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
@@ -117,6 +144,33 @@ final class ConversationGroupViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
static final class ReviewState {
|
||||
|
||||
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final Recipient recipient;
|
||||
private final int count;
|
||||
|
||||
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
|
||||
this.groupId = groupId;
|
||||
this.recipient = recipient;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public @Nullable GroupId.V2 getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
static final class GroupActiveState {
|
||||
private final boolean isActive;
|
||||
private final boolean isActiveV2;
|
||||
|
||||
Reference in New Issue
Block a user