Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/CryptoStatusItem.kt 0 → 100644 +52 −0 Original line number Diff line number Diff line 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 } } } app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsFragment.kt +64 −0 Original line number Diff line number Diff line package com.fsck.k9.ui.messagedetails import android.app.PendingIntent import android.content.Intent import android.net.Uri import android.os.Bundle Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading @@ -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 -> { 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) Loading @@ -109,6 +132,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() { } val adapter = FastAdapter.with(itemAdapter).apply { addEventHook(cryptoStatusClickEventHook) addEventHook(participantClickEventHook) addEventHook(addToContactsClickEventHook) addEventHook(composeClickEventHook) Loading @@ -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) { Loading Loading @@ -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() Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsUi.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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>, Loading @@ -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? Loading app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt +47 −0 Original line number Diff line number Diff line package com.fsck.k9.ui.messagedetails import android.app.PendingIntent import android.content.res.Resources import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope Loading @@ -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( Loading @@ -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) { Loading @@ -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(), Loading Loading @@ -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( Loading @@ -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 Loading @@ -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 } app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/CryptoInfoDialog.javadeleted 100644 → 0 +0 −140 Original line number Diff line number Diff line package com.fsck.k9.ui.messageview; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.fsck.k9.ui.R; import com.fsck.k9.view.MessageCryptoDisplayStatus; import com.fsck.k9.view.ThemeUtils; public class CryptoInfoDialog extends DialogFragment { public static final String ARG_DISPLAY_STATUS = "display_status"; public static final String ARG_HAS_SECURITY_WARNING = "has_security_warning"; private ImageView statusIcon; private TextView titleText; private TextView descriptionText; public static CryptoInfoDialog newInstance(MessageCryptoDisplayStatus displayStatus, boolean hasSecurityWarning) { CryptoInfoDialog frag = new CryptoInfoDialog(); Bundle args = new Bundle(); args.putString(ARG_DISPLAY_STATUS, displayStatus.toString()); args.putBoolean(ARG_HAS_SECURITY_WARNING, hasSecurityWarning); frag.setArguments(args); return frag; } @SuppressLint("InflateParams") // inflating without root element is fine for creating a dialog @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Builder b = new AlertDialog.Builder(getActivity()); View dialogView = LayoutInflater.from(getActivity()).inflate(R.layout.message_crypto_info_dialog, null); statusIcon = dialogView.findViewById(R.id.crypto_info_top_icon_1); titleText = dialogView.findViewById(R.id.crypto_info_title); descriptionText = dialogView.findViewById(R.id.crypto_info_text); MessageCryptoDisplayStatus displayStatus = MessageCryptoDisplayStatus.valueOf(getArguments().getString(ARG_DISPLAY_STATUS)); setMessageForDisplayStatus(displayStatus); b.setView(dialogView); b.setPositiveButton(R.string.crypto_info_ok, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dismiss(); } }); boolean hasSecurityWarning = getArguments().getBoolean(ARG_HAS_SECURITY_WARNING); if (hasSecurityWarning) { b.setNeutralButton(R.string.crypto_info_view_security_warning, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Fragment frag = getTargetFragment(); if (!(frag instanceof OnClickShowCryptoKeyListener)) { throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!"); } ((OnClickShowCryptoKeyListener) frag).onClickShowSecurityWarning(); } }); } else if (displayStatus.isUnknownKey()) { b.setNeutralButton(R.string.crypto_info_search_key, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Fragment frag = getTargetFragment(); if (! (frag instanceof OnClickShowCryptoKeyListener)) { throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!"); } ((OnClickShowCryptoKeyListener) frag).onClickSearchKey(); } }); } else if (displayStatus.hasAssociatedKey()) { int buttonLabel = displayStatus.isUnencryptedSigned() ? R.string.crypto_info_view_signer : R.string.crypto_info_view_sender; b.setNeutralButton(buttonLabel, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Fragment frag = getTargetFragment(); if (! (frag instanceof OnClickShowCryptoKeyListener)) { throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!"); } ((OnClickShowCryptoKeyListener) frag).onClickShowCryptoKey(); } }); } return b.create(); } private void setMessageForDisplayStatus(MessageCryptoDisplayStatus displayStatus) { if (displayStatus.getTitleTextRes() == null) { throw new AssertionError("Crypto info dialog can only be displayed for items with text!"); } setMessageSingleLine(displayStatus.getColorAttr(), displayStatus.getTitleTextRes(), displayStatus.getDescriptionTextRes(), displayStatus.getStatusIconRes()); } private void setMessageSingleLine(@AttrRes int colorAttr, @StringRes int titleTextRes, @StringRes Integer descTextRes, @DrawableRes int statusIconRes) { @ColorInt int color = ThemeUtils.getStyledColor(getActivity(), colorAttr); statusIcon.setImageResource(statusIconRes); statusIcon.setColorFilter(color); titleText.setText(titleTextRes); if (descTextRes != null) { descriptionText.setText(descTextRes); descriptionText.setVisibility(View.VISIBLE); } else { descriptionText.setVisibility(View.GONE); } } public interface OnClickShowCryptoKeyListener { void onClickShowCryptoKey(); void onClickShowSecurityWarning(); void onClickSearchKey(); } } Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/CryptoStatusItem.kt 0 → 100644 +52 −0 Original line number Diff line number Diff line 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 } } }
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsFragment.kt +64 −0 Original line number Diff line number Diff line package com.fsck.k9.ui.messagedetails import android.app.PendingIntent import android.content.Intent import android.net.Uri import android.os.Bundle Loading @@ -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 Loading @@ -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) Loading @@ -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 Loading @@ -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 -> { 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) Loading @@ -109,6 +132,7 @@ class MessageDetailsFragment : ToolbarBottomSheetDialogFragment() { } val adapter = FastAdapter.with(itemAdapter).apply { addEventHook(cryptoStatusClickEventHook) addEventHook(participantClickEventHook) addEventHook(addToContactsClickEventHook) addEventHook(composeClickEventHook) Loading @@ -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) { Loading Loading @@ -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() Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsUi.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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>, Loading @@ -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? Loading
app/ui/legacy/src/main/java/com/fsck/k9/ui/messagedetails/MessageDetailsViewModel.kt +47 −0 Original line number Diff line number Diff line package com.fsck.k9.ui.messagedetails import android.app.PendingIntent import android.content.res.Resources import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope Loading @@ -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( Loading @@ -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) { Loading @@ -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(), Loading Loading @@ -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( Loading @@ -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 Loading @@ -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 }
app/ui/legacy/src/main/java/com/fsck/k9/ui/messageview/CryptoInfoDialog.javadeleted 100644 → 0 +0 −140 Original line number Diff line number Diff line package com.fsck.k9.ui.messageview; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Bundle; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.fsck.k9.ui.R; import com.fsck.k9.view.MessageCryptoDisplayStatus; import com.fsck.k9.view.ThemeUtils; public class CryptoInfoDialog extends DialogFragment { public static final String ARG_DISPLAY_STATUS = "display_status"; public static final String ARG_HAS_SECURITY_WARNING = "has_security_warning"; private ImageView statusIcon; private TextView titleText; private TextView descriptionText; public static CryptoInfoDialog newInstance(MessageCryptoDisplayStatus displayStatus, boolean hasSecurityWarning) { CryptoInfoDialog frag = new CryptoInfoDialog(); Bundle args = new Bundle(); args.putString(ARG_DISPLAY_STATUS, displayStatus.toString()); args.putBoolean(ARG_HAS_SECURITY_WARNING, hasSecurityWarning); frag.setArguments(args); return frag; } @SuppressLint("InflateParams") // inflating without root element is fine for creating a dialog @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Builder b = new AlertDialog.Builder(getActivity()); View dialogView = LayoutInflater.from(getActivity()).inflate(R.layout.message_crypto_info_dialog, null); statusIcon = dialogView.findViewById(R.id.crypto_info_top_icon_1); titleText = dialogView.findViewById(R.id.crypto_info_title); descriptionText = dialogView.findViewById(R.id.crypto_info_text); MessageCryptoDisplayStatus displayStatus = MessageCryptoDisplayStatus.valueOf(getArguments().getString(ARG_DISPLAY_STATUS)); setMessageForDisplayStatus(displayStatus); b.setView(dialogView); b.setPositiveButton(R.string.crypto_info_ok, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { dismiss(); } }); boolean hasSecurityWarning = getArguments().getBoolean(ARG_HAS_SECURITY_WARNING); if (hasSecurityWarning) { b.setNeutralButton(R.string.crypto_info_view_security_warning, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Fragment frag = getTargetFragment(); if (!(frag instanceof OnClickShowCryptoKeyListener)) { throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!"); } ((OnClickShowCryptoKeyListener) frag).onClickShowSecurityWarning(); } }); } else if (displayStatus.isUnknownKey()) { b.setNeutralButton(R.string.crypto_info_search_key, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Fragment frag = getTargetFragment(); if (! (frag instanceof OnClickShowCryptoKeyListener)) { throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!"); } ((OnClickShowCryptoKeyListener) frag).onClickSearchKey(); } }); } else if (displayStatus.hasAssociatedKey()) { int buttonLabel = displayStatus.isUnencryptedSigned() ? R.string.crypto_info_view_signer : R.string.crypto_info_view_sender; b.setNeutralButton(buttonLabel, new OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { Fragment frag = getTargetFragment(); if (! (frag instanceof OnClickShowCryptoKeyListener)) { throw new AssertionError("Displaying activity must implement OnClickShowCryptoKeyListener!"); } ((OnClickShowCryptoKeyListener) frag).onClickShowCryptoKey(); } }); } return b.create(); } private void setMessageForDisplayStatus(MessageCryptoDisplayStatus displayStatus) { if (displayStatus.getTitleTextRes() == null) { throw new AssertionError("Crypto info dialog can only be displayed for items with text!"); } setMessageSingleLine(displayStatus.getColorAttr(), displayStatus.getTitleTextRes(), displayStatus.getDescriptionTextRes(), displayStatus.getStatusIconRes()); } private void setMessageSingleLine(@AttrRes int colorAttr, @StringRes int titleTextRes, @StringRes Integer descTextRes, @DrawableRes int statusIconRes) { @ColorInt int color = ThemeUtils.getStyledColor(getActivity(), colorAttr); statusIcon.setImageResource(statusIconRes); statusIcon.setColorFilter(color); titleText.setText(titleTextRes); if (descTextRes != null) { descriptionText.setText(descTextRes); descriptionText.setVisibility(View.VISIBLE); } else { descriptionText.setVisibility(View.GONE); } } public interface OnClickShowCryptoKeyListener { void onClickShowCryptoKey(); void onClickShowSecurityWarning(); void onClickSearchKey(); } }