Add support for displaying both ACI and e164 safety numbers.

This commit is contained in:
Clark
2023-07-19 10:17:45 -04:00
committed by Nicholas
parent 00bbb6bc6e
commit 461875b0e4
29 changed files with 1633 additions and 848 deletions

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.verify
import android.animation.Animator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import com.airbnb.lottie.LottieDrawable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.SafetyNumberPnpEducationBottomSheetBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.visible
class PnpSafetyNumberEducationDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.66f
private val binding by ViewBinderDelegate(SafetyNumberPnpEducationBottomSheetBinding::bind)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.safety_number_pnp_education_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lottie.visible = true
binding.lottie.playAnimation()
binding.lottie.addAnimatorListener(object : AnimationCompleteListener() {
override fun onAnimationEnd(animation: Animator) {
binding.lottie.removeAnimatorListener(this)
binding.lottie.setMinAndMaxFrame(60, 360)
binding.lottie.repeatMode = LottieDrawable.RESTART
binding.lottie.repeatCount = LottieDrawable.INFINITE
binding.lottie.frame = 60
binding.lottie.playAnimation()
}
})
binding.okay.setOnClickListener {
SignalStore.uiHints().markHasSeenSafetyNumberUpdateNux()
dismiss()
}
binding.help.setOnClickListener {
CommunicationActions.openBrowserLink(requireContext(), "https://support.signal.org/hc/en-us/articles/360007060632")
}
}
companion object {
@JvmStatic
fun showIfNeeded(fragmentManager: FragmentManager) {
if (SignalStore.uiHints().hasSeenSafetyNumberUpdateNux()) {
return
}
val fragment = PnpSafetyNumberEducationDialogFragment()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}

View File

@@ -1,593 +0,0 @@
package org.thoughtcrime.securesms.verify;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.TextSwitcher;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.fingerprint.Fingerprint;
import org.signal.libsignal.protocol.fingerprint.FingerprintVersionMismatchException;
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.database.IdentityTable;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.qr.QrCodeUtil;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.signal.core.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.api.SignalSessionLock;
import java.nio.charset.Charset;
import java.util.Locale;
/**
* Fragment to display a user's identity key.
*/
public class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.OnScrollChangedListener {
private static final String TAG = Log.tag(VerifyDisplayFragment.class);
private static final String RECIPIENT_ID = "recipient_id";
private static final String REMOTE_IDENTITY = "remote_identity";
private static final String LOCAL_IDENTITY = "local_identity";
private static final String LOCAL_NUMBER = "local_number";
private static final String VERIFIED_STATE = "verified_state";
private LiveRecipient recipient;
private IdentityKey localIdentity;
private IdentityKey remoteIdentity;
private Fingerprint fingerprint;
private Toolbar toolbar;
private ScrollView scrollView;
private View numbersContainer;
private View loading;
private View qrCodeContainer;
private ImageView qrCode;
private ImageView qrVerified;
private TextSwitcher tapLabel;
private TextView description;
private Callback callback;
private Button verifyButton;
private View toolbarShadow;
private View bottomShadow;
private TextView[] codes = new TextView[12];
private boolean animateSuccessOnDraw = false;
private boolean animateFailureOnDraw = false;
private boolean currentVerifiedState = false;
static VerifyDisplayFragment create(@NonNull RecipientId recipientId,
@NonNull IdentityKeyParcelable remoteIdentity,
@NonNull IdentityKeyParcelable localIdentity,
@NonNull String localNumber,
boolean verifiedState)
{
Bundle extras = new Bundle();
extras.putParcelable(RECIPIENT_ID, recipientId);
extras.putParcelable(REMOTE_IDENTITY, remoteIdentity);
extras.putParcelable(LOCAL_IDENTITY, localIdentity);
extras.putString(LOCAL_NUMBER, localNumber);
extras.putBoolean(VERIFIED_STATE, verifiedState);
VerifyDisplayFragment fragment = new VerifyDisplayFragment();
fragment.setArguments(extras);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Callback) {
callback = (Callback) context;
} else if (getParentFragment() instanceof Callback) {
callback = (Callback) getParentFragment();
} else {
throw new ClassCastException("Cannot find ScanListener in parent component");
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
return ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
this.toolbar = view.findViewById(R.id.toolbar);
this.scrollView = view.findViewById(R.id.scroll_view);
this.numbersContainer = view.findViewById(R.id.number_table);
this.loading = view.findViewById(R.id.loading);
this.qrCodeContainer = view.findViewById(R.id.qr_code_container);
this.qrCode = view.findViewById(R.id.qr_code);
this.verifyButton = view.findViewById(R.id.verify_button);
this.qrVerified = view.findViewById(R.id.qr_verified);
this.description = view.findViewById(R.id.description);
this.tapLabel = view.findViewById(R.id.tap_label);
this.toolbarShadow = view.findViewById(R.id.toolbar_shadow);
this.bottomShadow = view.findViewById(R.id.verify_identity_bottom_shadow);
this.codes[0] = view.findViewById(R.id.code_first);
this.codes[1] = view.findViewById(R.id.code_second);
this.codes[2] = view.findViewById(R.id.code_third);
this.codes[3] = view.findViewById(R.id.code_fourth);
this.codes[4] = view.findViewById(R.id.code_fifth);
this.codes[5] = view.findViewById(R.id.code_sixth);
this.codes[6] = view.findViewById(R.id.code_seventh);
this.codes[7] = view.findViewById(R.id.code_eighth);
this.codes[8] = view.findViewById(R.id.code_ninth);
this.codes[9] = view.findViewById(R.id.code_tenth);
this.codes[10] = view.findViewById(R.id.code_eleventh);
this.codes[11] = view.findViewById(R.id.code_twelth);
this.qrCodeContainer.setOnClickListener(v -> callback.onQrCodeContainerClicked());
this.registerForContextMenu(numbersContainer);
updateVerifyButton(getArguments().getBoolean(VERIFIED_STATE, false), false);
this.verifyButton.setOnClickListener((button -> updateVerifyButton(!currentVerifiedState, true)));
this.scrollView.getViewTreeObserver().addOnScrollChangedListener(this);
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
toolbar.setOnMenuItemClickListener(this::onToolbarOptionsItemSelected);
toolbar.setTitle(R.string.AndroidManifest__verify_safety_number);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
initializeFingerprint();
}
@Override
public void onDestroyView() {
this.scrollView.getViewTreeObserver().removeOnScrollChangedListener(this);
super.onDestroyView();
}
private void initializeFingerprint() {
RecipientId recipientId = getArguments().getParcelable(RECIPIENT_ID);
IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY);
IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY);
this.localIdentity = localIdentityParcelable.get();
this.recipient = Recipient.live(recipientId);
this.remoteIdentity = remoteIdentityParcelable.get();
int version;
byte[] localId;
byte[] remoteId;
//noinspection WrongThread
Recipient resolved = recipient.resolve();
if (FeatureFlags.verifyV2() && resolved.getServiceId().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = SignalStore.account().requireAci().toByteArray();
remoteId = resolved.requireServiceId().toByteArray();
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
Log.i(TAG, "Using E164 (version 1).");
version = 1;
localId = Recipient.self().requireE164().getBytes();
remoteId = resolved.requireE164().getBytes();
} else {
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getServiceId().isPresent(), resolved.getE164().isPresent()));
new MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
.setOnDismissListener(dialog -> {
requireActivity().finish();
dialog.dismiss();
})
.show();
return;
}
this.recipient.observe(this, this::setRecipientText);
SimpleTask.run(() -> new NumericFingerprintGenerator(5200).createFor(version,
localId, localIdentity,
remoteId, remoteIdentity),
fingerprint -> {
if (getActivity() == null) return;
VerifyDisplayFragment.this.fingerprint = fingerprint;
setFingerprintViews(fingerprint, true);
initializeOptionsMenu();
});
}
@Override
public void onResume() {
super.onResume();
setRecipientText(recipient.get());
if (fingerprint != null) {
setFingerprintViews(fingerprint, false);
}
if (animateSuccessOnDraw) {
animateSuccessOnDraw = false;
animateVerifiedSuccess();
} else if (animateFailureOnDraw) {
animateFailureOnDraw = false;
animateVerifiedFailure();
}
ThreadUtil.postToMain(this::onScrollChanged);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view,
ContextMenu.ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (fingerprint != null) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.verify_display_fragment_context_menu, menu);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (fingerprint == null) return super.onContextItemSelected(item);
if (item.getItemId() == R.id.menu_copy) {
handleCopyToClipboard(fingerprint, codes.length);
return true;
} else if (item.getItemId() == R.id.menu_compare) {
handleCompareWithClipboard(fingerprint);
return true;
} else {
return super.onContextItemSelected(item);
}
}
private void initializeOptionsMenu() {
if (fingerprint != null) {
requireActivity().getMenuInflater().inflate(R.menu.verify_identity, toolbar.getMenu());
}
}
public boolean onToolbarOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.verify_identity__share) {
handleShare(fingerprint, codes.length);
return true;
}
return false;
}
public void setScannedFingerprint(String scanned) {
try {
if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) {
this.animateSuccessOnDraw = true;
} else {
this.animateFailureOnDraw = true;
}
} catch (FingerprintVersionMismatchException e) {
Log.w(TAG, e);
if (e.getOurVersion() < e.getTheirVersion()) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
}
this.animateFailureOnDraw = true;
} catch (Exception e) {
Log.w(TAG, e);
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
this.animateFailureOnDraw = true;
}
}
private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) {
String[] segments = getSegments(fingerprint, segmentCount);
StringBuilder result = new StringBuilder();
for (int i = 0; i < segments.length; i++) {
result.append(segments[i]);
if (i != segments.length - 1) {
if (((i + 1) % 4) == 0) result.append('\n');
else result.append(' ');
}
}
return result.toString();
}
private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) {
Util.writeTextToClipboard(requireContext(), "Safety numbers", getFormattedSafetyNumbers(fingerprint, segmentCount));
}
private void handleCompareWithClipboard(Fingerprint fingerprint) {
String clipboardData = Util.readTextFromClipboard(getActivity());
if (clipboardData == null) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
return;
}
String numericClipboardData = clipboardData.replaceAll("\\D", "");
if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
return;
}
if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) {
animateVerifiedSuccess();
} else {
animateVerifiedFailure();
}
}
private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) {
String shareString =
getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" +
getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n";
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, shareString);
intent.setType("text/plain");
try {
startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via)));
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
}
}
private void setRecipientText(Recipient recipient) {
String escapedDisplayName = Html.escapeHtml(recipient.getDisplayName(getContext()));
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s), escapedDisplayName)));
description.setMovementMethod(LinkMovementMethod.getInstance());
}
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
String[] segments = getSegments(fingerprint, codes.length);
for (int i = 0; i < codes.length; i++) {
if (animate) setCodeSegment(codes[i], segments[i]);
else codes[i].setText(segments[i]);
}
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1"));
Bitmap qrCodeBitmap = QrCodeUtil.create(qrCodeString);
qrCode.setImageBitmap(qrCodeBitmap);
if (animate) {
ViewUtil.fadeIn(qrCode, 1000);
ViewUtil.fadeIn(tapLabel, 1000);
ViewUtil.fadeOut(loading, 300, View.GONE);
} else {
qrCode.setVisibility(View.VISIBLE);
tapLabel.setVisibility(View.VISIBLE);
loading.setVisibility(View.GONE);
}
}
private void setCodeSegment(final TextView codeView, String segment) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
valueAnimator.addUpdateListener(animation -> {
int value = (int) animation.getAnimatedValue();
codeView.setText(String.format(Locale.getDefault(), "%05d", value));
});
valueAnimator.setEvaluator(new TypeEvaluator<Integer>() {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
return Math.round(startValue + (endValue - startValue) * fraction);
}
});
valueAnimator.setDuration(1000);
valueAnimator.start();
}
private String[] getSegments(Fingerprint fingerprint, int segmentCount) {
String[] segments = new String[segmentCount];
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
int partSize = digits.length() / segmentCount;
for (int i = 0; i < segmentCount; i++) {
segments[i] = digits.substring(i * partSize, (i * partSize) + partSize);
}
return segments;
}
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Bitmap check = BitmapFactory.decodeResource(getResources(), id);
float offset = (width - check.getWidth()) / 2;
canvas.drawBitmap(check, offset, offset, null);
return bitmap;
}
private void animateVerifiedSuccess() {
Bitmap qrBitmap = ((BitmapDrawable) qrCode.getDrawable()).getBitmap();
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_check_white_48dp);
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
tapLabel.setText(getString(R.string.verify_display_fragment__successful_match));
animateVerified();
}
private void animateVerifiedFailure() {
Bitmap qrBitmap = ((BitmapDrawable) qrCode.getDrawable()).getBitmap();
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_close_white_48dp);
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
tapLabel.setText(getString(R.string.verify_display_fragment__failed_to_verify_safety_number));
animateVerified();
}
private void animateVerified() {
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new FastOutSlowInInterpolator());
scaleAnimation.setDuration(800);
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
qrVerified.postDelayed(() -> {
ScaleAnimation scaleAnimation1 = new ScaleAnimation(1, 0, 1, 0,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation1.setInterpolator(new AnticipateInterpolator());
scaleAnimation1.setDuration(500);
ViewUtil.animateOut(qrVerified, scaleAnimation1, View.GONE);
ViewUtil.fadeIn(qrCode, 800);
qrCodeContainer.setEnabled(true);
tapLabel.setText(getString(R.string.verify_display_fragment__tap_to_scan));
}, 2000);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
ViewUtil.fadeOut(qrCode, 200, View.INVISIBLE);
ViewUtil.animateIn(qrVerified, scaleAnimation);
qrCodeContainer.setEnabled(false);
}
private void updateVerifyButton(boolean verified, boolean update) {
currentVerifiedState = verified;
if (verified) {
verifyButton.setText(R.string.verify_display_fragment__clear_verification);
} else {
verifyButton.setText(R.string.verify_display_fragment__mark_as_verified);
}
if (update) {
final RecipientId recipientId = recipient.getId();
final Context context = requireContext().getApplicationContext();
SignalExecutors.BOUNDED.execute(() -> {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
if (verified) {
Log.i(TAG, "Saving identity: " + recipientId);
ApplicationDependencies.getProtocolStore().aci().identities()
.saveIdentityWithoutSideEffects(recipientId,
Recipient.resolved(recipientId).requireServiceId(),
remoteIdentity,
IdentityTable.VerifiedStatus.VERIFIED,
false,
System.currentTimeMillis(),
true);
} else {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipientId, remoteIdentity, IdentityTable.VerifiedStatus.DEFAULT);
}
ApplicationDependencies.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
remoteIdentity,
verified ? IdentityTable.VerifiedStatus.VERIFIED
: IdentityTable.VerifiedStatus.DEFAULT));
StorageSyncHelper.scheduleSyncForDataChange();
IdentityUtil.markIdentityVerified(context, recipient.get(), verified, false);
}
});
}
}
@Override public void onScrollChanged() {
if (scrollView.canScrollVertically(-1)) {
if (toolbarShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(toolbarShadow, 250);
}
} else {
if (toolbarShadow.getVisibility() != View.GONE) {
ViewUtil.fadeOut(toolbarShadow, 250);
}
}
if (scrollView.canScrollVertically(1)) {
if (bottomShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(bottomShadow, 250);
}
} else {
ViewUtil.fadeOut(bottomShadow, 250);
}
}
interface Callback {
void onQrCodeContainerClicked();
}
}

