diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/verify/SafetyNumberQrView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/verify/SafetyNumberQrView.kt index afb49e3bea..af91a2110f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/verify/SafetyNumberQrView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/verify/SafetyNumberQrView.kt @@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.widget.ImageViewCompat import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import com.google.android.material.button.MaterialButton import org.signal.core.util.dp import org.signal.libsignal.protocol.fingerprint.Fingerprint import org.thoughtcrime.securesms.R @@ -65,6 +66,7 @@ class SafetyNumberQrView : ConstraintLayout { val qrCodeContainer: View val shareButton: ImageView + val verifyButton: MaterialButton private val loading: View private val qrCode: ImageView @@ -97,6 +99,7 @@ class SafetyNumberQrView : ConstraintLayout { ) shareButton = findViewById(R.id.share) + verifyButton = findViewById(R.id.verify_button) outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/EncryptionVerifiedSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/EncryptionVerifiedSheet.kt new file mode 100644 index 0000000000..cf6e965809 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/EncryptionVerifiedSheet.kt @@ -0,0 +1,135 @@ +package org.thoughtcrime.securesms.verify + +import android.os.Bundle +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentManager +import org.signal.core.ui.compose.BottomSheets +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.horizontalGutters +import org.signal.core.util.getSerializableCompat +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment +import org.thoughtcrime.securesms.util.BottomSheetUtil + +/** + * Bottom sheet info explaining the results of automatic key verification + */ +class EncryptionVerifiedSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 0.67f + + companion object { + + private const val ARG_STATUS = "arg.status" + private const val ARG_NAME = "arg.name" + + @JvmStatic + fun show(fragmentManager: FragmentManager, status: AutomaticVerificationStatus, name: String) { + EncryptionVerifiedSheet().apply { + arguments = Bundle().apply { + putSerializable(ARG_STATUS, status) + putString(ARG_NAME, name) + } + show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + } + + @Composable + override fun SheetContent() { + VerifiedSheet( + verifiedStatus = requireArguments().getSerializableCompat(ARG_STATUS, AutomaticVerificationStatus::class.java)!!, + name = requireArguments().getString(ARG_NAME, "") + ) { + this.dismissAllowingStateLoss() + } + } +} + +@Composable +fun VerifiedSheet( + verifiedStatus: AutomaticVerificationStatus = AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY, + name: String = "", + onClick: () -> Unit = {} +) { + val (icon, title, body) = when (verifiedStatus) { + AutomaticVerificationStatus.VERIFIED -> { + Triple( + ImageVector.vectorResource(R.drawable.symbol_check_48), + stringResource(R.string.EncryptionVerifiedSheet__title_success), + stringResource(R.string.EncryptionVerifiedSheet__body_success) + ) + } + AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> { + Triple( + ImageVector.vectorResource(R.drawable.symbol_info_48), + stringResource(R.string.EncryptionVerifiedSheet__title_unavailable), + stringResource(R.string.EncryptionVerifiedSheet__body_unavailable) + ) + } + else -> { + Triple( + ImageVector.vectorResource(R.drawable.symbol_info_48), + stringResource(R.string.EncryptionVerifiedSheet__title_no_longer_unavailable), + stringResource(R.string.EncryptionVerifiedSheet__body_no_longer_unavailable, name) + ) + } + } + + return Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .horizontalGutters() + ) { + BottomSheets.Handle() + Icon( + imageVector = icon, + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.padding(top = 28.dp) + ) + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 12.dp) + ) + Text( + text = body, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + Buttons.LargeTonal( + onClick = onClick, + modifier = Modifier.defaultMinSize(minWidth = 220.dp).padding(vertical = 40.dp) + ) { + Text(stringResource(id = android.R.string.ok)) + } + } +} + +@DayNightPreviews +@Composable +fun FinishedSheetSheetPreview() { + Previews.BottomSheetContentPreview { + VerifiedSheet() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt index 98839945f7..5e3d01680c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt @@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.DialogInterface import android.content.Intent +import android.content.res.ColorStateList import android.os.Bundle import android.text.TextUtils import android.view.ContextMenu @@ -12,9 +13,9 @@ 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.core.content.ContextCompat +import androidx.core.widget.ImageViewCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -32,13 +33,14 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.ViewUtil +import org.thoughtcrime.securesms.util.visible import java.nio.charset.StandardCharsets import java.util.Locale /** * Fragment to display a user's identity key. */ -class VerifyDisplayFragment : Fragment(), OnScrollChangedListener { +class VerifyDisplayFragment : Fragment() { private lateinit var viewModel: VerifySafetyNumberViewModel private val binding by ViewBinderDelegate(VerifyDisplayFragmentBinding::bind) @@ -71,11 +73,19 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener { updateVerifyButton(requireArguments().getBoolean(VERIFIED_STATE, false), false) - binding.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) } - binding.scrollView.viewTreeObserver?.addOnScrollChangedListener(this) + binding.automaticVerification.visible = RemoteConfig.keyTransparency + binding.safetyQrView.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) } binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() } binding.toolbar.setTitle(R.string.AndroidManifest__verify_safety_number) + binding.caption.text = getString(R.string.verify_display_fragment__auto_verify_not_available) + binding.caption.setLink(getString(R.string.verify_display_fragment__link)) + binding.caption.setLinkColor(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary)) + + viewModel.getAutomaticVerification().observe(viewLifecycleOwner) { status -> + updateStatus(status) + } + viewModel.recipient.observe(this) { recipient: Recipient -> setRecipientText(recipient) } viewModel.getFingerprint().observe(viewLifecycleOwner) { fingerprint: SafetyNumberFingerprint? -> if (fingerprint == null) { @@ -99,6 +109,48 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener { } } + private fun updateStatus(status: AutomaticVerificationStatus) { + when (status) { + AutomaticVerificationStatus.NONE -> { + binding.autoVerifyText.text = getString(R.string.verify_display_fragment__verify_automatic) + binding.autoVerifyIcon.setImageResource(R.drawable.symbol_key_24) + binding.autoVerifyIcon.imageTintList = null + binding.autoVerifyMore.visible = false + } + AutomaticVerificationStatus.VERIFYING -> { + binding.autoVerifyText.text = getString(R.string.verify_display_fragment__verifying) + binding.autoVerifyMore.visible = false + } + AutomaticVerificationStatus.UNAVAILABLE_PERMANENT -> { + binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_unavailable) + binding.autoVerifyIcon.setImageResource(R.drawable.symbol_info_24) + ImageViewCompat.setImageTintList(binding.autoVerifyIcon, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))) + binding.autoVerifyMore.visible = true + } + AutomaticVerificationStatus.UNAVAILABLE_TEMPORARY -> { + binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_unavailable) + binding.autoVerifyIcon.setImageResource(R.drawable.symbol_info_24) + ImageViewCompat.setImageTintList(binding.autoVerifyIcon, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))) + binding.autoVerifyMore.visible = true + } + AutomaticVerificationStatus.VERIFIED -> { + binding.autoVerifyText.text = getString(R.string.verify_display_fragment__encryption_verified) + binding.autoVerifyIcon.setImageResource(R.drawable.symbol_check_filled_circle_24) + binding.autoVerifyIcon.imageTintList = null + binding.autoVerifyMore.visible = true + } + } + + if (status == AutomaticVerificationStatus.VERIFYING) { + binding.autoVerifySpinner.visible = true + binding.autoVerifyIcon.visible = false + } else { + binding.autoVerifySpinner.visible = false + binding.autoVerifyIcon.visible = true + } + binding.autoVerifyMore.setOnClickListener { EncryptionVerifiedSheet.show(parentFragmentManager, status, viewModel.recipient.resolve().getDisplayName(requireContext())) } + } + private fun initializeViewModel() { val recipientId = requireArguments().requireParcelableCompat(RECIPIENT_ID, RecipientId::class.java) val localIdentity = requireArguments().requireParcelableCompat(LOCAL_IDENTITY, IdentityKeyParcelable::class.java).get()!! @@ -121,7 +173,6 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener { animateFailure() } } - ThreadUtil.postToMain { onScrollChanged() } } override fun onCreateContextMenu( @@ -252,34 +303,15 @@ class VerifyDisplayFragment : Fragment(), OnScrollChangedListener { private fun updateVerifyButton(verified: Boolean, update: Boolean) { currentVerifiedState = verified if (verified) { - binding.verifyButton.setText(R.string.verify_display_fragment__clear_verification) + binding.safetyQrView.verifyButton.setText(R.string.verify_display_fragment__clear_verification) } else { - binding.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified) + binding.safetyQrView.verifyButton.setText(R.string.verify_display_fragment__mark_as_verified) } if (update) { viewModel.updateSafetyNumberVerification(verified) } } - override fun onScrollChanged() { - if (binding.scrollView.canScrollVertically(-1) && currentFingerprint != null) { - 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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt index 2a1be96a57..bfab7d7728 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifySafetyNumberViewModel.kt @@ -39,6 +39,7 @@ class VerifySafetyNumberViewModel( val recipient: LiveRecipient = Recipient.live(recipientId) private val fingerprintLiveData = MutableLiveData() + private val automaticVerificationLiveData = MutableLiveData(AutomaticVerificationStatus.NONE) init { initializeFingerprints() @@ -69,6 +70,10 @@ class VerifySafetyNumberViewModel( return fingerprintLiveData } + fun getAutomaticVerification(): LiveData { + return automaticVerificationLiveData + } + fun updateSafetyNumberVerification(verified: Boolean) { val recipientId: RecipientId = recipientId val context: Context = AppDependencies.application @@ -159,3 +164,11 @@ data class SafetyNumberFingerprint( return result } } + +enum class AutomaticVerificationStatus { + NONE, + VERIFYING, + UNAVAILABLE_PERMANENT, + UNAVAILABLE_TEMPORARY, + VERIFIED +} diff --git a/app/src/main/res/drawable/symbol_check_48.xml b/app/src/main/res/drawable/symbol_check_48.xml new file mode 100644 index 0000000000..c0c9a66813 --- /dev/null +++ b/app/src/main/res/drawable/symbol_check_48.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/symbol_check_filled_circle_24.xml b/app/src/main/res/drawable/symbol_check_filled_circle_24.xml new file mode 100644 index 0000000000..9421779b8d --- /dev/null +++ b/app/src/main/res/drawable/symbol_check_filled_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/symbol_info_48.xml b/app/src/main/res/drawable/symbol_info_48.xml new file mode 100644 index 0000000000..a8f33ec156 --- /dev/null +++ b/app/src/main/res/drawable/symbol_info_48.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/layout/safety_number_qr_view.xml b/app/src/main/res/layout/safety_number_qr_view.xml index 2a2d4f964d..67dd842757 100644 --- a/app/src/main/res/layout/safety_number_qr_view.xml +++ b/app/src/main/res/layout/safety_number_qr_view.xml @@ -105,7 +105,6 @@ android:layout_marginBottom="49dp" android:layout_marginHorizontal="24dp" 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" @@ -229,4 +228,23 @@ android:layout_margin="24dp" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/verify_display_fragment.xml b/app/src/main/res/layout/verify_display_fragment.xml index 23cb9b0d51..7e219ad8d6 100644 --- a/app/src/main/res/layout/verify_display_fragment.xml +++ b/app/src/main/res/layout/verify_display_fragment.xml @@ -21,21 +21,13 @@ app:titleTextAppearance="@style/Signal.Text.TitleLarge" tools:title="@string/AndroidManifest__verify_safety_number" /> - - - + app:layout_constraintTop_toBottomOf="@id/toolbar" + app:layout_constraintBottom_toBottomOf="parent"> + + + + + + + + + + + + + + + + + + + + + + - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 59829dad6d..82c04c4c08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3822,6 +3822,32 @@ Loading… Mark as verified Clear verification + + Automatic Key Verification + + Verify automatically + + Verifying encryption… + + Encryption verified + + Auto-verification unavailable + + Auto-verification is not available for all chats. + https://signal.org/redirect/safety-numbers + + + Encryption was auto-verified for this chat + + For contacts you’re connected to by phone number, Signal can automatically confirm whether the connection is secure using a process called key transparency. For added security, verify end-to-end encryption manually by comparing the numbers on the previous screen or scanning the code on their device. + + Auto-verification is no longer available for this chat + + Signal can no longer automatically verify the encryption for this chat. This is likely because %1$s changed their phone number. Verify end-to-end encryption manually by comparing the numbers on the previous screen or scanning the code on their device. + + Auto-verification not available for this chat + + Signal can only automatically verify the encryption in chats where you’re connected to someone via a phone number. If the chat was started with a username or a group in common, verify end-to-end encryption by comparing the numbers on the previous screen or scanning the code on their device. Signal now auto-verifies end-to-end encryption