mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Add basic profile spoofing detection.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* Banner displayed within a conversation when a review is suggested.
|
||||
*/
|
||||
public class ReviewBannerView extends ConstraintLayout {
|
||||
|
||||
private static final @Px int ELEVATION = ViewUtil.dpToPx(4);
|
||||
|
||||
private ImageView bannerIcon;
|
||||
private TextView bannerMessage;
|
||||
private View bannerClose;
|
||||
private AvatarImageView topLeftAvatar;
|
||||
private AvatarImageView bottomRightAvatar;
|
||||
private View stroke;
|
||||
|
||||
public ReviewBannerView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ReviewBannerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
bannerIcon = findViewById(R.id.banner_icon);
|
||||
bannerMessage = findViewById(R.id.banner_message);
|
||||
bannerClose = findViewById(R.id.banner_close);
|
||||
topLeftAvatar = findViewById(R.id.banner_avatar_1);
|
||||
bottomRightAvatar = findViewById(R.id.banner_avatar_2);
|
||||
stroke = findViewById(R.id.banner_avatar_stroke);
|
||||
|
||||
FallbackPhotoProvider provider = new FallbackPhotoProvider();
|
||||
|
||||
topLeftAvatar.setFallbackPhotoProvider(provider);
|
||||
bottomRightAvatar.setFallbackPhotoProvider(provider);
|
||||
|
||||
bannerClose.setOnClickListener(v -> setVisibility(GONE));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
setOutlineProvider(new ViewOutlineProvider() {
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline) {
|
||||
outline.setRect(-100, -100, view.getWidth() + 100, view.getHeight() + ELEVATION);
|
||||
}
|
||||
});
|
||||
setElevation(ELEVATION);
|
||||
}
|
||||
}
|
||||
|
||||
public void setBannerMessage(@Nullable CharSequence charSequence) {
|
||||
bannerMessage.setText(charSequence);
|
||||
}
|
||||
|
||||
public void setBannerIcon(@Nullable Drawable icon) {
|
||||
bannerIcon.setImageDrawable(icon);
|
||||
|
||||
bannerIcon.setVisibility(VISIBLE);
|
||||
topLeftAvatar.setVisibility(GONE);
|
||||
bottomRightAvatar.setVisibility(GONE);
|
||||
stroke.setVisibility(GONE);
|
||||
}
|
||||
|
||||
public void setBannerRecipient(@NonNull Recipient recipient) {
|
||||
topLeftAvatar.setAvatar(recipient);
|
||||
bottomRightAvatar.setAvatar(recipient);
|
||||
|
||||
bannerIcon.setVisibility(GONE);
|
||||
topLeftAvatar.setVisibility(VISIBLE);
|
||||
bottomRightAvatar.setVisibility(VISIBLE);
|
||||
stroke.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
@Override
|
||||
public @NonNull
|
||||
FallbackContactPhoto getPhotoForGroup() {
|
||||
throw new UnsupportedOperationException("This provider does not support groups");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForResolvingRecipient() {
|
||||
throw new UnsupportedOperationException("This provider does not support resolving recipients");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
|
||||
throw new UnsupportedOperationException("This provider does not support local number");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FallbackContactPhoto getPhotoForRecipientWithName(String name) {
|
||||
return new FixedSizeGeneratedContactPhoto(name, R.drawable.ic_profile_outline_20);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
return new FallbackPhoto20dp(R.drawable.ic_profile_outline_20);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FixedSizeGeneratedContactPhoto extends GeneratedContactPhoto {
|
||||
public FixedSizeGeneratedContactPhoto(@NonNull String name, int fallbackResId) {
|
||||
super(name, fallbackResId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return new FallbackPhoto20dp(getFallbackResId()).asDrawable(context, color, inverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
/**
|
||||
* Represents a card showing user details for a recipient under review.
|
||||
*
|
||||
* See {@link ReviewCardViewHolder} for usage.
|
||||
*/
|
||||
class ReviewCard {
|
||||
|
||||
private final ReviewRecipient reviewRecipient;
|
||||
private final int inCommonGroupsCount;
|
||||
private final CardType cardType;
|
||||
private final Action primaryAction;
|
||||
private final Action secondaryAction;
|
||||
|
||||
ReviewCard(@NonNull ReviewRecipient reviewRecipient,
|
||||
int inCommonGroupsCount,
|
||||
@NonNull CardType cardType,
|
||||
@Nullable Action primaryAction,
|
||||
@Nullable Action secondaryAction)
|
||||
{
|
||||
this.reviewRecipient = reviewRecipient;
|
||||
this.inCommonGroupsCount = inCommonGroupsCount;
|
||||
this.cardType = cardType;
|
||||
this.primaryAction = primaryAction;
|
||||
this.secondaryAction = secondaryAction;
|
||||
}
|
||||
|
||||
@NonNull Recipient getReviewRecipient() {
|
||||
return reviewRecipient.getRecipient();
|
||||
}
|
||||
|
||||
@NonNull CardType getCardType() {
|
||||
return cardType;
|
||||
}
|
||||
|
||||
int getInCommonGroupsCount() {
|
||||
return inCommonGroupsCount;
|
||||
}
|
||||
|
||||
@Nullable ProfileChangeDetails.StringChange getNameChange() {
|
||||
if (reviewRecipient.getProfileChangeDetails() == null || !reviewRecipient.getProfileChangeDetails().hasProfileNameChange()) {
|
||||
return null;
|
||||
} else {
|
||||
return reviewRecipient.getProfileChangeDetails().getProfileNameChange();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable Action getPrimaryAction() {
|
||||
return primaryAction;
|
||||
}
|
||||
|
||||
@Nullable Action getSecondaryAction() {
|
||||
return secondaryAction;
|
||||
}
|
||||
|
||||
enum CardType {
|
||||
MEMBER,
|
||||
REQUEST,
|
||||
YOUR_CONTACT
|
||||
}
|
||||
|
||||
enum Action {
|
||||
UPDATE_CONTACT,
|
||||
DELETE,
|
||||
BLOCK,
|
||||
REMOVE_FROM_GROUP
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class ReviewCardAdapter extends ListAdapter<ReviewCard, ReviewCardViewHolder> {
|
||||
|
||||
private final @StringRes int noGroupsInCommonResId;
|
||||
private final @PluralsRes int groupsInCommonResId;
|
||||
private final CallbacksAdapter callbackAdapter;
|
||||
|
||||
protected ReviewCardAdapter(@StringRes int noGroupsInCommonResId, @PluralsRes int groupsInCommonResId, @NonNull Callbacks callback) {
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
|
||||
this.noGroupsInCommonResId = noGroupsInCommonResId;
|
||||
this.groupsInCommonResId = groupsInCommonResId;
|
||||
this.callbackAdapter = new CallbacksAdapter(callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ReviewCardViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ReviewCardViewHolder(LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.review_card, parent, false),
|
||||
noGroupsInCommonResId,
|
||||
groupsInCommonResId,
|
||||
callbackAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ReviewCardViewHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onCardClicked(@NonNull ReviewCard card);
|
||||
void onActionClicked(@NonNull ReviewCard card, @NonNull ReviewCard.Action action);
|
||||
}
|
||||
|
||||
private final class CallbacksAdapter implements ReviewCardViewHolder.Callbacks {
|
||||
|
||||
private final Callbacks callback;
|
||||
|
||||
private CallbacksAdapter(@NonNull Callbacks callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCardClicked(int position) {
|
||||
callback.onCardClicked(getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrimaryActionItemClicked(int position) {
|
||||
ReviewCard card = getItem(position);
|
||||
callback.onActionClicked(card, Objects.requireNonNull(card.getPrimaryAction()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSecondaryActionItemClicked(int position) {
|
||||
ReviewCard card = getItem(position);
|
||||
callback.onActionClicked(card, Objects.requireNonNull(card.getSecondaryAction()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
|
||||
public class ReviewCardDialogFragment extends FullScreenDialogFragment {
|
||||
|
||||
private static final String EXTRA_TITLE_RES_ID = "extra.title.res.id";
|
||||
private static final String EXTRA_DESCRIPTION_RES_ID = "extra.description.res.id";
|
||||
private static final String EXTRA_GROUPS_IN_COMMON_RES_ID = "extra.groups.in.common.res.id";
|
||||
private static final String EXTRA_NO_GROUPS_IN_COMMON_RES_ID = "extra.no.groups.in.common.res.id";
|
||||
private static final String EXTRA_RECIPIENT_ID = "extra.recipient.id";
|
||||
private static final String EXTRA_GROUP_ID = "extra.group.id";
|
||||
|
||||
private ReviewCardViewModel viewModel;
|
||||
|
||||
public static ReviewCardDialogFragment createForReviewRequest(@NonNull RecipientId recipientId) {
|
||||
return create(R.string.ReviewCardDialogFragment__review_request,
|
||||
R.string.ReviewCardDialogFragment__if_youre_not_sure,
|
||||
R.string.ReviewCardDialogFragment__no_groups_in_common,
|
||||
R.plurals.ReviewCardDialogFragment__d_groups_in_common,
|
||||
recipientId,
|
||||
null);
|
||||
}
|
||||
|
||||
public static ReviewCardDialogFragment createForReviewMembers(@NonNull GroupId.V2 groupId) {
|
||||
return create(R.string.ReviewCardDialogFragment__review_members,
|
||||
R.string.ReviewCardDialogFragment__d_group_members_have_the_same_name,
|
||||
R.string.ReviewCardDialogFragment__no_other_groups_in_common,
|
||||
R.plurals.ReviewCardDialogFragment__d_other_groups_in_common,
|
||||
null,
|
||||
groupId);
|
||||
}
|
||||
|
||||
private static ReviewCardDialogFragment create(@StringRes int titleResId,
|
||||
@StringRes int descriptionResId,
|
||||
@StringRes int noGroupsInCommonResId,
|
||||
@PluralsRes int groupsInCommonResId,
|
||||
@Nullable RecipientId recipientId,
|
||||
@Nullable GroupId.V2 groupId)
|
||||
{
|
||||
ReviewCardDialogFragment fragment = new ReviewCardDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putInt(EXTRA_TITLE_RES_ID, titleResId);
|
||||
args.putInt(EXTRA_DESCRIPTION_RES_ID, descriptionResId);
|
||||
args.putInt(EXTRA_GROUPS_IN_COMMON_RES_ID, groupsInCommonResId);
|
||||
args.putInt(EXTRA_NO_GROUPS_IN_COMMON_RES_ID, noGroupsInCommonResId);
|
||||
args.putParcelable(EXTRA_RECIPIENT_ID, recipientId);
|
||||
args.putString(EXTRA_GROUP_ID, groupId != null ? groupId.toString() : null);
|
||||
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
try {
|
||||
initializeViewModel();
|
||||
} catch (BadGroupIdException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
TextView description = view.findViewById(R.id.description);
|
||||
RecyclerView recycler = view.findViewById(R.id.recycler);
|
||||
|
||||
ReviewCardAdapter adapter = new ReviewCardAdapter(getNoGroupsInCommonResId(), getGroupsInCommonResId(), new AdapterCallbacks());
|
||||
recycler.setAdapter(adapter);
|
||||
|
||||
viewModel.getReviewCards().observe(getViewLifecycleOwner(), cards -> {
|
||||
adapter.submitList(cards);
|
||||
description.setText(getString(getDescriptionResId(), cards.size()));
|
||||
});
|
||||
|
||||
viewModel.getReviewEvents().observe(getViewLifecycleOwner(), this::onReviewEvent);
|
||||
}
|
||||
|
||||
private void initializeViewModel() throws BadGroupIdException {
|
||||
ReviewCardRepository repository = getRepository();
|
||||
ReviewCardViewModel.Factory factory = new ReviewCardViewModel.Factory(repository, getGroupId() != null);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReviewCardViewModel.class);
|
||||
}
|
||||
|
||||
private @StringRes int getDescriptionResId() {
|
||||
return requireArguments().getInt(EXTRA_DESCRIPTION_RES_ID);
|
||||
}
|
||||
|
||||
private @PluralsRes int getGroupsInCommonResId() {
|
||||
return requireArguments().getInt(EXTRA_GROUPS_IN_COMMON_RES_ID);
|
||||
}
|
||||
|
||||
private @StringRes int getNoGroupsInCommonResId() {
|
||||
return requireArguments().getInt(EXTRA_NO_GROUPS_IN_COMMON_RES_ID);
|
||||
}
|
||||
|
||||
private @Nullable RecipientId getRecipientId() {
|
||||
return requireArguments().getParcelable(EXTRA_RECIPIENT_ID);
|
||||
}
|
||||
|
||||
private @Nullable GroupId.V2 getGroupId() throws BadGroupIdException {
|
||||
GroupId groupId = GroupId.parseNullable(requireArguments().getString(EXTRA_GROUP_ID));
|
||||
|
||||
if (groupId != null) {
|
||||
return groupId.requireV2();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull ReviewCardRepository getRepository() throws BadGroupIdException {
|
||||
RecipientId recipientId = getRecipientId();
|
||||
GroupId.V2 groupId = getGroupId();
|
||||
|
||||
if (recipientId != null) {
|
||||
return new ReviewCardRepository(requireContext(), recipientId);
|
||||
} else if (groupId != null) {
|
||||
return new ReviewCardRepository(requireContext(), groupId);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private void onReviewEvent(ReviewCardViewModel.Event reviewEvent) {
|
||||
switch (reviewEvent) {
|
||||
case DISMISS:
|
||||
dismiss();
|
||||
break;
|
||||
case REMOVE_FAILED:
|
||||
toast(R.string.ReviewCardDialogFragment__failed_to_remove_group_member);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unhandled event: " + reviewEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private void toast(@StringRes int message) {
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTitle() {
|
||||
return requireArguments().getInt(EXTRA_TITLE_RES_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDialogLayoutResource() {
|
||||
return R.layout.fragment_review;
|
||||
}
|
||||
|
||||
private final class AdapterCallbacks implements ReviewCardAdapter.Callbacks {
|
||||
|
||||
@Override
|
||||
public void onCardClicked(@NonNull ReviewCard card) {
|
||||
RecipientBottomSheetDialogFragment.create(card.getReviewRecipient().getId(), null)
|
||||
.show(requireFragmentManager(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionClicked(@NonNull ReviewCard card, @NonNull ReviewCard.Action action) {
|
||||
switch (action) {
|
||||
case UPDATE_CONTACT:
|
||||
Intent contactEditIntent = new Intent(Intent.ACTION_EDIT);
|
||||
contactEditIntent.setDataAndType(card.getReviewRecipient().getContactUri(), ContactsContract.Contacts.CONTENT_ITEM_TYPE);
|
||||
startActivity(contactEditIntent);
|
||||
break;
|
||||
case REMOVE_FROM_GROUP:
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(getString(R.string.ReviewCardDialogFragment__remove_s_from_group,
|
||||
card.getReviewRecipient().getDisplayName(requireContext())))
|
||||
.setPositiveButton(R.string.ReviewCardDialogFragment__remove, (dialog, which) -> {
|
||||
viewModel.act(card, action);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel,
|
||||
(dialog, which) -> dialog.dismiss())
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
break;
|
||||
default:
|
||||
viewModel.act(card, action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
class ReviewCardRepository {
|
||||
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
private final RecipientId recipientId;
|
||||
|
||||
protected ReviewCardRepository(@NonNull Context context,
|
||||
@NonNull GroupId.V2 groupId)
|
||||
{
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
this.recipientId = null;
|
||||
}
|
||||
|
||||
protected ReviewCardRepository(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId)
|
||||
{
|
||||
this.context = context;
|
||||
this.groupId = null;
|
||||
this.recipientId = recipientId;
|
||||
}
|
||||
|
||||
void loadRecipients(@NonNull OnRecipientsLoadedListener onRecipientsLoadedListener) {
|
||||
if (groupId != null) {
|
||||
loadRecipientsForGroup(groupId, onRecipientsLoadedListener);
|
||||
} else if (recipientId != null) {
|
||||
loadSimilarRecipients(context, recipientId, onRecipientsLoadedListener);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
int loadGroupsInCommonCount(@NonNull ReviewRecipient reviewRecipient) {
|
||||
return ReviewUtil.getGroupsInCommonCount(context, reviewRecipient.getRecipient().getId());
|
||||
}
|
||||
|
||||
void block(@NonNull ReviewCard reviewCard, @NonNull Runnable onActionCompleteListener) {
|
||||
if (recipientId == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientUtil.blockNonGroup(context, reviewCard.getReviewRecipient());
|
||||
onActionCompleteListener.run();
|
||||
});
|
||||
}
|
||||
|
||||
void delete(@NonNull ReviewCard reviewCard, @NonNull Runnable onActionCompleteListener) {
|
||||
if (recipientId == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Recipient resolved = Recipient.resolved(recipientId);
|
||||
|
||||
if (resolved.isGroup()) throw new AssertionError();
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forDelete(recipientId));
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
long threadId = Objects.requireNonNull(threadDatabase.getThreadIdFor(recipientId));
|
||||
|
||||
threadDatabase.deleteConversation(threadId);
|
||||
onActionCompleteListener.run();
|
||||
});
|
||||
}
|
||||
|
||||
void removeFromGroup(@NonNull ReviewCard reviewCard, @NonNull OnRemoveFromGroupListener onRemoveFromGroupListener) {
|
||||
if (groupId == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.ejectFromGroup(context, groupId, reviewCard.getReviewRecipient());
|
||||
onRemoveFromGroupListener.onActionCompleted();
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
onRemoveFromGroupListener.onActionFailed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void loadRecipientsForGroup(@NonNull GroupId.V2 groupId,
|
||||
@NonNull OnRecipientsLoadedListener onRecipientsLoadedListener)
|
||||
{
|
||||
SignalExecutors.BOUNDED.execute(() -> onRecipientsLoadedListener.onRecipientsLoaded(ReviewUtil.getDuplicatedRecipients(groupId)));
|
||||
}
|
||||
|
||||
private static void loadSimilarRecipients(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId,
|
||||
@NonNull OnRecipientsLoadedListener onRecipientsLoadedListener)
|
||||
{
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
Recipient resolved = Recipient.resolved(recipientId);
|
||||
|
||||
List<RecipientId> recipientIds = DatabaseFactory.getRecipientDatabase(context)
|
||||
.getSimilarRecipientIds(resolved);
|
||||
|
||||
if (recipientIds.isEmpty()) {
|
||||
onRecipientsLoadedListener.onRecipientsLoadFailed();
|
||||
return;
|
||||
}
|
||||
|
||||
List<ReviewRecipient> recipients = Stream.of(recipientIds)
|
||||
.map(Recipient::resolved)
|
||||
.map(ReviewRecipient::new)
|
||||
.sorted(new ReviewRecipient.Comparator(context, recipientId))
|
||||
.toList();
|
||||
|
||||
onRecipientsLoadedListener.onRecipientsLoaded(recipients);
|
||||
});
|
||||
}
|
||||
|
||||
interface OnRecipientsLoadedListener {
|
||||
void onRecipientsLoaded(@NonNull List<ReviewRecipient> recipients);
|
||||
void onRecipientsLoadFailed();
|
||||
}
|
||||
|
||||
interface OnRemoveFromGroupListener {
|
||||
void onActionCompleted();
|
||||
void onActionFailed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.PluralsRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
|
||||
class ReviewCardViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final int noGroupsInCommonResId;
|
||||
private final int groupsInCommonResId;
|
||||
private final TextView title;
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView name;
|
||||
private final TextView subtextLine1;
|
||||
private final TextView subtextLine2;
|
||||
private final Button primaryAction;
|
||||
private final Button secondaryAction;
|
||||
|
||||
public ReviewCardViewHolder(@NonNull View itemView,
|
||||
@StringRes int noGroupsInCommonResId,
|
||||
@PluralsRes int groupsInCommonResId,
|
||||
@NonNull Callbacks callbacks)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.noGroupsInCommonResId = noGroupsInCommonResId;
|
||||
this.groupsInCommonResId = groupsInCommonResId;
|
||||
this.title = itemView.findViewById(R.id.card_title);
|
||||
this.avatar = itemView.findViewById(R.id.card_avatar);
|
||||
this.name = itemView.findViewById(R.id.card_name);
|
||||
this.subtextLine1 = itemView.findViewById(R.id.card_subtext_line1);
|
||||
this.subtextLine2 = itemView.findViewById(R.id.card_subtext_line2);
|
||||
this.primaryAction = itemView.findViewById(R.id.card_primary_action_button);
|
||||
this.secondaryAction = itemView.findViewById(R.id.card_secondary_action_button);
|
||||
|
||||
itemView.findViewById(R.id.card_tap_target).setOnClickListener(unused -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
callbacks.onCardClicked(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
primaryAction.setOnClickListener(unused -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
callbacks.onPrimaryActionItemClicked(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
|
||||
secondaryAction.setOnClickListener(unused -> {
|
||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
callbacks.onSecondaryActionItemClicked(getAdapterPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void bind(@NonNull ReviewCard reviewCard) {
|
||||
Context context = itemView.getContext();
|
||||
|
||||
avatar.setAvatar(reviewCard.getReviewRecipient());
|
||||
name.setText(reviewCard.getReviewRecipient().getDisplayName(context));
|
||||
title.setText(getTitleResId(reviewCard.getCardType()));
|
||||
|
||||
switch (reviewCard.getCardType()) {
|
||||
case MEMBER:
|
||||
case REQUEST:
|
||||
setNonContactSublines(context, reviewCard);
|
||||
break;
|
||||
case YOUR_CONTACT:
|
||||
subtextLine1.setText(reviewCard.getReviewRecipient().getE164().orNull());
|
||||
subtextLine2.setText(getGroupsInCommon(reviewCard.getInCommonGroupsCount()));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
setActions(reviewCard);
|
||||
}
|
||||
|
||||
private void setNonContactSublines(@NonNull Context context, @NonNull ReviewCard reviewCard) {
|
||||
subtextLine1.setText(getGroupsInCommon(reviewCard.getInCommonGroupsCount()));
|
||||
|
||||
if (reviewCard.getNameChange() != null) {
|
||||
subtextLine2.setText(SpanUtil.italic(context.getString(R.string.ReviewCard__recently_changed,
|
||||
reviewCard.getNameChange().getPrevious(),
|
||||
reviewCard.getNameChange().getNew())));
|
||||
}
|
||||
}
|
||||
|
||||
private void setActions(@NonNull ReviewCard reviewCard) {
|
||||
setAction(reviewCard.getPrimaryAction(), primaryAction);
|
||||
setAction(reviewCard.getSecondaryAction(), secondaryAction);
|
||||
}
|
||||
|
||||
private String getGroupsInCommon(int groupsInCommon) {
|
||||
if (groupsInCommon == 0) {
|
||||
return itemView.getContext().getString(noGroupsInCommonResId);
|
||||
} else {
|
||||
return itemView.getResources().getQuantityString(groupsInCommonResId, groupsInCommon, groupsInCommon);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setAction(@Nullable ReviewCard.Action action, @NonNull Button actionButton) {
|
||||
if (action != null) {
|
||||
actionButton.setText(getActionLabelResId(action));
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
actionButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onCardClicked(int position);
|
||||
void onPrimaryActionItemClicked(int position);
|
||||
void onSecondaryActionItemClicked(int position);
|
||||
}
|
||||
|
||||
private static @StringRes int getTitleResId(@NonNull ReviewCard.CardType cardType) {
|
||||
switch (cardType) {
|
||||
case MEMBER:
|
||||
return R.string.ReviewCard__member;
|
||||
case REQUEST:
|
||||
return R.string.ReviewCard__request;
|
||||
case YOUR_CONTACT:
|
||||
return R.string.ReviewCard__your_contact;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported card type " + cardType);
|
||||
}
|
||||
}
|
||||
|
||||
private static @StringRes int getActionLabelResId(@NonNull ReviewCard.Action action) {
|
||||
switch (action) {
|
||||
case UPDATE_CONTACT:
|
||||
return R.string.ReviewCard__update_contact;
|
||||
case DELETE:
|
||||
return R.string.ReviewCard__delete;
|
||||
case BLOCK:
|
||||
return R.string.ReviewCard__block;
|
||||
case REMOVE_FROM_GROUP:
|
||||
return R.string.ReviewCard__remove_from_group;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported action: " + action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ReviewCardViewModel extends ViewModel {
|
||||
|
||||
private final ReviewCardRepository repository;
|
||||
private final boolean isGroupThread;
|
||||
private final MutableLiveData<List<ReviewRecipient>> reviewRecipients;
|
||||
private final LiveData<List<ReviewCard>> reviewCards;
|
||||
private final SingleLiveEvent<Event> reviewEvents;
|
||||
|
||||
public ReviewCardViewModel(@NonNull ReviewCardRepository repository, boolean isGroupThread) {
|
||||
this.repository = repository;
|
||||
this.isGroupThread = isGroupThread;
|
||||
this.reviewRecipients = new MutableLiveData<>();
|
||||
this.reviewCards = LiveDataUtil.mapAsync(reviewRecipients, this::transformReviewRecipients);
|
||||
this.reviewEvents = new SingleLiveEvent<>();
|
||||
|
||||
repository.loadRecipients(new OnRecipientsLoadedListener());
|
||||
}
|
||||
|
||||
LiveData<List<ReviewCard>> getReviewCards() {
|
||||
return reviewCards;
|
||||
}
|
||||
|
||||
LiveData<Event> getReviewEvents() {
|
||||
return reviewEvents;
|
||||
}
|
||||
|
||||
public void act(@NonNull ReviewCard card, @NonNull ReviewCard.Action action) {
|
||||
if (card.getPrimaryAction() == action || card.getSecondaryAction() == action) {
|
||||
performAction(card, action);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Cannot perform " + action + " on review card.");
|
||||
}
|
||||
}
|
||||
|
||||
private void performAction(@NonNull ReviewCard card, @NonNull ReviewCard.Action action) {
|
||||
switch (action) {
|
||||
case BLOCK:
|
||||
repository.block(card, () -> reviewEvents.postValue(Event.DISMISS));
|
||||
break;
|
||||
case DELETE:
|
||||
repository.delete(card, () -> reviewEvents.postValue(Event.DISMISS));
|
||||
break;
|
||||
case REMOVE_FROM_GROUP:
|
||||
repository.removeFromGroup(card, new OnRemoveFromGroupListener());
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported action: " + action);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<ReviewCard> transformReviewRecipients(@NonNull List<ReviewRecipient> reviewRecipients) {
|
||||
return Stream.of(reviewRecipients)
|
||||
.map(r -> new ReviewCard(r,
|
||||
repository.loadGroupsInCommonCount(r) - (isGroupThread ? 1 : 0),
|
||||
getCardType(r),
|
||||
getPrimaryAction(r),
|
||||
getSecondaryAction(r)))
|
||||
.toList();
|
||||
|
||||
}
|
||||
|
||||
private @NonNull ReviewCard.CardType getCardType(@NonNull ReviewRecipient reviewRecipient) {
|
||||
if (reviewRecipient.getRecipient().isSystemContact()) {
|
||||
return ReviewCard.CardType.YOUR_CONTACT;
|
||||
} else if (isGroupThread) {
|
||||
return ReviewCard.CardType.MEMBER;
|
||||
} else {
|
||||
return ReviewCard.CardType.REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull ReviewCard.Action getPrimaryAction(@NonNull ReviewRecipient reviewRecipient) {
|
||||
if (reviewRecipient.getRecipient().isSystemContact()) {
|
||||
return ReviewCard.Action.UPDATE_CONTACT;
|
||||
} else if (isGroupThread) {
|
||||
return ReviewCard.Action.REMOVE_FROM_GROUP;
|
||||
} else {
|
||||
return ReviewCard.Action.BLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable ReviewCard.Action getSecondaryAction(@NonNull ReviewRecipient reviewRecipient) {
|
||||
if (reviewRecipient.getRecipient().isSystemContact()) {
|
||||
return null;
|
||||
} else if (isGroupThread) {
|
||||
return null;
|
||||
} else {
|
||||
return ReviewCard.Action.DELETE;
|
||||
}
|
||||
}
|
||||
|
||||
private class OnRecipientsLoadedListener implements ReviewCardRepository.OnRecipientsLoadedListener {
|
||||
@Override
|
||||
public void onRecipientsLoaded(@NonNull List<ReviewRecipient> recipients) {
|
||||
if (recipients.size() < 2) {
|
||||
reviewEvents.postValue(Event.DISMISS);
|
||||
} else {
|
||||
reviewRecipients.postValue(recipients);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecipientsLoadFailed() {
|
||||
reviewEvents.postValue(Event.DISMISS);
|
||||
}
|
||||
}
|
||||
|
||||
private class OnRemoveFromGroupListener implements ReviewCardRepository.OnRemoveFromGroupListener {
|
||||
@Override
|
||||
public void onActionCompleted() {
|
||||
repository.loadRecipients(new OnRecipientsLoadedListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionFailed() {
|
||||
reviewEvents.postValue(Event.REMOVE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final ReviewCardRepository repository;
|
||||
private final boolean isGroupThread;
|
||||
|
||||
public Factory(@NonNull ReviewCardRepository repository, boolean isGroupThread) {
|
||||
this.repository = repository;
|
||||
this.isGroupThread = isGroupThread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return Objects.requireNonNull(modelClass.cast(new ReviewCardViewModel(repository, isGroupThread)));
|
||||
}
|
||||
}
|
||||
|
||||
public enum Event {
|
||||
DISMISS,
|
||||
REMOVE_FAILED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
public class ReviewRecipient {
|
||||
private final Recipient recipient;
|
||||
private final ProfileChangeDetails profileChangeDetails;
|
||||
|
||||
ReviewRecipient(@NonNull Recipient recipient) {
|
||||
this(recipient, null);
|
||||
}
|
||||
|
||||
ReviewRecipient(@NonNull Recipient recipient, @Nullable ProfileChangeDetails profileChangeDetails) {
|
||||
this.recipient = recipient;
|
||||
this.profileChangeDetails = profileChangeDetails;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @Nullable ProfileChangeDetails getProfileChangeDetails() {
|
||||
return profileChangeDetails;
|
||||
}
|
||||
|
||||
public static class Comparator implements java.util.Comparator<ReviewRecipient> {
|
||||
|
||||
private final Context context;
|
||||
private final RecipientId alwaysFirstId;
|
||||
|
||||
public Comparator(@NonNull Context context, @Nullable RecipientId alwaysFirstId) {
|
||||
this.context = context;
|
||||
this.alwaysFirstId = alwaysFirstId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(ReviewRecipient recipient1, ReviewRecipient recipient2) {
|
||||
int weight1 = recipient1.getRecipient().getId().equals(alwaysFirstId) ? -100 : 0;
|
||||
int weight2 = recipient2.getRecipient().getId().equals(alwaysFirstId) ? -100 : 0;
|
||||
|
||||
if (recipient1.getProfileChangeDetails() != null && recipient1.getProfileChangeDetails().hasProfileNameChange()) {
|
||||
weight1--;
|
||||
}
|
||||
|
||||
if (recipient2.getProfileChangeDetails() != null && recipient2.getProfileChangeDetails().hasProfileNameChange()) {
|
||||
weight2--;
|
||||
}
|
||||
|
||||
if (recipient1.getRecipient().isSystemContact()) {
|
||||
weight1++;
|
||||
}
|
||||
|
||||
if (recipient2.getRecipient().isSystemContact()) {
|
||||
weight1++;
|
||||
}
|
||||
|
||||
if (weight1 == weight2) {
|
||||
return recipient1.getRecipient()
|
||||
.getDisplayName(context)
|
||||
.compareTo(recipient2.getRecipient()
|
||||
.getDisplayName(context));
|
||||
} else {
|
||||
return Integer.compare(weight1, weight2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.thoughtcrime.securesms.profiles.spoofing;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ReviewUtil {
|
||||
|
||||
private static final long TIMEOUT = TimeUnit.HOURS.toMillis(24);
|
||||
|
||||
/**
|
||||
* Checks a single recipient against the database to see whether duplicates exist.
|
||||
* This should not be used in the context of a group, due to performance reasons.
|
||||
*
|
||||
* @param recipientId Id of the recipient we are interested in.
|
||||
* @return Whether or not multiple recipients share this profile name.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static boolean isRecipientReviewSuggested(@NonNull RecipientId recipientId)
|
||||
{
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (recipient.isGroup() || recipient.isSystemContact()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DatabaseFactory.getRecipientDatabase(ApplicationDependencies.getApplication())
|
||||
.getSimilarRecipientIds(recipient)
|
||||
.size() > 1;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull List<ReviewRecipient> getDuplicatedRecipients(@NonNull GroupId.V2 groupId)
|
||||
{
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
List<MessageRecord> profileChangeRecords = getProfileChangeRecordsForGroup(context, groupId);
|
||||
|
||||
if (profileChangeRecords.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Recipient> members = DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_INCLUDING_SELF);
|
||||
|
||||
List<ReviewRecipient> changed = Stream.of(profileChangeRecords)
|
||||
.distinctBy(record -> record.getRecipient().getId())
|
||||
.map(record -> new ReviewRecipient(record.getRecipient().resolve(), getProfileChangeDetails(record)))
|
||||
.filter(recipient -> !recipient.getRecipient().isSystemContact())
|
||||
.toList();
|
||||
|
||||
List<ReviewRecipient> results = new LinkedList<>();
|
||||
|
||||
for (ReviewRecipient recipient : changed) {
|
||||
if (results.contains(recipient)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
members.remove(recipient.getRecipient());
|
||||
|
||||
for (Recipient member : members) {
|
||||
if (Objects.equals(member.getDisplayName(context), recipient.getRecipient().getDisplayName(context))) {
|
||||
results.add(recipient);
|
||||
results.add(new ReviewRecipient(member));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull List<MessageRecord> getProfileChangeRecordsForGroup(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getByGroupId(groupId).get();
|
||||
long threadId = Objects.requireNonNull(DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId));
|
||||
|
||||
return DatabaseFactory.getSmsDatabase(context).getProfileChangeDetailsRecords(threadId, System.currentTimeMillis() - TIMEOUT);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static int getGroupsInCommonCount(@NonNull Context context, @NonNull RecipientId recipientId) {
|
||||
return Stream.of(DatabaseFactory.getGroupDatabase(context)
|
||||
.getPushGroupsContainingMember(recipientId))
|
||||
.filter(g -> g.getMembers().contains(Recipient.self().getId()))
|
||||
.map(GroupDatabase.GroupRecord::getRecipientId)
|
||||
.toList()
|
||||
.size();
|
||||
}
|
||||
|
||||
private static @NonNull ProfileChangeDetails getProfileChangeDetails(@NonNull MessageRecord messageRecord) {
|
||||
try {
|
||||
return ProfileChangeDetails.parseFrom(Base64.decode(messageRecord.getBody()));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user