Add support for displaying both ACI and e164 safety numbers.
This commit is contained in:
parent
00bbb6bc6e
commit
461875b0e4
29 changed files with 1633 additions and 848 deletions
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
12
app/src/main/res/drawable/safety_dot_indicator.xml
Normal file
12
app/src/main/res/drawable/safety_dot_indicator.xml
Normal 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>
|
168
app/src/main/res/drawable/safety_numbers_updating_image.xml
Normal file
168
app/src/main/res/drawable/safety_numbers_updating_image.xml
Normal 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>
|
16
app/src/main/res/drawable/selected_page_dot_indicator.xml
Normal file
16
app/src/main/res/drawable/selected_page_dot_indicator.xml
Normal 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>
|
16
app/src/main/res/drawable/unselected_page_dot_indicator.xml
Normal file
16
app/src/main/res/drawable/unselected_page_dot_indicator.xml
Normal 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>
|
|
@ -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>
|
17
app/src/main/res/layout/safety_number_qr_page_fragment.xml
Normal file
17
app/src/main/res/layout/safety_number_qr_page_fragment.xml
Normal 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>
|
227
app/src/main/res/layout/safety_number_qr_view.xml
Normal file
227
app/src/main/res/layout/safety_number_qr_view.xml
Normal 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>
|
|
@ -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
|
||||
|
|
1
app/src/main/res/raw/safety_number_onboarding.json
Normal file
1
app/src/main/res/raw/safety_number_onboarding.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 don’t 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 contact’s device. If these don’t 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>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue