mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Add key transparency UI.
This commit is contained in:
committed by
Greyson Parrelli
parent
279f9578cc
commit
69f4c89f84
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ class VerifySafetyNumberViewModel(
|
||||
val recipient: LiveRecipient = Recipient.live(recipientId)
|
||||
|
||||
private val fingerprintLiveData = MutableLiveData<SafetyNumberFingerprint?>()
|
||||
private val automaticVerificationLiveData = MutableLiveData(AutomaticVerificationStatus.NONE)
|
||||
|
||||
init {
|
||||
initializeFingerprints()
|
||||
@@ -69,6 +70,10 @@ class VerifySafetyNumberViewModel(
|
||||
return fingerprintLiveData
|
||||
}
|
||||
|
||||
fun getAutomaticVerification(): LiveData<AutomaticVerificationStatus> {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user