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

@ -549,7 +549,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
isEnabled = !state.isDeprecatedOrUnregistered,
onClick = {
startActivity(VerifyIdentityActivity.newIntent(requireActivity(), recipientState.identityRecord))
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireActivity(), recipientState.identityRecord)
}
)
}

View file

@ -0,0 +1,229 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.verify
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.PorterDuff
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.view.View
import android.view.ViewOutlineProvider
import android.view.animation.Animation
import android.view.animation.AnticipateInterpolator
import android.view.animation.ScaleAnimation
import android.widget.ImageView
import android.widget.TextSwitcher
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.widget.ImageViewCompat
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.signal.core.util.dp
import org.signal.libsignal.protocol.fingerprint.Fingerprint
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.qr.QrCodeUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.nio.charset.Charset
import java.util.Locale
class SafetyNumberQrView : ConstraintLayout {
companion object {
private const val NUMBER_OF_SEGMENTS = 12
@JvmStatic
fun getSegments(fingerprint: Fingerprint): Array<String> {
val segments = arrayOfNulls<String>(NUMBER_OF_SEGMENTS)
val digits = fingerprint.displayableFingerprint.displayText
val partSize = digits.length / NUMBER_OF_SEGMENTS
for (i in 0 until NUMBER_OF_SEGMENTS) {
segments[i] = digits.substring(i * partSize, i * partSize + partSize)
}
return (0 until NUMBER_OF_SEGMENTS).map { digits.substring(it * partSize, it * partSize + partSize) }.toTypedArray()
}
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defaultStyle: Int) : super(context, attrs, defaultStyle)
private val codes: Array<TextView>
val numbersContainer: View
val qrCodeContainer: View
val shareButton: ImageView
private val loading: View
private val qrCode: ImageView
private val qrVerified: ImageView
private val tapLabel: TextSwitcher
init {
inflate(context, R.layout.safety_number_qr_view, this)
numbersContainer = findViewById(R.id.number_table)
loading = findViewById(R.id.loading)
qrCodeContainer = findViewById(R.id.qr_code_container)
qrCode = findViewById(R.id.qr_code)
qrVerified = findViewById(R.id.qr_verified)
tapLabel = findViewById(R.id.tap_label)
codes = arrayOf(
findViewById(R.id.code_first),
findViewById(R.id.code_second),
findViewById(R.id.code_third),
findViewById(R.id.code_fourth),
findViewById(R.id.code_fifth),
findViewById(R.id.code_sixth),
findViewById(R.id.code_seventh),
findViewById(R.id.code_eighth),
findViewById(R.id.code_ninth),
findViewById(R.id.code_tenth),
findViewById(R.id.code_eleventh),
findViewById(R.id.code_twelth)
)
shareButton = findViewById(R.id.share)
outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, 24.dp.toFloat())
}
}
clipToOutline = true
setSafetyNumberType(false)
}
fun setFingerprintViews(fingerprint: Fingerprint, animate: Boolean) {
val segments: Array<String> = getSegments(fingerprint)
for (i in codes.indices) {
if (animate) setCodeSegment(codes[i], segments[i]) else codes[i].text = segments[i]
}
val qrCodeData = fingerprint.scannableFingerprint.serialized
val qrCodeString = String(qrCodeData, Charset.forName("ISO-8859-1"))
val qrCodeBitmap = QrCodeUtil.create(qrCodeString)
qrCode.setImageBitmap(qrCodeBitmap)
shareButton.visible = true
if (animate) {
ViewUtil.fadeIn(qrCode, 1000)
ViewUtil.fadeIn(tapLabel, 1000)
ViewUtil.fadeOut(loading, 300, GONE)
} else {
qrCode.visibility = VISIBLE
tapLabel.visibility = VISIBLE
loading.visibility = GONE
}
}
fun setSafetyNumberType(newType: Boolean) {
if (newType) {
ImageViewCompat.setImageTintList(shareButton, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.signal_dark_colorOnSurface)))
setBackgroundColor(ContextCompat.getColor(context, R.color.safety_number_card_blue))
codes.forEach {
it.setTextColor(ContextCompat.getColor(context, R.color.signal_light_colorOnPrimary))
}
} else {
ImageViewCompat.setImageTintList(shareButton, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.signal_light_colorOnSurface)))
setBackgroundColor(ContextCompat.getColor(context, R.color.safety_number_card_grey))
codes.forEach {
it.setTextColor(ContextCompat.getColor(context, R.color.signal_light_colorOnSurfaceVariant))
}
}
}
fun animateVerifiedSuccess() {
val qrBitmap = (qrCode.drawable as BitmapDrawable).bitmap
val qrSuccess: Bitmap = createVerifiedBitmap(qrBitmap.width, qrBitmap.height, R.drawable.ic_check_white_48dp)
qrVerified.setImageBitmap(qrSuccess)
qrVerified.background.setColorFilter(resources.getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY)
tapLabel.setText(context.getString(R.string.verify_display_fragment__successful_match))
animateVerified()
}
fun animateVerifiedFailure() {
val qrBitmap = (qrCode.drawable as BitmapDrawable).bitmap
val qrSuccess: Bitmap = createVerifiedBitmap(qrBitmap.width, qrBitmap.height, R.drawable.ic_close_white_48dp)
qrVerified.setImageBitmap(qrSuccess)
qrVerified.background.setColorFilter(resources.getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY)
tapLabel.setText(context.getString(R.string.verify_display_fragment__failed_to_verify_safety_number))
animateVerified()
}
private fun animateVerified() {
val scaleAnimation = ScaleAnimation(
0f,
1f,
0f,
1f,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f
)
scaleAnimation.interpolator = FastOutSlowInInterpolator()
scaleAnimation.duration = 800
scaleAnimation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
qrVerified.postDelayed({
val scaleAnimation = ScaleAnimation(
1f,
0f,
1f,
0f,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f,
ScaleAnimation.RELATIVE_TO_SELF,
0.5f
)
scaleAnimation.interpolator = AnticipateInterpolator()
scaleAnimation.duration = 500
ViewUtil.animateOut(qrVerified, scaleAnimation, GONE)
ViewUtil.fadeIn(qrCode, 800)
qrCodeContainer.isEnabled = true
tapLabel.setText(context.getString(R.string.verify_display_fragment__tap_to_scan))
}, 2000)
}
override fun onAnimationRepeat(animation: Animation) {}
})
ViewUtil.fadeOut(qrCode, 200, INVISIBLE)
ViewUtil.animateIn(qrVerified, scaleAnimation)
qrCodeContainer.isEnabled = false
}
private fun createVerifiedBitmap(width: Int, height: Int, @DrawableRes id: Int): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val check = BitmapFactory.decodeResource(resources, id)
val offset = ((width - check.width) / 2).toFloat()
canvas.drawBitmap(check, offset, offset, null)
return bitmap
}
private fun setCodeSegment(codeView: TextView, segment: String) {
val valueAnimator = ValueAnimator.ofInt(0, segment.toInt())
valueAnimator.addUpdateListener { animation: ValueAnimator ->
val value = animation.animatedValue as Int
codeView.text = String.format(Locale.getDefault(), "%05d", value)
}
valueAnimator.duration = 1000
valueAnimator.start()
}
}

