Improve messaging and UX around safety number changes.

fork-5.53.8
Cody Henthorne 2020-06-26 11:10:54 -04:00 zatwierdzone przez GitHub
rodzic 819f0f68f6
commit bbe003a454
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
22 zmienionych plików z 713 dodań i 83 usunięć

Wyświetl plik

@ -47,5 +47,6 @@ public interface BindableConversationItem extends Unbindable {
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
void onReactionClicked(long messageId, boolean isMms);
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
}
}

Wyświetl plik

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.database.Cursor;
import android.os.AsyncTask;
import android.text.SpannableString;
import android.text.Spanned;
@ -15,7 +14,6 @@ import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
@ -105,7 +103,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
}
processMessageRecord(messageRecord);
processPendingMessageRecords(messageRecord.getThreadId(), mismatch);
return null;
}
@ -115,26 +112,6 @@ public class ConfirmIdentityDialog extends AlertDialog {
else processIncomingMessageRecord(messageRecord);
}
private void processPendingMessageRecords(long threadId, IdentityKeyMismatch mismatch) {
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(getContext());
Cursor cursor = mmsSmsDatabase.getIdentityConflictMessagesForThread(threadId);
MmsSmsDatabase.Reader reader = mmsSmsDatabase.readerFor(cursor);
MessageRecord record;
try {
while ((record = reader.getNext()) != null) {
for (IdentityKeyMismatch recordMismatch : record.getIdentityKeyMismatches()) {
if (mismatch.equals(recordMismatch)) {
processMessageRecord(record);
}
}
}
} finally {
if (reader != null)
reader.close();
}
}
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());

Wyświetl plik