View File

@@ -0,0 +1,432 @@
package org.thoughtcrime.securesms.verify
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.Html
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnScrollChangedListener
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.signal.core.util.ThreadUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.requireParcelableCompat
import org.signal.libsignal.protocol.fingerprint.Fingerprint
import org.signal.libsignal.protocol.fingerprint.FingerprintVersionMismatchException
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.verify.SafetyNumberQrView
import org.thoughtcrime.securesms.components.verify.SafetyNumberQrView.Companion.getSegments
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable
import org.thoughtcrime.securesms.databinding.VerifyDisplayFragmentBinding
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.verify.PnpSafetyNumberEducationDialogFragment.Companion.showIfNeeded
import java.nio.charset.StandardCharsets
import java.util.Locale
/**
* Fragment to display a user's identity key.
*/
class VerifyDisplayFragment : Fragment(), OnScrollChangedListener {
private lateinit var viewModel: VerifySafetyNumberViewModel
private val binding by ViewBinderDelegate(VerifyDisplayFragmentBinding::bind)
private lateinit var safetyNumberAdapter: SafetyNumberAdapter
private var selectedFingerPrint = 0
private var callback: Callback? = null
private var animateSuccessOnDraw = false
private var animateFailureOnDraw = false
private var currentVerifiedState = false
override fun onAttach(context: Context) {
super.onAttach(context)
callback = if (context is Callback) {
context
} else if (parentFragment is Callback) {
parentFragment as Callback?
} else {
throw ClassCastException("Cannot find ScanListener in parent component")
}
}
override fun onCreateView(inflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
return ViewUtil.inflate(inflater, viewGroup!!, R.layout.verify_display_fragment)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initializeViewModel()
binding.safetyNumberUpdatingBannerText.text = Html.fromHtml(String.format(getString(R.string.verify_display_fragment__safety_numbers_are_updating_banner)))
updateVerifyButton(requireArguments().getBoolean(VERIFIED_STATE, false), false)
binding.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) }
binding.scrollView.viewTreeObserver?.addOnScrollChangedListener(this)
binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
binding.toolbar.setTitle(R.string.AndroidManifest__verify_safety_number)
safetyNumberAdapter = SafetyNumberAdapter()
binding.verifyViewPager.adapter = safetyNumberAdapter
binding.verifyViewPager.registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
selectedFingerPrint = position
}
})
val peekSize = resources.getDimensionPixelSize(R.dimen.safety_number_qr_peek)
val pageWidth = resources.getDimensionPixelSize(R.dimen.safety_number_qr_width)
val pageTransformer = ViewPager2.PageTransformer { page: View, position: Float -> page.translationX = -position * (peekSize + (page.width - pageWidth) / 2f) }
binding.verifyViewPager.setPageTransformer(pageTransformer)
binding.verifyViewPager.offscreenPageLimit = 1
TabLayoutMediator(binding.dotIndicators, binding.verifyViewPager) { _: TabLayout.Tab?, _: Int -> }.attach()
viewModel.recipient.observe(this) { recipient: Recipient -> setRecipientText(recipient) }
viewModel.getFingerprints().observe(viewLifecycleOwner) { fingerprints: List<SafetyNumberFingerprint>? ->
if (fingerprints == null) {
return@observe
}
val multipleCards = fingerprints.size > 1
binding.safetyNumberChangeBanner.visible = multipleCards
binding.dotIndicators.visible = multipleCards
if (fingerprints.isEmpty()) {
val resolved = viewModel.recipient.resolve()
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.serviceId.isPresent, resolved.e164.isPresent))
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> requireActivity().finish() }
.setOnDismissListener { dialog: DialogInterface ->
requireActivity().finish()
dialog.dismiss()
}
.show()
return@observe
}
safetyNumberAdapter.setFingerprints(fingerprints)
}
binding.verifyViewPager.currentItem = selectedFingerPrint
}
private fun initializeViewModel() {
val recipientId = requireArguments().requireParcelableCompat(RECIPIENT_ID, RecipientId::class.java)
val localIdentity = requireArguments().requireParcelableCompat(LOCAL_IDENTITY, IdentityKeyParcelable::class.java).get()!!
val remoteIdentity = requireArguments().requireParcelableCompat(REMOTE_IDENTITY, IdentityKeyParcelable::class.java).get()!!
viewModel = ViewModelProvider(this, VerifySafetyNumberViewModel.Factory(recipientId, localIdentity, remoteIdentity)).get(VerifySafetyNumberViewModel::class.java)
}
override fun onStart() {
super.onStart()
showIfNeeded(childFragmentManager)
}
override fun onResume() {
super.onResume()
setRecipientText(viewModel.recipient.get())
val selectedSnapshot = selectedFingerPrint
if (animateSuccessOnDraw) {
animateSuccessOnDraw = false
ThreadUtil.postToMain {
animateSuccess(selectedSnapshot)
}
ThreadUtil.postToMain {
binding.verifyViewPager.currentItem = selectedSnapshot
}
} else if (animateFailureOnDraw) {
animateFailureOnDraw = false
ThreadUtil.postToMain {
animateFailure(selectedSnapshot)
}
ThreadUtil.postToMain {
binding.verifyViewPager.currentItem = selectedSnapshot
}
}
ThreadUtil.postToMain { onScrollChanged() }
}
override fun onCreateContextMenu(
menu: ContextMenu,
view: View,
menuInfo: ContextMenuInfo?
) {
super.onCreateContextMenu(menu, view, menuInfo)
val fingerprints = viewModel.getFingerprints().value
if (!fingerprints.isNullOrEmpty()) {
val inflater = requireActivity().menuInflater
inflater.inflate(R.menu.verify_display_fragment_context_menu, menu)
}
}
override fun onContextItemSelected(item: MenuItem): Boolean {
if (currentFingerprint == null) return super.onContextItemSelected(item)
return if (item.itemId == R.id.menu_copy) {
handleCopyToClipboard(currentFingerprint)
true
} else if (item.itemId == R.id.menu_compare) {
handleCompareWithClipboard()
true
} else {
super.onContextItemSelected(item)
}
}
private val currentFingerprint: Fingerprint?
get() {
val fingerprints = viewModel.getFingerprints().value ?: return null
return fingerprints[binding.verifyViewPager.currentItem].fingerprint
}
fun setScannedFingerprint(scanned: String) {
val fingerprints = viewModel.getFingerprints().value
var haveMatchingVersion = false
if (fingerprints != null) {
for (i in fingerprints.indices) {
try {
if (fingerprints[i].fingerprint.scannableFingerprint.compareTo(scanned.toByteArray(StandardCharsets.ISO_8859_1))) {
animateSuccessOnDraw = true
} else {
animateFailureOnDraw = true
}
haveMatchingVersion = true
selectedFingerPrint = i
break
} catch (e: FingerprintVersionMismatchException) {
Log.w(TAG, e)
} catch (e: Exception) {
Log.w(TAG, e)
Toast.makeText(activity, R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show()
animateFailureOnDraw = true
return
}
}
}
if (!haveMatchingVersion) {
Toast.makeText(activity, R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show()
animateFailureOnDraw = true
}
}
private fun getFormattedSafetyNumbers(fingerprint: Fingerprint): String {
val segments = getSegments(fingerprint)
val result = StringBuilder()
for (i in segments.indices) {
result.append(segments[i])
if (i != segments.size - 1) {
if ((i + 1) % 4 == 0) result.append('\n') else result.append(' ')
}
}
return result.toString()
}
private fun handleCopyToClipboard(fingerprint: Fingerprint?) {
Util.writeTextToClipboard(requireContext(), "Safety numbers", getFormattedSafetyNumbers(fingerprint!!))
}
private fun handleCompareWithClipboard() {
val clipboardData = Util.readTextFromClipboard(requireActivity())
if (clipboardData == null) {
Toast.makeText(requireActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show()
return
}
val numericClipboardData = clipboardData.replace("\\D".toRegex(), "")
if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length != 60) {
Toast.makeText(requireActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show()
return
}
var success = false
val fingerprints = viewModel.getFingerprints().value
if (fingerprints != null) {
for (i in fingerprints.indices) {
val (_, _, _, _, _, fingerprint1) = fingerprints[i]
if (fingerprint1.displayableFingerprint.displayText == numericClipboardData) {
binding.verifyViewPager.currentItem = i
animateSuccess(i)
success = true
break
}
}
}
if (!success) {
animateFailure(selectedFingerPrint)
}
}
private fun animateSuccess(position: Int) {
safetyNumberAdapter.notifyItemChanged(position, true)
}
private fun animateFailure(position: Int) {
safetyNumberAdapter.notifyItemChanged(position, false)
}
private fun handleShare(fingerprint: Fingerprint) {
val shareString = """
${getString(R.string.VerifyIdentityActivity_our_signal_safety_number)}
${getFormattedSafetyNumbers(fingerprint)}
""".trimIndent()
val intent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, shareString)
}
try {
startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via)))
} catch (e: ActivityNotFoundException) {
Toast.makeText(activity, R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show()
}
}
private fun setRecipientText(recipient: Recipient) {
val escapedDisplayName = Html.escapeHtml(recipient.getDisplayName(requireContext()))
binding.description.text = Html.fromHtml(String.format(getString(R.string.verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s_pnp), escapedDisplayName))
binding.description.movementMethod = LinkMovementMethod.getInstance()
}
private fun updateVerifyButton(verified: Boolean, update: Boolean) {
currentVerifiedState = verified
if (verified) {
binding.verifyButton.setText(R.string.verify_display_fragment__clear_verification)
} else {
binding.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified)
}
if (update) {
viewModel.updateSafetyNumberVerification(verified)
}
}
override fun onScrollChanged() {
val fingerprints = viewModel.getFingerprints().value
if (binding.scrollView.canScrollVertically(-1) && fingerprints != null && fingerprints.size <= 1) {
if (binding.toolbarShadow.visibility != View.VISIBLE) {
ViewUtil.fadeIn(binding.toolbarShadow, 250)
}
} else {
if (binding.toolbarShadow.visibility != View.GONE) {
ViewUtil.fadeOut(binding.toolbarShadow, 250)
}
}
if (binding.scrollView.canScrollVertically(1)) {
if (binding.verifyIdentityBottomShadow.visibility != View.VISIBLE) {
ViewUtil.fadeIn(binding.verifyIdentityBottomShadow, 250)
}
} else {
ViewUtil.fadeOut(binding.verifyIdentityBottomShadow, 250)
}
}
internal interface Callback {
fun onQrCodeContainerClicked()
}
private class SafetyNumberQrViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val safetyNumberQrView: SafetyNumberQrView
init {
safetyNumberQrView = itemView.findViewById(R.id.safety_qr_view)
}
}
private inner class SafetyNumberAdapter : RecyclerView.Adapter<SafetyNumberQrViewHolder>() {
private var fingerprints: List<SafetyNumberFingerprint>? = null
fun setFingerprints(fingerprints: List<SafetyNumberFingerprint>?) {
if (fingerprints == this.fingerprints) {
return
}
this.fingerprints = fingerprints?.let { ArrayList(it) }
notifyDataSetChanged()
}
init {
setHasStableIds(true)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SafetyNumberQrViewHolder {
return SafetyNumberQrViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.safety_number_qr_page_fragment, parent, false))
}
override fun onBindViewHolder(holder: SafetyNumberQrViewHolder, position: Int) {
val (version, _, _, _, _, fingerprint1) = fingerprints!![position]
holder.safetyNumberQrView.setFingerprintViews(fingerprint1, true)
holder.safetyNumberQrView.setSafetyNumberType(version == 2)
holder.safetyNumberQrView.shareButton.setOnClickListener { v: View? -> handleShare(fingerprints!![position].fingerprint) }
holder.safetyNumberQrView.qrCodeContainer.setOnClickListener { v: View? -> callback!!.onQrCodeContainerClicked() }
registerForContextMenu(holder.safetyNumberQrView.numbersContainer)
}
override fun onBindViewHolder(holder: SafetyNumberQrViewHolder, position: Int, payloads: List<Any>) {
super.onBindViewHolder(holder, position, payloads)
for (payload in payloads) {
if (payload is Boolean) {
if (payload) {
holder.safetyNumberQrView.animateVerifiedSuccess()
} else {
holder.safetyNumberQrView.animateVerifiedFailure()
}
break
}
}
}
override fun getItemId(position: Int): Long {
return fingerprints!![position].version.toLong()
}
override fun getItemCount(): Int {
return if (fingerprints != null) fingerprints!!.size else 0
}
}
companion object {
private val TAG = Log.tag(VerifyDisplayFragment::class.java)
private const val RECIPIENT_ID = "recipient_id"
private const val REMOTE_IDENTITY = "remote_identity"
private const val LOCAL_IDENTITY = "local_identity"
private const val LOCAL_NUMBER = "local_number"
private const val VERIFIED_STATE = "verified_state"
fun create(
recipientId: RecipientId,
remoteIdentity: IdentityKeyParcelable,
localIdentity: IdentityKeyParcelable,
localNumber: String,
verifiedState: Boolean
): VerifyDisplayFragment {
val fragment = VerifyDisplayFragment()
fragment.arguments = Bundle().apply {
putParcelable(RECIPIENT_ID, recipientId)
putParcelable(REMOTE_IDENTITY, remoteIdentity)
putParcelable(LOCAL_IDENTITY, localIdentity)
putString(LOCAL_NUMBER, localNumber)
putBoolean(VERIFIED_STATE, verifiedState)
}
return fragment
}
}
}