View file

@ -4391,7 +4391,7 @@ public class ConversationParentFragment extends Fragment
public void onClicked(final List<IdentityRecord> unverifiedIdentities) {
Log.i(TAG, "onClicked: " + unverifiedIdentities.size());
if (unverifiedIdentities.size() == 1) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities.get(0), false));
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), unverifiedIdentities.get(0), false);
} else {
String[] unverifiedNames = new String[unverifiedIdentities.size()];
@ -4403,7 +4403,7 @@ public class ConversationParentFragment extends Fragment
builder.setIcon(R.drawable.ic_warning);
builder.setTitle(R.string.ConversationFragment__no_longer_verified);
builder.setItems(unverifiedNames, (dialog, which) -> {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities.get(which), false));
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), unverifiedIdentities.get(which), false);
});
builder.show();
}

View file

@ -225,7 +225,7 @@ public final class SafetyNumberChangeDialog extends DialogFragment implements Sa
@Override
public void onViewIdentityRecord(@NonNull IdentityRecord identityRecord) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord));
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), identityRecord);
}
public interface Callback {

View file

@ -55,7 +55,7 @@ object ConversationDialogs {
{ ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(recipient.id) },
{ identityRecord ->
identityRecord.ifPresent {
fragment.startActivity(VerifyIdentityActivity.newIntent(fragment.requireContext(), identityRecord.get()))
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(fragment.requireContext(), identityRecord.get())
}
d.dismiss()
}

View file

@ -3246,7 +3246,7 @@ class ConversationFragment :
override fun onUnverifiedBannerClicked(unverifiedIdentities: List<IdentityRecord>) {
if (unverifiedIdentities.size == 1) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities[0], false))
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), unverifiedIdentities[0], false)
} else {
val unverifiedNames = unverifiedIdentities
.map { Recipient.resolved(it.recipientId).getDisplayName(requireContext()) }
@ -3255,7 +3255,7 @@ class ConversationFragment :
MaterialAlertDialogBuilder(requireContext())
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.ConversationFragment__no_longer_verified)
.setItems(unverifiedNames) { _, which: Int -> startActivity(VerifyIdentityActivity.newIntent(requireContext(), unverifiedIdentities[which], false)) }
.setItems(unverifiedNames) { _, which: Int -> VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), unverifiedIdentities[which], false) }
.show()
}
}