@ -6,14 +6,15 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -94,9 +95,17 @@ public class ConversationItemFooter extends LinearLayout {
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
dateView.setText(R.string.ConversationItem_error_not_delivered);
int errorMsg;
if (messageRecord.hasFailedWithNetworkFailures()) {
errorMsg = R.string.ConversationItem_error_network_not_delivered;
} else if (messageRecord.getRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) {
errorMsg = R.string.ConversationItem_error_partially_not_delivered;
} else {
errorMsg = R.string.ConversationItem_error_not_sent_tap_for_details;
}
dateView.setText(errorMsg);
} else if (messageRecord.isPendingInsecureSmsFallback()) {
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
} else {

Wyświetl plik

@ -108,9 +108,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.components.identity.UntrustedSendDialog;
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder;
@ -125,6 +123,7 @@ import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -173,6 +172,7 @@ import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
import org.thoughtcrime.securesms.mms.AttachmentManager;
@ -273,13 +273,16 @@ public class ConversationActivity extends PassphraseRequiredActivity
StickerKeyboardProvider.StickerEventListener,
AttachmentKeyboard.Callback,
ConversationReactionOverlay.OnReactionSelectedListener,
ReactWithAnyEmojiBottomSheetDialogFragment.Callback
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
SafetyNumberChangeDialog.Callback
{
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
private static final String TAG = ConversationActivity.class.getSimpleName();
public static final String SAFETY_NUMBER_DIALOG = "SAFETY_NUMBER";
public static final String RECIPIENT_EXTRA = "recipient_id";
public static final String THREAD_ID_EXTRA = "thread_id";
public static final String TEXT_EXTRA = "draft_text";
@ -1306,50 +1309,28 @@ public class ConversationActivity extends PassphraseRequiredActivity
startActivity(intent);
}
private void handleUnverifiedRecipients() {
List<Recipient> unverifiedRecipients = identityRecords.getUnverifiedRecipients();
List<IdentityRecord> unverifiedRecords = identityRecords.getUnverifiedRecords();
String message = IdentityUtil.getUnverifiedSendDialogDescription(this, unverifiedRecipients);
if (message == null) return;
//noinspection CodeBlock2Expr
new UnverifiedSendDialog(this, message, unverifiedRecords, () -> {
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
sendMessage();
}
@Override
public void onFailure(ExecutionException e) {
throw new AssertionError(e);
}
});
}).show();
private void handleRecentSafetyNumberChange() {
List<IdentityRecord> records = identityRecords.getUnverifiedRecords();
records.addAll(identityRecords.getUntrustedRecords());
SafetyNumberChangeDialog.create(records).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
}
private void handleUntrustedRecipients() {
List<Recipient> untrustedRecipients = identityRecords.getUntrustedRecipients();
List<IdentityRecord> untrustedRecords = identityRecords.getUntrustedRecords();
String untrustedMessage = IdentityUtil.getUntrustedSendDialogDescription(this, untrustedRecipients);
@Override
public void onSendAnywayAfterSafetyNumberChange() {
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
sendMessage();
}
});
}
if (untrustedMessage == null) return;
//noinspection CodeBlock2Expr
new UntrustedSendDialog(this, untrustedMessage, untrustedRecords, () -> {
initializeIdentityRecords().addListener(new ListenableFuture.Listener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
sendMessage();
}
@Override
public void onFailure(ExecutionException e) {
throw new AssertionError(e);
}
});
}).show();
@Override
public void onMessageResentAfterSafetyNumberChange() {
initializeIdentityRecords().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) { }
});
}
private void handleSecurityChange(boolean isSecureText, boolean isDefaultSms) {
@ -2329,10 +2310,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !isMmsEnabled) {
handleManualMmsRequired();
} else if (!forceSms && identityRecords.isUnverified()) {
handleUnverifiedRecipients();
} else if (!forceSms && identityRecords.isUntrusted()) {
handleUntrustedRecipients();
} else if (!forceSms && (identityRecords.isUnverified() || identityRecords.isUntrusted())) {
handleRecentSafetyNumberChange();
} else if (isMediaMessage) {
sendMediaMessage(forceSms, expiresIn, false, subscriptionId, initiating);
} else {
@ -2886,6 +2865,21 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactionOverlay.setListVerticalTranslation(translationY);
}
@Override
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
if (messageRecord.hasFailedWithNetworkFailures()) {
new AlertDialog.Builder(this)
.setMessage(R.string.conversation_activity__message_could_not_be_sent)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.conversation_activity__send, (dialog, which) -> MessageSender.resend(this, messageRecord))
.show();
} else if (messageRecord.isIdentityMismatchFailure()) {
SafetyNumberChangeDialog.create(this, messageRecord).show(getSupportFragmentManager(), SAFETY_NUMBER_DIALOG);
} else {
startActivity(MessageDetailsActivity.getIntentForMessageDetails(this, messageRecord, messageRecord.getRecipient().getId(), messageRecord.getThreadId()));
}
}
@Override
public void onCursorChanged() {
if (!reactionOverlay.isShowing()) {

Wyświetl plik

@ -989,6 +989,7 @@ public class ConversationFragment extends LoggingFragment {
@NonNull ConversationReactionOverlay.OnHideListener onHideListener);
void onCursorChanged();
void onListVerticalTranslationChanged(float translationY);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
}
private class ConversationScrollListener extends OnScrollListener {
@ -1249,6 +1250,11 @@ public class ConversationFragment extends LoggingFragment {
RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(requireFragmentManager(), "BOTTOM");
}
@Override
public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
listener.onMessageWithErrorClicked(messageRecord);
}
}
@Override

Wyświetl plik