View File

@@ -6,14 +6,20 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.database.IdentityTable;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
/**
* Activity for verifying identity keys.
@@ -28,23 +34,52 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
identityRecord.getVerifiedStatus() == IdentityTable.VerifiedStatus.VERIFIED);
public static void startOrShowExchangeMessagesDialog(@NonNull Context context,
@NonNull IdentityRecord identityRecord) {
startOrShowExchangeMessagesDialog(context, identityRecord.getRecipientId(), identityRecord.getIdentityKey(), identityRecord.getVerifiedStatus() == IdentityTable.VerifiedStatus.VERIFIED);
}
public static void startOrShowExchangeMessagesDialog(@NonNull Context context,
@NonNull IdentityRecord identityRecord,
boolean verified) {
startOrShowExchangeMessagesDialog(context, identityRecord.getRecipientId(), identityRecord.getIdentityKey(), verified);
}
public static void startOrShowExchangeMessagesDialog(@NonNull Context context,
@NonNull RecipientId recipientId,
@NonNull IdentityKey identityKey,
boolean verified) {
Recipient recipient = Recipient.live(recipientId).resolve();
if (FeatureFlags.showAciSafetyNumberAsDefault()) {
if (!recipient.hasServiceId()) {
showExchangeMessagesDialog(context);
return;
}
} else {
if (!recipient.hasServiceId() || !recipient.hasE164()) {
showExchangeMessagesDialog(context);
return;
}
}
context.startActivity(newIntent(context, recipientId, identityKey, verified));
}
private static void showExchangeMessagesDialog(@NonNull Context context) {
new MaterialAlertDialogBuilder(context)
.setMessage(R.string.VerifyIdentityActivity_dialog_exchange_messages_to_create_safety_number_message)
.setPositiveButton(R.string.VerifyIdentityActivity_dialog_exchange_messages_to_create_safety_number_ok, null)
.setNeutralButton(R.string.VerifyIdentityActivity_dialog_exchange_messages_to_create_safety_number_learn_more, (dialog, which) -> {
CommunicationActions.openBrowserLink(context, "https://support.signal.org/hc/en-us/articles/360007060632");
})
.show();
}
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityRecord identityRecord,
boolean verified)
@NonNull IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
verified);
return newIntent(context, identityRecord.getRecipientId(), identityRecord.getIdentityKey(), identityRecord.getVerifiedStatus() == IdentityTable.VerifiedStatus.VERIFIED);
}
public static Intent newIntent(@NonNull Context context,

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.verify
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.fingerprint.Fingerprint
import org.signal.libsignal.protocol.fingerprint.NumericFingerprintGenerator
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.LiveRecipient
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.IdentityUtil
class VerifySafetyNumberViewModel(
private val recipientId: RecipientId,
private val localIdentity: IdentityKey,
private val remoteIdentity: IdentityKey
) : ViewModel() {
companion object {
val TAG = Log.tag(VerifySafetyNumberViewModel::class.java)
}
val recipient: LiveRecipient = Recipient.live(recipientId)
private val fingerprintListLiveData = MutableLiveData<List<SafetyNumberFingerprint>>()
init {
initializeFingerprints()
}
private fun initializeFingerprints() {
SignalExecutors.UNBOUNDED.execute {
val resolved = recipient.resolve()
val fingerprintList: MutableList<SafetyNumberFingerprint> = ArrayList(2)
val generator = NumericFingerprintGenerator(5200)
var aciFingerprint: SafetyNumberFingerprint? = null
var e164Fingerprint: SafetyNumberFingerprint? = null
if (resolved.e164.isPresent) {
val localIdentifier = Recipient.self().requireE164().toByteArray()
val remoteIdentifier = resolved.requireE164().toByteArray()
val version = 1
e164Fingerprint = SafetyNumberFingerprint(version, localIdentifier, localIdentity, remoteIdentifier, remoteIdentity, generator.createFor(version, localIdentifier, localIdentity, remoteIdentifier, remoteIdentity))
}
if (resolved.serviceId.isPresent) {
val localIdentifier = SignalStore.account().requireAci().toByteArray()
val remoteIdentifier = resolved.requireServiceId().toByteArray()
val version = 2
aciFingerprint = SafetyNumberFingerprint(version, localIdentifier, localIdentity, remoteIdentifier, remoteIdentity, generator.createFor(version, localIdentifier, localIdentity, remoteIdentifier, remoteIdentity))
}
if (FeatureFlags.showAciSafetyNumberAsDefault()) {
if (aciFingerprint != null) {
fingerprintList.add(aciFingerprint)
if (e164Fingerprint != null) {
fingerprintList.add(e164Fingerprint)
}
}
} else {
if (aciFingerprint != null && e164Fingerprint != null) {
fingerprintList.add(e164Fingerprint)
fingerprintList.add(aciFingerprint)
}
}
fingerprintListLiveData.postValue(fingerprintList)
}
}
fun getFingerprints(): LiveData<List<SafetyNumberFingerprint>> {
return fingerprintListLiveData
}
fun updateSafetyNumberVerification(verified: Boolean) {
val recipientId: RecipientId = recipientId
val context: Context = ApplicationDependencies.getApplication()
SignalExecutors.BOUNDED.execute {
ReentrantSessionLock.INSTANCE.acquire().use { unused ->
if (verified) {
Log.i(TAG, "Saving identity: $recipientId")
ApplicationDependencies.getProtocolStore().aci().identities()
.saveIdentityWithoutSideEffects(
recipientId,
recipient.resolve().requireServiceId(),
remoteIdentity,
IdentityTable.VerifiedStatus.VERIFIED,
false,
System.currentTimeMillis(),
true
)
} else {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipientId, remoteIdentity, IdentityTable.VerifiedStatus.DEFAULT)
}
ApplicationDependencies.getJobManager()
.add(
MultiDeviceVerifiedUpdateJob(
recipientId,
remoteIdentity,
if (verified) IdentityTable.VerifiedStatus.VERIFIED else IdentityTable.VerifiedStatus.DEFAULT
)
)
StorageSyncHelper.scheduleSyncForDataChange()
IdentityUtil.markIdentityVerified(context, recipient.get(), verified, false)
}
}
}
class Factory(
private val recipientId: RecipientId,
private val localIdentity: IdentityKey,
private val remoteIdentity: IdentityKey
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(VerifySafetyNumberViewModel(recipientId, localIdentity, remoteIdentity))!!
}
}
}
data class SafetyNumberFingerprint(
val version: Int = 0,
val localStableIdentifier: ByteArray?,
val localIdentityKey: IdentityKey? = null,
val remoteStableIdentifier: ByteArray?,
val remoteIdentityKey: IdentityKey? = null,
val fingerprint: Fingerprint
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SafetyNumberFingerprint
if (version != other.version) return false
if (localStableIdentifier != null) {
if (other.localStableIdentifier == null) return false
if (!localStableIdentifier.contentEquals(other.localStableIdentifier)) return false
} else if (other.localStableIdentifier != null) return false
if (localIdentityKey != other.localIdentityKey) return false
if (remoteStableIdentifier != null) {
if (other.remoteStableIdentifier == null) return false
if (!remoteStableIdentifier.contentEquals(other.remoteStableIdentifier)) return false
} else if (other.remoteStableIdentifier != null) return false
if (remoteIdentityKey != other.remoteIdentityKey) return false
if (fingerprint != other.fingerprint) return false
return true
}
override fun hashCode(): Int {
var result = version
result = 31 * result + (localStableIdentifier?.contentHashCode() ?: 0)
result = 31 * result + (localIdentityKey?.hashCode() ?: 0)
result = 31 * result + (remoteStableIdentifier?.contentHashCode() ?: 0)
result = 31 * result + (remoteIdentityKey?.hashCode() ?: 0)
result = 31 * result + (fingerprint?.hashCode() ?: 0)
return result
}
}