View file

@ -17,6 +17,7 @@ public class UiHints extends SignalStoreValues {
private static final String HAS_SEEN_USERNAME_EDUCATION = "uihints.has_seen_username_education";
private static final String HAS_SEEN_TEXT_FORMATTING_ALERT = "uihints.text_formatting.has_seen_alert";
private static final String HAS_NOT_SEEN_EDIT_MESSAGE_BETA_ALERT = "uihints.edit_message.has_not_seen_beta_alert";
private static final String HAS_SEEN_SAFETY_NUMBER_NUX = "uihints.has_seen_safety_number_nux";
UiHints(@NonNull KeyValueStore store) {
super(store);
@ -109,4 +110,12 @@ public class UiHints extends SignalStoreValues {
public void markHasSeenEditMessageBetaAlert() {
putBoolean(HAS_NOT_SEEN_EDIT_MESSAGE_BETA_ALERT, false);
}
public boolean hasSeenSafetyNumberUpdateNux() {
return getBoolean(HAS_SEEN_SAFETY_NUMBER_NUX, false);
}
public void markHasSeenSafetyNumberUpdateNux() {
putBoolean(HAS_SEEN_SAFETY_NUMBER_NUX, true);
}
}

View file

@ -7,7 +7,9 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@ -181,7 +183,7 @@ final class RecipientDialogViewModel extends ViewModel {
}
void onViewSafetyNumberClicked(@NonNull Activity activity, @NonNull IdentityRecord identityRecord) {
activity.startActivity(VerifyIdentityActivity.newIntent(activity, identityRecord));
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(activity, identityRecord);
}
void onAvatarClicked(@NonNull Activity activity) {

View file

@ -109,6 +109,7 @@ public final class FeatureFlags {
private static final String CDS_COMPAT_MODE = "global.cds.return_acis_without_uaks";
private static final String CONVERSATION_FRAGMENT_V2 = "android.conversationFragmentV2";
private static final String SAFETY_NUMBER_ACI = "global.safetyNumberAci";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
* remotely, place it in here.
@ -167,7 +168,8 @@ public final class FeatureFlags {
AD_HOC_CALLING,
SVR2_KILLSWITCH,
CDS_COMPAT_MODE,
CONVERSATION_FRAGMENT_V2
CONVERSATION_FRAGMENT_V2,
SAFETY_NUMBER_ACI
);
@VisibleForTesting
@ -233,7 +235,8 @@ public final class FeatureFlags {
MAX_ATTACHMENT_SIZE_BYTES,
SVR2_KILLSWITCH,
CDS_COMPAT_MODE,
CONVERSATION_FRAGMENT_V2
CONVERSATION_FRAGMENT_V2,
SAFETY_NUMBER_ACI
);
/**
@ -340,6 +343,14 @@ public final class FeatureFlags {
return getBoolean(VERIFY_V2, false);
}
/** Whether or not we show the ACI safety number as the default initial safety number. */
public static boolean showAciSafetyNumberAsDefault() {
long estimatedServerTimeSeconds = (System.currentTimeMillis() - SignalStore.misc().getLastKnownServerTimeOffset()) / 1000;
long flagEnableTimeSeconds = getLong(SAFETY_NUMBER_ACI, Long.MAX_VALUE);
return estimatedServerTimeSeconds > flagEnableTimeSeconds;
}
/** The raw client expiration JSON string. */
public static String clientExpiration() {
return getString(CLIENT_EXPIRATION, null);

View file

@ -1,36 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.text.style.ClickableSpan;
import android.view.View;
import androidx.annotation.NonNull;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
public class VerifySpan extends ClickableSpan {
private final Context context;
private final RecipientId recipientId;
private final IdentityKey identityKey;
public VerifySpan(@NonNull Context context, @NonNull IdentityKeyMismatch mismatch) {
this.context = context;
this.recipientId = mismatch.getRecipientId(context);
this.identityKey = mismatch.getIdentityKey();
}
public VerifySpan(@NonNull Context context, @NonNull RecipientId recipientId, @NonNull IdentityKey identityKey) {
this.context = context;
this.recipientId = recipientId;
this.identityKey = identityKey;
}
@Override
public void onClick(@NonNull View widget) {
context.startActivity(VerifyIdentityActivity.newIntent(context, recipientId, identityKey, false));
}
}

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
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="10dp" />
<solid android:color="@color/qr_card_color" />
<corners android:radius="12dp" />
<solid android:color="@color/white" />
</shape>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/selected_page_dot_indicator"
android:state_selected="true"/>
<item android:drawable="@drawable/unselected_page_dot_indicator"/>
</selector>

View file

@ -0,0 +1,168 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M32.974,7.69L43.85,12.762A4,4 77.179,0 1,45.784 18.078L35.642,39.829A4,4 68.645,0 1,30.326 41.764L19.45,36.692A4,4 74.71,0 1,17.515 31.377L27.658,9.625A4,4 79.888,0 1,32.974 7.69z"
android:strokeWidth="1.75"
android:fillColor="#86A1E3"
android:strokeColor="#3D62BC"/>
<group>
<clip-path
android:pathData="M30.861,12.222l10.876,5.071l-5.071,10.876l-10.876,-5.071z"/>
<path
android:pathData="M36.711,19.426C36.975,19.261 37.055,18.914 36.89,18.651C36.726,18.388 36.379,18.307 36.115,18.472L32.992,20.422L32.691,19.108C32.622,18.805 32.32,18.616 32.018,18.685C31.715,18.754 31.525,19.056 31.595,19.359L32.071,21.443C32.112,21.62 32.235,21.767 32.403,21.837C32.571,21.907 32.763,21.891 32.917,21.795L36.711,19.426Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M30.639,24.377C30.988,25.096 31.793,25.471 32.568,25.277L34.84,24.706C36.164,24.374 37.297,23.521 37.983,22.342L39.359,19.978C39.752,19.302 39.629,18.443 39.06,17.904L36.989,15.943C36.429,15.413 35.713,15.079 34.947,14.991L32.113,14.665C31.335,14.575 30.598,15.033 30.333,15.769L29.406,18.343C28.944,19.626 29.019,21.042 29.615,22.27L30.639,24.377ZM32.294,24.186C32.036,24.25 31.767,24.125 31.651,23.886L30.627,21.778C30.164,20.824 30.105,19.722 30.465,18.724L31.391,16.151C31.48,15.905 31.725,15.753 31.985,15.782L34.818,16.109C35.342,16.169 35.833,16.397 36.216,16.76L38.287,18.721C38.476,18.901 38.518,19.187 38.386,19.413L37.011,21.776C36.477,22.694 35.596,23.357 34.566,23.615L32.294,24.186Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M36.711,19.426C36.975,19.261 37.055,18.914 36.89,18.651C36.726,18.388 36.379,18.307 36.115,18.472L32.992,20.422L32.691,19.108C32.622,18.805 32.32,18.616 32.018,18.685C31.715,18.754 31.525,19.056 31.595,19.359L32.071,21.443C32.112,21.62 32.235,21.767 32.403,21.837C32.571,21.907 32.763,21.891 32.917,21.795L36.711,19.426Z"
android:strokeWidth="0.25"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
<path
android:pathData="M30.639,24.377C30.988,25.096 31.793,25.471 32.568,25.277L34.84,24.706C36.164,24.374 37.297,23.521 37.983,22.342L39.359,19.978C39.752,19.302 39.629,18.443 39.06,17.904L36.989,15.943C36.429,15.413 35.713,15.079 34.947,14.991L32.113,14.665C31.335,14.575 30.598,15.033 30.333,15.769L29.406,18.343C28.944,19.626 29.019,21.042 29.615,22.27L30.639,24.377ZM32.294,24.186C32.036,24.25 31.767,24.125 31.651,23.886L30.627,21.778C30.164,20.824 30.105,19.722 30.465,18.724L31.391,16.151C31.48,15.905 31.725,15.753 31.985,15.782L34.818,16.109C35.342,16.169 35.833,16.397 36.216,16.76L38.287,18.721C38.476,18.901 38.518,19.187 38.386,19.413L37.011,21.776C36.477,22.694 35.596,23.357 34.566,23.615L32.294,24.186Z"
android:strokeWidth="0.25"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#ffffff"/>
</group>
<path
android:pathData="M28.631,28.836L30.443,29.681"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M24.099,26.723L25.912,27.568"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M33.162,30.949L34.975,31.794"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M27.363,31.555L29.175,32.4"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M22.831,29.442L24.644,30.287"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M31.894,33.668L33.707,34.513"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M26.095,34.274L27.907,35.119"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M21.563,32.161L23.376,33.006"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M30.626,36.387L32.439,37.232"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M3.625,12.762L14.501,7.691A4,4 122.492,0 1,19.817 9.625L29.959,31.377A4,4 115.097,0 1,28.025 36.692L17.149,41.764A4,4 105.482,0 1,11.833 39.829L1.69,18.078A4,4 112.5,0 1,3.625 12.762z"
android:strokeWidth="1.75"
android:fillColor="#EBEAE8"
android:strokeColor="#837D72"/>
<group>
<clip-path
android:pathData="M5.738,17.294l10.876,-5.071l5.071,10.876l-10.876,5.071z"/>
<path
android:pathData="M15.017,17.442C15.061,17.135 14.847,16.85 14.539,16.807C14.231,16.764 13.947,16.978 13.903,17.285L13.389,20.932L12.19,20.317C11.913,20.176 11.574,20.285 11.433,20.562C11.291,20.838 11.4,21.177 11.677,21.319L13.58,22.293C13.741,22.376 13.934,22.376 14.095,22.292C14.257,22.208 14.368,22.051 14.393,21.871L15.017,17.442Z"
android:fillColor="#837D72"/>
<path
android:pathData="M14.907,25.277C15.682,25.471 16.487,25.096 16.836,24.377L17.86,22.27C18.456,21.042 18.531,19.626 18.069,18.343L17.142,15.769C16.877,15.033 16.14,14.575 15.362,14.665L12.528,14.991C11.762,15.079 11.046,15.413 10.486,15.943L8.415,17.904C7.846,18.443 7.722,19.302 8.116,19.978L9.492,22.342C10.178,23.521 11.311,24.374 12.635,24.706L14.907,25.277ZM15.824,23.886C15.708,24.125 15.439,24.25 15.181,24.185L12.908,23.615C11.879,23.357 10.998,22.694 10.464,21.776L9.089,19.413C8.957,19.187 8.999,18.901 9.188,18.721L11.259,16.76C11.642,16.397 12.133,16.169 12.656,16.108L15.49,15.782C15.75,15.752 15.995,15.905 16.084,16.151L17.01,18.724C17.37,19.722 17.311,20.823 16.848,21.778L15.824,23.886Z"
android:fillColor="#837D72"
android:fillType="evenOdd"/>
<path
android:pathData="M15.017,17.442C15.061,17.135 14.847,16.85 14.539,16.807C14.231,16.764 13.947,16.978 13.903,17.285L13.389,20.932L12.19,20.317C11.913,20.176 11.574,20.285 11.433,20.562C11.291,20.838 11.4,21.177 11.677,21.319L13.58,22.293C13.741,22.376 13.934,22.376 14.095,22.292C14.257,22.208 14.368,22.051 14.393,21.871L15.017,17.442Z"
android:strokeWidth="0.25"
android:fillColor="#00000000"
android:strokeColor="#837D72"/>
<path
android:pathData="M14.907,25.277C15.682,25.471 16.487,25.096 16.836,24.377L17.86,22.27C18.456,21.042 18.531,19.626 18.069,18.343L17.142,15.769C16.877,15.033 16.14,14.575 15.362,14.665L12.528,14.991C11.762,15.079 11.046,15.413 10.486,15.943L8.415,17.904C7.846,18.443 7.722,19.302 8.116,19.978L9.492,22.342C10.178,23.521 11.311,24.374 12.635,24.706L14.907,25.277ZM15.824,23.886C15.708,24.125 15.439,24.25 15.181,24.185L12.908,23.615C11.879,23.357 10.998,22.694 10.464,21.776L9.089,19.413C8.957,19.187 8.999,18.901 9.188,18.721L11.259,16.76C11.642,16.397 12.133,16.169 12.656,16.108L15.49,15.782C15.75,15.752 15.995,15.905 16.084,16.151L17.01,18.724C17.37,19.722 17.311,20.823 16.848,21.778L15.824,23.886Z"
android:strokeWidth="0.25"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#837D72"/>
</group>
<path
android:pathData="M17.032,29.681L18.844,28.836"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M12.5,31.794L14.313,30.949"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M21.563,27.568L23.376,26.723"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M18.3,32.4L20.112,31.555"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M13.768,34.513L15.581,33.668"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M22.831,30.287L24.644,29.442"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M19.567,35.119L21.38,34.274"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M15.036,37.232L16.848,36.387"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
<path
android:pathData="M24.099,33.006L25.912,32.161"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#837D72"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="5dp"
android:useLevel="false">
<solid android:color="@color/signal_colorOnSurfaceVariant"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="5dp"
android:useLevel="false">
<solid android:color="@color/signal_colorOnSurface_12"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="48dp"
android:layout_height="2dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:background="@color/signal_icon_tint_tab_unselected"/>
<TextView
style="@style/Signal.Text.Headline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:layout_marginTop="43dp"
android:text="@string/PnpSafetyNumberEducationDialog__title"
/>
<TextView
style="@style/Signal.Text.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="start"
android:layout_marginTop="12dp"
android:layout_marginHorizontal="24dp"
android:layout_marginBottom="24dp"
android:text="@string/PnpSafetyNumberEducationDialog__body"
/>
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottie"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="1dp"
android:layout_marginHorizontal="24dp"
android:background="@drawable/rounded_outline_12"
app:lottie_rawRes="@raw/safety_number_onboarding"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="47dp"
android:layout_marginBottom="24dp"
android:gravity="center_vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/help"
style="@style/Signal.Widget.Button.Medium.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/PnpSafetyNumberEducationDialog__help"/>
<View
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="0dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/okay"
style="@style/Signal.Widget.Button.Medium.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/PnpSafetyNumberEducationDialog__confirm"/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.verify.SafetyNumberQrView
android:id="@+id/safety_qr_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</FrameLayout>

View file

@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<merge
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="wrap_content"
android:layout_height="wrap_content"
tools:background="#EBEAE8"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:parentTag="org.thoughtcrime.securesms.components.verify.SafetyNumberQrView">
<LinearLayout
android:id="@+id/qr_code_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginStart="64dp"
android:layout_marginEnd="64dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@drawable/qr_code_background"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<TextView
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/verify_display_fragment__loading"
android:textSize="20sp" />
<org.thoughtcrime.securesms.components.SquareImageView
android:id="@+id/qr_code"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"
android:visibility="invisible"
tools:src="@drawable/ic_about_mc_80"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.SquareImageView
android:id="@+id/qr_verified"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="center"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/green_500"
android:src="@drawable/ic_check_white_48dp"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<TextSwitcher
android:id="@+id/tap_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="17dp"
android:inAnimation="@android:anim/fade_in"
android:outAnimation="@android:anim/fade_out">
<TextView
style="@style/Signal.Text.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/verify_display_fragment__tap_to_scan"
android:textColor="@color/qr_card_text_color" />
<TextView
style="@style/Signal.Text.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/verify_display_fragment__successful_match"
android:textColor="@color/qr_card_text_color" />
</TextSwitcher>
</LinearLayout>
<TableLayout
android:id="@+id/number_table"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/verify_identity_vertical_margin"
android:layout_marginBottom="49dp"
android:layout_marginHorizontal="25dp"
app:layout_constraintTop_toBottomOf="@id/qr_code_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:clickable="true"
android:focusable="true">
<TableRow
android:clickable="false"
android:focusable="false"
android:gravity="center_horizontal">
<TextView
android:id="@+id/code_first"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="22934" />
<TextView
android:id="@+id/code_second"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="56944" />
<TextView
android:id="@+id/code_third"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="42738" />
<TextView
android:id="@+id/code_fourth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="20038" />
</TableRow>
<TableRow android:gravity="center_horizontal">
<TextView
android:id="@+id/code_fifth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="34431" />
<TextView
android:id="@+id/code_sixth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="24922" />
<TextView
android:id="@+id/code_seventh"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="58594" />
<TextView
android:id="@+id/code_eighth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="24109" />
</TableRow>
<TableRow android:gravity="center_horizontal">
<TextView
android:id="@+id/code_ninth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="00257" />
<TextView
android:id="@+id/code_tenth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="34956" />
<TextView
android:id="@+id/code_eleventh"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="32440" />
<TextView
android:id="@+id/code_twelth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="15774" />
</TableRow>
</TableLayout>
<ImageView
android:id="@+id/share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/symbol_share_android_24"
android:background="?selectableItemBackgroundBorderless"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:layout_margin="24dp"
/>
</merge>

View file

@ -1,7 +1,6 @@
<?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"
tools:viewBindingIgnore="true"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
@ -22,12 +21,57 @@
app:titleTextAppearance="@style/Signal.Text.TitleLarge"
tools:title="@string/AndroidManifest__verify_safety_number" />
<ImageView
android:id="@+id/safety_number_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/safety_numbers_updating_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/safety_number_updating_banner_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/safety_number_image"
app:layout_constraintBottom_toBottomOf="@id/safety_number_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/safety_number_image"
android:layout_marginStart="16dp"
android:layout_marginEnd="30dp"
tools:text="Safety numbers are being updated. Learn more"/>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_marginTop="24dp"
app:layout_constraintTop_toBottomOf="@id/safety_number_image"
android:background="@color/signal_colorSurfaceVariant"/>
<androidx.constraintlayout.widget.Group
android:id="@+id/safety_number_change_banner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="divider,safety_number_updating_banner_text,safety_number_image"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/header_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="divider,toolbar"/>
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintTop_toBottomOf="@id/header_barrier"
app:layout_constraintBottom_toTopOf="@id/verify_button_container">
<LinearLayout
@ -35,206 +79,34 @@
android:layout_height="wrap_content"
android:background="@color/signal_background_primary"
android:gravity="center_horizontal"
android:orientation="vertical"
android:layout_marginStart="36dp"
android:layout_marginEnd="36dp">
android:orientation="vertical">
<LinearLayout
android:id="@+id/qr_code_container"
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/verify_view_pager"
android:layout_marginTop="40dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/dot_indicators"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/verify_identity_vertical_margin"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:background="@drawable/qr_code_background"
android:orientation="vertical">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="36dp">
<TextView
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/verify_display_fragment__loading"
android:textSize="20sp" />
<org.thoughtcrime.securesms.components.SquareImageView
android:id="@+id/qr_code"
android:layout_width="164dp"
android:layout_height="164dp"
android:layout_gravity="center_horizontal"
android:visibility="invisible"
tools:src="@drawable/ic_about_mc_80"
tools:visibility="visible" />
<org.thoughtcrime.securesms.components.SquareImageView
android:id="@+id/qr_verified"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/green_500"
android:src="@drawable/ic_check_white_48dp"
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
<TextSwitcher
android:id="@+id/tap_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="27dp"
android:inAnimation="@android:anim/fade_in"
android:outAnimation="@android:anim/fade_out">
<TextView
style="@style/Signal.Text.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/verify_display_fragment__tap_to_scan"
android:textColor="@color/qr_card_text_color" />
<TextView
style="@style/Signal.Text.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:text="@string/verify_display_fragment__successful_match"
android:textColor="@color/qr_card_text_color" />
</TextSwitcher>
</LinearLayout>
<TableLayout
android:id="@+id/number_table"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/verify_identity_vertical_margin"
android:clickable="true"
android:focusable="true">
<TableRow
android:clickable="false"
android:focusable="false"
android:gravity="center_horizontal">
<TextView
android:id="@+id/code_first"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="22934" />
<TextView
android:id="@+id/code_second"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="56944" />
<TextView
android:id="@+id/code_third"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="42738" />
<TextView
android:id="@+id/code_fourth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="20038" />
</TableRow>
<TableRow android:gravity="center_horizontal">
<TextView
android:id="@+id/code_fifth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="34431" />
<TextView
android:id="@+id/code_sixth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="24922" />
<TextView
android:id="@+id/code_seventh"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="58594" />
<TextView
android:id="@+id/code_eighth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="24109" />
</TableRow>
<TableRow android:gravity="center_horizontal">
<TextView
android:id="@+id/code_ninth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="00257" />
<TextView
android:id="@+id/code_tenth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="34956" />
<TextView
android:id="@+id/code_eleventh"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="32440" />
<TextView
android:id="@+id/code_twelth"
style="@style/IdentityKey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
tools:text="15774" />
</TableRow>
</TableLayout>
android:layout_marginTop="8dp"
app:tabBackground="@drawable/safety_dot_indicator"
app:tabGravity="center"
app:tabPaddingEnd="10dp"
app:tabPaddingStart="10dp"
app:tabIndicatorHeight="0dp"/>
<TextView
android:id="@+id/description"
style="@style/Signal.Text.BodyMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/verify_identity_vertical_margin"
android:layout_marginEnd="30dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="20dp"
android:gravity="center"
android:lineSpacingExtra="3sp"
android:text="@string/verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s"
@ -251,7 +123,7 @@
android:background="@drawable/toolbar_shadow"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintTop_toBottomOf="@id/header_barrier"
tools:visibility="visible" />
<View

File diff suppressed because one or more lines are too long

View file

@ -58,4 +58,7 @@
<color name="storage_color_files">#ffA23474</color>
<color name="conversation_list_archive_background_start">@color/signal_colorPrimary</color>
<color name="conversation_list_archive_background_end">@color/signal_colorPrimary</color>
<color name="safety_number_card_grey">#ffEBEAE8</color>
<color name="safety_number_card_blue">#ff506ECD</color>
</resources>

View file

@ -192,7 +192,7 @@
<dimen name="toolbar_avatar_margin">26dp</dimen>
<dimen name="conversation_list_avatar_size">48dp</dimen>
<dimen name="verify_identity_vertical_margin">16dp</dimen>
<dimen name="verify_identity_vertical_margin">26dp</dimen>
<dimen name="signal_context_menu_corner_radius">18dp</dimen>
@ -220,4 +220,7 @@
<dimen name="media_rail_item_size">46dp</dimen>
<dimen name="media_rail_thumbnail_size">44dp</dimen>
<dimen name="registrationactivity_text_view_padding">32dp</dimen>
<dimen name="safety_number_qr_peek">24dp</dimen>
<dimen name="safety_number_qr_width">288dp</dimen>
</resources>

View file

@ -2140,6 +2140,12 @@
<string name="VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied">Signal needs the Camera permission in order to scan a QR code, but it has been permanently denied. Please continue to app settings, select \"Permissions\", and enable \"Camera\".</string>
<string name="VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission">Unable to scan QR code without Camera permission</string>
<string name="VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view">You must first exchange messages in order to view %1$s\'s safety number.</string>
<!-- Dialog message explaining to user they must exchange messages first to create a safety number -->
<string name="VerifyIdentityActivity_dialog_exchange_messages_to_create_safety_number_message">A safety number will be created with this person after they exchange messages with you.</string>
<!-- Confirmation option for dialog explaining to user they must exchange messages first to create a safety number -->
<string name="VerifyIdentityActivity_dialog_exchange_messages_to_create_safety_number_ok">OK</string>
<!-- Learn more option for dialog explaining to user they must exchange messages first to create a safety number -->
<string name="VerifyIdentityActivity_dialog_exchange_messages_to_create_safety_number_learn_more">Learn more</string>
<!-- ViewOnceMessageActivity -->
<string name="ViewOnceMessageActivity_video_duration" translatable="false">%1$02d:%2$02d</string>
@ -2702,12 +2708,24 @@
<!-- verify_display_fragment -->
<string name="verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s"><![CDATA[To verify the security of your end-to-end encryption with %s, compare the numbers above with their device. You can also scan the code on their phone. <a href="https://signal.org/redirect/safety-numbers">Learn more.</a>]]></string>
<string name="verify_display_fragment__to_verify_the_security_of_your_end_to_end_encryption_with_s_pnp"><![CDATA[To verify end-to-end encryption with %s, compare the numbers above with their device. match the color card above with their device and compare the numbers. If these dont match, swipe and try the other pair of safety numbers. Only one pair needs to match. <a href="https://signal.org/redirect/safety-numbers">Learn more.</a>]]></string>
<string name="verify_display_fragment__tap_to_scan">Tap to scan</string>
<string name="verify_display_fragment__successful_match">Successful match</string>
<string name="verify_display_fragment__failed_to_verify_safety_number">Failed to verify safety number</string>
<string name="verify_display_fragment__loading">Loading…</string>
<string name="verify_display_fragment__mark_as_verified">Mark as verified</string>
<string name="verify_display_fragment__clear_verification">Clear verification</string>
<!-- Banner at top of safety numbers screen explaining that we're updating how safety numbers work. -->
<string name="verify_display_fragment__safety_numbers_are_updating_banner"><![CDATA[Safety numbers are being updated. <a href="https://signal.org/redirect/safety-numbers">Learn more.</a>]]></string>
<!-- Title for dialog explaining for how safety numbers are transitioning to support usernames -->
<string name="PnpSafetyNumberEducationDialog__title">Changes to safety numbers</string>
<!-- Message for dialog explaining for how safety numbers are transitioning to support usernames -->
<string name="PnpSafetyNumberEducationDialog__body">Safety numbers are being updated over a transition period to enable upcoming privacy features in Signal.\n\nTo verify safety numbers, match the color card with your contacts device. If these dont match, swipe and try the other pair of safety numbers. Only one pair needs to match.</string>
<!-- Button for more information about the safety number changes. Takes user to support article -->
<string name="PnpSafetyNumberEducationDialog__help">Need help?</string>
<!-- Confirmation button for educating users about new safety number changes -->
<string name="PnpSafetyNumberEducationDialog__confirm">Got it</string>
<!-- verify_identity -->
<string name="verify_identity__share_safety_number">Share safety number</string>

View file

@ -13,6 +13,15 @@ fun <T : Parcelable> Bundle.getParcelableCompat(key: String, clazz: Class<T>): T
}
}
fun <T : Parcelable> Bundle.requireParcelableCompat(key: String, clazz: Class<T>): T {
return if (Build.VERSION.SDK_INT >= 33) {
this.getParcelable(key, clazz)!!
} else {
@Suppress("DEPRECATION")
this.getParcelable(key)!!
}
}
fun <T : Parcelable> Bundle.getParcelableArrayListCompat(key: String, clazz: Class<T>): ArrayList<T>? {
return if (Build.VERSION.SDK_INT >= 33) {
this.getParcelableArrayList(key, clazz)