@ -90,7 +90,6 @@ import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
@ -1375,7 +1374,9 @@ public class ConversationItem extends LinearLayout implements BindableConversati
if (!shouldInterceptClicks(messageRecord) && parent != null) {
parent.onClick(v);
} else if (messageRecord.isFailed()) {
context.startActivity(MessageDetailsActivity.getIntentForMessageDetails(context, messageRecord, conversationRecipient.getId(), messageRecord.getThreadId()));
if (eventListener != null) {
eventListener.onMessageWithErrorClicked(messageRecord);
}
} else if (!messageRecord.isOutgoing() && messageRecord.isIdentityMismatchFailure()) {
handleApproveIdentity();
} else if (messageRecord.isPendingInsecureSmsFallback()) {

Wyświetl plik

@ -0,0 +1,38 @@
package org.thoughtcrime.securesms.conversation.ui.error;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
/**
* Wrapper class for helping show a list of recipients that had recent safety number changes.
*
* Also provides helper methods for behavior used in multiple spots.
*/
final class ChangedRecipient {
private final Recipient recipient;
private final IdentityRecord record;
ChangedRecipient(@NonNull Recipient recipient, @NonNull IdentityRecord record) {
this.recipient = recipient;
this.record = record;
}
@NonNull Recipient getRecipient() {
return recipient;
}
@NonNull IdentityRecord getIdentityRecord() {
return record;
}
boolean isUnverified() {
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.UNVERIFIED;
}
boolean isVerified() {
return record.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED;
}
}

Wyświetl plik

@ -0,0 +1,85 @@
package org.thoughtcrime.securesms.conversation.ui.error;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
final class SafetyNumberChangeAdapter extends ListAdapter<ChangedRecipient, SafetyNumberChangeAdapter.ViewHolder> {
private final Callbacks callbacks;
SafetyNumberChangeAdapter(@NonNull Callbacks callbacks) {
super(new AlwaysChangedDiffUtil<>());
this.callbacks = callbacks;
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.safety_number_change_recipient, parent, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
final ChangedRecipient changedRecipient = getItem(position);
holder.bind(changedRecipient);
}
class ViewHolder extends RecyclerView.ViewHolder {
final AvatarImageView avatar;
final FromTextView name;
final TextView subtitle;
final View viewButton;
public ViewHolder(@NonNull View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.safety_number_change_recipient_avatar);
name = itemView.findViewById(R.id.safety_number_change_recipient_name);
subtitle = itemView.findViewById(R.id.safety_number_change_recipient_subtitle);
viewButton = itemView.findViewById(R.id.safety_number_change_recipient_view);
}
void bind(@NonNull ChangedRecipient changedRecipient) {
avatar.setRecipient(changedRecipient.getRecipient());
name.setText(changedRecipient.getRecipient());
if (changedRecipient.isUnverified() || changedRecipient.isVerified()) {
subtitle.setText(R.string.safety_number_change_dialog__previous_verified);
Drawable check = ContextCompat.getDrawable(itemView.getContext(), R.drawable.check);
if (check != null) {
check.setBounds(0, 0, ViewUtil.dpToPx(12), ViewUtil.dpToPx(12));
subtitle.setCompoundDrawables(check, null, null, null);
}
} else if (changedRecipient.getRecipient().hasAUserSetDisplayName(itemView.getContext())) {
subtitle.setText(changedRecipient.getRecipient().getE164().or(""));
subtitle.setCompoundDrawables(null, null, null, null);
} else {
subtitle.setText("");
}
subtitle.setVisibility(TextUtils.isEmpty(subtitle.getText()) ? View.GONE : View.VISIBLE);
viewButton.setOnClickListener(view -> callbacks.onViewIdentityRecord(changedRecipient.getIdentityRecord()));
}
}
interface Callbacks {
void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord);
}
}

Wyświetl plik

