Display crypto status in message details bottom sheet
This commit is contained in:
parent
cbf3ef985d
commit
80c68e1e3e
8 changed files with 254 additions and 1 deletions
|
@ -0,0 +1,52 @@
|
|||
package com.fsck.k9.ui.messagedetails
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.resolveColorAttribute
|
||||
import com.mikepenz.fastadapter.FastAdapter
|
||||
import com.mikepenz.fastadapter.items.AbstractItem
|
||||
|
||||
internal class CryptoStatusItem(val cryptoDetails: CryptoDetails) : AbstractItem<CryptoStatusItem.ViewHolder>() {
|
||||
override val type = R.id.message_details_crypto_status
|
||||
override val layoutRes = R.layout.message_details_crypto_status_item
|
||||
|
||||
override fun getViewHolder(v: View) = ViewHolder(v)
|
||||
|
||||
class ViewHolder(view: View) : FastAdapter.ViewHolder<CryptoStatusItem>(view) {
|
||||
private val titleTextView = view.findViewById<TextView>(R.id.crypto_status_title)
|
||||
private val descriptionTextView = view.findViewById<TextView>(R.id.crypto_status_description)
|
||||
private val imageView = view.findViewById<ImageView>(R.id.crypto_status_icon)
|
||||
private val originalBackground = view.background
|
||||
|
||||
override fun bindView(item: CryptoStatusItem, payloads: List<Any>) {
|
||||
val context = itemView.context
|
||||
val cryptoDetails = item.cryptoDetails
|
||||
val cryptoStatus = cryptoDetails.cryptoStatus
|
||||
|
||||
imageView.setImageResource(cryptoStatus.statusIconRes)
|
||||
val tintColor = context.theme.resolveColorAttribute(cryptoStatus.colorAttr)
|
||||
imageView.imageTintList = ColorStateList.valueOf(tintColor)
|
||||
|
||||
cryptoStatus.titleTextRes?.let { stringResId ->
|
||||
titleTextView.text = context.getString(stringResId)
|
||||
}
|
||||
cryptoStatus.descriptionTextRes?.let { stringResId ->
|
||||
descriptionTextView.text = context.getString(stringResId)
|
||||
}
|
||||
|
||||
if (!cryptoDetails.isClickable) {
|
||||
itemView.background = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbindView(item: CryptoStatusItem) {
|
||||
imageView.setImageDrawable(null)
|
||||
titleTextView.text = null
|
||||
descriptionTextView.text = null
|
||||
itemView.background = originalBackground
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package com.fsck.k9.ui.messagedetails
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
@ -11,13 +12,16 @@ import android.widget.ProgressBar
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.k9mail.ui.utils.bottomsheet.ToolbarBottomSheetDialogFragment
|
||||
import com.fsck.k9.activity.MessageCompose
|
||||
import com.fsck.k9.contacts.ContactPictureLoader
|
||||
import com.fsck.k9.controller.MessageReference
|
||||
import com.fsck.k9.mail.Address
|
||||
import com.fsck.k9.mailstore.CryptoResultAnnotation
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.ui.observe
|
||||
import com.fsck.k9.ui.withArguments
|
||||
|
@ -36,6 +40,9 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
|
||||
private lateinit var messageReference: MessageReference
|
||||
|
||||
// FIXME: Replace this with a mechanism that survives process death
|
||||
var cryptoResult: CryptoResultAnnotation? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -54,6 +61,10 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
cryptoResult?.let {
|
||||
viewModel.cryptoResult = it
|
||||
}
|
||||
|
||||
val dialog = checkNotNull(dialog)
|
||||
dialog.isDismissWithAnimation = true
|
||||
|
||||
|
@ -71,6 +82,14 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
val errorView = view.findViewById<View>(R.id.message_details_error)
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.message_details_list)
|
||||
|
||||
viewModel.uiEvents.observe(this) { event ->
|
||||
when (event) {
|
||||
is MessageDetailEvent.ShowCryptoKeys -> showCryptoKeys(event.pendingIntent)
|
||||
MessageDetailEvent.SearchCryptoKeys -> searchCryptoKeys()
|
||||
MessageDetailEvent.ShowCryptoWarning -> showCryptoWarning()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.loadData(messageReference).observe(this) { state ->
|
||||
when (state) {
|
||||
MessageDetailsState.Loading -> {
|
||||
|
@ -97,6 +116,10 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
val itemAdapter = ItemAdapter<GenericItem>().apply {
|
||||
add(MessageDateItem(details.date ?: getString(R.string.message_details_missing_date)))
|
||||
|
||||
if (details.cryptoDetails != null) {
|
||||
add(CryptoStatusItem(details.cryptoDetails))
|
||||
}
|
||||
|
||||
addParticipants(details.from, R.string.message_details_from_section_title, showContactPicture)
|
||||
addParticipants(details.sender, R.string.message_details_sender_section_title, showContactPicture)
|
||||
addParticipants(details.replyTo, R.string.message_details_replyto_section_title, showContactPicture)
|
||||
|
@ -109,6 +132,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
val adapter = FastAdapter.with(itemAdapter).apply {
|
||||
addEventHook(cryptoStatusClickEventHook)
|
||||
addEventHook(participantClickEventHook)
|
||||
addEventHook(addToContactsClickEventHook)
|
||||
addEventHook(composeClickEventHook)
|
||||
|
@ -133,6 +157,27 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private val cryptoStatusClickEventHook = object : ClickEventHook<CryptoStatusItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
|
||||
return if (viewHolder is CryptoStatusItem.ViewHolder) {
|
||||
viewHolder.itemView
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(
|
||||
v: View,
|
||||
position: Int,
|
||||
fastAdapter: FastAdapter<CryptoStatusItem>,
|
||||
item: CryptoStatusItem
|
||||
) {
|
||||
if (item.cryptoDetails.isClickable) {
|
||||
viewModel.onCryptoStatusClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val participantClickEventHook = object : ClickEventHook<ParticipantItem>() {
|
||||
override fun onBind(viewHolder: RecyclerView.ViewHolder): View? {
|
||||
return if (viewHolder is ParticipantItem.ViewHolder) {
|
||||
|
@ -237,9 +282,28 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showCryptoKeys(pendingIntent: PendingIntent) {
|
||||
requireActivity().startIntentSender(pendingIntent.intentSender, null, 0, 0, 0)
|
||||
}
|
||||
|
||||
private fun searchCryptoKeys() {
|
||||
setFragmentResult(FRAGMENT_RESULT_KEY, bundleOf(RESULT_ACTION to ACTION_SEARCH_KEYS))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun showCryptoWarning() {
|
||||
setFragmentResult(FRAGMENT_RESULT_KEY, bundleOf(RESULT_ACTION to ACTION_SHOW_WARNING))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_REFERENCE = "reference"
|
||||
|
||||
const val FRAGMENT_RESULT_KEY = "messageDetailsResult"
|
||||
const val RESULT_ACTION = "action"
|
||||
const val ACTION_SEARCH_KEYS = "search_keys"
|
||||
const val ACTION_SHOW_WARNING = "show_warning"
|
||||
|
||||
fun create(messageReference: MessageReference): MessageDetailsFragment {
|
||||
return MessageDetailsFragment().withArguments(
|
||||
ARG_REFERENCE to messageReference.toIdentityString()
|
||||
|
|
|
@ -2,9 +2,11 @@ package com.fsck.k9.ui.messagedetails
|
|||
|
||||
import android.net.Uri
|
||||
import com.fsck.k9.mail.Address
|
||||
import com.fsck.k9.view.MessageCryptoDisplayStatus
|
||||
|
||||
data class MessageDetailsUi(
|
||||
val date: String?,
|
||||
val cryptoDetails: CryptoDetails?,
|
||||
val from: List<Participant>,
|
||||
val sender: List<Participant>,
|
||||
val replyTo: List<Participant>,
|
||||
|
@ -13,6 +15,11 @@ data class MessageDetailsUi(
|
|||
val bcc: List<Participant>
|
||||
)
|
||||
|
||||
data class CryptoDetails(
|
||||
val cryptoStatus: MessageCryptoDisplayStatus,
|
||||
val isClickable: Boolean
|
||||
)
|
||||
|
||||
data class Participant(
|
||||
val address: Address,
|
||||
val contactLookupUri: Uri?
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.fsck.k9.ui.messagedetails
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.res.Resources
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -7,14 +8,18 @@ import com.fsck.k9.controller.MessageReference
|
|||
import com.fsck.k9.helper.ClipboardManager
|
||||
import com.fsck.k9.helper.Contacts
|
||||
import com.fsck.k9.mail.Address
|
||||
import com.fsck.k9.mailstore.CryptoResultAnnotation
|
||||
import com.fsck.k9.mailstore.MessageDate
|
||||
import com.fsck.k9.mailstore.MessageRepository
|
||||
import com.fsck.k9.ui.R
|
||||
import com.fsck.k9.view.MessageCryptoDisplayStatus
|
||||
import java.text.DateFormat
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class MessageDetailsViewModel(
|
||||
|
@ -26,6 +31,10 @@ internal class MessageDetailsViewModel(
|
|||
) : ViewModel() {
|
||||
private val dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.getDefault())
|
||||
private val uiState = MutableStateFlow<MessageDetailsState>(MessageDetailsState.Loading)
|
||||
private val eventChannel = Channel<MessageDetailEvent>()
|
||||
|
||||
val uiEvents = eventChannel.receiveAsFlow()
|
||||
var cryptoResult: CryptoResultAnnotation? = null
|
||||
|
||||
fun loadData(messageReference: MessageReference): StateFlow<MessageDetailsState> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
@ -35,6 +44,7 @@ internal class MessageDetailsViewModel(
|
|||
val senderList = messageDetails.sender?.let { listOf(it) } ?: emptyList()
|
||||
val messageDetailsUi = MessageDetailsUi(
|
||||
date = buildDisplayDate(messageDetails.date),
|
||||
cryptoDetails = cryptoResult?.toCryptoDetails(),
|
||||
from = messageDetails.from.toParticipants(),
|
||||
sender = senderList.toParticipants(),
|
||||
replyTo = messageDetails.replyTo.toParticipants(),
|
||||
|
@ -63,6 +73,15 @@ internal class MessageDetailsViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CryptoResultAnnotation.toCryptoDetails(): CryptoDetails {
|
||||
val messageCryptoDisplayStatus = MessageCryptoDisplayStatus.fromResultAnnotation(this)
|
||||
return CryptoDetails(
|
||||
cryptoStatus = messageCryptoDisplayStatus,
|
||||
isClickable = messageCryptoDisplayStatus.hasAssociatedKey() || messageCryptoDisplayStatus.isUnknownKey ||
|
||||
hasOpenPgpInsecureWarningPendingIntent()
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<Address>.toParticipants(): List<Participant> {
|
||||
return this.map { address ->
|
||||
Participant(
|
||||
|
@ -72,6 +91,28 @@ internal class MessageDetailsViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onCryptoStatusClicked() {
|
||||
val cryptoResult = cryptoResult ?: return
|
||||
val cryptoStatus = MessageCryptoDisplayStatus.fromResultAnnotation(cryptoResult)
|
||||
|
||||
if (cryptoStatus.hasAssociatedKey()) {
|
||||
val pendingIntent = cryptoResult.openPgpSigningKeyIntentIfAny
|
||||
if (pendingIntent != null) {
|
||||
viewModelScope.launch {
|
||||
eventChannel.send(MessageDetailEvent.ShowCryptoKeys(pendingIntent))
|
||||
}
|
||||
}
|
||||
} else if (cryptoStatus.isUnknownKey) {
|
||||
viewModelScope.launch {
|
||||
eventChannel.send(MessageDetailEvent.SearchCryptoKeys)
|
||||
}
|
||||
} else if (cryptoResult.hasOpenPgpInsecureWarningPendingIntent()) {
|
||||
viewModelScope.launch {
|
||||
eventChannel.send(MessageDetailEvent.ShowCryptoWarning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCopyEmailAddressToClipboard(participant: Participant) {
|
||||
val label = resources.getString(R.string.clipboard_label_email_address)
|
||||
val emailAddress = participant.address.address
|
||||
|
@ -93,3 +134,9 @@ sealed interface MessageDetailsState {
|
|||
val details: MessageDetailsUi
|
||||
) : MessageDetailsState
|
||||
}
|
||||
|
||||
sealed interface MessageDetailEvent {
|
||||
data class ShowCryptoKeys(val pendingIntent: PendingIntent) : MessageDetailEvent
|
||||
object SearchCryptoKeys : MessageDetailEvent
|
||||
object ShowCryptoWarning : MessageDetailEvent
|
||||
}
|
||||
|
|
|
@ -38,6 +38,10 @@ public class MessageCryptoPresenter implements OnCryptoClickListener {
|
|||
this.messageCryptoMvpView = messageCryptoMvpView;
|
||||
}
|
||||
|
||||
public CryptoResultAnnotation getCryptoResultAnnotation() {
|
||||
return cryptoResultAnnotation;
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
if (reloadOnResumeWithoutRecreateFlag) {
|
||||
reloadOnResumeWithoutRecreateFlag = false;
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.core.app.ActivityCompat
|
|||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import com.fsck.k9.Account
|
||||
import com.fsck.k9.K9
|
||||
import com.fsck.k9.activity.MessageCompose
|
||||
|
@ -134,6 +135,8 @@ class MessageViewFragment :
|
|||
fragmentManager = parentFragmentManager,
|
||||
callback = messageLoaderCallbacks
|
||||
)
|
||||
|
||||
setFragmentResultListener(MessageDetailsFragment.FRAGMENT_RESULT_KEY, ::onMessageDetailsResult)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -384,7 +387,8 @@ class MessageViewFragment :
|
|||
private val messageHeaderClickListener = object : MessageHeaderClickListener {
|
||||
override fun onParticipantsContainerClick() {
|
||||
val messageDetailsFragment = MessageDetailsFragment.create(messageReference)
|
||||
messageDetailsFragment.show(childFragmentManager, "message_details")
|
||||
messageDetailsFragment.cryptoResult = messageCryptoPresenter.cryptoResultAnnotation
|
||||
messageDetailsFragment.show(parentFragmentManager, "message_details")
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(itemId: Int) {
|
||||
|
@ -578,6 +582,20 @@ class MessageViewFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun onMessageDetailsResult(requestKey: String, result: Bundle) {
|
||||
when (val action = result.getString(MessageDetailsFragment.RESULT_ACTION)) {
|
||||
MessageDetailsFragment.ACTION_SEARCH_KEYS -> {
|
||||
onClickSearchKey()
|
||||
}
|
||||
MessageDetailsFragment.ACTION_SHOW_WARNING -> {
|
||||
onClickShowSecurityWarning()
|
||||
}
|
||||
else -> {
|
||||
error("Unsupported action: $action")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateDocumentResult(data: Intent?) {
|
||||
if (data != null && data.data != null) {
|
||||
createAttachmentController(currentAttachmentViewInfo).saveAttachmentTo(data.data)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/keyline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="72dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/crypto_status_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@null"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/keyline"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@drawable/status_lock_dots_3"
|
||||
tools:tint="?attr/openpgp_green" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/crypto_status_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
app:layout_constraintBottom_toTopOf="@+id/crypto_status_description"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/keyline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="@string/crypto_msg_title_encrypted_signed_e2e" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/crypto_status_description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/keyline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/crypto_status_title"
|
||||
tools:text="@string/crypto_msg_encrypted_sign_verified" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item type="id" name="message_details_date"/>
|
||||
<item type="id" name="message_details_crypto_status"/>
|
||||
<item type="id" name="message_details_section_header"/>
|
||||
<item type="id" name="message_details_participant"/>
|
||||
<item type="id" name="message_details_divider"/>
|
||||
|
|
Loading…
Reference in a new issue