Skip to content

Commit

Permalink
Merge pull request #6616 from thundernest/crypto_status
Browse files Browse the repository at this point in the history
Message View Redesign: Display crypto status
  • Loading branch information
cketti authored Feb 1, 2023
2 parents cbf3ef9 + 677ef15 commit 7779089
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 205 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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

Expand All @@ -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 -> {
Expand All @@ -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)
Expand All @@ -109,6 +132,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() {
}

val adapter = FastAdapter.with(itemAdapter).apply {
addEventHook(cryptoStatusClickEventHook)
addEventHook(participantClickEventHook)
addEventHook(addToContactsClickEventHook)
addEventHook(composeClickEventHook)
Expand All @@ -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) {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -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?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package com.fsck.k9.ui.messagedetails

import android.app.PendingIntent
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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(
Expand All @@ -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) {
Expand All @@ -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(),
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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
}
Loading

0 comments on commit 7779089

Please sign in to comment.