@ -0,0 +1,157 @@
package org.thoughtcrime.securesms.conversation.ui.error;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class SafetyNumberChangeDialog extends DialogFragment implements SafetyNumberChangeAdapter.Callbacks {
private static final String RECIPIENT_IDS_EXTRA = "recipient_ids";
private static final String MESSAGE_ID_EXTRA = "message_id";
private SafetyNumberChangeViewModel viewModel;
private SafetyNumberChangeAdapter adapter;
private View dialogView;
public static @NonNull SafetyNumberChangeDialog create(List<IdentityDatabase.IdentityRecord> identityRecords) {
List<String> ids = Stream.of(identityRecords)
.map(record -> record.getRecipientId().serialize())
.distinct()
.toList();
Bundle arguments = new Bundle();
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
fragment.setArguments(arguments);
return fragment;
}
public static @NonNull SafetyNumberChangeDialog create(Context context, MessageRecord messageRecord) {
List<String> ids = Stream.of(messageRecord.getIdentityKeyMismatches())
.map(mismatch -> mismatch.getRecipientId(context).serialize())
.distinct()
.toList();
Bundle arguments = new Bundle();
arguments.putStringArray(RECIPIENT_IDS_EXTRA, ids.toArray(new String[0]));
arguments.putLong(MESSAGE_ID_EXTRA, messageRecord.getId());
SafetyNumberChangeDialog fragment = new SafetyNumberChangeDialog();
fragment.setArguments(arguments);
return fragment;
}
private SafetyNumberChangeDialog() { }
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return dialogView;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
List<RecipientId> recipientIds = Stream.of(getArguments().getStringArray(RECIPIENT_IDS_EXTRA)).map(RecipientId::from).toList();
long messageId = getArguments().getLong(MESSAGE_ID_EXTRA, -1);
viewModel = ViewModelProviders.of(this, new SafetyNumberChangeViewModel.Factory(recipientIds, (messageId != -1) ? messageId : null)).get(SafetyNumberChangeViewModel.class);
viewModel.getChangedRecipients().observe(getViewLifecycleOwner(), adapter::submitList);
}
@Override
public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
dialogView = LayoutInflater.from(requireActivity()).inflate(R.layout.safety_number_change_dialog, null);
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), getTheme());
configureView(dialogView);
builder.setTitle(R.string.safety_number_change_dialog__safety_number_changes)
.setView(dialogView)
.setPositiveButton(R.string.safety_number_change_dialog__send_anyway, this::handleSendAnyway)
.setNegativeButton(android.R.string.cancel, null);
return builder.create();
}
@Override public void onDestroyView() {
dialogView = null;
super.onDestroyView();
}
private void configureView(View view) {
RecyclerView list = view.findViewById(R.id.safety_number_change_dialog_list);
adapter = new SafetyNumberChangeAdapter(this);
list.setAdapter(adapter);
list.setItemAnimator(null);
list.setLayoutManager(new LinearLayoutManager(requireContext()));
}
private void handleSendAnyway(DialogInterface dialogInterface, int which) {
Activity activity = getActivity();
Callback callback;
if (activity instanceof Callback) {
callback = (Callback) activity;
} else {
callback = null;
}
LiveData<TrustAndVerifyResult> trustOrVerifyResultLiveData = viewModel.trustOrVerifyChangedRecipients();
Observer<TrustAndVerifyResult> observer = new Observer<TrustAndVerifyResult>() {
@Override
public void onChanged(TrustAndVerifyResult result) {
if (callback != null) {
switch (result) {
case TRUST_AND_VERIFY:
callback.onSendAnywayAfterSafetyNumberChange();
break;
case TRUST_VERIFY_AND_RESEND:
callback.onMessageResentAfterSafetyNumberChange();
break;
}
}
trustOrVerifyResultLiveData.removeObserver(this);
}
};
trustOrVerifyResultLiveData.observeForever(observer);
}
@Override
public void onViewIdentityRecord(@NonNull IdentityDatabase.IdentityRecord identityRecord) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
}
public interface Callback {
void onSendAnywayAfterSafetyNumberChange();
void onMessageResentAfterSafetyNumberChange();
}
}

Wyświetl plik

@ -0,0 +1,155 @@
package org.thoughtcrime.securesms.conversation.ui.error;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.SignalProtocolAddress;
import java.util.List;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
final class SafetyNumberChangeRepository {
private final Context context;
SafetyNumberChangeRepository(Context context) {
this.context = context.getApplicationContext();
}
@NonNull LiveData<SafetyNumberChangeState> getSafetyNumberChangeState(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
MutableLiveData<SafetyNumberChangeState> liveData = new MutableLiveData<>();
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(getSafetyNumberChangeStateInternal(recipientIds, messageId)));
return liveData;
}
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients(@NonNull List<ChangedRecipient> changedRecipients) {
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsInternal(changedRecipients)));
return liveData;
}
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipientsAndResend(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
MutableLiveData<TrustAndVerifyResult> liveData = new MutableLiveData<>();
SignalExecutors.BOUNDED.execute(() -> liveData.postValue(trustOrVerifyChangedRecipientsAndResendInternal(changedRecipients, messageRecord)));
return liveData;
}
@WorkerThread
private @NonNull SafetyNumberChangeState getSafetyNumberChangeStateInternal(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
MessageRecord messageRecord = null;
if (messageId != null) {
messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageRecord(messageId);
}
List<Recipient> recipients = Stream.of(recipientIds).map(Recipient::resolved).toList();
List<ChangedRecipient> changedRecipients = Stream.of(DatabaseFactory.getIdentityDatabase(context).getIdentities(recipients).getIdentityRecords())
.map(record -> new ChangedRecipient(Recipient.resolved(record.getRecipientId()), record))
.toList();
return new SafetyNumberChangeState(changedRecipients, messageRecord);
}
@WorkerThread
private TrustAndVerifyResult trustOrVerifyChangedRecipientsInternal(@NonNull List<ChangedRecipient> changedRecipients) {
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
synchronized (SESSION_LOCK) {
for (ChangedRecipient changedRecipient : changedRecipients) {
IdentityRecord identityRecord = changedRecipient.getIdentityRecord();
if (changedRecipient.isUnverified()) {
identityDatabase.setVerified(identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
IdentityDatabase.VerifiedStatus.DEFAULT);
} else {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
}
}
}
return TrustAndVerifyResult.TRUST_AND_VERIFY;
}
@WorkerThread
private TrustAndVerifyResult trustOrVerifyChangedRecipientsAndResendInternal(@NonNull List<ChangedRecipient> changedRecipients,
@NonNull MessageRecord messageRecord) {
synchronized (SESSION_LOCK) {
for (ChangedRecipient changedRecipient : changedRecipients) {
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(changedRecipient.getRecipient().requireServiceId(), 1);
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context);
identityKeyStore.saveIdentity(mismatchAddress, changedRecipient.getIdentityRecord().getIdentityKey(), true);
}
}
if (messageRecord.isOutgoing()) {
processOutgoingMessageRecord(changedRecipients, messageRecord);
}
return TrustAndVerifyResult.TRUST_VERIFY_AND_RESEND;
}
@WorkerThread
private void processOutgoingMessageRecord(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
for (ChangedRecipient changedRecipient : changedRecipients) {
RecipientId id = changedRecipient.getRecipient().getId();
IdentityKey identityKey = changedRecipient.getIdentityRecord().getIdentityKey();
if (messageRecord.isMms()) {
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
if (messageRecord.getRecipient().isPushGroup()) {
MessageSender.resendGroupMessage(context, messageRecord, id);
} else {
MessageSender.resend(context, messageRecord);
}
} else {
smsDatabase.removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
MessageSender.resend(context, messageRecord);
}
}
}
static final class SafetyNumberChangeState {
private final List<ChangedRecipient> changedRecipients;
private final MessageRecord messageRecord;
SafetyNumberChangeState(List<ChangedRecipient> changedRecipients, @Nullable MessageRecord messageRecord) {
this.changedRecipients = changedRecipients;
this.messageRecord = messageRecord;
}
@NonNull List<ChangedRecipient> getChangedRecipients() {
return changedRecipients;
}
@Nullable MessageRecord getMessageRecord() {
return messageRecord;
}
}
}

Wyświetl plik

@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.conversation.ui.error;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeRepository.SafetyNumberChangeState;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.List;
import java.util.Objects;
public final class SafetyNumberChangeViewModel extends ViewModel {
private final SafetyNumberChangeRepository safetyNumberChangeRepository;
private final LiveData<SafetyNumberChangeState> safetyNumberChangeState;
private SafetyNumberChangeViewModel(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId, SafetyNumberChangeRepository safetyNumberChangeRepository) {
this.safetyNumberChangeRepository = safetyNumberChangeRepository;
safetyNumberChangeState = this.safetyNumberChangeRepository.getSafetyNumberChangeState(recipientIds, messageId);
}
@NonNull LiveData<List<ChangedRecipient>> getChangedRecipients() {
return Transformations.map(safetyNumberChangeState, SafetyNumberChangeState::getChangedRecipients);
}
@NonNull LiveData<TrustAndVerifyResult> trustOrVerifyChangedRecipients() {
SafetyNumberChangeState state = Objects.requireNonNull(safetyNumberChangeState.getValue());
if (state.getMessageRecord() != null) {
return safetyNumberChangeRepository.trustOrVerifyChangedRecipientsAndResend(state.getChangedRecipients(), state.getMessageRecord());
} else {
return safetyNumberChangeRepository.trustOrVerifyChangedRecipients(state.getChangedRecipients());
}
}
public static final class Factory implements ViewModelProvider.Factory {
private final List<RecipientId> recipientIds;
private final Long messageId;
public Factory(@NonNull List<RecipientId> recipientIds, @Nullable Long messageId) {
this.recipientIds = recipientIds;
this.messageId = messageId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
SafetyNumberChangeRepository repo = new SafetyNumberChangeRepository(ApplicationDependencies.getApplication());
return Objects.requireNonNull(modelClass.cast(new SafetyNumberChangeViewModel(recipientIds, messageId, repo)));
}
}
}

Wyświetl plik

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.conversation.ui.error;
public enum TrustAndVerifyResult {
TRUST_AND_VERIFY,
TRUST_VERIFY_AND_RESEND,
UNKNOWN
}

Wyświetl plik

@ -280,6 +280,18 @@ public class MmsSmsDatabase extends Database {
else return id;
}
public @Nullable MessageRecord getMessageRecord(long messageId) {
try {
return DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
} catch (NoSuchMessageException e1) {
try {
return DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
} catch (NoSuchMessageException e2) {
return null;
}
}
}
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true);
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true);

Wyświetl plik

@ -1,12 +1,15 @@
package org.thoughtcrime.securesms.database.identity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -24,6 +27,10 @@ public final class IdentityRecordList {
identityRecords.addAll(identityRecordList.identityRecords);
}
public List<IdentityRecord> getIdentityRecords() {
return Collections.unmodifiableList(identityRecords);
}
public boolean isVerified() {
for (IdentityRecord identityRecord : identityRecords) {
if (identityRecord.getVerifiedStatus() != VerifiedStatus.VERIFIED) {

Wyświetl plik

@ -285,6 +285,10 @@ public abstract class MessageRecord extends DisplayRecord {
return networkFailures != null && !networkFailures.isEmpty();
}
public boolean hasFailedWithNetworkFailures() {
return isFailed() && ((getRecipient().isPushGroup() && hasNetworkFailures()) || !isIdentityMismatchFailure());
}
protected SpannableString emphasisAdded(String sequence) {
SpannableString spannable = new SpannableString(sequence);
spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

Wyświetl plik

@ -84,11 +84,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
}
private void bindErrorState(MessageRecord messageRecord) {
boolean isPushGroup = messageRecord.getRecipient().isPushGroup();
boolean isGroupNetworkFailure = messageRecord.isFailed() && !messageRecord.getNetworkFailures().isEmpty();
boolean isIndividualNetworkFailure = messageRecord.isFailed() && !isPushGroup && messageRecord.getIdentityKeyMismatches().isEmpty();
if (isGroupNetworkFailure || isIndividualNetworkFailure) {
if (messageRecord.hasFailedWithNetworkFailures()) {
errorText.setVisibility(View.VISIBLE);
resendButton.setVisibility(View.VISIBLE);
resendButton.setOnClickListener(unused -> {

Wyświetl plik

@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.util.adapter;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
public final class AlwaysChangedDiffUtil<T> extends DiffUtil.ItemCallback<T> {
@Override
public boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem) {
return false;
}
@Override
public boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem) {
return false;
}
}

Wyświetl plik

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
tools:background="?dialog_background_color">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/safety_number_change_dialog__the_following_people_may_have_reinstalled_or_changed_devices" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/safety_number_change_dialog_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
tools:listitem="@layout/safety_number_change_recipient" />
</LinearLayout>

Wyświetl plik

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:background="?dialog_background_color">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/safety_number_change_recipient_avatar"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginEnd="12dp"
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo"
android:cropToPadding="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/safety_number_change_recipient_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_contact_picture" />
<org.thoughtcrime.securesms.components.FromTextView
android:id="@+id/safety_number_change_recipient_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="marquee"
android:singleLine="true"
android:textColor="?title_text_color_primary"
app:layout_constraintBottom_toTopOf="@+id/safety_number_change_recipient_subtitle"
app:layout_constraintEnd_toStartOf="@+id/safety_number_change_recipient_view"
app:layout_constraintStart_toEndOf="@+id/safety_number_change_recipient_avatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="Jules Bonnot" />
<TextView
android:id="@+id/safety_number_change_recipient_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="marquee"
android:textColor="?title_text_color_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/safety_number_change_recipient_view"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/safety_number_change_recipient_name"
app:layout_constraintTop_toBottomOf="@+id/safety_number_change_recipient_name"
tools:text="+1 817-647-3790" />
<com.google.android.material.button.MaterialButton
android:id="@+id/safety_number_change_recipient_view"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="?attr/safety_number_change_dialog_button_background"
android:elevation="0dp"
android:minWidth="0dp"
android:text="@string/safety_number_change_dialog__view"
android:textColor="?safety_number_change_dialog_button_text_color"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/safety_number_change_recipient_name"
app:layout_constraintTop_toTopOf="parent"
tools:targetApi="lollipop" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -141,6 +141,9 @@
<attr name="conversation_scroll_to_bottom_background" format="reference" />
<attr name="conversation_scroll_to_bottom_foreground_color" format="color" />
<attr name="safety_number_change_dialog_button_background" format="reference|color" />
<attr name="safety_number_change_dialog_button_text_color" format="color" />
<attr name="tinted_circle_background" format="reference" />
<attr name="dialog_info_icon" format="reference" />

Wyświetl plik

@ -187,7 +187,9 @@
<string name="ContactShareEditActivity_invalid_contact">Selected contact was invalid</string>
<!-- ConversationItem -->
<string name="ConversationItem_error_not_delivered">Send failed, tap for details</string>
<string name="ConversationItem_error_not_sent_tap_for_details">Not sent, tap for details</string>
<string name="ConversationItem_error_partially_not_delivered">Partially sent, tap for details</string>
<string name="ConversationItem_error_network_not_delivered">Send failed</string>
<string name="ConversationItem_received_key_exchange_message_tap_to_process">Received key exchange message, tap to process.</string>
<string name="ConversationItem_group_action_left">%1$s has left the group.</string>
<string name="ConversationItem_click_to_approve_unencrypted">Send failed, tap for unsecured fallback</string>
@ -1444,6 +1446,7 @@
<string name="conversation_activity__quick_attachment_drawer_record_and_send_audio_description">Record and send audio attachment</string>
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Lock recording of audio attachment</string>
<string name="conversation_activity__enable_signal_for_sms">Enable Signal for SMS</string>
<string name="conversation_activity__message_could_not_be_sent">Message could not be sent. Check your connection and try again.</string>
<!-- conversation_input_panel -->
<string name="conversation_input_panel__slide_to_cancel">Slide to cancel</string>
@ -1479,6 +1482,13 @@
<!-- conversation_fragment -->
<string name="conversation_fragment__scroll_to_the_bottom_content_description">Scroll to the bottom</string>
<!-- safety_number_change_dialog -->
<string name="safety_number_change_dialog__safety_number_changes">Safety Number Changes</string>
<string name="safety_number_change_dialog__send_anyway">Send anyway</string>
<string name="safety_number_change_dialog__the_following_people_may_have_reinstalled_or_changed_devices">The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy.</string>
<string name="safety_number_change_dialog__view">View</string>
<string name="safety_number_change_dialog__previous_verified">Previous verified</string>
<!-- country_selection_fragment -->
<string name="country_selection_fragment__loading_countries">Loading countries…</string>
<string name="country_selection_fragment__search">Search</string>

Wyświetl plik

@ -258,6 +258,9 @@
<item name="conversation_title_color">@color/white</item>
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
<item name="safety_number_change_dialog_button_background">@color/core_grey_05</item>
<item name="safety_number_change_dialog_button_text_color">@color/core_ultramarine</item>
<item name="tinted_circle_background">@drawable/tinted_circle_light</item>
<item name="contact_list_divider">@drawable/contact_list_divider_light</item>
@ -546,6 +549,9 @@
<item name="conversation_item_image_outline_color">@color/transparent_white_20</item>
<item name="conversation_item_reveal_viewed_background_color">?conversation_background</item>
<item name="safety_number_change_dialog_button_background">@color/core_grey_75</item>
<item name="safety_number_change_dialog_button_text_color">@color/core_grey_05</item>
<item name="contact_list_divider">@drawable/contact_list_divider_dark</item>
<item name="debuglog_color_none">@color/debuglog_dark_none</item>